Linux多線程總結


1 進程環境

 

C程序總是從main函數開始執行。main函數的原型是:

     int main( int argc,  char* argv[]); 

      當內核執行C程序時(使用一個exec函數),在調用main前先調用一個特殊的啟動例程。啟動例程從內核取得命令行參數和環境變量值,然后調用main函數。

1.1 進程終止

 

有8種方式能種子進程,其中5種是正常終止,3種是異常終止。

正常:

  • 從main返回;
  • 調用exit;
  • 調用_exit或_Exit;
  • 最后一個線程從其啟動例程返回;
  • 從最后一個線程調用pthread_exit。

異常:

  • 調用abort;
  • 接到一個信號;
  • 最后一個線程對取消請求做出響應。

    其中三個正常終止函數:_exit、_Exit和exit之間是有區別的。_exit、_Exit立即退出;而exit則先執行一些清理工作,然后再退出。

    並且可以利用函數atexit來聲明在exit退出前會被調用的函數(稱為終止處理程序),atexit原型為:

  int atexit(  void (*func)( void) ); 
                                 返回值:若成功返回0,若出錯返回非0;

 如下圖所示的一個C程序是如何啟動和終止的:

    注意:內核使程序執行的唯一方法是調用一個exec函數。進程自願終止的唯一方法是顯示或隱式地(通過調用exit)滴啊用_exit或_Exit;進程也可非自願地有 一個信號使其終止。

1.2 進程內存布局

 

每個進程所分配的內存由很多部分組成,通常稱之為"段"。如下所示:

  • 正文段:包含了進程運行的程序機器語言指令,此段具有只讀屬性。
  • 初始化數據段:包含顯示初始化的全局變量和靜態變量
  • 為初始化數據段:包含未顯示初始化的全局變量和靜態變量,系統為此段所有內存初始化為0。
  • 棧:自動變量以及每次函數調用時所需保存的信息都存放在此段中,包括局部變量、實參、返回值、臨時變量和環境信息。
  • 堆:通常在堆中進程動態存儲分配

 

Figure 1 典型的存儲空間安排

2 進程控制

 

    每個進程都有進程ID,其中ID為0的進程是調度進程(交換進程);進程為1的是init進程,並且init進程決不會終止,

2.1 進程的創建

 

    進程的創建可以通過函數fork和vfork進行。

重點注意:

不同的進程擁有不同的地址空間。如在不同的兩個進程都擁有100的地址,那么這兩個100存放的值是不一樣的。

2.1.1 fork函數 

pid_t fork( void
                返回值:子進程返回0,父進程返回子進程的ID;若出錯返回- 1

    fork函數被調用一次,卻返回兩次,子進程返回0,父進程返回子進程的ID值;並且子進程的內存是父進程的完全副本,包括局部、全局、靜態的數據空間、堆和棧的副本。其中有些系統對fork進行了優化,即寫時復制(Copy-On-Write)技術,在fork調用之后父子進程共享同一個區域,只有當父進程或子進程的任意一方試圖修改這些區域,內核才會為修改區域的那塊內存制作一個副本。

1) 緩沖問題

在fork函數調用之前,需要考慮緩沖中是否有數據存在。是為不帶緩沖的系統調用,還是帶緩沖的標准I/O。

  • 如果在調用fork函數之前,調用了不帶緩沖的系統函數(如write),則只輸出一次之前的數據;
  • 如果在調用fork之前,調用標准I/O(帶緩沖函數,如printf),則可能輸出會輸出多次,因為父子進程都保存了緩沖中的數據,如果此時是與終端交互,則是行緩沖,而如果是磁盤交互,則是全緩沖。

2) 文件共享

    在fork調用之后,子進程復制了父進程文件描述符,並且子進程和父進程共享該文件的偏移量。若父子進程之間沒有任何形式的同步,那么它們的輸出就會相互混合。在fork之后處理文件描述符有以下兩種使用模式:

  • 父進程等待子進程完成:父進程無需對其描述符做任何處理。當子進程終止后,它曾進行過讀、寫操作的任一共享描述符的偏移量已做了相應更新。
  • 父進程和子進程各自執行不同的程序段:fork之后,父進程和子進程各自關閉它們不需使用的文件描述符,這樣就不會干擾對方使用的文件描述符。這種方法是網絡服務器進程經常使用的

子進程還繼承父進程的屬性還有:

  • 實際用戶ID、控制終端
  • 設置用戶ID標志和設置組ID標志
  • 當前工作目錄、環境、根目錄、文件屏蔽字
  • 對任一打開文件描述符的執行時關閉(close-on-exec)標志
  • 連接的共享存儲段、存儲映像

3) fork使用模式

fork有兩種用法,復制自己從而進行不同的操作,或是復制自己執行不同的程序。

  •     父進程和子進程同時執行不同的代碼段:如在網絡服務進程中,父進程等待客戶的服務請求,父進程調用fork使子進程處理此請求,父進程則繼續等待下一個服務請求。
  •     父進程和子進程要執行一個不同的程序:如在shell中,子進程從fork返回之后立即調用exec

2.1.2 vfork函數

 

    vfork函數用於創建一個新進程,而該新進程的目的是exec一個新程序。所以vfork與fork一樣都創建一個子進程,但是它並不將父進程的地址空間完全復制到子進程中,因為子進程會立即調用exec(或exit),於是也就不會引用該地址空間。vfork保證子進程先運行,並在子進程調用exec或exit之前,它在父進程的空間中運行。

