linux的程序間通訊——訊號量
訊號量的本質是一種資料操作鎖,它本⾝身不具有資料交換的功能,而是通過控制其他的通訊資源(檔案,外部裝置)來實現程序間通訊,它本身只是一種外部資源的標識。訊號量在此過程中負責資料操作的互斥、同步等功能。當請求一個使⽤用訊號量來表⽰示的資源時,程序需要先讀取訊號量的值來判斷資源是否可 用。大於0,資源可以請求,等於0,無資源可用,程序會進入睡眠狀態直⾄至資源可用。 當程序不再使用一個訊號量控制的共享資源時,訊號量的值+1,對訊號量的值進行的增減 操作均為原子操作,這是由於訊號量主要的作⽤用是維護資源的互斥或多程序的同步訪問。 而在訊號量的建立及初始化上,不能保證操作均為原子性。
為什麼要使⽤用訊號量?
為了防⽌止出現因多個程式同時訪問一個共享資源⽽而引發的一系列問題,我們需要一種⽅方法, 它可以通過⽣生成並使⽤用令牌來授權,在任⼀時刻只能有一個執⾏行執行緒訪問程式碼的臨界區域。 臨界區域是指執⾏行資料更新的程式碼需要獨佔式地執⾏。而訊號量就可以提供這樣的一種訪 問機制,讓一個臨界區同一時間只有一個執行緒在訪問它, 也就是說訊號量是⽤用來調協程序 對共享資源的訪問的。其中共享記憶體的使⽤用就要⽤用到訊號量。
訊號量的工作原理
由於訊號量只能進⾏行兩種操作等待和傳送訊號,即P(sv)和V(sv),他們的⾏行為是這樣的:
(1) P(sv):如果sv的值⼤大於零,就給它減1;如果它的值為零,就掛起該程序的執⾏
(2) V(sv):如果有其他程序因等待sv而被掛起,就讓它恢復運⾏,如果沒有程序因等待sv⽽掛起,就給它加1.
Linux的訊號量機制
Linux提供了一組精心設計的訊號量介面來對訊號量進⾏行操作,它們不只是針對二進位制訊號 量,下面將會對這些函式進⾏行介紹,但請注意,這些函式都是⽤用來對成組的訊號量值進行操作的。它們宣告在標頭檔案sys/sem.h中。
(1)建立訊號量集(semget):
函式原型:
int semget(int semid, int senmnum, int cmd, …);
訊號量集被建立的情況有兩種:
(1).如果鍵的值是IPC_PRIVATE。
(2).或者鍵的值不是IPC_PRIVATE,並且鍵所對應的訊號量集不存在,同時標誌中指定IPC_CREAT。
當呼叫semget建立一個訊號量時,他的相應的semid_ds結構被初始化。ipc_perm中各個量被設定為相應值:
sem_nsems被設定為nsems所示的值;
sem_otime被設定為0;
sem_ctime被設定為當前時間
用法:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
引數:
key:所建立或開啟訊號量集的鍵值。
nsems:建立的訊號量集中的訊號量的個數,該引數只在建立訊號量集時有效。
semflg:呼叫函式的操作型別,也可用於設定訊號量集的訪問許可權,兩者通過or表示
sem_flags是一組標誌,當想要當訊號量不存在時建立一個新的訊號量,可以和值IPC_CREAT做按位或操作。設定了IPC_CREAT標誌後,即使給出的鍵是一個已有訊號量的鍵,也不會產生錯誤。而IPC_CREAT || IPC_EXCL則可以建立一個新的,唯一的訊號量,如果訊號量已存在,返回一個錯誤。
返回值說明:
如果成功,則返回訊號量集的IPC識別符號。
如果失敗,則返回-1,errno被設定成以下的某個值
EACCES:沒有訪問該訊號量集的許可權
EEXIST:訊號量集已經存在,無法建立
EINVAL:引數nsems的值小於0或者大於該訊號量集的限制;或者是該key關聯的訊號量集已存在,並且nsems
大於該訊號量集的訊號量數
ENOENT:訊號量集不存在,同時沒有使用IPC_CREAT
ENOMEM :沒有足夠的記憶體建立新的訊號量集
ENOSPC:超出系統限制
(2)摧毀和初始化訊號量(semctl):
函式原型:
int semctl(int semid, int semnum, int cmd, //union semun arg//);
返回值:如果成功,則返回一個正數。
如果失敗,則為-1。
EFAULT(arg指向的地址無效)
EIDRM(訊號量集已經刪除)
EINVAL(訊號量集不存在,或者semid無效)
EPERM(EUID沒有cmd的權利)
ERANGE(訊號量值超出範圍)
因為訊號量一般是作為一個訊號量集使用的,而不是一個單獨的訊號量。所以在訊號量集的操作中,不但要知道IPC關鍵字值,也要知道訊號量集中的具體的訊號量。這兩個系統呼叫都使用了引數cmd,它用來指出要操作的具體命令。
在系統呼叫msgctl中,最後一個引數是指向核心中使用的資料結構的指標。我們使用此資料結構來取得有關訊息佇列的一些資訊,以及設定或者改變佇列的存取許可權和使用者。但在訊號量中支援額外的可選的命令,這樣就要求有一個更為複雜的資料結構。
系統呼叫semctl()的第一個引數是訊號量集IPC識別符號。第二個引數是操作訊號在訊號集中的編號,第一個訊號的編號是0。
引數cmd中可以使用的命令如下:
·IPC_STAT讀取一個訊號量集的資料結構semid_ds,並將其儲存在semun中的buf引數中。
·IPC_SET設定訊號量集的資料結構semid_ds中的元素ipc_perm,其值取自semun中的buf引數。
·IPC_RMID將訊號量集從記憶體中刪除。
·GETALL用於讀取訊號量集中的所有訊號量的值。
·GETNCNT返回正在等待資源的程序數目。
·GETPID返回最後一個執行semop操作的程序的PID。
·GETVAL返回訊號量集中的一個單個的訊號量的值。
·GETZCNT返回正在等待完全空閒的資源的程序數目。
·SETALL設定訊號量集中的所有的訊號量的值。
·SETVAL設定訊號量集中的一個單獨的訊號量的值。
引數arg代表一個semun的例項。semun是在linux/sem.h中定義的:
/*arg for semctl systemcalls.*/
union semun{
int val; /*value for SETVAL*/
struct semid_ds *buf; /*buffer for IPC_STAT&IPC_SET*/
ushort *array; /*array for GETALL&SETALL*/
struct seminfo *__buf; /*buffer for IPC_INFO*/
void *__pad;
};
val當執行SETVAL命令時使用。buf在IPC_STAT/IPC_SET命令中使用。代表了核心中使用的訊號量的資料結構。array在使用GETALL/SETALL命令時使用的指標。
下面的程式返回訊號量的值。當使用GETVAL命令時,呼叫中的最後一個引數被忽略:
int get_sem_val(int semid,int semnum)
{
return (semctl(sid,semnum,GETVAL,0));
}
下面是一個實際應用的例子:
#define MAX_PRINTERS 5
printer_usage()
{
int x;
for(x=0; x<MAX_PRINTERS; x++)
printf("Printer%d:%d\n\r",x, get_sem_val(sid,x));
}
下面的程式可以用來初始化一個新的訊號量值:
void init_sem(int semid, int semnum, int initval)
{
union semun semopts;
semopts.val = initval;
semctl(sid, semnum, SETVAL, semopts);
}
注意系統呼叫semctl中的最後一個引數是一個聯合型別的副本,而不是一個指向聯合型別的指標。
需要注意的是,對於semun聯合體,最好自己定義,否則GCC編譯器可能會報“semun大小未知”。
(3)p操作,v操作(semop):
用法:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
引數:
semid:訊號集的識別碼,可通過semget獲取。
sops:指向儲存訊號操作結構的陣列指標,訊號操作結構的原型如下
struct sembuf
{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
};
這三個欄位的意義分別為:
sem_num:操作訊號在訊號集中的編號,第一個訊號的編號是0。
sem_op:如果其值為正數,該值會加到現有的訊號內含值中。通常用於釋放所控資源的使用權;如果sem_op的值為負數,而其絕對值又大於訊號的現值,操作將會阻塞,直到訊號值大於或等於sem_op的絕對值。通常用於獲取資源的使用權;如果sem_op的值為0,則操作將暫時阻塞,直到訊號的值變為0。
sem_flg:訊號操作標誌,可能的選擇有兩種
(1)IPC_NOWAIT //對訊號的操作不能滿足時,semop()不會阻塞,並立即返回,同時設定錯誤資訊。
(2)IPC_UNDO //程式結束時(不論正常或不正常),保證訊號值會被重設為semop()呼叫前的值。這樣做的目的在於避免程式在異常情況下結束時未將鎖定的資源解鎖,造成該資源永遠鎖定。
nsops:訊號操作結構的數量,恆大於或等於1。
timeout:當semtimedop()呼叫致使程序進入睡眠時,睡眠時間不能超過本引數指定的值。如果睡眠超時,semtimedop()將失敗返回,並設定錯誤值為EAGAIN。如果本引數的值為NULL,semtimedop()將永遠睡眠等待。
返回說明:
成功執行時,兩個系統呼叫都返回0。失敗返回-1,errno被設為以下的某個值
E2BIG:一次對訊號的運算元超出系統的限制
EACCES:呼叫程序沒有權能執行請求的操作,並且不具有CAP_IPC_OWNER權能
EAGAIN:訊號操作暫時不能滿足,需要重試
EFAULT:sops或timeout指標指向的空間不可訪問
EFBIG:sem_num指定的值無效
EIDRM:訊號集已被移除
EINTR:系統呼叫阻塞時,被訊號中斷
EINVAL:引數無效
ENOMEM:記憶體不足
ERANGE:訊號所允許的值越界
使用訊號量的例項:
comm.h程式碼:
comm.c程式碼:
sem.c程式碼:
執行結果
例子分析 :因為每個程式都在其進入臨界區後和離開臨界區前列印一個字元,所以每個字元都應該成對出現,正如你看到的上圖的輸出那樣。在main函式中要輸出字元時,每次都要檢查訊號量是否可用(即stdout有沒有正在被其他程序使用)。所以,當一個程序A在呼叫函式p進入了臨界區,輸出字元後,呼叫sleep時,另一個程序B可能想訪問stdout,但是訊號量的P請求操作失敗,只能掛起自己的執行,當程序A呼叫函式v離開了臨界區,程序B馬上被恢復執行。然後程序A和程序B就這樣一直迴圈了。
程序間的資源競爭
看了上面的例子,你可能還不是很明白,不過沒關係,下面我就以另一個例子來說明一下,它實現的功能與前面的例子一樣,執行方式也一樣,都是兩個相同的程序,同時向stdout中輸出字元,只是沒有使用訊號量,兩個程序在互相競爭stdout。它的程式碼非常簡單,程式碼如下:
#include "comm.h"
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t _k = fork();
if(_k == 0)
{ //child
while(1){
printf("A");
fflush(stdout);
usleep(12345);
printf("A");
fflush(stdout);
usleep(12345);
}
exit(1);
DestorySem(semid);
}
else if(_k > 0) { //father
while(1){
printf("B");
fflush(stdout);
usleep(12345);
printf("B");
usleep(12345);
fflush(stdout);
}
}
return 0;
}
執行結果:
由執行結果可以看出,輸出結果不是成對存在的,而是隨機出現的。
SEM_UNDO
semop函式原型如下:
int semop(int semid, struct sembuf *sops, unsigned nsops);
semop操作中:sembuf結構的sem_flg成員可以為0、IPC_NOWAIT、SEM_UNDO 。為SEM_UNDO時,它將使作業系統跟蹤當前程序對這個訊號量的修改情況,如果這個程序在沒有釋放該訊號量的情況下終止,作業系統將自動釋放該程序持有的。
sembuf結構的sem_flg成員為SEM_UNDO時,它將使作業系統跟蹤當前程序對這個訊號量的修改情況,如果這個程序在沒有釋放該訊號量的情況下終止,作業系統將自動釋放該程序持有的訊號量。防止其他程序因為得不到訊號量而 發生【死鎖現象】。
設定sem.sem_flg為 0【終止子程序 出現死鎖現象】
(正如上述程式碼所執行的結果)
設定sem.sem_flg為SEM_UNDO【終止子程序 系統自動V操作 不會出現死鎖現象】
結論:
若通過kill命令把其中一個程序殺死,且該程序還沒有執行V操作釋放資源。若使用SEM_UNDO標誌,則作業系統將自動釋放該程序持有的訊號量,從而使得另外一個程序可以繼續工作。若沒有這個標誌,另外程序將P操作永遠阻塞。
因此,一般建議使用SEM_UNDO標誌。