謹防fork與鎖之間的深坑

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章