線程和fork

一、簡介

    當線程調用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返回到父進程前運行。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章