从一次深夜调试说起

上周三凌晨两点,实验室的示波器还亮着。同事老张拍我肩膀:“这个传感器驱动,应用程序用select等数据,怎么有时候能唤醒,有时候卡死十几秒?” 我们盯着逻辑分析仪,发现中断确实触发了,但用户层的select调用就是没返回。最终发现是驱动里的poll函数少处理了一个状态标志——这种问题在嵌入式开发中太典型了。

用户空间与内核的桥梁

先看用户空间怎么用。应用程序调用select或poll时,本质是问内核:“我关心的这几个文件描述符,有没有就绪的?” 内核会去遍历每个fd对应的驱动poll方法,收集状态信息。如果当前没数据,进程就进入睡眠,等驱动主动唤醒。

// 用户空间典型用法(简化版)
fd_set readfds;
struct timeval tv = {5, 0}; // 等5秒

FD_ZERO(&readfds);
FD_SET(sensor_fd, &readfds);

int ret = select(sensor_fd + 1, &readfds, NULL, NULL, &tv);
if (ret > 0) {
    // 有数据可读
}

驱动侧的三个关键动作

驱动要实现poll,核心就三件事:定义等待队列、实现poll函数、在合适时机唤醒队列。

先看等待队列。这相当于一个“睡眠进程名单”,驱动要声明并初始化:

static DECLARE_WAIT_QUEUE_HEAD(my_waitqueue);

别在每次poll调用时临时创建队列,那样会导致等待的进程丢失。我见过有人把队列声明在函数局部变量里,结果进程永远唤不醒——这里踩过坑。

poll函数的实现细节

驱动里的poll函数模板长这样:

static unsigned int my_poll(struct file *filp, poll_table *wait)
{
    unsigned int mask = 0;
    struct my_device *dev = filp->private_data;
    
    // 这行必须调用!把当前进程加入等待队列
    poll_wait(filp, &dev->waitqueue, wait);
    
    // 检查设备状态
    if (数据就绪) {
        mask |= POLLIN | POLLRDNORM;  // 可读事件
    }
    
    if (可以写入) {
        mask |= POLLOUT | POLLWRNORM; // 可写事件  
    }
    
    // 设备异常时
    if (dev->error_flag) {
        mask |= POLLERR;
    }
    
    return mask;
}

注意:poll_wait只是注册等待队列,并不阻塞。真正的阻塞发生在用户空间的select/poll调用中。这个函数要快速检查状态并返回,别在里面做耗时操作。

唤醒的时机与方式

数据到达时(通常在中断处理函数或工作队列中),唤醒等待的进程:

// 中断上下文或进程上下文
wake_up_interruptible(&dev->waitqueue);
// 或者用wake_up_all()唤醒所有等待者

这里有个坑:唤醒后,驱动要确保数据确实已经准备好。我遇到过时序问题——先唤醒进程,但DMA传输还没完成,用户读到的是旧数据。正确的顺序是:准备数据 -> 设置状态标志 -> 唤醒。

完整驱动代码片段

来看个传感器驱动的简化示例:

struct my_dev {
    wait_queue_head_t waitq;
    atomic_t data_ready;  // 用原子变量,安全
    struct mutex lock;
    u32 sensor_data;
};

static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    struct my_dev *dev = filp->private_data;
    DEFINE_WAIT(wait);
    
    // 等待数据就绪
    while (!atomic_read(&dev->data_ready)) {
        prepare_to_wait(&dev->waitq, &wait, TASK_INTERRUPTIBLE);
        if (!atomic_read(&dev->data_ready)) {
            schedule();  // 让出CPU
        }
        finish_wait(&dev->waitq, &wait);
        
        if (signal_pending(current))  // 处理信号中断
            return -ERESTARTSYS;
    }
    
    // 拷贝数据到用户空间
    mutex_lock(&dev->lock);
    copy_to_user(buf, &dev->sensor_data, sizeof(u32));
    atomic_set(&dev->data_ready, 0);  // 重置标志
    mutex_unlock(&dev->lock);
    
    return sizeof(u32);
}

static unsigned int my_poll(struct file *filp, poll_table *wait)
{
    struct my_dev *dev = filp->private_data;
    unsigned int mask = 0;
    
    poll_wait(filp, &dev->waitq, wait);
    
    // 检查数据是否就绪
    if (atomic_read(&dev->data_ready)) {
        mask |= POLLIN | POLLRDNORM;
    }
    
    return mask;
}

// 中断处理函数(简版)
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_dev *dev = dev_id;
    
    // 读取硬件数据
    dev->sensor_data = readl(dev->reg_base + DATA_REG);
    
    atomic_set(&dev->data_ready, 1);  // 先设标志
    wake_up_interruptible(&dev->waitq);  // 后唤醒
    
    return IRQ_HANDLED;
}

调试技巧与常见陷阱

调试poll/select问题,我习惯加几个打印:

// 在poll函数里加调试信息
printk(KERN_DEBUG "poll called, mask=0x%x\n", mask);

常见陷阱:

  1. 忘记调用poll_wait:进程永远等不到唤醒
  2. 唤醒时机不对:数据没准备好就唤醒,用户读到垃圾数据
  3. 竞争条件:多个进程等待时,要用锁保护共享数据
  4. 标志位没重置:数据被读走后,要清除就绪标志

特别提醒:在SMP系统上,标志位的读写要用原子操作或加锁。我调试过一个八核ARM平台的问题,就是因为普通变量做标志,导致偶尔漏唤醒。

性能考量

在高速数据采集场景,频繁唤醒进程开销很大。这时候可以考虑:

  • 使用epoll(后续章节会讲)
  • 批量处理数据,一次唤醒传递多个数据包
  • 调整等待队列类型,用wake_up_interruptible替代wake_up,减少不必要的调度

对于实时性要求高的场景,select/poll的超时参数要仔细设置。别用NULL(无限等待),也别设太短的超时导致忙等待。

个人经验谈

做了十几年驱动,我的经验是:poll/select机制看似简单,但真正稳定可靠需要处理好细节。驱动开发不是实现功能就行,要站在整个系统角度思考

建议:

  1. 新驱动先实现阻塞IO,再加poll支持,这样调试起来更简单
  2. 等待队列和状态标志的初始化,放在设备探测函数里,别放在open中,避免竞态
  3. 多测试边界条件:多个进程同时等待、信号中断、超时处理
  4. 用fasync机制配合poll,处理异步通知场景(这个我们后面单独讲)

最后记住:驱动是桥梁,一端连着硬件,一端连着应用。好的驱动要让应用层感觉不到硬件的存在,而poll/select就是让应用能“舒适等待”的关键机制。下次遇到进程卡在select,先查驱动poll返回的mask,再查唤醒逻辑,八成能找到问题。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