Linux驱动学习笔记(五)

发布于:2025-03-22 ⋅ 阅读:(14) ⋅ 点赞:(0)
中断

1.中断可以提高CPU的运行效率,但是中断会打断内核进程中正常调度和运行,为了保证系统的实时性,中断服务程序必须足够短。但是在实际应用过程中发生中断的时候要处理很多事情,如果这时候都在中断服务函数中来完成,就会降低中断的实时性。基于这个原因,Linux提出了一个概念,把中断服务程序分成两个部分,中断上文和中断下文。中断上文:完成尽可能少且比较急的任务,特点就是响应快。中断下文:处理比较耗时的任务。Linux中的中断子系统框架如下图所示:

2.在 Linux 系统中,IRQ号(中断号)也称为软中断号,它在系统中是唯一的,程序员在处理中断时需要使用这个号。硬件中断则通过 HW中断ID 来进行标识,硬件中断ID是由中断控制器分配的,用于区分不同外设的中断。而 IRQ domain负责将硬件中断号和软件中断号进行映射,确保软中断号与硬件中断号之间的关系和正确性。

3.Linux提供了中断注册函数request_irq,定义在include/linux/interrupt.h中,如下图所示:

其中irq为要请求的中断号;handler指向中断服务函数(原型为typedef irqreturn_t (*irq_handler_t)(int, void *););flags指定中断处理程序的行为特性,它是一个由标志位组成的参数,可以控制中断的触发方式、中断是否可共享、中断是否自动启用等;name指定中断的名称,在Linux系统启动后可以在/proc/irq中找到注册的中断的名字;dev是中断发生后调用中断处理函数传递给中断处理函数的参数,如果中断设置标志为共享(IRQFSHARED)的话,则此参数用来区分具体的中断,共享中断只有在释放最后中断处理函数的时候才会被禁止掉。request_threaded_irq函数在kernel/irq/manage.c中定义,如下图所示:

这个函数主要是根据中断号获取中断描述符结构体irq_desc(desc = irq_to_desc(irq);,每个中断号绑定到一个irq_desc结构体上),然后初始化action结构体并将其添加到中断描述符结构体中的irqaction链表中。irq_desc结构体定义在include/linux/irqdesc.h中,如下图所示:

在Linux中每一个外设的外部中断都是使用irq_desc这个结构体来描述的,所以这个结构体也可以被称作是中断描述符。Linux系统可以用通过静态方法或者动态方法来管理这些中断描述符,静态方法即静态数组extern struct irq_desc irq_desc[NR_IRQS];(NR_IRQS表示系统中最大支持的中断号的数量,即中断号的上限)。在内核中通过CONFIG_SPARSE_IRQ宏来选择是静态方法还是动态方法,这个宏在.config文件中定义,在arm64的内核源码中,这个宏是默认打开的。这个结构体中最重要的一个成员就是上图中指出的irqaction链表,irqaction结构体如下图所示,定义在include/linux/interrupt.h中:

该结构体中包含了中断处理函数handler、区分共享中断用的dev_id等内容,共享中断就是多个外设共享一个中断源,对于非共享中断其irq_desc结构体中的irqaction链表只有一个成员,而对于共享中断其irq_desc结构体中的irqaction链表有多个成员,通过dev_id来区分(可参考讯为Linux驱动视频第五期P6)。在驱动程序中使用request_irq函数注册中断时,其中的参数待注册的中断号irq可以通过int gpio_to_irq(unsigned int gpio)函数获取,该函数在include/linux/gpio.h中定义,其中gpio为对应硬件上的gpio编号(例如对于RK3568处理器,它有5个GPIO组GPIO0至GPIO4,每个GPIO组包含32个引脚,编号为A0至A7,B0至B7,C0至C7,D0至D7,GPIO引脚编号计算公式为:pin = bank * 32 + number,其中bank是组号,number是该组内的编号,GPIO编号计算公式为:number = group * 8 + X,其中group是ABCD小组号,X是小组内的编号。例如:GPIO0_B5对应的引脚编号为:0*32+(1*8+5)=13),该函数会返回Linux系统分配的gpio对应的系统中唯一的中断号。void free_irq(unsigned int irq, void * dev_id)函数是注销中断函数,其中irq表示要释放的中断对应的终端号,dev_id作用是如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。

