詳解Linux進程間通信——使用信號量
一、什么是信號量
為了防止出現(xiàn)因多個程序同時訪問一個共享資源而引發(fā)的一系列問題,我們需要一種方法,它可以通過生成并使用令牌來授權(quán),在任一時刻只能有一個執(zhí)行線程訪問代碼的臨界區(qū)域。臨界區(qū)域是指執(zhí)行數(shù)據(jù)更新的代碼需要獨占式地執(zhí)行。而信號量就可以提供這樣的一種訪問機制,讓一個臨界區(qū)同一時間只有一個線程在訪問它,也就是說信號量是用來調(diào)協(xié)進程對共享資源的訪問的。
信號量是一個特殊的變量,程序?qū)ζ湓L問都是原子操作,且只允許對它進行等待(即P(信號變量))和發(fā)送(即V(信號變量))信息操作。最簡單的信號量是只能取0和1的變量,這也是信號量最常見的一種形式,叫做二進制信號量。而可以取多個正整數(shù)的信號量被稱為通用信號量。這里主要討論二進制信號量。
二、信號量的工作原理
由于信號量只能進行兩種操作等待和發(fā)送信號,即P(sv)和V(sv),他們的行為是這樣的:
P(sv):如果sv的值大于零,就給它減1;如果它的值為零,就掛起該進程的執(zhí)行
V(sv):如果有其他進程因等待sv而被掛起,就讓它恢復運行,如果沒有進程因等待sv而掛起,就給它加1.
舉個例子,就是兩個進程共享信號量sv,一旦其中一個進程執(zhí)行了P(sv)操作,它將得到信號量,并可以進入臨界區(qū),使sv減1。而第二個進程將被阻止進入臨界區(qū),因為當它試圖執(zhí)行P(sv)時,sv為0,它會被掛起以等待第一個進程離開臨界區(qū)域并執(zhí)行V(sv)釋放信號量,這時第二個進程就可以恢復執(zhí)行。
三、Linux的信號量機制
Linux提供了一組精心設(shè)計的信號量接口來對信號進行操作,它們不只是針對二進制信號量,下面將會對這些函數(shù)進行介紹,但請注意,這些函數(shù)都是用來對成組的信號量值進行操作的。它們聲明在頭文件sys/sem.h中。
1、semget函數(shù)
它的作用是創(chuàng)建一個新信號量或取得一個已有信號量,原型為:
int semget(key_t key, int num_sems, int sem_flags);
第一個參數(shù)key是整數(shù)值(唯一非零),不相關(guān)的進程可以通過它訪問一個信號量,它代表程序可能要使用的某個資源,程序?qū)λ行盘柫康脑L問都是間接的,程序先通過調(diào)用semget函數(shù)并提供一個鍵,再由系統(tǒng)生成一個相應(yīng)的信號標識符(semget函數(shù)的返回值),只有semget函數(shù)才直接使用信號量鍵,所有其他的信號量函數(shù)使用由semget函數(shù)返回的信號量標識符。如果多個程序使用相同的key值,key將負責協(xié)調(diào)工作。
第二個參數(shù)num_sems指定需要的信號量數(shù)目,它的值幾乎總是1。
第三個參數(shù)sem_flags是一組標志,當想要當信號量不存在時創(chuàng)建一個新的信號量,可以和值IPC_CREAT做按位或操作。設(shè)置了IPC_CREAT標志后,即使給出的鍵是一個已有信號量的鍵,也不會產(chǎn)生錯誤。而IPC_CREAT | IPC_EXCL則可以創(chuàng)建一個新的,唯一的信號量,如果信號量已存在,返回一個錯誤。
semget函數(shù)成功返回一個相應(yīng)信號標識符(非零),失敗返回-1.
2、semop函數(shù)
它的作用是改變信號量的值,原型為:
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
sem_id是由semget返回的信號量標識符,sembuf結(jié)構(gòu)的定義如下:
struct sembuf{ short sem_num;//除非使用一組信號量,否則它為0 short sem_op;//信號量在一次操作中需要改變的數(shù)據(jù),通常是兩個數(shù),一個是-1,即P(等待)操作, //一個是+1,即V(發(fā)送信號)操作。 short sem_flg;//通常為SEM_UNDO,使操作系統(tǒng)跟蹤信號, //并在進程沒有釋放該信號量而終止時,操作系統(tǒng)釋放信號量 };
3、semctl函數(shù)
該函數(shù)用來直接控制信號量信息,它的原型為:
int semctl(int sem_id, int sem_num, int command, ...);
如果有第四個參數(shù),它通常是一個union semum結(jié)構(gòu),定義如下:
union semun{ int val; struct semid_ds *buf; unsigned short *arry; };
前兩個參數(shù)與前面一個函數(shù)中的一樣,command通常是下面兩個值中的其中一個
- SETVAL:用來把信號量初始化為一個已知的值。p 這個值通過union semun中的val成員設(shè)置,其作用是在信號量第一次使用前對它進行設(shè)置。
- IPC_RMID:用于刪除一個已經(jīng)無需繼續(xù)使用的信號量標識符。
四、進程使用信號量通信
下面使用一個例子來說明進程間如何使用信號量來進行通信,這個例子是兩個相同的程序同時向屏幕輸出數(shù)據(jù),我們可以看到如何使用信號量來使兩個進程協(xié)調(diào)工作,使同一時間只有一個進程可以向屏幕輸出數(shù)據(jù)。注意,如果程序是第一次被調(diào)用(為了區(qū)分,第一次調(diào)用程序時帶一個要輸出到屏幕中的字符作為一個參數(shù)),則需要調(diào)用set_semvalue函數(shù)初始化信號并將message字符設(shè)置為傳遞給程序的參數(shù)的第一個字符,同時第一個啟動的進程還負責信號量的刪除工作。如果不刪除信號量,它將繼續(xù)在系統(tǒng)中存在,即使程序已經(jīng)退出,它可能在你下次運行此程序時引發(fā)問題,而且信號量是一種有限的資源。
在main函數(shù)中調(diào)用semget來創(chuàng)建一個信號量,該函數(shù)將返回一個信號量標識符,保存于全局變量sem_id中,然后以后的函數(shù)就使用這個標識符來訪問信號量。
源文件為seml.c,代碼如下:
#include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <sys/sem.h> union semun { int val; struct semid_ds *buf; unsigned short *arry; }; static int sem_id = 0; static int set_semvalue(); static void del_semvalue(); static int semaphore_p(); static int semaphore_v(); int main(int argc, char *argv[]) { char message = 'X'; int i = 0; //創(chuàng)建信號量 sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT); if(argc > 1) { //程序第一次被調(diào)用,初始化信號量 if(!set_semvalue()) { fprintf(stderr, "Failed to initialize semaphore\n"); exit(EXIT_FAILURE); } //設(shè)置要輸出到屏幕中的信息,即其參數(shù)的第一個字符 message = argv[1][0]; sleep(2); } for(i = 0; i < 10; ++i) { //進入臨界區(qū) if(!semaphore_p()) exit(EXIT_FAILURE); //向屏幕中輸出數(shù)據(jù) printf("%c", message); //清理緩沖區(qū),然后休眠隨機時間 fflush(stdout); sleep(rand() % 3); //離開臨界區(qū)前再一次向屏幕輸出數(shù)據(jù) printf("%c", message); fflush(stdout); //離開臨界區(qū),休眠隨機時間后繼續(xù)循環(huán) if(!semaphore_v()) exit(EXIT_FAILURE); sleep(rand() % 2); } sleep(10); printf("\n%d - finished\n", getpid()); if(argc > 1) { //如果程序是第一次被調(diào)用,則在退出前刪除信號量 sleep(3); del_semvalue(); } exit(EXIT_SUCCESS); } static int set_semvalue() { //用于初始化信號量,在使用信號量前必須這樣做 union semun sem_union; sem_union.val = 1; if(semctl(sem_id, 0, SETVAL, sem_union) == -1) return 0; return 1; } static void del_semvalue() { //刪除信號量 union semun sem_union; if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1) fprintf(stderr, "Failed to delete semaphore\n"); } static int semaphore_p() { //對信號量做減1操作,即等待P(sv) struct sembuf sem_b; sem_b.sem_num = 0; sem_b.sem_op = -1;//P() sem_b.sem_flg = SEM_UNDO; if(semop(sem_id, &sem_b, 1) == -1) { fprintf(stderr, "semaphore_p failed\n"); return 0; } return 1; } static int semaphore_v() { //這是一個釋放操作,它使信號量變?yōu)榭捎?,即發(fā)送信號V(sv) struct sembuf sem_b; sem_b.sem_num = 0; sem_b.sem_op = 1;//V() sem_b.sem_flg = SEM_UNDO; if(semop(sem_id, &sem_b, 1) == -1) { fprintf(stderr, "semaphore_v failed\n"); return 0; } return 1; }
運行結(jié)果如下:
注:這個程序的臨界區(qū)為main函數(shù)for循環(huán)不的semaphore_p和semaphore_v函數(shù)中間的代碼。
例子分析 :同時運行一個程序的兩個實例,注意第一次運行時,要加上一個字符作為參數(shù),例如本例中的字符‘O',它用于區(qū)分是否為第一次調(diào)用,同時這個字符輸出到屏幕中。因為每個程序都在其進入臨界區(qū)后和離開臨界區(qū)前打印一個字符,所以每個字符都應(yīng)該成對出現(xiàn),正如你看到的上圖的輸出那樣。在main函數(shù)中循環(huán)中我們可以看到,每次進程要訪問stdout(標準輸出),即要輸出字符時,每次都要檢查信號量是否可用(即stdout有沒有正在被其他進程使用)。
所以,當一個進程A在調(diào)用函數(shù)semaphore_p進入了臨界區(qū),輸出字符后,調(diào)用sleep時,另一個進程B可能想訪問stdout,但是信號量的P請求操作失敗,只能掛起自己的執(zhí)行,當進程A調(diào)用函數(shù)semaphore_v離開了臨界區(qū),進程B馬上被恢復執(zhí)行。然后進程A和進程B就這樣一直循環(huán)了10次。
五、對比例子——進程間的資源競爭
看了上面的例子,你可能還不是很明白,不過沒關(guān)系,下面我就以另一個例子來說明一下,它實現(xiàn)的功能與前面的例子一樣,運行方式也一樣,都是兩個相同的進程,同時向stdout中輸出字符,只是沒有使用信號量,兩個進程在互相競爭stdout。它的代碼非常簡單,文件名為normalprint.c,代碼如下:
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { char message = 'X'; int i = 0; if(argc > 1) message = argv[1][0]; for(i = 0; i < 10; ++i) { printf("%c", message); fflush(stdout); sleep(rand() % 3); printf("%c", message); fflush(stdout); sleep(rand() % 2); } sleep(10); printf("\n%d - finished\n", getpid()); exit(EXIT_SUCCESS); }
運行結(jié)果如下:
例子分析:
從上面的輸出結(jié)果,我們可以看到字符‘X'和‘O'并不像前面的例子那樣,總是成對出現(xiàn),因為當?shù)谝粋€進程A輸出了字符后,調(diào)用sleep休眠時,另一個進程B立即輸出并休眠,而進程A醒來時,再繼續(xù)執(zhí)行輸出,同樣的進程B也是如此。所以輸出的字符就是不成對的出現(xiàn)。這兩個進程在競爭stdout這一共同的資源。通過兩個例子的對比,我想信號量的意義和使用應(yīng)該比較清楚了。
六、信號量的總結(jié)
信號量是一個特殊的變量,程序?qū)ζ湓L問都是原子操作,且只允許對它進行等待(即P(信號變量))和發(fā)送(即V(信號變量))信息操作。我們通常通過信號來解決多個進程對同一資源的訪問競爭的問題,使在任一時刻只能有一個執(zhí)行線程訪問代碼的臨界區(qū)域,也可以說它是協(xié)調(diào)進程間的對同一資源的訪問權(quán),也就是用于同步進程的。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Linux如何修改hosts文件并刷新DNS生效hosts文件
這篇文章主要介紹了Linux如何修改hosts文件并刷新DNS生效hosts文件問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07在Linux分區(qū)或邏輯卷中創(chuàng)建文件系統(tǒng)的方法
這篇文章主要給大家介紹了關(guān)于如何在Linux分區(qū)或邏輯卷中創(chuàng)建文件系統(tǒng)的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用Linux具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧2019-04-04解決hadoop啟動報錯ERROR: Attempting to operate 
這篇文章主要介紹了解決hadoop啟動報錯ERROR: Attempting to operate on hdfs namenode as root的方法,hadoop-3.1.0啟動hadoop集群時還有可能可能會報如下錯誤,需要的朋友可以參考下2023-03-03