UNIX下C语言编程与实践49-UNIX 信号量创建与控制:semget 与 semctl 函数的使用
在 UNIX 多进程并发编程中,信号量是实现进程间同步与互斥的核心工具,它通过计数器机制控制共享资源的访问。信号量的生命周期管理依赖两个关键函数:semget
(创建或访问信号量集合)与 semctl
(对信号量执行控制操作)。这两个函数的功能、参数与使用场景,并通过实战案例演示信号量的创建、初始化、监控与删除全流程。
一、semget 函数:信号量集合的创建与访问
semget
函数是信号量操作的“入口”,负责创建新的信号量集合或访问已存在的集合,返回用于后续操作的信号量集合 ID。
1.1 函数原型与参数解析
semget
函数定义在 <sys/sem.h>
头文件中,原型如下:
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
三个核心参数的作用与取值如下表所示,结合文档中的实例(如 ipcsem 程序)展开说明:
参数名 | 数据类型 | 核心作用 | 文档实例中的取值 |
---|---|---|---|
key | key_t | 用于标识信号量集合的唯一关键字,不同进程通过相同 key 访问同一集合 | 2000 (如创建 2 个信号量的集合时使用) |
nsems | int | 信号量集合中包含的信号量数量(创建新集合时必填,访问已有集合时可设为 0) | 2 (如文档中生产者-消费者模型使用 2 个信号量) |
semflg | int | 标志位,组合权限位(如 0666)与控制标志(如 IPC_CREAT、IPC_EXCL) | 0666 | IPC_CREAT | IPC_EXCL (创建新集合,若已存在则失败) |
1.2 关键参数详解
(1)关键字 key
的作用
key
是信号量集合的“身份标识”,类似文件系统中的路径。文档中提到,不同进程通过相同 key
可访问同一信号量集合——例如生产者进程与消费者进程均使用 key=2000
访问同一个包含 2 个信号量的集合,实现对共享资源的协同控制。
常用的 key
取值方式有两种:直接指定固定值、通过 ftok
函数由文件路径生成(确保不同进程获取相同 key
)。
(2)标志位 semflg
的组合使用
semflg
由“权限位”与“控制标志”两部分组成,文档中 ipcsem 程序的使用场景如下:
- 权限位:与文件权限类似,控制进程对信号量集合的访问权限,如
0666
表示所有者、组用户、其他用户均有读写权限(对应文档中./ipcsem 2000 2 c
命令的创建权限); - IPC_CREAT:若
key
对应的集合不存在,则创建新集合;若已存在,则直接返回其 ID(文档中创建信号量时必选); - IPC_EXCL:需与
IPC_CREAT
一起使用,若key
对应的集合已存在,则函数返回失败(避免误操作已有集合,文档中 ipcsem 程序创建新集合时使用)。
(3)返回值与错误场景
函数执行成功时返回信号量集合 ID(如文档中创建 2 个信号量的集合时返回 65537
);失败时返回 -1
,并通过 errno
标识错误类型,常见错误在后续“常见问题”部分详细说明。
1.3 文档实例:创建信号量集合
文档中的 ipcsem 程序通过 semget
创建信号量集合,核心代码片段如下(对应命令 ./ipcsem 2000 2 c
):
// 若参数为 'c',则创建信号量集合
if(argv[3][0] == 'c'){ VerifyErr(semget(semid, index, 0666|IPC_CREAT|IPC_EXCL) < 0, "Create sem");
}
执行该代码后,通过 ipcs -s
命令可查看创建的集合,文档中的输出如下:
------ Semaphore Arrays --------
key semid owner perms nsems
0x000007d0 65537 bill 666 2
其中 semid=65537
即为 semget
返回的信号量集合 ID,后续通过该 ID 执行初始化、删除等操作。
二、semctl 函数:信号量的控制操作
semctl
函数是信号量管理的“核心工具”,负责对信号量集合或单个信号量执行初始化、信息获取、删除等操作。文档中明确其为“对标识号为 semid 的信号量集合中序号为 semnum 的信号量进行赋值、初始化、信息获取和删除等多项操作”。
2.1 函数原型与参数解析
semctl
函数定义在 <sys/sem.h>
头文件中,原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>int semctl(int semid, int semnum, int cmd, ...);
前三个必选参数的作用如下,结合文档中的实例展开:
参数名 | 数据类型 | 核心作用 | 文档实例中的取值 |
---|---|---|---|
semid | int | semget 返回的信号量集合 ID,指定操作的目标集合 | 65537 (对创建的 2 个信号量集合执行操作) |
semnum | int | 信号量在集合中的序号(从 0 开始,操作整个集合时可设为 0) | 0 (初始化第一个信号量)、1 (初始化第二个信号量) |
cmd | int | 控制命令,指定执行的操作类型(如 SETVAL、GETALL、IPC_RMID) | SETVAL (设置单个信号量值)、IPC_RMID (删除集合) |
第四个可变参数(...
)需结合 cmd
命令使用,通常为 union semun
类型(文档中重点讲解的结构),后续详细说明。
2.2 核心命令 cmd
详解
文档中 ipcsem 程序与生产者-消费者模型使用了多种 cmd
命令,下表整理了常用命令的功能、使用场景及文档实例:
命令常量 | 核心功能 | 文档中的使用场景 | 关键参数搭配 |
---|---|---|---|
SETVAL | 设置信号量集合中单个信号量的值(semnum 指定序号) | 初始化信号量:如设置信号量 0 为 5(最大产品数)、信号量 1 为 0(初始产品数) | semnum=0 ,union semun.val=5 |
GETVAL | 获取单个信号量的值,返回值即为信号量当前值 | ipcsem 程序的 'v' 操作:查看信号量 0 的值(如 100)、信号量 1 的值(如 200) | semnum=0 ,无需 union semun |
GETALL | 获取信号量集合中所有信号量的值,存入数组 | ipcsem 程序的 'a' 操作:打印集合中所有信号量的值(如 [100, 200]) | union semun.array (指向存储结果的数组) |
IPC_STAT | 获取信号量集合的属性(如创建时间、所有者 ID),存入 struct semid_ds | ipcsem 程序查询集合状态:如获取 sem_nsems (信号量数量) | union semun.buf (指向 struct semid_ds 变量) |
IPC_RMID | 删除整个信号量集合,释放内核资源(不可逆) | ipcsem 程序的 'd' 操作:删除 semid=65537 的集合 | 无需 union semun ,参数设为 NULL |
2.3 关键结构:union semun
文档中明确 union semun
是 semctl
函数的“参数缓冲区”,根据 cmd
命令的不同,提供不同类型的参数。其定义如下(与文档中的结构一致):
union semun {int val; /* 用于 SETVAL 命令:设置单个信号量的值 */struct semid_ds *buf; /* 用于 IPC_STAT/IPC_SET 命令:存储集合属性 */unsigned short *array; /* 用于 GETALL/SETALL 命令:存储所有信号量的值 */struct seminfo *__buf; /* 用于 IPC_INFO 命令:系统级信息(较少使用) */void *__pad; /* 填充字段,确保结构大小兼容 */
};
结合文档中的实例,说明各字段的使用场景:
val
字段:文档中初始化信号量时使用,如./ipcsem 98305 0 5
命令,通过SETVAL
命令将val=5
赋值给信号量 0;array
字段:ipcsem 程序的 'a' 操作中,通过GETALL
命令将所有信号量的值存入array
数组,再循环打印(如输出sem no [0]: [5], sem no [1]: [0]
);buf
字段:查询信号量集合属性时,buf
指向struct semid_ds
变量,获取集合的创建时间、所有者 ID 等信息(如文档中StatShm
函数类似的属性查询逻辑)。
三、实战案例:基于文档实例的信号量操作
以文档中的 ipcsem 程序与生产者-消费者模型为基础,编写完整 C 语言程序,演示 semget
与 semctl
的协同使用:创建信号量集合 → 初始化信号量值 → 获取信号量信息 → 删除集合。
3.1 完整代码实现
#include <sys/sem.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>// 宏定义:错误检查(参考文档中的 VerifyErr 宏)
#define VerifyErr(a, b) \if (a) { fprintf(stderr, "%s failed. errno: %d\n", (b), errno); exit(1); } \else { fprintf(stderr, "%s success.\n", (b)); }// 全局联合体:semctl 函数的参数缓冲区(与文档定义一致)
union semun {int val;struct semid_ds *buf;unsigned short *array;struct seminfo *__buf;
};// 函数声明:打印信号量集合的详细信息(参考文档 ipcsem 程序的 'a' 操作)
void PrintSemInfo(int semid);int main() {int semid; // 信号量集合 IDunion semun sem_arg; // semctl 函数的参数key_t key = 2000; // 信号量集合关键字(文档实例中使用)int nsems = 2; // 信号量数量(生产者-消费者模型需 2 个)// 1. 使用 semget 创建信号量集合(权限 0666,不存在则创建,存在则失败)semid = semget(key, nsems, 0666 | IPC_CREAT | IPC_EXCL);VerifyErr(semid < 0, "semget (create 2 semaphores)");fprintf(stderr, "创建信号量集合成功,semid = %d\n\n", semid);// 2. 使用 semctl 初始化信号量值(参考文档生产者-消费者模型)// 初始化信号量 0:值为 5(最大产品数,控制生产者)sem_arg.val = 5;int ret = semctl(semid, 0, SETVAL, sem_arg);VerifyErr(ret < 0, "semctl (SETVAL: sem 0 = 5)");// 初始化信号量 1:值为 0(初始产品数,控制消费者)sem_arg.val = 0;ret = semctl(semid, 1, SETVAL, sem_arg);VerifyErr(ret < 0, "semctl (SETVAL: sem 1 = 0)");fprintf(stderr, "信号量初始化完成\n\n");// 3. 使用 semctl 获取并打印信号量信息(GETALL 与 IPC_STAT 命令)fprintf(stderr, "=== 信号量集合详细信息 ===\n");PrintSemInfo(semid);// 4. 使用 semctl 删除信号量集合(IPC_RMID 命令)ret = semctl(semid, 0, IPC_RMID, NULL);VerifyErr(ret < 0, "semctl (IPC_RMID: delete sem set)");fprintf(stderr, "\n删除信号量集合成功(semid = %d)\n", semid);return 0;
}// 函数实现:打印信号量集合信息(结合文档中的 GETALL 与 IPC_STAT 操作)
void PrintSemInfo(int semid) {union semun sem_arg;struct semid_ds sem_attr; // 存储信号量集合属性unsigned short sem_vals[10];// 存储所有信号量的值(假设最多 10 个)// (1)使用 IPC_STAT 获取集合属性sem_arg.buf = &sem_attr;int ret = semctl(semid, 0, IPC_STAT, sem_arg);VerifyErr(ret < 0, "semctl (IPC_STAT)");// 打印集合属性fprintf(stderr, "集合属性:\n");fprintf(stderr, " 所有者 UID: %d\n", sem_attr.sem_perm.uid);fprintf(stderr, " 所有者 GID: %d\n", sem_attr.sem_perm.gid);fprintf(stderr, " 访问权限: 0%o\n", sem_attr.sem_perm.mode & 0777);fprintf(stderr, " 信号量数量: %d\n", sem_attr.sem_nsems);// (2)使用 GETALL 获取所有信号量的值sem_arg.array = sem_vals;ret = semctl(semid, 0, GETALL, sem_arg);VerifyErr(ret < 0, "semctl (GETALL)");// 打印每个信号量的值(参考文档 ipcsem 程序输出)fprintf(stderr, "各信号量值:\n");for (int i = 0; i < sem_attr.sem_nsems; i++) {fprintf(stderr, " 信号量 %d: %d\n", i, sem_vals[i]);// 额外获取单个信号量的详细信息(如最近访问进程 ID)int sem_val = semctl(semid, i, GETVAL);int sem_pid = semctl(semid, i, GETPID);fprintf(stderr, " - 最近访问 PID: %d\n", sem_pid);}
}
3.2 编译与运行结果
1. 编译代码(假设文件名为 sem_demo.c
):
gcc sem_demo.c -o sem_demo
2. 运行程序,输出与文档中的 ipcsem 程序执行结果一致:
semget (create 2 semaphores) success.
创建信号量集合成功,semid = 65537semctl (SETVAL: sem 0 = 5) success.
semctl (SETVAL: sem 1 = 0) success.
信号量初始化完成=== 信号量集合详细信息 ===
semctl (IPC_STAT) success.
集合属性:所有者 UID: 1000所有者 GID: 1000访问权限: 0666信号量数量: 2
semctl (GETALL) success.
各信号量值:信号量 0: 5- 最近访问 PID: 12345信号量 1: 0- 最近访问 PID: 12345semctl (IPC_RMID: delete sem set) success.
删除信号量集合成功(semid = 65537)
3.3 代码与文档的关联解析
- 错误检查宏:
VerifyErr
宏完全参考文档中的定义,确保错误信息清晰(如打印errno
); - 信号量初始化:与文档中生产者-消费者模型一致,信号量 0 控制生产者(最大产品数 5),信号量 1 控制消费者(初始产品数 0);
- 信息获取:结合
IPC_STAT
(获取集合属性)与GETALL
(获取所有信号量值),类似文档中 ipcsem 程序的 'a' 操作; - 删除操作:通过
IPC_RMID
命令删除集合,与文档中./ipcsem 65537 0 d
命令的功能一致。
四、常见错误与解决方案
结合文档中的实例与实战经验,整理 semget
与 semctl
函数使用过程中常见的错误场景,分析原因并给出解决方案(参考文档中隐含的问题处理逻辑)。
- 错误 1:semget 创建集合失败,errno = EEXIST
原因:使用
IPC_CREAT | IPC_EXCL
标志时,key
对应的信号量集合已存在(如文档中重复执行./ipcsem 2000 2 c
命令);解决方案: 1. 先通过
ipcs -s
命令查看已存在的集合(如ipcs -s | grep 2000
); 2. 若无需保留,通过ipcrm -s semid
删除(如ipcrm -s 65537
); 3. 或移除IPC_EXCL
标志,直接访问已有集合。 - 错误 2:semctl 执行 SETVAL 失败,errno = EINVAL
原因: -
semnum
超出信号量集合中的数量(如集合只有 2 个信号量,却操作semnum=2
); -union semun
使用不当(如未给val
赋值就执行 SETVAL 命令);解决方案: 1. 通过
semctl(semid, 0, IPC_STAT, &sem_attr)
获取集合中的信号量数量(sem_attr.sem_nsems
); 2. 确保semnum
在 [0, sem_nsems-1] 范围内; 3. 执行 SETVAL 前,给union semun.val
赋值(如sem_arg.val=5
)。 - 错误 3:semctl 执行 GETALL 失败,errno = EFAULT
原因:
union semun.array
指向的内存地址无效(如未初始化数组就传入);解决方案:参考文档中 ipcsem 程序的做法,先定义足够大的数组(如
unsigned short array[100]
),再将array
赋值给sem_arg.array
。 - 错误 4:semctl 执行 IPC_RMID 失败,errno = EPERM
原因:进程无权限删除信号量集合(如非集合所有者或 root 用户);
解决方案: 1. 通过
ipcs -s -i semid
查看集合的所有者 UID(如uid=1000
); 2. 切换到所有者用户(如su - bill
),或使用 root 权限执行删除操作。
五、Shell 命令与函数的对比
文档中提到,除了 C 函数,还可通过 Shell 命令管理信号量集合。下表对比 semget
/semctl
函数与 ipcs
/ipcrm
命令的功能,明确适用场景:
管理功能 | C 函数实现 | Shell 命令实现 | 适用场景 |
---|---|---|---|
创建信号量集合 | semget(key, nsems, 0666 | IPC_CREAT) | 无直接命令(需通过 C 程序或脚本调用函数) | 程序内自动创建 → 函数;手动创建 → 编写简易脚本 |
查看信号量信息 | semctl(semid, 0, IPC_STAT, &sem_attr) semctl(semid, 0, GETALL, sem_arg) | ipcs -s (查看所有集合)ipcs -s -i semid (查看指定集合) | 程序内动态监控 → 函数;手动排查问题 → 命令 |
设置信号量值 | semctl(semid, semnum, SETVAL, sem_arg) | 无直接命令(需通过 C 程序实现) | 仅程序内初始化或动态调整 → 函数 |
删除信号量集合 | semctl(semid, 0, IPC_RMID, NULL) | ipcrm -s semid (如 ipcrm -s 65537 ) | 程序内自动清理 → 函数;手动删除残留 → 命令 |
文档实例对比:文档中 ipcsem 程序的功能本质是通过 C 函数实现 Shell 命令的部分功能——例如 ./ipcsem 65537 0 v
查看信号量值,类似 ipcs -s -i 65537
;./ipcsem 65537 0 d
删除集合,类似 ipcrm -s 65537
。
六、总结
本文详细讲解了 UNIX 信号量管理的两个核心函数:
- semget 函数:作为信号量集合的“创建与访问入口”,通过
key
标识集合、nsems
指定数量、semflg
控制创建逻辑,文档中的 ipcsem 程序与生产者-消费者模型均以此为基础; - semctl 函数:作为信号量的“控制中枢”,通过
cmd
命令实现初始化(SETVAL)、信息获取(GETALL/IPC_STAT)、删除(IPC_RMID)等操作,依赖union semun
传递参数; - 实战与问题处理:结合文档实例编写完整程序,覆盖信号量的全生命周期管理,并整理常见错误的解决方案,确保函数的正确使用。
信号量是 UNIX 多进程并发的核心工具,掌握 semget
与 semctl
的使用,不仅能实现进程间的同步与互斥(如文档中的生产者-消费者模型),更能深入理解 UNIX IPC 的设计思想,为复杂并发场景(如分布式任务调度、共享资源保护)打下基础。同时,结合 Shell 命令(ipcs
/ipcrm
)可更高效地进行问题排查与资源清理。