信号量
信号量(Semaphore)是一种用于同步和互斥的机制,能够控制对共享资源的访问,广泛应用于多线程和多进程编程中。应用程序中使用的信号量是基于内核信号量实现的。
信号量的基本概念
信号量是一个计数器,用于管理对共享资源的访问。它支持两种基本操作:
- P操作(
**<font style="color:rgb(64, 64, 64);">sem_wait</font>**):尝试获取信号量。如果信号量的值大于 0,则将其减 1;否则,调用者会被阻塞,直到信号量的值变为正数。 - V操作(
**<font style="color:rgb(64, 64, 64);">sem_post</font>**):释放信号量。将信号量的值加 1,并唤醒等待该信号量的线程或进程。
信号量是由荷兰计算机科学家提出的,P/V是荷兰语的缩写。
信号量可以分为:
- 二进制信号量(Binary Semaphore),又称互斥信号量:值只有 0 和 1,用于互斥。
- 计数信号量(Counting Semaphore):值可以是任意非负整数,用于资源池管理。
内核信号量
信号量数据结构
内核中的信号量是 计数信号量(Counting Semaphore),它由一个 计数值(count) 和 等待队列 组成:
count > 0:资源可用,线程/进程可以继续执行;count == 0:资源已被占用,新的进程/线程必须阻塞;count < 0:表示有多个进程/线程正在等待该资源。
内核中的信号量定义如下:
struct semaphore {
raw_spinlock_t lock; // 保护其他成员
unsigned int count; // 表示还可以允许多少个进程进入临界区
struct list_head wait_list; // 等待进入临界区的进程队列
};
信号量的操作
内核中的信号量提供以下几个主要操作:
- 初始化信号量
- 获取信号量(P操作)
- 释放信号量(V操作)
初始化信号量
宏定义初始化信号量的方法:
__SEMAPHORE_INITIALIZER(name, n),指定信号量名称和计数值,允许n个进程同时进入临界区。DEFINE_SEMAPHORE(name),初始化一个互斥信号量。
#define __SEMAPHORE_INITIALIZER(name, n) \
{ \
.lock = __RAW_SPIN_LOCK_UNLOCKED((name).lock), \
.count = n, \
.wait_list = LIST_HEAD_INIT((name).wait_list), \
}
#define DEFINE_SEMAPHORE(name) \
struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)
函数初始化信号量的方法:
static inline void sema_init(struct semaphore *sem, int val)
{
static struct lock_class_key __key;
*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}
将信号量的计数器设置为指定的初始值val,并初始化等待队列。
获取信号量(P操作)
<font style="color:rgb(64, 64, 64);">down()</font> 操作用于获取信号量。如果信号量的 <font style="color:rgb(64, 64, 64);">count > 0</font>,则直接减少 <font style="color:rgb(64, 64, 64);">count</font>;否则,当前进程会被阻塞并加入等待队列。
伪代码如下:
void down(struct semaphore *sem) {
unsigned long flags;
// 获取自旋锁,保护对信号量的操作
raw_spin_lock_irqsave(&sem->lock, flags);
if (sem->count > 0) {
// 如果 count > 0,直接减少 count
sem->count--;
} else {
// 如果 count == 0,将当前进程加入等待队列并阻塞
struct task_struct *task = current;
list_add_tail(&task->wait_entry, &sem->wait_list); // 加入等待队列
set_current_state(TASK_UNINTERRUPTIBLE); // 设置进程状态为不可中断睡眠
raw_spin_unlock_irqrestore(&sem->lock, flags); // 释放自旋锁
schedule(); // 让出 CPU,进入睡眠状态
// 当进程被唤醒后,重新获取自旋锁
raw_spin_lock_irqsave(&sem->lock, flags);
list_del(&task->wait_entry); // 从等待队列中移除
}
// 释放自旋锁
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
- 自旋锁的作用:
- 在修改
<font style="color:rgb(64, 64, 64);">count</font>或操作<font style="color:rgb(64, 64, 64);">wait_list</font>时,必须持有自旋锁。 - 自旋锁通过
<font style="color:rgb(64, 64, 64);">raw_spin_lock_irqsave</font>和<font style="color:rgb(64, 64, 64);">raw_spin_unlock_irqrestore</font>实现,同时禁用本地 CPU 的中断。
- 在修改
- 等待队列的作用:
- 当
<font style="color:rgb(64, 64, 64);">count == 0</font>时,当前进程会被加入等待队列,并进入睡眠状态(<font style="color:rgb(64, 64, 64);">TASK_UNINTERRUPTIBLE</font>)。 - 当信号量被释放时(其他进程调用
<font style="color:rgb(64, 64, 64);">up()</font>释放信号量),等待队列<font style="color:rgb(64, 64, 64);">wait_list</font>中队首的进程会被唤醒。
- 当
释放信号量(V操作)
<font style="color:rgb(64, 64, 64);">up()</font> 操作用于释放信号量。它会增加 <font style="color:rgb(64, 64, 64);">count</font>,并唤醒等待队列中的一个进程。
伪代码如下:
void up(struct semaphore *sem) {
unsigned long flags;
// 获取自旋锁,保护对信号量的操作
raw_spin_lock_irqsave(&sem->lock, flags);
if (list_empty(&sem->wait_list)) {
// 如果等待队列为空,直接增加 count
sem->count++;
} else {
// 如果等待队列不为空,唤醒队列中的第一个进程
struct task_struct *task = list_first_entry(&sem->wait_list, struct task_struct, wait_entry);
wake_up_process(task); // 唤醒进程
}
// 释放自旋锁
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
- 唤醒进程:
- 如果等待队列不为空,
<font style="color:rgb(64, 64, 64);">up</font>操作会唤醒队列中的第一个进程。 - 被唤醒的进程会从
<font style="color:rgb(64, 64, 64);">down</font>操作中的<font style="color:rgb(64, 64, 64);">schedule()</font>返回,并继续执行。
- 如果等待队列不为空,
信号量的特性
基于上述实现,信号量具有以下特性:
- 原子性:
- 通过自旋锁确保对
<font style="color:rgb(64, 64, 64);">count</font>和<font style="color:rgb(64, 64, 64);">wait_list</font>的操作是原子的。
- 通过自旋锁确保对
- 阻塞机制:
- 当
<font style="color:rgb(64, 64, 64);">count == 0</font>时,请求信号量的进程会被阻塞并加入等待队列。
- 当
- 唤醒机制:
- 当信号量被释放时,等待队列中的进程会被唤醒。
信号量的变体
Linux 内核提供了多种信号量变体,以适应不同的使用场景:
**<font style="color:rgb(64, 64, 64);">down_interruptible</font>**:- 类似于
<font style="color:rgb(64, 64, 64);">down</font>,但可以被信号中断(返回<font style="color:rgb(64, 64, 64);">-EINTR</font>)。
- 类似于
**<font style="color:rgb(64, 64, 64);">down_killable</font>**:- 类似于
<font style="color:rgb(64, 64, 64);">down</font>,但可以被kill信号中断
- 类似于
**<font style="color:rgb(64, 64, 64);">down_trylock</font>**:- 非阻塞版本,如果
<font style="color:rgb(64, 64, 64);">count == 0</font>,立即返回失败。
- 非阻塞版本,如果
**<font style="color:rgb(64, 64, 64);">down_timeout</font>**:- 带有超时机制的
<font style="color:rgb(64, 64, 64);">down</font>操作,如果超时仍未获取到信号量,返回失败。
- 带有超时机制的
信号量的应用场景
信号量在内核中广泛应用于以下场景:
- 资源管理:
- 用于管理有限的资源(如内存池、设备访问)。
- 同步机制:
- 用于多个进程或线程之间的同步。
- 互斥锁的实现:
- 二进制信号量(
<font style="color:rgb(64, 64, 64);">count = 1</font>)可以用作互斥锁。
- 二进制信号量(
与 <font style="color:rgb(64, 64, 64);">mutex</font> 的区别
| 特性 | Semaphore | Mutex |
|---|---|---|
| 计数值 | >=0可多个进程/线程同时获取 | 只能是 0或 1 |
| 是否允许多个进程持有 | 是 | 否 |
| 释放机制 | 任何进程 都能 up()释放,即使它没有获取过信号量 | 只能由持有者释放 |
| 递归锁 | 不支持,递归执行P操作会死锁 | 支持(posix可以设置mutex属性支持递归,内核mutex不支持递归) |
| 适用场景 | 资源计数(如生产者-消费者) | 互斥访问(如临界区保护) |
如果需要纯粹的互斥锁,应该使用 **mutex**,而不是 **semaphore**。
Sample
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/semaphore.h>
static struct semaphore my_sema;
int my_thread(void *arg) {
down(&my_sema); // 获取信号量
printk(KERN_INFO "Thread running...\n");
msleep(5000);
up(&my_sema); // 释放信号量
return 0;
}
static int __init my_init(void) {
sema_init(&my_sema, 1); // 初始化信号量,初始值 1
kthread_run(my_thread, NULL, "my_thread");
return 0;
}
static void __exit my_exit(void) {
printk(KERN_INFO "Module exit\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
用户空间的信号量
应用程序使用的信号量,实现上可以依赖内核信号量,也可以基于内核其它互斥机制实现,甚至可以直接在用户空间实现
Linux 提供了两种主要的信号量实现:
- System V 信号量:较老的实现,功能强大但接口复杂。
- POSIX 信号量:较新的实现,接口简单且更符合现代编程需求。
POSIX 信号量是更推荐的方式,因为它接口简单且易于使用。POSIX信号量在实现上与内核中的信号量有所不同,但它们的基本原理是相同的,都是用于同步多线程或多进程访问共享资源。POSIX 信号量分为两种:
- 进程间信号量(命名信号量):基于文件系统实现的信号量(实际实现依赖于内核的共享内存,而不是普通的文件系统存储),用于不同进程间的同步。
- 进程内信号量(未命名信号量):基于内存实现的信号量,用于同一进程中的线程同步。
(1)命名信号量(进程间同步)
命名信号量通过一个名字标识,可以在不同进程之间共享。
在 Linux 中,POSIX 命名信号量 会以 伪文件 形式出现在 <font style="color:rgb(64, 64, 64);">/dev/shm/sem.<name></font>,但它们并不是真正的文件,而是由 内核管理的共享内存对象。
ls -l /dev/shm/sem.*
示例输出:
-rw------- 1 user user 32 Feb 13 12:00 /dev/shm/sem.my_sem
- 这个 “文件” 只是 POSIX 共享内存的标识,但数据本身在内核空间管理。
- 进程间可以通过
**sem_open()**、**sem_wait()**、**sem_post()**等方法访问这个共享信号量。
Sample
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <unistd.h>
int main() {
const char *sem_name = "/my_named_sem";
sem_t *sem;
// 创建或打开一个命名信号量
sem = sem_open(sem_name, O_CREAT, 0644, 1); // 初始值为 1
if (sem == SEM_FAILED) {
perror("sem_open failed");
return 1;
}
printf("Semaphore opened successfully\n");
// 获取信号量(P 操作)
if (sem_wait(sem) < 0) {
perror("sem_wait failed");
return 1;
}
printf("Critical section start\n");
sleep(2); // 模拟临界区操作
printf("Critical section end\n");
// 释放信号量(V 操作)
if (sem_post(sem) < 0) {
perror("sem_post failed");
return 1;
}
// 关闭信号量
sem_close(sem);
// 删除信号量
sem_unlink(sem_name);
return 0;
}
关键函数
<font style="color:rgb(64, 64, 64);">sem_open</font>:创建或打开一个命名信号量。<font style="color:rgb(64, 64, 64);">sem_wait</font>:获取信号量(P 操作),如果信号量为 0,则阻塞。<font style="color:rgb(64, 64, 64);">sem_post</font>:释放信号量(V 操作),增加信号量的值。<font style="color:rgb(64, 64, 64);">sem_close</font>:关闭 进程当前打开的信号量句柄,但不会删除信号量。<font style="color:rgb(64, 64, 64);">sem_unlink</font>:删除命名信号量(从<font style="color:rgb(64, 64, 64);">/dev/shm/</font>中移除)。
(2)未命名信号量(线程间同步)
未命名信号量通常用于线程间同步,需要放在共享内存中。未命名信号量其实也可以用于进程间通信,但要将信号量放在共享内存中,使用比较麻烦,一般不这样用。
Sample
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
sem_t sem;
void* thread_func(void* arg) {
printf("Thread waiting for semaphore\n");
sem_wait(&sem); // 获取信号量
printf("Thread entered critical section\n");
sleep(2); // 模拟临界区操作
printf("Thread leaving critical section\n");
sem_post(&sem); // 释放信号量
return NULL;
}
int main() {
pthread_t tid;
// 初始化未命名信号量
if (sem_init(&sem, 0, 1) < 0) { // 初始值为 1
perror("sem_init failed");
return 1;
}
// 创建线程
if (pthread_create(&tid, NULL, thread_func, NULL) < 0) {
perror("pthread_create failed");
return 1;
}
// 主线程获取信号量
printf("Main thread waiting for semaphore\n");
sem_wait(&sem);
printf("Main thread entered critical section\n");
sleep(2); // 模拟临界区操作
printf("Main thread leaving critical section\n");
sem_post(&sem);
// 等待线程结束
pthread_join(tid, NULL);
// 销毁信号量
sem_destroy(&sem);
return 0;
}
关键函数
<font style="color:rgb(64, 64, 64);">sem_init</font>:初始化未命名信号量。<font style="color:rgb(64, 64, 64);">sem_wait</font>:获取信号量(P 操作)。<font style="color:rgb(64, 64, 64);">sem_post</font>:释放信号量(V 操作)。<font style="color:rgb(64, 64, 64);">sem_destroy</font>:销毁未命名信号量。
几个需要注意的问题:
sem_wait()是否会被信号(SIGINT)打断?
不确定,取决于操作系统实现和具体的使用场景。
- 任何进程都可以通过
sem_post()释放信号量,即使该进程没有获取到信号量。
虽然没有限制,但不建议这样使用。
参考资料
- Professional Linux Kernel Architecture,Wolfgang Mauerer
- Linux内核深度解析,余华兵
- Linux设备驱动开发详解,宋宝华
- linux kernel 4.12

1224

被折叠的 条评论
为什么被折叠?



