程序間通訊之訊號量
何為訊號量
訊號量的本質是一種資料操作鎖,它本身不具有資料交換的功能,而是通過控制其他的通訊資源(檔案,外部裝置)來實現程序間通訊,它本身只是一種外部資源的標識。訊號量在此過程中負責資料操作的互斥、同步等功能。
對訊號量的操作
當請求一個使用訊號量來表示的資源時,程序需要先讀取訊號量的值來判斷資源是否可用:大於0,資源可以請求,將訊號量的值-1(P操作);
等於0,無資源可用,程序會進入睡眠狀態直至資源可用;
當程序不再使用一個訊號量控制的共享資源時,訊號量的值+1(V操作)。
PV操作均為原子操作。這是由於訊號量主要的作用是維護資源的互斥或多程序的同步訪問。而在訊號量的建立及初始化上,不能保證操作均為原子性(SystemV版本訊號量的缺陷),因為這個版本的訊號量的建立與初始化是分開的。。
補充兩個概念:
臨界資源:一次只允許一個程序(一個執行緒)使用的資源叫做臨界資源。
臨界區:訪問臨界資源的程式碼稱為臨界區。
為什麼要使用訊號量
19為了防止出現因多個程式同時訪問一個共享資源而而引發的一系列問題,我們需要一種方法,它可以通過生成並使用令牌來授權,在任一時刻只能有一個執行執行緒訪問程式碼的臨界區域。臨界區域是指執行資料更新的程式碼需要獨佔式地執行。而訊號量就可以提供這樣的一種訪問機制,讓一個臨界區同一時間只有一個執行緒在訪問它, 也就是說訊號量是用來調協程序對共享資源的訪問的。其中共享記憶體的使用就要用到訊號量。
函式原型
像對於訊息佇列來說,訊號量也有一套類似的介面,從它們的名字上就可以看出來。
訊號量的建立
其中的key依舊是可以通過ftok函式來獲得,與訊息佇列中的key一模一樣。關於第二個引數,值得一提的是這個版本的訊號量的建立是以訊號量集為單位的。也就是說可以一次性建立多個訊號量。所以這個nsems表示的就是建立的訊號量集中訊號量的個數。最後一個引數又是和訊息佇列的flg引數一模一樣:IPC_CREAT:存在則開啟,否則建立;IPC_CREAT | IPC_EXCL存在則出錯返回,否則建立,這樣保證了開啟的是一個全新的訊號量集。還是要注意,這個IPC_EXCL單獨使用沒有任何意義。
訊號量的初始化
第一個引數為訊號量集的id,semnum表示給訊號量集中的哪一個訊號量進行初始化(從0開始,分別表示第一個訊號量,... ...)。cmd就是初始化命令了:SETVAL。最後用可變引數的方式傳遞訊號量的初始值,這個引數的型別是union:
其中val就是訊號量的初始值,表示初始時臨界資源的個數。其它欄位暫時不關心。
訊號量的銷燬
首先說是如何在終端童通過命令檢視訊號量:ipcs -s ; 刪除是 ipcrm -s 對應sem_id。命令都是類似的。下來是函式:
對,依舊是這個函式,這個ctl能幹的事情多著呢。刪除就是一次性刪除整個訊號量集,所以第二個引數預設0即可。第三個引數就是刪除命令:IPC_RMID.
最重要的操作:
P操作和V操作
通過semop函式來完成:
其中sembuf包含如下欄位:
sem_num表示要對訊號量集中的哪一個訊號量操作,sem_op為1,表示V操作,表示加1,sem_op為-1,表示P操作,表示減1。
sem_flg:訊號操作標誌,可能的選擇有兩種 IPC_NOWAIT //對訊號的操作不能滿足時,semop()不會阻塞,並立即返回,同時設定錯誤資訊。 SEM_UNDO //程式結束時(不論正常或不正常),保證訊號值會被重設為semop()呼叫前的值。這樣做的目的在於避免程式在異常情況下結束時未將鎖定的資源解鎖,造成該資源永遠鎖定。
舉個栗子
此程式由於父子程序同時向顯示器列印語句,導致父子程序列印結果交叉出現:
如圖AB交叉出現,通過引進訊號量後,父子程序對顯示器這個臨界資源實行互斥訪問,保證了父子程序列印的訊息成對出現:
comm.h
#pragma once #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <stdio.h> #define _PATH_NAME_ "/tmp" #define _PROJ_ID_ 0x666 union semun { int val; struct semid_ds *buf; unsigned short *array; struct seminfo *__buf; }; int create_sem_set(int nums); int get_sem_set(int nums); int init_sem_set(int sem_id, int which, int val); int P(int sem_id); int V(int sem_id); int destroy(int sem_id);
comm.c#include "comm.h" static int comm_sem_set(int nums, int flag) { key_t key = ftok(_PATH_NAME_, _PROJ_ID_); if (key < 0) { perror("ftok"); return -2; } return semget(key, nums, flag); } int create_sem_set(int nums) { int flag = IPC_CREAT | IPC_EXCL | 0644; return comm_sem_set(nums, flag); } int get_sem_set(int nums) { int flag = IPC_CREAT; return comm_sem_set(nums, flag); } int init_sem_set(int sem_id, int which, int val) { union semun un; un.val = val; return semctl(sem_id, which, SETVAL, un); } static int pv(int sem_id, int op) { struct sembuf buf; buf.sem_num = 0; buf.sem_op = op; buf.sem_flg = 0; return semop(sem_id, &buf, 1); } int P(int sem_id) { return pv(sem_id, -1); } int V(int sem_id) { return pv(sem_id, 1); } int destroy(int sem_id) { return semctl(sem_id, 0, IPC_RMID); }
test_sem.c#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include "comm.h" int main() { int sem_id = create_sem_set(1); init_sem_set(sem_id, 0, 1); pid_t id = fork(); if (id < 0) { perror("fork"); return 1; } else if (id == 0) // child { while (1) { P(sem_id); printf("A"); fflush(stdout); usleep(rand()%12345); usleep(200000); printf("A"); fflush(stdout); usleep(rand()%12345); V(sem_id); } } else // father { while (1) { P(sem_id); printf("B"); fflush(stdout); usleep(rand()%12345); usleep(200000); printf("B"); fflush(stdout); usleep(rand()%12345); V(sem_id); } } destroy(sem_id); return 0; }