詳解Linux進(jìn)程間通信——使用信號(hào)量
一、什么是信號(hào)量
為了防止出現(xiàn)因多個(gè)程序同時(shí)訪問一個(gè)共享資源而引發(fā)的一系列問題,我們需要一種方法,它可以通過生成并使用令牌來授權(quán),在任一時(shí)刻只能有一個(gè)執(zhí)行線程訪問代碼的臨界區(qū)域。臨界區(qū)域是指執(zhí)行數(shù)據(jù)更新的代碼需要獨(dú)占式地執(zhí)行。而信號(hào)量就可以提供這樣的一種訪問機(jī)制,讓一個(gè)臨界區(qū)同一時(shí)間只有一個(gè)線程在訪問它,也就是說信號(hào)量是用來調(diào)協(xié)進(jìn)程對(duì)共享資源的訪問的。
信號(hào)量是一個(gè)特殊的變量,程序?qū)ζ湓L問都是原子操作,且只允許對(duì)它進(jìn)行等待(即P(信號(hào)變量))和發(fā)送(即V(信號(hào)變量))信息操作。最簡(jiǎn)單的信號(hào)量是只能取0和1的變量,這也是信號(hào)量最常見的一種形式,叫做二進(jìn)制信號(hào)量。而可以取多個(gè)正整數(shù)的信號(hào)量被稱為通用信號(hào)量。這里主要討論二進(jìn)制信號(hào)量。
二、信號(hào)量的工作原理
由于信號(hào)量只能進(jìn)行兩種操作等待和發(fā)送信號(hào),即P(sv)和V(sv),他們的行為是這樣的:
P(sv):如果sv的值大于零,就給它減1;如果它的值為零,就掛起該進(jìn)程的執(zhí)行
V(sv):如果有其他進(jìn)程因等待sv而被掛起,就讓它恢復(fù)運(yùn)行,如果沒有進(jìn)程因等待sv而掛起,就給它加1.
舉個(gè)例子,就是兩個(gè)進(jìn)程共享信號(hào)量sv,一旦其中一個(gè)進(jìn)程執(zhí)行了P(sv)操作,它將得到信號(hào)量,并可以進(jìn)入臨界區(qū),使sv減1。而第二個(gè)進(jìn)程將被阻止進(jìn)入臨界區(qū),因?yàn)楫?dāng)它試圖執(zhí)行P(sv)時(shí),sv為0,它會(huì)被掛起以等待第一個(gè)進(jìn)程離開臨界區(qū)域并執(zhí)行V(sv)釋放信號(hào)量,這時(shí)第二個(gè)進(jìn)程就可以恢復(fù)執(zhí)行。
三、Linux的信號(hào)量機(jī)制
Linux提供了一組精心設(shè)計(jì)的信號(hào)量接口來對(duì)信號(hào)進(jìn)行操作,它們不只是針對(duì)二進(jìn)制信號(hào)量,下面將會(huì)對(duì)這些函數(shù)進(jìn)行介紹,但請(qǐng)注意,這些函數(shù)都是用來對(duì)成組的信號(hào)量值進(jìn)行操作的。它們聲明在頭文件sys/sem.h中。
1、semget函數(shù)
它的作用是創(chuàng)建一個(gè)新信號(hào)量或取得一個(gè)已有信號(hào)量,原型為:
int semget(key_t key, int num_sems, int sem_flags);
第一個(gè)參數(shù)key是整數(shù)值(唯一非零),不相關(guān)的進(jìn)程可以通過它訪問一個(gè)信號(hào)量,它代表程序可能要使用的某個(gè)資源,程序?qū)λ行盘?hào)量的訪問都是間接的,程序先通過調(diào)用semget函數(shù)并提供一個(gè)鍵,再由系統(tǒng)生成一個(gè)相應(yīng)的信號(hào)標(biāo)識(shí)符(semget函數(shù)的返回值),只有semget函數(shù)才直接使用信號(hào)量鍵,所有其他的信號(hào)量函數(shù)使用由semget函數(shù)返回的信號(hào)量標(biāo)識(shí)符。如果多個(gè)程序使用相同的key值,key將負(fù)責(zé)協(xié)調(diào)工作。
第二個(gè)參數(shù)num_sems指定需要的信號(hào)量數(shù)目,它的值幾乎總是1。
第三個(gè)參數(shù)sem_flags是一組標(biāo)志,當(dāng)想要當(dāng)信號(hào)量不存在時(shí)創(chuàng)建一個(gè)新的信號(hào)量,可以和值IPC_CREAT做按位或操作。設(shè)置了IPC_CREAT標(biāo)志后,即使給出的鍵是一個(gè)已有信號(hào)量的鍵,也不會(huì)產(chǎn)生錯(cuò)誤。而IPC_CREAT | IPC_EXCL則可以創(chuàng)建一個(gè)新的,唯一的信號(hào)量,如果信號(hào)量已存在,返回一個(gè)錯(cuò)誤。
semget函數(shù)成功返回一個(gè)相應(yīng)信號(hào)標(biāo)識(shí)符(非零),失敗返回-1.
2、semop函數(shù)
它的作用是改變信號(hào)量的值,原型為:
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
sem_id是由semget返回的信號(hào)量標(biāo)識(shí)符,sembuf結(jié)構(gòu)的定義如下:
struct sembuf{
short sem_num;//除非使用一組信號(hào)量,否則它為0
short sem_op;//信號(hào)量在一次操作中需要改變的數(shù)據(jù),通常是兩個(gè)數(shù),一個(gè)是-1,即P(等待)操作,
//一個(gè)是+1,即V(發(fā)送信號(hào))操作。
short sem_flg;//通常為SEM_UNDO,使操作系統(tǒng)跟蹤信號(hào),
//并在進(jìn)程沒有釋放該信號(hào)量而終止時(shí),操作系統(tǒng)釋放信號(hào)量
};
3、semctl函數(shù)
該函數(shù)用來直接控制信號(hào)量信息,它的原型為:
int semctl(int sem_id, int sem_num, int command, ...);
如果有第四個(gè)參數(shù),它通常是一個(gè)union semum結(jié)構(gòu),定義如下:
union semun{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
前兩個(gè)參數(shù)與前面一個(gè)函數(shù)中的一樣,command通常是下面兩個(gè)值中的其中一個(gè)
- SETVAL:用來把信號(hào)量初始化為一個(gè)已知的值。p 這個(gè)值通過union semun中的val成員設(shè)置,其作用是在信號(hào)量第一次使用前對(duì)它進(jìn)行設(shè)置。
- IPC_RMID:用于刪除一個(gè)已經(jīng)無需繼續(xù)使用的信號(hào)量標(biāo)識(shí)符。
四、進(jìn)程使用信號(hào)量通信
下面使用一個(gè)例子來說明進(jìn)程間如何使用信號(hào)量來進(jìn)行通信,這個(gè)例子是兩個(gè)相同的程序同時(shí)向屏幕輸出數(shù)據(jù),我們可以看到如何使用信號(hào)量來使兩個(gè)進(jìn)程協(xié)調(diào)工作,使同一時(shí)間只有一個(gè)進(jìn)程可以向屏幕輸出數(shù)據(jù)。注意,如果程序是第一次被調(diào)用(為了區(qū)分,第一次調(diào)用程序時(shí)帶一個(gè)要輸出到屏幕中的字符作為一個(gè)參數(shù)),則需要調(diào)用set_semvalue函數(shù)初始化信號(hào)并將message字符設(shè)置為傳遞給程序的參數(shù)的第一個(gè)字符,同時(shí)第一個(gè)啟動(dòng)的進(jìn)程還負(fù)責(zé)信號(hào)量的刪除工作。如果不刪除信號(hào)量,它將繼續(xù)在系統(tǒng)中存在,即使程序已經(jīng)退出,它可能在你下次運(yùn)行此程序時(shí)引發(fā)問題,而且信號(hào)量是一種有限的資源。
在main函數(shù)中調(diào)用semget來創(chuàng)建一個(gè)信號(hào)量,該函數(shù)將返回一個(gè)信號(hào)量標(biāo)識(shí)符,保存于全局變量sem_id中,然后以后的函數(shù)就使用這個(gè)標(biāo)識(shí)符來訪問信號(hào)量。
源文件為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)建信號(hào)量
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
if(argc > 1)
{
//程序第一次被調(diào)用,初始化信號(hào)量
if(!set_semvalue())
{
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
//設(shè)置要輸出到屏幕中的信息,即其參數(shù)的第一個(gè)字符
message = argv[1][0];
sleep(2);
}
for(i = 0; i < 10; ++i)
{
//進(jìn)入臨界區(qū)
if(!semaphore_p())
exit(EXIT_FAILURE);
//向屏幕中輸出數(shù)據(jù)
printf("%c", message);
//清理緩沖區(qū),然后休眠隨機(jī)時(shí)間
fflush(stdout);
sleep(rand() % 3);
//離開臨界區(qū)前再一次向屏幕輸出數(shù)據(jù)
printf("%c", message);
fflush(stdout);
//離開臨界區(qū),休眠隨機(jī)時(shí)間后繼續(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)用,則在退出前刪除信號(hào)量
sleep(3);
del_semvalue();
}
exit(EXIT_SUCCESS);
}
static int set_semvalue()
{
//用于初始化信號(hào)量,在使用信號(hào)量前必須這樣做
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()
{
//刪除信號(hào)量
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()
{
//對(duì)信號(hào)量做減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()
{
//這是一個(gè)釋放操作,它使信號(hào)量變?yōu)榭捎?,即發(fā)送信號(hào)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;
}
運(yùn)行結(jié)果如下:

注:這個(gè)程序的臨界區(qū)為main函數(shù)for循環(huán)不的semaphore_p和semaphore_v函數(shù)中間的代碼。
例子分析 :同時(shí)運(yùn)行一個(gè)程序的兩個(gè)實(shí)例,注意第一次運(yùn)行時(shí),要加上一個(gè)字符作為參數(shù),例如本例中的字符‘O',它用于區(qū)分是否為第一次調(diào)用,同時(shí)這個(gè)字符輸出到屏幕中。因?yàn)槊總€(gè)程序都在其進(jìn)入臨界區(qū)后和離開臨界區(qū)前打印一個(gè)字符,所以每個(gè)字符都應(yīng)該成對(duì)出現(xiàn),正如你看到的上圖的輸出那樣。在main函數(shù)中循環(huán)中我們可以看到,每次進(jìn)程要訪問stdout(標(biāo)準(zhǔn)輸出),即要輸出字符時(shí),每次都要檢查信號(hào)量是否可用(即stdout有沒有正在被其他進(jìn)程使用)。
所以,當(dāng)一個(gè)進(jìn)程A在調(diào)用函數(shù)semaphore_p進(jìn)入了臨界區(qū),輸出字符后,調(diào)用sleep時(shí),另一個(gè)進(jìn)程B可能想訪問stdout,但是信號(hào)量的P請(qǐng)求操作失敗,只能掛起自己的執(zhí)行,當(dāng)進(jìn)程A調(diào)用函數(shù)semaphore_v離開了臨界區(qū),進(jìn)程B馬上被恢復(fù)執(zhí)行。然后進(jìn)程A和進(jìn)程B就這樣一直循環(huán)了10次。
五、對(duì)比例子——進(jìn)程間的資源競(jìng)爭(zhēng)
看了上面的例子,你可能還不是很明白,不過沒關(guān)系,下面我就以另一個(gè)例子來說明一下,它實(shí)現(xiàn)的功能與前面的例子一樣,運(yùn)行方式也一樣,都是兩個(gè)相同的進(jìn)程,同時(shí)向stdout中輸出字符,只是沒有使用信號(hào)量,兩個(gè)進(jìn)程在互相競(jìng)爭(zhēng)stdout。它的代碼非常簡(jiǎn)單,文件名為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);
}
運(yùn)行結(jié)果如下:
例子分析:
從上面的輸出結(jié)果,我們可以看到字符‘X'和‘O'并不像前面的例子那樣,總是成對(duì)出現(xiàn),因?yàn)楫?dāng)?shù)谝粋€(gè)進(jìn)程A輸出了字符后,調(diào)用sleep休眠時(shí),另一個(gè)進(jìn)程B立即輸出并休眠,而進(jìn)程A醒來時(shí),再繼續(xù)執(zhí)行輸出,同樣的進(jìn)程B也是如此。所以輸出的字符就是不成對(duì)的出現(xiàn)。這兩個(gè)進(jìn)程在競(jìng)爭(zhēng)stdout這一共同的資源。通過兩個(gè)例子的對(duì)比,我想信號(hào)量的意義和使用應(yīng)該比較清楚了。
六、信號(hào)量的總結(jié)
信號(hào)量是一個(gè)特殊的變量,程序?qū)ζ湓L問都是原子操作,且只允許對(duì)它進(jìn)行等待(即P(信號(hào)變量))和發(fā)送(即V(信號(hào)變量))信息操作。我們通常通過信號(hào)來解決多個(gè)進(jìn)程對(duì)同一資源的訪問競(jìng)爭(zhēng)的問題,使在任一時(shí)刻只能有一個(gè)執(zhí)行線程訪問代碼的臨界區(qū)域,也可以說它是協(xié)調(diào)進(jìn)程間的對(duì)同一資源的訪問權(quán),也就是用于同步進(jìn)程的。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Linux如何修改hosts文件并刷新DNS生效hosts文件
這篇文章主要介紹了Linux如何修改hosts文件并刷新DNS生效hosts文件問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
在Linux分區(qū)或邏輯卷中創(chuàng)建文件系統(tǒng)的方法
這篇文章主要給大家介紹了關(guān)于如何在Linux分區(qū)或邏輯卷中創(chuàng)建文件系統(tǒng)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Linux具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
解決hadoop啟動(dòng)報(bào)錯(cuò)ERROR: Attempting to operate 
這篇文章主要介紹了解決hadoop啟動(dòng)報(bào)錯(cuò)ERROR: Attempting to operate on hdfs namenode as root的方法,hadoop-3.1.0啟動(dòng)hadoop集群時(shí)還有可能可能會(huì)報(bào)如下錯(cuò)誤,需要的朋友可以參考下2023-03-03
如何配置apache虛擬主機(jī)的實(shí)例小結(jié)
如果你是第一次配置apache虛擬主機(jī),那么通過閱讀這篇文章你將會(huì)了解到如何實(shí)現(xiàn)apache虛擬主機(jī)配置。其實(shí)要配置好一臺(tái)虛擬主機(jī)沒有想象中那么難2014-01-01

