当一个线程分叉时会发生什么?

15

我知道在线程中调用 fork() sys_call 是个坏主意。然而,如果一个线程使用 fork() 创建了一个新进程会发生什么呢?

新进程将会是创建该线程的主线程的子进程。我认为是这样。

如果它的父进程先结束,那么新进程将会被附加到 init 进程上。其父进程是主线程,而不是创建它的线程。

如果我有误请指出。

#include <stdio.h>
#include <pthread.h>

int main () 
{
     thread_t pid;
     pthread_create(&(pid), NULL, &(f),NULL);
     pthread_join(tid, NULL);
     return 0;
}

void* f()
{
     int i;
     i = fork();

     if (i < 0) {
         // handle error
     } else if (i == 0) // son process
     {
          // Do something;
     } else {
          // Do something;
     }
 }
5个回答

28
新的进程将是创建该线程的主线程的子进程。我想是这样的。 fork 创建一个新的进程。进程的父进程是另一个进程,而不是线程。所以新进程的父进程是旧进程。
请注意,子进程只有一个线程,因为fork只复制调用fork的线程的(堆栈)。 (这并非完全正确:整个内存都被复制,但子进程只有一个活动线程。)
如果父进程先完成,则会向子进程发送SIGHUP信号。如果子进程不因SIGHUP退出,则会将其附加到init进程。有关SIGHUP的更多信息,请参见nohupsignal(7)的手册页。
其父进程是主线程,而不是创建它的线程。
一个进程的父进程是一个进程,而不是特定的线程,因此说主线程或子线程是父进程是没有意义的。整个进程都是父进程。
最后提醒一点:混合使用线程和fork必须谨慎处理。其中一些问题在这里讨论。

3
只有在 POSIX 标准下,父进程是一个控制进程时才会发送 SIGHUB 信号。子进程接收 SIGHUB 信号的情况很少见。 - P.P
@Klas Lindback,如果由父进程/线程创建的子进程先完成其工作,该如何通知父进程/线程?此外,在分叉后,如果我们调用exec来运行某个命令,并在完成命令后销毁子进程,那么子进程如何向创建它的线程/父进程发送通知呢? - y_159
1
@y_159 父进程不会被通知。相反,父进程需要监视子进程,可以通过从管道中读取或等待退出代码来实现。 - Ichthyo
1
@y_159 而且 exec 替换 了当前进程。如果这个当前进程是由 fork 创建的,那么父进程就像“继承”了启动的其他可执行文件作为子进程。也就是说,如果子进程打开管道与其父进程连接到 STDIN 和 STOUT,当你使用 exec 时,父进程就通过这些管道连接到新可执行文件的 STDIN 和 STDOUT。请参考这个 SO 回答中的非常好的例子:https://dev59.com/6XRB5IYBdhLWcg3w4bGv#479103 - Ichthyo

9

如果我错了,请纠正我。

好的 :)

由于fork()是POSIX系统调用,因此其行为已经定义明确:

将创建一个带有单个线程的进程。如果多线程进程调用fork(),则新进程将包含调用线程及其整个地址空间的副本,可能包括互斥锁和其他资源的状态。因此,为避免错误,子进程只能执行异步信号安全操作,直到调用其中一个exec函数。

https://pubs.opengroup.org/onlinepubs/9699919799/functions/fork.html

分叉的子进程是其父进程的精确副本,但仅保留在父进程中调用fork()的线程,在子进程中成为新的主线程,直到您调用exec()

POSIX描述中“将创建一个带有单个线程的进程”是具有误导性的,因为实际上大多数实现都会真正创建父进程的完全副本,因此所有其他线程及其内存也都被复制了,这意味着线程实际上是存在的,但它们无法再次运行,因为系统从不为它们分配任何CPU时间;它们实际上在内核线程调度器表中已经缺失。

更容易理解的概念是:

当父进程调用fork时,整个进程会在瞬间被冻结,然后被原子地复制,然后父进程被整体解冻,但在子进程中仅解冻调用fork的一个线程,其他所有内容都保持冻结状态。

这就是为什么在fork()exec()之间执行某些系统调用是不安全的,正如POSIX标准所指出的那样。理想情况下,您不应该做更多事情,只需关闭或复制文件描述符,设置或恢复信号处理程序,然后调用exec()


谢谢,我都忘了这个问题还存在。 - Tony
在早期版本的Linux中,线程是通过硬件实现的。因此,它要么存在,要么不存在。 - Алексей Неудачин

5
然而,如果线程使用fork()创建了一个新进程会发生什么?将通过复制调用线程的地址空间(而不是整个进程的地址空间)来创建一个新进程。通常被认为是一个坏主意,因为很难做到正确。 POSIX表示,子进程(在多线程程序中创建)只能调用异步信号安全函数,直到它调用exec*函数之一。如果父进程先完成,则新进程通常由init进程继承。如果父进程是控制进程(例如shell),则POSIX要求:如果进程是控制进程,则必须向属于调用进程的前台进程组的每个进程发送SIGHUP信号。然而,这对大多数进程来说并不是真实的,因为大多数进程都不是控制进程。

它的父线程是主线程,而不是创建它的线程。

分叉子进程的父进程将始终是调用fork()的进程。因此,child process的PPID将是您程序的PID。


1
“复制调用线程的地址空间”:进程内的线程共享相同的地址空间,所以实际上fork()确实会复制整个进程的地址空间。关于异步信号安全函数的部分也是不正确的:这只适用于vfork(),而不是完全的fork()。 - Arnout
1
@Arnout POSIX的理由与您的说法不符。请参见http://pubs.opengroup.org/onlinepubs/009695399/functions/pthread_atfork.html,其中提到“在多线程程序中,fork()语义存在至少两个严重问题[...]建议使用fork()的程序在子进程中很快调用exec函数,从而重置所有状态。同时,只有一小部分异步信号安全库例程才能使用”。实际上,整个理由在这里都是相关的。 - P.P

2

问题源于fork(2)本身的行为。每当使用fork(2)创建一个新的子进程时,新进程会获得一个新的内存地址空间,但是内存中的所有内容都从旧进程中复制过来(虽然使用了写时复制技术,但语义上相同)。
如果在多线程环境下调用fork(2),则执行该调用的线程现在成为新进程中的主线程,而在父进程中运行的所有其他线程都已死亡。并且他们所做的一切都保持在调用fork(2)之前的状态。
现在想象一下,在调用fork(2)之前,这些其他线程正在愉快地工作,而几毫秒后它们就死了。如果这些现在已经死亡的线程所做的一些事情不应该保持原样怎么办?
让我给你举个例子。假设我们的主线程(将要调用fork(2)的线程)正在睡眠,而我们有很多其他线程正在愉快地进行一些工作。分配内存,写入数据,从中复制数据,写入文件,写入数据库等等。他们可能使用类似malloc(3)的函数来分配内存。实际上,malloc(3)在内部使用互斥锁以确保线程安全。这就是问题所在。
如果其中一个线程正在使用malloc(3)并在主线程调用fork(2)的确切时刻获得了互斥锁,那么在新的子进程中,该锁仍然被持有 - 被一个已经死亡的线程持有,他永远不会返回它。
新的子进程将不知道是否可以安全地使用malloc(3)。在最坏的情况下,它将调用malloc(3),并阻塞直到获取锁,但这永远不会发生,因为应该返回锁的线程已经死亡。而这只是malloc(3)。想想数据库驱动程序、文件处理库、网络库等所有可能存在的互斥锁和锁。
完整的解释可以通过这个链接进行查看。

0

Linux内核本身没有线程和进程之间的区别。当一个进程fork时,它指定了与父进程共享的内容(内存、打开的文件句柄等)。但这只是一组标志。线程和进程的概念是在内核实现之上应用的。

当然,大多数人通过libc调用内核,根据线程/进程的通用概念选择标志。

在操作系统级别上,fork线程与fork进程相同。这是UNIX实现之间的一个(棘手的)区别之一。例如,一些UNIX确实有线程的概念 - 然后他们最终面临的问题是:如果我fork一个进程,我是否要在新进程中复制所有线程?但对于Linux来说,线程和进程本质上是相同的。


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