Linux程式間通訊——使用訊號量

後開啟撒打發了發表於2018-01-10
這篇文章將講述別一種程式間通訊的機制——訊號量。注意請不要把它與之前所說的訊號混淆起來,訊號與訊號量是不同的兩種事物。有關訊號的更多內容,可以閱讀我的另一篇文章:Linux程式間通訊——使用訊號。下面就進入訊號量的講解。

一、什麼是訊號量
為了防止出現因多個程式同時訪問一個共享資源而引發的一系列問題,我們需要一種方法,它可以通過生成並使用令牌來授權,在任一時刻只能有一個執行執行緒訪問程式碼的臨界區域。臨界區域是指執行資料更新的程式碼需要獨佔式地執行。而訊號量就可以提供這樣的一種訪問機制,讓一個臨界區同一時間只有一個執行緒在訪問它,也就是說訊號量是用來調協程式對共享資源的訪問的。

訊號量是一個特殊的變數,程式對其訪問都是原子操作,且只允許對它進行等待(即P(訊號變數))和傳送(即V(訊號變數))資訊操作。最簡單的訊號量是隻能取0和1的變數,這也是訊號量最常見的一種形式,叫做二進位制訊號量。而可以取多個正整數的訊號量被稱為通用訊號量。這裡主要討論二進位制訊號量。

二、訊號量的工作原理
由於訊號量只能進行兩種操作等待和傳送訊號,即P(sv)和V(sv),他們的行為是這樣的:
P(sv):如果sv的值大於零,就給它減1;如果它的值為零,就掛起該程式的執行
V(sv):如果有其他程式因等待sv而被掛起,就讓它恢復執行,如果沒有程式因等待sv而掛起,就給它加1.

舉個例子,就是兩個程式共享訊號量sv,一旦其中一個程式執行了P(sv)操作,它將得到訊號量,並可以進入臨界區,使sv減1。而第二個程式將被阻止進入臨界區,因為當它試圖執行P(sv)時,sv為0,它會被掛起以等待第一個程式離開臨界區域並執行V(sv)釋放訊號量,這時第二個程式就可以恢復執行。

三、Linux的訊號量機制
Linux提供了一組精心設計的訊號量介面來對訊號進行操作,它們不只是針對二進位制訊號量,下面將會對這些函式進行介紹,但請注意,這些函式都是用來對成組的訊號量值進行操作的。它們宣告在標頭檔案sys/sem.h中。

1、semget函式
它的作用是建立一個新訊號量或取得一個已有訊號量,原型為:
[cpp] view plain copy
  1. int semget(key_t key, int num_sems, int sem_flags);  
第一個引數key是整數值(唯一非零),不相關的程式可以通過它訪問一個訊號量,它代表程式可能要使用的某個資源,程式對所有訊號量的訪問都是間接的,程式先通過呼叫semget函式並提供一個鍵,再由系統生成一個相應的訊號識別符號(semget函式的返回值),只有semget函式才直接使用訊號量鍵,所有其他的訊號量函式使用由semget函式返回的訊號量識別符號。如果多個程式使用相同的key值,key將負責協調工作。

第二個引數num_sems指定需要的訊號量數目,它的值幾乎總是1。

第三個引數sem_flags是一組標誌,當想要當訊號量不存在時建立一個新的訊號量,可以和值IPC_CREAT做按位或操作。設定了IPC_CREAT標誌後,即使給出的鍵是一個已有訊號量的鍵,也不會產生錯誤。而IPC_CREAT | IPC_EXCL則可以建立一個新的,唯一的訊號量,如果訊號量已存在,返回一個錯誤。

semget函式成功返回一個相應訊號識別符號(非零),失敗返回-1.

2、semop函式
它的作用是改變訊號量的值,原型為:
[cpp] view plain copy
  1. int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);  
sem_id是由semget返回的訊號量識別符號,sembuf結構的定義如下:
[cpp] view plain copy
  1. struct sembuf{  
  2.     short sem_num;//除非使用一組訊號量,否則它為0  
  3.     short sem_op;//訊號量在一次操作中需要改變的資料,通常是兩個數,一個是-1,即P(等待)操作,  
  4.                     //一個是+1,即V(傳送訊號)操作。  
  5.     short sem_flg;//通常為SEM_UNDO,使作業系統跟蹤訊號,  
  6.                     //並在程式沒有釋放該訊號量而終止時,作業系統釋放訊號量  
  7. };  
3、semctl函式
該函式用來直接控制訊號量資訊,它的原型為:
[cpp] view plain copy
  1. int semctl(int sem_id, int sem_num, int command, ...);  