2.2 進程的終止

    在1.1小節中,介紹了進程5種正常終止和3種異常終止的方式。其中不管進程如何終止,最后都會執行內核中的同一段代碼,來為相應進程關閉所有打開描述符,及釋放它所使用的存儲器等。

    子進程的通過如下方式來通知父進程自己是如何終止的:

  • 對於3個終止函數(exit、_exit、_Exit),是將退出狀態作為參數傳送給函數。如exit(0)。
  • 在異常終止情況下,內核(不是進程本身)產生一個指示器異常終止原因的終止狀態(termination status)。

    在任意一種情況下,該終止進程的父進程都能用wait或waitpid函數取得其終止狀態。

注意:

  • 一個已經終止、但其父進程尚未對其進行善后處理(獲取終止子進程的有關信息,釋放它仍占用的資源)的進程被稱為僵死進程
  • 如果父進程在子進程之前終止,那么此已終止父進程的所有子進程,將會改變它們的父進程為init進程,稱這些子進程被init收養

2.3 監控子進程

 

    當一個進程正常或異常終止時,內核就向其父進程發送SIGCHLD信號因為子進程終止時一個異步事件(這可以在父進程運行的任何時候發生),所以這種信號也是內核向父進程發的異步通知。其中父進程可以通過調用wait或waitpid來獲得子進程的終止狀態。

其中在調用上述兩函數之后,可能會發生如下的情況

  • 如果其所有子進程都還在運行,則阻塞。
  • 如果一個子進程已終止,正等待父進程獲取其終止狀態,則取得該子進程的終止狀態立即返回。
  • 如果它沒有任何子進程,則立即出錯返回。
pid_t wait( int *statloc); 
pid_t waitpid(pid_t pid,  int *statloc,  int options); 
    兩個函數返回值:若成功,返回進程ID,statloc返回終止狀態;若出錯,返回0或- 1

 這兩個函數的區別是:

  • 在一個子進程終止前,wait使其調用者阻塞,而waitpid有一選項(ANOHANG),可使調用者不阻塞。
  • waitpid並不等待在其調用之后第一個終止子進程,它由若干個選項,可以控制它所等待的進程。

    由於UNIX信號一般是不排隊的,所以當有多個子進程同時終止,那么wait和waitpid只能獲得一個子進程的終止狀態,為了防止余下的子進程得不到處理(成為僵死進程),那么只能使用waitpid監控了

 

  while( (pid=waitpid(- 1,&stat, WNOHANG) )> 0// 一直等待子進程終止,直到沒有已終止子進程,則跳出循環 
        printf( " child %d teminated\n ",pid); 

      上述代碼如果將waitpid改為wait,也可以獲得所有子進程的終止狀態,但父進程(當前進程)將進入永遠的阻塞狀態,因為如果沒有子進程,那么wait將會阻塞,不能返回。

2.4 程序的執行

 

    Linux可以通過一系列exec函數(7個)來執行另一個程序,被加載的新程序將替換某一進程的內存空間,舊進程的棧、數據以及堆段會被新程序的相應部件所替換,並且新程序會從main()函數出開始執行。調用exec函數之后,進程的ID仍保持不變。

2.4.1 exec函數 

int execl( const  char *pathname,  const  char *arg0,… ); 
int execv( const  char *pathname,  char* const argv[]); 
int execle( const  char *pathname,  const  char* arg0, …); 
int execve( const  char *pathname,  charconst argv[],  char * const envp[]); 

int execlp( const  char *filename,  const  char *arg0,…); 
int execvp( const  char *filename,  charconst argv[]); 
 
int fexecve( int fd,  char * const argv[],  char * const envp[]); 
                                                           7個函數返回值:若出錯,返回- 1,若成功,不返回
 
這些函數之間的第一個區別是:

    前4個函數取路徑名作為參數,后兩個取文件名作為參數(p),最后一個取文件描述符作為參數(f)

第二個區別是:

    是參數表傳遞的不同,l表示列表listv表示矢量vector

第三個區別是

    是否傳遞環境表e

    在Linux中只有execve是內核的系統調用,另外6個則只是庫函數,6個庫函數都需調用該系統調用,如下圖所示:

 

2.4.1 屬性的繼承

1) 基本屬性

    在執行exec之后,除了進程ID不變外,新程序還繼承了如下屬性:

  • 進程ID和父進程ID
  • 實際用戶ID和實際組ID
  • 附屬組ID、進程組ID、會話ID
  • 控制終端
  • 當前工作目錄、跟目錄
  • 文件模式創建屏蔽字、進程信號屏蔽字
  • 文件鎖
  • 未處理信號
  • 資源限制

2) 文件描述符

    其中打開文件的處理與每個描述符的執行時關閉(close-on-exec)標志值有關,即FD_CLOEXEC,若對文件描述符設置了此標志,則在執行exec時關閉該描述符;否則該描述符仍打開。

3) 設置用戶ID和設置組ID

    注意,在exec前后實際用戶ID和實際組ID保持不變,但如果對pathname所指定的程序文件設置了set_user-ID(set-group-ID)權限位,那么系統調用會在執行此文件時將進程的有效用戶(組)ID置為程序文件的屬主(組)ID。

2.4.3 system函數

 

程序可通過調用system函數來執行任意的shell命令。其函數原型如下: 

#include <stdlib.h> 
int system( const  char* cmdstring);

      函數system()的實現中調用了fork、exec和waitpid等函數,其創建一個子進程來運行shell,從而執行了命令cmdstring。如system("ls | less")

 

