目录

1 简介

1.1 本质

1.2 实现原理

1.3 标准

1.4 接口

2 C语言实现

2.1 System V 信号量使用示例

 2.2 POSIX 信号量示例

匿名信号量示例

命名信号量示例

3 面试常见问题


1 简介

信号量 是一种操作系统提供的 同步机制(Synchronization Mechanism),用于在多进程或多线程中协调共享资源的访问,防止出现竞争条件(race condition)。

1.1 本质

信号量的本质是一个由操作系统内核维护的原子计数器,通过阻塞与唤醒机制协调多个执行流对共享资源的访问;当资源可用时计数减一,不可用时线程进入等待队列,释放资源时计数加一并唤醒等待线程,从而实现并发同步与互斥控制。

1.2 实现原理

信号量的实现原理可以通俗地理解为:操作系统在内核中维护一个“计数器”(一般俗称S)和一个“排队区”。当线程想要使用某个共享资源时,就去计数器那里“抢票”(P操作)——如果还有票(计数>0),它拿走一张就能进入;如果没票了(计数≤0),它就被操作系统安排到“等待区”睡觉。等其他线程用完资源把票还回来时(V操作),操作系统会从等待区叫醒一个线程继续运行。整个“抢票、睡觉、唤醒”的过程由内核保证是原子的,防止竞争和混乱。

P 操作(也叫 waitdown)表示“请求资源”:把信号量计数器减一;如果结果小于 0,说明资源已用完,线程就会被阻塞等待。
V 操作(也叫 signalup)表示“释放资源”:把计数器加一;如果结果小于等于 0,说明有线程在等待,系统会唤醒其中一个继续执行。

1.3 标准

好的,我帮你把 信号量标准对比 梳理成一张紧凑表格:

特性 System V 信号量 POSIX 信号量
标准来源 UNIX System V POSIX.1b(实时扩展)
使用范围 进程间通信(IPC) 线程与进程同步
创建方式 通过 key_t 获取或创建信号量集 匿名信号量:sem_init();命名信号量:sem_open()
P/V 操作 semop() sem_wait() / sem_post()
控制/删除 semctl() sem_destroy() / sem_unlink()
命名支持 不直接支持 支持命名信号量
接口复杂度 相对复杂 简单直观
常见场景 传统 UNIX IPC,跨进程同步 多线程同步,现代进程同步
适合场景 长期存在的资源同步 临时或动态资源同步

1.4 接口

2 C语言实现

2.1 System V 信号量使用示例

/**
 * File: sysv_sem_demo.c
 * Build: gcc sysv_sem_demo.c -o sysv_sem_demo
 * Run:   ./sysv_sem_demo
 */

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>

/* System V 的 semctl 第4个参数需要 union semun */
union semun {
    int val;                // SETVAL
    struct semid_ds *buf;   // IPC_STAT, IPC_SET
    unsigned short *array;  // GETALL, SETALL
};

int main(void)
{
    key_t key = 1234;             // 信号量键值(可使用 ftok() 动态生成)
    int semid;

    // 1️⃣ 创建或获取信号量集(只有1个信号量)
    semid = semget(key, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        exit(EXIT_FAILURE);
    }

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

    printf("Semaphore created (semid=%d), initial value=1\n", semid);

    // 定义 P / V 操作结构
    struct sembuf p = {0, -1, SEM_UNDO}; // P 操作:占用资源
    struct sembuf v = {0,  1, SEM_UNDO}; // V 操作:释放资源

    // 3️⃣ 执行 P 操作(进入临界区)
    printf("Process %d trying to enter critical section...\n", getpid());
    if (semop(semid, &p, 1) == -1) {
        perror("semop - P");
        exit(EXIT_FAILURE);
    }
    printf("Process %d entered critical section.\n", getpid());

    // 临界区:模拟资源使用
    sleep(2);

    // 4️⃣ 执行 V 操作(离开临界区)
    printf("Process %d leaving critical section...\n", getpid());
    if (semop(semid, &v, 1) == -1) {
        perror("semop - V");
        exit(EXIT_FAILURE);
    }

    printf("Process %d exited critical section.\n", getpid());

    // 5️⃣ 删除信号量(清理)
    if (semctl(semid, 0, IPC_RMID, arg) == -1) {
        perror("semctl - IPC_RMID");
        exit(EXIT_FAILURE);
    }

    printf("Semaphore removed.\n");
    return 0;
}

🧩 运行效果示例:

$ gcc sysv_sem_demo.c -o sysv_sem_demo
$ ./sysv_sem_demo
Semaphore created (semid=32768), initial value=1
Process 12563 trying to enter critical section...
Process 12563 entered critical section.
Process 12563 leaving critical section...
Process 12563 exited critical section.
Semaphore removed.

 2.2 POSIX 信号量示例

