Linux進程間通訊——使用信號量

這篇文章將講述別一種進程間通訊的機制——信號量。注意請不要把它與以前所說的信號混淆起來,信號與信號量是不一樣的兩種事物。有關信號的更多內容,能夠閱讀個人另外一篇文章:Linux進程間通訊——使用信號。下面就進入信號量的講解。函數

 
1、什麼是信號量
爲了防止出現因多個程序同時訪問一個共享資源而引起的一系列問題,咱們須要一種方法,它能夠經過生成並使用令牌來受權,在任一時刻只能有一個執行線程訪問代碼的臨界區域。臨界區域是指執行數據更新的代碼須要獨佔式地執行。而信號量就能夠提供這樣的一種訪問機制,讓一個臨界區同一時間只有一個線程在訪問它,也就是說信號量是用來調協進程對共享資源的訪問的。
 
信號量是一個特殊的變量,程序對其訪問都是原子操做,且只容許對它進行等待(即P(信號變量))和發送(即V(信號變量))信息操做。最簡單的信號量是隻能取0和1的變量,這也是信號量最多見的一種形式,叫作二進制信號量。而能夠取多個正整數的信號量被稱爲通用信號量。這裏主要討論二進制信號量。
 
2、信號量的工做原理
因爲信號量只能進行兩種操做等待和發送信號,即P(sv)和V(sv),他們的行爲是這樣的:
P(sv):若是sv的值大於零,就給它減1;若是它的值爲零,就掛起該進程的執行
V(sv):若是有其餘進程因等待sv而被掛起,就讓它恢復運行,若是沒有進程因等待sv而掛起,就給它加1.
 
舉個例子,就是兩個進程共享信號量sv,一旦其中一個進程執行了P(sv)操做,它將獲得信號量,並能夠進入臨界區,使sv減1。而第二個進程將被阻止進入臨界區,由於當它試圖執行P(sv)時,sv爲0,它會被掛起以等待第一個進程離開臨界區域並執行V(sv)釋放信號量,這時第二個進程就能夠恢復執行。
 
3、Linux的信號量機制
Linux提供了一組精心設計的信號量接口來對信號進行操做,它們不僅是針對二進制信號量,下面將會對這些函數進行介紹,但請注意,這些函數都是用來對成組的信號量值進行操做的。它們聲明在頭文件sys/sem.h中。
 
一、semget函數
它的做用是建立一個新信號量或取得一個已有信號量,原型爲:
[cpp]  view plain  copy
 
 print?
  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.
 
二、semop函數
它的做用是改變信號量的值,原型爲:
[cpp]  view plain  copy
 
 print?
  1. int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);  
sem_id是由semget返回的信號量標識符,sembuf結構的定義以下:
 
[cpp]  view plain  copy
 
 print?
  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. };  
三、semctl函數
該函數用來直接控制信號量信息,它的原型爲:
[cpp]  view plain  copy
 
 print?
  1. int semctl(int sem_id, int sem_num, int command, ...);  
若是有第四個參數,它一般是一個union semum結構,定義以下:
 
[cpp]  view plain  copy
 
 print?
  1. union semun{  
  2.     int val;  
  3.     struct semid_ds *buf;  
  4.     unsigned short *arry;  
  5. };  
前兩個參數與前面一個函數中的同樣,command一般是下面兩個值中的其中一個
SETVAL:用來把信號量初始化爲一個已知的值。p 這個值經過union semun中的val成員設置,其做用是在信號量第一次使用前對它進行設置。
IPC_RMID:用於刪除一個已經無需繼續使用的信號量標識符。
 
4、進程使用信號量通訊
下面使用一個例子來講明進程間如何使用信號量來進行通訊,這個例子是兩個相同的程序同時向屏幕輸出數據,咱們能夠看到如何使用信號量來使兩個進程協調工做,使同一時間只有一個進程能夠向屏幕輸出數據。注意,若是程序是第一次被調用(爲了區分,第一次調用程序時帶一個要輸出到屏幕中的字符做爲一個參數),則須要調用set_semvalue函數初始化信號並將message字符設置爲傳遞給程序的參數的第一個字符,同時第一個啓動的進程還負責信號量的刪除工做。若是不刪除信號量,它將繼續在系統中存在,即便程序已經退出,它可能在你下次運行此程序時引起問題,並且信號量是一種有限的資源。
 
在main函數中調用semget來建立一個信號量,該函數將返回一個信號量標識符,保存於全局變量sem_id中,而後之後的函數就使用這個標識符來訪問信號量。
 
源文件爲seml.c,代碼以下:
 
[cpp]  view plain  copy
 
 print?
  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次。
 
5、對比例子——進程間的資源競爭
看了上面的例子,你可能還不是很明白,不過不要緊,下面我就以另外一個例子來講明一下,它實現的功能與前面的例子同樣,運行方式也同樣,都是兩個相同的進程,同時向stdout中輸出字符,只是沒有使用信號量,兩個進程在互相競爭stdout。它的代碼很是簡單,文件名爲normalprint.c,代碼以下:
 
[cpp]  view plain  copy
 
 print?
  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這一共同的資源。經過兩個例子的對比,我想信號量的意義和使用應該比較清楚了。
 
6、信號量的總結
信號量是一個特殊的變量,程序對其訪問都是原子操做,且只容許對它進行等待(即P(信號變量))和發送(即V(信號變量))信息操做。咱們一般經過信號來解決多個進程對同一資源的訪問競爭的問題,使在任一時刻只能有一個執行線程訪問代碼的臨界區域,也能夠說它是協調進程間的對同一資源的訪問權,也就是用於同步進程的。
相關文章
相關標籤/搜索