Linux设备驱动中的阻塞和非阻塞IO
Linux设备驱动中的阻塞和非阻塞IO
幸福阶梯 发表于5个月前
Linux设备驱动中的阻塞和非阻塞IO
  • 发表于 5个月前
  • 阅读 25
  • 收藏 1
  • 点赞 0
  • 评论 0

腾讯云 技术升级10大核心产品年终让利>>>   

备注:本文摘录自《Linux设备驱动开发详解:基于最新的Linux 4.0内核》

阻塞和非阻塞I/O是设备访问的两种不同模式,驱动程序可以灵活地支持这两种用户空间对设备的访问方式。

8.1阻塞和非阻塞I/O

阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再操作。被挂起的进程进入睡眠状态,被从调度器的运行队列移走,直到等待的条件被满足。而非阻塞操作的进程在不能进行设备操作时,并不挂起,它要么放弃,要么不停地查询,直至可以进行操作为止。

驱动程序通常需要提供这样的能力:当应用程序进行read()、write()等系统调用时,若设备的资源不能获取,而用户又希望以阻塞的方式访问设备,驱动程序应该在设备驱动的xxx_read()、xxx_write()等操作中将进程阻塞直到资源可以获取,此后,应用程序的read()、write()等调用才返回,整个过程仍然进行了正确的设备访问,用户并没有感知到;若用户以非阻塞的方式访问设备文件,则当设备资源不可获取时,设备驱动xxx_read()、xxx_write()等操作立即返回,read()、write()等系统调用也随机返回,应用程序收到-EAGAIN返回值。

如图8.1所示,在阻塞访问时,不能获取资源的进程将进入休眠,它将CPU资源“礼让”给其他进程。因为阻塞的进程会进入休眠状态,所以必须确保有一个地方能够唤醒休眠的进程,否则,进程就真的“寿终正寝”了。唤醒进程的地方最大可能发生在中断里面,因为在硬件资源获得的同时往往伴随着一个中断。而非阻塞的进程则不断尝试,直到可以进行I/O。

 

代码清单8.1和8.2分别演示了以阻塞和非阻塞方式读取串口一个字符的代码。前者在打开文件的时候没有O_NONBLOCK标记,后者使用了O_NONBLOCK标记打开文件。

 

除了在打开文件时可以指定阻塞还是非阻塞方式以外,在文件打开后,也可以通过ioctl()和fcntl()改变读写的方式,如从阻塞变更为非阻塞或者从非阻塞变更为阻塞。例如,调用fcntl(fd, F_SETL, O_NONBLOCK)可以设置fd对应的I/O为非阻塞。

8.1.1等待队列

在linux驱动程序中,可以使用等待队列(wait queue)来实现阻塞进程的唤醒。等待队列很早就作为一个基本的功能单位出现在linux内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,可以用来同步对系统资源的访问,第七章中所讲述的信号量在内核中也依赖等待队列来实现。

Linux内核提供了如下关于等待队列的操作。

1、 定义“等待队列头部”

wait_queue_head_t my_queue;

wait_queue_head_t是__wait_queue_head结构体的一个typedef。

2、初始化“等待队列头部”

init_waitqueue_head(&my_queue);

而下面的DECLARE_WAIT_QUEUE_HEAD()宏可以作为定义并初始化等待队列头部的“快捷方式”。

DECLARE_WAIT_QUEUE_HEAD(name);

3、定义等待队列元素

DECLARE_WAITQUEUE(name, tsk) ;

该宏用于定义并初始化一个名为name的等待队列元素。

4、添加/移除等待队列

add_wait_queue(wait_queue_head_t * q, wait_queue_t * wait);

remove_wait_queue(wait_queue_head_t * q, wait_queue_t * wait);

add_wait_queue()用于将等待队列元素wait添加到等待队列头部q指向的双向链表中;而remove_wait_queue()用于将等待队列wait从由 q头部指向的链表中移除。

5、等待事件

wait_event(queue, condition);

wait_event_interruptible(queue, condition);

wait_event_timeout(queue, condition, timeout);

wait_event_interruptible_timeout(queue, condition, timeout);

等待第一个参数queue作为等待队列头部的队列被唤醒,而且第二个参数condition必须满足,否则继续阻塞。wait_event()和wait_event_interruptible()的区别在于后者可以被信号打断,而前者不能。加上_timeout后的宏意味着阻塞等待的超时时间,以jiffy为单位,在第三个参数的timeout到达时,不论condition是否满足,均返回。

6、唤醒队列

void wake_up(wait_queue_head_t *queue);

void wake_up_interruptible(wait_queue_head_t *queue);

上述操作会唤醒以queue作为等待队列头部的队列中所有的进程。

