linux的信号量初识

Linux下的信号量(Semaphore)深度解析

在多线程或多进程并发编程的领域中,确保对共享资源的安全访问和协调不同执行单元的同步至关重要。信号量(Semaphore)作为经典的同步原语之一,在 Linux 系统中扮演着核心角色。本文将深入探讨 Linux 环境下 POSIX 信号量的概念、工作原理、API 使用、示例代码、流程图及注意事项。

1. 什么是信号量?

信号量是由荷兰计算机科学家艾兹格·迪科斯彻(Edsger Dijkstra)在 1965 年左右提出的一个同步机制。本质上,信号量是一个非负整数计数器,它被用于控制对一组共享资源的访问。它主要支持两种原子操作:

  1. P 操作 (Proberen - 测试/尝试): 也称为 wait(), down(), acquire()。此操作会检查信号量的值。
    • 如果信号量的值大于 0,则将其减 1,进程/线程继续执行。
    • 如果信号量的值等于 0,则进程/线程会被阻塞(放入等待队列),直到信号量的值变为大于 0。
  2. V 操作 (Verhogen - 增加): 也称为 signal(), up(), post(), release()。此操作会将信号量的值加 1。
    • 如果此时有其他进程/线程因等待该信号量而被阻塞,则系统会唤醒其中一个(或多个,取决于实现)等待的进程/线程。

核心思想: 信号量的值代表了当前可用资源的数量。当一个进程/线程需要使用资源时,它执行 P 操作;当它释放资源时,执行 V 操作。

类比:

  • 计数信号量 (Counting Semaphore): 想象一个有 N 个停车位的停车场。信号量的初始值是 N。每当一辆车进入,信号量减 1 (P 操作)。当车位满 (信号量为 0) 时,新来的车必须等待。每当一辆车离开,信号量加 1 (V 操作),并可能通知等待的车辆有空位了。
  • 二值信号量 (Binary Semaphore): 停车场只有一个车位 (N=1)。信号量的值只能是 0 或 1。这常被用作互斥锁 (Mutex),确保同一时间只有一个进程/线程能访问某个临界区。

2. Linux 中的信号量类型

Linux 主要支持两种信号量实现:

  1. System V 信号量: 这是较老的一套 IPC (Inter-Process Communication) 机制的一部分(还包括 System V 消息队列和共享内存)。它功能强大但 API 相对复杂,信号量通常是内核持久的,需要显式删除。相关函数有 semget(), semop(), semctl()
  2. POSIX 信号量: 这是 POSIX 标准定义的一套接口,通常更推荐在新代码中使用。它提供了更简洁、更易于使用的 API。POSIX 信号量可以是命名信号量 (可在不相关的进程间共享,通过名字访问,如 /mysemaphore)或未命名信号量(通常在同一进程的线程间或父子进程间共享,存在于内存中)。

本文将重点关注更常用且推荐的 POSIX 未命名信号量。

3. POSIX 信号量核心 API (C/C++)

使用 POSIX 信号量需要包含头文件 <semaphore.h>

3.1 sem_init() - 初始化未命名信号量

c 复制代码
#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

功能: 初始化位于 sem 指向地址的未命名信号量。

参数:

  • sem_t *sem: 指向要初始化的信号量对象的指针。sem_t 是信号量类型。
  • int pshared: 控制信号量的共享范围。
    • 0: 信号量在当前进程的线程间共享。信号量对象 sem 应位于所有线程都能访问的内存区域(如全局变量、堆内存)。
    • 0: 信号量在进程间共享。信号量对象 sem 必须位于共享内存区域(例如使用 mmap 创建的共享内存段)。
  • unsigned int value: 信号量的初始值。对于二值信号量(用作锁),通常初始化为 1;对于计数信号量,根据可用资源数量初始化。

返回值:

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置 errno。常见的 errno 包括 EINVAL (value 超过 SEM_VALUE_MAX),ENOSYS (不支持进程间共享)。

3.2 sem_destroy() - 销毁未命名信号量

c 复制代码
#include <semaphore.h>

int sem_destroy(sem_t *sem);

