除了通常关注的问题(如锁定共享数据)以外,当只有 fork() 线程处于运行状态时,还应根据 fork 子进程的操作来处理库。问题在于子进程中的唯一线程可能会尝试获取由未复制到子进程中的线程持有的锁定。
大多数程序不可能遇到此问题。从 fork() 返回后,大多数程序都会调用子进程中的 exec()。但是,如果程序在调用 exec() 之前必须在子进程中执行操作,或永远不会调用 exec(),则子进程可能会遇到死锁。每个库编写者都应提供安全的解决方案,尽管提供一个非 fork 安全的库不是一个很大的问题。
例如,假设当 T2 fork 新进程时,T1 在进行打印,且对 printf() 持有锁定。在子进程中,如果唯一的线程 (T2) 调用 printf(),则 T2 将快速死锁。
POSIX fork() 或 Solaris fork1() 函数仅复制用于调用 fork() 或 fork1() 的线程。如果调用 Solaris forkall() 来复制所有线程,则此问题不是要关注的问题。
但是,forkall() 可能会导致其他问题,使用时应小心。 例如,如果一个线程调用 forkall(),则将在子进程中复制对文件执行 I/O 的父线程。线程的两个副本都将继续对同一个文件执行 I/O,一个副本在父进程中,一个副本在子进程中,这将导致出现异常或文件损坏。
要防止在调用 fork1() 时出现死锁,请确保在执行 fork 时任何锁定都未被持有。防止死锁的最有效的方式就是让 fork 线程获取可能由子进程使用的所有锁定。由于无法获取对 printf() 的所有锁定(由于 printf() 由 libc 所有),因此必须确保在使用 fork() 时没有使用 printf()。
要管理库中的锁定,应执行以下操作:
确定库使用的所有锁定。
确定库所使用锁定的锁定顺序。如果没有使用严格的锁定顺序,则必须谨慎管理锁定获取。
安排在 fork 时获取所有锁定。
在以下示例中,库使用的锁定列表为 {L1,...Ln}。这些锁定的锁定顺序也为 L1...Ln。
mutex_lock(L1); mutex_lock(L2); fork1(...); mutex_unlock(L1); mutex_unlock(L2);
使用 Solaris 线程或 POSIX 线程时,应该在库的 .init() 部分中添加对 pthread_atfork(f1, f2, f3) 的调用。f1()、f2()、f3() 定义如下:
f1() /* This is executed just before the process forks. */ { mutex_lock(L1); | mutex_lock(...); | -- ordered in lock order mutex_lock(Ln); | } V f2() /* This is executed in the child after the process forks. */ { mutex_unlock(L1); mutex_unlock(...); mutex_unlock(Ln); } f3() /* This is executed in the parent after the process forks. */ { mutex_unlock(L1); mutex_unlock(...); mutex_unlock(Ln); }
标准 vfork(2) 函数在多线程程序中并不安全。vfork(2)(与 fork1(2) ¤@样)仅复制子进程中的调用线程。就像在非线程实现中一样,vfork() 不会复制子进程的地址空间。
请注意,子进程中的线程在调用 exec(2) 之前不会更改内存。vfork() 为子进程提供父地址空间。子进程调用 exec() 或退出之后,父进程将取回其地址空间。子进程不得更改父进程的状态。
例如,如果在对 vfork() 的调用与对 exec() 的调用之间创建新的线程,则会出现灾难性问题。
使用 Fork-One 模型时,请使用 pthread_atfork() 来防止死锁。
#include <pthread.h> int pthread_atfork(void (*prepare) (void), void (*parent) (void), void (*child) (void) );
pthread_atfork() 函数声明了在调用 fork() 的线程的上下文中的 fork() 前后调用的 fork() 处理程序。
在 fork() 启动前调用 prepare 处理程序。
在父进程中返回 fork() 后调用 parent 处理程序。
在子进程中返回 fork() 后调用 child 处理程序。
可以将任何处理程序参数都设置为 NULL。对 pthread_atfork() 进行连续调用的顺序非常重要。
例如,prepare 处理程序可能会获取所有需要的互斥。然后,parent 和 child 处理程序可能会释放互斥。获取所有需要的互斥的 prepare 处理程序可确保在对进程执行 fork 之前,所有相关的锁定都由调用 fork 函数的线程持有。此技术可防止子进程中出现死锁。