zoukankan      html  css  js  c++  java
  • 08.进程控制

    创建新进程、执行程序和进程终止。还将说明进程属性的各种ID----实际、有效和保存的用户和组ID,以及它们如何受到进程控制原语的影响。

     

    1、进程标识符

    每个进程都有一个非负整型表示的唯一ID。

    系统中一些专用的进程:

    a、ID为0的进程通常是调度进程,常常被称为交换进程。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此被称为系统进程;

    b、ID为1的进程通常是init进程,在自举过程结束时由内核调用。init通常读与系统有关的初始化文件(/etc/rc*文件或/etc/inittab文件,以及/etc/init.d中的文件),并将系统引导到一个状态(例如多用户)。init进程不会终止。它是一个普通的用户进程(与交换进程不同,它不是内核中的系统进程),但是它以超级用户特权运行。init进程可以成为所有孤儿进程的父进程。

     

    2、fork函数

    一个现有进程可以调用fork函数创建一个新进程。

     

    fork函数被调用一次,但返回两次。

    两次返回的唯一区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。

    将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID。

    fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得其父进程的进程ID(ID为0的进程总是由内核交换进程使用,所以一个子进程的进程ID不可能为0)。

     

    子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父、子进程并不共享这些存储空间部分。父、子进程共享正文段。

    由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、栈和堆的完全复制。作为替代,使用了写时复制(Copy-On-Write,COW)技术。这些区域由父、子进程共享,而且内核将它们的访问权限改变为只读的。如果父、子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储器系统中的一“页”。

     

    下面例子可以看到,子进程对变量所作的改变并不影响父进程中该变量的值:

    	int glob = 6;
    	char buf[] = "a write to stdout
    ";
    	
    	int main()
    	{
    	     int var;
    	     pid_t pid;
    	     
    	     var = 88;
    	     
    	     if(write(STDOUT_FILENO,buf,sizeof(buf) - 1) != sizeof(buf) - 1)
    	         printf("write error!
    ");
    	     
    	     printf("before fork!
    ");
    	     
    	     if((pid = fork()) < 0) {
    	         printf("fork error!
    ");
    	     } else if(pid == 0) {
    	         glob++;
    	         var++;
    	     } else {
    	         sleep(2);
    	     }
    	     
    	     printf("pid = %d,glob = %d,var = %d
    ",getpid(),glob,var);
    	     
    	     exit(0);
    	}

    执行结果:

    	$ ./test
    	a write to stdout
    	before fork!
    	pid = 1487,glob = 7,var = 89
    	pid = 1486,glob = 6,var = 88
    	$ 
    	$ ./test > temp.out
    	$ cat temp.out
    	a write to stdout
    	before fork!
    	pid = 1491,glob = 7,var = 89
    	before fork!
    	pid = 1490,glob = 6,var = 88

     

    一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的。这取决于内核的调度算法。

    write函数是不带缓冲的。因为在fork之前调用write,所以其数据写到标准输出一次。但是,标准I/O库是带缓冲的。如果标准输出连到终端设备,则它是行缓冲的,否则它是全缓冲的。当以交互方式运行该程序时,只得到该printf输出的一次,其原因是标准输出缓冲区由换行符冲洗。但是当将标准输出重定向到一个文件时,却得到printf输出两次,其原因是,在fork之前调用了printf一次,但当调用fork时,该行数据仍在缓冲区中,然后再将父进程数据空间复制到子进程中时,该缓冲区也被复制到子进程中。于是那时父、子进程各自有了带该行内容的标准I/O缓冲区。在exit之前的第二个printf将其数据添加到现有的缓冲区中。当每个进程终止时,最终会冲洗其缓冲区的副本(全缓冲)。

    相当于:

    	before fork!(父)
    		before fork!(子,继承自父进程)
    		pid = %d,glob = %d,var = %d(子)
    		exit(0)(冲洗子进程缓冲区)
    	pid = %d,glob = %d,var = %d(父)
    	exit(0)(冲洗父进程缓冲区)

     

    文件共享

    在重定向父进程的标准输出时,子进程的标准输出也被重定向。实际上,fork的一个特性是父进程的所有打开文件描述符(注意,不是流FILE)都被复制到子进程中。父、子进程的每个相同的打开描述符共享一个文件表项。

    考虑下述情况,一个进程具有三个不同的打开文件,它们是标准输入、标准输出和标准出错。

    这种共享文件表项的方式使父、子进程对同一文件使用了一个文件偏移量。考虑下列情况:一个进程fork了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父、子进程都向标准输出进行写操作。如果父进程的标准输出已重定向(很可能由shell实现),那么子进程写到该标准输出时,它将更新与父进程共享的该文件的偏移量。我们所考虑的例子中,当父进程等待子进程时,子进程写到标准输出;而在子进程终止后,父进程也写到标准输出上,并且知道其输出会添加在子进程所写数据之后。

    如果父、子进程不共享同一文件偏移量,这种形式的交互很难实现。如果父、子进程写到同一描述符文件,但又没有任何形式的同步(例如,使父进程等待子进程),那么它们的输出就会相互混合(假定所用的描述符是在fork之前打开的)。

    如下图所示:

    图片1

     

    在fork之后处理文件描述符有两种常见的情况:

    a、父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已执行了相应更新;

    b、父、子进程各自执行不同的程序段。这种情况下,在fork之后,父、子进程各自关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程中经常使用的。

     

    fork失败的两个主要原因是:

    a、系统中已经有了太多的进程;

    b、该实际用户ID的进程总数超过了系统限制。

     

    fork下面两种用法:

    a、一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的---父进程等待客户端的服务请求。在这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一服务请求到达;

    b、一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。

     

    3、vfork函数

    vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序。vfork与fork一样都创建一个子进程,但是它并不将父进程的地址空间复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会存访该地址空间。相反,在子进程调用exec或exit之前,它在父进程的空间中运行。

    vfork和fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。

    例子:

    	int glob = 6;
    	
    	int main()
    	{
    		 int var;
    	     pid_t pid;
    	 
    	     var = 88;
    	 
    	     printf("before fork!
    ");
    	 
    	     if((pid = vfork()) < 0) {
    	         printf("fork error!
    ");
    	     } else if(pid == 0) {
    	         glob++;
    	         var++;
    	         _exit(0);
    	     }
    	 
    	     printf("pid = %d,glob = %d,var = %d
    ",getpid(),glob,var);
    	 
    	     exit(0);
    	 }

    结果为:

    	$ ./test
    	before fork!
    	pid = 1554,glob = 7,var = 89
    	$ 

     

    子进程对变量glob和var做增1操作,结果改变了父进程中的变量值,因为子进程在父进程中的地址空间运行。

    _exit并不执行标准I/O操作的冲洗操作。如果调用的是exit而不是_exit,则该程序的输出是不确定的。

    如果调用的是exit,而该实现冲洗所有标准IO流,那么我们会见到输出与子进程_exit所产生的输出相同(注意,printf函数在这里是行缓冲的),没有任何区别。如果该实现也关闭标准IO流,那么表示标准输出FILE对象的相关存储区将被清0.因为子进程借用了父进程的地址空间,所以当父进程恢复运行并调用printf时,也就不会产生任何输出,printf返回-1.注意,父进程的STDOUT_FILENO仍有效,子进程得到的是父进程的文件描述符数组的副本。

     

    4、exit函数

    进程有5种正常终止方式:

    a、在main函数内执行return语句。等效于调用exit;

    b、调用exit函数。其操作包括调用各终止处理程序(终止处理程序在调用atexit函数时登记),然后关闭所有标准I/O流等;

    c、调用_exit或_Exit函数。其目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法;

    d、进程的最后一个线程在其启动例程中执行返回语句。但是,该线程的返回值不会用作进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回;

    e、进程的最后一个线程调用pthread_exit函数。在这种情况中,进程终止状态是0,这与传送给pthread_exit的参数无关。

     

    三种异常终止方法如下:

    a、调用abort。它产生SIGABRT信号;

    b、当进程接收到某些信号时。信号可由进程自身(例如调用abort函数)、其他进程或内核产生。例如,若进程越出其他地址空间访问存储单元或者除以0,内核就会为该进程产生相应的信号;

    c、最后一个线程对“取消”请求做出响应。按系统默认,“取消”以延迟方式发生:一个线程要求取消另一个线程,一段时间之后,目标线程终止。

    不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。

     

    对上述任意一种终止情况,我们都希望终止进程能够通知其父进程它是如何终止的。对于三种终止函数(exit、_exit和_Exit),实现这一点的方法是,将其退出状态(exit status)作为参数传送给函数。在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。

    注意,这里使用了“退出状态”(传向exit或_exit函数的参数,或main的返回值)和“终止状态”两个术语。在最后调用_exit时,内核将“退出状态”转换为“终止状态”。

    注意,exit函数没有直接返回内核,而_exit和_Exit函数则直接返回内核,所以才说,在最后调用_exit时,内核将“退出状态”转换为“终止状态”。

     

    子进程在父进程调用fork之后生成,子进程将其终止状态返回给父进程。

    如果父进程在子进程之前终止,那么所有子进程的父进程都改变为init进程(父进程ID改为1)。我们称这些进程由init进程领养的。

    如果子进程在父进程之前终止,那么父进程如何得到子进程的终止状态?回答是:内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。在UNIX术语中,一个已经终止、但是其父进程尚未对其进行善后处理(执行wait来获取终止子进程的有关信息,释放它仍占用的资源)的进程被称为“僵死进程”(zombie)。

    (In the case of a terminated child, performing a wait allows the system to release the resources associated with the child; if a wait is not performed, then the terminated child remains  in  a  "zombie"state)

    如果编写一个长期运行的程序,它调用fork产生了很多子进程,那么除非父进程等待取得子进程的终止状态,否则这些子进程终止后就会变成僵死进程。

    一个由init进程领养的进程终止时会发生什么?它会不会变成一个僵死进程?答案是:否。因为init被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数取得其终止状态,这样也就防止了系统中有很多僵死进程。当提及“一个init的子进程”时,这指的是可能是init直接产生的进程,也可能是其父进程已终止,由init领养的进程。

     

    5、wait和waitpid函数

    当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件(这可以在父进程运行的任何时候产生),所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。

    调用wait或waitpid函数的进程可能会发生什么?

    a、如果其所有子进程都还在运行,则阻塞;

    b、如果一个子进程已终止,正等待父进程获取其终止状态(僵死进程),则取得该子进程的终止状态立即返回;

    c、如果它没有任何子进程,则立即出错返回;

    如果进程由于接收到SIGCHLD信号(在信号处理函数中而调用wait),则可期望wait会立即返回。但是如果在任意时刻调用wait,则进程可能会阻塞。

     

    wait和waitpid函数的区别如下:

    a、在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞;

    b、waitpid并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程;

    c、waitpid支持作业控制。

     

    wait() and waitpid()

    The wait() system call suspends execution of the calling process until one of its children terminates.The call wait(&status) is equivalent to :

    waitpid(-1,&status,0);

    The waitpid() system call suspends execution of the calling process until a child specified by pid argument has changed state.By default,waitpid() waits only for terminated children,but this behavior is modifiable via the options argument,as described below.

    The value of pid can be:

    < –1      meaning wait for any child process group ID is equal to the absolute value of pid.

    -1         meaning wait for any child process.

    0          meaning wait for any child process whose process group ID is equal to that of the calling process.

    > 0        meaning wait for the child whose process ID is equal to the value of pid.

     

    6、竞争条件

    当多个进程都企图对共享数据进行某种操作,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞争条件。如果在fork之后的某种逻辑显式或隐式地依赖于在fork之后是父进程先运行还是子进程先运行,那么fork函数就会是竞争条件活跃的滋生地。通常,我们不能预料哪一个进程先运行,即使知道哪一个进程先运行,那么在该进程开始运行后,所发生的事情也依赖于系统负载以及内核的调度算法。

     

    7、exec函数

    用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程(该子进程),所以前后的进程ID未改变。exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段。

    用fork可以创建新进程,用exec可以执行新程序。exit函数和两个wait函数处理终止和等待终止。这些是我们需要的基本的控制原语。

    对打开的文件的处理与每个描述符的执行时关闭(close_on_exec)标志值有关。进程中每个打开描述符都有一个执行时关闭标志。若此标志设置,则在执行exec时关闭该描述符,否则该描述符打开。除非特地用fcntl设置了该标志,否则系统的默认操作时在执行exec后仍保持这种描述符打开。

    POSIX.1明确要求在执行exec时关闭打开的目录流。它调用fcntl函数为对应于打开目录流的描述符设置执行时关闭标志。

    注意,在执行exec前后实际用户ID和实际组ID保持不变,而有效ID是否改变则取决于所执行程序文件的设置用户ID位和设置组ID位是否设置。如果新程序的设置用户ID位已设置,则有效用户ID变成程序文件所有者ID,否则有效用户ID不变。对组ID的处理方式与此相同。

     

    8、更改用户ID和组ID

    	int setuid(uid_t uid);
    	int setgid(gid_t gid);

    有关改变用户ID(适用于组ID)的规则:

    (1)若进程具有超级用户特权,则setuid函数将实际用户ID、有效用户ID,以及保存的设置用户ID设置为uid;

    (2)若进程没有超级用户特权,但是uid等于实际用户ID或保存的设置用户ID,则setuid只讲有效用户ID设置为uid,不改变实际用户ID和保存的设置用户ID;

    (3)如果上面两个条件都不满足,则将errno设置为EPERM,并返回

     

    关于内核所维护的三个用户ID,还要注意下列三点:

    (1)只有超级用户进程可以更改实际用户ID。通常,永远用户ID是在用户登录时,由login程序设置的,而且永远不会改变它。因为login是一个超级用户进程,当它调用setuid时,会设置所有三个用户ID;

    (2)仅当对程序文件设置了设置用户ID时,exec函数才会设置有效用户ID。如果设置用户ID位没有设置,则exec函数不会改变有效用户ID,而将其维持为原先值。任何时候都可以调用setuid,将有效用户ID设置为实际用户ID或保存的设置用户ID。自然,不能将有效用户ID设置为任意随机值;

    (3)保存的设置用户ID是由exec复制有效用户ID而的来的。如果设置了文件的设置用户ID位,则在exec根据文件的用户ID设置了进程的有效用户ID以后,就将这个副本保存起来。

    下图显示改变三个用户ID的不同方法

    图片1

     

    例子:

    Changing User IDs and Group IDs

    To see the utility of the saved set-user-ID feature, let's examine the operation of a program that uses it. We'll look at the man(1) program, which is used to display online manual pages. The man program can be installed either set-user-ID or set-group-ID to a specific user or group, usually one reserved for man itself. The man program can be made to read and possibly overwrite files in locations that are chosen either through a configuration file (usually /etc/man.config or /etc/manpath.config) or using a command-line option.

     

    The man program might have to execute several other commands to process the files containing the manual page to be displayed. To prevent being tricked into running the wrong commands or overwriting the wrong files, the man command has to switch between two sets of privileges: those of the user running the man command and those of the user that owns the man executable file. The following steps take place.

     

    Assuming that the man program file is owned by the user name man and has its set-user-ID bit set, when we exec it, we have

        real user ID = our user ID

        effective user ID = man

        saved set-user-ID = man

     

    The man program accesses the required configuration files and manual pages. These files are owned by the user name man, but because the effective user ID is man, file access is allowed.

     

    Before man runs any command on our behalf, it calls setuid(getuid()). Because we are not a superuser process, this changes only the effective user ID. We have

        real user ID = our user ID (unchanged)

        effective user ID = our user ID

        saved set-user-ID = man (unchanged)

     

    Now the man process is running with our user ID as its effective user ID. This means that we can access only the files to which we have normal access. We have no additional permissions. It can safely execute any filter on our behalf.

     

    When the filter is done, man calls setuid(euid), where euid is the numerical user ID for the user name man. (This was saved by man by calling geteuid.) This call is allowed because the argument to setuid equals the saved set-user-ID. (This is why we need the saved set-user-ID.) Now we have

        real user ID = our user ID (unchanged)

        effective user ID = man

        saved set-user-ID = man (unchanged)

     

    The man program can now operate on its files, as its effective user ID is man.

     

    By using the saved set-user-ID in this fashion, we can use the extra privileges granted to us by the set-user-ID of the program file at the beginning of the process and at the end of the process. In between, however, the process runs with our normal permissions. If we weren't able to switch back to the saved set-user-ID at the end, we might be tempted to retain the extra permissions the whole time we were running (which is asking for trouble).

     

    Let's look at what happens if man spawns a shell for us while it is running. (The shell is spawned using fork and exec.) Because the real user ID and the effective user ID are both our normal user ID (step 3), the shell has no extra permissions. The shell can't access the saved set-user-ID that is set to man while man is running, because the saved set-user-ID for the shell is copied from the effective user ID by exec. So in the child process that does the exec, all three user IDs are our normal user ID.

     

    9.system函数

    int system(const char *command);

    因为system在其实现中调用了fork、exec和waitpid,因此有三种返回值:

    (1)如果fork失败或者waitpid返回出EINTR之外的出错,则system返回-1,而且errno设置了错误类型值;

    (2)如果exec失败(表示不能执行shell),则其返回值如果shell执行了exit(127)一样;

    (3)否则所有三个函数(fork、exec和waitpid)都执行成功,并且system的返回值是shell的终止状态,其格式已在waitpid中说明。

     

    system函数(没有信号处理)

    	int system(const char *cmdstring)
    	{
    		pid_t pid;
    		int status;
    		
    		if(cmdstring == NULL) {
    			return (1);
    		}
    		
    		if((pid = fork()) < 0) {
    			status = -1;			// probably out of processes
    		} else if(pid == 0) {
    			execl("/bin/sh","sh","-c",cmdstring,(char *)0);
    			_exit(127);
    		} else {
    			while(waitpid(pid,&status,0) < 0) {
    				if(errno != EINTR) {
    					status = -1;	// error other than EINTR from waitpid()
    					break;
    				}
    			}
    		}
    		
    		return (status);
    	}

    注意,我们调用_exit而不是exit,这是为了防止任一标准I/O缓冲区(这些缓冲区会在fork中由父进程复制到子进程)在子进程中被冲洗。

    使用system而不是直接使用fork和exec的优点是:system进行了所需的各种出错处理,以及各种信号处理。

     

    设置用户ID程序

    这是一个安全性漏洞,如:

    	// tsys.c
    	int main(int argc,char *argv[])
    	{
    		int status;
    
    		if(argc < 2) {
    			printf("command-line argument required.
    ");
    			exit(-1);
    		}
    
    		if((status = system(argv[1])) < 0) {
    			printf("system error!
    ");
    			exit(-1);
    		}
    
    		return 0;
    	}
    	
    	// printuids.c
    	int main()
    	{
    		printf("real uid = %d,effective uid = %d
    ",getuid(),geteuid());
    		return 0;
    	}

    运行结果:

    	$ ./main ./printuids
    	real uid = 1000,effective uid = 1000
    	$ sudo chown root main
    	$ sudo chmod u+s main
    	$ ls -l main
    	-rwsrwxr-x 1 root xxx 7232  4月 23 16:16 main
    	$ ./main ./printuids
    	real uid = 1000,effective uid = 0

    我们给予tsys程序的超级用户权限在system中执行了fork和exec之后仍会保存下来。

    如果一个进程正以特殊的权限(设置用户ID或设置组ID)运行,它又想生成另一个进程执行另一个程序,则它应当直接使用fork和exec,而且在fork之后、exec之前要改回到普通权限。设置用户ID或设置组ID程序决不应调用system函数。

  • 相关阅读:
    使用json-lib进行Java和JSON之间的转换
    PHP实现RabbitMQ消息队列(转)
    PHP错误日志和内存查看(转)
    linux下安装python3(转)
    如何在Linux上设置SSH密码以进行无密码登录(转)
    事务的ACID特性(转)
    PHP之缓存雪崩,及解决方法(转)
    php字符串统计次数的各种方法(转)
    php批量检测和去掉bom头(转)
    go延时队列
  • 原文地址:https://www.cnblogs.com/sheshiji/p/3683277.html
Copyright © 2011-2022 走看看