4.request_irq函数绑定的中断服务函数只是中断的上文,完成尽可能少且比较急的任务,特点就是响应快,中断上文是必须要有的但是中断下文可以没有。tasklet机制是Linux中断处理机制中的软中断延迟机制,通常用于减少中断处理的时间,将本应该是在中断服务程序中完成的任务转化成软中断完成。tasklet是一种特殊的软中断,在Linux内核中,一般使用tasklet机制来实现中断下文,tasklet绑定的函数在同一时间只能在一个CPU上运行,每个CPU会维护一个tasklet链表,所以在多核处理系统上不会出现并发的问题,在tasklet绑定的函数中不能调用任何可能会引起休眠的函数,否则会导致内核异常。tasklet定义在include/linux/interrupt.h头文件中,如下图:

其中next用于实现 tasklet 的链表结构,这是为了实现多个任务在同一个 CPU 上的顺序执行;state用于表示当前的tasklet是否被调度;count表示当前tasklet是否被使能(为1表示未使能,为0表示使能);func和callback就是传递给中断下文的函数(tasklet结构体发生过一些变化,以前的内核版本中只有func没有callback);data是传递给中断下文函数的参数。tasklet支持静态初始化和动态初始化,静态初始化函数如下图所示:

上面两个函数的区别仅仅在于初始化时是否使能(通过count的值控制)。动态初始化函数定义在kernel/softirq.c中,如下图所示:

动态初始化时默认是使能的,其中t是待动态初始化的tasklet,func是要绑定的中断下文的函数,data是func的参数。tasklet使能函数和失能函数如下图所示,就是通过改变count的值实现使能和失能:

在tasklet处于使能的状态时,调用tasklet_shedule调度函数,绑定的调度函数会在不确定的时间后被调度,函数定义如下:

tasklet取消调度函数tasklet_kill如下图所示,该函数用于取消一个已经调度的tasklet,但是这个函数会等待已经绑定的函数执行完成。

在驱动程序中,需要在驱动的初始化函数中调用tasklet_init动态初始化tasklet(动态初始化时就已经使能),然后在中断处理函数即中断上文中调用tasklet调度函数tasklet_schedule将其加入调度队列,在驱动退出函数中调用tasklet_kill将tasklet取消调度(可参考讯为Linux驱动视频第五期P7)。

5.软中断也是实现中断下半部分的方法之一,但是软中断的资源有限,对应的中断号不多,Linux只实现以下以下几种软中断:

软中断号是从0开始的,中断号越小优先级越高,图中的NR_SOFTIRQS表示软中断的数量,也可以自己添加软中断,在NR_SOFTIRQS上面添加即可,如图中的TEST_SOFTIRQ就是自定义的软中断。从上图中的TASKLET_SOFTIRQ这个成员可以看出tasklet是软中断的一种。与软中断相关的函数有:void open_softirq(int nr, void (*action)(struct softirq_action *));函数用于注册一个软中断;void raise_softirq(unsigned int nr);函数用于触发一个软中断(不会禁用中断);void raise_softirq_irqoff(unsigned int nr);函数用于触发软中断(会禁用中断)。在驱动程序中想要使用软中断时,需要在驱动的初始化函数中调用open_softirq注册软中断(例如open_softirq(TEST_SOFTIRQ,testsoft_func);注册自己定义的软中断),然后在中断处理函数即中断上文中调用raise_softirq函数触发这个软中断,但仅仅这样驱动程序是无法编译通过的,因为Linux不希望程序员擅自增加软中断的数量,所以默认没有将open_softirq、raise_softirq等函数导出到符号表,所以需要在这些函数原型下面用EXPORT_SYMBOL()将其导出到符号表再重新编译内核,之后再编译驱动程序即可编译成功(可参考讯为Linux驱动视频第五期P9)。

