fork函数

fork函数

一个存在的进程可以通过fork函数建立一个新的进程。

#include <unistd.h>

pid_t fork(void);

Returns: 0 in child, process ID of child in parent, 1 on error

 

fork建立的新进程叫“子进程”。这个函数调用一次,但返回两次。两次返回值唯一不同的地方是:在子进程中返回0,而在父进程中返回子进程的ID。这样做是因为一个进程可以有多个子进程,如果不在父进程中获得子进程的ID,那将不会有任何程序能得到子进程的ID。fork的子进程返回0,是因为子进程只能有一个父进程,并且子进程可以调用getppid获得父进程的ID。(PS:进程ID0是预留给内核的,所以不可能分配给子进程。)

无论子进程还是父进程都会继续执行fork后面的指令。子进程会复制父进程。如:子进程会复制父进程的数据空间、堆和栈。 注意:这只是复制,而不是父、子进程共享一段内存。但父、子进程共享文本段。

因为fork后面通常是跟一个exec函数,所以当前发行版本都没复制完整的父进程数据空间、栈和堆的复制。取而代之的是一种叫做COW(COPY-ON-WRITE,写时复制)的技术被使用。被父、子进程共享的区域由内核设置成只读防止更改。如果父、子进程之一试图修改这个区域,那么内核会生成该块内存的复制,通常是虚拟存储器系统中的“一页”。

Linux 2.4.22同样提供了新的建立进程的方法——clone系统调用,它从fork中衍生出来,并且允许调用者控制父、子进程的哪些可以共享。

一般来说不会知道子进程是在父进程之前执行还是之后。这依赖于内核的进程调度算法。如果需要父、子进程同部,需要使用某些进程间通信的方法。

strlen计算字符串的长度不包含最后的null字符。sizeof计算缓存的长度,包含最后的null字符。它们两个的区别是:strlen需要函数调用,而sizeof是在编译时刻才运行的,因为缓存使用已知的字符串初始化,并且它是固定长度的。

注意在fork的程序中使用i/o操作时,它们之间的相互作用。write函数是无缓冲的。而标准函数库在连接到终端设备时是行缓冲的,其它情况是全缓冲的。在交互环境使用printf时,换行会刷新缓冲区。但是当重定向标准输出到文件时,会得到两个printf的行拷贝,printffork之前调用一次,但是缓存会保留到fork调用,之后在复制父进程数据空间到子进程时,这个缓存会复制到子进程。

#include "apue.h"

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);
}

If we execute this program, we get

$ ./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的一个特性是:所有在父进程中打开的文件描述符都会复制(duplicated)到子进程中。说复制(duplicated)的原因是它的行为如同在文件描述符上调用了dup程序。父、子进程共享同一个文件表的内容。

父、子进程共享同一个文件偏移量,这点非常重要。

这有两个常用方法,用于处理fork后的文件描述符。

  1. 父进程等待子进程完成。这种情况下父进程不对文件描述符做任何操作。当子进程结束后,偏移量会相应的更新。
  2. 父、子进程做他们自己想做的事。在这,当fork后,父进程关闭不需要的文件描述符,子进程也做同样的事情。这样就不会干扰对方使用的文件描述符了。这种方法常用在网络服务进程中。

除了文件描述符之外,还有很多属性从父进程中继承到子进程中:

  • 实际用户ID、实际组ID、有效用户ID、有效组ID
  • 附加组ID
  • 进程组ID
  • 会话ID
  • 控制终端
  • SUID和SGID
  • 当前工作目录
  • 根目录
  • 文件模式创建屏蔽字
  • signal mask 和 dispositions
  • 任何打开的文件描述符的close-on-exec标记
  • 环境
  • attached shared memory segments
  • 内存映射
  • 资源限制

父子进程间不同的是:

  • fork的返回值
  • 进程ID
  • 父、子进程有不同的父进程ID。
  • 子进程的tms_utime, tms_stime, tms_cutimetms_cstime值设置成0。
  • 父进程设置的文件锁不会继承到子进程。
  • 为子进程清除未处理的警告
  • 为子进设置空的未处理信号集

还有很多以后章节再讲。

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

(a)系统中有太多的进程。这通常意味着某些地方出问题了。

(b)超过当前用户的最大进程总数限制。CHILD_MAX指定了实际用户ID可以同时拥有的最大进程数。

fork有两种用法:

  1. 当一个进程想复制自己,去同时执行不同的代码段。这通常用于网络服务,父进程做为服务端等待,子进程处理客户端发起的请求。当请求到达时,父进程调用fork,然后让子进程处理请求。然后父进程再次回到等待状态。
  2. 当一个进程想执行一个不同的程序时。这常用于shell。在这种情况下子进程从fork返回后立即执行一个exec。一些系统捆绑了fork后跟一个exec到一个单独操作,被称作:spawn。而unix系统分开了这两步,因为很多情况要单独使用fork。分开操作允许子进程在fork和exec之间改变每个进程的属性。如:I/O重定向,用户ID,signal disposition等等。

发表回复