当前位置: 首页 > 科技观察

Linux驱动技术(5)_设备阻塞-非阻塞读写

时间:2023-03-11 21:30:05 科技观察

等待队列是内核中进程调度非常重要的数据结构。它的任务是维护一个链表,链表中的每个节点都是一个PCB(Processcontrolblock),内核会调度PCB的等待队列中的所有进程休眠,直到某个唤醒条件出现。我在LinuxI/O多路复用一文中讨论了阻塞IO和非阻塞IO在应用层的使用。本文主要讨论如何在驱动中实现设备IO的阻塞和非阻塞读写。显然,要实现这种阻塞相关的机制,就要用到等待队列机制。本文内核源码使用了3.14.0版本的deviceblockingIO实现。当我们读写设备文件的IO时,最终会在驱动中回调相应的接口,而这些接口在读写设备进程(kernel)空间的过程中也会出现,如果条件不是满足了,接口函数会让进程进入休眠状态,即使读写设备的用户进程进入休眠,也就是我们常说的阻塞。总之,读写设备文件阻塞的本质是驱动程序在驱动程序中实现了对设备文件的阻塞。读写的过程可以总结如下:1.定义-初始化等待队列头//定义等待队列头wait_queue_head_twaitq_h;//初始化,等待队列头init_waitqueue_head(wait_queue_head_t*q);//或//定义并初始化等待队列头DECLARE_WAIT_QUEUE_HEAD(waitq_name);以上选项中,最后一个会直接定义并初始化一个waithead,但是如果在模块中使用全局变量传递参数,使用起来不方便,具体使用哪个看需求。我们可以跟着源码看看上面几行是做什么的://include/linux/wait.hstruct__wait_queue_head{spinlock_tlock;structlist_headtask_list;};typedefstruct__wait_queue_headwait_queue_head_t;wait_queue_head_t--36-->Spinlockforthisqueue--27-->The将整个队列“串”在一起然后让我们看一下初始化宏:#define__WAIT_QUEUE_HEAD_INITIALIZER(name){.lock=__SPIN_LOCK_UNLOCKED(name.lock),.task_list={&(name).task_list,&(name).task_list}}defineDECLARE_WAIT_QUEUE_HEAD(name)\wait_queue_head_tname=__WAIT_QUEUE_HEAD_INITIALIZER(name)DECLARE_WAIT_QUEUE_HEAD()--60-->根据传入的字符串name,创建一个名为name的等待队列头--57-->初始化上面的task_list字段,有没有内核标准的初始化宏,无语。..2.将本进程加入等待队列,将事件加入等待队列,即进程进入睡眠状态,直到条件为真才返回。**_interruptibleversion表示sleep可以被中断,_timeout**version表示timeout版本,超时就会返回,这种命名约定在kernelAPI中随处可见。voidwait_event(wait_queue_head_t*waitq_h,intcondition);voidwait_event_interruptible(wait_queue_head_t*waitq_h,intcondition);voidwait_event_timeout(wait_queue_head_t*waitq_h,intcondition);voidwait_event_interruptible_timeout(wait_queue_head_t*waitq_h,intcondition);这可是等待队列的核心,我们来看一下wait_event└──wait_event└──_wait_event├──abort_exclusive_wait├──finish_wait├──prepare_to_wait_event└──___wait_is_interruptible#definewait_event(wq,condition)do{if(condition)break;__wait_event(wq,condition);}while(0)wait_event--246-->如果条件为真,立即返回--248-->否则调用__wait_event#define___wait_event(wq,condition,state,exclusive,ret,cmd)\({\for(;;){\long__int=prepare_to_wait_event(&wq,&__wait,state);\\if(condition)\break;\if(___wait_is_interruptible(state)&&__int){\__ret=__int;\if(exclusive){\abort_exclusive_wait(&wq,&__wait,\state,NULL);\goto__out;\}\break;\}\cmd;\}\finish_wait(&wq,&__wait);\__out:__ret;\})--206-->死循环的轮询--209-->如果条件为真,则跳出循环,执行finish_wait();进程被唤醒--212-->如果进程sleep方法是可中断的,那么abort_exclusive_wait也会在中断到来时被唤醒--222-->如果上面两个都不满足,就会回调传入schedule(),即继续休眠,condition);}staticfile_operationsfops={.read=demo_read,};static__initdemo_init(void){init_waitqueue_head(&xj_waitq_h);}普通非阻塞IO的IO多路复用实现,只需要读/写不使用阻塞机制在驱动程序中注册的接口。这里要讨论的是IO多路复用,即当驱动中的读写接口没有实现阻塞机制时,我们如何利用内核机制在驱动中实现对IO多路复用的支持呢?以下是我们要使用的API:intpoll(structfile*filep,poll_table*wait);当应用层调用select/poll/epoll机制时,内核实际上是在回调相关文件的驱动中遍历poll接口,通过各个poll接口的返回值判断文件IO是否有对应的事件司机。我们知道,这三种IO多路复用机制的核心区别在于内核管理和监控文件的方式,分别是位、数组和链表,但是对于每个驱动来说,回调接口都是poll。模板structwait_queue_head_twaitq_h;staticunsignedintdemo_poll(structfile*filp,structpoll_table_struct*pts){unsignedintmask=0;poll_wait(filp,&wwaitq_h,pts);if(counter){mask=(POLLIN|POLLRDNORM);}returnmask。THIS_MODULE,.poll=demo_poll,};static__initdemo_init(void){init_waitqueue_head(&xj_waitq_h);}其他API刚才我们讨论了如何使用等待队列实现阻塞IO和非阻塞IO。其实内核还为等待队列提供了很多其他的API来完成相关操作,这里就知道了//sleeponthewaitingqueuesleep_on(wait_queue_head_t*wqueue_h);sleep_on_interruptible(wait_queue_head_t*wqueue_h);//唤醒等待进程voidwake_up(wait_queue_t*wqueue);voidwake_up_interruptible(wait_queue_t*wqueue);