3 信號

 

    信號是事件發生時對進程的一種通知方式。事件可以是硬件異常(如除以0)、軟件條件(如alarm定時器超時)、終端產生的信號或調用kill函數。

  • 信號產生(generation):是指事件發生的動作,信號產生的時間就是事件發生的時刻;
  • 信號遞送(delivery):是指這樣一個過程,即當信號產生時,內核會在進程表中以某種形式設置一個標志。
  • 信號未決(pending):是指在信號產生和遞送之間的時間間隔內。

3.1 信號的傳遞

 

    進程可以選用"阻塞信號遞送"。如果為進程產生了一個阻塞的信號,而且對該信號的動作是系統默認動作捕捉該信號,則該進程將此信號保持為未決狀態,直到該進程對此信號解除了阻塞,或者將對此信號的動作更改為忽略

內核將信號傳遞給進程有如下幾種可能

  • 如果內核接下來要調度該進程運行,而等待信號會馬上傳遞給進程。
  • 如果進程正在運行,則會立即傳遞信號給進程。
  • 如果所產生的信號是進程的阻塞信號之一,那么信號將保持等待狀態,直至對該信號解除阻塞(從信號掩碼移除)。

注意:

  1. 由於信號是不進行排隊處理的,所以如果在解除阻塞某種信號之前,該信號發生了多次,那么在解除之后該信號只會被遞送給進程一次。
  2. 由於不同信號的處理是無序的,所以在解除阻塞之前產生了多個信號,那么解除之后進程接收的順序不一定與產生的順序一致。

3.2 信號的處理方式

 

在某個信號出現時,可以告訴內核按下列3種方式之一進行處理:

  1. 忽略此信號

    大多數可以這樣處理,但有兩種信號不能被忽略,是SIGKILL和SIGSTOP。

  2. 捕捉信號

    用戶可以通知內核在某種信號發生時,調用一個用戶函數。但SIGKILL和SIGSTOP不能被捕捉。

  3. 執行系統默認動作

    每一種信號都有默認動作,基本都是終止該進程。

     

3.3 接收信號:signal和sigaction

signal和sigaction函數都是設定接收信號的處理方式,若未對信號重新設置,則進程按默認方式處理。

3.3.1 signal

    UNIX系統信號機制最簡單的接口是signal函數:

void (*signal( int signo,  void (*func)( int) ) )( int); 
                                           返回值:若成功,返回以前的信號處理函數;若出錯,返回SIG_ERR 

    signo參數是要處理的信號名,func的值可以是常量SIG_IGN,常量SIG_DEL或當接到信號后要調用的函數地址。即對應信號的三種處理方式。

  • SIG_IGN:忽略此信號
  • SIG_DEL:按系統默認動作
  • 函數地址:在信號發生時,調用該函數(信號處理程序);

 

在exec或fork后的信號處理方式;

1) 調用exec

    一個進程原先要捕捉的信號,當調用exec執行一個新程序后,這些被捕獲信號的處理方式都被更改為按系統默認方式處理,因為信號捕捉函數的地址很可能在所執行的新程序文件中已無意義。

2)   fork進程

    當一個進程調用fork時,其子進程繼承父進程的信號處理方式。因為子進程在開始時復制了父進程內存映象,所以信號捕捉函數的地址在子進程中是有意義的。

 

3.3.2 sigaction

 

    sigaction函數的功能是檢測或修改與制定信號相關聯的處理動作。

 

int sigaction( int signo,  const  struct sigaction* restrict act,  struct sigaction* restrict oact); 
返回值:若成功,返回0;若出錯,返回- 1 

struct sigaction{ 
         void (*sa_hander)( int);                   // 處理函數,或SIG_IGN, SIG_DEL 
        sigset_t sa_mask;                         // 屏蔽字 
         int sa_flags;                             // 選項 
  void (*sa_sigaction)( int , siginfo_t*,  void*);  // 選擇項 
  • signo是要檢測或修改其具體動作的信號編號。
  • act指針非空,則要修改其動作。
  • 如果oact指針非空,則系統經由oact指針返回該信號的上一個動作。

使用方法:

    當更改信號動作時,如果sa_handler字段包含一個信號捕捉函數的地址(不是常量SIG_IGN或SIG_DFL),則sa_mask字段說明了一個信號集,在調用該信號捕捉函數之前,這一信號集要加到進程的信號屏蔽字中。僅當信號捕捉函數返回時再將進程的信號屏蔽字恢復為原先值。

 

3.4 發送信號:kill、raise和alarm

 

    kill函數將信號發送指定進程或進程組;raise函數是立即向進程自身發送信號;alarm函數是設置一個定時器,當超時后自身發送一個信號。

3.4.1 kill函數 

int kill(pid_t pid,  int signo); 
                             兩個函數返回值:若成功,返回0;若出錯,返回- 1
 kill的pid參數有以下4種不同的情況:

  • pid>0將信號發送給進程IDpid的進程。
  • pid==0將信號發送給與發送進程屬於同一進程組的所有進程。
  • pid<0將信號發送給進程組ID等於pid絕對值。
  • pid==-1將信號發送給所有進程(有權限的進程)。

注意:將信號發生給其它進程需要權限

  • 超級用戶:可將信號發送給任一進程;
  • 非超級用戶:信號發生進程的實際用戶ID或有效用戶ID必須等於接收進程的實際用戶ID或有效用戶ID。

3.4.2 raise函數 

 

 

int raise( int signo); 
                                 兩個函數返回值:若成功,返回0;若出錯,返回- 1

     當進程調用raise函數,信號立即被傳遞(即,在raise函數返回調用者之前)。並且raise出錯的唯一原因是signo無效。 

3.4.3 alarm函數 

#include <unistd.h> 
unsigned  int alam(unsigned  int seconds) 
                                  返回值:0或以前設置的鬧鍾時間的余留秒數

