调用pthread_atfork是因为父进程在fork子进程之后,子进程也有了父进程的锁,一把锁怎么能两个人拥有呢?所以就需要调用pthread_atfork函数来处理这个问题(注整个回答是基于父进程拥有多个线程,如果是单个就不需要)
- 在进行linux系统里开发时,经常会调用linux的系统函数fork来产生一个子进程,如果父子进程都没有用到pthread线程相关函数,则就不存在需要理解pthread_atfork的函数的必要。问题是有时候既要考虑多线程,又要考虑多进程,这个时候就要仔细理解pthread_atfork这个函数的作用了。
- 在父进程调用fork函数派生子进程的时候,如果父进程创建了pthread的互斥锁(pthread_mutex_t)对象,那么子进程将自动继承父进程中互斥锁对象,并且互斥锁的状态也会被子进程继承下来:如果父进程中已经加锁的互斥锁在子进程中也是被锁住的,如果在父进程中未加锁的互斥锁在子进程中也是未加锁的。
- 在父进程调用fork之前所创建的pthread_mutex_t对象会在子进程中继续有效,而pthread_mutex_t对象通常是全局对象,会在父进程的任意线程中被操作(加锁或者解锁),这样就无法通过简单的方法让子进程明确知道被继承的 pthread_mutex_t对象到底有没有处于加锁状态。因此 pthread线程库就有了 pthread_atfork 这个函数,该函数的原型声明如下:
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
该函数通过3个不同阶段的回调函数来处理互斥锁状态。参数如下:
-
prepare:
- 将在fork调用创建出子进程之前被执行,它可以给父进程中的互斥锁对象明明确确上锁。
- 这个函数是在父进程的上下文中执行的,正常使用时,我们应该在此回调函数调用pthread_mutex_lock 来给互斥锁明明确确加锁,这个时候如果父进程中的某个线程已经调用pthread_mutex_lock给互斥锁加上了锁,则在此回调中调用 pthread_mutex_lock 将迫使父进程中调用fork的线程处于阻塞状态,直到prepare能给互斥锁对象加锁为止。
-
parent: 是在fork调用创建出子进程之后,而fork返回之前执行,在父进程上下文中被执行。它的作用是释放所有在prepare函数中被明明确确锁住的互斥锁。
-
child: 是在fork返回之前,在子进程上下文中被执行。和parent处理函数一样,child函数也是用于释放所有在prepare函数中被明明确确锁住的互斥锁。
函数成功返回0, 错误返回错误码。
通过这个函数可以确保子进程继承pthread_mutex_t对象处在未加锁状态。该函数的正常的用法如下
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#define ERROR(err, msg) do { errno = err; perror(msg); exit(-1); } while(0)
int count = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void prepare() {
int err;
printf("prepare: pthread_mutex_lock ...\n");
err = pthread_mutex_lock(&lock);
if (err != 0) ERROR(err, "prepare: pthread_mutex_lock failed");
}
void parent() {
int err;
printf("parent: pthread_mutex_unlock ...\n");
err = pthread_mutex_unlock(&lock);
if (err != 0) ERROR(err, "parent: pthread_mutex_unlock");
}
void child() {
int err;
printf("child: pthread_mutex_unlock ...\n");
err = pthread_mutex_unlock(&lock);
if (err != 0) ERROR(err, "child: pthread_mutex_unlock");
}
void* thread_proc(void* arg) {
while(1) {
pthread_mutex_lock(&lock);
count++;
printf("parent thread: count:%d\n",count);
sleep(10);
pthread_mutex_unlock(&lock);
sleep(1);
}
return NULL;
}
int main(int argc,char * argv[])
{
int err;
pid_t pid;
pthread_t tid;
pthread_create(&tid, NULL, thread_proc, NULL);
err = pthread_atfork(prepare, parent, child);
if (err != 0) ERROR(err, "atfork");
sleep(1);
printf("parent is about to fork ...\n");
pid = fork();
if (pid < 0) ERROR(errno, "fork");
else if (pid == 0) {
// child process
int status;
printf("child running\n");
while(1) {
pthread_mutex_lock(&lock);
count ++;
printf("child: count:%d\n",count);
sleep(2);
pthread_mutex_unlock(&lock);
sleep(1);
}
exit(0);
}
pthread_join(tid, NULL);
}
运行 atfork 发现 main函数会在prepare回调中等待:prepare: pthread_mutex_lock,直到获得锁以后再执行子进程创建。
一个在多线程程序里fork造成死锁的例子:
// 一个在多线程程序里fork造成死锁的例子
// 一个输出示例:
/*
pid = 19445 Entering main ...
pid = 19445 begin doit ...
pid = 19447 begin doit ...
pid = 19445 end doit ...
pid = 19445 Exiting main ...
*/
#include <stdio.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* doit(void* arg)
{
printf("pid = %d begin doit ...\n",static_cast<int>(getpid()));
pthread_mutex_lock(&mutex);
struct timespec ts = {2, 0};
nanosleep(&ts, NULL);
pthread_mutex_unlock(&mutex);
printf("pid = %d end doit ...\n",static_cast<int>(getpid()));
return NULL;
}
int main(void)
{
printf("pid = %d Entering main ...\n", static_cast<int>(getpid()));
pthread_t tid;
pthread_create(&tid, NULL, doit, NULL);
struct timespec ts = {1, 0};
nanosleep(&ts, NULL);
if (fork() == 0)
{
doit(NULL);
}
pthread_join(tid, NULL);
printf("pid = %d Exiting main ...\n",static_cast<int>(getpid()));
return 0;
}
ps aux |grep deadlock_test
解决做法的本质上就是在fork执行之前先调用prepare函数解锁,这样确保子进程调用doit时可以成功上锁。
#include <stdio.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* doit(void* arg)
{
printf("pid = %d begin doit ...\n",static_cast<int>(getpid()));
pthread_mutex_lock(&mutex);
struct timespec ts = {2, 0};
nanosleep(&ts, NULL);
pthread_mutex_unlock(&mutex);
printf("pid = %d end doit ...\n",static_cast<int>(getpid()));
return NULL;
}
void prepare(void)
{
pthread_mutex_unlock(&mutex);
}
void parent(void)
{
pthread_mutex_lock(&mutex);
}
int main(void)
{
pthread_atfork(prepare, parent, NULL);
printf("pid = %d Entering main ...\n", static_cast<int>(getpid()));
pthread_t tid;
pthread_create(&tid, NULL, doit, NULL);
struct timespec ts = {1, 0};
nanosleep(&ts, NULL);
if (fork() == 0)
{
doit(NULL);
}
pthread_join(tid, NULL);
printf("pid = %d Exiting main ...\n",static_cast<int>(getpid()));
return 0;
}