APUE——線程控制之線程和fork

線程控制之線程和fork
fork()函數與Linux中的多線程編程
使用 Mutex 實現進程間同步
fork

  1. 子進程通過繼承整個地址空間的副本,也從父進程那裏繼承了所有互斥量、讀寫鎖和條件變量的狀態。如果父進程包含多個線程,子進程在fork返回以後,如果緊接着不是馬上調用exec的話,就需要清理鎖狀態。
  2. 在子進程內部只存在一個線程,它是由父進程中調用fork的線程的副本構成的。如果父進程中的線程佔有鎖,子進程同樣佔有這些鎖。問題是子進程並不包含佔有鎖的線程的副本,所以子進程沒有辦法知道它佔有了哪些鎖並且需要釋放哪些鎖。
    在這裏插入圖片描述

1、 fork()與多線程

  1. 雖然只將發起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.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是不能跨進程使用的!

  1. 如果父進程有多個線程,其中主線程進行fork,但是其他線程中,調用了互斥鎖,那麼子進程只包含了父進程的主線程,和其他子線程的互斥量,所以其並不知道子進程中的互斥量是否上鎖,如果再次上鎖,可能死鎖。
  2. 如果父進程中的線程佔有鎖,子進程同樣佔有這些鎖。問題是子進程並不包含佔有鎖的線程的副本,所以子進程沒有辦法知道它佔有了哪些鎖並且需要釋放哪些鎖。
  3. 如果子進程從fork返回以後馬上調用某個exec函數,就可以避免這樣的問題。
  4. 注意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));
  1. prepare fork處理程序由父進程在fork創建子進程前調用,這個fork處理程序的任務是獲取父進程定義的所有鎖。
  2. parent fork處理程序是在fork創建了子進程以後,但在fork返回之前在父進程環境中調用的,這個fork處理程序的任務是對prepare fork處理程序獲得的所有鎖進行解鎖。
  3. 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 …,因爲子進程中沒有退出!

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