從 0 開始學(xué)習(xí) Linux 系列之「24.信號(hào)量 semaphore」

信號(hào)量

版權(quán)聲明:本文為 cdeveloper 原創(chuàng)文章第煮,可以隨意轉(zhuǎn)載蚓挤,但必須在明確位置注明出處!

信號(hào)量 semaphore

信號(hào)量(semaphore)與之前介紹的管道躯泰,消息隊(duì)列的等 IPC 的思想不同拂苹,信號(hào)量是一個(gè)計(jì)數(shù)器安聘,用來為多個(gè)進(jìn)程或線程提供對(duì)共享數(shù)據(jù)的訪問。

信號(hào)量的原理

常用的信號(hào)量是二值信號(hào)量醋寝,它控制單個(gè)共享資源搞挣,初始值為 1带迟,操作如下:

  1. 測(cè)試該信號(hào)量是否可用
  2. 若信號(hào)量為 1音羞,則當(dāng)前進(jìn)程使用共享資源,并將信號(hào)量減 1(加鎖)
  3. 若信號(hào)量為 0仓犬,則當(dāng)前進(jìn)程不可以使用共享資源并休眠嗅绰,必須等待信號(hào)量為 1 時(shí)進(jìn)程才能繼續(xù)執(zhí)行(解鎖)

要注意因?yàn)槭鞘褂眯盘?hào)量來保護(hù)共享資源,所以信號(hào)量本身的操作不能被打斷,即必須是原子操作窘面,因此由內(nèi)核來實(shí)現(xiàn)信號(hào)量翠语。

查看信號(hào)量

類似消息隊(duì)列和共享內(nèi)存,我們也可以使用 ipcs 命令來查看當(dāng)前系統(tǒng)的信號(hào)量資源:

ipcs -s

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

目前我的系統(tǒng)中沒有信號(hào)量财边,在后面例子中會(huì)使用這個(gè)命令來查看創(chuàng)建的信號(hào)量肌括。

信號(hào)量的基本操作

Linux 內(nèi)核提供了一套對(duì)信號(hào)量的操作,包括獲取酣难,設(shè)置谍夭,操作信號(hào)量,下面就來學(xué)習(xí)具體的 API憨募。

1. 獲取信號(hào)量

使用 semget 來創(chuàng)建或獲取一個(gè)與 key 有關(guān)的信號(hào)量紧索。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

/*
 * key:返回的 ID 與 key 有關(guān)系
 * nsems:信號(hào)量的值
 * semflg:創(chuàng)建標(biāo)記
 * return:成功返回信號(hào)量 ID,失敗返回 -1菜谣,并設(shè)置 erron
 */
int semget(key_t key, int nsems, int semflg);

關(guān)于參數(shù)的詳細(xì)解釋參考 man semget

2. 操作信號(hào)量

使用 semop 可以對(duì)一個(gè)信號(hào)量加 1 或者減 1:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

/*
 * semid:信號(hào)量 ID
 * sops:對(duì)信號(hào)量的操作
 * nsops:要操作的信號(hào)數(shù)量
 * return:成功返回 0珠漂,失敗返回 -1,并設(shè)置 erron
 */
int semop(int semid, struct sembuf *sops, size_t nsops);

sembuf 表示了對(duì)信號(hào)量操作的屬性:

struct sembuf {
    /* 信號(hào)量的個(gè)數(shù)尾膊,除非使用多個(gè)信號(hào)量媳危,否則設(shè)置為 0 */
    unsigned short sem_num;  
    
    /* 信號(hào)量的操作,-1 表示 p 操作冈敛,1 表示 v 操作 */
    short          sem_op;   
    
    /* 通常設(shè)置為 SEM_UNDO济舆,使得 OS 能夠跟蹤信號(hào)量并在沒有釋放時(shí)自動(dòng)釋放 */
    short          sem_flg;  
};

在進(jìn)行信號(hào)量的 pv 操作時(shí)都是使用這個(gè)結(jié)構(gòu)作為參數(shù),詳細(xì)解釋參考 man semop莺债。

