版權(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带迟,操作如下:
- 測(cè)試該信號(hào)量是否可用
- 若信號(hào)量為 1音羞,則當(dāng)前進(jìn)程使用共享資源,并將信號(hào)量減 1(加鎖)
- 若信號(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)核往枷,semop
和 semctl
也是類似框产,下面來看看內(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):
其中回調(diào)了 newary
這個(gè)函數(shù),它完成信號(hào)量的創(chuàng)建和獲韧筒辍:
可以看出描睦,整個(gè)過程與消息隊(duì)列和共享內(nèi)存幾乎相同。
3. semop 分析
semop 對(duì)信號(hào)量進(jìn)行 PV 操作窿锉,其中主要是對(duì) sem_op
進(jìn)行加 1 或者減 1酌摇,大體的過程如下:
4. semctl 分析
semctl
對(duì)信號(hào)量進(jìn)行控制膝舅,主要是使用 switch
來判斷當(dāng)前的命令然后執(zhí)行相應(yīng)的操作:
要注意的是嗡载,主要的處理邏輯在 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ì)他們的理解拾弃,了解些原理總是有些好處的。那我們下次再見摆霉,謝謝你的閱讀豪椿。