进程控制(Note for apue and csapp)
1. Introduction
We now turn to the process control provided by the UNIX System. This includes the creation of new processes, program execution, and process termination. We also look at the various IDs that are the property of the process — real, effective, and saved; user and group IDs—and how they’re affected by the process control primitives. Interpreter files and the system function are also covered. We conclude the chapter by looking at the process accounting provided by most UNIX systems. This lets us look at the process control functions from a different perspective.
我们一起来看UNIX系统的进程控制。进程控制包括创建新进程,程序执行和进程终止。我们将关注关于进程属性的各种ID,包括实际用户ID(组ID)、有效用户ID(组ID)、被保存的用户ID和组ID,以及这些ID是如何受到进程控制原语的影响的。
2. 进程标识
每一个进程都有一个唯一的ID(非负整数)来标识。
There are some special processes, but the details differ from implementation to implementation. Process ID 0 is usually the scheduler process and is often known as the swapper. No program on disk corresponds to this process, which is part of the kernel and is known as a system process. Process ID 1 is usually the init process and is invoked by the kernel at the end of the bootstrap procedure.
系统中有一些专用进程,其具体细节随实现而有所不同。ID为0的进程通常是调度进程,常被称作交换进程(swapper)。调度进程是内核的一部分,作为系统进程存在,因此没有任何一个在磁盘上的程序对应于该进程。ID为1的进程通常是init进程,在引导程序结束后由内核调用。
The program file for this process was /etc/init in older versions of the UNIX System and is /sbin/init in newer versions. This process is responsible for bringing up a UNIX system after the kernel has been bootstrapped. init usually reads the system-dependent initialization files — the /etc/rc* files or /etc/inittab and the files in /etc/init.d—and brings the system to a certain state, such as multiuser.
init进程负责在引导完内核后,启动一个UNIX系统。init通常读取与系统有关的初始化文件,并将系统引导到一个状态,例如多用户。
The init process never dies. It is a normal user process, not a system process within the kernel, like the swapper, although it does run with superuser privileges.
init进程绝不会终止。它是一个普通的用户进程(与交换进程不同,init进程不是内核中的系统进程),但是它以超级用户特权运行。
注意:在计算机系统中,CPU的功能就是要执行程序,也就是:取指、译码、执行…那么,如果没有程序要执行,CPU怎么办?此时CPU将执行idle进程,当然该系统进程的优先级是最低的。
来看Linux kernel,Linux系统中,CPU被两类程序占用:一类是进程(或线程),也称进程上下文;另一类是各种中断、异常的处理程序,也称中断上下文。
进程的存在,是用来处理事务的,如读取用户输入并显示在屏幕上。而事务总有处理完的时候,如用户不再输入,也没有新的内容需要在屏幕上显示。此时这个进程就可以让出CPU,但会随时准备回来(如用户突然有按键动作)。同理,如果系统没有中断、异常事件,CPU就不会花时间在中断上下文。
在Linux kernel中,这种CPU的无所事事的状态,被称作idle状态,而cpuidle framework,就是为了管理这种状态。
除了进程ID,每一个进程还有一些其他标识符,来看下列函数:
注意,以上函数都没有出错返回。
3 fork Function
1. 创建和终止进程
从程序员的角度,我们可以认为进程总是处于下面三种状态之一:
1. 运行。进程要么在CPU上执行,要么在等待被执行且最终被内核调度。
2. 停止。进程的执行被挂起(suspend),且不会被调度。当收到SIGSTOP(相应事件:不是来自终端的停止信号)、SIGTSTP(相应事件:来自终端的停止信号)、SIGTTIN(相应事件:后台进程从终端读)或者SIGTTOU(相应事件:后台进程向终端写)信号时,进程就停止,并且保持停止直到它收到一个SIGCONT信号,在这个时刻,进程再次开始运行(注意,信号是一种软件中断的形式)。
3. 终止。进程永远地停止了。大体来讲,进程会因为三种原因终止:1)收到一个信号,该信号的默认行为是终止进程,2)从主程序返回,3)调用exit函数。
2. fork function
父进程通过fork函数创建一个新的运行子进程:
#include <unistd.h>
pid_t fork(void);
返回值:子进程返回0,父进程返回子进程ID;若出错,返回-1
1)调用一次,返回两次:
fork函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值是新建子进程的进程ID。
2)并发执行
父进程与子进程是并发运行的独立进程。内核能够以任意方式交替执行它们的逻辑控制流中的指令。
3)相同的但是独立的地址空间
新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟空间地址相同的但是独立的(会映射到内存的不同位置)一份拷贝,包括文本、数据和bss段、堆以及用户栈。子进程还获得与父进程任何打开文件描述符相同的拷贝,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的进程ID。
注意
值得一提的是如果使用标准I/O,调用进程在fork子进程之后,子进程同样会得到标准I/O缓冲区的拷贝(标准库I/O缓冲区通常由malloc分配,因此存在于堆中,在linux下可以通过setvbuf自行设置I/O缓冲区)。
4)共享文件
父进程的所有打开文件描述符都被复制到子进程中。之所以说是“复制”,是因为对每个文件描述符来说,就好像执行了dup函数。父进程和子进程每个相同的打开描述符共享一个文件表项,如下图:
因此,如果所用的文件描述符在fork之前打开,那么父进程和子进程将会写同一描述符指向的文件,如果没有任何形式的同步,那么它们的输出就会相互混合。一种比较常见的处理是,在fork之后,父进程和子进程各自关闭它们不需要使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。
4. vfork function
1. 写时复制
由于在fork之后经常跟随着exec,所以现在很多实现并不对父进程的数据段、栈和堆进行完整的拷贝。作为替代,使用了写时复制(Copy-On-Write,COW)技术。这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读。如果父进程和子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,这通常是虚拟存储器中的一“页”。
2. vfork
vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序。vfork与fork一样都创建一个子进程,但是vfork并不将父进程的地址空间复制到子进程中。刚刚提到过,实现采用写时复制可以提高fork之后跟随的exec操作的效率,但是不复制比部分复制还要快。
vfork与fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。当子进程调用这两个函数的任意一个时,父进程会恢复运行。
注意
使用vfork,那么在子进程调用exec或者exit之前,子进程将在父进程的地址空间中运行,因此会改变父进程的变量。
5 exit function
进程有5种正常终止以及3种异常终止的方式。
5种正常终止方式如下:
1. 在main函数中return。这等效于调用exit。
2. 调用exit函数。此函数由ISO C定义,其操作包括调用各终止处理程序(终止处理程序在调用atexit时登记),然后关闭所有标准I/O流等。由于ISO C并不处理文件描述符、多进程(父进程和子进程)以及作业控制,因此这一定义对于UNIX系统而言是不完整的。
3. 调用_exit或_Exit函数。ISO C定义_EXIT,其目的是为进程提供一种无需运行终止处理程序(exit handlers)或者信号处理程序(signal handlers)而终止的方法。在UNIX系统中,_EXIT和_exit是同义的,并不冲洗(flush)标准I/O流。_exit函数将由exit调用,它(_exit)处理UNIX系统特定的细节。_exit由POSIX.1定义。
In most UNIX system implementations, exit(3) is a function in the standard C library, whereas _exit(2) is a system call.
4. 进程的最后一个线程在其启动例程(start routine)中执行return语句。但是,该线程的返回值不作为进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态(termination status)0返回。
5. 进程的最后一个线程调用pthread_exit函数。同4一样,在这种情况中,进程的终止状态总是0,这与传递给pthread_exit的参数无关。
3种异常终止形式如下:
1. 调用进程本身执行abort函数,它产生SIGABRT信号,这是下一种异常终止的特例。
2. 当进程接受到某些信号时。信号可由进程自身(如调用abort函数)、其他进程或者内核产生(如进程引用地址空间之外的存储单元、或者除以0,内核就会为该进程产生相应的信号)。
3. 进程的最后一个线程对“取消”(cancellation)请求作出响应。默认情况下,“取消”以延迟方式产生:一个线程要求取消另一个线程,若干时间之后,目标线程终止。
无论进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。同时,无论进程如何终止,该终止进程的父进程都可以通过wait或者waitpid函数取得其终止状态。
Exit Functions
#include <stdlib.h>
void exit(int status);
void _Exit(int status);#include <unistd.h>
void _exit(int status);
参数status为退出状态,当最终执行_exit函数时,内核将退出状态转换成终止状态。下图描述了一个c程序是如何启动和终止的:
注意
3个函数用于正常终止一个程序:_exit和_EXIT立即进入内核,exit则先执行一些清理处理(执行标准I/O库的清理关闭操作:对于所有打开流调用fclose函数,这会使得输出缓冲区中的所有数据都被冲洗(写到文件中)),然后进入内核。
孤儿进程
自然地,子进程是在父进程调用fork之后生成的,通常,子进程会将其终止状态返回给父进程。但是如果父进程在子进程之前终止,又将如何呢?答案是:对于父进程已经终止的所有进程,它们将由init进程收养。其操作大致过程是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则该进程的父进程ID就更改为1(init进程的ID)。这种处理方法保证了每个进程有一个父进程。
僵尸进程
一个已经终止,但是其父进程尚未对其进行回收的进程称为僵尸进程(zombie)。
TIP
一个由init进程收养的进程终止时会发生什么?其是否会变成一个僵尸进程?不会。因为init进程被设计成无论何时只要有一个子进程终止,init就会调用一个wait函数取得其终止状态,这样就防止了在系统中塞满僵尸进程。
僵尸进程的避免
1. 父进程通过wait和waitpid来等待子进程的结束,但这会导致父进程的挂起。
2. 如果父进程不想阻塞在wait或者waitpid上,那么可以用signal函数为SIGCHLD信号安装handler,因为子进程终止后,父进程会收到该信号,可以在handler中调用wait或者waitpid回收。
3. 如果父进程不关心子进程什么时候终止,那么可以用signal(SIGCHLD, SIG_IGN) 通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号。(如果父进程没安装SIGCHLD信号处理函数调用wait或waitpid()回收子进程,又没有显式忽略SIGCHLD信号,那么终止子进程就一直保持僵尸状态,如果这时父进程结束了,那么init进程自动收养这个子进程,为它收尸,它还是能被清除的。但是如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。)
4. 还有一些技巧,就是fork两次,父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要自己做。
6 wait and waitpid Functions
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
Both return: process ID if OK, 0 (see later), or −1 on error
当一个进程终止时,无论是正常终止还是异常终止,内核将通过向其父进程发送SIGCHLD信号的方式来通知父进程子进程的终止。由于子进程的终止是一个异步事件(它可以在父进程运行的任何时刻发生),因此这个信号是内核向父进程的异步通知。父进程可以选择忽略这个信号,或者提供一个函数在该信号到达时被调用(信号处理函数)。父进程默认忽略对该信号。当一个进程调用wait或者waitpid可能会发生以下情况:
1. 如果该进程的所有子进程仍然在运行,那么父进程将阻塞。
2. 如果一个子进程已经终止,并在等待父进程获取其终止状态,则父进程取得该子进程的终止状态且立即返回。
3. 如果该进程没有任何子进程,则立即出错返回。
如果进程因为收到SIGCHLD信号而调用wait,那么wait会立即返回。但是,如果在随机时间点调用wait,则进程可能会阻塞。
wait和waitpid的不同之处如下:
1. wait会阻塞调用进程直到该调用进程的一个子进程终止,而waitpid有一个选项可以防止调用进程阻塞。
2. waitpid允许不wait第一个终止的子进程。其有若干个选项来控制进程wait的子进程。
如果子进程已经终止,并且是一个僵尸进程(也就是说还没有被父进程回收),wait函数将立即返回并取得该子进程的状态,否则,wait函数将阻塞调用进程直到该进程的一个子进程终止。如果调用进程阻塞在wait函数上,并且拥有多个子进程,那么只要有一个子进程终止,wait就会返回。调用进程可以区分哪一个子进程终止,因为wait函数将返回被回收的子进程的进程ID。
对于wait和waitpid,参数statloc都是指向integer的一个指针。如果statloc不是空指针,子进程的终止状态将保存在该指针所指向的integer中。如果,调用进程不关心子进程的终止状态,可以将这个参数设置为空指针。
通常,wait以及waitpid所返回的integer status由实现定义,其中某些位指示退出状态(针对正常返回),某些位指示signal编号(针对异常返回),也有一个位指示了是否产生core文件,等等。POSIX.1规定,终止状态用定义在<sys/wait.h>中的各个宏来查看。有4个互斥的宏可以用来取得进程终止的原因,它们的名字都以WIF(wait if …)开始。如下图所示:
对于waitpid函数中pid参数的作用解释如下:
1. pid == –1 等待任一子进程。此种情况下,waitpid与wait等效。
2. pid > 0 等待进程ID与pid相等的子进程。
3. pid == 0 等待与调用进程同属于一个进程组的任一子进程。
4. pid < –1 等待指定进程组(ID为pid的绝对值)中的任一子进程
waitpid函数返回终止子进程的进程ID,并将该子进程的终止状态存放在由statloc指向的存储单元中。对于wait,其唯一真正的出错是调用进程没有子进程(另一种可能出错返回的情况是,函数调用被信号中断)。而对于waitpid,如果指定的进程或者进程组不存在,或者参数pid指定的进程不是调用进程的子进程,都可能出错。
waitpid中的options参数可以让调用进程进一步的控制waitpid的操作。此参数或者是0,或者是由下图所示的常量按位或运算后的结果。
WCONTINUED:如果实现支持作业控制(job control),任一由pid指定的子进程在停止后恢复运行,但是状态尚未报告,则waitpid返回其状态(POSIX的XSI扩展)。
WNOHANG:如果由pid指定的子进程并不是立即可用(不是僵尸进程),则调用进程不会阻塞在waitpid上,此时其返回值为0。
WUNTRACED(wait untraced):若实现支持作业控制,而由pid指定的任一子进程已处于停止状态,并且其状态自停止以来还未报告过,则waitpid返回其状态。WIFSTOPPED宏确定返回值是否对应于一个停止的子进程。
waitpid函数提供了wait函数没有提供的3个功能:
1. waitpid可等待(回收)一个特定的进程,而wait则返回任一终止子进程的状态。
2. waitpid提供了一个wait的非阻塞版本。调用进程有时希望获取一个子进程的状态,但不想阻塞。
3. waitpid通过WUNTRACED和WCONTINUED选项支持作业控制(job control)。
(未完待续)