fork()函数是如何工作的?

23

我是一个新手,不太了解forking,这段代码中的pid是做什么用的?有人可以解释一下X行和Y行输出的内容吗?

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#define SIZE 5
int nums[SIZE] = {0,1,2,3,4};
int main()
{
    int i;
    pid_t pid;
    pid = fork();
    if (pid == 0) {
        for (i = 0; i < SIZE; i++) {
            nums[i] *= -i;
            printf("CHILD: %d ",nums[i]); /* LINE X */
        }
    }
    else if (pid > 0) {
        wait(NULL);
        for (i = 0; i < SIZE; i++)
            printf("PARENT: %d ",nums[i]); /* LINE Y */
    }
    return 0;
}

1
你尝试过实际编译和运行它吗?你认为会发生什么? - Mats Petersson
3
man fork 是一个 Linux 系统调用,用于创建一个新的进程作为原始进程的副本。子进程将获得与父进程相同的代码、数据和堆栈,并从父进程那里继承打开的文件和其他系统资源。在成功调用 fork() 后,父进程和子进程在不同的地址空间中运行,并且具有不同的进程 ID。子进程从 fork() 返回0,而父进程则返回子进程的进程 ID。 - RageD
请确保在 printf() 语句的末尾放置换行符,否则可能什么都不会显示。您应该使用 #include <sys/wait.h> 来声明 wait() 函数;您可能不需要显式地使用 #include <sys/types.h> - Jonathan Leffler
prog.c:9:1: 错误:未知类型名称‘pid’?为什么会这样? - PhoonOne
实际上,fork很难解释和理解。需要花费几个小时阅读一本关于Unix编程的好书。Advanced Linux Programming是免费提供的,其中大部分内容适用于任何Unix或POSIX系统。 - Basile Starynkevitch
显示剩余4条评论
5个回答

33

fork()函数会复制进程,因此在调用fork()函数后,实际上有两个程序实例在运行。

如何知道哪个进程是原始的(父)进程,哪个是新的(子)进程?

在父进程中,从fork()返回子进程的PID(将是正整数)。这就是为什么if (pid > 0) { /* PARENT */ }代码可以工作的原因。在子进程中,fork()函数只返回0

因此,由于if (pid > 0)检查,父进程和子进程将产生不同的输出,您可以在here(由@jxh在评论中提供)中看到。


2
代码中还有第三个分支(未处理)。 如果fork()失败了呢?=P - gEdringer
@gEdringer,虽然您可能已经知道了,但为了其他人的参考——当fork返回负值表示错误时,您可以使用perror(fork)。请查看man perror - Donbhupi

32

fork()的最简单示例

printf("I'm printed once!\n");
fork();
// Now there are two processes running one is parent and another child.
// and each process will print out the next line.
printf("You see this line twice!\n");

fork()的返回值。返回值-1=失败;0=子进程;正数=父进程(返回值是子进程id)
pid_t id = fork();
if (id == -1) exit(1); // fork failed 
if (id > 0)
{ 
// I'm the original parent and 
// I just created a child process with id 'id'
// Use waitpid to wait for the child to finish
} else { // returned zero
// I must be the newly made child process
}

子进程和父进程有何不同之处?

  • 父进程会在子进程结束时通过信号通知,但反过来则不会。
  • 子进程不会继承未决信号或定时器警报。完整列表请参见fork()
  • 此处可以使用getpid()返回进程ID。父进程ID可以由getppid()返回。

现在让我们将您的程序代码可视化

pid_t pid;
pid = fork();

现在操作系统会为父进程和子进程分别创建两个完全相同的地址空间副本。

enter image description here

父进程和子进程在系统调用fork()后开始执行。由于两个进程具有相同但分离的地址空间,因此在fork()调用之前初始化的那些变量在两个地址空间中具有相同的值。每个进程都有自己的地址空间,所以任何修改都将独立于其他进程。如果父进程更改其变量的值,则修改仅会影响父进程地址空间中的变量。由fork()系统调用创建的其他地址空间不会受到影响,即使它们具有相同的变量名称。

enter image description here

父进程的pid不为零,它调用函数ParentProcess()。另一方面,子进程的pid为零,它调用ChildProcess()如下所示: enter image description here 在您的代码中,父进程调用wait(),在该点暂停,直到子进程退出。因此,子进程的输出会先出现。
if (pid == 0) {                    
    // The child runs this part because fork returns 0 to the child
    for (i = 0; i < SIZE; i++) {
        nums[i] *= -i;
        printf("CHILD: %d ",nums[i]); /* LINE X */
    }
}

子进程的输出

第 X 行的输出内容

 CHILD: 0 CHILD: -1 CHILD: -4 CHILD: -9 CHILD: -16

