目录
阻塞和非阻塞IO简介
等待队列
等待队列头
等待队列项
轮询
Linux驱动下的poll操作函数
阻塞式访问IO实验
阻塞式访问IO驱动程序编写
运行测试
非阻塞式IO实验
运行测试
阻塞和非阻塞IO简介
阻塞和非阻塞IO是Linux驱动开发里面很常见的两种设备访问模式,在编写驱动的时候一定要考虑到阻塞和非阻塞。
这里的"IO”并不是我们学习STM32或者其他单片机的时候所说的"GPIO”(也就是引脚)。这里的IO指的是Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作。当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式IO就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。阻塞式IO如图所示:
图中应用程序调用read函数从设备中读取数据,当设备不可用或数据未准备好的时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给应用程序。非阻塞IO如图所示:
从图可以看出,应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,这样一直往复循环,直到数据读取成功。
应用程序可以使用如下所示示例代码来实现阻塞访问:
从示例代码可以看出,对于设备驱动文件的默认读取方式就是阻塞式的,所以前面所有的例程测试APP都是采用阻塞IO。
如果应用程序要采用非阻塞的方式来访问驱动设备文件,可以使用如下所示代码:
第4行使用open函数打开"/dev/xxx dev”设备文件的时候添加了参数"ONONBLOCK",表示以非阻塞方式打开设备,这样从设备中读取数据的时候就是非阻塞方式的了。
等待队列
等待队列头
阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。Linux内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作,如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体wait_queue_head_t表示,wait_queue_head_t结构体定义在文件include/linux/wait.h中,结构体内容如下所示:
定义好等待队列头以后需要初始化,使用init_waitqueue_head函数初始化等待队列头,函数原型如下:
参数q就是要初始化的等待队列头。
也可以使用宏DECLARE_WAIT_QUEUE_HEAD来一次性完成等待队列头的定义的初始化。
等待队列项
等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。结构体wait_queue_t表示等待队列项,结构体内容如下:
使用宏DECLARE_WAITQUEUE定义并初始化一个等待队列项,宏的内容如下:
name就是等待队列项的名字,tsk表示这个等待队列项属于哪个任务(进程),一般设置为current,在Linux内核中current相当于一个全局变量,表示当前进程。因此宏DECLARE_ WAITQUEUE就是给当前正在运行的进程创建并初始化了一个等待队列项。
将队列项添加/移除等待队列头
当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中,只有添加到等待队列头中以后进程才能进入休眠态。当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可,等待队列项添加API 函数如下:
等待队列项要加入的等待队列头。
wait:要加入的等待队列项。
返回值:无。
等待队列项移除API函数如下:
q:要删除的等待队列项所处的等待队列头。
wait:要删除的等待队列项。
返回值:无。
等待唤醒
当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下两个函数:
参数q就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。wake-up函数可以唤醒处于TASK-INTERRUPTIBLE和TASK-UNINTERRUPTIBLE状态的进程,而wake_up_interruptible函数只能唤醒处于TASK_INTERRUPTIBLE状态的进程。
等待事件
除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程,和等待事件有关的API函数如表所示:
轮询
如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,也就是轮询。poll、epoll和select可以用于处理轮询,应用程序通过select、epoll或poll函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用select、epoll或poll函数的时候设备驱动程序中的poll函数就会执行,因此需要在设备驱动程序中编写 poll函数。先来看一下应用程序中使用的select、poll和epoll这三个函数。
select函数
nfds:所要监视的这三类文件描述集合中,最大文件描述符加1。
readfds, writefds和exceptfds:这三个指针指向描述符集合,这三个参数指明了关心哪些描述符、需要满足哪些条件等等,这三个参数都是fd_set类型的, fd_set类型变量的每一个位都代表了一个文件描述符。readfds用于监视指定描述符集的读变化,也就是监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取那么seclect就会返回一个大于0的值表示文件可以读取。如果没有文件可以读取,那么就会根据 timeout参数来判断是否超时。可以将readfs设置为NULL,表示不关心任何文件的读变化。writefds和readfs类似,只是writefs用于监视这些文件是否可以进行写操作。exceptfds 用于监视这些文件的异常。
比如我们现在要从一个设备文件中读取数据,那么就可以定义一个fd_set变量,这个变量要传递给参数readfds。当定义好一个fd_set变量以后可以使用如下所示几个宏进行操作:
FD_ZERO用于将fd_set变量的所有位都清零,FD_SET用于将fd_set变量的某个位置1,也就是向fd_set添加一个文件描述符,参数fd就是要加入的文件描述符。FD_CLR用于将fd_set变量的某个位清零,也就是将一个文件描述符从fd_set中删除,参数fd就是要删除的文件描述符。FD_ISSET用于测试一个文件是否属于某个集合,参数fd就是要判断的文件描述符。
timeout:超时时间,当我们调用select函数等待某些文件描述符可以设置超时时间,超时时间使用结构体timeval表示,结构体定义如下所示:
当timeout为NULL的时候就表示无限期的等待。
返回值: 0,表示的话就表示超时发生,但是没有任何文件描述符可以进行操作; -1,发生错误;其他值,可以进行操作的文件描述符个数。
使用select函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:
poll函数
在单个线程中, select函数能够监视的文件描述符数量有最大的限制,一般为1024,可以修改内核将监视的文件描述符数量改大,但是这样会降低效率!这个时候就可以使用poll函数,poll函数本质上和select没有太大的差别,但是poll函数没有最大文件描述符限制, Linux应用程序中poll函数原型如下所示:
fds:要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体pollfd类型的, pollfd结构体如下所示:
fd是要监视的文件描述符,如果fd无效的话那么events监视事件也就无效,并且revents返回0。events是要监视的事件,可监视的事件类型如下所示:
revents是返回参数,也就是返回的事件,由Linux内核设置具体的返回事件。
nfds: poll函数要监视的文件描述符数量。
timeout:超时时间,单位为ms。
返回值:返回revents域中不为0的pollfd结构体个数,也就是发生事件或错误的文件描述符数量; 0,超时; -1,发生错误,并且设置errno为错误类型。
使用poll函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:
epoll函数
传统的selcet和poll函数都会随着所监听的fd数量的增加,出现效率低下的问题,而且poll函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。为此,epoll应运而生,epoll就是为处理大并发而准备的,一般常常在网络编程中使用epoll函数。应用程序需要先使用epoll_create函数创建一个epoll句柄, epoll_create函数原型如下:
size:从Linux2.6.8开始此参数已经没有意义了,随便填写一个大于0的值就可以。
返回值: epoll句柄,如果为-1的话表示创建失败。
epoll句柄创建成功以后使用epoll_ctl函数向其中添加要监视的文件描述符以及监视的事件, epoll ctl函数原型如下所示:
epfd:要操作的epoll句柄,也就是使用epoll_create函数创建的epoll句柄。
op:表示要对epfd(epoll句柄)进行的操作,可以设置为:
fd:要监视的文件描述符。
event:要监视的事件类型,为epoll_event结构体类型指针, epoll_event 结构体类型如下所示:
结构体epoll_event的events成员变量表示要监视的事件,可选的事件如下所示:
上面这些事件可以进行“或”操作,也就是说可以设置监视多个事件。
返回值: 0,成功: -1,失败,并且设置errno的值为相应的错误码。一切都设置好以后应用程序就可以通过epoll_wait 函数来等待事件的发生,类似select函数。epoll wait函数原型如下所示:
epfd:要等待的epoll。
events:指向epoll_event结构体的数组,当有事件发生的时候Linux内核会填写events,调用者可以根据events判断发生了哪些事件。
maxevents:events数组大小,必须大于0。
timeout:超时时间,单位为ms。
返回值:0,超时;-1,错误;其他值,准备就绪的文件描述符数量。
epoll更多的是用在大规模的并发服务器上,因为在这种场合下select和poll并不适合。当设计到的文件描述符(fd)比较少的时候就适合用selcet和poll。
Linux驱动下的poll操作函数
当应用程序调用select或poll函数来对驱动程序进行非阻塞访问的时候,驱动程序file_operations操作集中的poll函数就会执行。所以驱动程序的编写者需要提供对应的poll 函数,poll函数原型如下所示:
filp:要打开的设备文件(文件描述符)。
wait:结构体poll_table_struct类型指针,由应用程序传递进来的。一般将此参数传递给poll_wait函数。
返回值:向应用程序返回设备或者资源状态,可以返回的资源状态如下:
我们需要在驱动程序的poll函数中调用poll_wait函数, poll_wait函数不会引起阻塞,只是将应用程序添加到poll_table中,poll_wait函数原型如下:
参数wait_address是要添加到poll_table中的等待队列头,参数p就是poll_table,就是file_operations中poll函数的wait参数。
阻塞式访问IO实验
直接在应用程序中通过read函数不断的读取按键状态,当按键有效的时候就打印出按键值。这种方法有个缺点,那就是imx6uirqApp这个测试应用程序拥有很高的CPU占用率,大家可以在开发板中加载驱动程序模块imx6uirq.ko,然后以后台运行模式打开imx6uirgApp这个测试软件,命令如下:
测试驱动是否正常工作,如果驱动工作正常的话输入"top”命令查看imx6uirqApp这个应用程序的CPU使用率,结果如图所示:
从图52.2.1可以看出, imx6uirqApp这个应用程序的CPU使用率竟然高达99.6%,这仅仅是一个读取按键值的应用程序,这么高的CPU使用率显然是有问题的!原因就在于我们是直接在while循环中通过read函数读取按键值,因此imx6uirqApp这个软件会一直运行,一直读取按键值,CPU使用率肯定就会很高。最好的方法就是在没有有效的按键事件发生的时候,imx6uirqApp这个应用程序应该处于休眠状态,当有按键事件发生以后imx6uirqApp这个应用程序才运行,打印出按键值,这样就会降低CPU使用率,节下来就使用阻塞IO来实现此功能。
阻塞式访问IO驱动程序编写
第32行,修改设备文件名字为“blockio”,当驱动程序加载成功以后就会在根文件系统中出现一个名为"/dev/blockio”的文件。
第61行,在设备结构体中添加一个等待队列头r_wait,因为在Linux驱动中处理阻塞IO需要用到等待队列。
第107-110行,定时器中断处理函数执行,表示有按键按下,先在107行判断一下是否是一次有效的按键,如果是的话就通过|wake_up或者wake_up_interruptible函数来唤醒等待队列r_wait
第168行,调用init_waitqueue_head函数初始化等待队列头r_wait。
第200-206行,采用等待事件来处理read的阻塞访问, wait_event_interruptible函数等待releasekey有效,也就是有按键按下。如果按键没有按下的话进程就会进入休眠状态,因为采用了wait_event_interruptible函数,因此进入休眠态的进程可以被信号打断。
第208~218行,首先使用DECLARE_WAITQUEUE宏定义一个等待队列,如果没有按键按下的话就使用add_wait_queue函数将当前任务的等待队列添加到等待队列头r-wait中。随后调用-set_current_state函数设置当前进程的状态为TASK_INTERRUPTIBLE,也就是可以被信号打断。接下来调用schedule函数进行一次任务切换,当前进程就会进入到休眠态。如果有按键按下,那么进入休眠态的进程就会唤醒,然后接着从休眠点开始运行。在这里也就是从第213行开始运行,首先通过 signal_pending函数判断一下进程是不是由信号唤醒的,如果是由信号唤醒的话就直接返回-ERESTARTSYS这个错误码。如果不是由信号唤醒的(也就是被按键唤醒的)那么就在217行调用-setcurrent_state函数将任务状态设置为TASK_RUNNING,然后在218行调用remove_wait_queue函数将进程从等待队列中删除。
使用等待队列实现阻塞访问重点注意两点:
1.将任务或者进程加入到等待队列头。
2.在合适的点唤醒等待队列,一般都是中断处理函数里面。
运行测试
按下开发板上的KEY0按键,结果如图所示:
当按下KEY0按键以后blockioApp这个测试APP就会打印出按键值。输入"top"命令,查看blockioAPP这个应用APP的CPU使用率,如图所示:
从图可以看出,当在按键驱动程序里面加入阻塞访问以后, blockioApp这个应用程序的CPU使用率降低到了0.0%。大家注意,这里的0.0%并不是说blockioApp这个应用程序不使用CPU了,只是因为使用率太小了, CPU使用率可能为0.00001%,但是图只能显示出小数点后一位,因此就显示成了0.0%。
非阻塞式IO实验
第32行,修改设备文件名字为“noblockio”,当驱动程序加载成功以后就会在根文件系统中出现一个名为“/dev/noblockio”的文件。
第202~204行,判断是否为非阻塞式读取访问,如果是的话就判断按键是否有效,也就是判断一下有没有按键按下,如果没有的话就返回-EAGAIN。
第241~252行, imx6uirq_poll函数就是file_operations驱动操作集中的poll函数,当应用程序调用select或者poll函数的时候imx6uirq_poll函数就会执行。
第246行调用poll_wait 函数将等待队列头添加到poll_table中。
第248-250行判断按键是否有效,如果按键有效的话就向应用程序返回POLLIN这个事件,表示有数据可以读取。
第259行,设置file_operations的poll成员变量为imx6uirq_poll。
第52-73行,这段代码使用 poll函数来实现非阻塞访问,在while循环中使用poll函数不断的轮询,检查驱动程序是否有数据可以读取,如果可以读取的话就调用read函数读取按键数据。第75-101行,这段代码使用select函数来实现非阻塞访问。
运行测试
驱动加载成功以后使用如下命令打开noblockioApp这个测试APP,并且以后台模式运行:
按下开发板上的KEY0按键,结果如图所示:
当按下KEY0按键以后noblockioApp这个测试APP就会打印出按键值。输入"top"命令,查看noblockioAPP这个应用APP的CPU使用率,如图所示:
从图可以看出,采用非阻塞方式读处理以后,noblockioApp的CPU占用率也低至0.0%,和图中的blockioApp一样,这里的0.0%并不是说noblockioApp这个应用程序不使用CPU了,只是因为使用率太小了,而图中只能显示出小数点后一位,因此就显示成了0.0%。