一、概念
信号量是一种用于控制多进程访问共享资源的同步互斥机制,它是以集合的形式存在,一个信号量集可以包含多个信号量。此外,每个信号量都有一个整数值,表示可用资源的数量,进程就是通过对信号量值进行减少和增加来获取和释放资源的。
二、特点
-
功能强大,支持信号量集合
-
操作是原子性的,适合复杂同步场景
-
信号量集存在于内核中,不依赖于单个进程的生命周期,需要显式删除或系统重启才会消失
-
在 Linux 中,信号量有如下限制:
(1)SEMMNI :系统范围内信号量集的最大数量
(2)SEMMSL:单个信号量集中的信号量数量上限
(3)SEMMNS:所有信号量集中的信号量总数上限
(4)SEMOPM:单个 semop 调用能执行的最大操作数查看方法如下:
方法一:通过ipcs -ls
命令查看
方法二:查看系统内核信息(依次是:SEMMSL
,SEMMNS
,SEMOPM
,SEMMNI
)
三、信号量数据结构
信号量集具有自身特有的数据结构 semid_ds
,该结构描述了信号量集的一些属性和状态信息等,详细信息可参阅文件 /usr/include/linux/sem.h
。
struct semid_ds {
struct ipc_perm sem_perm; /* permissions .. see ipc.h */
__kernel_time_t sem_otime; /* last semop time */
__kernel_time_t sem_ctime; /* last change time */
struct sem *sem_base; /* ptr to first semaphore in array */
struct sem_queue *sem_pending; /* pending operations to be processed */
struct sem_queue **sem_pending_last; /* last pending operation */
struct sem_undo *undo; /* undo requests on this array */
unsigned short sem_nsems; /* no. of semaphores in array */
};
四、信号量相关函数
1. semget
- 【头文件】:
#include <sys/types.h>
、#include <sys/ipc.h>
、#include <sys/sem.h>
- 【函数原型】:
int semget(key_t key, int nsems, int semflg);
- 【功能】:创建或访问一个信号量集
- 【参数】:
(1)key:信号量集的键值
(2)nsems:信号量集中信号量的数量
(3)semflg:权限标志位,由两部分组成,一部分为IPC对象存取权限(含义同 ipc_perm 中的 mode),另一部分为IPC对象创建模式标志(IPC_CREAT、IPC_EXCL),一般会将这两部分进行|
运算,从而完成对IPC对象创建的管理
- 【返回值】:成功返回一个信号量集标识符(非负整数);失败则返回 -1,并将 errno 设置为错误标识符
【示例】:演示 IPC_CREAT|IPC_EXCL
的使用效果
【代码】:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
int main() {
extern int errno;
int semid = semget(0x123, 1, IPC_CREAT|IPC_EXCL|0644);
if (semid == -1) {
perror("semget");
printf("errno: %d\n", errno);
return -1;
}
printf("semid: %d\n", semid);
return 0;
}
【执行结果】:
【分析】:
从执行结果来看,当我们第一次运行程序时,成功创建了信号量集。但是,当第二次运行程序后,发现创建失败,errno
被置为17,也就是 EEXIST
,错误信息为 File exists
,出现这样的结果是因为我们在第一次程序运行后,该信号量集就已经在系统中存在了,而我们在创建信号量集时又使用了 IPC_EXCL
,这才导致 semget
出错返回。
2. semctl
- 【头文件】:
#include <sys/types.h>
、#include <sys/ipc.h>
、#include <sys/sem.h>
- 【函数原型】:
int semctl(int semid, int semnum, int cmd, ...);
- 【功能】:获取或设置信号量集的有关信息
- 【参数】:
(1)semid:信号量集标识符
(2)semnum:信号量集中的哪一个信号量(编号从0开始)
(3)cmd:要执行的操作
取值 说明 GETVAL
获取指定信号量的值(通过函数返回) SETVAL
设置指定信号量的值(取值来自于 val) IPC_STAT
获取信号量集的 semid_ds 结构信息(保存在 buf 中) IPC_SET
设置信号量集的 semid_ds 结构信息(取值来自于 buf) GETALL
获取信号量集中所有信号量的值(保存在 array 中) SETALL
设置信号量集中所有信号量的值(取值来自于 array) IPC_INFO
获取系统范围内信号量的限制信息(保存在 __buf 中) IPC_RMID
立即删除信号量集 (4)… :这个参数取决于cmd,如果存在,则定义如下:
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) */ }
- 【返回值】:成功时具体的返回值依赖于cmd;失败则返回 -1,并将errno设置为错误标识符
【示例】:演示 IPC_STAT
和 IPC_INFO
的使用效果
【代码】:
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int main() {
int semid = semget(0x123, 5, IPC_CREAT|0644);
if (semid == -1) {
perror("semget");
return -1;
}
printf("semid: %d\n", semid);
struct semid_ds buf;
if (semctl(semid, 0, IPC_STAT, &buf) == -1) {
perror("semctl");
return -1;
}
printf("key: %#x\n", buf.sem_perm.__key);
printf("mode: %#o\n", buf.sem_perm.mode);
printf("nsems: %lu\n", buf.sem_nsems);
struct seminfo __buf;
if (semctl(semid, 0, IPC_INFO, &__buf) == -1) {
perror("semctl");
return -1;
}
printf("semmni: %d\n", __buf.semmni);
printf("semmsl: %d\n", __buf.semmsl);
printf("semmns: %d\n", __buf.semmns);
printf("semopm: %d\n", __buf.semopm);
return 0;
}
【执行结果】:
3. semop
- 【头文件】:
#include <sys/types.h>
、#include <sys/ipc.h>
、#include <sys/sem.h>
- 【函数原型】:
int semop(int semid, struct sembuf *sops, unsigned nsops);
- 【功能】:对信号量集中的信号量进行操作
- 【参数】:
(1)semid:信号量集标识符
(2)sops:指向 sembuf 结构体数组的指针,每个结构体描述一个要执行的操作
(3)nsops:sops 数组中操作的数量
- 【返回值】:成功时返回 0;失败则返回 -1,并将errno设置为错误标识符
五、信号量操作
1. P 操作
- 属于原子操作,源于荷兰语 Passeren
- 行为:
- 获取资源:减少信号量的值
- 等待:如果信号量值小于要减去的值,进程会阻塞
2. V 操作
- 属于原子操作,源于荷兰语 Vrijgeven
- 行为:
- 释放资源:增加信号量的值
- 发信号:如果有进程因该信号量被阻塞,会唤醒其中一个
3. 操作数据结构
struct sembuf {
unsigned short sem_num; /* semaphore index in array */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
};
sem_num
:操作信号量集中的哪一个信号量(编号从0开始)sem_op
:操作值- 负数:将信号量的值减少,通常用于获取资源,也就是P操作
- 0:等待信号量的值变为零
- 正数:将信号量的值增加,通常用于释放资源,也就是V操作
sem_flg
:操作标志- 0:阻塞模式(默认行为)
- IPC_NOWAIT:非阻塞模式(如果信号量操作不能立即执行,不阻塞进程,而是返回错误)
- SEM_UNDO:进程退出时撤销(内核会记录该进程对信号量的修改,当进程正常或异常退出时,自动撤销这些修改,确保进程崩溃不会导致死锁)
【示例代码】:
struct sembuf p_op = {0, -1, 0}; //P操作结构
struct sembuf v_op = {0, 1, 0}; //V操作结构
六、综合应用
示例一
【功能】:模拟多个进程竞争资源,利用信号量实现互斥
【代码】:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#define PATHNAME "."
#define PROJ_ID 123
#define CHILD_NUM 3
int main() {
//生成IPC键值
key_t key = ftok(PATHNAME, PROJ_ID);
if (key < 0) {
perror("ftok");
return -1;
}
//创建含有1个信号量的信号量集
int semid = semget(key, 1, IPC_CREAT|IPC_EXCL|0644);
if (semid < 0) {
perror("semget");
return -1;
}
//设置信号量值为1
if (semctl(semid, 0, SETVAL, 1) < 0) {
perror("semctl(SETVAL)");
return -1;
}
//定义信号量操作结构
struct sembuf p_op = {0, -1, SEM_UNDO}; //P操作结构
struct sembuf v_op = {0, 1, SEM_UNDO}; //V操作结构
//创建多个子进程竞争资源
int i = 0;
for (i = 0; i < CHILD_NUM; ++i) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return -1;
}
else if (pid == 0) {
char child = 'A' + i;
printf("进程(%c)等待获取资源中...\n", child);
if (semop(semid, &p_op, 1) < 0) {
perror("semop(P)");
exit(EXIT_FAILURE);
}
printf("进程(%c)获取到资源,执行任务中...\n", child);
sleep(3);
printf("进程(%c)任务完成,释放资源中...\n", child);
if (semop(semid, &v_op, 1) < 0) {
perror("semop(V)");
exit(EXIT_FAILURE);
}
printf("进程(%c)资源释放完成!\n", child);
exit(EXIT_SUCCESS);
}
}
//等待所有子进程退出
for (i = 0; i < CHILD_NUM; ++i) {
wait(NULL);
}
//删除信号量集
if (semctl(semid, 0, IPC_RMID) < 0) {
perror("semctl(IPC_RMID)");
return -1;
}
return 0;
}
【执行结果】:
示例二
【功能】:模拟多个进程按照顺序输出,利用信号量实现同步与互斥
【代码】:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#define PATHNAME "."
#define PROJ_ID 123
#define COUNT 3
#define PRINT_COUNT 3
int main() {
//生成IPC键值
key_t key = ftok(PATHNAME, PROJ_ID);
if (key < 0) {
perror("ftok");
return -1;
}
//创建含有COUNT个信号量的信号量集
int semid = semget(key, COUNT, IPC_CREAT|IPC_EXCL|0644);
if (semid < 0) {
perror("semget");
return -1;
}
//设置各个信号量值
int semval[COUNT] = {1, 0, 0};
if (semctl(semid, 0, SETALL, semval) < 0) {
perror("semctl(SETALL)");
return -1;
}
//定义每个进程的P操作和V操作对应的信号量编号(控制进程执行顺序)
unsigned short p_semnum[COUNT] = {0, 1, 2};
unsigned short v_semnum[COUNT] = {1, 2, 0};
//创建多个子进程按照顺序输出
int i = 0;
for (i = 0; i < COUNT; ++i) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return -1;
}
else if (pid == 0) {
char text = 'A' + i;
int count = 0;
for (count = 0; count < PRINT_COUNT; ++count) {
//P操作
struct sembuf p_op = {p_semnum[i], -1, SEM_UNDO};
if (semop(semid, &p_op, 1) < 0) {
perror("semop(P)");
exit(EXIT_FAILURE);
}
printf("%c", text);
fflush(stdout);
sleep(1);
printf("%c", text);
fflush(stdout);
sleep(1);
//V操作
struct sembuf v_op = {v_semnum[i], 1, SEM_UNDO};
if (semop(semid, &v_op, 1) < 0) {
perror("semop(V)");
exit(EXIT_FAILURE);
}
}
exit(EXIT_SUCCESS);
}
}
//等待所有子进程退出
for (i = 0; i < COUNT; ++i) {
wait(NULL);
}
printf("\n");
//删除信号量集
if (semctl(semid, 0, IPC_RMID) < 0) {
perror("semctl(IPC_RMID)");
return -1;
}
return 0;
}
【执行结果】:
【分析】:
从执行结果来看,在没有信号量的情况下,三个进程执行顺序不确定,而且进程在执行中还会被其他进程打断;在加入了信号量后,三个进程既能按照指定的顺序执行(实现了同步),也不会被打断(实现了互斥)。
再次理解同步与互斥
- 同步
- 定义:协调进程的执行顺序,确保它们按照预期的逻辑顺序执行
- 核心问题:解决进程间的依赖关系(如生产者-消费者问题)
- 互斥
- 定义:确保同一时间只有一个进程能访问共享资源(如变量、文件、内存等),避免竞争条件
- 核心问题:解决多进程对资源的写冲突(例如同时修改同一数据)