1.system v信号量
在使用 System V 信号量的任何函数前,必须包含以下头文件:
核心接口详解
System V 信号量主要由三个函数操作:semget(创建 / 获取)、semop(操作)、semctl(控制)。
semget - 创建或打开一个信号量集
用于创建一个新的信号量集,或获取一个已存在的信号量集的标识符。
函数原型:
cpp
int semget(key_t key, int nsems, int semflg);
参数说明:
key: 键值,通常由ftok()生成,用于唯一标识一个 IPC 对象。也可以使用IPC_PRIVATE创建私有对象。nsems: 该信号量集中包含的信号量数量。如果是创建新集合,必须指定此值;如果是获取已存在的集合,可以设为 0。semflg: 标志位。- 权限位 :例如
0666(八进制),表示所有用户都可读写。 IPC_CREAT:如果 key 对应的信号量集不存在,则创建它。IPC_EXCL:与IPC_CREAT一起使用,如果 key 已存在,则报错返回(而不是打开它)。
- 权限位 :例如
返回值:
- 成功:返回一个非负整数,即信号量集标识符(semid)。
- 失败:返回
-1,并设置errno。
semop - 执行信号量操作(P/V 操作)
这是最核心的函数,用于改变信号量的值(加、减或等待为 0)。
函数原型:
cpp
int semop(int semid, struct sembuf *sops, size_t nsops);
参数说明:
semid: 由semget返回的标识符。sops: 指向一个struct sembuf数组的指针。nsops: 数组中sembuf结构体的数量(通常为 1)。
**关键数据结构 struct sembuf:**你需要填充这个结构体来告诉内核要做什么操作:
cpp
struct sembuf {
unsigned short sem_num; /* 信号量在集合中的序号 (0, 1, ... nsems-1) */
short sem_op; /* 操作:正数(V),负数(P),0(等待归零) */
short sem_flg; /* 操作标志:0, IPC_NOWAIT, SEM_UNDO */
};
sem_num: 操作第几个信号量,单个信号量时填0。sem_op:-n(P 操作):申请资源。如果信号量值 >= n,则减去 n 并继续;否则阻塞,直到资源足够。+n(V 操作):释放资源。信号量值加上 n。0:等待,直到信号量值变为 0。
sem_flg:0:阻塞模式。IPC_NOWAIT:非阻塞,如果操作无法立即执行则报错返回。SEM_UNDO:强烈建议设置。进程退出时,内核会自动撤销该进程对信号量的操作(防止进程异常退出导致死锁)。
返回值:
- 成功:返回
0。 - 失败:返回
-1,并设置errno。
semctl - 控制信号量(初始化、删除等)
用于获取或设置信号量集的属性,最常用于初始化 信号量的值或删除信号量集。
函数原型
cpp
int semctl(int semid, int semnum, int cmd, ...);
注意:这是一个可变参数函数,第 4 个参数通常是一个联合体 union semun,你必须自己定义这个联合体!
自定义联合体:
cpp
// 必须在代码中手动定义这个联合体
union semun {
int val; /* 用于 SETVAL:设置单个信号量的值 */
struct semid_ds *buf; /* 用于 IPC_STAT / IPC_SET:获取/设置信号量集属性 */
unsigned short *array; /* 用于 GETALL / SETALL:获取/设置所有信号量的值 */
struct seminfo *__buf; /* 用于 IPC_INFO:获取系统范围内的信号量信息 (Linux特有) */
};
参数说明:
semid: 标识符。semnum: 要操作的信号量序号(0 开始)。cmd: 执行的命令。SETVAL:初始化信号量的值为arg.val。GETVAL:获取信号量的当前值(作为返回值)。IPC_RMID:立即删除信号量集,唤醒所有等待的进程。此时semnum参数被忽略。
返回值:
- 成功:根据
cmd不同返回不同值(通常0,GETVAL返回信号量值)。 - 失败:返回
-1,并设置errno。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/wait.h>
// 必须手动定义 semun 联合体
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
// P 操作 (Wait/Decrement):获取锁
void p(int semid) {
struct sembuf sb = {0, -1, SEM_UNDO}; // 对0号信号量减1,设置SEM_UNDO
if (semop(semid, &sb, 1) == -1) {
perror("P operation failed");
exit(1);
}
}
// V 操作 (Signal/Increment):释放锁
void v(int semid) {
struct sembuf sb = {0, 1, SEM_UNDO}; // 对0号信号量加1
if (semop(semid, &sb, 1) == -1) {
perror("V operation failed");
exit(1);
}
}
int main() {
key_t key;
int semid;
union semun su;
// 1. 生成 key (路径名+项目ID,必须唯一)
// 这里用当前目录 "." 和 整数 66
if ((key = ftok(".", 66)) == -1) {
perror("ftok");
exit(1);
}
// 2. 创建信号量集 (包含1个信号量,权限 0666)
// IPC_CREAT | IPC_EXCL 确保如果已存在则报错,防止冲突
if ((semid = semget(key, 1, 0666 | IPC_CREAT | IPC_EXCL)) == -1) {
perror("semget (create)");
// 如果是因为已存在报错,我们可以尝试直接获取并删除旧的,或者退出
// 这里为了演示简单,我们尝试获取它 (实际项目中需谨慎处理)
if ((semid = semget(key, 1, 0666)) == -1) {
perror("semget (get)");
exit(1);
}
} else {
// 3. 初始化信号量 (只有创建者需要做这一步)
// 我们将其初始化为 1,表示资源可用 (互斥锁模式)
su.val = 1;
if (semctl(semid, 0, SETVAL, su) == -1) {
perror("semctl init");
exit(1);
}
printf("Semaphore created and initialized to 1.\n");
}
// --- 临界区测试开始 ---
printf("Forking process...\n");
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
if (pid == 0) {
// --- 子进程 ---
printf("Child (PID:%d) trying to enter critical section...\n", getpid());
p(semid); // 加锁
// 临界区:模拟一些耗时操作,故意让它容易被打断
printf("Child (PID:%d) IN critical section.\n", getpid());
for(int i=0; i<3; i++) {
printf("Child working... %d\n", i);
sleep(1);
}
printf("Child (PID:%d) LEAVING critical section.\n", getpid());
v(semid); // 解锁
exit(0);
} else {
// --- 父进程 ---
printf("Parent (PID:%d) trying to enter critical section...\n", getpid());
p(semid); // 加锁
// 临界区
printf("Parent (PID:%d) IN critical section.\n", getpid());
for(int i=0; i<3; i++) {
printf("Parent working... %d\n", i);
sleep(1);
}
printf("Parent (PID:%d) LEAVING critical section.\n", getpid());
v(semid); // 解锁
// 等待子进程结束
wait(NULL);
// --- 清理工作 ---
printf("\nCleaning up semaphore...\n");
// IPC_RMID 命令忽略第二个参数 semnum
if (semctl(semid, 0, IPC_RMID) == -1) {
perror("semctl RMID");
}
printf("Done.\n");
}
return 0;
}

