分类: 系统运维
2012-03-31 23:21:37
当一个线程调用fork时,整个进程地址空间被拷贝给子进程。回想8.3节的写时拷贝的讨论。子进程是一个和父进程完全不同的进程,而只要两者不对它们的内存内容进行修改,那么内存页的拷贝就可以在父进程和子进程之间共享。
通过继承地址空间的一份拷贝,子进程也从父进程继承了每个互斥体、读写锁和条件变量的状态。如果父进程由多个线程组成,那么子进程将需要清理锁状态,如果它不要在fork返回时立即调用exec。
在子进程里,只有一个线程存在。它是在父进程里调用fork的线程的一个拷贝。如果父进程里的线程握住了任何锁,在子进程这个锁也会被握住。问题是子进程并不包含握住这些锁的线程的拷贝,所以子进程没有办法知道哪些锁被握住并需要被解锁。
如果子进程在从fork函数返回后直接调用某个exec函数时可以避免这个问题。在这种情况下,老的地址空间被舍弃,所以锁状态无关紧要。然而,这不总是可能的,所以如果子进程需要继续运行,那么我们需要用另一种策略。
为了清理锁状态,我们可以建立分叉处理机,通过调用函数pthread_atfork。
有 了pthread_atfork,我们可以安装最多三个函数来帮助清理锁。prepare分叉处理机在父进程里fork创建子进程之前被调用。这个分叉处 理机的工作是申请所有由parent定义的锁。parent分叉处理机在fork创建子进程之后但在for返回前,在父进程上下文里被调用。这个分叉处理 机的工作是解锁所有prepare分叉处理机申请到的锁。child分叉处理机在从fork返回前在子进程的上下文里被调用。像parent分叉处理机一 样,child分叉处理机也必须释放所有prepare分叉处理机申请的锁。
注意锁没有被加锁一次而解锁两次,虽然它看起来是这样。当子进 程地址空间被创建时,它得到父进程定义的所有锁的一份拷贝。因为prepare分叉处理机申请所有的锁,父进程里的内存和子进程里的内存以相同的内容启 动。当父进程和子进程解锁这些锁的“拷贝”时,子进程的新内存被分配,而父进程的内存内容被拷贝到子进程的内存里(写时拷贝),所以我们进入一个看起来好 像父进程锁住它的锁的全部拷贝而子进程锁住它的锁的全部拷贝的情况。父进程和子进程最后解锁存储在不同内存位置的复制的锁,好像以下的事件序列发生一样:
1、父进程申请它所有的锁;
2、子进程申请它所有的锁;
3、父进程释放它的锁;
4、子进程释放它的锁。
我 们可以调用pthread_atfork多次来安装多个分叉处理机集。如果我们没有需要使用某个处理机,我们可以传递空指针给特定的处理机参数,而它会没 有效果。当多个分叉处理机被使用时,处理机被调用的顺序依情况而定。parent和child分叉处理机以它们被注册的顺序调用,而prepare分叉处 理机以注册的相反顺序被调用。这允许多个模块来注册它们自己的分叉处理机并仍然遵守锁层次。
例如,假设模块A调用模块B的函数而每个模块有它自己的锁集。如果锁层次是A在B之前,模块B必须在模块A之间安装它的分叉处理机。当父进程调用fork时,以下的步骤被执行,假定子进程在父进程之间运行:
1、模块A的prepare分叉处理机被调用以申请所有A模块的锁;
2、B模块的prepare分叉处理机被调用以申请所有B模块的锁;
3、一个子进程被创建;
4、B模块的child分叉处理机被调用以释放子进程里所有B模块的锁;
5、A模块的child分叉处理机被调用以释放子进程里所有A模块的锁;
6、fork函数返回到子进程;
7、B模块的parent分叉处理机被调用以释放父进程里所有B模块的锁;
8、A模块的parent分叉处理机被调用以释放父进程里所有A模块的锁;
9、fork函数返回到父进程。
如 果分叉处理机服务于清理锁状态,那么什么清理条件变量的状态呢?在一些实现上,条件变量可能不需要任何清理。然而,一个使用锁作为条件变量实现的一部分的 实现将需要清理。问题是没有接口允许我们这样做。如果锁被内嵌到条件变量数据结构里,那么我们不能在fork调用后使用条件变量,因为没有可移植的方法来 清理它的状态。另一方面,如果一个实现使用一个全局锁来保护进程里的所有条件变量,那么实现本身可以在fork库例程里清理这个锁。然而应用程序不应该依 赖于这样的实现细节。
下面的程序演示了pthread_atfork和分叉处理机的使用: