谨防fork与锁之间的深坑

来源:互联网 发布:中国最大的网络电商 编辑:程序博客网 时间:2024/04/29 00:37

fork之后应当谨慎使用锁:

这是因为fork有一个特点,那就是子进程只会保留调用fork的那个线程,父进程中其他的线程在子进程中都会消失。但是fork之后,除了文件锁以外,其他的锁都会被继承。这就导致了,如果在子进程中,对某个已经在父进程中加了锁的锁继续加锁,就会导致死锁发生。并且我们无法对该锁进行解锁,因为在子进程中,该锁的持有者并不存在。
下面给一个例子:

#include <stdio.h>#include <unistd.h>#include <pthread.h>#include <sys/types.h>#include <sys/wait.h>pthread_mutex_t mutex;pthread_mutexattr_t attr;void thread_func(void *arg){    pthread_mutex_init(&mutex, &attr);    pthread_mutex_lock(&mutex);    sleep(10);}int main(){    pthread_t tid;    pthread_mutexattr_init(&attr);    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);//设置互斥锁的类型为PTHREAD_MUTEX_ERRORCHECK。即会严格检查错误。如果不设置该属性,默认属性下解锁一个其他线程占用的锁时产生的行为是未定义的(我自己试了一下,可以解锁成功)。设置了之后,这种情况下就会产生错误EPERM。    pthread_create(&tid, NULL, (void *)thread_func, NULL);    int pid;    sleep(1);    pid = fork();    if(pid == 0)    {      /*        int ret;        ret = pthread_mutex_unlock(&mutex);        if(ret == EPERM)        {            printf("don't unlock a lock which not belong to you\n")        }        之前我尝试在不设置PTHREAD_MUTEX_ERRORCHECK属性下解锁由thread_func线程占有的锁,是会成功解锁的。当我们设置了该属性之后,便会产生错误值EPERM。      */        pthread_mutex_lock(&mutex);        printf("not a deadlock\n");        return 0;    }    else    {        waitpid(pid, NULL, 0);    }    pthread_join(tid, NULL);    pthread_mutexattr_destroy(&attr);    pthread_mutex_destroy(&mutex);    return 0;}

这里再强调一下自己做测试的时候,一定要设置互斥锁的类型,不然你在子进程中尝试解不属于它的锁是会成功的(其实该行为是未定义的,即不知道会发生什么)。。。。

接下来,问题是有了,但是如何解决呢?
系统提供了一个函数
pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void)),它会在调用fork时自动调用这三个注册的函数
void (*prepare)(void)的任务是获取父进程定义的所有锁,由父进程在fork之前调用
void (*parent)(void)的任务是prepare处理程序获取的所有锁进行解锁,在fork创建子进程之后、返回之前的父进程上下文中调用
void (*child)(void)的任务和parent处理程序的任务一样,也是prepare获取的所有锁进行解锁,在fork创建子进程之后、返回之前的子进程上下文中调用

它的意图是在fork之前,做好锁的清理工作
例子如下:

#include <stdio.h>#include <pthread.h>#include <unistd.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void func(void* arg){    pthread_mutex_lock(&mutex);    sleep(10);    pthread_mutex_unlock(&mutex);}void prepare(void){    pthread_mutex_lock(&mutex);}void parent(void){    pthread_mutex_unlock(&mutex);}void child(void){    pthread_mutex_unlock(&mutex);}int main(void) {    pthread_atfork(prepare, parent, child);    pthread_t tid;    pthread_create(&tid, NULL, (void *)func, NULL);    if (fork() == 0) {        func(NULL);        printf("no deadlock\n");        return 0;    }    pthread_join(tid, 0);    return 0;}

虽然这确实可以解决死锁问题,但是它并不是万能的。如果获取锁的次序有问题,它反而可能会造成死锁。而且它不能对比较复杂的同步对象(比如条件变量或屏障)进行状态的重新初始化等。这些apue中说的比较详细。

最后的结论就是,调用了fork之后,子进程最好马上调用exec函数。因为调用exec后,会把原子程序的正文段、数据段、堆、栈替换成新的可执行程序的对应段。由于性能问题,大部分系统的锁是实现在用户空间的,这样的话,所有的锁都不复存在了。

还有一点需要注意,调用exec之后,原来父进程打开的文件描述符其实是保持打开状态的。我们需要用open或者fcntl函数设置O_CLOEXEC或者FD_CLOEXEC标志,使得调用exec之后,关闭打开的文件描述符。
不过也可以利用这一点,来让exec执行的程序的结果回送到父进程,主要的操作就是使用dup2,将用于回送数据的描述符复制给STDOUT_FILENO标准输出