19

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

4 回答 4

28

fork()重复该过程,因此在调用 fork 之后实际上有 2 个程序实例正在运行。

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

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

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

于 2013-02-27T01:39:34.693 回答
28

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()
  • 这里进程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

有关更多信息,请参阅此链接

于 2015-12-17T17:14:22.743 回答
4

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()调用后继续,然后打印其输出。

于 2015-12-14T14:32:57.793 回答
3

在最简单的情况下,它的行为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 意味着它们共享相同的数据副本,并且一个进程所做的更改将在另一个进程中可见。

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

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

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

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

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

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

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

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

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

  • tms_utimetms_stimetms_cutime和的子进程值tms_cstime应设置为 0。

  • 距闹钟信号发出的剩余时间应归零,如有报警,应取消;见报警。

  • [XSI] ⌦ 清除所有 semadj 值。⌫</p>

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

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

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

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

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

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

  • [PS] ⌦ 对于 SCHED_FIFO 和 SCHED_RR 调度策略,子进程在一个fork()函数期间要继承父进程的策略和优先级设置。对于其他调度策略,策略和优先级设置fork()由实现定义。⌫</p>

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

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

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

  • 应使用单个线程创建进程。如果多线程进程调用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()函数来设置的。⌫</p>

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

    子进程不应被跟踪到其父进程的任何跟踪流中。⌫</p>

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

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

  • 【TCT】子进程单线程的CPU时钟初始值要设置为零。⌫</p>

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

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

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

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

于 2015-12-19T06:18:53.853 回答