标签:
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是如何受到进程控制原语的影响的。
每一个进程都有一个唯一的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,每一个进程还有一些其他标识符,来看下列函数:
注意,以上函数都没有出错返回。
从程序员的角度,我们可以认为进程总是处于下面三种状态之一:
1. 运行。进程要么在CPU上执行,要么在等待被执行且最终被内核调度。
2. 停止。进程的执行被挂起(suspend),且不会被调度。当收到SIGSTOP(相应事件:不是来自终端的停止信号)、SIGTSTP(相应事件:来自终端的停止信号)、SIGTTIN(相应事件:后台进程从终端读)或者SIGTTOU(相应事件:后台进程向终端写)信号时,进程就停止,并且保持停止直到它收到一个SIGCONT信号,在这个时刻,进程再次开始运行(注意,信号是一种软件中断的形式)。
3. 终止。进程永远地停止了。大体来讲,进程会因为三种原因终止:1)收到一个信号,该信号的默认行为是终止进程,2)从主程序返回,3)调用exit函数。
父进程通过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之后,父进程和子进程各自关闭它们不需要使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。
1. 写时复制
由于在fork之后经常跟随着exec,所以现在很多实现并不对父进程的数据段、栈和堆进行完整的拷贝。作为替代,使用了写时复制(Copy-On-Write,COW)技术。这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读。如果父进程和子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,这通常是虚拟存储器中的一“页”。
2. vfork
vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序。vfork与fork一样都创建一个子进程,但是vfork并不将父进程的地址空间复制到子进程中。刚刚提到过,实现采用写时复制可以提高fork之后跟随的exec操作的效率,但是不复制比部分复制还要快。
vfork与fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。当子进程调用这两个函数的任意一个时,父进程会恢复运行。
注意
使用vfork,那么在子进程调用exec或者exit之前,子进程将在父进程的地址空间中运行,因此会改变父进程的变量。
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);
#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)。
(未完待续)
标签:
原文地址:http://www.cnblogs.com/jianxinzhou/p/4671213.html