如果有第四個引數,它通常是一個union semum結構,定義如下:
[cpp] view plain copy
  1. union semun{  
  2.     int val;  
  3.     struct semid_ds *buf;  
  4.     unsigned short *arry;  
  5. };  
前兩個引數與前面一個函式中的一樣,command通常是下面兩個值中的其中一個
SETVAL:用來把訊號量初始化為一個已知的值。p 這個值通過union semun中的val成員設定,其作用是在訊號量第一次使用前對它進行設定。
IPC_RMID:用於刪除一個已經無需繼續使用的訊號量識別符號。

四、程式使用訊號量通訊
下面使用一個例子來說明程式間如何使用訊號量來進行通訊,這個例子是兩個相同的程式同時向螢幕輸出資料,我們可以看到如何使用訊號量來使兩個程式協調工作,使同一時間只有一個程式可以向螢幕輸出資料。注意,如果程式是第一次被呼叫(為了區分,第一次呼叫程式時帶一個要輸出到螢幕中的字元作為一個引數),則需要呼叫set_semvalue函式初始化訊號並將message字元設定為傳遞給程式的引數的第一個字元,同時第一個啟動的程式還負責訊號量的刪除工作。如果不刪除訊號量,它將繼續在系統中存在,即使程式已經退出,它可能在你下次執行此程式時引發問題,而且訊號量是一種有限的資源。

在main函式中呼叫semget來建立一個訊號量,該函式將返回一個訊號量識別符號,儲存於全域性變數sem_id中,然後以後的函式就使用這個識別符號來訪問訊號量。

原始檔為seml.c,程式碼如下:
[cpp] view plain copy
  1. #include <unistd.h>  
  2. #include <sys/types.h>  
  3. #include <sys/stat.h>  
  4. #include <fcntl.h>  
  5. #include <stdlib.h>  
  6. #include <stdio.h>  
  7. #include <string.h>  
  8. #include <sys/sem.h>  
  9.   
  10. union semun  
  11. {  
  12.     int val;  
  13.     struct semid_ds *buf;  
  14.     unsigned short *arry;  
  15. };  
  16.   
  17. static int sem_id = 0;  
  18.   
  19. static int set_semvalue();  
  20. static void del_semvalue();  
  21. static int semaphore_p();  
  22. static int semaphore_v();  
  23.   
  24. int main(int argc, char *argv[])  
  25. {  
  26.     char message = 'X';  
  27.     int i = 0;  
  28.   
  29.     //建立訊號量  
  30.     sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);  
  31.   
  32.     if(argc > 1)  
  33.     {  
  34.         //程式第一次被呼叫,初始化訊號量  
  35.         if(!set_semvalue())  
  36.         {  
  37.             fprintf(stderr, "Failed to initialize semaphore\n");  
  38.             exit(EXIT_FAILURE);  
  39.         }  
  40.         //設定要輸出到螢幕中的資訊,即其引數的第一個字元  
  41.         message = argv[1][0];  
  42.         sleep(2);  
  43.     }  
  44.     for(i = 0; i < 10; ++i)  
  45.     {  
  46.         //進入臨界區  
  47.         if(!semaphore_p())  
  48.             exit(EXIT_FAILURE);  
  49.         //向螢幕中輸出資料  
  50.         printf("%c", message);  
  51.         //清理緩衝區,然後休眠隨機時間  
  52.         fflush(stdout);  
  53.         sleep(rand() % 3);  
  54.         //離開臨界區前再一次向螢幕輸出資料  
  55.         printf("%c", message);  
  56.         fflush(stdout);  
  57.         //離開臨界區,休眠隨機時間後繼續迴圈  
  58.         if(!semaphore_v())  
  59.             exit(EXIT_FAILURE);  
  60.         sleep(rand() % 2);  
  61.     }  
  62.   
  63.     sleep(10);  
  64.     printf("\n%d - finished\n", getpid());  
  65.   
  66.     if(argc > 1)  
  67.     {  
  68.         //如果程式是第一次被呼叫,則在退出前刪除訊號量  
  69.         sleep(3);  
  70.         del_semvalue();  
  71.     }  
  72.     exit(EXIT_SUCCESS);  
  73. }  
  74.   
  75. static int set_semvalue()  
  76. {  
  77.     //用於初始化訊號量,在使用訊號量前必須這樣做  
  78.     union semun sem_union;  
  79.   
  80.     sem_union.val = 1;  
  81.     if(semctl(sem_id, 0, SETVAL, sem_union) == -1)  
  82.         return 0;  
  83.     return 1;  
  84. }  
  85.   
  86. static void del_semvalue()  
  87. {  
  88.     //刪除訊號量  
  89.     union semun sem_union;  
  90.   
  91.     if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)  
  92.         fprintf(stderr, "Failed to delete semaphore\n");  
  93. }  
  94.   
  95. static int semaphore_p()  
  96. {  
  97.     //對訊號量做減1操作,即等待P(sv)  
  98.     struct sembuf sem_b;  
  99.     sem_b.sem_num = 0;  
  100.     sem_b.sem_op = -1;//P()  
  101.     sem_b.sem_flg = SEM_UNDO;  
  102.     if(semop(sem_id, &sem_b, 1) == -1)  
  103.     {  
  104.         fprintf(stderr, "semaphore_p failed\n");  
  105.         return 0;  
  106.     }  
  107.     return 1;  
  108. }  
  109.   
  110. static int semaphore_v()  
  111. {  
  112.     //這是一個釋放操作,它使訊號量變為可用,即傳送訊號V(sv)  
  113.     struct sembuf sem_b;  
  114.     sem_b.sem_num = 0;  
  115.     sem_b.sem_op = 1;//V()  
  116.     sem_b.sem_flg = SEM_UNDO;  
  117.     if(semop(sem_id, &sem_b, 1) == -1)  
  118.     {  
  119.         fprintf(stderr, "semaphore_v failed\n");  
  120.         return 0;  
  121.     }  
  122.     return 1;  
  123. }  