匿名信号量示例

用于线程间同步(或者共享内存的多进程同步)

/**
 * posix_sem_anonymous.c
 * 构建: gcc posix_sem_anonymous.c -o posix_sem_anonymous -pthread
 * 运行: ./posix_sem_anonymous
 *
 * 使用匿名信号量在两个线程间同步。
 */

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

sem_t sem; // 匿名信号量

void* thread_func(void* arg) {
    for (int i = 0; i < 5; ++i) {
        sem_wait(&sem);  // P操作(等待信号量)
        printf("Child thread: step %d\n", i + 1);
        sleep(1);
        sem_post(&sem);  // V操作(释放信号量)
    }
    return NULL;
}

int main(void) {
    pthread_t tid;

    // 初始化信号量: pshared=0 (线程间同步), 初始值=1
    if (sem_init(&sem, 0, 1) != 0) {
        perror("sem_init");
        exit(EXIT_FAILURE);
    }

    pthread_create(&tid, NULL, thread_func, NULL);

    for (int i = 0; i < 5; ++i) {
        sem_wait(&sem);  // P操作
        printf("Main thread: step %d\n", i + 1);
        sleep(1);
        sem_post(&sem);  // V操作
    }

    pthread_join(tid, NULL);
    sem_destroy(&sem);  // 销毁信号量
    return 0;
}

✅ 输出示例:

Main thread: step 1
Child thread: step 1
Main thread: step 2
Child thread: step 2
...

命名信号量示例

不同进程通过同一个名字(以 / 开头)共享信号量。
适合两个不相关的程序同步。


📘 1) sem_create.c(初始化信号量)

/**
 * sem_create.c
 * Build: gcc sem_create.c -o sem_create -lrt
 * Run:   ./sem_create
 */

#include <stdio.h>
#include <semaphore.h>
#include <fcntl.h>    // O_CREAT, O_EXCL
#include <sys/stat.h> // mode constants
#include <stdlib.h>
#include <errno.h>

int main(void) {
    sem_t *sem = sem_open("/demo_sem", O_CREAT | O_EXCL, 0666, 1);
    if (sem == SEM_FAILED) {
        if (errno == EEXIST) {
            printf("Semaphore already exists, open existing one.\n");
            sem = sem_open("/demo_sem", 0);
        } else {
            perror("sem_open");
            exit(EXIT_FAILURE);
        }
    }

    printf("Semaphore created or opened successfully.\n");
    sem_close(sem);
    return 0;
}

📘 2) procA.c(等待资源)

/**
 * procA.c
 * Build: gcc procA.c -o procA -lrt
 * Run:   ./procA
 */

#include <stdio.h>
#include <semaphore.h>
#include <fcntl.h>
#include <unistd.h>

int main(void) {
    sem_t *sem = sem_open("/demo_sem", 0);
    if (sem == SEM_FAILED) {
        perror("sem_open");
        return 1;
    }

    for (int i = 0; i < 5; ++i) {
        sem_wait(sem);
        printf("[procA] got resource, iteration %d\n", i + 1);
        sleep(1);
        sem_post(sem);
        sleep(1);
    }

    sem_close(sem);
    return 0;
}

📘 3) procB.c(也访问同一信号量)

/**
 * procB.c
 * Build: gcc procB.c -o procB -lrt
 * Run:   ./procB
 */

#include <stdio.h>
#include <semaphore.h>
#include <fcntl.h>
#include <unistd.h>

int main(void) {
    sem_t *sem = sem_open("/demo_sem", 0);
    if (sem == SEM_FAILED) {
        perror("sem_open");
        return 1;
    }

    for (int i = 0; i < 5; ++i) {
        sem_wait(sem);
        printf("[procB] got resource, iteration %d\n", i + 1);
        sleep(1);
        sem_post(sem);
        sleep(1);
    }

    sem_close(sem);
    return 0;
}

📘 4) 运行方式:

# 先创建信号量(只需运行一次)
gcc sem_create.c -o sem_create -lrt
./sem_create

# 编译两个进程
gcc procA.c -o procA -lrt
gcc procB.c -o procB -lrt

# 在两个不同终端运行
./procA
./procB

输出将交替出现:

[procA] got resource, iteration 1
[procB] got resource, iteration 1
[procA] got resource, iteration 2
[procB] got resource, iteration 2
...

📘 5) 删除信号量

# 删除命名信号量
gcc -o sem_unlink -x c - <<'EOF'
#include <semaphore.h>
#include <stdio.h>
int main() {
    sem_unlink("/demo_sem");
    puts("Semaphore /demo_sem removed.");
    return 0;
}
EOF

