为什么信号量可以用于IPC(进程间通信)?

26 阅读8分钟

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:没有队列机制

// 多个进程等待时,唤醒顺序不确定
进程Asem_wait(&信号量);
进程Bsem_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?

因为信号量是一种"状态通信"机制,它让进程知道:

  1. 资源可用性:"还有多少资源可用"
  2. 其他进程状态:"有多少进程在等"
  3. 协调信息:"该谁执行了"

记住

  • 信号量不传数据,传状态
  • 信号量是同步工具,不是通信工具
  • 但同步本身也是一种通信(告诉对方"我好了"或"该你了")

类比

  • 管道/消息队列 = 打电话(传递具体信息)
  • 信号量 = 红绿灯(只传递"停"或"行"的状态)
  • 共享内存 = 公告板(大家都能看能写,但需要协调)

信号量让进程能够无需说话就能协调行动,这是最高效的"默契通信"!