wake_up()应该与wake_event()或wake_event_timeout()成对使用,而wake_up_interruptible()则应该与wake_event_interruptible()或wake_event_interruptible_timeout()成对使用。wake_up()可唤醒处于TASK_UNINTERRUPTIBLE和TASK_INTERRUPTIBLE的进程,而wake_up_interruptible()只能唤醒处于TASK_INTERRUPTIBLE的进程。

7、在等待队列上睡眠

sleep_on(wait_queue_head_t * q);

interruptible_sleep_on(wait_queue_head_t * q);

sleep_on()函数的作用就是讲目前进程的状态设置成TASK_UNINTERRUPTIBLE,并定义一个等待队列元素,之后把它挂到等待队列头部q指向的双向链表,直到资源可获得,q队列指向链接的进程被唤醒。

interruptible_sleep_on()和sleep_on()函数类似,其作用是将目前进程的状态设置成TASK_INTERRUPTIBLE,并定义一个等待队列元素,之后把它附属到q指向的队列,直到资源可获得或者进程收到信号。

sleep_on()函数应该与wake_up()成对使用,interruptible_sleep_on()函数应该与wake_up_interruptible()成对使用。

代码清单8.3演示了一个在设备驱动中使用等待队列的模板,在进行写I/O操作的时候,判断设备是否可写,如果不可写且为阻塞I/O,则进程休眠并挂起到等待队列。

代码清单8.3 在设备驱动中使用等待队列

static ssize_t xxx_write(struct file *file, const char *buffer, size_t count,loff_t *ppos)
{
   ...
   DECLARE_WAITQUEUE(wait, current); /* 定义等待队列元素 */
   add_wait_queue(&xxx_wait, &wait); /* 添加元素到等待队列 */
   /* 等待设备缓冲区可写 */
   do {
       avail = device_writable(...);
       if (avail < 0) {
            if (file->f_flags &O_NONBLOCK) { /* 非阻塞 */
                ret = -EAGAIN;
        goto out;
               }
         __set_current_state(TASK_INTERRUPTIBLE); /* 改变进程状态 */
         schedule(); /* 调度其他进程执行 */
         (signal_pending(current)) { /* 如果是因为信号唤醒 */
              ret = -ERESTARTSYS;
              goto out;
         }
       }
   } while (avail < 0);

   /* 写设备缓冲区 */
    device_write(...)27 out:
    remove_wait_queue(&xxx_wait, &wait); /* 将元素移出 xxx_wait 指引的队列 */
    set_current_state(TASK_RUNNING); /* 设置进程状态为 TASK_RUNNING */
    return ret;
}

读懂代码清单8.3对理解Linux进程状态切换非常重要,所以提醒读者反复阅读此段代码,直至完quan领悟,几个要点如下。

1、如果是非阻塞访问(O_NONBLOCK被设置),设备忙时,直接返回“-EAGAIN”。

2、对于阻塞访问,会调用__set_current_state(TASK_INTERRUPTIBLE)进行进程状态切换并显示通过“schedule()”调度其他进程执行。

3、醒来的时候需要注意,由于调度出去的时候,进程状态是TASK_INTERRUPTIBLE,即浅度睡眠,所以唤醒它的有可能是信号,因此,我们首先通过signal_pending(current)了解是不是信号唤醒的,如果是,立即返回“-ERESTARTSYS”。

DECLARE_WAITQUEUE、add_wait_queue这两个动作加起来完成的效果如图8.2所示。在wait_queue_head_t指向的链表上,新定义的wait_queue元素被插入,而这个新插入的元素绑定了一个task_struct。

 

8.2轮询操作

8.2.1轮询的概念和作用

在用户程序中,select()和poll()也是与设备阻塞与非阻塞访问息息相关的论题。使用非阻塞I/O的应用程序通常会使用select()和poll()系统调用查询是否可对设备进行无阻塞的访问。select()和poll()系统调用最终会使设备驱动中的poll()函数被执行,在Linux2.5.45内核中还引入了epoll(),即扩展的poll()。

select()和poll()系统调用的本质一样,前者在BSD UNIX中引入,后者在System V中引入。

8.2.2应用程序中的轮询编程

应用程序中最广泛用到的是BSD UNIX中引入的select()系统调用,其原型为:

 int select(int numfds, fd_set *readfds, fd_set *writefds fd_set *exceptfds,struct timeval *timeout);

其中,readfds、writefds 、exceptfds分别是被select()监视的读、写和异常处理的文件描述符集合,numfds的值是需要检查的号码最高的fd加1。readfds文件集中的任何一个文件变得可读,select()返回;同理,writefds文件集中的任何一个文件变得可写,select也返回。

如图8.3所示,第一次对n个文件进行select()的时候,若任何一个文件满足要求,select()就直接返回;第二次再进行select()的时候,没有文件满足读写要求,select()的进程阻塞且睡眠。由于调用select()的时候,每个驱动的poll()接口都会被调用到,实际上执行select()的进程被挂到了每个驱动的等待队列上,可以被任何一个驱动唤醒。如果FDn变得可读写,select()返回。

 

