信号量(SEM)
1. 基本概念
Linux 信号量是一种同步工具,用于在多个进程之间实现临界区互斥,保证某一时刻只有一个进程能够执行临界区内的代码。信号量有一个整型值,这个值表示当前可用的资源数量。
- 多个进程或线程有可能同时访问的资源(变量、链表、文件等等)称为共享资源,也叫临界资源(critical resources)。
- 访问这些资源的代码称为临界代码,这些代码区域称为临界区 (critical zone)。
- 程序进入临界区之前必须要对资源进行申请,这个动作被称为Р操作,这就像你要把车开进停车场之前,先要向保安申请一张停车卡一样,P操作就是申请资源,如果申请成功,资源数将会减少。如果申请失败,要不在门口等,要不走人。
- 程序离开临界区之后必须要释放相应的资源,这 个动作被称为V操作,这就像你把车开出停车场之后,要将停车卡归还给保安一样,V操作就是释放资源,释放资源就是让资源数增加。
2. 信号量API
2.1 获取信号量ID
semget() 是 Linux 系统中的一个函数,用于创建或获取信号量集合。该函数的原型如下:
int semget(key_t key, int nsems, int semflg);
该函数接受三个参数:
key:信号量集合的键值。
nsems:要创建的信号量数量。
semflg:控制创建信号量集合的方式的标志。如果函数执行成功,则返回一个信号量集合的描述符。否则,返回一个非 0 值。
semget() 函数用于创建或获取信号量集合。它能够用来在多个进程之间实现临界区互斥,保证某一时刻只有一个进程能够执行临界区内的代码。例如,可以使用 semget() 函数创建一个信号量集合,然后使用 semop() 函数来获取或释放信号量。
创建信号量时,还受到以下系统信息的影响:
- SEMMNI:系统中信号量的总数最大值。
- SEMMSL:每个信号量中信号量元素的个数最大值。
- SEMMNS:系统中所有信号量中的信号量元素的总数最大值。
Linux 中,以上信息在/proc/sys/kernel/sem中可查看。
2.2 PV操作
semop() 是 Linux 系统中的一个函数,用于在信号量集合上执行操作。该函数的原型如下:
int semop(int semid, struct sembuf *sops, unsigned nsops);
该函数接受三个参数:
semid:信号量集合的描述符。
sops:指向一个结构体数组的指针,每个结构体包含了一个操作的信息。
nsops:要执行的操作数量。如果函数执行成功,则返回 0。否则,返回一个非 0 值。
semop() 函数用于在信号量集合上执行操作。它能够用来在多个进程之间实现临界区互斥,保证某一时刻只有一个进程能够执行临界区内的代码。例如,可以使用 semget() 函数创建一个信号量集合,然后使用 semop() 函数来获取或释放信号量。
使用以上函数接口需要注意以下几点:
- 信号量操作结构体的定义如下:
struct sembuf
{
    unsigned shortsem_num;	/* 信号量元素序号(数组下标) */
    short sem_op;			/*操作参数*/
    short sem_flg;			/*操作选项*/
};
请注意: 信号量元素的序号从 0 开始,实际上就是数组下标。
- 根据 sem_op 的数值,信号量操作分成 3 种情况:
- A) 当 sem_op 大于0时:进行V操作,即信号量元素的值( semval) 将会被加上 sem_op 的值。如果SEM_UNDO被设置了,那么该V操作将会被系统记录。V操作永远不会导致进程阻塞。
- B) 当 sem_op 等于 0 时:进行等零操作,如果此时 semval 恰好为0,则 semop() 立即成功返回,否则如果 IPC_NOWAIT 被设置,则立即出错返回并将errno设置为EAGAIN,否则将使得进程进入睡眠,直到以下情况发生:
- B1) semval变为 0。
- B2) 信号量被删除。(将导致 semop() 出错退出,错误码为 EIDRM)
- B3) 收到信号。(将导致 semop() 出错退出,错误码为 EINTR)
 