当子进程退出后,父进程从等待(wait())调用之后继续执行并打印其输出。
else if (pid > 0) {
        wait(NULL);
        for (i = 0; i < SIZE; i++)
            printf("PARENT: %d ",nums[i]); /* LINE Y */
    }

来自父进程的输出:

在Y行输出什么

PARENT: 0 PARENT: 1 PARENT: 2 PARENT: 3 PARENT: 4

最后,子进程和父进程合并的输出将如下显示在终端上:
 CHILD: 0 CHILD: -1 CHILD: -4 CHILD: -9 CHILD: -16 PARENT: 0 PARENT: 1 PARENT: 2 PARENT: 3 PARENT: 4

了解更多信息,请参考此链接


4
看起来你从http://www.csl.mtu.edu/cs4411.ck/www/NOTES/process/fork/create.html复制了几张图片。请注意,这种复制需要署名;我建议你阅读我们的抄袭政策,网址为http://stackoverflow.com/help/referencing。 - josliber
是的,我同意您的建议。我会很快更新答案并提供适当的参考。感谢您的建议。 - Punit Vara
这个回答够好了吗?还需要改进吗?欢迎提出任何建议 :-) - Punit Vara
请确保提到您从该链接获取了图像。如果您从其他地方复制图像、代码或文本,则始终需要链接并注明出处。 - josliber
我刚刚按照你的建议插入了...以后我会注意的。 - Punit Vara
显示剩余2条评论

8
fork()函数很特殊,因为它实际上返回两次:一次给父进程,一次给子进程。在父进程中,fork()返回子进程的pid,在子进程中则返回0。如果出现错误,就不会创建子进程,并且返回-1给父进程。
成功调用fork()之后,子进程基本上是父进程的一个完全复制。两个进程都有它们自己的局部变量和全局变量的复制品,以及它们打开的文件描述符的复制品。两个进程同时运行,并且由于它们共享相同的文件描述符,每个进程的输出可能会交错。
仔细看一下问题中的例子:
pid_t pid;
pid = fork();
// When we reach this line, two processes now exist,
// with each one continuing to run from this point
if (pid == 0) {                    
    // The child runs this part because fork returns 0 to the child
    for (i = 0; i < SIZE; i++) {
        nums[i] *= -i;
        printf("CHILD: %d ",nums[i]); /* LINE X */
    }
}
else if (pid > 0) {
    // The parent runs this part because fork returns the child's pid to the parent
    wait(NULL);     // this causes the parent to wait until the child exits
    for (i = 0; i < SIZE; i++)
        printf("PARENT: %d ",nums[i]); /* LINE Y */
}

这将输出以下内容:
CHILD: 0 CHILD: -1 CHILD: -4 CHILD: -9 CHILD: -16 PARENT: 0 PARENT: 1 PARENT: 2 PARENT: 3 PARENT: 4

由于父进程调用 wait(),因此在子进程退出之前会暂停。所以子进程的输出先出现,然后在子进程退出后,父进程继续从wait()调用之后开始运行,并打印其输出。


4
在最简单的情况下,fork()的行为非常简单 - 尽管初次遇到它可能会有点令人震惊。它要么返回一个错误,要么返回两次:一次在原始(父)进程中,一次在几乎完全复制了原始进程的全新进程中(子进程)。返回后,这两个进程名义上是独立的,尽管它们共享很多资源。
pid_t original = getpid();
pid_t pid = fork();
if (pid == -1)
{
    /* Failed to fork - one return */
    …handle error situation…
}
else if (pid == 0)
{
    /* Child process - distinct from original process */
    assert(original == getppid() || getppid() == 1);
    assert(original != getpid());
    …be childish here…
}
else
{
    /* Parent process - distinct from child process */
    assert(original != pid);
    …be parental here…
}

子进程是父进程的一个副本。例如,它具有相同的一组打开文件描述符;在父进程中打开的每个文件描述符N也在子进程中打开,并且它们共享相同的打开文件描述符。这意味着如果其中一个进程更改了文件中的读或写位置,那么也会影响另一个进程。另一方面,如果其中一个进程关闭了一个文件,则不会直接影响另一个进程中的文件。

这也意味着,如果在父进程中标准I/O包中有缓冲数据(例如,从标准输入文件描述符(STDIN_FILENO)读取了一些数据到stdin的数据缓冲区中),则该数据对父进程和子进程都可用,并且两者都可以读取该缓冲数据而不会影响另一个进程,它们将看到相同的数据。另一方面,一旦读取了缓冲数据,如果父进程读取了另一个缓冲区满,那么这将移动父进程和子进程的当前文件位置,因此子进程将无法看到父进程刚刚读取的数据(但如果子进程也读取了一块数据,则父进程将看不到该数据)。这可能会令人困惑。因此,在fork之前通常最好确保没有未完成的标准I/O——fflush(0)是一种方法。
在代码片段中,assert(original == getppid() || getppid() == 1);允许这样一种可能性:当子进程执行该语句时,父进程可能已经退出,此时子进程将被系统进程继承 - 通常具有PID 1(我不知道有哪个POSIX系统会将孤儿子进程继承到不同的PID,但可能存在)。
其他共享资源,如内存映射文件或共享内存,在两者中仍然可用。内存映射文件的后续行为取决于用于创建映射的选项;MAP_PRIVATE表示两个进程具有独立的数据副本,而MAP_SHARED表示它们共享相同的数据副本,并且一个进程所做的更改将在另一个进程中可见。
然而,并非每个分叉程序都像上面描述的那样简单。例如,父进程可能已获得一些(建议性)锁定;这些锁定不会被子进程继承。父进程可能已经是多线程的;子进程只有一个执行线程 - 并且对子进程安全操作有限制。

