在Linux中支持System V進程通信的手段有三種:消息隊列(Message queue)、信號量(Semaphore)、共享內存(Shared memory)。下面我們闡述一下信號量的進程間通信方式以及工作原理。
IPC的一點補充
Linux中的內存空間分爲系統空間和用戶空間。
在系統空間中:由於各個線程的地址空間是共享的,即一個線程可以隨意訪問 kernel 中任意地址,所以無需進程通信機制的保護。
在用戶空間中:每個進程都有自己的地址空間,一個進程要和另外一個進程通信,必須有足夠的權限能夠訪問其他進程的 kernel ,從而與其他進程通信。
信號量的引入原因?
爲了防止多個進程同時訪問一塊資源而引發的一系列問題,我們需要一種可以通過它生成並使用令牌來授權,在任意時刻只能有一個執行線程訪問代碼的臨界區。而信號量則提供了這樣的機制讓一個臨界區同一時間只有一個線程在訪問,即信號量是用來協調進程對資源的訪問的。
當然在我們開始講信號量之前,我們需要對幾個重要的概念有一個初步的認識:
臨界資源:一次僅允許一個進程使用的資源稱爲臨界資源。許多物理設備都屬於臨界資源,如輸入機、打印機、磁帶機等。
臨界區:臨界區內的數據一次只能同時被一個進程使用,當一個進程使用臨界區內的數據時,其他需要使用臨界區數據的進程進入等待狀態。
互斥:指某一資源同時只允許一個訪問者對其進行訪問。
原子性:一個事務包含多個操作,這些操作要麼全部執行,要麼都不執行。
同步:基本都是以互斥爲條件,讓不同的進程訪問臨界資源,以某種特定的順序去訪問。
信號量的本質
信號量在本質上是一種數據操作鎖(計數器,記錄統計臨界資源的數目)。它本身不具備數據交換的功能,而是通過保護其他的通信(文件、外部設備)等臨界資源來實現進程間通信。信號量在此過程中負責數據操作的互斥、同步等功能。
當請求一個使用信號量來表示的臨界資源時,進程需要先讀取信號量的值來判斷資源是否可用:
(1)信號量 > 0:表示有資源可用。
(2)信號量 = 0:表示無資源可用,進程會進入睡眠狀態直至資源可用。
當進程不再使用一個信號量控制的共享資源時,信號量+1,對信號量的值進行增加操作均爲原子操作(原因在於:信號量的主要作用是維護資源的互斥或多個進程的同步訪問)。而在信號量的創建及初始化上,不能保證操作均爲原子的。
生命週期:信號量的生命週期並不隨進程的結束而結束,而是隨內核的。
信號量的工作原理
由於信號量只能進行兩種操作等待和發送信號,即P(sv)和 V(sv),sv爲信號量,它們的行爲如下:
P(sv):如果sv的值大於0,就對其減1;如果它的值爲0,就掛起該進程的執行。
V(sv):如果有其他進程因等待sv而被掛起,就讓它恢復運行;如果沒有進程因等待sv而被掛起,就給它加1。
舉例說明:兩個進程共享信號量sv,一旦其中一個進程執行了P(sv)操作,它將得到信號量,並可以進入臨界區,使sv減1;而第二個進程將被阻止進入臨界區,因爲當它試圖執行P(sv)時,sv這時候爲0,它就會被掛起以等待第一個進程離開臨界區並執行V(sv)釋放信號量後,這時候第二個進程纔可以恢復執行。
二元信號量:是最簡單的一種鎖,它只有兩種狀態(佔用和非佔用)。它適合只能被唯一一個線程訪問的資源。當個人員信號量處於非佔用狀態時,第一個試圖獲取該二元信號量的線程會獲得鎖,並將二元信號量置爲佔用狀態,這時其他的所有試圖獲取該二元信號量的線程將會被等待,直至該鎖被釋放。
Linux信號量機制
Linux中提供了一組精心設計的信號量接口來對信號量進行操作,它們不只是針對二進制信號量,但是這些函數都是用來對成組的信號量進程操作的,它們的聲明在 sys/sem.h 中。
在sem_structure中也有關於struct ipc_perm的結構體成員,這說明信號量同樣是IPC的進程通信方式之一。雖然信號量本質上並不能進行數據交換,但是其負責數據操作的互斥及同步的功能。
(1)在System V中信號量並非是單個非負值,而必須將信號量定義爲含有一個或多個信號量值的集合。當創建信號量時,要指定該集合中信號量值的數量。
(2)創建信號量(semget)和對信號量賦初值(semctl)分開進行,這是一個弱點,因爲不能原子地創建一個信號量集合,並且對該集合中的各個信號量賦初值。
(3)即使沒有進程在使用IPC資源,它們任然是存在的,要時刻防止資源被鎖定,避免程序在異常情況下結束時沒有解鎖資源,可以用關鍵字(SEM_UNDO)在退出時恢復信號量值爲初始值。
信號量相關接口函數
1.ftok函數:把一個已經存在的路徑名和一個整數標識得轉換成一個key_t值,稱爲IPC鍵:
key_t ftok(const char *pathname, int proj_id);
參數[pathname]:通常是跟本應用有關的目錄。
參數[proj_id]:指的是本應用所用到的IPC的一個序列號,成功返回IPC鍵,失敗返回-1。
[返回值]:成功返回鍵值,失敗返回-1。
注:兩進程如在pathname和proj_id上達成一致(或約定好),雙方就都能夠通過調用ftok函數得到同一個IPC鍵。
pathname的實現是組合了三個鍵,分別是:
(1)pathname所在文件系統的信息(stat結構的st_dev成員)。
(2)pathname在文件系統內的索引節點號(stat結構的st_ino成員)。
(3)id的低序8位(不能爲0)。
ftok調用返回的整數IPC鍵由proj_id的低序8位,st_dev成員的低序8位,st_info的低序16位組合而成。
不能保證兩個不同的路徑名與同一個proj_id的組合產生不同的鍵,因爲上面所列的三個條目(文件系統、標識符、索引節點、proj_id)中的信息位數可能大於一個整數的信息位數。
2.semget函數:創建一個信號量或訪問一個已經存在的信號量集。
int semget(key_t key, int nsems, int semflg);
參數[key]:類似於端口號,也可以由ftok函數生成。
參數[nsems]:在System V中,申請信號量是以信號量集nsems去申請,而不是一個一個去申請,底層是一個數組。
參數[semflg]:IPC_CREAT或IPC_EXCL
[返回值]:是一個稱爲信號量標識符的整數,semop 和 semctl 函數將使用它。
3.semop函數:用來創建和訪問一個信號量集。
int semop(int semid, struct sembuf *sops, unsigned nsops);
參數[semid]:是該信號量的標識碼,也就是semget函數的返回值。
參數[sops]:是個指向一個結構數值的指針。
參數[nsops]:信號量操作結構的個數,恆大於等於1。
[返回值]:成功返回0,失敗返回-1。
信號量操作由sembuf結構表示:
struct sembuf{
short sem_num; // 在信號集中的編碼0,1,2...
short sem_op; // 信號量在一次操作中需要改變的數據,通常是兩個數,
// 一個是-1,即P(等待)操作,一個是+1,即V(發送信號)操作
short sem_flg; // 通常爲SEM_UNDO,使操作系統跟蹤信號,並在進程沒有釋放該信號量而終止時, 操作系統釋放信號量
};
參數nsops規定sops數組元素的個數:sem_op的取值如下:
(1)若sem_op爲正(V操作),這對應於進程釋放佔用的資源數。sem_op值加到信號量的值上去。
(2)若sem_op爲負(P操作),這表示要獲取該信號量控制的資源數。信號量值減去sem_op的絕對值。
(3)若sem_op爲0,這表示調用進程希望等待到信號量值變爲0。
4.semctl函數:初始化或移除信號量集。
int semctl(int semid, int semnum, int cmd, ...);//可變參數列表
參數[semid]:信號量集IPC標識符。
參數[semnum]:表示信號量集中的哪個信號量,第一個爲0。
參數[cmd]:在semid指定的信號量集上指向此命令,cmd的選擇如下:
//10 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 設置信號量集中的一個單獨的信號量的值。
參數[可變參數列表]:是可選的,取決於第三個參數cmd。
[返回值]:成功返回正數,失敗返回-1。
5.查看信號量:
ipcs -s
6.刪除信號量:
ipcrm -s [semid]
通過代碼模擬父子進程的互斥
Makefile
//Makefile
sem:comm.c sem.c
gcc -o $@ $^
.PHONY:clean
clean:
rm sem
comm.h
//comm.h
#include<sys/wait.h>
#include<sys/ipc.h>
#include<sys/stat.h>
#include<sys/sem.h>
#define PROJ_ID 0x6666
#define PATHNAME "."
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *_buf;
};
int createSemSet(int nums);
int getSemSet();
int destroySems(int semid);
int initSems(int semid,int who,int value);
int P(int semid,int who);
int V(int semid,int who);
#endif
comm.c
//comm.c
#include"comm.h"
static int commSemSet(int nums,int flags)
{
key_t key = ftok(PATHNAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return -1;
}
int semid = semget(key,nums,flags);
if(semid < 0)
{
perror("semget");
return -2;
}
return semid;
}
int createSemSet(int nums)
{
return commSemSet(nums,IPC_CREAT | IPC_EXCL | 0666);
}
int initSems(int semid,int who,int initval)
{
union semun _un;
_un.val = initval;
if(semctl(semid,who,SETVAL,_un) < 0)
{
perror("semctl");
return -1;
}
return 0;
}
static int commPV(int semid,int who,int op)
{
struct sembuf _sf;
_sf.sem_num = who;
_sf.sem_op = op;
_sf.sem_flg = 0;
return semop(semid,&_sf,1);
}
int P(int semid,int who)
{
return commPV(semid,who,-1);
}
int V(int semid,int who)
{
return commPV(semid,who,1);
}
int getSemSet()
{
return commSemSet(0,IPC_CREAT);
}
int destroySems(int semid)
{
if(semctl(semid,0,IPC_RMID,NULL) < 0)
{
perror("semctl");
return -1;
}
return 0;
}
sem.c
//sem.c
#include"comm.h"
int main()
{
int semid = createSemSet(1);
initSems(semid,0,1);
pid_t id = fork();
if(id == 0)//child
{
int semid = getSemSet();
while(1)
{
P(semid,0);
printf("A");
fflush(stdout);
usleep(300000);
printf("A");
fflush(stdout);
usleep(300000);
V(semid,0);
}
}
else//father
{
while(1)
{
P(semid,0);
printf("B");
fflush(stdout);
usleep(400000);
printf("B");
fflush(stdout);
usleep(400000);
V(semid,0);
}
wait(NULL);
}
destroySemSet(semid);
return 0;
}