2.posix信号量
头文件
cpp
#include <semaphore.h>
所有 POSIX 信号量接口的返回值规则完全一致,无需单独记忆:
| 返回值 | 含义 | 错误处理 |
|---|---|---|
0 |
接口调用成功(比如信号量初始化成功、P/V 操作执行成功) | 无需处理 |
-1 |
接口调用失败(比如信号量已初始化、操作未初始化的信号量、权限不足等) | 全局变量errno会被设置错误码,可通过perror("接口名")打印具体错误原因 |
- 接口 1:sem_init(初始化无名信号量)
函数原型
cpp
int sem_init(sem_t *sem, int pshared, unsigned int value);
接口整体作用
创建并初始化一个无名信号量 (区别于有名信号量sem_open,新手先掌握无名),为后续的 P/V 操作做准备。
返回值
- 成功:
0 - 失败:
-1(如value为负数、sem指针无效、信号量已初始化等)
参数作用(逐个拆解)
| 参数名 | 类型 | 具体作用 | 新手注意点 |
|---|---|---|---|
sem |
sem_t * |
指向要初始化的信号量对象(sem_t是信号量句柄,无需关心内部结构) |
必须传入有效的sem_t变量地址(如&g_sem),不能传NULL |
pshared |
int |
信号量的作用域:✅ 0:仅用于线程间 同步(最常用,如示例)✅ 非 0:用于进程间同步(需配合共享内存) |
新手优先用0,进程间同步需额外处理共享内存,复杂度更高 |
value |
unsigned int |
信号量的初始值 (≥0):✅ 0:初始无资源,调用sem_wait会直接阻塞✅ 1:常用作互斥(临界区保护)✅ N:允许 N 个线程 / 进程同时访问资源 |
不能传负数,否则sem_init直接失败 |
- 接口 2:sem_wait(P 操作 / 等待信号量)
函数原型
cpp
int sem_wait(sem_t *sem);
接口整体作用
对信号量执行减 1 操作(P 操作),是信号量的核心 "等待 / 获取资源" 逻辑:
-
如果信号量当前值 > 0:值减 1,函数立即返回(获取资源成功);
-
如果信号量当前值 = 0:调用线程 / 进程阻塞 ,直到其他线程 / 进程调用
sem_post让信号量值 > 0。
返回值
-
成功:
0(信号量值已减 1) -
失败:
-1(如sem未初始化、被信号中断等)
参数作用
| 参数名 | 类型 | 具体作用 |
|---|---|---|
sem |
sem_t * |
指向已通过sem_init初始化的信号量对象 |
- 接口 3:sem_post(V 操作 / 释放信号量)
函数原型
cpp
int sem_post(sem_t *sem);
接口整体作用
对信号量执行加 1 操作(V 操作),是信号量的核心 "释放 / 唤醒" 逻辑:
-
信号量值加 1;
-
如果有线程 / 进程因调用
sem_wait阻塞在该信号量上,会唤醒其中一个 等待的线程 / 进程(使其完成sem_wait的减 1 操作)。
返回值
-
成功:
0(信号量值已加 1) -
失败:
-1(如sem未初始化、信号量值超出系统限制等)
参数作用
| 参数名 | 类型 | 具体作用 |
|---|---|---|
sem |
sem_t * |
指向已通过sem_init初始化的信号量对象 |
- 接口 4:sem_destroy(销毁信号量)
函数原型
cpp
int sem_destroy(sem_t *sem);
接口整体作用
释放信号量对象占用的系统资源(内存、内核结构体等),是信号量的 "收尾操作":
-
必须在信号量不再使用时调用;
-
调用后,该信号量不能再被
sem_wait/sem_post操作,除非重新sem_init。
返回值
-
成功:
0(资源释放成功) -
失败:
-1(如sem未初始化、还有线程阻塞在该信号量上)
参数作用
| 参数名 | 类型 | 具体作用 |
|---|---|---|
sem |
sem_t * |
指向已通过sem_init初始化的信号量对象 |
4.sem_trywait ------ 非阻塞等待信号量
作用 :非阻塞版本的 sem_wait,尝试对信号量执行 P 操作(减 1)。如果信号量值 > 0,就减 1 并立即返回;如果信号量值 = 0,不会阻塞,而是直接返回错误。
-
函数原型 :
cppint sem_trywait(sem_t *sem); -
参数 :
sem:指向要操作的信号量对象(sem_t类型指针)。
-
返回值 :
- 成功:返回
0,信号量值减 1。 - 失败:返回
-1,并设置errno:EAGAIN:信号量值为 0,无法执行 P 操作(非阻塞的核心体现)。EINVAL:sem指向的不是一个有效的已初始化信号量。EINTR:操作被信号中断。
- 成功:返回
sem_open------ 创建 / 打开有名信号量
作用 :创建或打开一个有名信号量(用于进程间同步)。有名信号量在文件系统中有对应的标识,不同进程可以通过相同的名称访问同一个信号量。
-
函数原型 :
cppsem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); -
参数 :
name:信号量的名称,必须以/开头(如/my_sem),这是 POSIX 有名信号量的命名规范。oflag:打开标志,常用值:O_CREAT:如果信号量不存在,则创建它;如果已存在,则打开它。O_EXCL:与O_CREAT一起使用,如果信号量已存在,则返回错误(确保创建新的信号量)。O_RDWR:以读写方式打开(默认)。
mode:权限位(如0644),仅在创建新信号量时有效,指定新信号量的访问权限。value:信号量的初始值(≥0),仅在创建新信号量时有效。
-
返回值 :
- 成功:返回指向信号量对象的指针(
sem_t *)。 - 失败:返回
SEM_FAILED(一个特殊的无效指针),并设置errno:EEXIST:O_CREAT | O_EXCL被指定,但信号量已存在。ENOENT:O_CREAT未被指定,且信号量不存在。EINVAL:name无效或value超过系统限制。
- 成功:返回指向信号量对象的指针(
sem_close------ 关闭有名信号量
作用:关闭当前进程对有名信号量的引用,释放相关资源。注意:这不会删除文件系统中的信号量标识,只是当前进程不再使用它。
-
函数原型 :
cppint sem_close(sem_t *sem); -
参数 :
sem:由sem_open返回的有效信号量指针。
-
返回值 :
- 成功:返回
0。 - 失败:返回
-1,并设置errno:EINVAL:sem不是一个有效的信号量指针。
- 成功:返回
sem_unlink------ 删除有名信号量
作用:删除文件系统中的有名信号量标识。当所有进程都关闭了对该信号量的引用后,信号量对象才会被真正销毁。
-
函数原型 :
cppint sem_unlink(const char *name); -
参数 :
name:要删除的有名信号量的名称(与sem_open中的name一致)。
-
返回值 :
- 成功:返回
0。 - 失败:返回
-1,并设置errno:ENOENT:指定名称的信号量不存在。EINVAL:name无效。
- 成功:返回
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <semaphore.h> // POSIX信号量核心头文件
#include <fcntl.h> // 包含O_CREAT、O_EXCL等标志
// POSIX信号量的P操作 (Wait/Decrement):获取锁
void p(sem_t *sem) {
// sem_wait:信号量值减1,若值为0则阻塞(核心P操作)
if (sem_wait(sem) == -1) {
perror("P operation (sem_wait) failed");
exit(1);
}
}
// POSIX信号量的V操作 (Signal/Increment):释放锁
void v(sem_t *sem) {
// sem_post:信号量值加1,唤醒阻塞的进程/线程(核心V操作)
if (sem_post(sem) == -1) {
perror("V operation (sem_post) failed");
exit(1);
}
}
int main() {
// POSIX有名信号量的名称(必须以/开头,是系统级唯一标识)
const char *sem_name = "/my_sem_66";
sem_t *sem = NULL;
// 1. 创建/打开POSIX有名信号量(进程间共享需用有名信号量)
// O_CREAT | O_EXCL:确保创建新信号量,若已存在则报错
// 0666:信号量文件权限,最后一个参数1是信号量初始值(互斥锁模式)
sem = sem_open(sem_name, O_CREAT | O_EXCL, 0666, 1);
if (sem == SEM_FAILED) {
perror("sem_open (create) failed");
// 如果是因为信号量已存在报错,先删除旧信号量再重新创建
if (sem_unlink(sem_name) == -1) {
perror("sem_unlink old semaphore failed");
exit(1);
}
// 重新尝试创建
sem = sem_open(sem_name, O_CREAT | O_EXCL, 0666, 1);
if (sem == SEM_FAILED) {
perror("sem_open (retry) failed");
exit(1);
}
}
printf("POSIX semaphore created and initialized to 1.\n");
// --- 临界区测试开始 ---
printf("Forking process...\n");
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
sem_close(sem); // 失败时关闭信号量
sem_unlink(sem_name); // 失败时删除信号量
exit(1);
}
if (pid == 0) {
// --- 子进程 ---
printf("Child (PID:%d) trying to enter critical section...\n", getpid());
p(sem); // 加锁(P操作:信号量值减1)
// 临界区:模拟耗时操作,确保互斥访问
printf("Child (PID:%d) IN critical section.\n", getpid());
for(int i=0; i<3; i++) {
printf("Child working... %d\n", i);
sleep(1);
}
printf("Child (PID:%d) LEAVING critical section.\n", getpid());
v(sem); // 解锁(V操作:信号量值加1)
sem_close(sem); // 子进程关闭信号量句柄
exit(0);
} else {
// --- 父进程 ---
printf("Parent (PID:%d) trying to enter critical section...\n", getpid());
p(sem); // 加锁(P操作:信号量值减1)
// 临界区
printf("Parent (PID:%d) IN critical section.\n", getpid());
for(int i=0; i<3; i++) {
printf("Parent working... %d\n", i);
sleep(1);
}
printf("Parent (PID:%d) LEAVING critical section.\n", getpid());
v(sem); // 解锁(V操作:信号量值加1)
// 等待子进程结束
wait(NULL);
// --- 清理工作 ---
printf("\nCleaning up POSIX semaphore...\n");
// 关闭信号量句柄(释放当前进程的引用)
if (sem_close(sem) == -1) {
perror("sem_close failed");
}
// 删除有名信号量(系统级清理,彻底移除信号量)
if (sem_unlink(sem_name) == -1) {
perror("sem_unlink failed");
}
printf("Done.\n");
}
return 0;
}
