線程控制之線程和fork
fork()函數與Linux中的多線程編程
使用 Mutex 實現進程間同步
fork
- 子進程通過繼承整個地址空間的副本,也從父進程那裏繼承了所有互斥量、讀寫鎖和條件變量的狀態。如果父進程包含多個線程,子進程在fork返回以後,如果緊接着不是馬上調用exec的話,就需要清理鎖狀態。
- 在子進程內部只存在一個線程,它是由父進程中調用fork的線程的副本構成的。如果父進程中的線程佔有鎖,子進程同樣佔有這些鎖。問題是子進程並不包含佔有鎖的線程的副本,所以子進程沒有辦法知道它佔有了哪些鎖並且需要釋放哪些鎖。
1、 fork()與多線程
- 雖然只將發起fork()調用的線程複製到子進程中,但全局變量的狀態以及所有的pthreads對象(如互斥量、條件變量等)都會在子進程中得以保留,這就造成一個危險的局面。例如:一個線程在fork()被調用前鎖定了某個互斥量,且對某個全局變量的更新也做到了一半,此時fork()被調用,所有數據及狀態被拷貝到子進程中,那麼子進程中對該互斥量就無法解鎖(因爲其並非該互斥量的屬主),如果再試圖鎖定該互斥量就會導致死鎖,這是多線程編程中最不願意看到的情況。同時,全局變量的狀態也可能處於不一致的狀態,因爲對其更新的操作只做到了一半對應的線程就消失了。fork()函數被調用之後,子進程就相當於處於signal handler之中,此時就不能調用線程安全的函數(用鎖機制實現安全的函數),除非函數是可重入的,而只能調用異步信號安全(async-signal-safe)的函數。fork()之後,子進程不能調用:
1、malloc(3)。因爲malloc()在訪問全局狀態時會加鎖。
2、任何可能分配或釋放內存的函數,包括new、map::insert()、snprintf() ……
3、任何pthreads函數。你不能用pthread_cond_signal()去通知父進程,只能通過讀寫pipe(2)來同步。
4、printf()系列函數,因爲其他線程可能恰好持有stdout/stderr的鎖。
5、除了man 7 signal中明確列出的“signal安全”函數之外的任何函數。
- 因爲並未執行清理函數和針對線程局部存儲數據的析構函數,所以多線程情況下可能會導致子進程的內存泄露。另外,子進程中的線程可能無法訪問(父進程中)由其他線程所創建的線程局部存儲變量,因爲(子進程)沒有任何相應的引用指針。
1.1 Mutex與多進程通信
我們知道 Mutex 互斥量是可以用在線程間同步的,線程之間共享進程的數據,mutex 就可以直接引用。而進程有自己獨立的內存空間,要怎樣將它應用在進程間同步呢?爲了達到這一目的,可以在 pthread_mutex_init 初始化之前,修改其屬性爲進程間共享,並將其映射到共享內存中即可。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <string.h>
/* 定義 mutex */
pthread_mutex_t mutex;
pthread_mutexattr_t mutexattr;
int main()
{
int shm_id = 0;
int i = 0;
pid_t pid;
pthread_mutex_t *m_mutex;
/* mutex attr 初始化 */
pthread_mutexattr_init(&mutexattr);
/* 修改屬性 */
pthread_mutexattr_setpshared(&mutexattr, PTHREAD_PROCESS_SHARED);
/* mutex 初始化 */
pthread_mutex_init(&mutex, &mutexattr);
/* 申請共享內存 */
shm_id = shmget((key_t)1004, sizeof(mutex), IPC_CREAT | 0600);
/* 映射共享內存到進程地址空間 */
m_mutex = (pthread_mutex_t*)shmat(shm_id, 0, 0);
memcpy(m_mutex, &mutex, sizeof(mutex));
pid = fork();
if(pid == 0){
pthread_mutex_lock(m_mutex);
for(; i<3; i++){
pthread_mutex_lock(m_mutex);
puts("This is the child process!");
}
}else if(pid > 0){
for(; i<3; i++){
sleep(1);
pthread_mutex_unlock(m_mutex);
puts("This is the parent process!");
}
/* 回收子進程資源 */
wait(NULL);
}
/* 銷燬 mutex */
pthread_mutexattr_destroy(&mutexattr);
pthread_mutex_destroy(&mutex);
return 0;
}
1.2 分析代碼
互斥量mutex的shared和共享內存必須同時使用,才能保證mutex能夠跨進程使用!!!
#include "apue.h"
#include "myerror.h"
// #include <pthread.h>
// #include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <string.h>
pthread_mutexattr_t mutexattr;
pthread_mutex_t mutex ;
pthread_attr_t pattr1;
pthread_cond_t pcond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t *m_mutex;
int i=0;
void init()
{
if(pthread_mutexattr_init(&mutexattr)!=0)
err_sys("pthread_mutexattr_init error\n");
pthread_mutexattr_setpshared(&mutexattr,PTHREAD_PROCESS_SHARED);
if(pthread_mutex_init(&mutex,&mutexattr)!=0)
err_sys("pthread_mutex__init error\n");
pthread_attr_init(&pattr1);
}
void* fun1(void* attr)
{
puts("hello world?????");
pthread_mutex_lock(&mutex);
pthread_cond_wait(&pcond,&mutex);
i++;
pthread_mutex_unlock(&mutex);
puts("hello world");
return NULL;
}
int main()
{
int err = 0;
pid_t pid = 0;
pthread_t tid1 = 0;
int shm_id = 0;
printf("start init");
init();
printf("start init");
shm_id = shmget((key_t)1008, sizeof(mutex), IPC_CREAT | 0600);
/* 映射共享內存到進程地址空間 */
m_mutex = (pthread_mutex_t*)shmat(shm_id, 0, 0);
if (memcpy(m_mutex, &mutex, sizeof(mutex))<0)
{
perror("error");
}
err = pthread_attr_setdetachstate(&pattr1,PTHREAD_CREATE_DETACHED);
if(err == 0)
pthread_create(&tid1,&pattr1,fun1,NULL);
//pthread_mutex_lock();
pid=fork();
if (pid<0)
{perror("fork error");}
if(pid == 0)
{
//pthread_mutex_unlock(&mutex);
sleep(2);
pthread_mutex_lock(m_mutex);
puts("this is child thread");
pthread_cond_signal(&pcond); //可以獲得互斥量,但是發送信號是有沒有用的,子線程沒有拷貝過去
//,全部終止了,且沒有運行清理函數,只能通過讀寫pipe(2)來同步
pthread_mutex_unlock(m_mutex);
//exit(0);
}
else if(pid>0)
{
sleep(1); //這裏必須讓控制線程sleep 1s,保證在發送信號之前,pthread_cond_wait運行結束!
//pthread_mutex_lock(&mutex);
puts("this is parent thread");
pthread_cond_signal(&pcond);
puts("this is parent thread22");
}
wait(NULL);
pthread_mutexattr_destroy(&mutexattr);
pthread_mutex_destroy(&mutex);
pthread_attr_destroy(&pattr1);
return(0);
}
2 pthread_atfork函數
個人理解,pthread_atfork函數是爲了在子進程生成前,先顯式獲取子進程可能衝突的互斥量,然後再解鎖,其與直接在子進程中解鎖互斥量的作用相同!而且互斥量如果沒有共享內存和shared是不能跨進程使用的!
- 如果父進程有多個線程,其中主線程進行fork,但是其他線程中,調用了互斥鎖,那麼子進程只包含了父進程的主線程,和其他子線程的互斥量,所以其並不知道子進程中的互斥量是否上鎖,如果再次上鎖,可能死鎖。
- 如果父進程中的線程佔有鎖,子進程同樣佔有這些鎖。問題是子進程並不包含佔有鎖的線程的副本,所以子進程沒有辦法知道它佔有了哪些鎖並且需要釋放哪些鎖。
- 如果子進程從fork返回以後馬上調用某個exec函數,就可以避免這樣的問題。
- 注意fork之後,子進程的代碼是從fork函數開始,直到exit
#include <pthread.h>
// Upon successful completion, pthread_atfork() shall return a value of zero; otherwise, an error number shall be returned to indicate the error.
// @prepare 新進程產生之前被調用
// @parent 新進程產生之後在父進程被調用
// @child 新進程產生之後,在子進程被調用
int pthread_atfork (void (*prepare) (void), void (*parent) (void), void (*child) (void));
- prepare fork處理程序由父進程在fork創建子進程前調用,這個fork處理程序的任務是獲取父進程定義的所有鎖。
- parent fork處理程序是在fork創建了子進程以後,但在fork返回之前在父進程環境中調用的,這個fork處理程序的任務是對prepare fork處理程序獲得的所有鎖進行解鎖。
- child fork處理程序在fork返回之前在子進程環境中調用,與parent fork處理程序一樣,child fork處理程序也必須釋放prepare fork處理程序獲得的所有鎖。
例子1
#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);
}
例子2
prepare裏先解鎖,然後子線程上鎖,
#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;
}
pid = 4905 Entering main ...
pid = 4905 begin doit ...
pid = 4908 begin doit ...
pid = 4905 end doit ...
pid = 4905 Exiting main ...
pid = 4908 end doit ...
pid = 4908 Exiting main ...
上例可以看出,子進程也退出,且運行了Exiting main …,因爲子進程中沒有退出!