POSIX规范中对于fork()的说明详细列出了差异:

fork()函数将创建一个新进程。新进程(子进程)除以下细节外应与调用进程(父进程)完全相同:

  • 子进程将拥有唯一的进程ID。

  • 子进程ID也不得与任何活动进程组ID匹配。

  • 子进程将拥有不同的父进程ID,该ID将是调用进程的进程ID。

  • 子进程将拥有父进程文件描述符的副本。每个子进程的文件描述符都将引用相应父进程的相同打开文件描述符的打开文件描述符。

  • 子进程将拥有父进程打开的目录流的副本。子进程中的每个打开目录流可能会与父进程对应的目录流共享目录流定位。

  • 子进程将拥有父进程消息目录描述符的副本。

  • 子进程的tms_utimetms_stimetms_cutimetms_cstime值将设置为0。

  • 距离闹钟信号触发的时间将被重置为0,并且如果有闹钟信号,则将其取消;请参见alarm。

  • [XSI] ⌦ 所有semadj值都必须清除。⌫

  • 父进程设置的文件锁不会被子进程继承。

  • 待处理信号集合将初始化为空集合。

  • [XSI] ⌦ 在fork()函数期间,间隔计时器将在子进程中重置。⌫

  • 在父进程中打开的任何信号量也将在子进程中打开。

  • [ML] ⌦ 子进程不会继承父进程通过调用mlockall()mlock()建立的任何地址空间内存锁。⌫

  • 在父进程中创建的内存映射将保留在子进程中。从父进程继承的MAP_PRIVATE映射也将是子进程中的MAP_PRIVATE映射,并且父进程在调用fork()之前对这些映射中的数据所做的任何修改都将对子进程可见。父进程在fork()返回后对MAP_PRIVATE映射中的数据所做的任何修改仅对父进程可见。子进程对MAP_PRIVATE映射中的数据所做的任何修改仅对子进程可见。

  • [PS] ⌦ 对于SCHED_FIFO和SCHED_RR调度策略,在fork()函数期间,子进程将继承父进程的策略和优先级设置。对于其他调度策略,在fork()上的策略和优先级设置是实现定义的。⌫

  • 由父进程创建的每个进程计时器都不会被子进程继承。

  • [MSG] ⌦ 子进程将拥有父进程的消息队列描述符的副本。子进程的每个消息描述符都将引用与父进程相应消息描述符相同的打开消息队列描述符

    大多数程序不受这些问题的影响,但多线程程序需要非常小心。值得阅读POSIX fork()定义中的理由部分。
    在内核中,系统管理上述定义中强调的所有问题。必须复制内存页面映射表。内核通常将(可写)内存页面标记为COW - 写时复制,因此在一个或另一个进程修改内存之前,它们可以访问相同的内存。这最大程度地减少了复制进程的成本;只有在修改时才使内存页面不同。然而,许多资源(例如文件描述符)必须被复制,因此fork()是一项相当昂贵的操作(虽然不像exec*()函数那样昂贵)。请注意,复制文件描述符会使两个描述符引用相同的打开文件描述符 - 有关文件描述符和打开文件描述符之间区别的讨论,请参见open()dup2() 系统调用。

0
简单来说,fork是由一个进程调用,但它返回两个进程。
为了更好地理解,进程只是Linux内核中的数据结构。该数据结构将具有应该下一步运行的代码的内存位置。因此,当进程正在执行时,所有其代码都会加载到RAM中,并且要执行的行的地址将存储在该数据结构中。每当Linux调度该进程时,它就会执行该特定行。在yield或中断(抢占)时,将在数据结构中更新要执行的行变量。让我们称该数据结构为task_struct。
当进程调用fork时,将创建其task_struct的副本。现在您可以看到将有两个进程在调度时执行相同的指令行。因此,在fork()调用之后的所有行都将由两个进程执行。
假设您想在父进程和子进程中运行不同的指令,则应检查fork的返回值并相应地执行这些指令。
这个视频提供了使用C程序对fork的简单解释- https://www.youtube.com/watch?v=W0AP03RG85E

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接