以下內容為本人的著作,如需要轉載,請聲明原文鏈接 微信公眾號「englyf」https://www.cnblogs.com/englyf/p/16645135.html 信號量的無序競爭和有序競爭 在linux的多進程(或者多線程,這裡以進程為例)開發里經常有進程間的通信部分,常見的技術手段有信號量 ...
以下內容為本人的著作,如需要轉載,請聲明原文鏈接 微信公眾號「englyf」https://www.cnblogs.com/englyf/p/16645135.html
信號量的無序競爭和有序競爭
在linux的多進程(或者多線程,這裡以進程為例)開發里經常有進程間的通信部分,常見的技術手段有信號量、消息隊列、共用記憶體等,而共用記憶體和信號量就像襯衫和外套一樣搭配才算完整。
信號量的使用可以使得對資源的訪問具有排它性,單一時刻只允許同一個進程訪問,而其它的進程統統排隊等候或者取消行程打道回府。
對資源的訪問權既然要有排它性,那麼訪問權的獲得就必然有競爭關係。競爭關係,又會使得結果是有順序的,包括有序和無序。無序就是,競爭是公平的,對資源的訪問權獲取是隨機的。而有序則是,對競爭的結果有刻意的安排,出現固定的順序,比如數據生產消費模型里,數據一般是安排先在生產端輸出,然後才輪到消費端訪問。
好了,扯得太長太陽都快出來了。
信號量的使用庫有System V庫和POXIS庫兩種,這裡僅簡單介紹System V庫和相關API,太詳細會讓人睡著的。
函數原型 | 備註 |
---|---|
int semget(key_t key, int nsems, int semflg) | 獲取或者創建一個信號量集的標識符,一個信號量集可以包含有多個信號量,nsems代表信號量數量,key可以通過ftok獲取(也可以直接使用IPC_PRIVATE,但是僅能用於父子進程間通信),semflg代表信號量集的屬性 |
int semctl(int semid, int semnum, int cmd, union semun arg) | 設置或者讀取信號量集的某個信號量的信息,semid代表semget返回值,semnum代表信號量的序號,類型union semun在某些系統中不一定存在(如有需要可以自定義) |
int semop(int semid, struct sembuf *sops, unsigned nsops) | 執行PV操作,P是對資源的占用,V是對資源的釋放,類型struct sembuf包含了操作的具體內容,nsops代表操作信號量的個數(一般僅用1) |
struct sembuf {
short sem_num; //指定信號量,信號量在信號量集中的序號,從0開始
short sem_op; //小於0,就是執行P操作,對信號量減去sem_op的絕對值;大於0,就是執行V操作,對信號量加上sem_op的絕對值;等於0,等待信號量值歸0
short sem_flg; //0,IPC_NOWAIT,SEM_UNDO(方便於調用進程崩潰時對信號量值的自動恢復,防止對資源的無用擠占)
}
下麵介紹一下信號量的兩種使用方式。
信號量的無序競爭
信號量最簡單的使用方式就是無序的競爭方式。比如在獲取資源時,只使用一個信號量,各個進程公平競爭上崗。預設其中一個特定進程啟動後,初始化信號量的值為1(調用semctl實現)。然後當所有進程其中的一個需要搶占資源時,P操作對信號量值減1,信號量值歸0,調用進程搶占資源成功,資源使用完成後,V操作對信號量值加1,信號量值變為1,釋放資源。
當信號量值歸0後,其它進程如果需要搶占資源,對信號量執行P操作會導致調用進程掛起並等待,這是調用進程堵塞了。如果執行P操作時,semop的sem_flg用了IPC_NOWAIT,則直接返回-1,通過errno可以獲取到錯誤代碼EAGAIN。
PV操作就是通過semop函數對信號量的值檢查再加減操作。
老是覺得話太多還不如幾行代碼來得直接明瞭。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/sem.h>
void P(int sid)
{
struct sembuf sem_p;
sem_p.sem_num = 0;
sem_p.sem_op = -1;
sem_p.sem_flg = 0;
if (semop(sid, &sem_p, 1) == -1) {
perror("p fail");
exit(-1);
}
}
void V(int sid)
{
struct sembuf sem_v;
sem_v.sem_num = 0;
sem_v.sem_op = 1;
sem_v.sem_flg = 0;
if (semop(sid, &sem_v, 1) == -1) {
perror("v fail");
exit(-1);
}
}
int main(int argc, char *argv[])
{
int fd = open("semtest", O_RDWR | O_CREAT, 0666);
if (fd == -1) {
perror("open");
exit(-1);
}
key_t key = ftok("semtest", 'a');
if (key == -1) {
perror("ftok");
exit(-1);
}
int sid = semget(key, 1, IPC_CREAT | 0666);
if (sid == -1) {
perror("semget");
exit(-1);
}
if (semctl(sid, 0, SETVAL, 1) == -1) {
perror("semctl");
exit(-1);
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(-1);
} else if (pid == 0) {
// child process
while (1) {
P(sid);
printf("child get\n");
sleep(1);
printf("child release\n");
V(sid);
}
} else {
// parent process
printf("parent pid %d child pid %d\n", getpid(), pid);
while (1) {
P(sid);
printf("parent get\n");
sleep(1);
printf("parent release\n");
V(sid);
}
}
return 0;
}
然後看看結果輸出,第一次可能是這樣子的
parent pid 13156 child pid 13157
child get
child release
parent get
parent release
child get
child release
parent get
parent release
child get
child release
...
第二次可能就是這樣子了
parent pid 12873 child pid 12874
parent get
parent release
child get
child release
parent get
parent release
child get
child release
parent get
parent release
...
很明顯這就是信號量的無序競爭結果,就像永遠猜不到下一個出現的會是如花姐姐還是白雪公主。
信號量的有序競爭
其實,進程間對資源的使用方式常常是有刻意順序的,比如數據的生產消費模型使用場景。我們去茶樓喝茶,都是要先下好單等廚房的師傅們弄好端出來,我們才下筷吃起來,這裡邊就有既定的順序啦。
那麼怎麼實現信號量的有序操作呢?如果僅僅使用一個信號量,對於各個進程來說,同一個信號量的值,你知我知大家知,大伙處在同一起跑線上,明顯一個信號量是不夠了。那麼可以嘗試使用多個信號量,畢竟人多力量大,大力出奇跡?(玩笑,給個評價____)
假設有兩個進程(A和B)競爭使用同一個資源,使用資源的順序要求先是A,然後B,如此迴圈。每個進程各分配一個代表的信號量(semA/semB)。由於信號量的值預設是0的,那麼可以在最優先的進程(A)中對信號量(semA)的值初始化為1,其它信號量(semB)初始化為0,而在其它進程中不需要再對信號量的值作初始化了。
當進程(A)需要搶占資源時,P操作信號量(semA),信號量(semA)的值歸0,搶占資源成功。進程(A)使用完需要釋放資源時,V操作信號量(semB),信號量(semB)的值變為1,釋放完成。在進程(A)中,資源釋放後,這時如果再次嘗試搶占資源,則P操作信號量(semA),檢查信號量(semA)的值,發現已為0,搶占資源失敗,進程(A)掛起等待資源。
在進程(A)釋放資源後,如果進程(B)嘗試搶占資源,P操作信號量(semB),信號量(semB)的值歸0,搶占資源成功。進程(B)使用完需要釋放資源時,V操作信號量(semA),信號量(semA)的值變為1,釋放完成。如果進程(A)未曾搶占資源並且釋放,這時進程(B)嘗試搶占資源,P操作信號量(semB),檢查信號量(semB)的值,發現已為0,搶占資源失敗,進程(B)掛起等待資源。
這樣就實現了資源總是先給到進程(A),待進程(A)釋放資源後,進程(B)才有資格獲取到。
下麵是代碼,look一look
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/sem.h>
#include <string.h>
#include <errno.h>
void P(int sid, int index)
{
struct sembuf sem_p;
sem_p.sem_num = index;
sem_p.sem_op = -1;
sem_p.sem_flg = 0;
if (semop(sid, &sem_p, 1) == -1) {
printf("%d p fail: %s", index, strerror(errno));
exit(-1);
}
}
void V(int sid, int index)
{
struct sembuf sem_v;
sem_v.sem_num = index;
sem_v.sem_op = 1;
sem_v.sem_flg = 0;
if (semop(sid, &sem_v, 1) == -1) {
printf("%d v fail: %s", index, strerror(errno));
exit(-1);
}
}
int main(int argc, char *argv[])
{
int fd = open("semtest", O_RDWR | O_CREAT, 0666);
if (fd == -1) {
perror("open");
exit(-1);
}
key_t key = ftok("semtest", 'a');
if (key == -1) {
perror("ftok");
exit(-1);
}
int sid = semget(key, 2, IPC_CREAT | 0666);
if (sid == -1) {
perror("semget 2");
exit(-1);
}
if (semctl(sid, 0, SETVAL, 1) == -1) {
perror("semctl 0");
exit(-1);
}
if (semctl(sid, 1, SETVAL, 0) == -1) {
perror("semctl 1");
exit(-1);
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(-1);
} else if (pid == 0) {
// child
while (1) {
P(sid, 1);
printf("child get\n");
sleep(1);
printf("child release\n");
V(sid, 0);
}
} else {
// parent
printf("parent pid %d child pid %d\n", getpid(), pid);
while (1) {
P(sid, 0);
printf("parent get\n");
sleep(1);
printf("parent release\n");
V(sid, 1);
}
}
return 0;
}
編譯執行,看看輸出。
parent pid 271 child pid 272
parent get
parent release
child get
child release
parent get
parent release
child get
child release
parent get
parent release
...
無論執行多少遍這程式,發現parent永遠是最先搶占資源的。不信的話,還可以在parent的while迴圈之前加個延時,再看看輸出結果(治好你的小雞咕嚕。。。)。你會發現parent這隻小兔子無論故意睡多久的懶覺,還是會第一個衝出屏幕(不是終點線)。
// parent
printf("parent pid %d child pid %d\n", getpid(), pid);
sleep(10);
while (1) {
P(sid, 0);
printf("parent get\n");
sleep(1);
printf("parent release\n");
V(sid, 1);
}
如果把上面信號量初始化的代碼改一改(會不會單車變摩托?想多了。。。)
改成:子進程的代表信號量值初始化為1,父進程的代表信號量初始化為0。
if (semctl(sid, 0, SETVAL, 0) == -1) {
perror("semctl 0");
exit(-1);
}
if (semctl(sid, 1, SETVAL, 1) == -1) {
perror("semctl 1");
exit(-1);
}
編譯後再執行程式看看輸出,發現最先搶占資源的變成永遠是child了
parent pid 298 child pid 299
child get
child release
parent get
parent release
child get
child release
parent get
parent release
child get
child release
...