       使用alarm函數可以設置一個定時器(鬧鍾時間),在將來的某個時刻該定時器會超時。當定時器超時時,會產生SIGALRM信號。如果忽略或不捕捉此信號,則其默認動作是終止調用該alarm函數的進程。其中進程調用alam()函數后是不掛起的,仍然繼續執行

3.5 掛起進程: sleep和pause

sleep和pause函數都能使調用進程掛起,但sleep在指定的時間超時或接收到信號時就能喚醒;而pause只能接收到信號才得以喚醒,否則將一直處於掛起狀態。

3.5.1 sleep函數 

 

#include <unistd.h> 
int sleep( int seconds) 
                       返回值:0或未休眠完成的秒數

     此函數使調用進程被掛起知道滿足以下兩個條件之一:

  1. 已經過了seconds鎖指定的牆上時鍾時間;
  2. 調用進程捕捉到一個信號並從信號處理程序返回。

3.5.2 pause函數

#include <unistd.h> 
int pause( void
                       返回值:- 1,errno設置為EINTR 

         pause函數使得調用進程自己掛起,直至當前進程接收到某種信號,才得以喚醒;否則一直處於阻塞狀態。

注意:

只有執行了一個信號處理程序並從其返回時,pause才返回。

 

3.6 信號屏蔽功能:sigprocmask、sigpending和sigsuspend

    內核會為每個進程維護一個信號掩碼(即一個信號集),從而進程將屏蔽信號掩碼中的信號集。若將被屏蔽的信號發送給進程,那么該信號的傳遞將被延后(只是被延后,不是被刪除),直至該進程解除該信號,從而該進程仍能夠接收到先前被屏蔽的信號。

關於進程的信號屏蔽,UNIX還提供了如下功能: 

l  sigprocmask

       調用函數sigprocmask可以檢測更改,或同時進行檢測和更改進程的信號屏蔽字

l  sigpending

       該函數返回正處於等待狀態的信號集,即由於某個信號是屏蔽字段的類型之一,並且發送給了調用進程,那么將從set返回。

l  sigsuspend(sigset_t *sigmask)

       該函數的功能是將屏蔽字更改sigmask,並掛起調用進程;直至接收到sigmask之外的信號,才喚醒該進程,並將屏蔽字恢復為調用sigsuspend函數之前的屏蔽字該過程可分為3個步驟完成:首先設置指定屏障字;然后使進程掛起(類似pause功能);最后接收到除sigmask信號,並恢復之前的屏蔽字。

 

注意:

    屏蔽信號,並不是刪除信號,當解除屏蔽信號時,被阻塞的信號仍能被進程接收。

4 進程通信

 

4.1 管道和FIFO

4.1.1 管道

管道有兩種局限性:

  • 歷史上,它們是半雙工的,雖然某些系統提供全雙工管道。
  • 管道只能在具有公共祖先的兩個進程之間使用。

創建管道函數原型是: 

int pipe( int fd[ 2]); 
                  返回值:若成功,返回0,,若出錯,返回- 1

 

    經由參數fd返回兩個文件描述符,fd[0]為讀而打開,fd[1]為寫而打開,然后就能像使用普通文件描述符一樣,進行讀寫了。

    單個進程中的觀點幾乎沒有任何用處。通常,進程會先調用pipe,接着調用fork,從而創建從父進程到子進程的IPC通道。對於從父進程到子進程的管道,如父進程關閉管道的讀端(fd[0]),子進程關閉寫端(fd[1])。如下圖所示:

當管道的一端被關閉后,下列兩條規則起作用:

  1. 當讀(read)一個寫端已被關閉的管道時,在所有數據都被讀取后,read返回0,表示文件結束。
  2. 如果寫(write)一個讀端已被關閉的管道,則產生信號SIGPIPE。如果忽略該信號或者捕獲該信號並從其處理程序返回,則write返回-1,errno設置為EPIPE。

 

如下程序:

 

 1 main(){ 
 2      int n,fd[ 2];    pid_t pid;      char line[ 100]; 
 3     pipe(fd); 
 4      if((pid=fork()< 0
 5          return
 6      else  if(pid> 0){ 
 7         close(fd[ 0]); 
 8         write(fd[ 1], " hello world\n ", 12);  // 向管道中寫入數據
 9      } 
10      else
11         close(fd[ 1]); 
12         n = read(fd[ 0],line, 12);      // 從管道中讀取數據 
13          write(STDOUT_FILENO, lien, n); // 輸出終端 
14      } 
15 } 

4.1.2 FIFO

 

    FIFO也稱為命名管道。與管道不同,通過FIFO不相關的進程也能交換數據。FIFO的使用方式是:先創建(mkfifo),然后使用open打開,接着就能使用I/O系統調用(如read()、write()和close())操作打開的文件描述符了。

 

int mkfifo( const  char* path, mode_t mode); 
                                             返回值:若成功,返回0;若出錯,返回- 1
  • path參數:表示FIFO文件的保存路徑,即mkfifo創建一個名為path的FIFO。
  • mode參數:指定了新FIFO的權限。

    與管道一樣,FIFO也有一個寫入端和讀取端並且從管道中讀取數據的順序與寫入的順序是一樣的。

其中需注意:

  • 打開一個FIFO以便讀取數據(open() O_RDONLY標志)將會阻塞,直到另一個進程開大FIFO以寫入數據(open() O_WRONLY標志)為止
  • 相應地,打開一個FIFO會同步讀取進程和寫入進程。如果一個FIFO的另一端已經打開(可能是因為一對進程已經打開了FIFO的兩端),那么open()調用會立即成功

4.2 消息隊列

 

    消息隊列與FIFO類似,都是需要先創建一個標識,然后通過這個標識進行消息的發送和接收。

1) 創建或打開一個消息隊列 

 

int msgget(ket_t key,  int msgflag); 
                                返回值:若成功,返回消息隊列ID;若出錯,返回- 1
  • key參數:是消息隊列的一個標識,將這個標識作為消息隊列的外部名;
  • msgflag參數:是消息隊列的權限。

2) 發送消息

msgsnd功能是將新消息添加到隊列尾端。 

 

int msgsnd( int msqid  const  void* ptr, size_t nbytes,  int flag); 
返回值:若陳工,返回0,若出錯,返回- 1

3) 接收消息

    msgrcv用於從隊列中取消息,可以按先進先出次序取消息,也可以按消息的類型字段取消息。 

 