- C) 当 sem_op 小于0时:进行Р操作,即信号量元素的值(semval)将会被减去 sem_op 的绝对值。如果s emval 大于或等于 sem_op 的绝对值,则 semop()立即成功返回,semval 的值将减去 sem_op的绝对值,并且如果 SEM_UNDO 被设置了,那么该Р操作将会被系统记录。如果semval小于sem_op的绝对值并且设置了IPC_NOWAIT,那么semop()将会出错返回且将错误码置为EAGAIN,否则将使得进程进入睡眠,直到以下情况发生:- C1) semval的值变得大于或者等于sem_op的绝对值。
- C2)信号量被删除。(将导致semop()出错退出,错误码为EIDRM)C3)收到信号。(将导致 semop()出错退出,错误码为EINTR)
 
 
2.3 设置或获取信号量属性
semctl() 是 Linux 系统中的一个函数,用于控制信号量集合。该函数的原型如下:
int semctl(int semid, int semnum, int cmd, union semun arg);
该函数接受四个参数:
semid:信号量集合的描述符。
semnum:要操作的信号量编号。
cmd:控制信号量集合的命令。
- IPC_STAT:获取属性信息。
- IPC_SET:设置属性信息。
- IPC_RMID:立即删除该信号量,参数semnum将被忽略。
- IPC_INFO:获得关于信号量的系统限制值信息。
- SEM_INFO:获得系统为共享内存消耗的资源信息。
- SEM_STAT:同IPC_STAT,但 shmid为该SEM在内核中记录所有SEM信息的数组的下标,因此通过迭代所有的下标可以获得系统中所有SEM的相关信息。
- GETALL:返回所有信号量元素的值,参数semnum 将被忽略。
- GETNCNT:返回正阻塞在对该信号量元素Р操作的进程总数。
- GETPID:返回最后一个队该信号量元素操作的进程PID。
- GETVAL:返回该信号量元素的值。
- GETZCNT:返回正阻塞在对该信号量元素等零操作的进程总数。
- SETALL:设置所有信号量元素的值,参数semnum将被忽略。
- SETVAL:设置该信号量元素的值。
arg:用于传递操作参数的联合体。如果函数执行成功,则返回 0。否则,返回一个非 0 值。
semctl() 函数用于控制信号量集合。它能够用来获取、设置或销毁信号量集合。例如,可以使用 semget() 函数创建一个信号量集合,然后使用 semctl() 函数来控制该信号量集合。
这是一个变参函数,根据cmd的不同,可能需要第四个参数,第四个参数是一个如下所示的联合体,用户必须自己定义:
union semun {
    int val;				/* 当cmd为 SETVAL时使用 */
    struct semid_ds *buf;	/* 当cmd为IPC_STAT 或IPC_SET时使用 */
    unsigned short *array;	/* 当cmd为GETALL或 SETALL时使用 */
    struct seminfo *_buf;	/* 当cmd为IPC_INFO时使用 */
}
3. 相关结构体
- 使用IPC_STAT和IPC_SET需要用到以下属性信息结构体:
struct semid_ds
{
    struct ipc _perm sem_perm;		/* 权限相关信息 */
    time_t sem_otime;				/* 最后一次semop( )的时间 */
    time_t sem_ctime;				/* 最后一次状态改变时间 */
    unsigned shortsem_nsems;		/* 信号量元素个数 */
};
- 权限结构体如下:
struct ipc_perm {
    key_t _key;					/* 该信号量的键值 key */
    uid_t uid;					/* 所有者有效UID */
    gid_t gid;					/* 所有者有效GID */
    uid_t cuid;					/* 创建者有效UID */
    gid_t cgid;					/* 创建者有效GID */
    unsigned short mode;		/* 读写权限 */
    unsigned short __seq;		/* 序列号 */
};
- 使用IPC_INFO时,需要提供以下结构体:
structseminfo {
    int semmap;		/* 当前系统信号量总数 */
    int semmni;		/* 系统信号量个数最大值 */
    int semmns;		/* 系统所有信号量元素总数最大值 */
    int semmnu;		/* 信号量操作撤销结构体个数最大值 */
    int semmsl;		/* 单个信号量中的信号量元素个数最大值*/
    int semopm;		/* 调用semop( )时操作的信号量元素个数最大值*/
    int semume;		/* 单个进程对信号量执行连续撤销操作次数的最大值*/
    int semusz;		/* 撤销操作的结构体的尺寸*/
    int semvmx;		/* 信号量元素的值的最大值 */
    int semaem;		/* 撤销操作记录个数最大值 */
};
- 使用SEM_INFO时,跟IPC_INFO一样都是得到一个seminfo结构体,但其中几个成员的含义发生了变化:
- A) semusz此时代表系统当前存在的信号量的个数。
- B) semaem 此时代表系统当前存在的信号量中信号量元素的总数。
 
