Linux内核设计与实现(8)第八章:中断下半部的处理

1. 中断背景,定义;分类;上下部机制;中断号;中断上下文等

1.定义: 中断是硬件在需要的时候向CPU发出信号,
	   CPU暂时停止正在进行的工作,来处理硬件请求的一种机制

2.背景:没有中断的话,CPU和外围设备之间协同工作/通信可能只有轮询这个方法:
	   CPU定期检查硬件状态,需要处理时就处理,否则就跳过。
		轮询的缺点/引入中断机制

3.类型:Linux中通常分为外部中断(又叫硬件中断)和内部中断(又叫异常)
	1. 同步中断(异常/内部中断):同步中断由CPU本身产生,又称为内部中断或异常
		同步中断举例:缺页中断
	2. 异步中断(中断/外部中断):异步中断是由外部硬件设备产生,又称为外部中断或中断
		异步中断举例:网卡的工作原理
		
4.中断号:中断对应着一个中断号,内核通过这个中断号查找相应的中断服务程序

5.中断上下文:中断服务程序不在进程上下文中执行,
			而是在一个与所有进程都无关的、专门的中断上下文中运行
		

6.中断和信号:中断: 硬件/进程发,内核收
		    信号:内核发,进程收

7.中断与异常:
	1. 异常与中断不同,中断是由硬件引起的;
	2. 异常则发生在编程失误而导致错误指令,
	   或者在执行期间出现特殊情况必须要靠内核来处理的时候

8.上下半部机制:
	1. 背景:中断处理程序运行需要快速执行(因为不可阻塞),
			同时要能完成尽可能多的工作,这里存在矛盾
	2. 上下半部: 因此把中断处理切分为两个部分
	3. 优点:这种设计可以使系统处于中断屏蔽状态的时间尽可能的短,以此来提高系统的响应能力。
	4. 上半部:处理紧急功能,取寄存器状态
	5. 下半部:完成中断事件绝大多数任务
	6. 上下半部划分原则:
		1) 如果一个任务对时间非常敏感,将其放在中断处理程序中执行;
		2) 如果一个任务和硬件有关,将其放在中断处理程序中执行;
		3) 如果一个任务要保证不被其他中断打断,将其放在中断处理程序中执行;
		4) 其他所有任务,考虑放置在下半部执行	

详细参考之前的文章:
Linux 中断
https://blog.csdn.net/lqy971966/article/details/111196470

2. 中断上下半部机制背景:

2.1 背景:

中断处理程序运行需要快速执行(因为不可阻塞),同时要能完成尽可能多的工作,这里存在矛盾。

2.2 解决:

因此把中断处理切分为两个部分,上半部分(top half)接收到一个中断后立即执行,但是只做有严格时限的工作,例如对接收到的中断进行应答或复位硬件。能够被允许稍后完成的工作会推迟到下半部分(bottom half)去,此后在合适的时机下半部分会被中断执行,Linux提供了实现下半部分的各种机制。

优点:这种设计可以使系统处于中断屏蔽状态的时间尽可能的短,以此来提高系统的响应能力。

2.3 举例:用网卡收包来解释一下这两半

当网卡接受到数据包时,通知内核,触发中断。
所谓的上半部就是,及时读取数据包到内存,防止因为延迟导致丢失,这是很急迫的工作。
读到内存后,对这些数据的处理不再紧迫,此时内核可以去执行中断前运行的程序,而对网络数据包的处理则交给下半部处理。

2.4 上下半部划分原则

1) 如果一个任务对时间非常敏感,将其放在中断处理程序中执行;
 2) 如果一个任务和硬件有关,将其放在中断处理程序中执行;
 3) 如果一个任务要保证不被其他中断打断,将其放在中断处理程序中执行;
 4) 其他所有任务,考虑放置在下半部执行

3. 上半部:

我们把中断处理切为两半。中断处理程序是上半部——接受中断,他就立即开始执行,但只有做严格时限的工作。
特点:上半部简单快速,执行时禁止一些或者全部中断。
工作内容:处理紧急功能,取寄存器状态。

4. 下半部:

能够被允许稍后完成的工作会推迟到下半部去并在合适的时机执行下半部。

工作内容:完成中断事件绝大多数任务。
特点:1. 允许响应所有的中断;
	  2. 执行时间并不确定

4.1 下半部的三种实现方式:

软中断实现 
tasklet 实现
工作队列实现。

4.2 下半部实现机制之软中断

4.2.1 定义,特点,应用,类型

1.定义:
软中断指一组静态定义的下半部接口,共32个,可以在所有处理器上同时执行,软中断必须在编译期间就进行静态注册(最多只能注册32个,当前内核已使用9个)

2 特点:
软中断一般是“可延迟函数”的总称,它不能睡眠,不能阻塞,它处于中断上下文,不能进程切换。
有时候也包括了 tasklet

3 应用:
软中断保留给系统中对时间要求最严格以及最重要的下半部使用,目前只有两个子系统(网络、SCSI)直接使用软中断。

4 特性包括:

a)产生后并不是马上可以执行,必须要等待内核的调度才能执行。
	软中断不能被自己打断,只能被硬件中断打断(上半部)。
b)可以并发运行在多个CPU上

5 软中断类型(目前有10个)
定义在 include/linux/interrupt.h 文件中

enum
{
	HI_SOFTIRQ=0,
	TIMER_SOFTIRQ,
	NET_TX_SOFTIRQ,
	NET_RX_SOFTIRQ,
	BLOCK_SOFTIRQ,
	IRQ_POLL_SOFTIRQ,
	TASKLET_SOFTIRQ,
	SCHED_SOFTIRQ,
	HRTIMER_SOFTIRQ,
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

	NR_SOFTIRQS
};

4.2.2 软中断执行函数

注册软中断的函数 open_softirq
触发软中断的函数 raise_softirq
执行软中断 do_softirq
执行相应的软中断 - 执行自己写的中断处理

4.3 下半部实现机制之 tasklet

4.3.1 tasklet 背景:

由于软中断用轮询的方式处理,导致其效率低下。
假如正好是最后一种中断,则必须循环完所有的中断类型,才能最终执行对应的处理函数。
显然当年开发人员为了保证轮询的效率,于是限制中断个数为32个。

4.3.2 tasklet 出现:

为了提高中断处理数量,顺道改进处理效率,于是产生了tasklet机制
Tasklet采用无差别的队列机制,有中断时才执行,免去了循环查表之苦。

tasklet 其实是一种在性能和易用性之间寻求平衡的产物,对于大部分下半部处理来说,使用tasklet就足够了,对于网络这种性能要求较高的情况才需要使用软中断

4.3.3 tasklet 本身也是软中断

tasklet 是通过软中断实现的,所以它本身也是软中断。
tasklet 实际上只是在软中断的基础上添加了一定的机制(其实就是基于软中断又封装了一下)
使用:
所以除了对性能要求特别高的情况,一般建议使用tasklet来实现自己的中断。

4.3.4 tasklet的优点:

无类型数量限制;
效率高,无需循环查表;
支持SMP机制;

4.3.5 tasklet状态只有3种值:

值 0 表示该tasklet没有被调度
值 TASKLET_STATE_SCHED 表示该tasklet已经被调度
值 TASKLET_STATE_RUN 表示该tasklet已经运行

引用计数器count 的值不为0,表示该tasklet被禁止。

4.4 下半部实现机制之工作队列(work queue)

4.4.1 工作队列定义,特点,应用场景

工作队列子系统是一个用于创建内核线程的接口,通过它可以创建一个工作者线程来专门处理中断的下半部工作。
工作队列和tasklet不一样,不是基于软中断来实现的。

