【问题标题】:How does fork() work?fork() 是如何工作的?
【发布时间】:2013-02-12 16:24:41
【问题描述】:

我对分叉真的很陌生,这段代码中的 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;
}

【问题讨论】:

  • 您是否尝试过实际编译和运行它?你认为会发生什么?
  • 你可以看这里:ideone.com/DasYqa
  • 确保在printf() 语句的末尾添加换行符;否则,什么都不能保证出现。你应该#include &lt;sys/wait.h&gt; 声明wait();你可能不需要明确地#include &lt;sys/types.h&gt;

标签: c fork


【解决方案1】:

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=在子进程中; positive = 在父进程中(返回值为子进程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()
  • 这里的进程ID可以通过getpid()返回。 getppid()可以返回父进程id。

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

pid_t pid;
pid = fork();

现在操作系统制作了两个相同的地址空间副本,一个用于父级,另一个用于子级。

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

这里的父 pid 是非零的,它调用函数 ParentProcess()。另一方面,子进程的 pid 为零,它调用 ChildProcess(),如下所示:

在您的代码父进程调用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

欲了解更多信息refer this link

【讨论】:

  • 您似乎从csl.mtu.edu/cs4411.ck/www/NOTES/process/fork/create.html 复制了一些图片。请注意,这种复制需要署名;我建议您阅读我们的剽窃政策stackoverflow.com/help/referencing
  • 是的,我同意你的建议。我将很快更新答案正确参考。谢谢建议
  • 够了还是我还需要改进答案?欢迎所有建议:-)
  • 请务必提及您从该链接获得了图像。如果您从其他地方复制图像、代码或文本,那么您总是需要与归属地链接。
  • 我刚刚按照你的建议插入了..我以后会注意的
【解决方案2】:

在最简单的情况下,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 的数据缓冲区中,那么该数据对父子双方都可用,并且双方都可以读取缓冲的数据而不会影响对方,对方也会看到相同的数据。另一方面,一旦读取了缓冲的数据,如果父级读取另一个缓冲区已满,这会移动父级和子级的当前文件位置,因此子级将看不到父级刚刚读取的数据(但如果子级也读取了一个数据块,则父级将看不到那)。这可能会造成混淆。因此,通常最好确保在分叉之前没有待处理的标准 I/O — fflush(0) 是一种方法。

在代码片段中,assert(original == getppid() || getppid() == 1); 允许在子进程执行语句时,父进程可能已经退出,在这种情况下子进程将被系统进程继承——这通常有 PID 1(我知道没有 POSIX 系统,其中孤儿由不同的 PID 继承,但可能有一个)。

其他共享资源(例如内存映射文件或共享内存)在两者中继续可用。内存映射文件的后续行为取决于用于创建映射的选项; MAP_PRIVATE 表示两个进程拥有独立的数据副本,MAP_SHARED 表示它们共享相同的数据副本,一个进程所做的更改将在另一个进程中可见。

然而,并不是每个分叉的程序都像目前所描述的那样简单。例如,父进程可能已经获得了一些(建议)锁;孩子不会继承这些锁。父级可能是多线程的;子进程有一个执行线程——并且对子进程可以安全执行的操作有一些限制。

fork() 的 POSIX 规范详细说明了差异:

fork() 函数将创建一个新进程。新进程(子进程)应是调用进程(父进程)的精确副本,以下详述除外:

  • 子进程应具有唯一的进程 ID。

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

  • 子进程应该有一个不同的父进程ID,它应该是调用进程的进程ID。

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

  • 子进程应拥有自己的父进程打开目录流的副本。子进程中每个打开的目录流都可以与父进程对应的目录流共享目录流定位。

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

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

  • 距离闹钟信号的剩余时间应重置为零,如果有闹钟,则应取消;看到警报。

  • [XSI] ⌦ 所有 semadj 值应被清除。 ⌫

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

  • 子进程的未决信号集应初始化为空集。

  • [XSI] ⌦ 间隔计时器应在子进程中重置。 ⌫

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

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

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

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

  • 父进程创建的每进程定时器不应被子进程继承。

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

  • 子进程不应继承任何异步输入或异步输出操作。对父级创建的异步控制块的任何使用都会产生未定义的行为。

  • 应使用单个线程创建进程。如果多线程进程调用fork(),则新进程应包含调用线程及其整个地址空间的副本,可能包括互斥锁和其他资源的状态。因此,为避免错误,子进程只能执行异步信号安全操作,直到调用其中一个 exec 函数。可以通过pthread_atfork() 函数建立分叉处理程序,以便在fork() 调用之间保持应用程序不变量。

  • 当应用程序从信号处理程序调用fork() 并且pthread_atfork() 注册的任何分叉处理程序调用非异步信号安全的函数时,行为未定义。

  • [OB TRC TRI] ⌦ 如果同时支持 Trace 选项和 Trace Inherit 选项:

    如果在其继承策略设置为 POSIX_TRACE_INHERITED 的跟踪流中跟踪调用进程,则应将子进程跟踪到该跟踪流中,并且子进程应继承父进程的跟踪事件名称到跟踪事件的映射类型标识符。如果正在跟踪调用进程的跟踪流将其继承策略设置为 POSIX_TRACE_CLOSE_FOR_CHILD,则不应将子进程跟踪到该跟踪流中。通过调用posix_trace_attr_setinherited() 函数来设置继承策略。 ⌫

  • [OB TRC] ⌦ 如果支持 Trace 选项,但不支持 Trace Inherit 选项:

    不应将子进程跟踪到其父进程的任何跟踪流中。 ⌫

  • [OB TRC] ⌦ 如果支持 Trace 选项,则跟踪控制器进程的子进程不应控制由其父进程控制的跟踪流。 ⌫

  • [CPT] ⌦ 子进程的 CPU 时间时钟的初始值应设置为零。 ⌫

  • [TCT]子进程单线程的CPU时间时钟初始值应设置为零。⌫

    POSIX.1-2008 定义的所有其他进程特征在父进程和子进程中应相同。 POSIX.1-2008 未定义的进程特征的继承在 POSIX.1-2008 中未指定。

    fork() 之后,父进程和子进程都应该能够在任一进程终止之前独立执行。

这些问题大部分不会影响大多数程序,但是 fork 的多线程程序需要非常小心。 fork() 的 POSIX 定义的基本原理部分值得一读。

在内核内部,系统管理上述定义中突出显示的所有问题。内存页映射表必须被复制。内核通常会将(可写的)内存页面标记为 COW(写时复制),以便在一个或另一个进程修改内存之前,它们可以访问相同的内存。这最大限度地降低了复制过程的成本;内存页面只有在修改时才会变得不同。但是,许多资源(例如文件描述符)必须被复制,因此fork() 是一项相当昂贵的操作(尽管没有exec*() 函数那么昂贵)。请注意,复制文件描述符会使两个描述符都引用相同的打开文件描述 - 请参阅 open()dup2() 系统调用以讨论文件描述符和打开文件描述之间的区别。

【讨论】:

    【解决方案3】:

    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() 复制了该进程,因此在调用 fork 之后实际上有 2 个您的程序实例正在运行。

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

      在父进程中,从fork()返回子进程的PID(将是一个正整数)。这就是if (pid &gt; 0) { /* PARENT */ } 代码有效的原因。在子进程中,fork() 只是返回0

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

      【讨论】:

      • 该代码中还有第三个分支(未处理)。如果 fork() 失败了怎么办? =P
      • @gEdringer 不是您不知道,而是其他人可能不知道 - 您可能希望使用 perror(fork) 表示由 fork 返回的负值指示错误。 man perror
      猜你喜欢
      • 2013-12-22
      • 2017-02-06
      • 2013-12-09
      • 2019-08-25
      • 2011-08-26
      • 2011-11-28
      • 2023-04-11
      • 1970-01-01
      相关资源
      最近更新 更多