執行結果如下:


注:這個程式的臨界區為main函式for迴圈不的semaphore_p和semaphore_v函式中間的程式碼。

例子分析 :同時執行一個程式的兩個例項,注意第一次執行時,要加上一個字元作為引數,例如本例中的字元‘O’,它用於區分是否為第一次呼叫,同時這個字元輸出到螢幕中。因為每個程式都在其進入臨界區後和離開臨界區前列印一個字元,所以每個字元都應該成對出現,正如你看到的上圖的輸出那樣。在main函式中迴圈中我們可以看到,每次程式要訪問stdout(標準輸出),即要輸出字元時,每次都要檢查訊號量是否可用(即stdout有沒有正在被其他程式使用)。所以,當一個程式A在呼叫函式semaphore_p進入了臨界區,輸出字元後,呼叫sleep時,另一個程式B可能想訪問stdout,但是訊號量的P請求操作失敗,只能掛起自己的執行,當程式A呼叫函式semaphore_v離開了臨界區,程式B馬上被恢復執行。然後程式A和程式B就這樣一直迴圈了10次。

五、對比例子——程式間的資源競爭
看了上面的例子,你可能還不是很明白,不過沒關係,下面我就以另一個例子來說明一下,它實現的功能與前面的例子一樣,執行方式也一樣,都是兩個相同的程式,同時向stdout中輸出字元,只是沒有使用訊號量,兩個程式在互相競爭stdout。它的程式碼非常簡單,檔名為normalprint.c,程式碼如下:
[cpp] view plain copy
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3.   
  4. int main(int argc, char *argv[])  
  5. {  
  6.     char message = 'X';  
  7.     int i = 0;    
  8.     if(argc > 1)  
  9.         message = argv[1][0];  
  10.     for(i = 0; i < 10; ++i)  
  11.     {  
  12.         printf("%c", message);  
  13.         fflush(stdout);  
  14.         sleep(rand() % 3);  
  15.         printf("%c", message);  
  16.         fflush(stdout);  
  17.         sleep(rand() % 2);  
  18.     }  
  19.     sleep(10);  
  20.     printf("\n%d - finished\n", getpid());  
  21.     exit(EXIT_SUCCESS);  
  22. }  
執行結果如下:


例子分析
從上面的輸出結果,我們可以看到字元‘X’和‘O’並不像前面的例子那樣,總是成對出現,因為當第一個程式A輸出了字元後,呼叫sleep休眠時,另一個程式B立即輸出並休眠,而程式A醒來時,再繼續執行輸出,同樣的程式B也是如此。所以輸出的字元就是不成對的出現。這兩個程式在競爭stdout這一共同的資源。通過兩個例子的對比,我想訊號量的意義和使用應該比較清楚了。

六、訊號量的總結
訊號量是一個特殊的變數,程式對其訪問都是原子操作,且只允許對它進行等待(即P(訊號變數))和傳送(即V(訊號變數))資訊操作。我們通常通過訊號來解決多個程式對同一資源的訪問競爭的問題,使在任一時刻只能有一個執行執行緒訪問程式碼的臨界區域,也可以說它是協調程式間的對同一資源的訪問權,也就是用於同步程式的。

相關文章