深入理解进程间通信:信号量机制
信号量概述
信号量(Semaphore)是进程间通信(IPC)机制中的一种同步工具,其本质是一个整型计数器,用于控制多个并发进程对共享资源的访问。它通过维护一个可用的资源计数,实现对共享资源的互斥访问和进程间的同步协调。当信号量的值大于零时,表示有可用资源;当值为零时,则无可用资源。
在操作信号量时,通常涉及两种原子操作:
- P 操作(Wait / Decrement): 尝试获取资源。如果信号量值大于0,则将其减1并继续执行;否则,进程将被阻塞,直到有资源可用。
- V 操作(Signal / Increment): 释放资源。将信号量值加1。如果有进程因等待该信号量而被阻塞,则唤醒其中一个。
为了更有效地管理多个相关共享资源,操作系统引入了信号量集的概念。一个信号量集可以包含一个或多个信号量,允许应用程序对多个信号量进行一次性原子操作。
信号量集属性结构
每个信号量集在系统内部都有一个关联的控制结构,通常是 struct semid_ds,它包含了信号量集的元数据信息:
#include <sys/sem.h> // 注意:原始文章中的 <sysm.h> 应为 <sys/sem.h>
struct semid_ds
{
struct ipc_perm sem_perm; // IPC权限结构体
unsigned short sem_nsems; // 信号量集中信号量的数量
time_t sem_otime; // 最后一次 semop 操作的时间
time_t sem_ctime; // 最后一次 semctl 修改的时间
};
其中,sem_perm 成员是一个 struct ipc_perm 结构体,用于定义信号量集的访问权限。
创建或获取信号量集
使用 semget 函数可以创建一个新的信号量集,或者获取一个已存在的信号量集的标识符(ID):
#include <sys/sem.h>
int semget(key_t ipc_key, int num_semaphores, int access_flags);
// 返回值:成功返回信号量集ID,失败返回 -1
ipc_key:用于标识信号量集的键值。可以是应用程序指定的非零值(通过ftok生成),也可以是IPC_PRIVATE(0),表示创建一个只对当前进程及其子进程可见的私有信号量集。num_semaphores:如果正在创建新的信号量集,此参数指定集中信号量的数量。如果获取已存在的信号量集,此参数通常设为 0。access_flags:权限标志位,用于控制信号量集的访问权限。例如,IPC_CREAT用于创建信号量集,IPC_EXCL与IPC_CREAT结合使用可确保创建新的信号量集(如果已存在则报错),以及读写权限如0666。
控制信号量集
semctl 函数提供了对信号量集进行各种控制操作的能力,包括查询状态、设置属性或直接删除信号量集:
#include <sys/sem.h>
int semctl(int sem_id, int sem_index, int command, ... /* union semun arg */);
// 返回值:成功返回 0,失败返回 -1
semctl 函数通常与一个联合体 union semun 配合使用,这个联合体在不同的命令下用于传递不同类型的数据:
// 在某些系统上,union semun 需要在调用代码中定义
union semun {
int val; // 用于 SETVAL 命令
struct semid_ds *buf; // 用于 IPC_STAT, IPC_SET 命令
unsigned short *array; // 用于 GETALL, SETALL 命令
struct seminfo *__buf; // 用于 IPC_INFO 命令
};
sem_id:要操作的信号量集ID。sem_index:信号量集中的某个信号量的索引(从 0 开始)。如果命令作用于整个信号量集,此参数通常设为 0。command:指定要执行的操作,常见的命令包括:IPC_STAT:获取信号量集的semid_ds结构,并将其内容存储在arg.buf指向的结构中。IPC_SET:根据arg.buf指向的结构中的值来设置信号量集的sem_perm字段。IPC_RMID:从系统中删除指定的信号量集及其所有关联数据。GETVAL:获取指定信号量(由sem_index标识)的当前值。SETVAL:设置指定信号量(由sem_index标识)的值为arg.val。GETALL:获取信号量集中所有信号量的值,并存储到arg.array指向的数组中。SETALL:设置信号量集中所有信号量的值,从arg.array指向的数组中读取。
... /* union semun arg */:这是一个可选参数,根据command的不同,它被解释为union semun类型的变量,用于传递或接收数据。
信号量集操作
semop 函数用于对信号量集中的一个或多个信号量执行 P 或 V 操作,这些操作是原子性的:
#include <sys/sem.h>
int semop(int sem_id, struct sembuf *operation_list, size_t num_operations);
// 返回值:成功返回 0,失败返回 -1
semop 函数通过一个 struct sembuf 数组来定义要执行的单个或批量操作:
struct sembuf
{
unsigned short sem_num; // 信号量集中目标信号量的编号(从 0 开始)
short sem_op; // 对信号量执行的操作值
short sem_flg; // 操作标志
};
sem_id:要操作的信号量集ID。operation_list:指向struct sembuf结构体数组的指针,每个结构体描述一个对信号量的操作。num_operations:operation_list数组中包含的操作数量。sem_num:数组中每个结构体用于指定操作的目标信号量在信号量集中的索引。sem_op:定义具体操作:- 正数:执行 V 操作,将信号量值增加
sem_op。 - 负数:执行 P 操作,将信号量值减少
-sem_op。如果当前值小于-sem_op,进程将被阻塞。 - 零:测试信号量值是否为 0。如果为 0,进程继续;否则,进程将被阻塞直到信号量变为 0。
- 正数:执行 V 操作,将信号量值增加
sem_flg:操作行为标志,可选值为:IPC_NOWAIT:如果操作会导致阻塞,则立即返回EAGAIN错误,而不是阻塞进程。SEM_UNDO:如果设置了此标志,当进程异常终止时,系统会自动撤销该进程对信号量所做的所有修改,恢复信号量的原始状态,防止资源死锁。
semop 提供的原子性操作和 SEM_UNDO 标志,使得信号量成为实现进程间互斥和同步的强大且可靠的机制。