Linux/Unix系統IPC是各種進程間通信方式的統稱,但是其中極少能在所有Linux/Unix系統實現中進行移植。隨着POSIX和Open Group(X/Open)標準化的推進呵護影響的擴大,情況雖已得到改善,但差別仍然存在。一般來說,Linux/Unix常見的進程間通信方式有:管道、消息隊列、信號、信號量、共享內存、套接字等。博主將在《進程間通信方式總結》系列博文中和大家一起探討學習進程間通信的方式,並對其進行總結,讓我們共同度過這段學習的美好時光。這裏我們就以其中一種方式信號量展開探討,爲了防止出現因多個程序同時訪問同一個共享資源而引發的一系列問題,我們需要一種方法,它可以通過生成並使用令牌來授權,在任一時刻只能有一個執行線程訪問代碼的臨界區域。臨界區域是指執行數據更新的代碼需要獨佔式地執行。而信號量就可以提供這樣的一種訪問機制,讓一個臨界區同一時間只有一個線程在訪問它,也就是說信號量是用來調協進程對共享資源的訪問的。
由於信號量只能進行兩種操作等待和發送信號,即P(sv)和V(sv),他們的行爲是這樣的:
P(sv):如果sv的值大於零,就給它減1;如果它的值爲零,就掛起該進程的執行
V(sv):如果有其他進程因等待sv而被掛起,就讓它恢復運行,如果沒有進程因等待sv而掛起,就給它加1。
Linux提供了一組精心設計的信號量接口來對信號進行操作,它們不只是針對二進制信號量,下面將會對這些函數進行介紹,但請注意,這些函數都是用來對成組的信號量值進行操作的。它們聲明在頭文件sys/sem.h中。下面讓我們一起來看看信號量的操作函數吧!
函數介紹
semget
函數原型:int semget(key_t key, int num_sems, int sem_flags);
key:當key值爲0(IPC_PRIVATE),會建立新信號量集對象;當值爲大於0的32位整數,視參數flags來確定操作。不相關的進程可以通過它訪問一個信號量,它代表程序可能要使用的某個資源,程序對所有信號量的訪問都是間接的,程序先通過調用semget函數並提供一個鍵,再由系統生成一個相應的信號標識符(semget函數的返回值),只有semget函數才直接使用信號量鍵,所有其他的信號量函數使用由semget函數返回的信號量標識符。如果多個程序使用相同的key值,key將負責協調工作。
num_sems:指定需要的信號量數目,它的值幾乎總是1。如果num_sems大於0,則表示一個信號集合(數組)。
sem_flags:是一組標誌,當想要在信號量不存在時創建一個新的信號量,可以和值IPC_CREAT做按位或操作。sem_flags的設置和open系統調用的設置類似,也可以用八進制表示法。設置了IPC_CREAT標誌後,即使給出的鍵是一個已有信號量的鍵,也不會產生錯誤。而IPC_CREAT| IPC_EXCL則可以創建一個新的,唯一的信號量,如果信號量已存在,返回一個錯誤。
semctl
函數原型:int semctl(int sem_id, int sem_num, int command, .../*unionsemun arg*/);
sem_id:信號標識符,semget函數的返回值。
sem_num:信號數組下標,其值幾乎總是0。
command:控制命令
命令
解 釋
IPC_STAT
從信號量集上檢索semid_ds結構,並存到semun聯合體參數的成員buf的地址中
IPC_SET
設置一個信號量集合的semid_ds結構中ipc_perm域的值,並從semun的buf中取出值
IPC_RMID
從內核中刪除信號量集合
GETALL
從信號量集合中獲得所有信號量的值,並把其整數值存到semun聯合體成員的一個array數組中
GETNCNT
返回當前等待資源的進程個數
GETPID
返回最後一個執行系統調用semop()進程的PID
GETVAL
返回信號量集合內單個信號量的值
GETZCNT
返回當前等待100%資源利用的進程個數
SETALL
與GETALL正好相反
SETVAL
用聯合體中val成員的值設置信號量集合中單個信號量的值
arg:semun結構體
union semun{
int val; //單個信號量的值(在SETVAL和GETVAL時使用)
struct semid_ds *buf;
unsigned short *arry; //信號量值數組
};
semop
函數原型:int semop(int semid, struct sembuf *sops, unsigned nsops)
semid:信號標識符,semget函數的返回值。
sops:sembuf結構體數組
struct sembuf {
shortsemnum;
short val;
short flag;
};
semnum:信號量集合中的信號量編號,0代表第1個信號量(即信號數組下標)
val:若val>0進行V操作信號量值加val,表示進程釋放控制的資源。若val<0進行P操作信號量值減val,若(semval-val)<0(semval爲該信號量值),則調用進程阻塞,直到資源可用;若設置IPC_NOWAIT不會睡眠,進程直接返回EAGAIN錯誤。若val==0時阻塞等待信號量爲0,調用進程進入睡眠狀態,直到信號值爲0;若設置IPC_NOWAIT,進程不會睡眠,直接返回EAGAIN錯誤。
flag:0設置信號量的默認操作。IPC_NOWAIT設置信號量操作不等待。SEM_UNDO選項會讓內核記錄一個與調用進程相關的UNDO記錄,如果該進程崩潰,則根據這個進程的UNDO記錄自動恢復相應信號量的計數值。若flag包含SEM_UNDO,則當進程退出的時候會還原該進程的信號量操作,這個標誌在某些情況下是很有用的,比如某進程做了P操作得到資源,但還沒來得及做V操作時就異常退出了,此時,其他進程就只能都阻塞在P操作上,於是造成了死鎖。若採取SEM_UNDO標誌,就可以避免因爲進程異常退出而造成的死鎖。
nsops:進行操作的信號量個數,即sops結構變量的個數,需大於或等於1。最常見設置此值等於1,只完成對一個信號量的操作。
看了一些函數的介紹,沒有使用過信號量的小夥伴可能一頭霧水,下面就我們一起來撥開迷霧,領悟信號量如何實現對多個進程訪問臨界資源的同步作用。哈哈,是不是很拗口啊,下面我們來看一個博主寫的栗子吧,博主將共享內存作爲臨界資源,通過信號量的P、V操作來控制多個進程對臨界資源的同步訪問。
程序實例
sempv.h
#ifndef SEMPV_H_
#define SEMPV_H_
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
//semun聯合體,在設置信號量值是使用
typedef union semun
{
int val;//設置或獲取單個信號量的值
struct semid_ds *buf;//IPC_STAT(讀取信號量的semid_ds結構體數據存於buf中)和IPC_SET(根據buf設置信號量的semid_ds結構體)緩存
unsigned short *array;//設置或獲取信號量集合(數組)所有信號量值
struct seminfo *__buf;//IPC_INFO緩存
}semun_t;
//C++編譯器執行
#ifdef __cplusplus
extern "C"
{
#endif
//P操作
int P(int semid, int semnum);
//V操作
int V(int semid, int semnum);
//C++編譯器執行
#ifdef __cplusplus
}
#endif
#endif
sempv.c
#include "sempv.h"
/*
*
*實現信號量的P、V操作
*
*
*/
//P操作
int P(int semid, int semnum)
{
//sembuf結構體
//成員1:信號量數組下標
//成員2:操作(P:使信號量值減小)
//參數3:flag標識(SEM_UNDO:當程序異常終止時,避免產生死鎖)
struct sembuf sops = {semnum, -1, SEM_UNDO};
//semop完成對信號量的P、V操作
//參數1:信號量標識,semget返回值
//參數2:sembuf結構體指針
//參數3:操作的信號量數量
return semop(semid, &sops, 1);
}
//V操作
int V(int semid, int semnum)
{
//sembuf結構體
//成員1:信號量數組下標
//成員2:操作(V使信號量值增加)
//參數3:flag標識(SEM_UNDO:當程序異常終止時,避免產生死鎖)
struct sembuf sops = {semnum, 1, SEM_UNDO};
//semop完成對信號量的P、V操作
//參數1:信號量標識,semget返回值
//參數2:sembuf結構體指針
//參數3:操作的信號量數量
return semop(semid, &sops, 1);
}
semwr.c
/*
*
*通過信號量實現多個進程對共享內存(臨界資源)的同步訪問
*
*
*/
#include "sempv.h"
#include <stdio.h>
#include <sys/shm.h>
#include <signal.h>
//key的子序號
#define IPC_KEY 0x12
//定義共享內存數據結構體
typedef struct share
{
int n_ID;
char szName[64];
}share_t;
//定義信號量標識
int semid = -1;
//定義共享內存標識
int shmid = -1;
//共享內存連接到進程中返回的共享內存地址
share_t *shmaddr = NULL;
//SIGINT信號處理函數
void sigint(int sig)
{
//卸載共享內存
shmdt(shmaddr);
//刪除信號量
semctl(semid, 0, IPC_RMID);
//刪除共享內存
shmctl(shmid, IPC_RMID, NULL);
exit(EXIT_SUCCESS);
}
//主函數
int main(int argc, char **argv)
{
//檢查命令行參數
if (argc < 2 || ('R' != argv[1][0] && 'W' != argv[1][0]))
{
fprintf(stdout, "Usage %s R|W\n", argv[0]);
exit(EXIT_FAILURE);
}
//註冊中斷信號(當進程接收到SIGINT信號(如:【strl+c】),將調用信號處理函數sigint)
//signal返回舊的信號處理函數
__sighandler_t ret = signal(SIGINT, sigint);
if (SIG_ERR == ret) perror("signal"), exit(EXIT_FAILURE);
//根據目錄創建key_t
char path[256] = {0};
sprintf(path, "%s", getenv("HOME"));//getenv讀取環境變量的值
//根據文件inode號和子序號生成key_t
//如果path的inode號16進製表示:0x450620,那麼key爲0x12450620(IPC_KEY:0x12)
key_t key = ftok(path, IPC_KEY);
/***************創建並初始化信號量*****************/
//創建信號量
//參數1:key_t,用於semget區分不同信號量
//參數2:信號量數量
//參數3:flag(此處爲根據key創建信號量,權限爲0644)
//內存創建者的進程對該信號量的權限爲6;
//內存創建者所在用戶組裏的其他進程對該信號量的權限爲4;
//其他進程對該信號量的權限爲4
semid = semget(key, 1, IPC_CREAT | 0644);
//注意此處爲逗號表達式,不要將","誤寫成";"
if (-1 == semid) perror("semget"), exit(EXIT_FAILURE);
//設置semun結構體
semun_t arg;
arg.val = 1;
//由寫共享內存進程初始化信號量的值
if ('W' == argv[1][0])
{
//初始化信號量
//參數1:信號量標識
//參數2:信號量數組下標
//參數3:控制命令(此處設置信號量的值)
//參數4:semun結構體(存有待設置的信號量的值)
if (-1 == semctl(semid, 0, SETVAL, arg)) perror("semctl"),exit(EXIT_FAILURE);
}
/****************創建並連接共享內存*********************/
//參數1:key_t,用於shmget區分不同共享內存
//參數2:共享內存所佔字節數
//參數3:flag(此處根據key創建共享內存,權限爲0644)
//內存創建者的進程對該內存的權限爲6;
//內存創建者所在用戶組裏的其他進程對該內存的權限爲4;
//其他進程對該內存的權限爲4
shmid = shmget(key, sizeof(share_t), IPC_CREAT | 0644);
//將共享內存連接到當前進程
shmaddr = (share_t*)shmat(shmid, 0, 0);
//寫共享內存
if ('W' == argv[1][0])
{
while (1)
{
//執行P操作
P(semid, 0);
fprintf(stdout, "*********寫共享內存*********\n");
fprintf(stdout, "請輸入用戶ID: ");
scanf("%d", &shmaddr->n_ID);
scanf("%*c");
fprintf(stdout, "請輸入用戶名: ");
scanf("%[^\n]%*c", shmaddr->szName);
//執行V操作
V(semid, 0);
sleep(3);//進程休眠3S
//pause();//將當前進程掛起
}
}
else if ('R' == argv[1][0])//讀共享內存
{
while (1)
{
//執行P操作
P(semid, 0);
fprintf(stdout, "*********讀共享內存*********\n");
fprintf(stdout, "用戶ID: %d\n", shmaddr->n_ID);
fprintf(stdout, "請輸入用戶名: %s\n", shmaddr->szName);
//執行V操作
V(semid, 0);
sleep(3);
//pause();
}
}
return 0;
}
程序運行結果:
在程序中,通過命令行參數區分讀寫邏輯,寫進程初始化信號量(信號量的值初始化爲1),同一個鍵對應的共享內存和信號量只在第一次調用shmget和semget時創建。不論是讀進程還是寫進程,在進入各自的代碼邏輯後,通過P、V操作使讀寫進程對臨界資源的操作是原子操作,同一時間只有一個進程操作共享內存。在代碼邏輯中,博主讓進程休眠3S,避免CPU使用率過高。在程序中,讀寫進程誰先搶佔到信號量誰就可以使用臨界資源,即獲得共享內存的使用權。本篇博文主要是總結信號量的使用方法,共享內存只是簡單的應用一下,後期博主會對其進行單獨總結。可能有的小夥伴會問:“信號量主要是實現進程對臨界資源的同步訪問,和進程間通信有什麼關係啊?”。不同進程在通過唯一的semid訪問信號量,並對信號量的值進行設置,着就是在通信,不同的進程在訪問一個共享資源信號量,並修改其值。
現在是不是對信號量有一定的瞭解啦,博主更希望你已經理解並掌握了信號量的使用方法,可能你第一次接觸P、V操作是在操作系統課程,並使用它寫了一些僞代碼,那麼現在你不用再寫僞代碼了,可以實際的寫一個程序去體驗一番了。感興趣的小夥伴可以對博主的栗子進行改進:當有進程寫共享內存時,進程獨享該臨界資源,避免交叉寫入和讀取髒數據;當只有進程在讀共享內存時,其他讀進程也可以訪問該臨界資源,而寫進程阻塞。
關於信號量的學習我們就到此結束了,相信大家都有所收穫,希望小夥伴們都已經理解並掌握了信號量的常用方法。如果你覺得對進程間通信的方式不勝瞭解,還有些許疑惑,請關注博主《進程間通信方式總結》系列博文,相信你在那裏能找到答案。