PCIE(RIFFA)驱动解读(五)
来源:AriesOpenFPGA 发布时间:2023-01-09 分享至微信

Part6Linux中断处理与阻塞I/O概述

16.1 Linux中断处理

6.1.1 中断服务函数的主旨

在单片机中经常用到中断,编写中断服务函数的主旨就是"执行越快越好"因为中断的优先级比普通任务优先级高,如果中断长时间占用CPU,其他任务就得不到调度;或者本次中断还没执行完下一个中断又来了,那CPU就永远在中断里出不去了。

比如控制一个I2C的触摸屏,需要以一定的周期刷新屏幕,这当然最好是在定时器中断做这件事了,但是每次刷新这个屏幕需要更新好几十个寄存器,而I2C的最高速率只有400Kbps,这是相当慢的速度如果这几十个寄存器都在定时器中断里更新,那恐怕在下一个中断到来前是执行不完了,所以非常耗时的程序不应该在中断里执行而应该在任务里执行,中断的职责是通知任务来执行那些原本应该在中断里执行的程序,由此引出中断处理的上半部和下半部。

6.1.2 中断上下文

Linux内核将中断处理分为两个部分:

上半部(top half)是接收到中断就立即执行的,只做有严格时限的工作,这些工作是在所有中断被禁止的情况下所作的。

下半部(bottom half)中执行那些允许稍后完成的工作,在合适的时机执行中断的下半部。

单片机开发中也有类似的思想,常常把耗时的又没有强实时性要求的处理放到main的大循环里去执行,或者在运行RTOS的单片机中会开一个任务来执行,中断服务函数置一个标志位就可以了。

如上图所示,上半部通知下半部的方式有很多种,比如可以像MCU裸机一样,上半部置一个标志位,下半部去轮询,当然这种方法实时性不强,Linux提供了几种主要的下半部机制:软中断请求(softirq)、小任务(tasklet)和工作队列。

tasklet就是用软中断来实现的,在上半部程序的末尾可以调用tasklet_schedule函数,就会使tasklet在合适的时间执行,如果要使用softirq和tasklet,那么推荐直接用tasklet。

工作队列是另一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度(因为它用于普通的内核线程)。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择softirq或tasklet。

然而,上述方法RIFFA驱动程序全都没采用,而是用阻塞I/O来实现的中断下半部处理。

26.2 阻塞I/O

6.2.1 阻塞I/O与非阻塞I/O

当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式IO就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃(返回错误码)。阻塞I/O访问方式如下所示。

6.2.2 等待队列简介

等待队列顾名思义首先它是一个队列,其次它的作用是存放等待的线程,一个内核线程如果要自行阻塞(自己放弃CPU使用权),需要先把本线程push进等待队列中,然后再进入睡眠,这样当其他线程想再唤醒时会从等待队列中pop出来。

之所以用队列不用栈我个人觉得是为了公平,如果等待队列换成了等待栈,那么先阻塞的线程可能很长时间都得不到唤醒。

6.2.3 等待队列头和等待队列项

想要使用一个等待队列首先需要初始化等待队列头,等待队列头就是等待队列的头部,Linux中用wait_queue_head_t结构体表示等待队列头类型,并提供了等待队列头的初始化函数init_waitqueue_head,此函数唯一的参数就是wait_queue_head_t结构体变量。

每个访问设备的进程都是可以是一个队列项,当进程想阻塞时需要把这些进程的队列项push进等待队列中。Linux使用wait_queue_t结构体表示等待队列项类型。

使用宏DEFINE_WAIT建立并初始化一个等待队列项,这个宏的用法就是像这样 DEFINE_WAIT(wait); 直接括号里写一个变量名就可以了,以后就可以用这个wait变量了,但需要注意,定义在函数里的话,这是一个局部变量。

宏DEFINE_WAIT定义在/linux/wait.h中,如下所示,这个宏是另一个宏DEFINE_WAIT_FUNC的宏。

宏DEFINE_WAIT_FUNC定义如下:

可见,宏DEFINE_WAIT的确就是定义了一个wait_queue_t结构体对象并初始化,.private=current表示属于当前进程;在Linux内核中current相当于一个全局变量,表示当前进程。因此宏DEFINE_WAIT就是给当前正在运行的进程创建并初始化了一个等待队列项。

6.2.4 将队列项添加/移除等待队列头

如果一个进程想要休眠,那么就需要将进程对应的等待队列项添加到前面创建的等待队列头中,只有添加到等待队列头中以后进程才能进入休眠态,否则后果你知道的,唤醒时找不到你这个进程了!

把进程push进等待队列的函数是prepare_to_wait,函数原型如下:

参数*q就是最先初始化的等待队列头,;参数*wait是休眠前创建的等待队列项;state是进程状态,RIFFA中填入的是宏TASK_INTERRUPTIBLE,表示这个进程是可被中断休眠模式,Linux源码中定义如下所示:

203行是可被中断休眠,204行是不可被中断休眠。这里的被中断不是指硬件irq中断,而是指信号触发,所谓信号其实是一种软中断,比如常用的kill命令(杀死进程)就是一种信号。如果一个休眠的进程设置的是可被中断,那么它不仅能被手动唤醒,还能被信号唤醒。所以在可被中断休眠的驱动程序中,进程被唤醒后需要判断是否由信号唤醒,如果是被信号唤醒那么驱动应该返回宏-ERESTARTSYS,表示应该重新执行系统调用,至于返回后用户是不是重新再进行系统调用,那就看用户应用程序怎么处理了。Linux中推荐使用TASK_INTERRUPTIBLE状态。

6.2.5 休眠与唤醒

这里的休眠就是进程自愿放弃CPU,由进程自己调用一个函数把自己休眠。这个函数是schedule,此函数无参数,直接休眠,除非由其他进程唤醒或信号触发,否则一直休眠。

但RIFFA作为一个可重构PCIe框架,自然要做到参数化了,它的驱动程序也不例外。RIFFA使用的是Linux中的另一个休眠函数­­---schedule_timeout,原型定义如下:

参数timeout是最大休眠时间(单位:毫秒),也就是如果没其他进程唤醒也没信号触发,但这个休眠的进程计时满了后依然会自己唤醒,这个计时功能用到了内核定时器,这里就不展开讲内核的时间管理了。

进程的唤醒很简单,Linux提供了唤醒的API,wake_up,其对应的内核API __wake_up定义如下所示:

实际上wake_up只有一个参数,就是*q,即等待队列头指针,把之前初始化的等待队列头指针传进去就可以了,系统会从这个等待队列中按序选择第一个队列项(进程)并唤醒。注意,此时等待队列中的这个被唤醒的队列项其实还没有被pop出队列,还需要调用finish_wait函数来pop出队列。finish_wait函数定义如下所示:

参数*q是等待队列头;*wait是等待队列项。

[ 新闻来源:AriesOpenFPGA,更多精彩资讯请下载icspec App。如对本稿件有异议,请联系微信客服specltkj]
存入云盘 收藏
举报
全部评论

暂无评论哦,快来评论一下吧!