3. 設(shè)置信號(hào)量

使用 semctl 可以設(shè)置一個(gè)信號(hào)量的初始值:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

/*
 * semid:要設(shè)置的信號(hào)量 ID
 * semnum:要設(shè)置的信號(hào)量的個(gè)數(shù)
 * cmd:設(shè)置的屬性
 */
int semctl(int semid, int semnum, int cmd, ...);

第 4 個(gè)參數(shù)的類型是 union semun 結(jié)構(gòu):

union semun {
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
};

在使用信號(hào)量時(shí)必須手動(dòng)定義這個(gè)結(jié)構(gòu)滋觉,并且在初始化設(shè)置信號(hào)量(SETVAL)時(shí)需要使用這個(gè)參數(shù),詳細(xì)解釋可以參考 man semctl齐邦。

例子:使用信號(hào)量進(jìn)行進(jìn)程間的同步

下面來學(xué)習(xí)一個(gè)實(shí)際使用信號(hào)量來進(jìn)行進(jìn)程間通信的例子椎侠,例子實(shí)現(xiàn)的功能是:一個(gè)程序的兩個(gè)實(shí)例同步訪問同一段代碼,先來看看使用的關(guān)鍵的函數(shù)措拇。

1. 獲取信號(hào)量

在這個(gè)例子中將獲取信號(hào)量包裝成一個(gè)函數(shù) sem_get

// 創(chuàng)建或獲取一個(gè)信號(hào)量
int sem_get(int sem_key) {
    int sem_id = semget(sem_key, 1, IPC_CREAT | 0666);
    
    if (sem_id == -1) {
        printf("sem get failed.\n");
        exit(-1);
    } else {
        printf("sem_id = %d\n", sem_id);
        return sem_id;
    }   
}

創(chuàng)建或者獲取成功打印信號(hào)量的 id我纪,否則打印錯(cuò)誤信息。

2. 初始化信號(hào)量

我們只初始化一個(gè)信號(hào)量丐吓,并設(shè)置 val = 1

// 初始化信號(hào)量
int set_sem(int sem_id) {
    union semun sem_union;  
    sem_union.val = 1;  
    if(semctl(sem_id, 0, SETVAL, sem_union) == -1) { 
        fprintf(stderr, "Failed to set sem\n");  
        return 0;  
    }
    return 1;  
}

主要使用了 union semun 作為第 4 個(gè)參數(shù)浅悉,其中 sem_union.val = 1,并且第 3 個(gè)參數(shù)必須為 SETVAL券犁。

3. 刪除信號(hào)量

雖然可以指定 OS 自動(dòng)釋放信號(hào)量术健,但這個(gè)還是要介紹手動(dòng)釋放的方法:

// 刪除信號(hào)量  
void del_sem(int sem_id) {  
    union semun sem_union;  
    if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)  
        fprintf(stderr, "Failed to delete sem, sem has been del.\n");  
} 

第 3 個(gè)參數(shù)指定 IPC_RMID 來刪除信號(hào)量。

4. 信號(hào)量的 PV 操作

下面的函數(shù)將信號(hào)量的 val 減 1粘衬,實(shí)現(xiàn)了 PV 操作:

// 減 1荞估,加鎖咳促,P 操作
void sem_down(int sem_id) {
    if (-1 == semop(sem_id, &sem_lock, 1)) 
        fprintf(stderr, "semaphore lock failed.\n");
}

// 加 1,解鎖勘伺,V 操作
void sem_up(int sem_id) {
    if (-1 == semop(sem_id, &sem_unlock, 1))
        fprintf(stderr, "semaphore unlock failed.\n");
}

5. main 函數(shù)

最后來看看主程序的邏輯跪腹,先創(chuàng)建或獲取信號(hào)量,然后在第一次調(diào)用時(shí)初始化飞醉,接著執(zhí)行 PV 操作冲茸,最后在第二次調(diào)用后刪除信號(hào)量:

int main(int argc, char **argv) {
    int sem_id = sem_get(12);

    // 第一次調(diào)用多加一個(gè)參數(shù),第二次調(diào)用不加參數(shù)缅帘,僅在第一次調(diào)用時(shí)創(chuàng)建信號(hào)量
    if (argc > 1 && (!set_sem(sem_id))) {
        printf("set sem failed.\n");
        return -1;
    }
    
    // P 操作
    sem_down(sem_id);
    printf("sem lock...\n");
    
    printf("do something...\n");
    sleep(10);

    // V 操作
    sem_up(sem_id);
    printf("sem unlock...\n");

    // 第二次調(diào)用后刪除信號(hào)量
    if (argc == 1)
        del_sem(sem_id);    

    return 0;
}

6. 編譯噪裕,運(yùn)行,測(cè)試

先編譯:

gcc sem.c -o sem

在第一個(gè)終端運(yùn)行股毫,我們多加一個(gè)無用的參數(shù)來表示這是第一次運(yùn)行:

./sem 1

sem_id = 0
sem lock...
do something...
# 10 s 等待
sem unlock...

我們使用 ipcs -s 查看一下當(dāng)前系統(tǒng)中的信號(hào)量:

ipcs -s

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     
0x0000000c 0          orange     666        1         

看到用戶 orange 已經(jīng)成功創(chuàng)建了一個(gè)權(quán)限為 666 膳音,ID 為 0 的信號(hào)量了,再打開第二個(gè)終端铃诬,不加額外的參數(shù)再運(yùn)行一次:

./sem

sem_id = 0
# 第一個(gè)終端打印完 sem unlock 后
sem lock...
do something...
# 10 s 等待
sem unlock...

因?yàn)槭堑诙芜\(yùn)行祭陷,所以最后信號(hào)量會(huì)被刪除,我們?cè)賮砜纯?ipcs -s 的結(jié)果:

ipcs -s

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

可以看到信號(hào)量被成功刪除了趣席,這個(gè)效果親自運(yùn)行測(cè)試后可以理解的更加深刻兵志,這兩個(gè)進(jìn)程是同步訪問 do something 這部分代碼的,第二個(gè)進(jìn)程會(huì)等待第一個(gè)進(jìn)程 unlock 后再運(yùn)行宣肚,建議你[下載代碼]({{ site.url }}/file/sem/sem.c)實(shí)際運(yùn)行一下想罕。

拓展:信號(hào)量在 Linux 內(nèi)核中的實(shí)現(xiàn)機(jī)制

最后,我們?cè)賮砗?jiǎn)單分析下信號(hào)量在 Linux 內(nèi)核中的實(shí)現(xiàn)機(jī)制霉涨,了解機(jī)制可以幫助我們更好的理解和使用信號(hào)量按价。其實(shí)內(nèi)核中的共享內(nèi)存,消息隊(duì)列和信號(hào)量的實(shí)現(xiàn)機(jī)制幾乎是相同的笙瑟,信號(hào)量也是開辟一片內(nèi)存楼镐,然后對(duì)鏈表進(jìn)行操作。

1. glibc 信號(hào)量函數(shù)分析

int semget (key, nsems, semflg)
key_t key;
int nsems;
int semflg;
{
    return INLINE_SYSCALL (ipc, 5, IPCOP_semget, key, nsems, semflg, NULL);
}

semget 函數(shù)直接使用 INLINE_SYSCALL 進(jìn)行系統(tǒng)調(diào)用陷入內(nèi)核往枷,semopsemctl 也是類似框产,下面來看看內(nèi)核中的實(shí)現(xiàn)。

2. semget 分析

semget 函數(shù)為信號(hào)量開辟一片新的內(nèi)存错洁,內(nèi)核中的調(diào)用如下秉宿,也是使用了 ipc_ops 這個(gè)數(shù)據(jù)結(jié)構(gòu):

semget

其中回調(diào)了 newary 這個(gè)函數(shù),它完成信號(hào)量的創(chuàng)建和獲韧筒辍:

newary

可以看出描睦,整個(gè)過程與消息隊(duì)列和共享內(nèi)存幾乎相同。

3. semop 分析

semop 對(duì)信號(hào)量進(jìn)行 PV 操作窿锉,其中主要是對(duì) sem_op 進(jìn)行加 1 或者減 1酌摇,大體的過程如下:

semop

4. semctl 分析

semctl 對(duì)信號(hào)量進(jìn)行控制膝舅,主要是使用 switch 來判斷當(dāng)前的命令然后執(zhí)行相應(yīng)的操作:

semctl

要注意的是嗡载,主要的處理邏輯在 semctl_main 這個(gè)函數(shù)中窑多,其中每個(gè) cmd 都有具體的執(zhí)行邏輯,有興趣可以詳細(xì)分析洼滚。

結(jié)語

本次就簡(jiǎn)單地介紹了信號(hào)量的基本操作和內(nèi)核的實(shí)現(xiàn)機(jī)制埂息,對(duì)與信號(hào)量的應(yīng)用并沒有介紹太多,更多的應(yīng)用方法還需要在實(shí)際工作中去實(shí)踐遥巴。建議你將共享內(nèi)存千康,消息隊(duì)列和信號(hào)量自己總結(jié)對(duì)照分析一遍,看看它們的實(shí)現(xiàn)機(jī)制是不是幾乎相同的铲掐,這可以加深你對(duì)他們的理解拾弃,了解些原理總是有些好處的。那我們下次再見摆霉,謝謝你的閱讀豪椿。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市携栋,隨后出現(xiàn)的幾起案子搭盾,更是在濱河造成了極大的恐慌,老刑警劉巖婉支,帶你破解...
    沈念sama閱讀 222,464評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸯隅,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡向挖,警方通過查閱死者的電腦和手機(jī)蝌以,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來何之,“玉大人饼灿,你說我怎么就攤上這事〉勖溃” “怎么了碍彭?”我有些...
    開封第一講書人閱讀 169,078評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)悼潭。 經(jīng)常有香客問我庇忌,道長(zhǎng),這世上最難降的妖魔是什么舰褪? 我笑而不...
    開封第一講書人閱讀 59,979評(píng)論 1 299
  • 正文 為了忘掉前任皆疹,我火速辦了婚禮,結(jié)果婚禮上占拍,老公的妹妹穿的比我還像新娘略就。我一直安慰自己捎迫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評(píng)論 6 398
  • 文/花漫 我一把揭開白布表牢。 她就那樣靜靜地躺著窄绒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪崔兴。 梳的紋絲不亂的頭發(fā)上彰导,一...
    開封第一講書人閱讀 52,584評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音敲茄,去河邊找鬼位谋。 笑死,一個(gè)胖子當(dāng)著我的面吹牛堰燎,可吹牛的內(nèi)容都是我干的掏父。 我是一名探鬼主播,決...
    沈念sama閱讀 41,085評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼秆剪,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼赊淑!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鸟款,我...
    開封第一講書人閱讀 40,023評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤膏燃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后何什,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體组哩,經(jīng)...
    沈念sama閱讀 46,555評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評(píng)論 3 342
  • 正文 我和宋清朗相戀三年处渣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了伶贰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,769評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡罐栈,死狀恐怖黍衙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情荠诬,我是刑警寧澤琅翻,帶...
    沈念sama閱讀 36,439評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站柑贞,受9級(jí)特大地震影響方椎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜钧嘶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評(píng)論 3 335
  • 文/蒙蒙 一棠众、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧有决,春花似錦闸拿、人聲如沸空盼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽揽趾。三九已至,卻和暖如春迟隅,著一層夾襖步出監(jiān)牢的瞬間但骨,已是汗流浹背励七。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工智袭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人掠抬。 一個(gè)月前我還...
    沈念sama閱讀 49,191評(píng)論 3 378
  • 正文 我出身青樓吼野,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親两波。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瞳步,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容