Linux 阻塞等待框架

在 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;
}
  1. 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),    \
        }
  2. signal_pending(current):检查当前进程 (current) 是否有待处理 的信号。实现可中断的睡眠 (TASK_INTERRUPTIBLE)。避免进程在需要响应信号(如 Ctrl+C)时被无谓地阻塞。

    • 返回非零值( true )表示有挂起的信号需要处理。此时,进程不应继续睡眠,而应退出等待循环并返回 -ERESTARTSYS 错误码给用户空间(让用户空间有机会处理信号)。
    • 通常在调用 schedule() 让出 CPU 之前检查,避免信号丢失导致多余阻塞。这也符合现代 Linux 关于"信号优先响应"的设计原则。
  3. 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); // 释放队列锁
    }
  4. schedule():主动让出 CPU,触发进程调度。当前进程进入睡眠状态,内核选择另一个就绪进程运行。

    • 调用 schedule() 时,进程状态必须已经是 TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE(由 prepare_to_wait_event 设置)。调度器根据此状态将进程移出运行队列。
    • 进程在此处"睡着"。
    • 当进程被唤醒时(通常是因为等待的条件满足或有信号到达),执行会从 schedule() 调用之后的下一条语句继续。
  5. 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);
        }
    }
相关推荐
青竹易寒2 小时前
Linux命令技术笔记-sed+awk命令详解
linux·运维·服务器
Kiri霧2 小时前
Kotlin泛型约束
android·linux·windows·kotlin
试着3 小时前
零基础学习性能测试第二章-linux服务器监控:CPU监控
linux·服务器·学习·零基础·性能测试·cpu监控
绵绵细雨中的乡音3 小时前
Linux进程通信——共享内存:System V 进程间通信的极速方案
linux·运维·服务器
老马啸西风4 小时前
windows wsl ubuntu 如何安装 open-jdk8
linux·windows·ubuntu·docker·容器·k8s·kvm
小白的程序空间6 小时前
Anaconda Prompt中删除库虚拟环境【保姆级教程】
linux·开发语言·python
努力自学的小夏6 小时前
RK3568 Linux驱动学习——SDK安装编译
linux·arm开发·笔记·学习
basketball6166 小时前
Linux C 信号操作
linux·c语言·开发语言
网易独家音乐人Mike Zhou8 小时前
【Linux应用】在PC的Linux环境下通过chroot运行ARM虚拟机镜像img文件(需要依赖qemu-aarch64、不需要重新安装iso)
linux·c语言·stm32·mcu·物联网·嵌入式·iot