一、什麼是信號量
信號量的本質是一種數據操作鎖,它本⾝身不具有數據交換的功能,而是通過控制其他的通信資源(文件,外部設備)來實現進程間通信,它本身只是一種外部資源的標識。信號量在此過程中負責數據操作的互斥、同步等功能。
當請求一個使用信號量來表示的資源時,進程需要先讀取信號量的值來判斷資源是否可用。⼤於0,資源可以請求,等於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 nsems,int semflg)
這個函數是對信號量的創建,第一個參數爲一個整數值(唯一非零),不相關的進程可以通過他來訪問一個信號量,他代表程序需要使用的某個資源,程序對所有信號量的訪問都是間接的,程序先通過調用semget()函數並提供一個鍵,再由系統生成一個相應的信號標識符(也就是semget函數的返回值)。只有semget函數才能直接使用信號量鍵,所有其他的信號量函數使用由semget函數返回的信號量標識符。如果多個程序使用相同的key值,key將負責協調工作。
第二個參數num_sems是指定需要的信號量數目,他的值幾乎總是1.
第三個參數sem_flags是一組標誌,用IPC_CREAT時表示若申請的信號量存在則返回,若不存在則創建之。IPC_CREAT||IPCEXCL時表示如果申請的信號量存在則返回錯誤,若沒有則創建之。
函數的返回值:成功返回一個相應信號標識符(非0),失敗返回-1。
2、semctl()
函數原型:int semctl(int sem_id,int sem_num,int command,...);
第二個參數:信號量集數組上的下標,表示某一信號量。
第三個參數:表示執行的命令,IPC_RMID表示刪除信號集合,SETVAL表示設置聯合體val的值。
第四個參數(可以沒有):如果有第四個參數,是一個聯合體
union semun {
int val; // 使⽤用的值
struct semid_ds *buf; // IPC_STAT、IPC_SET 使⽤用緩存區
unsigned short *array; // GETALL,、SETALL 使⽤用的數組
struct seminfo *__buf; // IPC_INFO(Linux特有) 使⽤用緩存區
};
注意:該聯合體沒有定義在任何系統頭⽂文件中,因此得⽤用戶⾃自⼰己聲明。 <Centos 下確實是這樣,但是UNIX下不同,不需要⾃己定義聲明>。
3、semop()改變信號量的值
函數原型:int semop(int sem_id,struct sembuf *sem_opa,size_t num_sem_ops)
第二個參數,sem_ops是一個指向結構數組的指針,其中的每一個結構至少包含下列成員:
這裏說下第三個成員 sem_flg:通常設置爲SEM_UNDO,這會使得操作系統跟蹤當前進程對信號量所做的改變,而且如果進程終止而沒有釋放這個信號量, 如果信號量爲這個進程所佔有,這個標記可以使得操作系統自動釋放這個信號量。
第三個參數,表示進行操作信號量的個數,即sops結構變量的個數,需大於或等於1。最常見設置此值等於1,只完成對一個信號量的操作。
函數返回值:成功:返回信號量集的標識符,錯誤,返回-1
信號量的意圖在於進程間同步,互斥鎖和條件變量的意圖則在於線程間同步。但是信號量也可⽤於線程間,互斥鎖和條件變量也可用於進程間。我們應該使⽤用適合具體應⽤的那組原語。
四、源碼驗證:
Comm.h:
#ifndef _COMM_H_
#define _COMM_H_
#define PATHNAME "."
#define PROJ_ID 0x666
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
typedef union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO */
void *_pad;
}mysemun;
int create_sem(int nums);
int destroy_sems(int semid);
int init_sem(int semid,int which,int _val);
int P(int semid);
int V(int semid);
int get_sems();
int comm_sems(int nums,int flags);
int comm_semop(int semid,int which,int op);
#endif
Comm.c:
#include"comm.h"
int comm_sems(int nums,int flags)
{
key_t k=ftok(PATHNAME,PROJ_ID);
if(k<0)
{
perror("fork");
return -1;
}
int semid=semget(k,nums,flags);
if(semid<0)
{
perror("semget");
return -1;
}
return semid;
}
//創建
int create_sems(int nums)
{
return comm_sems(nums,IPC_CREAT | IPC_EXCL | 0666);
}
//獲取
int get_sems()
{
return comm_sems(0,IPC_CREAT);
}
//銷燬
int destroy_sems(int semid)
{
if(semctl(semid,0,IPC_RMID)<0)
{
perror("semctl");
return -1;
}
return 0;
}
//操作
int comm_semop(int semid,int which ,int _op)
{
struct sembuf mysembuf;
mysembuf.sem_num=which;
mysembuf.sem_op=_op;
mysembuf.sem_flg=0;
if(semop(semid,&mysembuf,1)<0)
{
perror("semop");
return -1;
}
return 0;
}
//初始化
int init_sem(int semid,int which,int _val)
{
mysemun un;
un.val=_val;
if(semctl(semid,which,SETVAL,un)<0)
{
perror("semctl");
return -1;
}
return 0;
}
int P(int semid)
{
return comm_semop(semid,0,-1);
}
int V(int semid)
{
return comm_semop(semid,0,1);
}
Sem.c:
#include"comm.h"
int main()
{
int semid=create_sems(1);
init_sem(semid,0,1);
pid_t id=fork();
if(id<0)
{
perror("fork");
return -1;
}
if(id == 0)
{
//child
while(1)
{
int child_id=get_sems();
//P(child_id);
printf("A");
usleep(12345);
fflush(stdout);
printf("A");
usleep(22345);
fflush(stdout);
//V(child_id);
}
exit(1);
}
else
{
//father
while(1)
{
//P(semid);
printf("B");
usleep(12345);
fflush(stdout);
printf("B");
usleep(22345);
fflush(stdout);
//V(semid);
}
wait(NULL);
destory_sems(semid);
}
return 0;
}
結果:
沒有用信號量時:
亂序的,兩個進程只要臨界資源空閒就可以進去.
有信號量時:
成對出現,因爲只有一個線程執行完畢,才允許第二個進程進去執行。