谨防fork与锁之间的深坑

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Move_now/article/details/73537535

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标准输出

没有更多推荐了,返回首页