当线程调用fork时,就为子进程创建了整个进程地址空间的副本。回忆http://www.cnblogs.com/nufangrensheng/p/3509492.html中讨论的写时复制,子进程与父进程是完全不同的进程,只要两者都没有对内存作出改动,父进程和子进程之间还可以共享内存页的副本。
子进程通过继承整个地址空间的副本,也从父进程那里继承了所有互斥量、读写锁和条件变量的状态。如果父进程包含多个线程,子进程在fork返回以后,如果紧接着不是马上调用exec的话,就需要清理锁状态。
在子进程内部只存在一个线程,它是由父进程中调用fork的线程的副本构成的。如果父进程中的线程占有锁,子进程同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法知道它占有了哪些锁并且需要释放哪些锁。
如果子进程从fork返回以后马上调用某个exec函数,就可以避免这样的问题。这种情况下,老的地址空间被丢弃,所以锁的状态无关紧要。但如果子进程需要继续做处理工作的话,这种方法就行不通,还需要使用其他的策略。
要清除锁状态,可以通过调用pthread_atfork函数建立fork处理程序。
#include <pthread.h> int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void)); 返回值:若成功则返回0,否则返回错误编号
用pthread_atfork函数最多可以安装三个帮助清理锁的函数。prepare fork处理程序由父进程在fork创建子进程前调用,这个fork处理程序的任务是获取父进程定义的所有锁。parent fork处理程序是在fork创建了子进程以后,但在fork返回之前在父进程环境中调用的,这个fork处理程序的任务是对prepare fork处理程序获得的所有锁进行解锁。child fork处理程序在fork返回之前在子进程环境中调用,与parent fork处理程序一样,child fork处理程序也必须释放prepare fork处理程序获得的所有锁。
注意不会出现加锁一次解锁两次的情况,虽然看起来也许会出现。当子进程地址空间创建的时候,它得到了父进程定义的所有锁的副本。因为prepare fork处理程序获取所有的锁,父进程中的内存和子进程中的内存内容在开始的时候是相同的。当父进程和子进程对他们的锁的副本进行解锁的时候,新的内存是分配给子进程的,父进程的内存内容被复制到子进程的内存中(写时复制),所以就会陷入这样的假象,看起来父进程对它所有的副本进行了加锁,子进程对它所有的副本进行了加锁。父进程和子进程对在不同内存位置的重复的锁都进行了解锁操作,就好像出现了下列的时间序列:
(1)父进程获得所有的锁。
(2)子进程获得所有的锁。
(3)父进程释放它的锁。
(4)子进程释放它的锁。
可以多次调用pthread_atfork函数从而设置多套fork处理程序。如果不需要使用其中某个处理程序,可以给特定的处理程序参数传入空指针,这样它们就不会起任何作用。使用多个fork处理程序时,处理程序的调用顺序并不相同。parent和child fork处理程序是以它们注册时的顺序进行调用的,而prepare fork处理程序的调用顺序与它们注册时的顺序相反。这样可以允许多个模块注册它们自己的fork处理程序,并且保持锁的层次。
例如,假设模块A调用模块B中的函数,而且每个模块有自己的一套锁。如果锁的层次是A在B之前,模块B必须在模块A之前设置fork处理程序。当父进程调用fork时,就会执行以下的步骤,假设子进程在父进程之前运行。
(1)调用模块A的prepare fork处理程序获取模块A的所有锁。
(2)调用模块B的prepare fork处理程序获取模块B的所有锁。
(3)创建子进程。
(4)调用模块B中的child fork处理程序释放子进程中模块B的所有锁。
(5)调用模块A中的child fork处理程序释放子进程中模块A的所有锁。
(6)fork函数返回到子进程。
(7)调用模块B中的parent fork处理程序释放父进程中模块B的所有锁。
(8)调用模块A中的parent fork处理程序释放父进程中模块A的所有锁。
(9)fork函数返回到父进程。
如果fork处理程序是为了清理锁状态,那么又由谁来负责清理条件变量的状态呢?在有些操作系统的实现中,条件变量可能并不需要做任何清理。但是有些操作系统实现把锁作为条件变量实现的一部分,这种情况下的条件变量就需要清理。问题是目前不存在这样的接口,如果锁是嵌入到条件变量的数据结构中的,那么在调用fork之后就不能使用条件变量,因为还没有可移植的方法对其进行状态清理。另外,如果操作系统的实现是使用全局锁保护进程中所有的条件变量数据结构,那么操作系统实现本身可以在fork库例程中做清理锁的工作,但是应用程序不应该依赖操作系统实现中这样的细节。
实例
程序清单12-7中的程序描述了如何使用pthread_atfork和fork处理程序。
程序清单12-7 pthread_atfork实例
#include "apue.h" #include <pthread.h> pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER; void prepare(void) { printf("preparing locks... "); pthread_mutex_lock(&lock1); pthread_mutex_lock(&lock2); } void parent(void) { printf("parent unlocking locks... "); pthread_mutex_unlock(&lock1); pthread_mutex_unlock(&lock2); } void child(void) { printf("child unlocking locks... "); pthread_mutex_unlock(&lock1); pthread_mutex_unlock(&lock2); } void * thr_fn(void *arg) { printf("thread started... "); pause(); return(0); } int main(void) { int err; pid_t pid; pthread_t tid; #if defined(BSD) || defined(MACOS) printf("pthread_atfork is unsupported "); #else if((err = pthread_atfork(prepare, parent, child)) != 0) err_exit(err, "can't install fork handlers"); err = pthread_create(&tid, NULL, thr_fn, 0); if(err != 0) err_exit(err, "can't create thread"); sleep(2); printf("parent about to fork... "); if((pid = fork()) < 0) err_quit("fork failed"); else if(pid == 0) /* child */ printf("child returned from fork "); else /* parent */ printf("parent returned from fork "); #endif exit(0); }
程序中定义了两个互斥量,lock1和lock2,prepare fork处理程序获取这两把锁,child fork处理程序在子进程环境中释放锁,parent fork处理程序在父进程中释放锁。
运行该程序,得到如下输出:
可以看出,prepare fork处理程序在调用fork以后运行,child fork处理程序在fork调用返回到子进程之前运行,parent fork处理程序在fork调用返回给父进程前运行。
本篇博文内容摘自《UNIX环境高级编程》(第二版),仅作个人学习记录所用。关于本书可参考:http://www.apuebook.com/。