“fork是一种创建自身进程副本的操作。 ”

Background

最近正在阅读Twemproxy的源代码,从中发现涉及到大量《操作系统原理》和Linux系统编程的知识,由此我这些知识记录下来,做一个系列的笔记。

概论

在多任务操作系统中,运行中的程序需要一种方法来创建新进程,例如运行其他程序。Fork及其变种在类Unix系统中通常是这样做的唯一方式。如果进程需要启动另一个程序的可执行文件,它需要先Fork来创建一个自身的副本。然后由该副本即“子进程”调用exec系统调用,用其他程序覆盖自身:停止执行自己之前的程序并执行其他程序。

正文

进程复制

Fork操作会为子进程创建一个单独的定址空间。子进程拥有父进程所有内存段的精确副本,其中包括:进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。

当一个进程调用fork时,它被认为是父进程,新创建的进程是它的孩子(子进程)。在fork之后,两个进程还运行着相同的程序,都像是调用了该系统调用一般恢复执行。然后它们可以检查调用的返回值确定其状态:是父进程还是子进程,以及据此行事。

子进程与父进程的区别在于:

  • 父进程设置的锁,子进程不继承(因为如果是排它锁,被继承的话,矛盾了)。

  • 各自的进程ID和父进程ID不同,且与存在的进程PID都不同。

  • 子进程的未决告警被清除。

  • 子进程的未决信号集设置为空集。

  • 子进程不会从父进程继承dnotify(directory change notifications)。

  • 在子进程中的 resource utilizations (getrusage)和CPU time counters (times)会被归零。

  • 子进程不能从父进程继承计时器(setitimer, alarm, timer_create)。

  • 子进程不能从父进程继承异步I/0操作(aio_write,aio_read),也不能从父进程继承asynchronous I/O contexts.(io_setup)。

上面列出是在POSIX.1-2001下的不同点,下面是linux系统特用的不通点。

  • 子进程不能继承由ioperm设置的I/O 端口访问权限,子进程必须使用ioperm来开启端口访问权限。

  • 子进程的中断信号是SIGCHLDclone)。

  • Memory mappings 是被madvise设置的话,MADV_DONTFORK标识不会继承给子进程的

  • 默认的timer slack值由父进程中的当前timer slack值来设置。 (可以看一下prctl中的PR_SET_TIMERSLACK的描述)

  • prctl的PRSETPDEATHSIG设置会被重置,因此当父进程终止后,子进程不会收到信号。

注意以下几点:

  • 子进程只包含一个线程,并且有fork产生。父进程的整个虚拟地址空间被复制给子进程,包括互斥器、条件变量和其他的pthread对象;这种行为带来的问题pthread_atfork可能有帮助。

  • 子进程继承了父进程打开文件描述符。父进程和子进程指向相同的文件描述符。也就是说,两个进程的文件描述符共享相同的打开文件状态标志、当前文件偏移和信号驱动IO属性。

  • 子进程继承了父进程打开消息队列描述符。父进程和子进程指向相同的消息队列描述符。也就是说,两个描述符共享相同的标志。

  • 子进程继承了父进程打开目录流的集合。POSIX.1-2001说父子进程中的目录流可能共享目录流位置,但Linux/glibc没这么做。

实践

#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    printf("--beginning of program\n");

    int counter = 0;
    pid_t pid = fork();

    if (pid == 0)
    {
        // child process
        int i = 0;
        for (; i < 5; ++i)
        {
            printf("child process: counter=%d\n", ++counter);
        }
    }
    else if (pid > 0)
    {
        // parent process
        int j = 0;
        for (; j < 5; ++j)
        {
            printf("parent process: counter=%d\n", ++counter);
        }
    }
    else
    {
        // fork failed
        printf("fork() failed!\n");
        return 1;
    }

    printf("--end of program--\n");

    return 0;
}

运行结果

--beginning of program
parent process: counter=1
parent process: counter=2
parent process: counter=3
parent process: counter=4
parent process: counter=5
child process: counter=1
--end of program--
child process: counter=2
child process: counter=3
child process: counter=4
child process: counter=5
--end of program--