功能: 销毁由 sem_init() 初始化的未命名信号量 sem。销毁一个正在被其他线程等待的信号量会导致未定义行为。只有在确认没有线程再使用该信号量后才能销毁。

参数:

  • sem_t *sem: 指向要销毁的信号量对象的指针。

返回值:

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置 errno (如 EINVAL 表示 sem 不是一个有效的信号量)。

3.3 sem_wait() - 等待(P 操作/减 1)

c 复制代码
#include <semaphore.h>

int sem_wait(sem_t *sem);

功能: 对信号量 sem 执行 P 操作(尝试减 1)。

  • 如果信号量的值大于 0,则原子地将其减 1,函数立即返回。
  • 如果信号量的值等于 0,则调用线程/进程将被阻塞,直到信号量的值大于 0(通常是另一个线程/进程调用 sem_post() 之后)或收到一个信号。

参数:

  • sem_t *sem: 指向要操作的信号量对象的指针。

返回值:

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置 errno
    • EINVAL: sem 不是一个有效的信号量。
    • EINTR: 操作被信号中断。应用程序通常需要检查 errno 并重新尝试 sem_wait()

3.4 sem_trywait() - 非阻塞等待

c 复制代码
#include <semaphore.h>

int sem_trywait(sem_t *sem);

功能: sem_wait() 的非阻塞版本。

  • 如果信号量的值大于 0,则原子地将其减 1,函数立即返回 0。
  • 如果信号量的值等于 0,则函数立即返回 -1,并将 errno 设置为 EAGAIN,调用线程不会被阻塞。

参数:

  • sem_t *sem: 指向要操作的信号量对象的指针。

返回值:

  • 成功 (信号量减 1): 返回 0。
  • 失败: 返回 -1,并设置 errno
    • EAGAIN: 信号量当前为 0,无法立即减 1。
    • EINVAL: sem 不是一个有效的信号量。

3.5 sem_timedwait() - 带超时的等待

c 复制代码
#include <semaphore.h>
#include <time.h>

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

功能: 类似 sem_wait(),但带有超时限制。

  • 如果信号量的值大于 0,则原子地将其减 1,函数立即返回 0。
  • 如果信号量的值等于 0,则线程阻塞等待,但如果在 abs_timeout 指定的绝对时间(基于 CLOCK_REALTIME)到达之前信号量仍未增加,则函数返回错误。

参数:

  • sem_t *sem: 指向要操作的信号量对象的指针。
  • const struct timespec *abs_timeout: 指向一个 timespec 结构体,指定了阻塞等待的绝对超时时间点。struct timespec { time_t tv_sec; long tv_nsec; };

返回值:

  • 成功 (信号量减 1): 返回 0。
  • 失败: 返回 -1,并设置 errno
    • ETIMEDOUT: 在超时时间到达前未能成功将信号量减 1。
    • EINVAL: sem 无效或 abs_timeout 无效。
    • EINTR: 操作被信号中断。

3.6 sem_post() - 释放(V 操作/加 1)

c 复制代码
#include <semaphore.h>

int sem_post(sem_t *sem);

功能: 对信号量 sem 执行 V 操作(原子地将其值加 1)。如果有任何线程/进程因此信号量而被阻塞,则其中一个会被唤醒。

参数:

  • sem_t *sem: 指向要操作的信号量对象的指针。

返回值:

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置 errno
    • EINVAL: sem 不是一个有效的信号量。
    • EOVERFLOW: 信号量的值增加将超过 SEM_VALUE_MAX

3.7 sem_getvalue() - 获取信号量当前值

c 复制代码
#include <semaphore.h>

int sem_getvalue(sem_t *sem, int *sval);

功能: 获取信号量 sem 的当前值,并将其存储在 sval 指向的整数中。注意:获取到的值可能在函数返回后立即就过时了(因为其他线程可能同时修改了信号量),主要用于调试或特定场景。

参数:

  • sem_t *sem: 指向要查询的信号量对象的指针。
  • int *sval: 指向用于存储信号量当前值的整数的指针。

返回值:

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置 errno (如 EINVAL)。

4. 工作流程图 (sem_wait 和 sem_post)

