驱动开发-并发与竞争04信号量
信号量
信号量是 Linux 内核中一种允许进程进入睡眠状态等待资源的同步机制。它与自旋锁的“忙等待”形成鲜明对比,适用于**临界区执行时间可能较长、或者执行过程中可能发生阻塞(睡眠)**的场景。Linux 内核也提供了信号量机制,信号量常常用于控制对共享资源的访问。
其核心原理基于一个计数器和一个等待队列:
- 计数器 (
count
):- 表示可用资源的数量。
- 当
count > 0
时,表示有资源可用,进程可以立即获取资源(减少计数器)并继续执行。 - 当
count = 0
时,表示资源已被占用完,后续试图获取资源的进程需要睡眠等待。 - 计数器初始值决定了信号量的类型:
- 初始值 = 1: 称为互斥信号量 (Mutex Semaphore) 或二进制信号量 (Binary Semaphore)。这是最常用的类型,用于实现互斥访问,保证同一时刻只有一个进程可以进入临界区。它本质上可以当作一个允许睡眠的锁来用。
- 初始值 = N (N > 1): 称为计数信号量 (Counting Semaphore)。用于控制对一类有多个实例的资源(如 N 个空闲缓冲区、N 个设备槽位)的并发访问。最多允许 N 个进程同时访问该资源。
- 等待队列 (
wait_list
):- 当进程尝试获取信号量 (
down
操作) 但计数器为 0 时,该进程会被放入信号量的等待队列中。 - 进程状态被置为
TASK_UNINTERRUPTIBLE
或TASK_INTERRUPTIBLE
(取决于调用的 API),主动放弃 CPU,进入睡眠状态。 - 内核调度器会选择其他就绪状态的进程运行。
- 当进程尝试获取信号量 (
down
操作 (P 操作, 获取信号量):- 进程尝试获取信号量。
- 原子地检查计数器
count
:- 如果
count > 0
,则将count
减 1,进程成功获取信号量,继续执行临界区代码。 - 如果
count = 0
,进程将自己加入等待队列,设置状态为睡眠,然后调用调度器schedule()
让出 CPU。
- 如果
up
操作 (V 操作, 释放信号量):- 进程离开临界区后,调用
up
释放信号量。 - 原子地将计数器
count
加 1。 - 检查等待队列:
- 如果等待队列非空(有进程在睡眠等待),则唤醒队列中等待时间最长(通常是队列头部)的一个进程(或根据优先级策略唤醒)。
- 被唤醒的进程会从它之前睡眠的地方(
down
操作内部)继续执行,再次尝试减少计数器(此时因为up
增加了计数,所以通常能成功),然后进入临界区。
- 进程离开临界区后,调用
核心特性
- 睡眠等待: 这是与自旋锁最本质的区别。当资源不可用时,进程睡眠,不占用 CPU。
- 适用于长临界区/可阻塞操作: 因为进程在等待时可以睡眠,所以临界区代码执行时间长、或者包含可能阻塞的操作(如 I/O 等待、磁盘读写、获取其他信号量/互斥锁、
kmalloc(GFP_KERNEL)
内存分配)是允许的。 - 互斥与计数: 通过初始计数实现互斥或资源池管理。
- 可能导致睡眠:
down
操作可能导致调用者进程睡眠。 - 进程上下文: 信号量只能在进程上下文中使用(因为睡眠操作需要进程上下文)。不能在中断上下文、软中断、Tasklet 等不可睡眠的上下文中使用信号量的
down
操作! (但up
操作可以在中断上下文调用),因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。 - 开销: 睡眠和唤醒操作涉及进程状态切换、调度器决策、等待队列管理等,开销比自旋锁大。因此,在临界区非常短且不会阻塞的场景下,自旋锁通常是更好的选择。
核心API函数
1. 初始化信号量
1 |
|
示例:
1
2
3DEFINE_SEMAPHORE(my_mutex); // 初始化一个互斥信号量 my_mutex
struct semaphore my_count_sem;
sema_init(&my_count_sem, 5); // 初始化一个计数信号量,初始有5个资源
2. 获取信号量 (down
操作 - P 操作)
1 | // 基本不可中断的 down 操作 (推荐) |
- 选择指南:
down()
: 最简单,但进程在等待时不能被信号唤醒。适用于必须完成等待的场景。down_interruptible()
: 最常用。允许进程在等待时被信号中断(如用户按了 Ctrl+C)。驱动程序等需要处理用户交互的场景必须使用此版本,并检查返回值。如果返回-EINTR
,通常需要向上层返回-ERESTARTSYS
。down_trylock()
: 不想等待时使用。通常用于实现非阻塞操作或避免死锁检查。down_timeout()
: 需要限制等待时间的场景。
3. 释放信号量 (up
操作 - V 操作)
1 | void up(struct semaphore *sem); |
- 这个函数相对简单:增加计数器
count
,并唤醒等待队列中的一个进程(如果存在)。 - 可以在进程上下文和中断上下文中安全调用。
读写信号量 (struct rw_semaphore
)
类似于读写自旋锁,内核也提供了读写信号量,允许多个读者同时持有“读锁”,但写者必须独占持有“写锁”。
变量类型:
struct rw_semaphore
初始化:
1
void init_rwsem(struct rw_semaphore *sem);
读者操作:
1
2
3void down_read(struct rw_semaphore *sem); // 获取读锁 (可睡眠)
int down_read_trylock(struct rw_semaphore *sem); // 尝试获取读锁 (非阻塞)
void up_read(struct rw_semaphore *sem); // 释放读锁写者操作:
1
2
3
4void down_write(struct rw_semaphore *sem); // 获取写锁 (可睡眠)
int down_write_trylock(struct rw_semaphore *sem); // 尝试获取写锁 (非阻塞)
void up_write(struct rw_semaphore *sem); // 释放写锁
void downgrade_write(struct rw_semaphore *sem); // 将写锁降级为读锁 (释放写锁同时持有读锁)特性: 适用于“读多写少”的场景,且临界区较长或可能阻塞。同样遵循信号量的睡眠等待特性。
实验:
我们实验对LED的设备文件进行管理,在有单元对其进行访问的时候,其它所有单元都不能对其进行访问,主要的原理如下:
本次实验我们采用的是互斥信号量的原理进行编程的,我们将信号量初始化为1
1
sema_init(&leddev->sem,1);/* 初始化信号量 */
获取信号量,获取信号量,进入休眠状态的进程可以被信号打断 。在open函数中申请信号量,可以使用
down
函数,也可以使用down_interruptible
函数。如果信号量值大于等于 1 就表示可用,那么应用程序就会开始使用 LED 灯。如果信号量值为 0 就表示应用程序不能使用 LED 灯,此时应用程序就会进入到休眠状态。等到信号量值大于 1 的时候应用程序就会唤醒,申请信号量,获取 LED 灯使用权。1
2
3if(down_interruptible(&leddev->sem)){
return -ERESTARTSYS;
}释放信号量
1
up(&leddev->sem);
代码:
驱动代码:
1 |
|
应用代码:
1 |
|
实验效果:
当信号量 sem 为 1 的时候表示 LED 灯还没有被使用,如果应用程序 A 要使用LED 灯,先调用 open 函数打开/dev/gpioled
,这个时候会获取信号量 sem,获取成功以后 sem 的值减 1 变为 0。如果此时应用程序 B 也要使用 LED 灯,调用 open 函数打开/dev/gpioled
就会因为信号量无效(值为 0)而进入休眠状态。当应用程序 A 运行完毕,调用 release 函数关闭/dev/gpioled
的时候就会释放信号量 sem,此时信号量 sem 的值就会加 1,变为 1。信号量 sem 再次有效,表示其他应用程序可以使用 LED 灯了,此时在休眠状态的应用程序 B 就会获取到信号量 sem,获取成功以后就开始使用 LED 灯。