./sem_unlink

3 面试常见问题

  1. 什么是信号量
    信号量是一种用于进程或线程同步的内核机制,本质是一个整型计数器,用于表示可用资源的数量。它可以控制多个进程或线程对共享资源的访问,防止竞争条件。当信号量值大于0表示有可用资源,为0表示资源被占满,小于0时表示有等待的进程或线程。

  2. 信号量的分类
    信号量分为计数信号量和二值信号量两类。计数信号量的值可以大于1,适用于控制资源池数量;二值信号量的取值只能为0或1,功能类似互斥锁。此外按实现方式又分为System V信号量和POSIX信号量。

  3. 信号量与互斥锁的区别
    互斥锁主要用于线程间的互斥访问,只能取0或1,有锁的拥有者概念,必须由加锁者解锁;信号量可用于进程或线程间同步,无拥有者概念,值可以大于1,用于控制资源数量。

  4. 信号量与条件变量的区别
    条件变量用于等待某种条件的发生,不具备计数功能,只是同步手段;信号量具有计数属性,可同时表示资源数量和同步状态。

  5. System V信号量的主要接口
    System V信号量使用三个主要系统调用:semget创建或获取信号量集,semop执行P/V操作(等待或释放),semctl用于控制信号量(设置初始值、获取状态、删除信号量等)。

  6. System V信号量的使用步骤
    第一步调用semget创建或打开信号量集;第二步使用semctl初始化信号量值;第三步通过semop执行P操作(等待)或V操作(释放);最后在不再需要时调用semctl删除信号量集。

  7. POSIX信号量的主要接口
    POSIX信号量提供了更简洁的API,包括sem_init用于初始化,sem_wait执行P操作,sem_post执行V操作,sem_destroy销毁信号量。命名信号量还可使用sem_open、sem_unlink在进程间共享。

  8. System V与POSIX信号量的区别
    System V信号量基于内核持久化机制,可以在进程退出后依然存在,支持信号量集合;POSIX信号量更轻量,语义简单,分为命名和匿名两类,命名信号量可跨进程使用,匿名信号量用于线程间同步。

  9. P操作和V操作的含义
    P操作表示请求资源(通常会将信号量值减1),当值小于0时阻塞等待;V操作表示释放资源(将信号量值加1),若有等待进程则唤醒。

  10. 信号量中的union semun结构体
    union semun是semctl函数的第四个参数,用于传递不同类型的数据。其成员包括int val(用于SETVAL设置单个信号量值),struct semid_ds *buf(用于IPC_STAT或IPC_SET获取和修改信号量属性),unsigned short *array(用于GETALL或SETALL操作整个信号量集)。

  11. 一个信号量集合中可以包含多个信号量
    在System V机制下,semget创建的是一个信号量集(数组),每个信号量可独立控制不同的资源。val表示单个信号量的当前计数值,用于表示可用资源数量。

  12. 信号量在多进程同步中的作用
    两个或多个独立进程可以通过共享同一个System V或POSIX命名信号量实现同步。例如一个进程执行sem_wait等待资源,另一个进程执行sem_post释放资源,从而保证执行顺序。

  13. 信号量与共享内存、消息队列的区别
    共享内存用于数据共享,消息队列用于数据传递,而信号量主要用于同步和互斥控制。通常信号量会与共享内存配合使用,保证访问时的互斥安全。

  14. 信号量使用中的常见错误
    常见错误包括未初始化信号量、对已经销毁的信号量操作、未正确删除System V信号量(导致资源泄露)、信号量初始值设置错误或未匹配的P/V操作导致死锁。

  15. 信号量的典型应用场景
    用于生产者消费者模型、资源池管理、进程同步、共享内存保护、临界区访问控制等场景。

  16. 二值信号量与互斥锁的优劣
    二值信号量在跨进程同步方面比互斥锁更灵活,但无法记录锁拥有者,因此在异常退出时可能导致信号量永远占用;互斥锁能自动处理线程退出后的释放。

  17. POSIX命名信号量的使用示例
    使用sem_open创建命名信号量,sem_wait和sem_post进行同步,多个进程通过相同名称访问该信号量;使用sem_unlink删除。

  18. 信号量值的初始意义
    信号量初始值通常表示可用资源数,例如初始化为1表示一次只能有一个进程进入临界区。

  19. 如何查看System V信号量
    可使用ipcs命令查看当前系统中的System V信号量资源,使用ipcrm命令删除。

  20. 信号量的优缺点
    优点是机制简单、内核级同步、效率较高;缺点是操作复杂、调试困难、误用容易导致死锁,System V信号量还可能导致内核残留资源。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