graph TD subgraph Thread A (Calls sem_wait) A1(Start sem_wait(sem)) --> A2{Check sem value > 0?}; A2 -- Yes --> A3[Decrement sem value]; A3 --> A4[Proceed]; A2 -- No --> A5[Block Thread A]; end subgraph Thread B (Calls sem_post) B1(Start sem_post(sem)) --> B2[Increment sem value]; B2 --> B3{Any threads blocked on sem?}; B3 -- Yes --> B4[Wake up one blocked thread (e.g., Thread A)]; B3 -- No --> B5[Return]; B4 --> B5; end A5 --> B4; // Woken up by Thread B's post B4 -..-> A2; // Woken Thread A re-evaluates condition

流程图解释:

sem_wait 流程 (Thread A):

  1. 线程 A 调用 sem_wait
  2. 检查信号量的值是否大于 0。
    • 是: 信号量减 1,线程 A 继续执行。
    • 否: 线程 A 被阻塞,进入等待状态。

sem_post 流程 (Thread B):

  1. 线程 B 调用 sem_post
  2. 信号量的值加 1。
  3. 检查是否有其他线程(如线程 A)正因该信号量而被阻塞。
    • 是: 唤醒其中一个被阻塞的线程。被唤醒的线程会回到 sem_wait 的检查点,此时信号量值已大于 0,它将成功减 1 并继续执行。
    • 否: 直接返回。

5. C/C++ 测试用例:使用信号量保护临界区

这个例子演示了如何使用二值信号量(初始化为 1)来实现类似互斥锁的功能,保护一个共享计数器,防止多个线程同时修改导致竞态条件。

cpp 复制代码
#include <iostream>
#include <vector>
#include <thread>
#include <semaphore.h> // For POSIX semaphores
#include <unistd.h>    // For usleep

// Global shared resource
int shared_counter = 0;

// Global semaphore (acting as a mutex)
sem_t mutex_semaphore;

// Number of threads and increments per thread
const int NUM_THREADS = 5;
const int INCREMENTS_PER_THREAD = 100000;

// Thread function
void worker_thread(int id) {
    for (int i = 0; i < INCREMENTS_PER_THREAD; ++i) {
        // --- Enter Critical Section ---
        if (sem_wait(&mutex_semaphore) == -1) { // P operation (wait)
            perror("sem_wait failed");
            return; // Exit thread on error
        }

        // --- Critical Section Start ---
        // Access shared resource
        int temp = shared_counter;
        // Simulate some work inside the critical section
        // usleep(1); // Optional small delay to increase chance of race condition without semaphore
        shared_counter = temp + 1;
        // --- Critical Section End ---

        if (sem_post(&mutex_semaphore) == -1) { // V operation (post)
            perror("sem_post failed");
            // Handle error if necessary, though less critical than wait failure
        }
         // --- Exit Critical Section ---
    }
    std::cout << "Thread " << id << " finished." << std::endl;
}

int main() {
    // Initialize the semaphore
    // pshared = 0: shared between threads of the same process
    // value = 1: initial value, acting as a binary semaphore (mutex)
    if (sem_init(&mutex_semaphore, 0, 1) == -1) {
        perror("sem_init failed");
        return 1;
    }

    std::cout << "Starting " << NUM_THREADS << " threads, each incrementing counter "
              << INCREMENTS_PER_THREAD << " times." << std::endl;

    std::vector<std::thread> threads;
    for (int i = 0; i < NUM_THREADS; ++i) {
        threads.emplace_back(worker_thread, i);
    }

    // Wait for all threads to complete
    for (auto& t : threads) {
        t.join();
    }

    // Destroy the semaphore
    if (sem_destroy(&mutex_semaphore) == -1) {
        perror("sem_destroy failed");
        // Continue cleanup if possible
    }

    std::cout << "All threads finished." << std::endl;
    std::cout << "Expected final counter value: " << NUM_THREADS * INCREMENTS_PER_THREAD << std::endl;
    std::cout << "Actual final counter value:   " << shared_counter << std::endl;

    // Check if the result is correct
    if (shared_counter == NUM_THREADS * INCREMENTS_PER_THREAD) {
        std::cout << "Result is correct!" << std::endl;
    } else {
        std::cout << "Error: Race condition likely occurred!" << std::endl;
    }

    return 0;
}

编译与运行:

bash 复制代码
# Compile using g++ (or gcc if it were pure C)
# Link with pthread library for std::thread and potentially needed by semaphore implementation
g++ semaphore_example.cpp -o semaphore_example -pthread

# Run the executable
./semaphore_example

预期输出:

程序会创建多个线程,每个线程对共享计数器执行大量递增操作。由于信号量的保护,最终的 shared_counter 值应该等于 NUM_THREADS * INCREMENTS_PER_THREAD。如果没有信号量保护(注释掉 sem_waitsem_post),最终结果几乎肯定会小于预期值,因为会发生竞态条件。

6. 信号量的主要应用场景

  1. 互斥访问 (Mutual Exclusion): 使用初始值为 1 的二值信号量来保护临界区,确保同一时间只有一个线程/进程能访问共享资源或执行某段代码,功能类似互斥锁(Mutex)。
  2. 资源计数: 使用初始值为 N 的计数信号量来管理 N 个相同的资源(如数据库连接池中的连接、线程池中的工作线程等)。需要资源的线程执行 P 操作,释放资源的线程执行 V 操作。
  3. 同步 (Synchronization): 协调不同线程/进程的执行顺序。例如,一个线程(生产者)产生数据后执行 V 操作,另一个线程(消费者)在执行 P 操作时等待,直到有数据可用。

7. 注意事项与最佳实践

  1. 成对使用 sem_waitsem_post: 在保护临界区的场景下,每个 sem_wait 都必须有对应的 sem_post。忘记 sem_post 会导致资源永久锁定(死锁的一种形式),而错误地多调用 sem_post 会破坏互斥性。
  2. 初始化与销毁: 确保在使用前正确调用 sem_init 初始化信号量,并在不再需要时调用 sem_destroy 销毁它。对于进程间共享的信号量,销毁逻辑需要特别注意。
  3. 错误检查: 务必检查 sem_init, sem_wait, sem_trywait, sem_timedwait, sem_post, sem_destroy 等函数的返回值,并在失败时根据 errno 进行适当的错误处理。
  4. 处理 EINTR: sem_waitsem_timedwait 可能会被信号中断(返回 -1 且 errnoEINTR)。健壮的程序应该捕获这种情况并通常重新尝试等待操作。
  5. 死锁 (Deadlock): 当多个线程/进程相互等待对方持有的信号量时,会发生死锁。设计锁的获取顺序是避免死锁的关键策略之一。例如,总是按相同的固定顺序获取多个信号量。
  6. 避免在信号处理函数中使用 sem_wait: 信号处理函数的执行环境受限。在信号处理函数中调用可能阻塞的函数(如 sem_wait)通常是不安全的,可能导致
相关推荐
娃哈哈哈哈呀3 分钟前
组件通信-mitt
前端·javascript·vue.js
wuhen_n13 分钟前
鼠标悬浮特效:常见6种背景类悬浮特效
前端·css·css3·html5
娃哈哈哈哈呀20 分钟前
组件通信-v-model
java·服务器·前端
Johny_Zhao39 分钟前
Oracle、MySQL、SQL Server、PostgreSQL、Redis 五大数据库的区别
linux·redis·sql·mysql·信息安全·oracle·云计算·shell·yum源·系统运维
杜大哥1 小时前
Linux:如何查看Linux服务器的磁盘、CPU、内存信息?
linux·运维·服务器
ApiHug1 小时前
ApiHug SDK 1.3.5 Vue 框架 - 预览版
前端·javascript·vue.js·spring·vue·apihug·apismart
mljy.1 小时前
Linux《进程概念(下)》
linux
旺代1 小时前
React Router
前端·javascript·react.js
进取星辰1 小时前
18、状态库:中央魔法仓库——React 19 Zustand集成
前端·react.js·前端框架
陌上花开缓缓归以2 小时前
linux netlink实现用户态和内核态数据交互
linux·单片机