特点:
工作队列可以把工作推后,交由一个内核线程去执行,因此它是唯一能够在进程上下文中运行的下半部实现机制,也只有它才可以睡眠。

应用场景:
当需要用一个可以重新调度的实体来执行下半部处理的时候,比如需要获得大量内存、需要获取信号量、以及需要执行阻塞式的IO操作时,就应该选择工作队列。

4.4.2 工作者线程及源码分析

4.2.2.1 工作者线程

工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的任务,它创建的这些内核线程称为工作者线程。

驱动程序可以使用工作队列来创建一个专门的工作者线程来处理需要推后的工作。工作队列子系统提供了一个缺省的工作者线程events/n(n为CPU编号),以处理被推后的工作

4.4.2.2 工作者线程源码分析

工作队列主要的3个结构体 work_struct cpu_workqueue_struct workqueue_struct

/* 在 include/linux/workqueue.h 文件中定义 */
struct work_struct {
	atomic_long_t data;             /* 这个并不是处理函数的参数,而是表示此work是否pending等状态的flag */
#define WORK_STRUCT_PENDING 0        /* T if work item pending execution */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
	struct list_head entry;         /* 中断下半部处理函数的链表 */
	work_func_t func;               /* 处理中断下半部工作的函数 */
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

/* 在 kernel/workqueue.c文件中定义
* 每个工作者线程对应一个 cpu_workqueue_struct ,其中包含要处理的工作的链表
* (即 work_struct 的链表,当此链表不空时,唤醒工作者线程来进行处理)
*/
/*
* The per-CPU workqueue (if single thread, we always use the first
* possible cpu).
*/
struct cpu_workqueue_struct {

	spinlock_t lock;                   /* 锁保护这种结构 */

	struct list_head worklist;         /* 工作队列头节点 */
	wait_queue_head_t more_work;
	struct work_struct *current_work;

	struct workqueue_struct *wq;       /* 关联工作队列结构 */
	struct task_struct *thread;        /* 关联线程 */
} ____cacheline_aligned;

/* 也是在 kernel/workqueue.c 文件中定义的
* 每个 workqueue_struct 表示一种工作者类型,系统默认的就是 events 工作者类型
* 每个工作者类型一般对应n个工作者线程,n就是处理器的个数
*/
/*
* The externally visible workqueue abstraction is an array of
* per-CPU workqueues:
*/
struct workqueue_struct {
	struct cpu_workqueue_struct *cpu_wq;  /* 工作者线程 */
	struct list_head list;
	const char *name;
	int singlethread;
	int freezeable;        /* Freeze threads during suspend */
	int rt;
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

分析:
工作者线程用workqueue_struct表示,该结构内部维护了一个cpu_workqueue_struct的数组,数组的每一项对应系统中一个处理器。所有的工作者线程都是用普通的内核线程实现的,它们都要执行worker_thread()函数,在它初始化完成以后,这个函数执行一个死循环并开始休眠,当有操作被插入到队列里的时候,线程就被唤醒,并执行这些操作。工作用work_struct表示,每个处理器上的每种类型的队列(cpu_workqueue_struct的数组)都有一个该结构的链表,当一个工作者线程被唤醒时,它会执行它链表上的所有工作,执行完毕后就移除该工作,当链表上不再有对象时,它就会继续休眠。

4.5 三种方式总结

下半部机制	上下文	复杂度								    执行性能
软中断		中断	高 (需要自己确保软中断的执行顺序及锁机制)	好 (全部自己实现,便于调优)
tasklet		中断	中 (提供了简单的接口来使用软中断)			中
工作队列	    进程	低 (在进程上下文中运行,与写用户程序差不多,	差
					    可以睡眠)	

参考:
书籍
https://www.cnblogs.com/wang_yb/archive/2013/04/23/3037268.html
https://zhuanlan.zhihu.com/p/70958648

猜你喜欢

转载自blog.csdn.net/lqy971966/article/details/119571137