跳到主要内容

信号量(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;
}