1. 信号量的本质:一个"公共计数器"
类比:停车场的剩余车位显示器
想象一个停车场(共享资源):
- 剩余车位显示器(信号量):显示还有多少个空车位
- 车(进程):想停车的司机
- 停车位(共享资源)
工作流程:
1. 车到入口,看显示器:显示3(有3个空位)
2. 车进入,显示器变成2
3. 又一辆车进入,显示器变成1
4. 有车离开,显示器变成2
关键:这个显示器本身不传输任何数据,只传递数量信息,所有车都能看到。
2. 信号量作为IPC的特殊之处
传统IPC vs 信号量IPC
| 通信类型 | 传递什么 | 例子 |
|---|---|---|
| 数据传输IPC | 传递实际数据 | 管道、消息队列、共享内存 |
| 信号IPC | 传递简单信号 | kill信号 |
| 信号量IPC | 传递"可用数量" | 同步、互斥、资源计数 |
信号量是"数量传递器" ,不是"数据传输器"。
3. 信号量如何实现进程间通信?
场景:两个进程共享打印机
// 进程A:打印文档1
// 进程B:打印文档2
// 只有一个打印机,需要协调
// 创建信号量(初始值1,表示1个打印机可用)
sem_t 打印机信号量;
sem_init(&打印机信号量, 1, 1); // 第二个参数1表示进程间共享
进程A的代码:
void 进程A() {
printf("进程A:想打印文档\n");
sem_wait(&打印机信号量); // 等待打印机
printf("进程A:开始打印...\n");
sleep(3); // 模拟打印
sem_post(&打印机信号量); // 释放打印机
printf("进程A:打印完成\n");
}
进程B的代码:
void 进程B() {
printf("进程B:想打印文档\n");
sem_wait(&打印机信号量); // 等待打印机
printf("进程B:开始打印...\n");
sleep(2); // 模拟打印
sem_post(&打印机信号量); // 释放打印机
printf("进程B:打印完成\n");
}
通信过程:
进程A:想打印 → 看信号量=1 → 减1变成0 → 开始打印
进程B:想打印 → 看信号量=0 → 等待...
进程A:完成 → 信号量+1变成1
进程B:看到信号量=1 → 减1变成0 → 开始打印
这就是通信:通过信号量的值变化,进程知道了对方的状态!
4. 信号量IPC的三种使用模式
模式1:互斥锁模式(二元信号量)
// 像一个只能一个人用的厕所
sem_t 厕所;
sem_init(&厕所, 1, 1); // 初始值1,只有一个坑位
// 进程A
sem_wait(&厕所); // 如果有人,就等
用厕所();
sem_post(&厕所); // 出来,下个人可以用
// 进程B
sem_wait(&厕所);
用厕所();
sem_post(&厕所);
通信内容:"厕所是否有人"
模式2:资源池模式(计数信号量)
// 像停车场,有多个车位
#define 总车位 5
sem_t 停车场;
sem_init(&停车场, 1, 总车位); // 初始5个空位
// 车进入
void 停车() {
sem_wait(&停车场); // 等空位
printf("找到车位停车\n");
}
// 车离开
void 离开() {
sem_post(&停车场); // 释放车位
printf("开走,腾出车位\n");
}
通信内容:"还有多少空位"
模式3:条件同步模式
// 像接力赛,等队友交棒
sem_t 接力棒;
sem_init(&接力棒, 1, 0); // 初始0,表示棒还没传过来
// 进程A(传棒人)
void 传棒() {
跑到交接点();
sem_post(&接力棒); // 把棒递出去
printf("棒已传出\n");
}
// 进程B(接棒人)
void 接棒() {
sem_wait(&接力棒); // 等棒
接到棒();
printf("接到棒,继续跑\n");
}
通信内容:"棒是否传过来了"
5. 实际IPC示例:生产者-消费者问题
使用信号量协调多个进程
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#define BUFFER_SIZE 5
// 共享数据结构
struct 共享数据 {
int buffer[BUFFER_SIZE];
int in; // 生产位置
int out; // 消费位置
sem_t 空位; // 空位信号量
sem_t 有数据; // 数据信号量
sem_t 互斥锁; // 互斥信号量
};
int main() {
// 创建共享内存
struct 共享数据 *共享 = mmap(NULL, sizeof(struct 共享数据),
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
共享->in = 0;
共享->out = 0;
// 初始化信号量(进程间共享)
sem_init(&共享->空位, 1, BUFFER_SIZE); // 初始满空位
sem_init(&共享->有数据, 1, 0); // 初始无数据
sem_init(&共享->互斥锁, 1, 1); // 互斥锁
// 创建生产者进程
if (fork() == 0) {
// 子进程:生产者
for (int i = 1; i <= 10; i++) {
sem_wait(&共享->空位); // 等空位
sem_wait(&共享->互斥锁); // 加锁
// 生产数据
共享->buffer[共享->in] = i;
printf("生产者放入: %d 到位置 %d\n", i, 共享->in);
共享->in = (共享->in + 1) % BUFFER_SIZE;
sem_post(&共享->互斥锁); // 解锁
sem_post(&共享->有数据); // 通知有数据
sleep(1); // 生产慢一点
}
exit(0);
}
// 创建消费者进程
if (fork() == 0) {
// 子进程:消费者
for (int i = 1; i <= 10; i++) {
sem_wait(&共享->有数据); // 等有数据
sem_wait(&共享->互斥锁); // 加锁
// 消费数据
int 数据 = 共享->buffer[共享->out];
printf("消费者取出: %d 从位置 %d\n", 数据, 共享->out);
共享->out = (共享->out + 1) % BUFFER_SIZE;
sem_post(&共享->互斥锁); // 解锁
sem_post(&共享->空位); // 通知有空位
sleep(2); // 消费慢一点
}
exit(0);
}
// 父进程等待
wait(NULL);
wait(NULL);
// 清理
sem_destroy(&共享->空位);
sem_destroy(&共享->有数据);
sem_destroy(&共享->互斥锁);
munmap(共享, sizeof(struct 共享数据));
return 0;
}
6. 信号量IPC的优势
优势1:轻量级
// 信号量 vs 其他IPC
管道:需要创建管道文件,数据传输
消息队列:需要内核维护队列
共享内存:需要分配大块内存
信号量:只是一个计数器,非常轻量
优势2:速度快
因为只操作计数器,不需要数据拷贝:
// 信号量操作
sem_wait(sem); // 只是计数器减1
sem_post(sem); // 只是计数器加1
// 管道操作
write(pipe, data, size); // 要拷贝数据到内核
read(pipe, buffer, size); // 要从内核拷贝数据
优势3:可扩展
// 从2个进程扩展到N个进程
sem_t 资源;
sem_init(&资源, 1, 资源数量); // 改个初始值就行
// 所有进程用同样的代码访问
for (int i = 0; i < 进程数; i++) {
if (fork() == 0) {
sem_wait(&资源);
用资源();
sem_post(&资源);
exit(0);
}
}
7. 信号量IPC的局限性
局限性1:只能传递数量,不能传递数据
// 不能这样:
sem_wait(&信号量, 数据); // 错误!不能附带数据
sem_post(&信号量, 数据); // 错误!
// 必须配合其他IPC使用
sem_wait(&信号量);
从共享内存读数据(); // 需要共享内存
sem_post(&信号量);
局限性2:没有队列机制
// 多个进程等待时,唤醒顺序不确定
进程A:sem_wait(&信号量);
进程B:sem_wait(&信号量);
进程C:sem_wait(&信号量);
sem_post(&信号量); // 只唤醒一个,哪个不确定
8. 信号量IPC的实现方式
方式1:POSIX信号量
#include <semaphore.h>
// 命名信号量(通过名字访问)
sem_t *信号量 = sem_open("/my_sem", O_CREAT, 0644, 初始值);
// 无名信号量(放在共享内存)
sem_t *信号量 = mmap共享的区域;
sem_init(信号量, 1, 初始值); // 第二个参数1表示进程间共享
方式2:System V信号量
#include <sys/sem.h>
// 创建信号量集
int semid = semget(IPC_PRIVATE, 信号量数量, IPC_CREAT | 0666);
// 初始化
union semun { int val; } arg;
arg.val = 初始值;
semctl(semid, 0, SETVAL, arg);
// 操作
struct sembuf op = {0, -1, 0}; // P操作
semop(semid, &op, 1);
9. 实际应用场景
场景1:Web服务器连接池
// 限制同时处理的连接数
#define MAX_CONNECTIONS 100
sem_t 连接池;
void 处理请求() {
sem_wait(&连接池); // 等可用连接
处理HTTP请求();
sem_post(&连接池); // 释放连接
}
场景2:数据库连接池
// 多个进程共享数据库连接
sem_t 数据库连接;
连接数组[10]; // 10个连接
int 获取连接() {
sem_wait(&数据库连接);
for (int i = 0; i < 10; i++) {
if (连接数组[i]可用) {
return i; // 返回连接索引
}
}
return -1;
}
场景3:限流器
// 限制每秒请求数
sem_t 限流器;
sem_init(&限流器, 1, 100); // 每秒100个
void 处理请求() {
if (sem_trywait(&限流器) == 0) {
处理(); // 在限制内
} else {
返回429; // 太多请求
}
}
10. 总结:为什么信号量可以用于IPC?
因为信号量是一种"状态通信"机制,它让进程知道:
- 资源可用性:"还有多少资源可用"
- 其他进程状态:"有多少进程在等"
- 协调信息:"该谁执行了"
记住:
- 信号量不传数据,传状态
- 信号量是同步工具,不是通信工具
- 但同步本身也是一种通信(告诉对方"我好了"或"该你了")
类比:
- 管道/消息队列 = 打电话(传递具体信息)
- 信号量 = 红绿灯(只传递"停"或"行"的状态)
- 共享内存 = 公告板(大家都能看能写,但需要协调)
信号量让进程能够无需说话就能协调行动,这是最高效的"默契通信"!