什麼是信號量
信號量本質是一個數據操作鎖,本身並不可以進行數據的通信。而是通過其他資源進行進程間的通信,本身是一種外部標識符。
信號量在此操作中負責數據的同步、互斥等功能
信號量的作用
爲了防止多個線程同時訪問同一個臨界資源導致的一系列問題,使得在同一時刻只有一個線程訪問代碼的臨界資源
信號量就是提供了這種機制,讓一個臨界區同一時刻只有一個線程在訪問
信號量的工作原理
由於信號量只能進行兩種操作等待和發送信號,即P(sv)和V(sv),他們的行爲是這樣的:
P(sv):如果sv的值大於0,就給他減1;如果值等於0,就掛起該進程的執行
V(sv):如果有其他進程因等待sv而被掛起,就讓他恢復運行,如果沒有進程因等待sv而掛起,就給他加1;
Linux下的信號量機制
(1)獲得信號量 semget
頭文件
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
函數功能
得到一個信號量集標識符,或者創建一個信號量集對象並返回信號量集標識符
函數原型
int semget(key_t key,int nsems, int semflg);
key的傳入參數
<1>當key傳入0時,建立新的信號量集對象
<2>大於0的32位整數,視參數semflg來決定操作,通常要求此值來源於fork的返回的IPC鍵值
nsems
創建信號量集中信號量的個數,該參數只在創建信號量集的時候有效
msgflg
<1>傳入0時,取信號量集標識符,若不存在函數會報錯
<2>IPC_CREAT,當semflg&IPC_CREAT爲真時,如果內存中不存在鍵值與key相等的信號量集,則建一個新的信號量集;如果存在這樣的信號量集,返回此信號量集的標識符
<3>IPC_CREAT|IPC_EXCL ,如果內存中不存在鍵值與key相等的信號量集,則創建一個新的消息隊列,如果存在這樣的信號量集則報錯
函數返回值
成功:返回信號量集的標識符
錯誤:-1
錯誤代碼
EACCESS:沒有權限
EEXIST:信號量集已經存在,無法創建
EIDRM:信號量集已經刪除
ENOENT:信號量集不存在,同時semflg沒有設置IPC_CREAT標誌
ENOMEM:沒有足夠的內存創建新的信號量集
ENOSPC:超出限制
(2)銷燬信號量 semctl
頭文件
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
函數功能
得到一個信號量集標識符,或創建一個信號量集對象並返回信號量集標識符
函數原型
int semctl(int semid, int semnum, int cmd, union semnum arg);
函數傳入值
semid : 信號量集標識符
semnum : 信號量集數組上的下標,表示某一個信號量
cmd :
arg :
union semun
{
short val; /*SETVAL用的值*/
struct semid_ds* buf; /*IPC_STAT、IPC_SET用的semid_ds結構*/
unsigned short* array; /*SETALL、GETALL用的數組值*/
struct seminfo *buf; /*爲控制IPC_INFO提供的緩存*/
} arg;
函數返回值
成功:大於或者等於0
錯誤:-1
錯誤代碼:
EACCESS:權限不夠
EFAULT:arg指向的地址無效
EIDRM:信號量集已經刪除
EINVAL:信號量集不存在,或者semid無效
EPERM:進程有效用戶沒有cmd的權限
ERANGE:信號量值超出範圍
(3)PV操作
頭文件
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
函數功能
對信號量集標識符爲semid的一個或者多個信號量進行P或者V操作
函數原型
int semop(int semid, struct sembuf* sops, unsigned nsops);
函數傳入值
semid : 信號量標識符
sops : 指向進行操作的信號量集結構體數組的首地址,此結構的具體說明如下:
struct sembuf {
short semnum; /*信號量集合中的信號量編號,0代表第1個信號量*/
short val;/*若val>0進行V操作信號量值加val,表示進程釋放控制的資源 */
/*若val<0進行P操作信號量值減val,若(semval-val)<0(semval爲該信號量值),則調用進程阻塞,直到資源可用;
若設置IPC_NOWAIT不會睡眠,進程直接返回EAGAIN錯誤*/
/*若val==0時阻塞等待信號量爲0,調用進程進入睡眠狀態,直到信號值爲0;若設置IPC_NOWAIT,進程不會睡眠,直接返回EAGAIN錯誤*/
short flag; /*0 設置信號量的默認操作*/
/*IPC_NOWAIT設置信號量操作不等待*/
/*SEM_UNDO 選項會讓內核記錄一個與調用進程相關的UNDO記錄,如果該進程崩潰,則根據這個進程的UNDO記錄自動恢復相應信號量的計數值*/
};
函數返回值
成功:信號量集的標識符
錯誤:-1
錯誤代碼:
E2BIG:一次對信號量個數的操作超過了系統限制
EACCESS:權限不夠
EAGAIN:使用了IPC_NOWAIT,但操作不能繼續進行
EFAULT:sops指向的地址無效
EIDRM:信號量集已經刪除
EINTR:當睡眠時接收到其他信號
EINVAL:信號量集不存在,或者semid無效
ENOMEM:使用了SEM_UNDO,但無足夠的內存創建所需的數據結構
ERANGE:信號量值超出範圍
信號量的模擬實現
comm.h
#ifndef __COMM_H__
#define __COMM_H__
#include<stdlib.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<string.h>
#include<unistd.h>
#define PATHNAME "."
#define PROJ_ID 88
union semun_t
{
int val;
struct semid_ds* buf;
unsigned short* array;
struct seminfo* __buf;
}semun;
int CreatSem(int sem_num);
int GetSem(int sem_num);
int InitSem(int sem_id, int which);
int P(int sem_id, int which);
int V(int sem_id, int which);
int DestorySem(int sem_id);
#endif
comm.c
#include"comm.h"
//創建信號量和獲取信號量所調取的公共函數
static int commSem(int sem_num, int flag)
{
key_t key = ftok(PATHNAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return -1;
}
int semid = semget(key,sem_num,flag);
if(semid < 0)
{
perror("semget");
return -2;
}
return semid;
}
//創建信號量
int CreatSem(int sem_num)
{
return commSem(sem_num,IPC_CREAT|IPC_EXCL|0666);
}
//獲得已經產生的信號量
int GetSem(int sem_num)
{
return commSem(sem_num,IPC_CREAT);
}
//初始化信號量
//which表示服務端或者客戶端
int InitSem(int semid,int which)
{
semun.val = 1;
//獲取信號量集或者創建一個信號量
int ret = semctl(semid,which,SETVAL,semun);
if(ret < 0)
{
perror("semctl");
return -1;
}
return ret;
}
//進行P、V操作的、可調用的公共代碼
static int opsem(int semid,int op,int which)
{
struct sembuf buf;
memset(&buf,'\0',sizeof(buf));
buf.sem_op = op;
buf.sem_num = which;
buf.sem_flg = 0;
return semop(semid,&buf,1);
}
//進行P操作,令信號量的值-1
int P(int semid, int which)
{
int ret = opsem(semid, -1, which);
if(ret < 0)
{
perror("opsem");
return -1;
}
return ret;
}
//進行V操作,令信號量的值+1
int V(int semid, int which)
{
int ret = opsem(semid, 1, which);
if(ret < 0)
{
perror("opsem");
return -1;
}
return ret;
}
//進行信號量的銷燬
int DestroySem(int semid)
{
int ret = semctl(semid, 0, IPC_RMID, NULL);
if(ret < 0)
{
perror("semctl");
return -1;
}
return ret;
}
測試代碼
mainSem.c
#include"comm.h"
int main()
{
int semid = CreatSem(4);//創建信號量,且大小爲4
InitSem(semid, 0);//初始化信號量
int id = fork();//創建子進程
if(id == 0)//child,打印a
{
printf("child , pid :%d ppid : %d\n",getpid(),getppid());
while(1)
{
P(semid, 0);//進行P操作
printf("a");
usleep(100000);
fflush(stdout);
printf("a");
usleep(200000);
fflush(stdout);
V(semid, 0);//進行V操作
}
}
else//father,打印b
{
printf("father , pid : %d ppid : %d \n",getpid(),getppid());
while(1)
{
P(semid,0);//P操作
printf("b");
usleep(300000);
fflush(stdout);
printf("b");
usleep(400000);
fflush(stdout);
V(semid,0);//V操作
}
wait(NULL);
}
DestorySem(semid);//進行信號量的銷燬
return 0;
}
運行結果
實現了AB成對打印的目的
SEM_UNDO標識符
當操作信號量(semop)時,sem_flg可以設置SEM_UNDO標識
SEM_UNDO用於將修改的信號量值在進程正常退出(調用exit退出或main執行完)或異常退出(如段異常、除0異常、收到KILL信號等)時歸還給信號量。
如信號量初始值是20,進程以SEM_UNDO方式操作信號量減2,減5,加1;
在進程未退出時,信號量變成20-2-5+1=14;在進程退出時,將修改的值歸還給信號量,信號量變成14+2+5-1=20。