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
標準輸出。