线程和 fork

    父进程调用 fork 创建的子进程会继承整个地址空间的副本,以及每个互斥量、读写锁和条件变量的状态。如果父进程包含一个以上的线程,子进程在 fork 返回后,如果不是紧接着调用 exec 的话,就需要清理锁状态。因为在子进程内部,只存在父进程中调用 fork 的线程的副本一个线程。如果父进程中的线程占有锁,子进程将同样占有这些锁。问题是子进程可能并不包含占有锁的线程的副本,所以它就没法知道它占有了哪些锁、需要释放哪些锁。
    因此在多线程的进程中,为了避免不一致状态的问题,POSIX.1 声明,在 fork 返回和子进程调用 exec 族函数之间,子进程只能调用异步信号安全的函数。这就限制了在调用 exec 之前子进程能做什么,但不涉及锁状态的问题。要清除锁状态,可以调用 pthread_atfork 函数建立 fork 处理程序(对于条件变量,由于它可能是使用全局锁来保护,也可能是直接把锁嵌入到条件变量的数据结构中,目前还没有可移植的方法对这样的锁进行状态清理)。
#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
                                  /* 返回值:若成功,返回 0;否则,返回错误编号 */

    使用该函数一次最多可以安装 3 个清理锁的函数。其中 prepare 函数由父进程在 fork 创建子进程前调用,用来获取父进程定义的所有锁。parent 函数是在 fork 创建子进程之后、返回之前在父进程上下文中调用的,用来对 prepare 获取的所有锁进行解锁。child 函数则是在 fork 返回之前在子进程上下文中调用,用来释放 prepare 获取的所有锁。
    可以多次调用该函数来设置多套 fork 处理程序,对不需要的某个处理程序可以在对应位置传入空指针。多个 fork 处理程序的调用顺序是不相同的。parent 和 child 是以它们注册时的顺序调用,而 prepare 的调用则与注册顺序相反。这样可以允许多个模块注册它们自己的 fork 处理程序,而且可以保持锁的层次。例如,假设模块 A 调用模块 B 中的函数,而且每个模块有自己的一套锁。如果锁的层次是 A 在 B 之前,则模块 B 必须在模块 A 之前设置它的 fork 处理程序。当父进程调用 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 函数返回到父进程。
    下面的程序描述了如何使用 pthread_atfork 和 fork 处理程序。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

pthread_mutex_t	lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;

void prepare(void){
	printf("prepare acquiring locks\n");
	pthread_mutex_lock(&lock1);
	pthread_mutex_lock(&lock2);
}

void parent(void){
	printf("parent unlocking locks\n");
	pthread_mutex_unlock(&lock1);
	pthread_mutex_unlock(&lock2);
}

void child(void){
	printf("child unlocking locks\n");
	pthread_mutex_unlock(&lock1);
	pthread_mutex_unlock(&lock2);
}

void *thr_fn(void *arg){
	printf("thread started\n");
	pause();
	printf("thread ended\n");  // can't reach here, for no signal handlers.
	return (void *)0;
}

int main(void){
	if(pthread_atfork(prepare, parent, child) != 0){
		printf("pthread_atfork error\n");
		exit(1);
	}
	pthread_t	tid;
	if(pthread_create(&tid, NULL, thr_fn, NULL) != 0){
		printf("pthread_create error\n");
		exit(1);
	}
	sleep(2);		// Not reliably waiting for thread to run.
	printf("parent process about to call fork()...\n");
	pid_t	pid;
	if((pid = fork()) < 0){
		printf("fork error\n");
		exit(1);
	}
	if(pid == 0)
		printf("child returned from fork\n");
	else
		printf("parent returned from fork\n");
	exit(0);
}

    运行结果如下:
$ ./atforkDemo.out 
thread started
parent process about to call fork()...
prepare acquiring locks
parent unlocking locks
parent returned from fork
child unlocking locks
child returned from fork

    由此可见 prepare 处理程序在调用 fork 之后运行,child 在调用返回到子进程之前执行,parent 在 fork 调用返回给父进程之前运行。
    虽然 pthread_atfork 的意图是使 fork 之后的锁状态保持一致,但它还是存在下面这些不足之处,因此只能在有限情况下可用。
   (1)没有很好的办法对较复杂的同步对象(如条件变量和屏障)进行状态的重新初始化。
   (2)某些错误检查的互斥量实现在 child fork 处理程序试图对被父进程加锁的互斥量进行解锁时会产生错误。
   (3)递归互斥量不能在 child fork 处理程序中清理,因为没法确定加锁的次数。
   (4)如果子进程只允许调用异步信号安全的函数,child fork 处理程序就不可能清理同步对象,因为用于操作清理的所有函数都不是异步信号安全的。实际的问题是同步对象在某个线程调用 fork 时可能处于中间状态,除非同步对象处于一致状态,否则无法被清理。
   (5)如果应用程序在信号处理程序中调用了 fork(这是合法的,因为 fork 本身是异步信号安全的),则 pthread_atfork 注册的 fork 处理程序只能调用异步信号安全的函数,否则结果将是未定义的。

猜你喜欢

转载自aisxyz.iteye.com/blog/2404915