说几句废fu之言,前几天没有接着写进程调度记录的文章,当然现在也不会写,如题,从现在开始记录linux内核基础知识。
中断处理程序原则:接受到一个中断,便立即开始执行应答或复位硬件,前提是在所有中断被禁止的额情况下完成。
中断和异常相关概念:
中断——异步的:
由硬件随机产生,在程序执行的任何时候可能出现
异常——同步的:
在(特殊的或出错的)指令执行时由CPU控制单元产生
中断处理程序重要函数:
First job.
1.注册、分配中断号irq、激活中断处理程序handler、设置标志掩码、署名设备ASCII名称、设置共享中断线:
kernel/irq/manage.c
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char*name,void* dev)
注:request_irq可能睡眠,不能在中断上下文或不允许阻塞的代码中调用。
换句话可以知道,中断上下文不允许睡眠,而该函数可能引起阻塞。
2.注销、中断处理程序,释放中断线
kernel/irq/manage.c
void free_irq(unsigned int irq,void* dev_id)
通常在驱动卸载时使用,必须在进程上下文中调用。
3.启用中断
kernel/irq/manage.c 中的 enable_irq:
static void __enable_irq(struct irq_desc *desc, unsigned int irq)
内部调用了 __enable_irq,首先上自旋锁,找到 irq_desc 结构体指针,判断嵌套深度,刷新 IRQ 状态,释放自旋锁。
参数desc: 指向 irq_desc 结构体的指针irq: 中断通道号
4.关闭中断
kernel/irq/manage.c 中的 disable_irq:
void disable_irq(unsigned int irq)
参数irq: 中断通道号
5.关闭中断 (无等待)
disable_irq 会保证存在的 IRQ handler 完成操作,而 disable_irq_nosync 立即关中断并返回。事实上,disable_irq 首先调用 disable_irq_nosync,然后调用 synchronize_irq 同步。
void disable_irq_nosync(unsigned int irq)
6.同步中断 (多处理器)
void synchronize_irq(unsigned int irq)
7.设置 IRQ 芯片
kernel/irq/chip.c:
set_irq_chip()
int set_irq_chip(unsigned int irq, struct irq_chip *chip)
8.设置 IRQ 类型
kernel/irq/chip.c: set_irq_type()
int set_irq_type(unsigned int irq, unsigned int type)
9.设置 IRQ 数据
kernel/irq/chip.c: set_irq_data()
150 int set_irq_data(unsigned int irq, void *data)
10.设置 IRQ 芯片数据
kernel/irq/chip.c: set_irq_chip_data()
int set_irq_chip_data(unsigned int irq, void *data)
重头戏
中断处理流程:
一、监视 IRQ 线,检查产生的信号。如果有两条以上的 IRQ 线上产生信号,就选择引脚编号较小的 IRQ 线。
二、如果一个引发信号出现在 IRQ 线上:
1.把接收到的引发信号转换成对应的向量号
2.把这个向量存放在中断控制器的一个 I/O 端口(0x20、0x21),从而允许 CPU 通过数据总线读此向量。
3.把引发信号发送到处理器的 INTR 引脚,即产生一个中断。
4.等待,直到 CPU 通过把这个中断信号写进可编程中断控制器的一个 I/O 端口来确认它;当这种情况发生时,清 INTR 线。
三、继续监控IRQ线。
处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。中断或异常被处理完后,相应的处理程序必须产生一条iret指令,把控制权转交给被中断的进程。
中断信息的保存
紧急的事情马上做,其他事情往后推。
中断处理程序首先要做:
1、将中断号压入栈中,以便找到对应的中断服务程序
2、将当前寄存器信息压入栈中,以便中断退出时恢复上下文
显然, 这两步都是不可重入的。因此在进入中断服务程序时,CPU 已经自动禁止了本 CPU 上的中断响应。
引为 n 的元素中存放着下列指令的地址:
pushl n-256
jmp common_interrupt
执行结果是将中断号 - 256 保存在栈中,这样栈中的中断都是负数,而正数用来表示系统调用。这样,系统调用和中断可以用一个有符号整数统一表示。
common_interrupt 的定义:
// arch/x86/kernel/entry_32.S
613 common_interrupt:
614 SAVE_ALL
615 TRACE_IRQS_OFF
616 movl %esp,%eax # 将栈顶地址放入 eax,这样 do_IRQ 返回时控制转到 ret_from_intr()
617 call do_IRQ # 核心中断处理函数
618 jmp ret_from_intr # 跳转到 ret_from_intr()
其中 SAVE_ALL 宏将被展开成:
cld
push %es # 保存除 eflags、cs、eip、ss、esp (已被 CPU 自动保存) 外的其他寄存器
push %ds
pushl %eax
pushl %ebp
pushl %edi
pushl %edx
pushl %ecx
pushl %ebx
movl $ _ _USER_DS, %edx
movl %edx, %ds # 将用户数据段选择符载入 ds、es
movl %edx, %es
处理中断
前面汇编代码的实质是,以中断发生时寄存器的信息为参数,调用 arch/x86/kernel/irq32.c 中的 do_IRQ 函数。
我们注意到 unlikely 和 unlikely 宏定义,它们的含义是
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
__builtin_expect 是 GCC 的内部机制,意思是告诉编译器哪个分支条件更有可能发生。这使得编译器把更可能发生的分支条件与前面的代码顺序串接起来,更有效地利用 CPU 的指令流水线。
do_IRQ 函数流程:
1、保存寄存器上下文
2、调用 irq_enter:// kernel/softirq.c
void irq_enter(void)
282 {
283 #if def CONFIG_NO_HZ
// 无滴答内核,它将在需要调度新任务时执行计算并在这个时间设置一个时钟中断,允许处理器在更长的时间内(几秒钟)保持在最低功耗状态,从而减少了电能消耗。
284 int cpu = smp_processor_id();
285 if (idle_cpu(cpu) && !in_interrupt())
286 tick_nohz_stop_idle(cpu); // 如果空闲且不在中断中,则停止空闲,开始工作
287 #end if
288 __irq_enter();
289 #if def CONFIG_NO_HZ
290 if (idle_cpu(cpu))
291 tick_nohz_update_jiffies(); // 更新 jiffies
292 #end if
293 }
// include/linux/hardirq.h
135 #define __irq_enter() \
/* 在宏定义函数中,do { ... } while(0) 结构可以把语句块作为一个整体,就像函数调用,避免宏展开后出现问题 */
136 do { \
137 rcu_irq_enter(); \
138 account_system_vtime(current); \
139 add_preempt_count(HARDIRQ_OFFSET); \ /* 程序嵌套数量计数器递增1 */
140 trace_hardirq_enter(); \
141 } while (0)
3、如果可用空间不足 1KB,可能会引发栈溢出,输出内核错误信息
4、如果 thread_union 是 4KB 的,进行一些特殊处理
5、调用 desc->handle_irq(irq, desc),调用 __do_IRQ() (kernel/irq/handle.c)
5.1 取得中断号,获取对应的 irq_desc
5.2 如果是 CPU 内部中断,不需要上锁,简单处理完就返回了
5.3 上自旋锁
5.4 应答中断芯片,这样中断芯片就能开始接受新的中断了。
5.5 更新中断状态。
IRQ_REPLAY:如果被禁止的中断管脚上产生了中断,这个中断是不会被处理的。当这个中断号被允许产生中断时,会将这个未被处理的中断转为 IRQ_REPLAY。
IRQ_WAITING:探测用,探测时会将所有没有中断处理函数的中断号设为 IRQ_WAITING,只要这个中断管脚上有中断产生,就把这个状态去掉,从而知道哪些中断管脚上产生过中断。
IRQ_PENDING、IRQ_INPROGRESS 是为了确保同一个中断号的处理程序不能重入,且不能丢失这个中断的下一个处理程序。具体地说,当内核在运行某个中断号对应的处理程序时,状态会设置成IRQ_INPROGRESS。如果发现已经有另一实例在运行了,就将这下一个中断标注为 IRQ_PENDING 并返回。这个已在运行的实例结束的时候,会查看是否期间有同一中断发生了,是则再次执行一遍。
5.6 如果链表上没有中断处理程序,或者中断被禁止,或者已经有另一实例在运行,则进行收尾工作。
5.7 循环:
释放自旋锁
执行函数链:handle_IRQ_event()。其中主要是一个循环,依次执行中断处理程序链表上的函数,并根据返回值更新中断状态。如果愿意,可以参与随机数采样。中断处理程序执行期间,打开本地中断。
上自旋锁
如果当前中断已经处理完,则退出;不然取消中断的 PENDING 标志,继续循环。
5.8 取消中断的 INPROGRESS 标志
5.9 收尾工作:有的中断在处理过程中被关闭了,->end() 处理这种情况;释放自旋锁。
6、执行 irq_exit(),在 kernel/softirq.c 中
6.1 递减中断计数器
6.2 检查是否有软中断在等待执行,若有则执行软中断。
6.3 如果使用了无滴答内核看是不是该休息了。
7、恢复寄存器上下文,跳转到 ret_from_intr (跳转点早在 common_interrupt 中就被指定了)
在中断处理过程中,我们反复看到对自旋锁的操作。在单处理器系统上,spinlock是没有作用的;在多处理器系统上,由于同种类型的中断可能连续产生,同时被几个 CPU处理(注意,应答中断芯片是紧接着获得自旋锁后,位于整个中断处理流程的前部,因此在中断处理流程的其余部分,中断芯片可以触发新的中断并被另一个CPU 开始处理),如果没有自旋锁,多个 CPU 可能同时访问 IRQ 描述符,造成混乱。因此在访问 IRQ 描述符的过程中需要有spinlock 保护。
下半部的事
三足鼎力:软中断、tasklet、工作队列
软中断:
软中断作为下半部机制的代表,是随着SMP(share memory processor)的出现应运而生的,它也是tasklet实现的基础(tasklet实际上只是在软中断的基础上添加了一定的机制)。
软中断一般是“可延迟函数”的总称,有时候也包括了tasklet(请读者在遇到的时候根据上下文推断是否包含tasklet)。它的出现就是因为要满足上面所提出的上半部和下半部的区别,使得对时间不敏感的任务延后执行,而且可以在多个CPU上并行执行,使得总的系统效率可以更高。
它的特性包括:
a)产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不能被自己打断,只能被硬件中断打断(上半部)。
b)可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。
tasklet:
由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。
tasklet是由软中断引出的,是IO驱动程序实现可延迟函数的首选方法。 内核定义了两个软中断掩码HI_SOFTIRQ和TASKLET_SOFTIRQ(两者优先级不同), 这两个掩码对应的软中断处理函数作为入口, 进入tasklet处理过程.
它具有以下特性:
a)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。
b)多个不同类型的tasklet可以并行在多个CPU上。
c)软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。
tasklet是在两种软中断类型的基础上实现的,因此如果不需要软中断的并行特性,tasklet就是最好的选择
工作队列
软中断不能睡眠、不能阻塞。由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。但可阻塞函数不能用在中断上下文中实现,必须要运行在进程上下文中,例如访问磁盘数据块的函数。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟.
工作队列有着自己的处理线程, 这些work被推迟到这些线程中去处理. 处理过程只可能发生在这些工作线程中, 所以这里可以睡眠.
内核默认启动了一个工作队列, 对应一组工作线程events/n(n代表处理器编号, 这样的线程有n个). 驱动程序可以直接向这个工作队列添加任务. 某些驱动程序还可能会创建并使用属于自己的工作队列.
因此在2.6版的内核中出现了在内核态运行的工作队列(替代了2.4内核中的任务队列)。它也具有一些可延迟函数的特点(需要被激活和延后执行),但是能够能够在不同的进程间切换,即可在进程上下文中完成任务,这就是工作队列的关键。
(未完)
中断处理程序编写
(1)他运行在中断上下文,所以不能使用可能引起阻塞或者调度的函数。否则实时性得不到满足。
(2)一开始判断是否产生中断
(3)清除中中断标志
(4)硬件相关操作
网友评论