int msgrcv( int msqid,  void* ptr, size_t nbytes,  long type,  int flag); 
返回值:若成功,返回消息數據部分的長度;若出錯,返回- 1

4.3 信號量

 

    信號量不是用來在進程間傳輸數據的,相反,它是用來同步進程的動作。信號量的一個常見用途是同步對一塊共享內存的訪問以防止出現一個進程在訪問共享內存的同時另一個進程更新這塊內存的情況。

    

為了獲得共享資源,進程需要執行下列操作:

  1. 測試控制該資源的信號量。
  2. 若此信號量的值為正,則進程可以使用該資源在這種情況下,進程會將信號量值減1,表示它使用了一個資源單位。
  3. 否則,若此信號量的值為0,則進程進入休眠狀態直至信號量值大於0.進程被喚醒后,它返回至步驟1)。  

使用信號量的常規步驟如下:

  1. 使用semget() 創建或打開一個信號量集。
  2. 使用semctl() SETVAL或SETALL操作初始化集合中的信號量。(只有一個進程需要完成這個任務)
  3. 使用semop() 操作信號量值。使用信號量的進程通常會使用這些操作來表示一種共享資源獲取和釋放
  4. 當所有進程都不在需要使用信號量集之后使用semctl() IPC_RMID操作刪除這個集合。(只有一個進程需要完成這個任務)

4.4 共享內存

    共享存儲允許多個進程共享一個給定的存儲區。因為數據不需要在客戶進程和服務器進程之間復制,所以這是最快的一種IPC。為了防止多個進程同時訪問共享存儲區,通常使用信號量同步共享存儲區的訪問(也可使用記錄鎖或互斥量)。

    共享存儲與內存映射不同之處在於,前者沒有相關的文件,共享存儲段是內存的匿名段。

為使用一個共享內存段通常需要執行下面的步驟:

  1. 調用shmget()創建一個新共享內存段或取得一個既有共享內存段的標識符(有其它進程創建的共享內存段)。這個調用將返回后續調用中需要用到的共享內存標識符。
  2. 使用shmat()來附上共享內存段,即使該段成為調用進程的虛擬內存的一部分。
  3. 此刻在程序中可以像對待其它可用內存那樣對待這個共享內存段。為引用這塊共享內存,程序需要使用由shmat()調用返回的addr值,它是一個執行進程的虛擬地址空間中該共享內存段的起點的指針。
  4. 調用shmdt()來分離共享內存段。這個調用之后,進程就無法在引用這塊共享內存了。這一步是可選的,並且在進程終止時會自動完成這一步。
  5. 調用shmctl()來刪除共享內存段。是由當當前所有附加內存段的進程都與之分離之后內存段才會被銷毀。只有一個進程需要執行這一步。

5 線程

 

5.1 基本概念

1) 什么是線程

線程是一個進程內部的一個控制序列。所有的進程都至少有一個執行線程。

一個進程的所有信息對該進程的所有線程都是共享的,包括可執行程序的代碼,程序的全局內存和堆內存、棧以及文件描述符,即同一個進程的線程同處於一個地址空間,彼此能夠互相訪問棧地址(只要可見)。但不同的進程擁有完成不相干的地址空間。

