1、简介

本章我们将讲述UNIX系统提供的进程控制相关的内容。包括新进程的创建,程序的执行,以及进程的终止。我们也会看到进程属性相关的各种ID:真实(real user)用户/组ID,有效(effective user)用户/组ID,保存(saved)用户/组ID,以及进程的基本控制(控制原语)如何影响它们。本章也谈到了解释器文件以及system函数。最后我们通过讲述大多UNIX系统提供的进程记账机制结束本章,这也使得我们从一个不同的角度来看待进程控制函数。

译者注

原文参考

参考: APUE2/ch08lev1sec1.html

2、进程标识

每一个进程用非负数字来唯一标识(即pid),由于进程pid具有唯一性,所以也常用做其他需要确保唯一性的标识的一部分。例如一些特殊的文件名称中就含有进程pid。

尽管进程pid是唯一的,但是它可以在进程结束之后再度被重新利用。大多数系统会在进程结束之后延迟其pid的重复利用,防止随后的进程很快采用先前结束进程的pid,导致一些错误。

另外有一些特殊的进程:pid为0的进程是调度进程(swapper),磁盘中没有这个进程对应的可执行文件,因为它是内核的一部分,作为系统进程;pid为1的进程一般就是init程序,它在内核加载的最后阶段被调用,其对应的可执行文件在早期的版本中是 /etc/init ,后来的版本中是 /sbin/init ,这个进程用来在系统内核启动之后运行一些操作系统必须的脚本,它会读取一些例如 /etc/rc* 或者 /etc/inittab , /etc/init.d/* 系统无关的脚本,然后系统将进入特定状态,例如多用户状态等。init进程不会死掉,它是一个一般用户进程,不像swapper那样是内核里面的系统进程,init用supersuser的权限运行,后面会提到init也是所有孤儿进程的父亲进程。

每个系统的实现也有一些自己的核心进程集合,用来提供一些服务。例如在一些unix系统中pid为2的进程是 pagedaemon,支持虚拟内存系统的页机制。

除了PID,还有进程还有其他的标识属性,如下是获取这些属性的相关函数:

#include <unistd.h>
pid_t getpid(void);

返回:调用进程的PID.

pid_t getppid(void);

返回:调用进程的父进程PID.

uid_t getuid(void);

返回:调用进程的真实用户ID。

uid_t geteuid(void);

返回:调用进程的有效用户ID.

gid_t getgid(void);

返回:调用进程的真实ID组。

gid_t getegid(void);

返回:调用进程的有效ID组。

注意:这些函数都不会返回错误。

译者注

原文参考

参考: APUE2/ch08lev1sec2.html

3、fork

(1)创建进程使用fork来实现,fork函数如下

#include <unistd.h>
pid_t fork(void);

返回:在子进程中返回为0;父进程中返回为子进程PID,如果出错返回1(实际值一般为-1)。

通过fork创建的新进程叫做子进程。这个fork函数调用了一次(在父进程中),但是返回了两次(在父进程中,以及新出现的子进程中)。通过返回值可知是父还是子进程。因为没有获得一个进程子进程PID的函数,所以在父进程的fork返回中会返回子进程PID,以做记录和判断等用。因为所有子进程只可能有一个父进程,所以在子进程中fork返回0,子进程可以通过getppid来获得父进程的进程ID(进程PID为0的进程是内核中的swapper所以它不可能是某个进程的子进程)。

在调用fork之后,产生的子进程和父进程会继续在代码中fork调用的后面继续执行。子进程是父进程的一个拷贝,它拷贝了父进程的数据空间,堆和栈空间,一定要注意这是一份拷贝,父子进程不会共享这部分内存的内容。父子进程共享的部分是代码段部分。

由于fork一般会接着一个exec函数,当前的系统实现一般不会立即执行父进程数据,堆,栈的拷贝。一般会使用一种copy-on-write(COW)的技术,也就是说,这些区域起初是父子进程共享的,一旦有进程进行了修改这些区域的操作,内核才会创建相应区域内存的一份拷贝(一般是虚拟内存的页)。

实际上有不同种类的fork函数。本文中的四种系统都支持vfork(2)函数,这个函数在后面会提到。

  • Linux 2.4.22 提供使用clone系统调用来创建新进程。这个系统调用是一个fork的通用形式,允许调用者控制在父子进程***享哪部分数据。
  • FreeBSD 5.2.1 提供rfork系统调用,和Linux中的clone系统调用类似,rfork是从Plan 9操作系统中继承过来的。
  • Solaris 9 提供两个线程库,一个是POSIX的,一个是Solaris threads.两种fork的动作有所不同。对于POSIX,fork创建一个只包含调用线程的进程,但是Solaris的fork创建的进程包含调用线程的进程中的所有线程的拷贝。为了提供和posix类似的fork,solaris提供了一个fork1函数,它可以创建一个只拷贝调用线程的进程。

(2)fork的使用

下面给出一个使用fork函数的例子:

int     glob = 6;       /* external variable in initialized data */
char    buf[] = "a write to stdout\n";

int
main(void)
{
    int       var;      /* automatic variable on the stack */
    pid_t     pid;

    var = 88;
    if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
        err_sys("write error");
    printf("before fork\n");    /* we don't flush stdout */

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {      /* child */
        glob++;                 /* modify variables */
        var++;
    } else {
        sleep(2);               /* parent */
    }

    printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);
    exit(0);
}

运行这个例子,得到如下结果:

$ ./a.out
a write to stdout
before fork
pid = 430, glob = 7, var = 89      child's variables were changed
pid = 429, glob = 6, var = 88      parent's copy was not changed
$ ./a.out > temp.out
$ cat temp.out
a write to stdout
before fork
pid = 432, glob = 7, var = 89
before fork
pid = 431, glob = 6, var = 88

在上面的这个例子中,给出了一个使用fork的例子,并且通过这个例子我们应该知道:

  1. 如果fork之后,是parent先运行还是children这是不确定的,取决于内核的调度算法。
  2. 子进程是父进程的拷贝,包括文件缓存等都是一个副本,所以子进程修改不会影响父进程。
  3. 由于子进程拷贝了父进程的数据空间,如果使用库函数打印,重定向时可能同一个打印语句在文件中出现两次,因为缓存也拷贝了。

(3)文件共享

父子进程之间共享文件偏移量,这样在父子进程同时写一个文件的时候容易控制它们之间的配合;如果不共享的话,有些定位之类的操作很难配合进行或者很麻烦。当然如果父子不同步的话,两者对文件的输出可能会相互干扰。

一般fork之后,有两种操作文件描述符号的情况:

  1. 父进程等待子进程结束。这时候父进程不需要对文件描述符号做任何事情,在子进程结束之后子进程读写的文件描述符号的偏移会自动更新。
  2. 父子进程进行它们各自的操作。这时候,父子进程需要关闭它们不需要的文件描述符号,防止两者之间互相干扰。这在网络服务的环境下面经常会用到。

下面的图给出了调用fork之后父子进程打开共享文件的图示情况:

parent process table entry                  file table                v-node table
+------------------------+ ----------->+-------------------+     ---->+-----------+
|      fd       file     |/       +--->| file status flags |    /     |   v-node  |
|     flags    pointer   /        |    +-------------------+   /      |information|
|     +-----+---------+ /|        |    |current file offset|  /       +-----------+
| fd0:|     |         |/ |        |    +-------------------+ /        |   i-node  |
|     +-----+---------+  |        |    |   v-node pointer  |/         |information|
| fd1:|     |         |\ |        |    +-------------------+          |...........|
|     +-----+---------+ \|        |                                   |  current  |
| fd2:|     |         |\ \        |                                   | file size |
|     +-----+---------+ \|\       |                                   +-----------+
|     |               |  \ -------+--->+-------------------+
|     |   ......      |  |\       |  ->| file status flags |    ----->+-----------+
|     |               |  | \      | /  +-------------------+   /      |   v-node  |
|     +---------------+  |  \    / /   |current file offset|  /       |information|
+------------------------+   \  / /    +-------------------+ /        +-----------+
                              \/ /     |   v-node pointer  |/         |   i-node  |
                              /\/      +-------------------+          |information|
                             / /\                                     |...........|
child  process table entry  / /  \                                    |  current  |
+------------------------+ / /    ---->+-------------------+          | file size |
|      fd       file     |/ / -------->| file status flags |          +-----------+
|     flags    pointer   / / /         +-------------------+
|     +-----+---------+ /|/ /          |current file offset|  ------->+-----------+
| fd0:|     |         |/ / /           +-------------------+ /        |   v-node  |
|     +-----+---------+ /|/            |   v-node pointer  |/         |information|
| fd1:|     |         |/ /             +-------------------+          +-----------+
|     +-----+---------+ /|                                            |   i-node  |
| fd2:|     |         |/ |                                            |information|
|     +-----+---------+  |                                            |...........|
|     |               |  |                                            |  current  |
|     |   ......      |  |                                            | file size |
|     |               |  |                                            +-----------+
|     +---------------+  |
+------------------------+

除了打开的文件之外,还有许多父子进程共享的内容,具体参见参考资料给出列表。

父子进程不同的地方是:

  • fork返回值不同。
  • 进程PID不同。
  • 进程的父进程PID不同,子进程的父进程PID设置为父进程,父进程的父进程PID不变。
  • 子进程的tms_utime, tms_stime, tms_cutime, 和 tms_cstime取值设置为0.
  • 父进程设置的文件锁不会被子进程继承。
  • 在子进程中会把申请的警钟清空。
  • 子进程会把申请的信号集合清空。

fork失败的原因有两个:

  1. 系统中进程数目太多(一般这是因为出现了什么问题而导致的)
  2. 当前用户 UID的进程数目超过了系统的限制。可以通过CHILD_MAX指定每个用户UID同时可以运行的进程的数目。

fork 一般有两种使用的方法:

  1. 进程想要复制自己,并且在同时执行另外的代码。这在网络中很常见:服务器端监听,接收到一个客户请求,之后它fork一个子进程来处理客户请求,然后父进程继续监听其他的请求。
  2. 进程想要执行一个不同的程序:一般都是fork之后调用exec来做的。

有些系统把2)的fork紧接这exec合并成了一个操作spawn. unix 把操作分成了两个部分,因为许多时候fork不需紧接着exec或者fork和exec之间需要做一些修改进程属性的操作等等。

Single UNIX Specification 在高级的real-time选项组包含了spawn接口。这些接口不是fork和exec的替代。他们用来支持很难高效地执行fork的系统,尤其是那些没有内存管理硬件支持的系统。

译者注

原文参考

参考: APUE2/ch08lev1sec3.html