進程間通信之信號量
一、什麼是信號量
爲了防止出現因多個程序同時訪問一個共享資源而引發的一系列問題,我們需要一種方法,它可以通過生成並使用令牌來授權,在任一時刻只能有一個執行線程訪問代碼的臨界區域。臨界區域是指執行數據更新的代碼需要獨佔式地執行。而信號量就可以提供這樣的一種訪問機制,讓一個臨界區同一時間只有一個線程在訪問它,也就是說信號量是用來調協進程對共享資源的訪問的。
信號量是一個特殊的變量,程序對其訪問都是原子操作,且只允許對它進行等待(即P(信號變量))和發送(即V(信號變量))信息操作。最簡單的信號量是隻能取0和1的變量,這也是信號量最常見的一種形式,叫做二進制信號量(二元信號量)。而可以取多個正整數的信號量被稱爲通用信號量。這裏主要討論二進制信號量。
二、信號量的工作原理
由於信號量只能進行兩種操作等待和發送信號,即P(sv)和V(sv),他們的行爲是這樣的:
P(sv):如果sv的值大於零,就給它減1;如果它的值爲零,就掛起該進程的執行
V(sv):如果有其他進程因等待sv而被掛起,就讓它恢復運行,如果沒有進程因等待sv而掛起,就給它加1。
舉個例子:
兩個進程共享信號量sv,一旦其中一個進程執行了P(sv)操作,它將得到信號量,並可以進入臨界區,使sv減1。而第二個進程將被阻止進入臨界區,因爲當它試圖執行P(sv)時,sv爲0,它會被掛起以等待第一個進程離開臨界區域並執行V(sv)釋放信號量,這時第二個進程就可以恢復執行。
三、Linux的信號量機制
Linux提供了一組精心設計的信號量接口來對信號進行操作,它們不只是針對二進制信號量,下面將會對這些函數進行介紹,但請注意,這些函數都是用來對成組的信號量值進行操作的。它們聲明在頭文件sys/sem.h中。
1、semget函數
它的作用是創建一個新信號量或取得一個已有信號量,原型爲:
int semget(key_t key, int num_sems, int sem_flags);
第一個參數key是整數值(唯一非零),不相關的進程可以通過它訪問一個信號量,它代表程序可能要使用的某個資源,程序對所有信號量的訪問都是間接的,程序先通過調用semget函數並提供一個鍵,再由系統生成一個相應的信號標識符(semget函數的返回值),只有semget函數才直接使用信號量鍵,所有其他的信號量函數使用由semget函數返回的信號量標識符。如果多個程序使用相同的key值,key將負責協調工作。
第二個參數num_sems指定需要的信號量數目,它的值幾乎總是1。
第三個參數sem_flags是一組標誌,當想要當信號量不存在時創建一個新的信號量,可以和值IPC_CREAT做按位或操作。設置了IPC_CREAT標誌後,即使給出的鍵是一個已有信號量的鍵,也不會產生錯誤。而IPC_CREAT | IPC_EXCL則可以創建一個新的,唯一的信號量,如果信號量已存在,返回一個錯誤。
semget函數成功返回一個相應信號標識符(非零),失敗返回-1.
2、semop函數
它的作用是改變信號量的值,原型爲:
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
sem_id是由semget返回的信號量標識符,sembuf結構的定義如下:
struct sembuf{
short sem_num; //除非使用一組信號量,否則它爲0
short sem_op; //信號量在一次操作中需要改變的數據,通常是兩個數,
//一個是-1,即P(等待)操作,
//一個是+1,即V(發送信號)操作。
short sem_flg; //通常爲SEM_UNDO,使操作系統跟蹤信號,
//並在進程沒有釋放該信號量而終止時,操作系統釋放信號量
};
num_sem_ops是信號操作結構的數量,恆大於或等於1。
3、semctl函數
該函數用來直接控制信號量信息,它的原型爲:
int semctl(int sem_id, int sem_num, int command, ...);
如果有第四個參數,它通常是一個union semum結構,定義如下:
union semun{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
sem_id是由semget返回的信號量標識符。sem_num參數是操作信號在信號集中的編號,第一個信號的編號是0。command通常是下面兩個值中的其中一個
SETVAL:用來把信號量初始化爲一個已知的值。p 這個值通過union semun中的val成員設置,其作用是在信號量第一次使用前對它進行設置。
IPC_RMID:用於刪除一個已經無需繼續使用的信號量標識符。
舉個例子:
下面這個例子由兩個進程(父子進程)同時向屏幕輸出字符,父進程每次輸出兩個‘B’,子進程每次輸出兩個‘A’,如果不加信號量控制的話,則輸出的字符並沒有規律。
此時利用一個二元信號量加以控制,則輸出的字符變得有規律了。
程序組成:
comm.h:封裝所需的函數聲明
comm.c:函數的實現
test_sem.c:測試代碼
代碼:
comm.h
#pragma once #include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <unistd.h> #include <errno.h> #define _PROJ_NAME_ "./tmp" #define _PROJ_ID_ 0x6666 //struct sembuf //{ // unsigned short sem_num; /* semaphore number */ // short sem_op; /* semaphore operation */ // short sem_flg; /* operation flags */ //}; int creat_sem_set(int sems); int init_sem_set(int msg_id, int which, int val); int get_sem_set(); int destory_sem_set(int msg_id); int P(int sem_id, int which); int V(int sem_id, int which);
comm.c
#include "comm.h" static int comm_sem_set(int sems, int flag) { key_t _key = ftok(_PROJ_NAME_, _PROJ_ID_); if(_key < 0) { perror("ftok"); return -1; } int sem_id = semget(_key, sems, flag); if(sem_id < 0) { perror("semget"); return -2; } return sem_id; } int create_sem_set(int sems) { int flag = IPC_CREAT | IPC_EXCL | 0644; return comm_sem_set(sems, flag); } int init_sem_set(int msg_id, int which, int val) { int ret = semctl(msg_id, which, SETVAL, val); if(ret < 0) { perror("semctl"); return -1; } return 0; } int get_sem_set() { int flag = IPC_CREAT; return comm_sem_set(0, flag); } int destory_sem_set(int sem_id) { int ret = semctl(sem_id, 0, IPC_RMID); if(ret < 0) { perror("semctl"); return -1; } return 0; } static int op(int sem_id, int which, int op) { struct sembuf _sem; _sem.sem_num = which; _sem.sem_op = op; _sem.sem_flg = 0; int ret = semop(sem_id, &_sem, 1); if(ret < 0) { perror("semop"); return -1; } return 0; } int P(int sem_id, int which) { return op(sem_id, which, -1); } int V(int sem_id, int which) { return op(sem_id, which, +1); }
test_sem.c
#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) //child { int c_sem_id = get_sem_set(); while(1) { P(c_sem_id, 0); printf("A"); fflush(stdout); usleep(rand()%123456); printf("A"); fflush(stdout); usleep(rand()%12345); V(c_sem_id, 0); } } else // father { while(1) { P(sem_id, 0); printf("B"); fflush(stdout); usleep(rand()%123459); printf("B"); fflush(stdout); usleep(rand()%12344); V(sem_id, 0); } wait(NULL); destory_sem_set(sem_id); } return 0; }
總結
信號量是一個特殊的變量,程序對其訪問都是原子操作,且只允許對它進行等待(即P(信號變量))和發送(即V(信號變量))信息操作。我們通常通過信號來解決多個進程對同一資源的訪問競爭的問題,使在任一時刻只能有一個執行線程訪問代碼的臨界區域,也可以說它是協調進程間的對同一資源的訪問權,也就是用於同步進程的。