如:

 

 1 #include <stdio.h> 
 2 #include <pthread.h> 
 3 #include <unistd.h> 
 4  int main() 
 5 { 
 6      int num= 100, a; 
 7      int *p = &num; 
 8     pid_t pid; 
 9  
10     printf( " before fork num:%d\n ",num); 
11      if((pid=fork())== 0
12     { 
13         *p =  200// 修改了子進程的p地址所指的值 
14          printf( " %d@%d:%d\n ",getppid(),getpid(),num); 
15     } else  if(pid> 0
16     {     
17         wait(&a); 
18         printf( " %d@%d:%d\n ",getppid(),getpid(),num); 
19     } 
20 } 
21 
22 輸出:即父子進程地址P的值相同,但彼此不相關 
23     before fork num: 100 
24     30754@ 30755: 200 
25     18163@ 30754: 100 

 

2) 線程標識

每個線程也有一個線程ID。進程ID在整個系統中是唯一的,但線程ID不同,線程ID只有在它所屬的進程上下文中才有意義。

 

#include<pthread.h> 
int pthread_equal(pthread_t tid1, pthread_t tid2) 
                                             返回值:若相等,返回非0數值:否則,返回0 
pthread_t pthread_self( void
                                            返回值:調用線程的線程ID 

 

5.2 線程創建

在POSIX線程(pthread)情況下,程序開始運行時,它也是以進程中的個控制線程(主線程)啟動的。 

 

int pthread_create(pthread_t *tidp, pthread_atti_t *attr,  void *(*start_rtn)( void*),  void* arg) 
返回值:若成功,返回0;否則,返回出錯編號

       當創建成功返回時,新創建線程的線程ID會被設置成tidp指向的內存單元;新創建的線程從start_rtn(稱為啟動例程)函數的地址開始運行,該函數只有一個不類型指針參數art。

5.3 線程終止

      如果進程中的任意線程調用了exit、_Exit或者_exit,那么整個進程就會終止,其中主線程退出也會使整個進程終止。與此類似,如果信號的默認動作是終止進程,那么,發送到線程的信號就會終止整個進程。

單個線程可以通過3種方式退出,因此可以在不終止整個進程的情況下,停止它的控制流:

  1. 線程可以簡單地從啟動例程中返回,返回值是線程的退出碼;
  2. 線程可以被同一進程中的其他線程取消;
  3. 線程調用pthread_exit。

5.3.1 線程退出

#include <pthread.h> 
void pthread_exit( void* rval_ptr); 

    pthread_exit是線程退出函數,rval_ptr是推出的狀態碼;

5.3.2 線程等待

#include <pthread.h> 
int pthread_join(pthread_t thread,  void** rval_ptr) 

     調用pthead_join線程將獲得指定線程的退出狀態;當線程調用了pthread_join函數則將一直阻塞,直到指定的線程調用pthread_exit、從啟動例程中返回或者被取消

5.3.3 線程取消

 

#include <pthread.h> 
void pthread_cancel(pthread_t tid); 

   線程可以通過調用pthrad_cancel函數來請求取消同一個進程中其他線程。在默認情況下,pthread_cancel函數會使得tid標識的線程的行為表現為如同調用了參數為PTHREAD_CANCEL的pthread_exit函數但是,線程可以忽略取消或者控制如何被取消。

5.3.4 線程清理處理程序

線程內存空間中存在着一個特殊棧,此棧用來記錄清理函數的地址。該棧由如下兩個函數來完成入棧和出棧操作。它們的執行順序與它們的注冊時相反。

#include <pthread.h> 
void pthread_cleanup_push( void (*rtn)( void*),  void * arg); 
void pthread_cleanup_pop( int execute); 

    當線程執行以下動作時清理函數rtn將被調用執行,其中arg是傳遞的函數。

  • 調用pthread_exit時
  •     響應取消請求時
  •     用非零execute參數調用pthread_cleanup_pop時。

5.4 線程同步

 

術語臨界區(critical section)是指訪問某一共享資源的代碼片段,並且這段代碼的執行應為原子操作,亦即,同時訪問同一共享資源的其他線程不應中斷該片段的執行。

線程有如下的5種基本的同步機制。

5.4.1 互斥量(pthread_mutex_t)

互斥量(mutex)是線程同步的一把鎖,該鎖有兩個狀態:已鎖定未鎖定。互斥量保證了同一個時間只有一個線程(獲得鎖的進程)訪問臨界區,並且該鎖只能由獲得鎖的線程主動釋放。

1) 初始化與銷毀

互斥量的數據類型是pthread_mutex_t在使用互斥量之前,首先必須對它進行初始化,有如下兩種情況:

  • 靜態分配的互斥量:將互斥量賦值為常量PTHREAD_MUTEX_INITIALIZER;
  • 動態分配的互斥量:通過調用pthread_mutex_init函數進行初始化,

如果是動態分配的互斥量 (例如,通過調用malloc函數或全局變量),在釋放內存前需要調用pthread_mutex_destroy。

 

#include <pthread.h> 
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr); 
int pthread_mutex_destroy(pthread_mutex_t *mutex);

    要默認的屬性初始化互斥量,只需把attr設為NULL。

2) 加鎖與解鎖

對互斥量進行加鎖,需要調用pthread_mutex_lock,如果互斥量已經上鎖了,調用線程將阻塞知道互斥量被解鎖;對互斥量解鎖,需要調用pthread_mutex_unlock。 

 

#include <pthread.h> 
int pthread_mutex_lock(pthread_mutex_t *mutex) 
int pthread_mutex_trylock(pthread_mutex_t *mutex) 
int pthread_mutex_unlock(pthread_mutex_t *mutex) 
                                                           所有函數的返回值:若成功,返回0;否則,返回錯誤編號

      如果線程不希望被阻塞,可以使用pthread_mutex_trylock嘗試對互斥量進行加鎖調用pthread_mutex_trylock有兩種情況:

  • 互斥量處於未鎖住狀態:則直接返回0,那么成功鎖住互斥量,不會出現阻塞;
  • 互斥量處於已鎖住狀態:則返回EBUSY,不能鎖住互斥量。

5.4.2 條件變量(pthread_cond_t)

 

互斥量為防止多個線程同時訪問同一個共享量,而條件變量允許一個線程就某個共享變量(或其他共享資源)的狀態變化通知其他線程,並讓其他線程等待(阻塞於)這一通知。條件變量總是結合互斥量一起使用,條件變量就共享變量的狀態改變發出通知,而互斥量則提供對該共享變量訪問的互斥。

1) 初始化與銷毀

與互斥量類似,對條件變量的初始化分為靜態類型和動態類型,動態類型的初始化和銷毀函數為:

#include <pthread.h> 
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr *attr) 
int pthread_cond_destroy(pthread_cond_t *cond) 

 

2) 等待通知

等待其他線程的發送的條件變量,可以使用如下兩個函數,第二個只是增加了等待時間,其余功能都一樣。

 

#include <pthread.h> 
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) 
int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex,  struct timespec *tsptr); 

調用線程將鎖住的mutex信號量傳遞給pthread_cond_wait函數,然后該線程自動進入等待狀態(阻塞),並對此互斥量解鎖。從pthread_cond_wait喚醒必須同時具備如下條件:

  • 其它線程對cond條件變量發送了喚醒操作:pthread_cond_signal或pthread_cond_broadcast。
  • mutex沒有被鎖住

