在多线程或多进程并发编程的领域中,确保对共享资源的安全访问和协调不同执行单元的同步至关重要。信号量(Semaphore)作为经典的同步原语之一,在 Linux 系统中扮演着核心角色。本文将深入探讨 Linux 环境下 POSIX 信号量的概念、工作原理、API 使用、示例代码、流程图及注意事项。
信号量是由荷兰计算机科学家艾兹格·迪科斯彻(Edsger Dijkstra)在 1965 年左右提出的一个同步机制。本质上,信号量是一个非负整数计数器,它被用于控制对一组共享资源的访问。它主要支持两种原子操作:
wait()
, down()
, acquire()
。此操作会检查信号量的值。
signal()
, up()
, post()
, release()
。此操作会将信号量的值加 1。
核心思想: 信号量的值代表了当前可用资源的数量。当一个进程/线程需要使用资源时,它执行 P 操作;当它释放资源时,执行 V 操作。
类比:
Linux 主要支持两种信号量实现:
semget()
, semop()
, semctl()
。/mysemaphore
)或未命名信号量(通常在同一进程的线程间或父子进程间共享,存在于内存中)。本文将重点关注更常用且推荐的 POSIX 未命名信号量。
使用 POSIX 信号量需要包含头文件
。
sem_init()
- 初始化未命名信号量#include
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;对于计数信号量,根据可用资源数量初始化。返回值:
errno
。常见的 errno
包括 EINVAL
(value 超过 SEM_VALUE_MAX
),ENOSYS
(不支持进程间共享)。sem_destroy()
- 销毁未命名信号量#include
int sem_destroy(sem_t *sem);
功能: 销毁由 sem_init()
初始化的未命名信号量 sem
。销毁一个正在被其他线程等待的信号量会导致未定义行为。只有在确认没有线程再使用该信号量后才能销毁。
参数:
sem_t *sem
: 指向要销毁的信号量对象的指针。返回值:
errno
(如 EINVAL
表示 sem
不是一个有效的信号量)。sem_wait()
- 等待(P 操作/减 1)#include
int sem_wait(sem_t *sem);
功能: 对信号量 sem
执行 P 操作(尝试减 1)。
sem_post()
之后)或收到一个信号。参数:
sem_t *sem
: 指向要操作的信号量对象的指针。返回值:
errno
。
EINVAL
: sem
不是一个有效的信号量。EINTR
: 操作被信号中断。应用程序通常需要检查 errno
并重新尝试 sem_wait()
。sem_trywait()
- 非阻塞等待#include
int sem_trywait(sem_t *sem);
功能: sem_wait()
的非阻塞版本。
errno
设置为 EAGAIN
,调用线程不会被阻塞。参数:
sem_t *sem
: 指向要操作的信号量对象的指针。返回值:
errno
。
EAGAIN
: 信号量当前为 0,无法立即减 1。EINVAL
: sem
不是一个有效的信号量。sem_timedwait()
- 带超时的等待#include
#include
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
功能: 类似 sem_wait()
,但带有超时限制。
abs_timeout
指定的绝对时间(基于 CLOCK_REALTIME
)到达之前信号量仍未增加,则函数返回错误。参数:
sem_t *sem
: 指向要操作的信号量对象的指针。const struct timespec *abs_timeout
: 指向一个 timespec
结构体,指定了阻塞等待的绝对超时时间点。struct timespec { time_t tv_sec; long tv_nsec; };
。返回值:
errno
。
ETIMEDOUT
: 在超时时间到达前未能成功将信号量减 1。EINVAL
: sem
无效或 abs_timeout
无效。EINTR
: 操作被信号中断。sem_post()
- 释放(V 操作/加 1)#include
int sem_post(sem_t *sem);
功能: 对信号量 sem
执行 V 操作(原子地将其值加 1)。如果有任何线程/进程因此信号量而被阻塞,则其中一个会被唤醒。
参数:
sem_t *sem
: 指向要操作的信号量对象的指针。返回值:
errno
。
EINVAL
: sem
不是一个有效的信号量。EOVERFLOW
: 信号量的值增加将超过 SEM_VALUE_MAX
。sem_getvalue()
- 获取信号量当前值#include
int sem_getvalue(sem_t *sem, int *sval);
功能: 获取信号量 sem
的当前值,并将其存储在 sval
指向的整数中。注意:获取到的值可能在函数返回后立即就过时了(因为其他线程可能同时修改了信号量),主要用于调试或特定场景。
参数:
sem_t *sem
: 指向要查询的信号量对象的指针。int *sval
: 指向用于存储信号量当前值的整数的指针。返回值:
errno
(如 EINVAL
)。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):
sem_wait
。sem_post 流程 (Thread B):
sem_post
。sem_wait
的检查点,此时信号量值已大于 0,它将成功减 1 并继续执行。这个例子演示了如何使用二值信号量(初始化为 1)来实现类似互斥锁的功能,保护一个共享计数器,防止多个线程同时修改导致竞态条件。
#include
#include
#include
#include // For POSIX semaphores
#include // 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;
}
编译与运行:
# 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_wait
和 sem_post
),最终结果几乎肯定会小于预期值,因为会发生竞态条件。
sem_wait
和 sem_post
: 在保护临界区的场景下,每个 sem_wait
都必须有对应的 sem_post
。忘记 sem_post
会导致资源永久锁定(死锁的一种形式),而错误地多调用 sem_post
会破坏互斥性。sem_init
初始化信号量,并在不再需要时调用 sem_destroy
销毁它。对于进程间共享的信号量,销毁逻辑需要特别注意。sem_init
, sem_wait
, sem_trywait
, sem_timedwait
, sem_post
, sem_destroy
等函数的返回值,并在失败时根据 errno
进行适当的错误处理。sem_wait
和 sem_timedwait
可能会被信号中断(返回 -1 且 errno
为 EINTR
)。健壮的程序应该捕获这种情况并通常重新尝试等待操作。sem_wait
: 信号处理函数的执行环境受限。在信号处理函数中调用可能阻塞的函数(如 sem_wait
)通常是不安全的,可能导致