linux 進程間通信系列5,使用信號量
信號量的工作原理:
由於信號量只能進行兩種操作等待和發送信號,即P(sv)和V(sv),他們的行為是這樣的:
P(sv):如果sv的值大於零,就給它減1;如果它的值為零,就掛起該進程的執行
V(sv):如果有其他進程因等待sv而被掛起,就讓它恢復運行,如果沒有進程因等待sv而掛起,就給它加1.
舉個例子,就是兩個進程共享信號量sv,一旦其中一個進程執行了P(sv)操作,它將得到信號量,並可以進入臨界區,使sv減1。而第二個進程將被阻止進入臨界區,因為當它試圖執行P(sv)時,sv為0,它會被掛起以等待第一個進程離開臨界區域並執行V(sv)釋放信號量,這時第二個進程就可以恢復執行。
1,semget()函數
int semget(key_t key, int num_sems, int sem_flags);
-
第二個參數nsems指明要創建的信號量集包含的信號量個數。如果只是打開信號量,把nsems設置為0即可。該參數只在創建信號量集時有效。
-
第三個參數semflg為操作標識,可取如下值:
-
0:取信號量集標識符,若不存在則函數會報錯
-
IPC_CREAT:當semflg&IPC_CREAT為真時,如果內核中不存在鍵值與key相等的信號量集,則新建一個信號量集;如果存在這樣的信號量集,返回此信號量集的標識符
-
IPC_CREAT|IPC_EXCL:如果內核中不存在鍵值與key相等的信號量集,則新建一個消息隊列;如果存在這樣的信號量集則報錯
上述semflg參數為模式標志參數,使用時需要與IPC對象存取權限(如0600)進行|運算來確定信號量集的存取權限
錯誤代碼:
EACCESS:沒有權限
EEXIST:信號量集已經存在,無法創建
EIDRM:信號量集已經刪除
ENOENT:信號量集不存在,同時semflg沒有設置IPC_CREAT標志
ENOMEM:沒有足夠的內存創建新的信號量集
ENOSPC:超出限制
-
2,semop()函數
int semop(int sem_id, struct sembuf *sops, size_t nsops);
-
semid:信號量集標識符
-
sops:指向進行操作的信號量集結構體數組的首地址,此結構的具體說明如下:
如果sembuf里的semnum超過了集合中信號量的最大個數,在執行semop時,會報出:FIle too large。
struct sembuf { short semnum; /*信號量集合中的信號量編號,0代表第1個信號量*/ short val;/*若val>0進行V操作信號量值加val,表示進程釋放控制的資源 */ /*若val<0進行P操作信號量值減val,若(semval-val)<0(semval為該信號量值),則調用進程阻塞,直到資源可 用;若設置IPC_NOWAIT不會睡眠,進程直接返回EAGAIN錯誤*/ /*若val==0時阻塞等待信號量為0,調用進程進入睡眠狀態,直到信號值為0;若設置IPC_NOWAIT,進程不會睡眠,直接返回EAGAIN錯誤*/ short flag; /*0 設置信號量的默認操作*/ /*IPC_NOWAIT設置信號量操作不等待*/ /*SEM_UNDO 選項會讓內核記錄一個與調用進程相關的UNDO記錄,如果該進程崩潰,則根據這個進程的UNDO記錄自動恢復相應信號量的計數值*/ };
-
nsops:進行操作信號量的個數,即sops結構變量的個數,需大於或等於1。最常見設置此值等於1,只完成對一個信號量的操作
錯誤代碼:
E2BIG:一次對信號量個數的操作超過了系統限制
EACCESS:權限不夠
EAGAIN:使用了IPC_NOWAIT,但操作不能繼續進行
EFAULT:sops指向的地址無效
EIDRM:信號量集已經刪除
sops為指向sembuf數組,定義所要進行的操作序列。下面是信號量操作舉例。
struct sembuf sem_get={0,-1,IPC_NOWAIT}; /將信號量對象中序號為0的信號量減1/
struct sembuf sem_get={0,1,IPC_NOWAIT}; /將信號量對象中序號為0的信號量加1/
struct sembuf sem_get={0,0,0}; /進程被阻塞,直到對應的信號量值為0/
flag一般為0,若flag包含IPC_NOWAIT,則該操作為非阻塞操作。若flag包含SEM_UNDO,則當進程退出的時候會還原該進程的信號量操作,這個標志在某些情況下是很有用的,
比如某進程做了P操作得到資源,但還沒來得及做V操作時就異常退出了,此時,其他進程就只能都阻塞在P操作上,於是造成了死鎖。若采取SEM_UNDO標志,就可以避免因為進程異常退出而造成的死鎖。
semops函數詳細說明參考:semops
3,semctl()函數
int semctl(int semid, int semnum, int cmd, ...);
作用:根據cmd的不同,操作創建成功的semid,比如設置信號量集合里的每個信號,同時可以訪問的進程數,通過cmd:SETALL,來設置,並且后面需要跟一個指向數組的指針。
- semid:信號量集合的標識符。
- semnum:信號量集合中的第幾個信號量。當cmd為SETALL時,這個參數感覺沒有實際意義,多少都可以。
- cmd:根據您cmd的不同,做不同的操作。
下面的例子1創建了一個信號量的集合,可以用【ipcs -s】查看;例子2去訪問例子1創建的信號量集合。
例子1里的關鍵點:
- 信號集合里信號的個數為16個
- 因為這句代碼【semun_array[i] = 1;】,所以,每個信號,只能同時有且只有一個進程訪問。如果【semun_array[i] = 2;】,則,每個信號,同時有2個進程可以訪問這個信號。
- smectl的第一個參數,感覺設置多少都可以。
例子2里的關鍵點:
- sb.sem_num = 15;//指定要訪問的信號量集合中的哪個信號量
sb.sem_op = -1;//semop前信號量的值都為1,這里指定的是-1,所以減一后為0,由於為0了,所以下個進程再想去訪問,就需要排隊,等現在訪問這個信號量的進程結束后,才能訪問。 - SEM_UNDO的作用為,訪問信號量的進程結束后,會自動回復這個信號量被訪問前的狀態,也就是說,某個進程訪問前,信號量的狀態是1,這個進程退出后,把信號量的狀態回復回1.
例子1
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define NSEMS 16
int main(){
int semid;
unsigned short semun_array[NSEMS];
int i;
semid = semget(IPC_PRIVATE, NSEMS, 0600);
if(semid < 0){
perror("semget");
return 1;
}
for(i =0; i < NSEMS; ++i){
semun_array[i] = 1;
}
if(semctl(semid, 1000, SETALL, &semun_array) != 0){
perror("semctl");
return 1;
}
printf("semid:%d\n", semid);
return 0;
}
例子2:
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
#define NSEMS 16
int main(int argc, char* argv[]){
int semid;
sembuf sb;
if(argc != 2){
printf("arg is wrong\n");
return 1;
}
semid = atoi(argv[1]);
sb.sem_num = 15;//指定要操作的信號量是信號集合中的號碼為0信號量
sb.sem_op = -1;
sb.sem_flg = SEM_UNDO;
printf("before semop()\n");
if(semop(semid, &sb, 1) != 0){
perror("semop");
return 1;
}
printf("after semop()\n");
printf("press enter to exti\n");
getchar();
return 0;
}
例子3:多線程之間,使用信號量。
第一個進程:
- step1:創建有16個信號量的信號量集合的結果是成功的,所以結果semid >= 0,進入if分支。
- step2:把每個信號量可同時訪問的進程數目設置為1,在代碼的22-28行。
- step3:准備訪問16個信號量,首先在31-35行,把訪問設置為減一,並且UNDO。
- step4:按回車后,開始訪問16個信號量。
第二個進程:
- step1:因為是用同一個key去創建信號量集合,所以是失敗的,進入else分支。
- step2:去拿,已經被創建過了的信號量集合的id,semid = semget(MYIPCKEY, NSEMS, 0600);
- step3:進入while循環,觀察sem_otime是否為0,不為0后,跳出循環,准備訪問第0號信號量和第1號信號量。如果某個進程訪問了信號量集合,sem_otime就從0變為非0.
- step4:准備訪問第0號信號量和第1號信號量。
- step5:訪問第0號信號量和第1號信號量,但是發現信號量的值為-1(因為第一個進程訪問是給他減一了),說明已經有個進程(第一個進程)正在訪問,所以就一直等待。
- step6:按回車,讓第一個進程結束。信號量會從-1變為0。
- step7:因為訪問的進程退出了(信號量會從-1變為0),在step5處的等待就結束了,才可以訪問第0號信號量和第1號信號量。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MYIPCKEY 0xabcdabcd
#define NSEMS 16
int sem_init(){
int semid;
unsigned short semun_array[NSEMS];
sembuf sb[NSEMS];
int i;
semid = semget(MYIPCKEY, NSEMS, 0600 | IPC_CREAT | IPC_EXCL);
if(semid >= 0){
for(i = 0; i < NSEMS; ++i){
semun_array[i] = 1;
}
if(semctl(semid, NSEMS, SETALL, &semun_array) != 0){
perror("semctl");
return 1;
}
printf("[pid:%d] new semaphore set, semid=%d\n", getpid(), semid);
for(i = 0; i < NSEMS; ++i){
sb[i].sem_num = i;
sb[i].sem_op = -1;
sb[i].sem_flg = SEM_UNDO;
}
printf("IF[pid:%d] before semop()\n", getpid());
printf("IF[pid:%d] press enter to start semop()\n", getpid());
getchar();
if(semop(semid, sb, NSEMS)){
perror("semop");
return 1;
}
printf("IF[pid:%d] press enter to exit this process\n", getpid());
getchar();
exit(0);
}
else{
if(errno != EEXIST){
perror("semget");
return 1;
}
else{
printf("in else\n");
semid_ds sds;
semid = semget(MYIPCKEY, NSEMS, 0600);
if(semid < 0){
perror("semget 1");
return 1;
}
printf("ELSE[pid:%d] before semctl()\n", getpid());
while(true){
//IPC_STAT的時候,忽略第二個參數
if(semctl(semid, 0, IPC_STAT, &sds) != 0){
perror("semctl 1");
return 1;
}
printf("###########################\n");
printf("sem_perm.mode:%d,sem_perm.__seq:%d\n",sds.sem_perm.mode,sds.sem_perm.__seq);
printf("otime:%ld\n",sds.sem_otime);/* Last semop time */
printf("ctime:%ld\n",sds.sem_ctime);/* Last change time */
printf("sem_nsems:%ld\n",sds.sem_nsems);/* No. of semaphores in set */
printf("###########################\n");
if(sds.sem_otime != 0){
break;
}
printf("ELSE[pid:%d] waiting otime change...\n", getpid());
sleep(2);
}
sb[0].sem_num = 0;
sb[0].sem_op = -1;
sb[0].sem_flg = SEM_UNDO;
sb[1].sem_num = 0;
sb[1].sem_op = -1;
sb[1].sem_flg = SEM_UNDO;
printf("ELSE[pid:%d] before semop()\n", getpid());
if(semop(semid, sb, 2) != 0){
perror("semop 1");
return 1;
}
printf("ELSE[pid:%d] after semop()\n", getpid());
}
}
return 0;
}
int main(){
pid_t pid;
pid = fork();
if(sem_init() < 0){
printf("[pid:%d] sem_init() failed\n", getpid());
}
}
注意:執行一次后,再次執行前,必須用下面的命令刪除信號量集合。
1,首先找到信號量集合的ID:
ipcs -s
2,刪除信號量集合:
ipcrm -s id