如何fork()的工作?(How does fork() work?)

2019-07-20 14:53发布

我真的新的分岔,什么是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;
}

Answer 1:

fork()复制过程,所以调用fork后,居然有你的程序运行的两个实例。

你怎么知道哪个进程是原始(父)之一,这是新的(孩子)呢?

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

这样,因为if (pid > 0)检查,父进程和子进程会产生不同的输出,你可以看到这里 (由@jxh在评论中提供)。



Answer 2:

为叉最简单的例子()

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

叉的返回值()。 返回值-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
}

什么是比父进程的子进程有什么不同?

  • 当子进程结束,但反之则不然父通过信号通知。
  • 孩子不继承挂起信号或定时器报警。 有关完整列表,请参阅叉()
  • 在这里,进程ID可以通过GETPID返回()。 父进程ID可以通过getppid返回()。

现在,让我们想象你的程序代码

pid_t pid;
pid = fork();

现在OS使地址空间的两个相同副本,一个家长和其他的孩子。

父母和子女有过程开始其执行系统调用fork之后向右()。 由于这两种方法具有相同的,但是独立的地址空间中,fork()调用之前初始化这些变量有两个地址空间相同的值。 每个进程都有自己的地址空间,所以任何的修改将是相互独立的。 如果父改变其变量的值,修改只会影响在父进程的地址空间中的变量。 通过创建叉其他地址空间()调用sysem不会,即使他们有相同的变量名的影响。

这里父pid是非零,它调用函数ParentProcess()。 在另一方面,孩子有一个零PID和如下所示调用子进程():

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

欲了解更多信息请参考以下链接



Answer 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()调用和下一个打印输出。



Answer 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 ,则该数据可用于既家长和孩子,都可以读取缓冲的数据,而不影响其他,这也将看到相同的数据。另外,一旦缓冲的数据被读取时,如果父母再读取缓冲区满了,移动当前父都和孩子,所以孩子不会再看到父母只是读取数据文件的位置(但如果孩子还读取数据块,母公司将看不到),这可能会造成混淆,因此,它通常是一个好主意,以确保没有任何悬而未决的标准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_cutimetms_cstime应设置为0。

  • 左,直到警报时钟信号的时间应复位到零,并且报警,如果有的话,将被取消; 看到报警。

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

  • 由父进程设置文件锁定,不得由子进程继承。

  • 该组的子进程挂起信号将被初始化为空集。

  • [XSI]⌦间隔计时器应在子进程复位。 ⌫

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

  • [ML]⌦儿童过程中不得通过调用由继承父进程建立的任何地址空间内存锁mlockall()mlock()

  • 在父创建内存映射应在子进程被保留。 继承自父MAP_PRIVATE映射也应在子MAP_PRIVATE映射,并调用之前的任何修改,在由父作出这些映射的数据fork()应使儿童可见的。 在后由父制成MAP_PRIVATE映射数据的任何修改fork()返回应仅到母体是可见的。 修改由儿童作出MAP_PRIVATE映射的数据只能给孩子可见。

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

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

  • [MSG]⌦子进程都有自己的父的消息队列描述符的副本。 每个儿童的信息的描述符应当是指相同的开放式消息队列描述作为父的相应的消息的描述符。 ⌫

  • 没有异步输入或异步输出操作由子进程继承。 任何使用由父创建异步控制块的生成未定义的行为。

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

  • 当应用程序调用fork()从信号处理程序,并通过任何注册叉处理程序pthread_atfork()调用一个函数不是异步信号安全的,行为是不确定的。

  • [OB TRC TRI]⌦如果跟踪选项和跟踪继承选项都支持:

    如果主叫过程在有设置为POSIX_TRACE_INHERITED其继承策略跟踪流被跟踪,子进程应被跟踪到,微量流,子进程将继承跟踪事件名字的父母的映射跟踪事件类型标识符。 如果在呼叫过程中被跟踪的跟踪流有其继承策略设置为POSIX_TRACE_CLOSE_FOR_CHILD,子进程不得追查到该跟踪流。 继承策略由所涉及的呼叫建立posix_trace_attr_setinherited()函数。 ⌫

  • [OB TRC]⌦如果跟踪选项支持,但不支持跟踪继承选项:

    子进程不得被跟踪到任何它的父进程的跟踪流。 ⌫

  • [OB TRC]⌦如果跟踪选项是支持的,一个轨迹控制器进程的子进程不得控制由其父进程控制的跟踪流。 ⌫

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

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

    由POSIX.1-2008定义的所有其它工艺特征应在父和子进程一样。 的过程特性不受POSIX.1-2008定义的继承是由POSIX.1-2008未指定的。

    fork()父和子进程应能够任意一方终止之前独立执行的。

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

在内核中,系统管理上述定义强调的所有问题。 内存页映射表必须被复制。 内核通常标记(写的)内存页的COW - 写时拷贝 - 这样,直到一个或另一个进程修改存储器,他们可以访问相同的内存。 这最小化复制方法的成本; 当他们修改的内存页仅取得明显。 很多资源,但是,如文件描述符,必须复制,因此fork()是一个相当昂贵的操作(尽管由于不那么贵exec*()函数)。 需要注意的是复制一个文件描述符叶既描述指的是同一个打开的文件描述-看到open()dup2()系统调用的文件描述符和打开文件描述之间的区别的讨论。



文章来源: How does fork() work?
标签: c fork