文章目录
- 信号量
-
- [P/V 操作](#P/V 操作)
- 使用场景
- [System V 信号量](#System V 信号量)
- [POSIX 信号量](#POSIX 信号量)
- 信号量解决共享内存同步问题
- 线程同步工具在进程间应用
-
- 基本原理
- 互斥锁(Mutex)进程间使用
- [条件变量(Condition Variable)进程间使用](#条件变量(Condition Variable)进程间使用)
- [读写锁(RW Lock)进程间使用](#读写锁(RW Lock)进程间使用)
- 进程通信与同步工具总结
信号量
- 一个由内核维护的整数,其值被限定为大于或等于0,任何试图将信号量值降低到0之下的操作都会被阻塞
P/V 操作
- P操作 (Proberen):测试/等待,减少信号量值,阻塞
- V操作 (Verhogen):增加,释放资源,增加信号量值
- 操作是原子的,保证线程安全
- 内核会将所有试图将信号量值降低到0之下的操作阻塞
使用场景
- 保护临界区(互斥)
- 限制并发访问数量
- 实现生产者-消费者模型
- 同步多个进程/线程

System V 信号量
核心函数
创建/获取信号量集
c
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
- key:用于标识系统级唯一信号量集的键值(本质是 int 类型),不同进程通过相同 key 可访问同一个信号量集(IPC_PRIVATE 或 ftok() 生成)
- nsems:指定信号量集中包含的信号量个数(非负整数)
- semflg:操作标志,可通过按位或组合多个标志,分为「控制标志」和「权限标志」两类
- IPC_CREAT:若 key 对应的信号量集不存在则创建,已存在则直接获取
- IPC_EXCL:与 IPC_CREAT 配合使用,若信号量集已存在则返回错误,保证创建的是全新资源
- 权限标志(占低 9 位,与文件权限格式一致):例如 0644(所有者读写、同组读、其他读)、0666(所有用户读写),控制不同进程对信号量集的访问权限
- 返回值:
- 成功:返回一个有效的 信号量集 ID(semid,非负整数),后续 semctl()、semop() 操作均依赖该 ID
- 失败:返回 -1,同时设置全局变量 errno 指示错误原因(例如 EEXIST:IPC_CREAT|IPC_EXCL 时信号量集已存在;ENOENT:无对应 key 的集且未指定 IPC_CREAT;ENOMEM:系统无足够资源创建新集)
信号量原子操作
c
struct sembuf {
unsigned short sem_num; // 信号量编号
short sem_op; // 操作值(-1:P操作,+1:V操作)
short sem_flg; // 标志(SEM_UNDO, IPC_NOWAIT)
};
int semop(int semid, struct sembuf *sops, size_t nsops);
- semid:由 semget() 成功返回的信号量集 ID,标识要操作的目标信号量集
- sops:指向 struct sembuf 结构体数组的指针,每个结构体描述一个单独的信号量操作
- sem_num:信号量在集中的编号(从 0 开始,例如 0 表示第一个信号量)
- sem_op:
- 正数:将 sem_op 的值加到信号量的当前值上(释放资源,例如 sem_op=1 对应 V 操作)
- 0:等待信号量的当前值变为 0(直到满足条件,或被信号中断)
- 负数:尝试将信号量当前值减去 sem_op 的绝对值(获取资源,例如 sem_op=-1 对应 P 操作),若信号量当前值小于该绝对值,进程会阻塞(直到信号量值满足条件)
- sem_flg:
- 0(默认):阻塞模式,操作无法立即完成时进程挂起等待
- IPC_NOWAIT:非阻塞模式,操作无法立即完成时直接返回错误(errno=EAGAIN)
- SEM_UNDO:进程退出时,内核自动撤销该进程对信号量的所有操作(避免进程异常退出导致信号量处于无效状态,防止死锁
- nsops:指定 sops 数组中操作结构体的个数(即要执行的原子操作数量,必须 ≥ 1),semop() 对 sops 数组中的所有操作执行「原子操作」------ 要么所有操作全部执行完成,要么一个都不执行,不会出现部分执行的中间状态,这是实现进程同步 / 互斥的关键
- 返回值:
- 返回 0,表示 sops 数组中的所有信号量操作已原子性执行完成
- 失败:返回 -1,同时设置全局变量 errno 指示错误原因(例如 EINVAL:无效的 semid 或 sem_num;EAGAIN:IPC_NOWAIT 模式下操作无法立即完成;EIDRM:信号量集已被删除;EINTR:阻塞等待时被信号中断)
一个 semop() 调用可同时操作多个信号量(通过 nsops 指定),原子性保证了复杂同步场景的安全性
控制操作
- 信号量集控制操作,含初始化、删除、查询
c
union semun {
int val; // SETVAL 时使用
struct semid_ds *buf; // IPC_STAT/IPC_SET 时使用
unsigned short *array; // GETALL/SETALL 时使用
};
int semctl(int semid, int semnum, int cmd, ...);
- semid:由 semget() 成功返回的信号量集 ID,标识要进行控制操作的目标信号量集
- semnum:指定信号量集中要操作的单个信号量编号(从 0 开始,对应 semget() 创建时的信号量顺序)
- cmd:要执行的控制命令(系统定义宏),分为「单个信号量命令」和「整个信号量集命令」两类:
- 单个信号量命令(常用):
- SETVAL:设置单个信号量的初始值(或当前值),需配合第 4 个可变参数(union semun 类型)传递要设置的值
- GETVAL:获取单个信号量的当前值,返回值即为该信号量的值(无需第 4 个参数)
- 整个信号量集命令(常用):
- IPC_RMID:删除整个信号量集(最核心的删除命令),此时无需第 4 个参数(可设为 NULL)
- IPC_STAT:查询信号量集的属性信息,将属性写入第 4 个参数(union semun 中的 buf 成员
- IPC_SET:修改信号量集的属性(仅能修改权限相关字段),需通过第 4 个参数传递新属性
- 其他少见命令:GETPID(获取最后操作该信号量的进程 ID)、GETNCNT(获取等待信号量值增加的进程数)、SEMA(设置整个信号量集的所有值)
- 单个信号量命令(常用):
示例:进程间互斥
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define SEM_KEY 1234 // 信号量的关键字
// 信号量操作结构体
struct sembuf sb;
// 创建一个信号量
int create_semaphore() {
int semid = semget(SEM_KEY, 1, IPC_CREAT | 0666); // 创建信号量集
if (semid == -1) {
perror("semget");
exit(1);
}
// 初始化信号量的值为 1(表示资源可用)
union semun {
int val;
} arg;
arg.val = 1;
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl");
exit(1);
}
return semid;
}
// P 操作(等待信号量,减小信号量的值)
void P(int semid) {
sb.sem_num = 0;
sb.sem_op = -1; // 对信号量做减操作
sb.sem_flg = SEM_UNDO;
if (semop(semid, &sb, 1) == -1) {
perror("semop - P");
exit(1);
}
}
// V 操作(释放信号量,增加信号量的值)
void V(int semid) {
sb.sem_num = 0;
sb.sem_op = 1; // 对信号量做加操作
sb.sem_flg = SEM_UNDO;
if (semop(semid, &sb, 1) == -1) {
perror("semop - V");
exit(1);
}
}
// 进程对共享资源的操作
void critical_section(int semid) {
P(semid); // 等待信号量,进入临界区
printf("Process %d is in critical section.\n", getpid());
printf("Process %d is leaving critical section.\n", getpid());
V(semid); // 释放信号量,离开临界区
}
int main() {
int semid = create_semaphore(); // 创建信号量
if (fork() == 0) {
// 子进程
critical_section(semid);
exit(0);
} else {
// 父进程
critical_section(semid);
wait(NULL); // 等待子进程完成
}
// 删除信号量
if (semctl(semid, 0, IPC_RMID, 0) == -1) {
perror("semctl - IPC_RMID");
exit(1);
}
return 0;
}
POSIX 信号量
命名信号量
- 命名信号量通过文件系统路径标识,可用于无关进程间同步
- 命名信号量由以下形式的名称标识
/somename - 具有内核持久性,虚拟文件系统/dev/shm
核心函数
创建/打开
c
#include <semaphore.h>
#include <fcntl.h>
// 仅打开已存在的 POSIX 命名信号量
sem_t *sem_open(const char *name, int oflag);
// 打开或创建新的 POSIX 命名信号量,带权限和初始值配置
sem_t *sem_open(const char *name, int oflag,
mode_t mode, unsigned int value);
- name:POSIX 命名信号量的系统级唯一标识名,命名规则严格且固定,必须以 / 开头,且后续字符串中不能再包含其他
- oflag: 打开 / 创建标志, 可通过 按位或组合多个标志,核心分为「访问标志」和「创建辅助标志」两类
- 核心基础标志(无额外扩展,仅用于打开已存在信号量)传入 0 即可(表示以默认模式打开已存在的信号量)
- 可选创建 / 辅助标志(核心):
- O_CREAT:若 name 对应的信号量不存在,则创建新的信号量;若已存在,则直接打开该信号量(此时忽略 mode 和 value 参数)
- O_EXCL:必须与 O_CREAT 配合使用,若 name对应的信号量已存在,则返回错误(避免重复创建,保证创建的是全新的信号量,防止覆盖已有资源)
注意:若使用O_CREAT标志,必须使用第二个重载原型,传入mode和value参数
- mode:仅当 oflag 包含 O_CREAT 时有效,用于指定新创建信号量的访问权限,取值格式与 open()、shm_open()、mq_open() 等函数的权限一致
- 0644:所有者读写、同组用户读、其他用户读
- 0666:所有用户读写(最常用,适用于进程间共享访问)
- 最终实际权限会被进程的 umask 掩码修正(实际权限 = mode & ~umask)
- value:仅当 oflag 包含 O_CREAT 且信号量不存在(需要创建新信号量)时有效,用于指定新信号量的初始值。表示当前可用的「资源数量」,用于同步 / 互斥场景(例如初始值为 1 用于实现互斥锁,初始值为 n 用于实现 n 个共享资源的分配)
信号量创建后,初始值仅通过 value 设定一次,后续需通过 sem_post()(增加值)、sem_wait()(减少值)修改,无法再通过 sem_open() 重置
操作函数
c
// 等待(P操作)
int sem_wait(sem_t *sem); // 阻塞
int sem_trywait(sem_t *sem); // 非阻塞
// 带绝对超时的信号量等待操作
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
// 释放(V操作)
int sem_post(sem_t *sem);
// 获取当前值
int sem_getvalue(sem_t *sem, int *sval);
// 关闭和删除
int sem_close(sem_t *sem);
int sem_unlink(const char *name);
- abs_timeout:指向 struct timespec 结构体的指针,指定绝对超时时间点
- sval:指向 int 类型变量的有效指针,用于存储查询到的信号量当前值
- name:与 sem_open() 中一致的POSIX 命名信号量唯一标识名,必须符合合法命名规则
命名信号量示例:
c
// 进程1:创建并初始化
sem_t *sem = sem_open("/mysem", O_CREAT, 0666, 1);
sem_wait(sem);
// 临界区操作
sem_post(sem);
sem_close(sem);
// 进程2:打开并使用
sem_t *sem = sem_open("/mysem", 0);
sem_wait(sem);
// 临界区操作
sem_post(sem);
sem_close(sem);
sem_unlink("/mysem"); // 最后一个进程删除
匿名信号量
- 匿名信号量存在于内存中,用于相关进程或线程间同步
核心函数
初始化与销毁
c
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
- sem:指向 sem_t 类型信号量对象的有效指针,用于存储初始化后的信号量数据,该指针指向的内存空间必须提前分配(可声明为局部变量、全局变量或通过 malloc() 动态分配),禁止传入 NULL
- pshared:指定信号量的共享范围,用于区分「线程间共享」和「进程间共享」,仅支持两个核心取值(非 0 或 0)
- 0(默认且最常用):信号量为线程内共享(私有),仅适用于同一进程内的多个线程间同步 / 互斥;此时 sem 通常指向进程内的全局变量或线程间可访问的共享内存(如堆内存)
- 非 0(通常为 1):信号量为进程间共享,适用于不同进程间的同步 / 互斥;此时 sem 必须指向「共享内存区域」,否则不同进程无法访问到同一个 sem_t 对象
- pshared:0表示线程间,非0表示进程间
- value:指定无名信号量的初始值,表示当前可用的资源数量,常用取值:1(用于实现互斥锁,保证同一时间只有一个线程 / 进程访问临界资源)、n(用于实现 n 个共享资源的分配),不能超过系统定义的 SEM_VALUE_MAX(通常为 32767,可在 <semaphore.h> 中查看宏定义
匿名信号示例:
c
#include <sys/mman.h>
#include <semaphore.h>
struct shared_data {
sem_t sem;
int value;
} *shm_data;
// 在共享内存中初始化
sem_init(&shm_data->sem, 1, 1); // 1表示进程间共享
// 使用
sem_wait(&shm_data->sem);
shm_data->value++;
sem_post(&shm_data->sem);
// 清理
sem_destroy(&shm_data->sem);
信号量解决共享内存同步问题
问题分析
- 多个进程同时访问共享内存会导致:
- 数据竞争(Data Race)
- 数据不一致
- 不可预测的行为
解决方案
使用命名信号量
c
#include <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>
int main() {
// 1. 创建共享内存
int shmid = shmget(ftok(".", 65), 1024, IPC_CREAT | 0666);
int *data = shmat(shmid, NULL, 0);
// 2. 创建/打开命名信号量
sem_t *sem = sem_open("/shm_sem", O_CREAT, 0666, 1);
// 3. 同步访问
sem_wait(sem);
(*data)++; // 安全操作
sem_post(sem);
// 4. 清理
shmdt(data);
sem_close(sem);
sem_unlink("/shm_sem");
return 0;
}
使用匿名信号量
c
#include <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>
struct shared_memory {
sem_t lock; // 信号量
int counter; // 共享数据
};
int main() {
// 1. 创建包含信号量的共享内存
int shmid = shmget(ftok(".", 66), sizeof(struct shared_memory),
IPC_CREAT | 0666);
struct shared_memory *shm = shmat(shmid, NULL, 0);
// 2. 初始化信号量(在第一个进程中)
sem_init(&shm->lock, 1, 1); // 进程间共享,初始值1
// 3. 使用
sem_wait(&shm->lock);
shm->counter++;
sem_post(&shm->lock);
// 4. 清理
sem_destroy(&shm->lock);
shmdt(shm);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
线程同步工具在进程间应用
基本原理
- 默认情况下,互斥量用于同一进程内的线程同步
- 在初始化时设置其属性为
PTHREAD_PROCESS_SHARED。可以实现多个进程通过共享内存来访问同一个互斥量,从而实现进程间的同步
互斥锁(Mutex)进程间使用
c
#include <pthread.h>
#include <sys/mman.h>
typedef struct {
pthread_mutex_t mutex;
int data;
} shared_data_t;
int main() {
// 创建共享内存
shared_data_t *shm = mmap(NULL, sizeof(shared_data_t),
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
// 设置互斥锁属性为进程间共享
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
// 初始化互斥锁
pthread_mutex_init(&shm->mutex, &attr);
// 使用
pthread_mutex_lock(&shm->mutex);
shm->data++;
pthread_mutex_unlock(&shm->mutex);
// 清理
pthread_mutex_destroy(&shm->mutex);
munmap(shm, sizeof(shared_data_t));
return 0;
}
条件变量(Condition Variable)进程间使用
- 条件变量通常与互斥量配合使用,用于线程间的同步
- 与共享内存结合以后可以变成进程间的同步工具
- 具体而言,可以在共享内存中创建条件变量,并确保相关的互斥量也设置为进程间共享。多个进程可以通过共享内存中的条件变量来进行同步
c
#include <pthread.h>
#include <sys/mman.h>
typedef struct {
pthread_mutex_t mutex;
pthread_cond_t cond;
int ready;
} shared_data_t;
int main() {
shared_data_t *shm = mmap(NULL, sizeof(shared_data_t),
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
// 设置互斥锁和条件变量为进程间共享
pthread_mutexattr_t m_attr;
pthread_mutexattr_init(&m_attr);
pthread_mutexattr_setpshared(&m_attr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&shm->mutex, &m_attr);
pthread_condattr_t c_attr;
pthread_condattr_init(&c_attr);
pthread_condattr_setpshared(&c_attr, PTHREAD_PROCESS_SHARED);
pthread_cond_init(&shm->cond, &c_attr);
shm->ready = 0;
if (fork() == 0) {
// 子进程:发送信号
pthread_mutex_lock(&shm->mutex);
shm->ready = 1;
pthread_cond_signal(&shm->cond);
pthread_mutex_unlock(&shm->mutex);
exit(0);
} else {
// 父进程:等待
pthread_mutex_lock(&shm->mutex);
while (shm->ready == 0) {
pthread_cond_wait(&shm->cond, &shm->mutex);
}
pthread_mutex_unlock(&shm->mutex);
}
// 清理
pthread_cond_destroy(&shm->cond);
pthread_mutex_destroy(&shm->mutex);
munmap(shm, sizeof(shared_data_t));
return 0;
}
读写锁(RW Lock)进程间使用
- 读写锁在Linux中主要用于线程间的同步
- 要在进程间使用读写锁,需要将其与共享内存结合使用,并确保相关的互斥量设置为进程间共享
- 通过这种方式,多个进程可以通过共享内存中的读写锁来进行同步
c
#include <pthread.h>
#include <sys/mman.h>
typedef struct {
pthread_rwlock_t rwlock;
int data;
} shared_data_t;
int main() {
shared_data_t *shm = mmap(NULL, sizeof(shared_data_t),
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_rwlock_init(&shm->rwlock, &attr);
// 读锁
pthread_rwlock_rdlock(&shm->rwlock);
// 读操作
pthread_rwlock_unlock(&shm->rwlock);
// 写锁
pthread_rwlock_wrlock(&shm->rwlock);
shm->data = 100;
pthread_rwlock_unlock(&shm->rwlock);
pthread_rwlock_destroy(&shm->rwlock);
munmap(shm, sizeof(shared_data_t));
return 0;
}
进程通信与同步工具总结

通信工具对比
| 工具 | 通信方式 | 使用场景 | 特点 |
|---|---|---|---|
| 管道 | 字节流 | 相关进程 | 简单,单向 |
| 命名管道 | 字节流 | 任意进程 | 文件系统可见 |
| 消息队列 | 消息 | 任意进程 | 结构化,支持类型 |
| 共享内存 | 内存 | 任意进程 | 最高性能,需同步 |
| 信号 | 信号值 | 任意进程 | 异步,轻量级 |

同步工具对比
| 工具 | 类型 | 适用范围 | 特点 |
|---|---|---|---|
| System V信号量 | 内核 | 进程间 | 功能强大,复杂 |
| POSIX命名信号量 | 文件系统 | 进程间 | 简单,易用 |
| POSIX匿名信号量 | 内存 | 线程/进程间 | 灵活,需共享内存 |
| 互斥锁(进程间) | 内存 | 进程间 | 互斥访问 |
| 条件变量(进程间) | 内存 | 进程间 | 条件等待 |
| 读写锁(进程间) | 内存 | 进程间 | 读写分离 |