6.Linux内核中软中断的初始化函数定义在文件/kernel/softirq.c中,如下图所示:

softirq_init函数会在/init/main.c的start_kernel函数中被调用,在softirq_init函数中,为每个可用的CPU都创建了两个链表tasklet_vec和tasklet_hi_vec,这两个链表用来存放用户自定义的tasklet,这个函数还初始化了两个软中断并绑定到相应的中断处理函数。例如对于TASKLET_SOFTIRQ来说,它的软中断处理函数为tasklet_action,如下图:

tasklet_action_common函数的主要作用是遍历CPU上的链表tasklet_vec,根据链表上的每个tasklet当前是否使能来选择是否运行tasklet对应的回调函数。在使用tasklet_shedule调度函数将tasklet加入调度队列时会触发一个软中断,tasklet_shedule实际是调用了__tasklet_schedule_common函数(定义在/kernel/softirq.c),如下图:

这个函数会触发软中断,就会去执行之前在软中断的初始化函数softirq_init中给TASKLET_SOFTIRQ绑定的处理函数tasklet_action。由此可见,tasklet是运行软中断环境中的,它是一种特殊的软中断。

7.工作队列是实现中断下半部分的机制之一,是一种将工作推后执行的形式。工作队列和tasklet最主要的区别是tasklet不能休眠,而工作队列是可以休眠的。tasklet可以用来处理比较耗时间的事情,而工作队列可以处理更耗时间的事情。工作队列将工作推到以后,会交给内核线程去执行。Linux在启动过程中会创建一个工作者内核线程,这个线程创建以后处于sleep状态。当有工作需要处理的时候,会唤醒这个线程去处理这个工作(也就是说Linux默认创建了一个工作队列,并用工作者内核线程与处理这个工作队列上的工作)。内核工作队列分成共享工作队列和自定义工作队列两种(共享工作队列就是Linux默认创建的那个)。共享工作队列:结构体struct work_struct描述的是要延迟执行的工作,定义在include/linux/workqueue.h中,如下图所示:

其中func为工作函数,绑定需要做的工作,原型为typedef void (*work_func_t)(struct work_struct *work);,工作可以通过DECLARE_WORK函数进行静态初始化:

也可以通过INIT_WORK函数进行动态初始化:

与共享工作队列有关的函数有调度工作函数static inline bool schedule_work(struct work_struct *work);和取消某工作调度的函数bool cancel_work_sync(struct work_struct *work);,如果被取消的工作已经正在执行,则会等待它执行完成再返回。在驱动程序中想要使用共享工作队列时,需要在驱动的初始化函数中调用INIT_WORK初始化工作,然后在中断处理函数即中断上文中调用schedule_work函数调度这个工作,在工作的工作函数func中是可以使用msleep等休眠函数的。自定义工作队列:内核使用struct workqueue_struct结构体描述一个工作队列,定义在include/linux/workqueue.h当中,创建工作队列的函数如下图:

create_workqueue会给每个CPU都创建一个CPU相关的工作队列,其中name为创建的工作队列的名字,创建成功返回一个struct workqueue_struct类型指针,创建失败返回NULL。create_singlethread_workqueue只给一个CPU创建一个CPU相关的工作队列,其中name为创建的工作队列的名字,创建成功返回一个struct workqueue_struct类型指针,创建失败返回NULL。与自定义工作队列有关的函数有调度工作队列工作函数bool queue_work(struct workqueue_struct *wq, struct work_struct *work);;取消调度工作函数bool cancel_work_sync(struct work_struct *work);,如果被取消的工作已经正在执行,则会等待它执行完成再返回;刷新工作队列函数void flush_workqueue(struct workqueue_struct *wq);,该函数会刷新工作队列,告诉内核尽快处理工作队列上的工作;删除工作队列函数void destroy_workqueue(struct workqueue_struct *wq);,该函数会删除自定义的工作队列。在驱动程序中想要使用共享工作队列时,需要在驱动的初始化函数中先调用create_workqueue 创建一个自定义的工作队列,然后调用INIT_WORK初始化工作,然后在中断处理函数即中断上文中调用queue_work函数调度这个工作队列,在驱动退出函数中依次调用cancel_work_sync、flush_workqueue、destroy_workqueue取消工作的调度、刷新自定义的工作队列、删除自定义的工作队列(可参考讯为Linux驱动视频第五期P12、P14)。在驱动中使用工作队列时可以通过以下步骤传递参数:1打包数据包;2在中断的下半部分拆包。如下图:

可以通过该container_of函数获取打包的结构体的地址(可参考讯为Linux驱动视频第五期P18)。

8.延迟工作队列的工作项通常会被放入一个队列中,并指定延迟时间,延迟时间到了,工作队列的工作线程就会执行这些工作项。Linux内核使用struct delayed_work结构体描述一个延迟工作,定义在include/linux/workqueue.h当中,如下图:

延迟工作可以使用静态初始化,其中n为工作的名字,f为与工作绑定的函数:

也可以使用动态初始化:

延迟工作可以放在共享工作队列上调度,对应的调度函数为static inline bool schedule_delayed_work(struct delayed_work *dwork,unsigned long delay);,其中delay是要延迟的时间,单位是节拍。也可以放在自定义的队列上调度,对应的调度函数为static inline bool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, unsigned long delay);。bool cancel_delayed_work_sync(struct delayed_work *dwork);函数用于取消已经调度的延迟工作。在驱动程序中想要使用延迟队列时,需要在驱动的初始化函数中调用INIT_DELAYED_WORK初始化延迟工作,然后在中断处理函数即中断上文中调用queue_delayed_work函数调度这个延迟工作,并在驱动退出函数中调用cancel_delayed_work_sync取消延迟工作的调度(可参考讯为Linux驱动视频第五期P16)。

9.前面提到的共享队列、自定义队列等一般是和CPU绑定的,有时候会导致性能低下,因此Linux提供了并发管理工作队列CMWQ来提高性能,主要通过函数struct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags,  int max_active, ...);实现,该函数用于创建一个工作队列,其中fmt是创建的工作队列的名称;flags可以是WQ_UNBOUND、WQ_FREEZABLE、WQ_HIGHPRI、WQ_CPU_INTENSIVE、WQ_MEM_RECLAIM,分别表示工作队列中的工作项可以由任意可用的工作线程处理,不受 CPU 核心的限制、工作队列中的工作项是可冻结的,当系统进入休眠状态时,内核会冻结这些工作项,避免工作项在休眠期间执行、工作队列的优先级较高,高优先级工作队列会尽快执行工作项、工作队列中的工作项是 CPU 密集型的任务、工作队列中的工作项可能会进行内存回收操作;max_active是线程池中最大的线程数。之前的工作队列创建函数create_workqueue是对alloc_workqueue函数的封装,所以并发管理工作队列CMWQ的使用除了创建队列的函数与之前的自定义队列不一样之外,其他的工作初始化、工作调度函数的使用都是一样的(可参考讯为Linux驱动视频第五期P20)。如下图:

10.中断线程化:中断线程化的处理仍然可以看作是将原来的中断分成中断上半部分和中断下半部分,上半部分还是用来处理紧急的事情,下半部分也是处理比较耗时的操作,但是下半部分会交给一个专门的内核线程来处理,这个内核线程只用于这个中断。当发生中断的时候,会唤醒这个内核线程,然后由这个内核线程来执行中断下半部分的函数。中断线程化与之前的中断的主要区别在于注册中断的函数需使用int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long flags, const char *name, void *dev);,其中irq是注册的中断号;handler是中断上文处理函数;thread_fn指定中断线程要执行的中断下文处理函数,若传入NULL则表示没有中断线程化;flags是中断标志位;name是中断的名称;dev供共享中断使用。其实之前的中断注册函数request_irq是对request_threaded_irq的封装(将其中的thread_fn传入NULL)。在驱动中使用中断线程化时需要在中断上文的处理函数中return IRQ_WAKE_THREAD;表示唤醒中断下文对应的处理线程,如下图(可参考讯为Linux驱动视频第五期P22):

11.本期知识点总结如下图: