22. 信号量.md 8.1 KB

信号量 (Semaphore)

概念

信号量是一个计数器,用于控制多个进程/线程对共享资源的访问。信号量支持两个原子操作:

  • P操作(wait):计数器减1,若计数器为0则阻塞等待
  • V操作(post):计数器加1,唤醒等待的进程/线程

核心特点

  • 用于同步,而非数据传输
  • 支持进程间和线程间同步
  • 二值信号量(0/1)实现互斥,计数信号量控制并发数量

二值信号量 vs 计数信号量

类型 计数值范围 典型用途
二值信号量 0 或 1 互斥访问(类似互斥锁)
计数信号量 0 ~ N 限制并发访问数量(如连接池)

System V 信号量

关键API

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

// 创建/获取信号量集
int semget(key_t key, int nsems, int semflg);

// 信号量操作
int semop(int semid, struct sembuf *sops, unsigned nsops);

// 信号量控制
int semctl(int semid, int semnum, int cmd, ...);

sembuf 结构体

struct sembuf {
    unsigned short sem_num;  // 信号量编号(在信号量集中的索引)
    short sem_op;            // 操作:-1=P操作,+1=V操作,0=等待为0
    short sem_flg;           // 标志:IPC_NOWAIT(非阻塞),SEM_UNDO(进程退出自动还原)
};

semctl 常用命令

命令 说明
SETVAL 设置信号量值
GETVAL 获取信号量值
IPC_RMID 删除信号量集
GETALL 获取所有信号量值
SETALL 设置所有信号量值

POSIX 信号量

关键API

#include <semaphore.h>

// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);

// P操作(等待/减1)
int sem_wait(sem_t *sem);

// 非阻塞P操作
int sem_trywait(sem_t *sem);

// V操作(通知/加1)
int sem_post(sem_t *sem);

// 销毁信号量
int sem_destroy(sem_t *sem);

参数说明

  • pshared:0 表示线程间共享,非0 表示进程间共享(需放在共享内存中)
  • 命名信号量:sem_open / sem_close / sem_unlink(可用于无亲缘关系的进程)

信号量与共享内存配合(经典组合)

信号量最常见的用途是保护共享内存的并发访问:

进程A                    进程B
  │                        │
  ├── P(写锁) ────────────>│
  │   写入共享内存          │
  ├── V(读锁) ────────────>│
  │                   P(读锁)
  │                   读取共享内存
  │                   V(写锁)

完整代码示例:生产者-消费者

// sem_prodcons.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

#define BUFFER_SIZE 5

int buffer[BUFFER_SIZE];
int in = 0, out = 0;

sem_t sem_empty;  // 空闲槽位数
sem_t sem_full;   // 已用槽位数
sem_t sem_mutex;  // 互斥访问缓冲区

void *producer(void *arg) {
    int id = *(int *)arg;
    for (int i = 0; i < 10; i++) {
        int item = id * 100 + i;

        sem_wait(&sem_empty);   // 等待空闲槽位
        sem_wait(&sem_mutex);   // 互斥访问

        buffer[in] = item;
        in = (in + 1) % BUFFER_SIZE;
        printf("[Producer %d] Produced: %d\n", id, item);

        sem_post(&sem_mutex);   // 释放互斥
        sem_post(&sem_full);    // 增加已用槽位

        usleep(100000);  // 模拟生产耗时
    }
    return NULL;
}

void *consumer(void *arg) {
    int id = *(int *)arg;
    for (int i = 0; i < 10; i++) {
        sem_wait(&sem_full);    // 等待有数据
        sem_wait(&sem_mutex);   // 互斥访问

        int item = buffer[out];
        out = (out + 1) % BUFFER_SIZE;
        printf("[Consumer %d] Consumed: %d\n", id, item);

        sem_post(&sem_mutex);   // 释放互斥
        sem_post(&sem_empty);   // 增加空闲槽位

        usleep(150000);  // 模拟消费耗时
    }
    return NULL;
}

int main(void) {
    pthread_t prod_tid[2], cons_tid[2];
    int prod_ids[2] = {1, 2};
    int cons_ids[2] = {1, 2};

    // 初始化信号量
    sem_init(&sem_empty, 0, BUFFER_SIZE);  // 初始空闲槽位 = 缓冲区大小
    sem_init(&sem_full, 0, 0);             // 初始已用槽位 = 0
    sem_init(&sem_mutex, 0, 1);            // 互斥信号量初始值 = 1

    // 创建生产者和消费者线程
    for (int i = 0; i < 2; i++) {
        pthread_create(&prod_tid[i], NULL, producer, &prod_ids[i]);
        pthread_create(&cons_tid[i], NULL, consumer, &cons_ids[i]);
    }

    // 等待线程结束
    for (int i = 0; i < 2; i++) {
        pthread_join(prod_tid[i], NULL);
        pthread_join(cons_tid[i], NULL);
    }

    // 销毁信号量
    sem_destroy(&sem_empty);
    sem_destroy(&sem_full);
    sem_destroy(&sem_mutex);

    printf("All done.\n");
    return 0;
}

编译与运行

gcc -o sem_prodcons sem_prodcons.c -pthread
./sem_prodcons

System V 信号量示例

// sem_sysv.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <sys/wait.h>

#define SEM_KEY 0x5678

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

void p_operation(int semid, int sem_num) {
    struct sembuf op = {sem_num, -1, 0};
    if (semop(semid, &op, 1) == -1) {
        perror("P operation");
        exit(EXIT_FAILURE);
    }
}

void v_operation(int semid, int sem_num) {
    struct sembuf op = {sem_num, 1, 0};
    if (semop(semid, &op, 1) == -1) {
        perror("V operation");
        exit(EXIT_FAILURE);
    }
}

int main(void) {
    // 创建信号量集(1个信号量)
    int semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        exit(EXIT_FAILURE);
    }

    // 初始化信号量值为 1(二值信号量)
    union semun arg;
    arg.val = 1;
    if (semctl(semid, 0, SETVAL, arg) == -1) {
        perror("semctl SETVAL");
        exit(EXIT_FAILURE);
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // 子进程:进入临界区
        printf("[Child] Waiting for semaphore...\n");
        p_operation(semid, 0);
        printf("[Child] Entered critical section\n");
        sleep(2);
        printf("[Child] Leaving critical section\n");
        v_operation(semid, 0);
        exit(EXIT_SUCCESS);
    } else {
        // 父进程:等待信号量
        printf("[Parent] Waiting for semaphore...\n");
        p_operation(semid, 0);
        printf("[Parent] Entered critical section\n");
        sleep(2);
        printf("[Parent] Leaving critical section\n");
        v_operation(semid, 0);
        wait(NULL);
    }

    // 删除信号量集
    semctl(semid, 0, IPC_RMID);
    printf("Semaphore removed\n");

    return 0;
}

编译与运行

gcc -o sem_sysv sem_sysv.c
./sem_sysv

注意事项

  1. 信号量不是锁:信号量用于同步计数,互斥锁才是真正的互斥机制
  2. 死锁风险:P操作顺序不当可能导致死锁,应按固定顺序获取信号量
  3. SEM_UNDO 标志:使用 SEM_UNDO 可防止进程异常退出后信号量未释放
  4. POSIX 信号量移植性更好:优先使用 POSIX 接口
  5. 命名信号量:POSIX 命名信号量(sem_open)可用于无亲缘关系的进程
  6. 性能:信号量操作是系统调用,频繁操作有开销;线程间可考虑用户态同步原语

面试要点

  1. P/V 操作含义:P = Proberen(测试/等待),V = Verhogen(增加/释放)
  2. 信号量 vs 互斥锁:信号量可以跨进程、可计数;互斥锁更简单但仅限线程
  3. 经典同步问题:生产者-消费者、读者-写者、哲学家就餐
  4. 信号量与共享内存配合:共享内存提供数据,信号量提供同步
  5. System V vs POSIX:POSIX 更现代、可移植,推荐在新项目中使用
  6. 死锁预防:固定加锁顺序、超时机制、资源分级