3) 發送通知

 

#include <pthread.h> 
int pthread_cond_signal(pthread_cond_t *cond) 
int pthread_cond_broadcast(pthread_cond_t *cond) 

     這兩個函數都可以用於喚醒阻塞於cond條件變量的線程,pthread_cond_signal函數至少能喚醒一個等待該條件變量的線程,而pthread_cond_broadcast函數則能喚醒等待該條件變量的所有線程。

注意:

  1. pthread_cond_signal和pthread_cond_broadcast只是通知的cond條件變量的線程,而未對mutex信號量進行修改,需在調用上述兩個喚醒函數之前手動進行解鎖(pthread_mutex_unlock)。
  2. 若調用了pthread_cond_broadcast喚醒所有等待線程,則只有一個線程能從阻塞狀態喚醒,因為只有一個線程能獲得mutex互斥量,其它線程只能等待獲得mutex鎖。
  3. 應該把調用pthread_cond_wait函數放在while之中,判斷條件是"共享資源不可用",即當判斷了共享資源不可用,應繼續調用pthread_cond_wait等待。

5.4.3 讀寫鎖(pthread_rwlock_t)

 

       與只有2種狀態的互斥量不同,讀寫鎖有3種狀態:模式下加鎖狀態、模式下加鎖狀態、不加鎖狀態。其中一次只有一個線程可以占有寫模式的讀寫鎖,但是多個線程可以同時占有讀模式的讀寫鎖。

在讀寫鎖的3種狀態下,進行讀或寫申請給出的響應各不相同:

  • 寫加鎖狀態:所有試圖對這個鎖進行加鎖的線程都會被阻塞
  • 讀加鎖狀態:所有試圖以讀模式對這個鎖進行加鎖都可以訪問,但是任何以寫模式對此鎖進行加鎖的線程都會阻塞

1) 初始化與銷毀

與互斥量相比,讀寫鎖在使用之前必須初始化在釋放內存之間必須銷毀。 

 

#include <pthread.h> 
int pthread_rwlock_init(pthread_rwlock_t *rwlock , pthread_rwlockattr_t *attr); 
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); 
返回值:若成功,返回0;否則,返回錯誤編號

 

2) 加鎖與解鎖

對讀寫鎖進行讀加鎖、寫加鎖,以及解鎖的3個函數如下: 

#include <pthread.h> 
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock) 
int pthread_rwlock_wdlock(pthread_rwlock_t *rwlock) 
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock) 
返回值:若成功,返回0;否則,返回錯誤編號

 

Single UNIX Specification還定義了讀寫鎖原語的條件版本 

 

#include <pthread.h> 
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock) 
int pthread_rwlock_trywdlock(pthread_rwlock_t *rwlock) 
返回值:若成功,返回0;否則,返回錯誤編號

 

5.4.4 自旋鎖(pthread_spinlock_t)

 

自旋鎖與互斥量類似,但它不是通過休眠使進程阻塞,而是在獲取鎖之前一直處於忙等(自旋)阻塞狀態。自旋鎖可用於以下情況:鎖被持有的時間短,而且線程並不希望在重新調度上花費太多的成本。

自選鎖通常作為底層原語用於實現其他類型的鎖。但是,在用戶層,自旋鎖並不是非常有用,除非運行在不允許搶占的實時調度類中。所以這里不過多敘述.

5.4.5 屏障(pthread_barrier_t)

屏障是用戶協調多個線程並行工作的同步機制。屏障允許每個線程等待,直到所有合作線程都到達某一點,然后從該點繼續執行。

1) 初始化與銷毀

與讀寫鎖類似,屏障不分靜態和動態變量,所有屏障類型變量都需通過pthread_barrier_init進行初始化。

#include <pthread.h> 
int pthread_barrier_init(pthread_barrier_t *barrier, pthread_barrierattr_t *attr, unsigned  int count) 
int pthread_barrier_destroy(pthread_barrier_t *barrier); 

 初始化屏障時,可以使用count參數指定,在允許所有線程繼續運行之前,必須到達屏障的線程數目。  

2)等待

可以使用pthread_barrier_wait函數來表明,線程已完成工作,准備等所有其他線程趕上來。

#include <pthread.h> 
int pthread_barrier_wait(pthread_barrier_t *barrier) 
                                         返回值:若成功,返回0或者PTHREAD_BARRTER_SERIAL_THREAD;否則,返回錯誤編號

調用pthread_barrier_wait的線程在屏障計數(調用pthread_barrier_init時設定)未滿足條件時,會進入休眠狀態。如果該線程是最后一個調用pthread_barrier_wait的線程,就滿足了屏障計數,所有線程都被喚醒。

 

 

 

6 附錄

 

附錄1:fork和exec函數對進程屬性的影響

當進程執行fork和exec會促使子進程繼承父進程很多屬性,下表對fork和exec執行后對進程屬性的影響進行的對比和總結。

進程屬性

exec()

fork()

影響屬性的接口;額外說明

進程地址空間

文本段

û

共享

子進程與父進程共享文本段

棧段

û

ü

函數入口/出口:alloca()、longjmp()、siglongjmp()

數據段和堆段

見注釋

ü

Brk()、sbrk()

環境變量

û

ü

putenv()、setenv();直接修改environ。execle()和execve()隊對其改進,其它exec()調用則會加以保護。

內存映射

û

ü;見注釋

mmap()、munmap()。跨越fork()進程,映射的MAP_NORESERVE標志得以繼承。帶有madvise(MADV_DONTFORK)標志的映射則不會跨fork()繼承

內存鎖

û

û

mlock()、munlock()

進程標識符和憑證

進程ID

ü

û

 

父進程ID

ü

û

 

進程組ID

ü

ü

setpgid()

回話ID

ü

ü

setsid()

實際ID

ü

ü

setuid()、setgid(),以及相關調用

有效和保存設置ID

見注釋

ü

Setuid()、setgid(),以及相關調用

補充組ID

ü

ü

setgroups()、initgroups()

文件、文件IO和目錄

打開文件描述符

見注釋

ü

open(),close()、dup()、pipe()、socket()等。文件描述符在跨越exec()調用的過程中得以保存,除非對其設置了執行時關閉(close-on-exec)標志。父、子進程中的描述符指向相同的打開文件描述符

執行時關閉(close-on-exec)標志

ü (如果關閉)

ü

fcntl(F_SETFD)

文件偏移

ü

共用

lseek()、read()、write()、readv()、writev()。父、子進程共享文件偏移

打開文件狀態標志

ü

共用

Open()、fcntl(F_SETFL)。父子進程共享打開文件狀態標志

異步IO操作

見注釋

û

aio_read()、aio_write()以及相關調用。調用exec()期間,會取消尚未完成的操作

目錄流

û

ü (見注釋)

opendir()、readdir()。SUSv3規定,子級才能獲得父進程目錄流的一份副本,不過這些副本可以(也可以不)共享目錄流的位置。Linux系統不共享目錄流的位置

文件系統

當前工作目錄

ü

ü

chdir()

根目錄

ü

ü

chroot()

文件模型創建掩碼

ü

ü

umask()

信號

信號設置

見注釋

ü

Signal()、sigaction()。將處置設置成默認或忽略的信號在執行exec()期間保持不變;已捕獲的信號會恢復為默認處置

信號掩碼

ü

ü

信號傳遞;sigprocmask()、sigaction()

掛起(pending)信號集合

ü

û

信號傳遞;raise()、kill()、sigqueue()

備選信號棧

û

ü

sigaltstack()

定時器

間隔定時器

ü

û

setitimer()

由alarm()設置的定時器

ü

û

alarm()

POSIX定時器

û

û

timer_create()及其相關調用

POSIX線程

線程

û

見注釋

fork()調用期間,子進程只會復制調用線程

線程可撤銷狀態與類型

û

ü

Exec()之后,將可撤銷類型和狀態分別重置為PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DEFERRED

互斥量與條件變量

û

ü

關於調用fork()期間對互斥量

優先級與調度

nice值

ü

ü

nice()、setpriority()

調度策略及優先級

ü

ü

sched_setcheduler()、sched_setparam()

資源與CPU時間

資源限制

ü

û

setrlimit()

進程和子進程的CPU時間

ü

û

由times()返回

資源使用量

ü

û

由getrusage()返回

進程間通信

System V共享內存段

û

ü

shmat()、shemdt()

POSIX共享內存段

û

ü

shm_open()及相關調用

POSIX消息隊列

û

ü

mq_open()及相關調用。父子進程的描述符都指向同一打開消息隊列描述。子進程並不繼承父進程的消息通知注冊消息

POSIX命名信號量

û

共用

sem_open()及其相關調用。子進程與父進程共享對相同信號量的引用

POSIX未命名信號量

û

見注釋

sem_init()及其相關調用。如果信號量位於共享內存區域,那么子進程與父進程共享信號量;否則,子進程擁有屬於自己的信號量拷貝。

System V信號量調整

ü

û

 

文件鎖

ü

見注釋

flock()。子進程自父進程處繼承對同一鎖的引用

記錄鎖

見注釋

û

fcntl(F_SETLK)。除非將指代文件的文件描述符標記為執行時關閉,否則會跨越exec()對鎖加以保護

雜項

地區設置

û

ü

setlocale()。作為C運行時初始化的一部分,執行新程序后會調用setlocale(LC_ALL,"C")的等效函數

浮點環境

û

ü

運行新程序時,將浮點環境狀態重置為默認值,參考fenv

控制終端

ü

ü

 

退出處理器程序

û

ü

atexit()、on_exit()

Linux特有

文件系統ID

見注釋

ü

setfsuid()、setfsgid()。一旦相應的有效ID發生變化,那么這些ID也會隨之改變

timeerfd定時器

ü

見注釋

timerfd_create(),子進程繼承的文件描述符與父進程指向相同的定時器

能力

見注釋

ü

capset()。

功能外延集合

ü

ü

 

能力安全位(securebits)標志

見注釋

ü

執行exec()期間,會保全所有的安全位標志,SECBIT_KEEP_CAPS除外,總是會清除該標志

CPU黏性(affinity)

ü

ü

sched_setaffinity()

SCHED_RESET_ON_FORK

ü

ü

 

允許的CPU

ü

ü

 

允許的內存節點

ü

ü

 

內存策略

ü

ü

 

文件租約

ü

ü

Fcntl(F_SETLEASE)。子進程從父進程處繼承對相同租約的引用

目錄變更通知

ü

û

dnotify API,通過fcntl(F_NOTIFY)來實現支持

prctl(PR_SET_DUMPABLE)

見注釋

ü

exec()執行期間會設置PR_SET_DUMPABLE標志,自行設置用戶或組ID程序的情況除外,此時將清除該標志

prctl(PR_SET_PDEATHIG)

ü

û

 

prctl(PR_SET_NAME)

û

ü

 

oom_adj

ü

ü

 

coredump_filter

ü

ü

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM