Linux设备驱动之阻塞I/O与异步通知

阻塞与非阻塞访问是 I/O 操作的两种不同模式,前者在 I/O 操作暂时不可进行时会让进程睡眠,后者则不然。在设备驱动中阻塞 I/O一般基于等待队列来实现,等待队列可用于同步驱动中事件发生的先后顺序。使用非阻塞 I/O 的应用程序也可借助轮询函数来查询设备是否能立即被访问,用户空间调用 select()和 poll()接口,设备驱动提供 poll()函数。设备驱动的 poll()本身不会阻塞,但是 poll()和 select()系统调用则会阻塞地等待文件描述符集合中的至少一个可访问或超时。
在设备驱动中使用异步通知可以使得对设备的访问可进行时,由驱动主动通知应用程序进行访问。这样,使用无阻塞 I/O 的应用程序无需轮询设备是否可访问,而阻塞访问也可以被类似“中断”的异步通知所取代。


等待队列

进程阻塞会进入睡眠,进入睡眠时应确保有其他进程或者中断将其唤醒,而用于唤醒的进程必须知道正在睡眠的进程在哪儿,于是就有了等待队列——睡眠进程的队列,都等待一个特定的唤醒信号。

  • 等待队列是被等待队列头管理的
    include <linux/wait.h>

    /* defined and initialized statically */
    DECLARE_WAIT_QUEUE_HEAD(name);

    /* dynamicly */
    wait_queue_head_t my_queue;
    init_waitqueue_head(&my_queue);
  • 进入睡眠
    /* 
       condition为等待条件,睡眠前后都要判断,如果condition == false则继续睡眠;
       interruptible版被中断时会返回非0值,驱动应据此返回-ERESTARTSYS;
       timeout版等待超时后返回0,单位jiffies。
    */
    wait_event(queue, condition)
    wait_event_interruptible(queue, condition)
    wait_event_timeout(queue, condition, timeout)
    wait_event_interruptible_timeout(queue, condition, timeout)
  • 唤醒
    /*
       对应的常见的唤醒方法会唤醒等待队列头所管理的等待队列中所有进程;
       interruptible版只能唤醒interruptible版的wait_event。
    */
    void wake_up(wait_queue_head_t *queue);
    void wake_up_interruptible(wait_queue_head_t *queue);

轮询

使用非阻塞 I/O的应用程序通常会使用 select()和 poll()系统调用查询是否可对设备进行无阻塞的访问。 select()和poll()系统调用最终会引发设备驱动中的 poll()函数被执行。
设备驱动中 poll()函数的原型是:

    include <linux/poll.h>

    /*
       wait: 输入的轮询表指针
       返回是否能对设备进行无阻塞读、写访问的掩码:
            POLLIN | POLLRDNORM -> 可读
            POLLOUT | POLLWRNORM -> 可写
            POLLHUP -> 读到文件尾
            POLLERR -> 设备错误
            read more: poll.h
    */        
    unsigned int(*poll)(struct file * filp, struct poll_table* wait);

关键的用于向 poll_table 注册等待队列的 poll_wait()函数的原型如下:

    /* 把管理当前进程的队列头queue添加到wait参数指定的等待列表(poll_table)中 */
    void poll_wait(struct file *filp, wait_queue_heat_t *queue, poll_table * wait);

异步通知(Asynchronous Notification)

异步通知的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这一点非常类似于硬件上“中断”的概念,比较准确的称谓是“信号驱动的异步 I/O”。
阻塞 I/O 意味着一直等待设备可访问后再访问,非阻塞 I/O 中使用 poll()意味着查询设备是否可访问,而异步通知则意味着设备通知自身可访问,实现了异步 I/O。
为了使设备支持异步通知机制,驱动程序中涉及 3 项工作:

  1. 支持 F_SETOWN 命令,能在这个控制命令处理中设置 filp->f_owner 为对应进程 ID。不过此项工作已由内核完成,设备驱动无需处理。
  2. 支持 F_SETFL 命令的处理,每当 FASYNC 标志改变时,驱动程序中的 fasync()函数将得以执行。因此,驱动中应该实现 fasync()函数。
  3. 在设备资源可获得时,调用 kill_fasync()函数激发相应的信号。

驱动中的上述 3 项工作和应用程序中的 3 项工作是一一对应的,如图所示为异步通知处理过程中用户空间和设备驱动的交互:
异步通知处理过程中用户空间和设备驱动的交互

设备驱动中异步通知编程比较简单,主要用到一项数据结构和两个函数。数据结构是fasync_struct 结构体, 两个函数分别是:

    /* 处理 FASYNC 标志变更 */
    int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
    /* 释放信号给用户 */
    void kill_fasync(struct fasync_struct **fa, int sig, int band);

使用方法:

    static int xxx_fasync(int fd, struct file *filp, int mode)
    {
        struct xxx_dev *dev = filp->private_data;
        return fasync_helper(fd, filp, mode, &dev->async_queue);
    }

    static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
    {
        struct xxx_dev *dev = filp->private_data;
        ... 
        /* 产生异步读信号 */
        if (dev->async_queue)
            kill_fasync(&dev->async_queue, SIGIO, POLL_IN); /* POLL_OUT可写 */
        ...
    }

    static int xxx_release(struct inode *inode, struct file *filp)
    {
        /* 将文件从异步通知列表中删除 */
        xxx_fasync(-1, filp, 0);
        ...
        return 0;
    }

猜你喜欢

转载自www.linuxidc.com/Linux/2016-12/138069.htm