Linux 设备驱动中的阻塞与非阻塞 I/O

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux 设备驱动中的阻塞与非阻塞 I/O相关的知识,希望对你有一定的参考价值。


阻塞操作是指在执行设备操作时若不能获得资源则挂起进程,直到满足可操作的条件后再进行操作。被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被满足。而非阻塞操作的进程在不能进行设备操作时并不挂起,它或者放弃,或者不停地查询,直至可以进行操作为止。
阻塞从字面上听起来似乎意味着低效率,实则不然,如果设备驱动不阻塞,则用户想获取设备资源只能不停地查询,这反而会无谓地耗费 CPU 资源。而阻塞访问时,不能获取资源的进程将进入休眠,它将 CPU 资源让给其他进程。因为阻塞的进程会进入休眠状态,因此,必须确保有一个地方能够唤醒休眠的进程。唤醒进程的地方最大可能发生在中断里面,因为硬件资源获得的同时往往伴随着一个中断。
下面以两个实例演示阻塞和非阻塞的读取串口一个字符:

  • 阻塞地读取串口一个字符
char buf; 
fd = open("/dev/ttyS1", O_RDWR);
...
res = read(fd,&buf,1); //当串口上有输入时才返回
if(res==1)
printf("%c\\n", buf);
  • 非阻塞地读取串口一个字符
char buf; 
fd = open("/dev/ttyS1", O_RDWR| O_NONBLOCK);
...
while(read(fd,&buf,1)!=1); //串口上无输入也返回,所以要循环尝试读取串口
printf("%c\\n", buf);

1、等待队列(阻塞)

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

  • 定义等待队列头:​​wait_queue_head_t my_queue;​
  • 初始化等待队列头:​​init_waitqueue_head(&my_queue);​
  • 定义并初始化等待队列头:​​DECLARE_WAIT_QUEUE_HEAD (name)​
  • 定义并初始化等待队列name:​​DECLARE_WAITQUEUE(name, tsk)​
  • 添加等待队列:​​void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);​
  • 移除等待队列:​​void fastcall remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);​
  • 阻塞等待事件(condition)发生:
wait_event(queue, condition);
wait_event_interruptible(queue, condition);
wait_event_timeout(queue, condition, timeout); //time以jiffy 为单位
wait_event_interruptible_timeout(queue, condition, timeout);
  • 唤醒队列:
void wake_up(wait_queue_head_t *queue); 
void wake_up_interruptible(wait_queue_head_t *queue);

wake_up() 可 唤 醒 处 于TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 的进程, 而wake_up_interruptible()只能唤醒处于 TASK_INTERRUPTIBLE 的进程。

  • 在等待队列上睡眠
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。

  • 改变进程状态:
set_current_state();
__add_wait_queue();
current->state = TASK_UNINTERRUPTIBLE/TASK_RUNNING/TASK_INTERRUPTIBLE //对进程状态直接赋值

set_current_state()函数在任何环境下都可以使用,不会存在并发问题,但是效率要低于​​__add_ wait_queue()​​​。因此,在许多设备驱动中,并不调用​​sleep_on()​​​或 ​​interruptible_sleep_on()​​,而是亲自进行进程的状态改变和切换。

  • 以下为在驱动程序中改变进程状态并调用 schedule() 的实例:
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); //添加等待队列
ret = count;
/* 等待设备缓冲区可写 */
do
avail = device_writable(...);
if (avail < 0)
_ _set_current_state(TASK_INTERRUPTIBLE);//改变进程状态
if (avail < 0)

if (file->f_flags &O_NONBLOCK) //非阻塞

if (!ret)
ret = - EAGAIN;
goto out;

schedule(); //调度其他进程执行
if (signal_pending(current))//如果是因为信号唤醒

if (!ret)
ret = - ERESTARTSYS;
goto out;


while (avail < 0);

/* 写设备缓冲区 */
device_write(...)
out:
remove_wait_queue(&xxx_wait, &wait);//将等待队列移出等待队列头
set_current_state(TASK_RUNNING);//设置进程状态为 TASK_RUNNING
return ret;

2、轮询操作(非阻塞)

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

2.1 应用程序中的轮询编程

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

int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval()监视的读、写和异常处理的文件描述符集合,numfds 的值是需要检查的号码最高的文件描述符加 1。timeout 参数是一个指向 struct timeval()在等待 timeout 时间后若没有文件描述符准备好则返回。struct timeval(232, 232, 232); background: rgb(249, 249, 249);">
struct timeval(231, 243, 237); padding: 0px 3px; border-radius: 4px; overflow-wrap: break-word; text-indent: 0px;">​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)​​:判断文件描述符是否被置位。

2.2 驱动程序中的轮询编程

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

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

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

  • 对可能引起设备文件状态变化的等待队列调用 poll_wait()函数,将对应的等待队列头添加到poll_table。
  • 返回表示是否能对设备进行无阻塞读、写访问的掩码。
    关键的用于向 poll_table 注册等待队列的 poll_wait()函数的原型如下:
void poll_wait(struct file *filp, wait_queue_heat_t *queue, poll_table * wait);

poll_wait()函数的名称非常容易让人误会它会阻塞地等待某事件的发生,但这个函数并不会引起阻塞。poll_wait()函数所做的工作是把当前进程添加到 wait 参数指定的等待列表(poll_table)中。
驱动程序 poll()函数应该返回设备资源的可获取状态,即 POLLIN、POLLOUT、POLLPRI、POLLERR、POLLNVAL 等宏的位“或”结果。每个宏的含义都表明设备的一种状态,如 POLLIN(定义为 0x0001)意味着设备可以无阻塞地读,POLLOUT(定义为 0x0004)意味着设备可以无阻塞地写。
通过以上分析,可得出设备驱动中 poll()函数的典型模板:

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;


以上是关于Linux 设备驱动中的阻塞与非阻塞 I/O的主要内容,如果未能解决你的问题,请参考以下文章

Linux中的五种I/O模型

Linux Kernel TCP/IP Stack — L7 Layer — Application Socket I/O 接口类型

Linux设备驱动中的IO模型---阻塞和非阻塞IO

11.python并发入门(part14阻塞I/O与非阻塞I/O,以及引入I/O多路复用)

Linux设备驱动中的阻塞和非阻塞I/O

阻塞与非阻塞 I/O 缓冲?