fork()如何工作?
我真的很新的分叉,这个代码中的pid做了什么? 有人可以解释一下X行和Y行的内容吗?
#include #include #include #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 0) { wait(NULL); for (i = 0; i < SIZE; i++) printf("PARENT: %d ",nums[i]); /* LINE Y */ } return 0; }
fork()
复制了进程,因此在调用fork之后,实际上有2个程序实例正在运行。
你怎么知道哪个进程是原始进程(父进程),哪个是新进程(父进程)?
在父进程中,从fork()
返回子进程的PID(将是一个正整数fork()
。 这就是if (pid > 0) { /* PARENT */ }
代码有效的原因。 在子进程中, fork()
只返回0
。
因此,由于if (pid > 0)
检查,父进程和子进程将产生不同的输出,您可以在此处看到(由注释中的@jxh提供)。
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 }
子进程与父进程有什么不同?
- 子进程完成时通过信号通知父进程,反之则不然。
- 孩子不会inheritance未决信号或计时器警报。 有关完整列表,请参阅fork()
- 这里的进程id可以由getpid()返回。 getppid()可以返回父进程id。
现在让我们可视化您的程序代码
pid_t pid; pid = fork();
现在OS制作两个相同的地址空间副本,一个用于父级,另一个用于子级。
父系统和子进程都在系统调用fork()之后立即开始执行。 由于两个进程具有相同但独立的地址空间,因此在fork()调用之前初始化的那些变量在两个地址空间中具有相同的值。 每个进程都有自己的地址空间,因此任何修改都将独立于其他进程。 如果父级更改其变量的值,则修改将仅影响父进程的地址空间中的变量。 fork()sysem调用创建的其他地址空间即使具有相同的变量名也不会受到影响。
这里父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
有关更多信息, 请参阅此链接
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()
调用之后继续,然后打印其输出。
fork()
是一个创建进程的函数调用。 调用fork()
的进程称为父进程 ,新创建的进程称为子进程 。
从fork()
系统调用返回时 , 除了返回值pid
之外 ,这两个进程的用户级上下文具有相同的副本。
在父进程中 , pid
是Child process ID
(新创建的子进程的进程ID)。
在子进程中 , pid
为0
。
内核为fork()
执行以下操作序列。
- 它在进程表中为新进程分配一个槽。
- 它为子进程分配唯一的
ID
号。 - 它生成父进程上下文的逻辑副本。 由于进程的某些部分(例如文本区域)可以在进程之间共享,因此内核有时可以增加区域引用计数,而不是将区域复制到内存中的新物理位置,
- 它为与进程关联的文件递增文件和模式表计数器。
- 它将子进程的
ID
号返回给父进程 ,并将0
值返回给子进程 。
现在让我们看一下调用fork()
时代码中会发生什么
01: pid = fork(); 02: if (pid == 0) { 03: for (i = 0; i < SIZE; i++) { 04: nums[i] *= -i; 05: printf("CHILD: %d ",nums[i]); /* LINE X */ 06: } 07: } 08: else if (pid > 0) { 09: wait(NULL); 10: for (i = 0; i < SIZE; i++) 11: printf("PARENT: %d ",nums[i]); /* LINE Y */ 12: }
第01行:调用fork()
,创建子进程 。 fork()
返回,返回值存储在pid
。
[注意:由于OP代码中没有错误检查,将在后面讨论]
第02行:根据值0
检查pid
值。 请注意,此检查在父进程和新创建的子进程中完成 。 如上所述, 子进程中pid
值为0
, 父进程中的 child process ID
为0
。 因此,此条件检查在子进程中评估为True
,在父进程中评估为False
。 因此,在子进程中执行第03-07行 。
03-07号线:这些线路非常简单。 子进程的num[]
数组被更改( nums[i] *= -i;
),并使用printf("CHILD: %d ",nums[i]);
打印出来printf("CHILD: %d ",nums[i]);
。
这里要注意的是,正在打印的值是子进程的num[]
数组。 父进程的num[]
数组现在与之前相同。
这里有一个巧妙的技巧,称为写时复制 。 虽然这个问题没有被问到,但它仍然是一个有趣的读物。
第08行:现在在父进程中检查此行。 它不会像以前那样检查子进程 if
成功。 进程ID始终为正数,因此当父进程获得新创建的子进程的进程ID时, else if (pid > 0)
,它将始终通过测试else if (pid > 0)
,并输入该块。
[注意:它永远不会为0
因为0
保留。 在这里阅读。]
第09行:此行使父进程等待,直到子进程完成执行。 这就是您将在父进程的任何printf()
之前看到子进程的所有printf()
的原因。
第10-12行:这也是一个非常前进的for
循环,它打印num[]
数组的值。 请注意, 父进程的值不变。 因为之前的子进程已经改变了它,它拥有自己的数组num[]
的副本。
当fork()
失败时。
fork()
调用可能会失败。 在这种情况下,返回值为-1
。 这也应该为程序正确处理。
pid = fork(); if (pid == -1) perror("Fork failed");
一些内容取自“UNIX操作系统的设计 ”一书。
在最简单的情况下, 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);
允许孩子执行语句时父进程可能已退出的可能性,在这种情况下,子进程将由系统进程inheritance – 通常具有PID 1(我知道没有POSIX系统孤立的子进程)由不同的PIDinheritance,但可能有一个)。
其他共享资源(例如内存映射文件或共享内存)仍可在两者中使用。 内存映射文件的后续行为取决于用于创建映射的选项; MAP_PRIVATE表示两个进程具有数据的独立副本,MAP_SHARED表示它们共享相同的数据副本,并且一个进程所做的更改将在另一个进程中可见。
然而,并不是每个程序的分支都像目前为止描述的那样简单。 例如,父进程可能已经获得了一些(建议)锁; 这些锁不是由孩子inheritance的。 父母可能是multithreading的; 孩子有一个执行的线程 – 并且对孩子可以安全地做什么有限制。
fork()
的POSIX规范详细说明了差异:
fork()
函数将创建一个新进程。 新进程(子进程)应该是调用进程(父进程)的精确副本,除非详述如下:
子进程应具有唯一的进程ID。
子进程ID也不应与任何活动进程组ID匹配。
子进程应具有不同的父进程ID,该进程ID应该是调用进程的进程ID。
子进程应拥有自己父文件描述符的副本。 每个子文件描述符都应引用与父对应文件描述符相同的打开文件描述。
子进程应拥有自己父开放目录流的副本。 子进程中的每个打开的目录流可以与父对应的目录流共享目录流定位。
子进程应拥有自己父进程的消息目录描述符的副本。
tms_utime
,tms_stime
,tms_cutime
和tms_cstime
的子进程值应设置为0。闹钟信号应重置为零所需的时间,并取消闹钟(如果有的话); 看到警报。
[XSI]⌦所有semadj值都应被清除。 ⌫
父进程设置的文件锁不应由子进程inheritance。
应为子进程挂起的信号集初始化为空集。
[XSI]⌦间隔计时器应在子进程中重置。 ⌫
在父进程中打开的任何信号量也应在子进程中打开。
[ML] child子进程不得通过调用
mlockall()
或mlock()
inheritance父进程建立的任何地址空间内存锁。 ⌫在父进程中创建的内存映射应保留在子进程中。 从父节点inheritance的MAP_PRIVATE映射也应该是子节点中的MAP_PRIVATE映射,并且在调用
fork()
之前父节点对这些映射中的数据的任何修改都应对子节点可见。fork()
返回后父节点对MAP_PRIVATE映射中的数据所做的任何修改都只对父节点可见。 对子项进行的MAP_PRIVATE映射中的数据的修改应仅对子项可见。[PS]⌦对于SCHED_FIFO和SCHED_RR调度策略,子进程应在
fork()
函数期间inheritance父进程的策略和优先级设置。 对于其他调度策略,fork()
上的策略和优先级设置是实现定义的。 ⌫父进程创建的每进程计时器不应由子进程inheritance。
[MSG] child子进程应拥有自己的父进程的消息队列描述符副本。 子节点的每个消息描述符应引用与父节点的相应消息描述符相同的开放消息队列描述。 ⌫
子进程不应inheritance异步输入或异步输出操作。 父类创建的异步控制块的任何使用都会产生未定义的行为。
应使用单个线程创建进程。 如果multithreading进程调用
fork()
,则新进程应包含调用线程的副本及其整个地址空间,可能包括互斥锁和其他资源的状态。 因此,为了避免错误,子进程可能只执行异步信号安全操作,直到调用其中一个exec函数为止。 可以通过pthread_atfork()
函数建立fork处理程序,以便跨fork()
调用维护应用程序不变量。当应用程序从信号处理程序调用
fork()
并且由pthread_atfork()
注册的任何fork处理程序调用非异步信号安全的函数时,行为是未定义的。[OB TRC TRI]⌦如果同时支持Trace选项和Trace Inherit选项:
如果在其inheritance策略设置为POSIX_TRACE_INHERITED的跟踪流中跟踪调用进程,则应将子进程跟踪到该跟踪流,并且子进程应inheritance父进程的跟踪事件名称到跟踪事件类型标识符。 如果正在跟踪调用进程的跟踪流将其inheritance策略设置为POSIX_TRACE_CLOSE_FOR_CHILD,则不应将子进程跟踪到该跟踪流。 inheritance策略是通过调用
posix_trace_attr_setinherited()
函数来设置的。 ⌫[OB TRC]⌦如果支持Trace选项,但不支持Trace Inherit选项:
不应将子进程跟踪到其父进程的任何跟踪流。 ⌫
[OB TRC]⌦如果支持Trace选项,则跟踪控制器进程的子进程不应控制其父进程控制的跟踪流。 ⌫
[CPT] child子进程的CPU时间时钟的初始值应设置为零。 ⌫
[TCT]子进程单线程的CPU时间时钟的初始值应设置为零
POSIX.1-2008定义的所有其他过程特征在父过程和子过程中应相同。 POSIX.1-2008未指定POSIX.1-2008未定义的过程特性的inheritance。
在
fork()
,父进程和子进程都应该能够在任何一个终止之前独立执行。
大多数这些问题不会影响大多数程序,但fork的multithreading程序需要非常小心。 值得阅读fork()
的POSIX定义的Rationale部分。
在内核中,系统管理上面定义中突出显示的所有问题。 必须复制内存页映射表。 内核通常将(可写)内存页标记为COW – 写入时复制 – 这样,在一个或另一个进程修改内存之前,它们可以访问相同的内存。 这最大限度地降低了复制过程的成本; 内存页面只有在修改后才会有所不同。 但是,必须复制许多资源,例如文件描述符,因此fork()
是一项非常昂贵的操作(尽管不像exec*()
函数那样昂贵)。 请注意,复制文件描述符会使两个描述符引用相同的打开文件描述 – 请参阅open()
和dup2()
系统调用,以讨论文件描述符和打开文件描述之间的区别。