【Linux】IPC通信之信號量

什麼是信號量

信號量本質是一個數據操作鎖,本身並不可以進行數據的通信。而是通過其他資源進行進程間的通信,本身是一種外部標識符。

信號量在此操作中負責數據的同步、互斥等功能

信號量的作用

爲了防止多個線程同時訪問同一個臨界資源導致的一系列問題,使得在同一時刻只有一個線程訪問代碼的臨界資源

信號量就是提供了這種機制,讓一個臨界區同一時刻只有一個線程在訪問

信號量的工作原理

由於信號量只能進行兩種操作等待和發送信號,即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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章