4. 示例代码
下面的示例代码 a 可以向 b 发送消息。
- head.h
#ifndef _HEAD_H_
#define _HEAD_H_
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <time.h>
#define PATH	"./"
#define PROJ_SHM_ID	1
#define PROJ_SEM_ID	2
#define SHMSIZE		1024	//共享内存的尺寸
#define SPACE		0
#define DATA		1
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	   (Linux-specific) */
};
//初始化信号量
//semid: 要初始化的信号量的集合
//semnum: 要初始化的是第几个信号量(数组下标)
//value: 要初始化给信号量的初始资源个数
static void sem_init(int semid, int semnum, int value)
{
	union semun v;
	v.val = value;
	if(semctl(semid, semnum, SETVAL, v) == -1) {
		perror("sem_init() fail");
		exit(0);
	}
}
//P操作
//semid: 要对哪一个信号量ID进行操作
//semnum: 要初始化的是第几个信号量(数组下标)
//value: 想要申请的资源个数
static bool sem_p(int semid, int semnum, int value)
{
	struct sembuf sops;
	sops.sem_num = semnum;
	sops.sem_op = -value;	//申请资源,减操作
	sops.sem_flg = 0;
	
	if( semop(semid, &sops, 1) == -1)
		return false;
	return true;
}
//V操作
//semid: 要对哪一个信号量ID进行操作
//semnum: 要操作的是第几个信号量(数组下标)
//value: 想要释放的资源个数
static bool sem_v(int semid, int semnum, int value)
{
	struct sembuf sops;
	sops.sem_num = semnum;
	sops.sem_op = value;	//释放资源,加操作
	sops.sem_flg = 0;
	
	if( semop(semid, &sops, 1) == -1)
		return false;
	return true;
}
#endif
- a.c
#include "head.h"
int main(int argc, char *argv[])
{
	//获取一个IPC对象key
	key_t shm_key = ftok(PATH, PROJ_SHM_ID);
	key_t sem_key = ftok(PATH, PROJ_SEM_ID);
	
	//获取一个共享内存的ID
	int shmid = shmget(shm_key, SHMSIZE, IPC_CREAT|0666);
	if(shmid == -1) {
		perror("shmget() fail");
		exit(0);
	}
	
	//将共享内存映射到本进程空间
	char *p = shmat(shmid, NULL, 0);
	if(p == (void *)-1) {
		perror("shmat() fail");
		exit(0);
	}
	
	//获取信号量IPC的ID
	int semid = semget(sem_key, 2, IPC_CREAT|IPC_EXCL|0666);
	if(semid == -1 && errno == EEXIST) {
		semid = semget(sem_key, 2, 0666);	//存在就打开两个信号量
	}
	else if(semid == -1) {	//其他的错误
		perror("semget() fail");
		exit(0);
	} else { //正常创建成功, 初始化信号量所代表资源数量
		sem_init(semid, SPACE, 1);	//初始化空间资源为1
		sem_init(semid, DATA, 0);	//初始化数据资源为0
	}
	
	while(1) {	
		// P操作
		sem_p(semid, SPACE, 1);
	
		// 写入数据
		fgets(p, SHMSIZE, stdin);
		//V操作
		sem_v(semid, DATA, 1);
		
		if( !strncmp(p, "quit", 4) )
			break;
	}
	
	//释放共享内存
	shmdt(p);
	return 0;
}
- b.c
#include "head.h"
int main(int argc, char *argv[])
{
	//获取一个IPC对象key
	key_t shm_key = ftok(PATH, PROJ_SHM_ID);
	key_t sem_key = ftok(PATH, PROJ_SEM_ID);
	
	//获取一个共享内存的ID
	int shmid = shmget(shm_key, SHMSIZE, IPC_CREAT|0666);
	if(shmid == -1) {
		perror("shmget() fail");
		exit(0);
	}
	
	//将共享内存映射到本进程空间
	char *p = shmat(shmid, NULL, 0);
	if(p == (void *)-1) {
		perror("shmat() fail");
		exit(0);
	}
	
	//获取信号量IPC的ID
	int semid = semget(sem_key, 2, IPC_CREAT|IPC_EXCL|0666);
	if(semid == -1 && errno == EEXIST) {
		semid = semget(sem_key, 2, 0666);	//存在就打开两个信号量
	}
	else if(semid == -1) {	//其他的错误
		perror("semget() fail");
		exit(0);
	}
	else {	//正常创建成功, 初始化信号量所代表资源数量
		sem_init(semid, SPACE, 1);	//初始化空间资源为1
		sem_init(semid, DATA, 0);	//初始化数据资源为0
	}
	
	
	while(1) {
		// P操作
		sem_p(semid, DATA, 1);
		
		// 读出数据
		printf("%s\n", p);
		if( !strncmp(p, "quit", 4) )
			break;
		
		//V操作
		sem_v(semid, SPACE, 1);
	}
	
	//释放资源
	shmdt(p);	//解除共享内存映射
	semctl(semid, 0, IPC_RMID);		//删除信号量的IPC对象
	shmctl(shmid, IPC_RMID, NULL);	//删除共享内存的IPC对象
	return 0;
}