timeout参数是一个指向struct timeval类型的指针,它可以使select()在等待timeout时间后若仍然没有文件描述符准备好则超时返回,struct timeval数据结构体的定义如代码清单8.7所示。

struct timeval {

time_t tv_sec; /* seconds */

suseconds_t tv_usec; /* microseconds */

};

下列操作用来设置、清除、判断文件描述符集合:

FD_ZERO(fd_set *set)

清除一个文件描述符集合;

FD_SET(int fd, fd_set *set)

将一个文件描述符加入到文件描述符集合中;

FD_CLR(int fd,fd_set *set)

将一个文件描述符从文件描述符集合中清除;

FD_ISSET(int fd,fd_set *set)

判断文件描述符是否被置位。

poll()的功能和实现原理与select()相似,其函数原型为:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

当多路复用的文件数量庞大、I/O流量频繁的时候,一般不太适合用select()和poll(),此种情况下,select()和poll()的性能表现较差,我们宜使用epoll。epoll的最大好处是不会随着fd的数目增长而降低效率,select()则会随着fd的数量增大性能下降明显。

与epoll相关的用户空间编程接口包括:

int epoll_create(int size);

创建一个epoll句柄,size用来告诉内核要监听多少个fd。需要注意的是,当创建好epoll句柄后,它本身也会占用一个fd的值,所以在使用完epoll后,必须调用close()关闭。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

告诉内核要监听的什么类型的事件。第一个参数是epoll_create()的返回值,第二个参数表示动作,包含:

EPOLL_CTL_ADD:注册新的fd到epfd中。

EPOLL_CTL_MOD:修改已经注册的fd的监听事件。

EPOLL_CTL_DEL:从epfd中删除一个fd。

第三个参数是需要监听的fd,第四个参数是告诉内核需要监听的事件类型,struct epoll_event结构如下:

struct epoll_event {

__u32 events;

__u64 data;

};

events可以是以下几个宏的“或”:

EPOLLIN:表示对应的文件描述符可以读。

EPOLLOUT:表示对应的文件描述符可以写。

EPOLLPRI:表示对应的文件描述符有紧急的数据可以读。

8.2.3设备驱动中的轮询编程

设备驱动中poll()函数的原型是:

unsigned int (*poll) (struct file *filp , struct poll_table *wait);

第一个参数为file结构体指针,第二个参数为轮询表指针,这个函数应该进行两项工作。

1、对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应的等待队列头部添加到poll_table中。

2、返回表示是否能对设备进行无阻塞读、写访问的掩码。

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

void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table * p);

poll_wait()函数的名称非常容易让人产生误会,以为它和wait_event()等一样,会阻塞地等待某事件的发生,其实这个函数并不会引起阻塞。poll_wait()函数所做的工作是把当前进程添加到wait参数指定的等待列表(poll_table)中,实际作用是让唤醒参数queue对应的等待队列可以唤醒因select()而睡眠的进程。

驱动程序poll()函数应该返回设备资源的可获取状态,即POLLIN、POLLOUT、POLLPRI、POLLERR、POLLNVAL等宏的位或结果。每个宏的含义都表明设备的一种状态,如POLLIN意味着设备可以无阻塞地读,POLLOUT意味着设备可以无阻塞地写。

通过以上分析,可得出设备驱动中poll()函数的典型模板,如代码清单8.8所示。

static unsigned int xxx_poll(struct file *filp, poll_table *wait)
{
   unsigned int mask = 0;
   struct xxx_dev *dev = filp->private_data; /* 获得设备结构体指针 */
   ...

   poll_wait(filp, &dev->r_wait, wait); /* 加入读等待队列 */
   poll_wait(filp, &dev->w_wait, wait); /* 加入写等待队列 */

   if (...) /* 可读 */

       mask |= POLLIN | POLLRDNORM; /* 标示数据可获得(对用户可读) */

   if (...) /* 可写 */
       mask |= POLLOUT | POLLWRNORM; /* 标示数据可写入 */
   ...
   return mask;
}

总结

阻塞和非阻塞访问是I/O操作的两种不同模式,前者在暂时不可进行I/O操作时会让进程睡眠,后者则不然。

在设备驱动中阻塞I/O一般基于等待队列或者基于等待队列的其他Linux内核API来实现,等待队列可用于同步驱动中事件发生的先后顺序。使用非阻塞I/O的应用程序也可借助轮询来查询是否能立即被访问,用户空间用select()、poll()或者epoll接口,设备驱动提供poll()函数。设备驱动的poll()本身不会阻塞,但是与poll()、select()和epoll相关的系统调用则会阻塞地等待至少一个文件描述符集合可访问或超时。

共有 人打赏支持
粉丝 0
博文 61
码字总数 114148
×
幸福阶梯
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: