在 Linux 设备驱动开发中,阻塞机制
是处理资源暂时不可用 (如设备未准备好数据、缓冲区满等)的核心手段。驱动程序可以将被阻塞的进程设置成休眠状态 ,然后,在资源可用后,再将该进程唤醒。
在 Linux 驱动开发中,休眠(等待)机制分为 简单休眠
(使用 wait_event系列宏)和 高级休眠
(手动使用 prepare_to_wait/finish_wait),它们的核心区别在于封装程度和灵活性。
其中,"简单休眠"由"高级休眠"封装而来,可以当做"高级休眠"的语法糖。
本质区别对比:
如何选择?
- 优先使用简单休眠,比如:只需要等待单一条件、不需要在等待过程中执行额外操作;
- 当需要实现非标准的唤醒条件(如同时等待多个事件)、需要更精细的性能优化时必须使用高级休眠;
- 现代内核更推荐使用 wait_event_interruptible 等封装宏。
简单休眠
最佳框架:
c
ssize_t scullpipe_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
struct scull_pipe *dev = filp->private_data;
int ret;
// 检查参数
if (count == 0)
return 0;
if (mutex_lock_interruptible(&dev->lock))
return -ERESTARTSYS;
// 如果缓冲区为空,进入休眠等待
while (read_buffer_empty(dev)) {
mutex_unlock(&dev->lock); // 必须释放锁再休眠
if (filp->f_flags & O_NONBLOCK)
return -EAGAIN;
ret = wait_event_interruptible(dev->inq, !read_buffer_empty(dev));
if (ret == -ERESTARTSYS) // 被信号打断
return -ERESTARTSYS;
// 被唤醒后再次获取锁再判断
if (mutex_lock_interruptible(&dev->lock))
return -ERESTARTSYS;
}
// 此时缓冲区有数据,可以读取
ret = copy_data_to_user(dev, buf, count);
mutex_unlock(&dev->lock);
return ret;
}
高级休眠
最佳框架:
c
ssize_t mydev_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
struct my_device *dev = filp->private_data;
DEFINE_WAIT_FUNC(wait, default_wake_function);
ssize_t ret = 0;
if (mutex_lock_interruptible(&dev->lock))
return -ERESTARTSYS;
while (!data_available(dev)){
mutex_unlock(&dev->lock);
/* 非阻塞模式 */
if (filp->f_flags & O_NONBLOCK)
return -EAGAIN;
if (prepare_to_wait_event(&dev->inq, &wait, TASK_INTERRUPTIBLE))
return -ERESTARTSYS; // 信号
/* 在当前进程挂入等待队列后,再次确认条件*/
if (!data_available(dev))
schedule();
if (signal_pending(current)) {
ret = -ERESTARTSYS; // 信号
break;
}
if (mutex_lock_interruptible(&dev->lock)){
ret = -ERESTARTSYS;
break;
}
}
finish_wait(&dev->inq, &wait);
if(ret)
return ret;
// 现在有锁,并且数据有效,下面进行读操作
count = min(count, xxx);
if (copy_to_user(buf, dev->rp, count) )
{
mutex_unlock(&dev->lock);
return -EFAULT;
}
mutex_unlock(&dev->lock);
return count;
}
-
DEFINE_WAIT_FUNC(wait, default_wake_function)
:定义一个等待队列项
(wait_queue_entry),并初始化其唤醒回调函数 。
为当前进程 创建一个"代表自己"的等待队列项 (wait
),并指定当条件满足(通常是资源可用)时如何唤醒自己(通过default_wake_function
函数)。c// 源码 (include/linux/wait.h): #define DEFINE_WAIT_FUNC(name, function) \ wait_queue_entry_t name = { \ .private = current, \ .func = function, \ .entry = LIST_HEAD_INIT((name).entry), \ }
-
signal_pending(current)
:检查当前进程 (current) 是否有待处理 的信号。实现可中断的睡眠 (TASK_INTERRUPTIBLE)。避免进程在需要响应信号(如 Ctrl+C)时被无谓地阻塞。- 返回非零值(
true
)表示有挂起的信号需要处理。此时,进程不应继续睡眠,而应退出等待循环并返回 -ERESTARTSYS 错误码给用户空间(让用户空间有机会处理信号)。 - 通常在调用 schedule() 让出 CPU 之前检查,避免信号丢失导致多余阻塞。这也符合现代 Linux 关于"信号优先响应"的设计原则。
- 返回非零值(
-
prepare_to_wait_event(&dev->inq, &wait, state)
:准备当前进程 进入睡眠状态 。核心任务是将等待项 (&wait) 添加到设备的等待队列头 (&dev->inq),并设置 当前进程的状态 (根据参数state
,通常是TASK_INTERRUPTIBLE
, 表示可被信号唤醒的睡眠)。
正式将当前进程"注册"到设备的等待队列 (参数dev->inq
),并标记为睡眠状态,为安全让出 CPU 做准备。- 它将等待项 (
wait
) 安全地(加锁)添加到等待队列 ()。
c// 简化逻辑,kernel/sched/wait.c void prepare_to_wait_event(wait_queue_head_t *wq_head, wait_queue_entry_t *wait, int state) { unsigned long flags; wait->flags &= ~WQ_FLAG_EXCLUSIVE; // 通常是非独占等待 spin_lock_irqsave(&wq_head->lock, flags); // 获取队列锁 if (list_empty(&wait->entry)) // 防止重复添加 __add_wait_queue(wq_head, wait); // 将等待项加入队列尾部 set_current_state(state); // 设置当前进程状态 (TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE) spin_unlock_irqrestore(&wq_head->lock, flags); // 释放队列锁 }
- 它将等待项 (
-
schedule()
:主动让出 CPU,触发进程调度。当前进程进入睡眠状态,内核选择另一个就绪进程运行。- 调用
schedule()
时,进程状态必须已经是TASK_INTERRUPTIBLE
或TASK_UNINTERRUPTIBLE
(由prepare_to_wait_event
设置)。调度器根据此状态将进程移出运行队列。 - 进程在此处"睡着"。
- 当进程被唤醒时(通常是因为等待的条件满足或有信号到达),执行会从
schedule()
调用之后的下一条语句继续。
- 调用
-
finish_wait(&dev->inq, &wait)
:清理等待状态。将等待项从等待队列中移除,并将当前进程状态设置回 TASK_RUNNING。c// 简化逻辑,kernel/sched/wait.c void finish_wait(wait_queue_head_t *wq_head, wait_queue_entry_t *wait) { unsigned long flags; set_current_state(TASK_RUNNING); // 重要:恢复为可运行状态 if (!list_empty_careful(&wait->entry)) { // 如果还在队列里 spin_lock_irqsave(&wq_head->lock, flags); list_del_init(&wait->entry); // 从等待队列中移除 spin_unlock_irqrestore(&wq_head->lock, flags); } }