12.9 线程与fork
当一个线程调用函数fork的时候,整个进程地址空间会被拷贝到子进程中,在8.3节中有提到copy-on-write.子进程是一个与父进程完全不同的进程,但是如果父进程和子进程都没有对内存内容进行修改,那么该内存页就可以在父进程与子进程之间进行共享。
通过继承父进程的整个地址空间,子进程也会继承父进程每个互斥锁,读写锁以及条件变量的状态,如果父进程包含了多个线程,而且在fork函数返回之后并不会立即调用exec的话,子进程就需要清除锁状态。
在fork后的子进程内部,只会出现一个线程,它是父进程中调用fork函数的线程的拷贝。如果父进程中任何线程锁定了锁,相同的锁在子进程中也会处于锁定状态,问题是子进程并没有包含锁定锁的线程的拷贝,因此子进程没有办法知道哪一个锁需要锁定以及哪一个锁需要解除锁定。
上述问题可以通过如下方法避免:在fork之后调用函数exec,在这种情况下,老的地址空间将被抛弃,因此锁定状态并不重要。然而,这种方法并不总是可行的,如果子进程需要继续运行,那么我们就需要使用一个不同的策略。
为了避免在一个多线程进程中不一致的状态,POSIX.1指出在fork返回之后到exec函数之前的时间内只能调用异步信号安全的函数。这限制了子进程在调用exec之前可以做的事情,但是并不能解决子进程中锁状态的问题。
为了清除锁状态,我们可以建立fork handler来进行处理。
#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
Returns: 0 if OK, error number on failure.
使用函数pthread_atfork,我们可以建立起三个函数来帮组清除锁的锁定状态。prepare函数在父进程调用函数fork创建子进程之前被父进程调用,该fork handler的作用是获取父进程定义的所有锁。parent fork handler是在父进程fork了子进程但是fork函数还没有返回之前由父进程调用执行的,该fork handler的作用是解除所有prepare fork handler获取到的锁的锁定状态;child fork handler在子进程中fork函数返回之前被调用,就像parent fork handler一样,child fork handler必须释放所有prepare fork handler获取到的锁。
注意,这些锁并没有被锁定一次,解锁两次,因为在子进程被创建的时候,它获取到了父进程定义的锁的所有状态,因为prepare锁定了所有锁,父进程和子进程会在相同的内存内容下开始运行,当父进程以及子进程分别解除它们锁的拷贝的锁定状态的时候,新的内存空间将被分配给子进程,并且父进程的内存内容将被拷贝到子进程(copy-on-write),所以看起来就是父进程锁定了父进程以及子进程的所有的锁,然后父进程和子进程分别解除两份处于不同地址空间的锁的锁定状态,就像执行了如下的一个序列:
- 父进程锁定所有的锁;
- 子进程锁定所有的锁;
- 父进程释放锁;
- 子进程释放锁;
我们可以调用函数pthread_atfork多次,从而可以创建多个fork handler的集合,如果我们不需要使用其中任何一个handlers,可以传入一个null指针即可,这并不会产生什么问题。当多个fork handlers被调用的时候,handlers被调用的顺序是不一样的,parent以及child fork handlers按照它们被注册的顺序进行调用,然而prepare函数会以它们被注册顺序的反序被调用。这一顺序允许多个模块注册它们自己的fork handlers并且保持锁定的层次结构。
举例来讲,假设模块A调用模块B中的函数,同时两个模块都有各自的锁,如果锁定层次是A在B之前,模块B必须在模块A之前安装fork handlers.当父进程调用函数fork的时候,如下步骤将会被执行,假设子进程在父进程之前开始运行:
- 模块A中的prepare fork handler被调用来获取模块A的锁;
- 模块B中的prepare fork handler被调用来获取模块B的锁;
- 子进程被创建;
- 模块B的child fork handler被调用来释放子进程中所有模块B的锁;
- 模块B的child fork handler被调用来释放子进程中所有模块A的锁;
- fork函数返回到子进程;
- 模块B的parent fork handler被调用来释放所有模块B的锁;
- 模块A的parent fork handler被调用来释放所有模块A的锁;
- fork函数返回到父进程;
如果使用fork handlers来清除锁的状态的话,条件变量的状态需要如何清除呢?在一些实现上,条件变量可能并不需要任何清除工作,然而,使用锁作为条件变量的一部分的实现需要执行清除工作,但是问题是没有提供接口供我们实现清除工作,如果锁被嵌入到了条件变量的数据结构中,那么我们在调用fork函数以后不能在使用条件变量了(只是在子进程中由这一限制吧???),因为没有可移植的接口来清除其状态,另一方面,如果实现使用了一个全局锁来保护条件变量数据结构。那么实现本身可以在fork函数库中实现清理工作,然而,应用程序不应该依赖与这样的细节。
Example
图12.17中的程序阐述了函数pthread_atfork以及fork handler的使用:
#include "apue.h"
#include <pthread.h>
pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;
void prepare(void)
{
int err;
printf("prepare locks ...\n");
if((err = pthread_mutex_lock(&lock1)) != 0)
err_cont(err, "can't lock lock1 in prepare handler");
if((err = pthread_mutex_lock(&lock2)) != 0)
err_cont(err, "can't lock lock2 in prepare handler");
}
void parent(void)
{
int err;
printf("parent unlocking locks ...\n");
if((err = pthread_mutex_unlock(&lock1)) != 0)
err_cont(err, "can't unlock lock1 in parent handler");
if((err = pthread_mutex_unlock(&lock2)) != 0)
err_cont(err, "can't unlock lock2 in parent handler");
}
void child(void)
{
int err;
printf("child unlocking locks ...\n");
if((err = pthread_mutex_unlock(&lock1)) != 0)
err_cont(err, "can't unlock lock1 in child handler");
if((err = pthread_mutex_unlock(&lock2)) != 0)
err_cont(err, "can't unlock lock2 in child handler");
}
void *thr_fn(void *arg)
{
printf("thread started ... \n");
pause();
return (0);
}
int main(void)
{
int err;
pid_t pid;
pthread_t tid;
if((err = pthread_atfork(prepare, parent, child)) != 0)
err_exit(err, "can't install fork handlers");
if((err = pthread_create(&tid, NULL, thr_fn, 0)) != 0)
err_exit(err, "can't create thread");
sleep(2);
printf("parent about to fork...\n");
if((pid = fork()) < 0)
err_quit("fork failed");
else if(pid == 0)
printf("child returned from fork\n");
else
printf("parent returned from fork\n");
exit(0);
}
图12.17 pthread_atfork example
运行效果如下所示:
yuekunhu@debian:~/APUE/chapter12$ ./12_17.exe
thread started ...
parent about to fork...
prepare locks ...
parent unlocking locks ...
parent returned from fork
child unlocking locks ...
child returned from fork
yuekunhu@debian:~/APUE/chapter12$
虽然pthread_atfork机制想要解决fork之后锁状态的问题,但是由于一些缺陷导致该机制只能在受限的环境下使用:
- 没有办法重新初始化复杂同步对象的状态,比如说条件变量以及barriers;
- 一些实现对于互斥锁的错误检测机制在子进程尝试解锁一个其父进程锁定的互斥锁的时候会产生一个错误;
- 递归锁不能在child fork handler中清除状态,因为没有办法知道递归锁被锁定的次数;
- 如果子进程只允许调用异步信号安全函数,那么child fork handler都不能用于清除同步对象,因为这些函数全部都不是异步信号安全的。实际上可能出现这样的情况,同步对象在线程调用fork的时候可能处于中间状态,但是同步对象的清除工作要求其处于一致状态。
- 如果应用程序在信号处理函数中调用fork函数(这是合法的,因为fork函数是异步信号安全的),那么pthread_atfork注册的fork handlers只能调用异步信号安全函数,否则的话其结果是未定义的。