一、簡介
當線程調用fork時,就爲子進程創建了整個進程地址空間的副本,父子進程通過寫時複製技術來共享內存頁的這一副本。
子進程通過幾成整個地址空間的副本,也從父進程那裏繼承了所有互斥量、讀寫鎖和條件變量的狀態。如果父進程包含多個線程,子進程在fork返回後,如果緊接着不是馬上調用exec的話,就需要清理鎖狀態。
在子進程內部只存在一個線程,它是由父進程中調用fork的線程的副本構成的。如果父進程中線程佔用鎖,子進程同樣佔用這些鎖。問題就是子進程並不包含佔用鎖的線程的副本,所以子進程沒辦法知道它佔用了哪些鎖並需要釋放哪些鎖。
1、在子進程從fork返回後立馬調用exec函數,可以避免這個問題。這種情況下,老的地址空間被丟棄,所以鎖的狀態無關緊要了。但如果子進程需要繼續做處理工作的話,這種方法就行不通了,所以還需要其他策略。
2、另一種方法就是通過調用pthread_atfork函數建立fork處理程序。其原型如下:
#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
這一函數的作用是爲fork安裝三個幫助清理鎖的函數。其中:
prepare函數由父進程在fork創建子進程之前調用,這個fork處理程序的任務是獲取父進程定義的所有鎖;
parent函數在fork創建子進程後,但在fork返回之前在父進程環境中調用的,其任務是對prepare獲取的所有鎖進行解鎖;
child函數是在fork返回前在子進程環境中調用的,和parent函數一樣,child函數也必須釋放prepare處理函數中的所有的鎖。
重點來了,看似這裏會出現加鎖一次,解鎖兩次的情況。其實不然,因爲fork後對鎖進行操作時,子進程和父進程通過寫時複製已經不對相關的地址空間進行共享了,所以,此時對於父進程,其釋放原有自己在prepare中獲取的鎖,而子進程則釋放從父進程處繼承來的相關鎖。兩個並不衝突。
下面是一段代碼,用來重現此現象:
#include "apue.h"
#include <pthread.h>
pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;
void
prepare(void)
{
printf("preparing locks...\n");
pthread_mutex_lock(&lock1);
pthread_mutex_lock(&lock2);
}
void
parent(void)
{
printf("parent unlocking locks...\n");
pthread_mutex_unlock(&lock1);
pthread_mutex_unlock(&lock2);
}
void
child(void)
{
printf("child unlocking locks...\n");
pthread_mutex_unlock(&lock1);
pthread_mutex_unlock(&lock2);
}
void *
thr_fn(void *arg)
{
printf("thread started...\n");
pause();
return(0);
}
int
main(void)
{
int err;
pid_t pid;
pthread_t tid;
#if defined(BSD) || defined(MACOS)
printf("pthread_atfork is unsupported\n");
#else
if ((err = pthread_atfork(prepare, parent, child)) != 0)
err_exit(err, "can't install fork handlers");
err = pthread_create(&tid, NULL, thr_fn, 0);
if (err != 0)
err_exit(err, "can't create thread");
sleep(2);
printf("parent about to fork...\n");
if ((pid = fork()) < 0)
err_quit("fork failed");
else if (pid == 0) /* child */
printf("child returned from fork\n");
else /* parent */
printf("parent returned from fork\n");
#endif
exit(0);
}
運行結果如下(假設子進程先運行):
$ ./a.out
thread started...
parent about to fork...
preparing locks...
child unlocking locks...
child returned from fork
parent unlocking locks...
parent returned from fork
可以看出,prepare函數在調用fork後運行,child在fork返回到子進程之前運行,parent在fork返回到父進程前運行。