目录
0. 引言 1. Linux 中断的概念 2. Linux 中断相关的源代码分析 3. Linux 硬件中断 4. Linux 软中断 5. 中断优先级 6. CPU在关中断状态下编程要注意的事项
0. 引言
中断是现代计算机体系结构的重要组成部分。现代体系结构的基本输入输出方式有三种
1. 程序查询: CPU周期性询问外部设备是否准备就绪。该方式的明显的缺点就是浪费CPU资源,效率低下。但是在特定的场景下这种"程序查询"的方式还有有它的用武之地的 例如,在网络驱动中,通常接口(Interface)每接收一个报文,就发出一个中断。而对于高速网络,每秒就能接收几千个报文,在这样的负载下,系统性能会受到极大的损害。为了提高系统性能,内核开发者已经为网络子系统开发了一种可选的基于查询的接口NAPI(代表new API)。当系统拥有一个高流量的高速接口时,系统通常会收集足够多的报文,而不是马上中断CPU 2. 中断方式 这是现代CPU最常用的与外围设备通信方式。相对于轮询,该方式不用浪费稀缺的CPU资源,所以高效而灵活。中断处理方式的缺点是每传送一个字符都要进行中断,启动中断控制器,还要保留和恢复现场以便能继续原程序的执行,花费的工作量很大,这样如果需要大量数据交换,系统的性能会很低 3. DMA方式 通常用于高速设备,设备请求直接访问内存,不用CPU干涉。但是这种方式需要DMA控制器,增加了硬件成本。在进行DMA数据传送之前,DMA控制器会向CPU申请总线控制 权,CPU如果允许,则将控制权交出,因此,在数据交换时,总线控制权由DMA控制器掌握,在传输结束后,DMA控制器将总线控制权交还给CPU
今天我们要重点学习的就是"中断"这种输入输出机制
Relevant Link:
http://blog.sina.com.cn/s/blog_5d0e8d0d01019cds.html
1. Linux 中断的概念
今天我们谈论"中断",会发现这个词在很多场景下都会以不同的身份出现,我们今天对这些概念进行一个梳理
对于这张图,有一点需要说明的是,在linux 2.6内核之后,系统调用已经不使用int 0x80的方式进行了,关于这部分的知识,请参阅另一篇文章
http://www.cnblogs.com/LittleHann/p/4111692.html
0x1: 中断的分类
翻阅不同的书籍和内核相关资料,我们会发现关于中断的分类有很多种标准,事实情况也就是这样的,根据不同的观察角度,可以有如下的分类
1. 异步(外部中断) OR 同步(内部中断):根据发生中断的时机来分类
1. 异步中断(外部中断) 外部中断是由外部设备引发的中断,这类中断的特点是中断的发生完全是"瞬发"的,例如 1) 鼠标 2) 键盘 2. 同步中断(内部中断) 从CPU的角度看,内部中断是一个同步事件,它是执行某条指令时产生的,例如 1) 缺页异常 2) 除零异常 3) 系统调用systemcall trap
2. 可屏蔽中断 OR 不可屏蔽中断:根据处于中断状态中是否可以被其他中断递归打断来分类
中断的屏蔽基于CPU中的"中断屏蔽寄存器"进行实现
1. 可被屏蔽中断:允许延迟响应 2. 不可被屏蔽中断:高实时场景需求,必须立即被响应
3. 硬件中断 OR 软件中断:根据中断源来分类
1. 软件中断 1) Ring3代码中触发的指令中断 2) 操作系统自身触发的缺页中断 这是操作系统提供的对CPU中断的一个接口,以中断号的形式进行编号 2. 硬件中断 1) 外部硬件设备触发的硬件中断(电平信号)
可以看到,中断归根结底是CPU提供的一种"硬件机制",是CPU的引脚在最底层的硬件实现
0x2: 处理中断
这里讨论的是在CPU底层硬件这个层面对中断的处理
当CPU得知发生中断之后,它将进一步的处理委托给一个"软件例程",该例程可能会修复故障、提供专门的处理或将外部事件通知用户进程。由于每个CPU中断和异常都有一个唯一的编号,内核使用一个数组,数组项是指向处理程序函数的指针(通过中断号进行数组索引)
一个完整的中断处理可以划分为3部分
1. 建立一个适当的环境,即保存现场(entry path) 进入路径的关键任务有: 1) 从用户态栈切换到和心态栈(内存栈的切换) 2) 保存用户应用程序当前的寄存器状态,以便在中断活动结束后恢复 2. 调用处理程序自身 3. 将系统还原,即还原现场(exit path) 退出路径的关键任务有: 1) 调度器是否应该选择一个新进程代替旧的进程 2) 是否有信号必须投递到原进程 3) 从中断返回后,只有确认了以上1.)、2.)问题后,内核才能完成其常规任务,即还原寄存器集合、切换到用户态栈、切换到适用于用户应用程序的适当的处理器状态
中断服务例程(interrupt service routine ISR),或者叫(中断处理程序 interrupt handler)严格来说是指在进入路径和退出路径之间执行、由C语言实现的例程
需要明白的是,中断是CPU的一种机制,因此在中断例程的执行中,CPU是完全被独占的(在不考虑屏蔽、中断级的情况下),因此,ISR(interrupt service routine)必须满足如下两个要求
1. 实现(特别是在禁用其他中断时,即最高中断级情况下)的代码逻辑应该包含尽可能少的代码,已支持快速处理 2. 可以在其他ISR执行期间调用的中断处理程序例程,不能彼此干扰(即对全局资源、同步操作的锁操作)
内核在处理这两个问题的时候,采取了根据ISR重要性对执行代码进行分段的思路。
通常,并非ISR的每个部分都同等重要,可以将它们分为3个部分
1. 关键操作必须在中断发生后立即执行。否则,无法维持系统的稳定性,或计算机的正确运作,在执行此类操作期间,必须禁用其他中断 2. 非关键操作也应该尽快执行,但允许启用中断(因而可能被其他系统事件中断) 3. 可延期操作不是特别重要,不必在中断处理程序中实现。内核可以延迟这些操作,在时间充裕时进行。内核提供了tasklet,用于在稍后执行可延期操作
0x3: CPU中断处理例程代码分析(数据结构)
中断技术上的实现有两方面:
1. 汇编语言代码:与处理器高度相关,用于处理特定平台上相关的底层细节 2. 抽象接口:设备驱动程序及其他内核代码安装和管理IRQ处理程序所需的
本文将重点学习抽象接口方面的相关原理,关于体系相关汇编代码的部分,可以参考体系结构方面的书籍或手册
为了响应外部设备的中断(硬件中断),内核必须为每个潜在的IRQ提供一个"抽象接口函数",该"抽象接口函数"必须能够动态注册和注销IRQ相关信息管理的关键点是一个全局数组,每个数组项对应一个IRQ编号,因此数组位置和中断号是相同的(例如:IRQ0在位置0、IRQ15在位置15)
该数组的定义如下
sourcecode\kernel\irq\handle.c
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = { /* IRQ的最大数量主要取决于辅助CPU管理IRQ的辅助芯片 1. alpha: 32个中断 2. wildfire: 2048个中断 3. IA-64: 256个中断 4. IA-32、8256A控制器: 16个中断 */ [0 ... NR_IRQS-1] = { .status = IRQ_DISABLED, .chip = &no_irq_chip, //handle_bad_irq对没有安装处理程序的中断进行确认 .handle_irq = handle_bad_irq, .depth = 1, .lock = __SPIN_LOCK_UNLOCKED(irq_desc->lock), } };
在Linux 2.6的内核开发期间,重构出了一个新的通用的IRQ子系统。它能够以统一的方式处理不同的中断控制器和不同类型的中断
1. 高层ISR(high-level interrupt service routines 高层中断服务例程) 针对设备驱动程序端(或其他内核组件)的中断,执行由此引起的所有必要的工作。例如,如果设备使用中断通知一些数据已经到达,那么高层ISR的工作应该是将数据复制到适当的位置 2. 中断电流处理(interrup flow handling) 处理不同的中断电流类型之间的各种差别,如 1) 边沿触发(edge-triggering): 硬件通过感知线路上的点位差来检测中断 2) 电平触发(level-triggering): 根据特定的电势值检测中断,与电势是否改变无关 3. 芯片级硬件封装(chip-level hardware encapsultion) 需要与在电子学层次上产生中断的底层硬件直接通信,该抽象层可以视为中断控制器的某种"设备驱动程序"
我们继续回到irq_desc[NR_IRQS]这个保存中断处理程序的全局数组上来,用于表示IRQ描述符的结构定义请参阅另一篇文章
http://www.cnblogs.com/LittleHann/p/3865490.html
从内核中高层代码的角度来看,每个IRQ都可以由该结构完全描述,事实上,操作系统的每一种机制在背后都一定有一个完善的数据结构提供支持,回到上面的那张IRQ子系统的架构图
其中的3个抽象层在该结构(struct irq_desc)中表示如下
1. 电流层ISR: 由handle_irq提供 handler_data可以指向任意数据,该数据可以是特定于IRQ或处理程序的。每当发生中断时,特定于体系结构的代码都会调用handle_irq。该函数负责使用chip中提供的特定于控制器的方法,进行处理中断所必需的一些底层操作 2. action提供了一个操作链: 需要在中断发生时执行 由中断通知的设备驱动程序,可以将与之相关的处理程序函数放置在此处 3. 电流处理和芯片相关操作被封装在chip中 chip_data指向可能与chip相关的任意数据 4. name指定了电流层处理程序的名称,将显示在/proc/interrupts中。对边沿触发中断,通常是"edge",对电平触发中断,通常是"level" 5. delth: 用于确定IRQ电路是启用的还是禁用的 1) 0: 表示启用 2) 正值: 表示禁用 6. irq_count、irq_unhandled字段提供了一些统计量,可用于检测停顿和未处理,但持续发生的中断。即假中断(spurious interrupt) 7. status: 描述了IRQ的当前状态 IRQ不仅可以在处理程序安装期间改变其状态,而且可以在运行时改变,根据status当前的值,内核很容易获知某个IRQ的状态,而无需了解底层实现的硬件相关特性 irq.h中定义了各种表示当前状态的常数,可用于描述IRQ电路当前的状态。每个常数表示位串中的一个置为的标志位(可以同时设置)
1. 高层ISR(high-level interrupt service routines 高层中断服务例程):irq_desc->irq_chip
http://www.cnblogs.com/LittleHann/p/3865490.html //搜索:struct irq_chip //搜索:struct irqaction
2. 中断电流处理(interrup flow handling):irq_desc->irq_flow_handler_t
在2.6之后的内核代码中,电流处理的实现已经被极大地简化了,内核代码封装了大量的体系结构相关的代码,并在高层提供了一个几乎可以使用于所有硬件的通用框架
1. 设置控制器硬件 内核提供了一系列的标准函数,用于注册irq_chip和设置电流程序 1) extern int set_irq_chip(unsigned int irq, struct irq_chip *chip); 将一个IRQ芯片以irq_chip实例的形式关联到某个特定的中断 2) static inline void set_irq_handler(unsigned int irq, irq_flow_handler_t handle); 3) static inline void set_irq_chained_handler(unsigned int irq, irq_flow_handler_t handle); 为某个给定的IRQ编号设置电流处理程序 4) extern void set_irq_chip_and_handler(unsigned int irq, struct irq_chip *chip, irq_flow_handler_t handle); 5) extern void set_irq_chip_and_handler_name(unsigned int irq, struct irq_chip *chip, irq_flow_handler_t handle, const char *name); 这是一个快捷方式,它相当于连续调用上述的各函数 2. 电流处理 1) 边沿触发中断 2) 电平触发中断 3) 其他中断类型
0x4: 初始化和分配IRQ
1. 注册IRQ 由设备驱动程序动态注册ISR的工作,IRQ注册的过程就是基础数据结构的创建和填充的过程 2. 释放IRQ 释放IRQ的过程和注册的过程正好相反,即将相关的数据结构从内核的一般数据结构中删除 3. 注册中断 我们知道,CPU需要响应的中断由两部分引起 1) 系统外设的中断请求所引发的中断 2) 由处理器本身、或用户进程中的软件机制所引发的中断
和硬件产生的中断IRQ相比,CPU或用户程序产生的软中断所使用的编号在初始化时就是已知的,此后不会改变,中断和异常的注册在内核初始化时进行,其分配在运行时并不会改变
0x5: 处理IRQ
在注册了IRQ处理程序之后,每次发生中断时将执行处理程序例程(ISR interruot service routines),为了协调不同平台差异的问题,IRQ中断处理逻辑这块的代码不仅涉及到平台相关的各个C函数,还会涉及到底层硬件相关的汇编代码。即使这样,从大的整体概念上来讲,不同平台上中断的整体框架还是基本相同
1. 进入路径从用户态切换到内核态 2. 执行实际的处理程序例程 3. 从和心态切换回用户态
Relevant Link:
http://www.cnblogs.com/LittleHann/p/3850655.html
http://www.cnblogs.com/LittleHann/p/3850653.html
http://www.cnblogs.com/LittleHann/p/3871630.html
http://blog.csdn.net/droidphone/article/details/7445825
2. Linux 中断相关的源代码分析
中断是一个CPU硬件级的机制,是CPU为上层提供的一种响应机制,就像一棵树的根,因为有了中断,才有了上层的一些很有用的功能,例如
1. 系统调用(在2.6以后,系统调用不采用中断而是采用sysenter进入内核了) 2. 键盘鼠标外设 3. 硬盘的数据传输 4. 网卡的数据通信 ...
我们从源代码角度逐步分析一下CPU中断的整个逻辑流程
0x1: 切换到和内核态
到内核态的切换,是基于每个中断之后由处理器自动执行的汇编代码的(暂不考虑Linux sysenter)。该代码是C语言和汇编语言的混合体,位于:sourcecode\arch\x86\kernel\entry_32.S中,其中定义了各个入口点,在中断发生时处理器可以将控制流转到这些入口点。通常,只有那些最为必要的操作直接在汇编语言代码中执行,内核试图尽快的返回到常规的C代码,因为C代码更容易处理。为此,必须创建一个环境。在C语言中调用函数时,需要将所需的数据(返回地址和参数)按一定的顺序放到栈上,在用户态和内核态之间切换时,还需要将最重要的寄存器保存到栈上,以便以后恢复。这2个操作由平台相关的汇编代码执行。
sourcecode\arch\x86\kernel\entry_32.S
.. common_interrupt: addl $-0x80,(%esp) /* Adjust vector into the [-256,-1] range */ SAVE_ALL TRACE_IRQS_OFF movl %esp,%eax call do_IRQ jmp ret_from_intr ENDPROC(common_interrupt) CFI_ENDPROC ..
在大多数平台上,控制流接下来传递到C函数do_IRQ,其实现也是平台相关的,但在Linux 2.6内核之后,情况仍然得到了很大的简化,根据平台不同,该函数的参数或者是处理器寄存器的集合
1. x86 \linux-3.15.5\arch\x86\kernel\irq.c __visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs) 2. arm \linux-3.15.5\arch\arm\kernel\irq.c asmlinkage void __exception_irq_entry asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
pt_regs用于保存内核使用的寄存器集合,各个寄存器的值被依次压栈(通过汇编代码),在C函数调用之前,一直保存在栈上(为了遵循C函数的调用约定)。pt_regs的定义可以确保栈上的各个寄存器项与该结构的各个成员相对应,这些值并不是仅仅保存用于后续的使用,C代码也可以读取这些值
struct pt_regs的定义是平台相关的,因为不同的处理器提供了不同的寄存器集合,pt_regs中包含了内核使用的寄存器,其中不包含的寄存器,可能只能由用户态应用程序使用
1. IA-32 CPU体系结构下,pt_regs通常定义如下 \linux-3.15.5\arch\x86\include\asm\ptrace.h struct pt_regs { unsigned long bx; unsigned long cx; unsigned long dx; unsigned long si; unsigned long di; unsigned long bp; unsigned long ax; unsigned long ds; unsigned long es; unsigned long fs; unsigned long gs; unsigned long orig_ax; unsigned long ip; unsigned long cs; unsigned long flags; unsigned long sp; unsigned long ss; }; #else /* __i386__ */ struct pt_regs { unsigned long r15; unsigned long r14; unsigned long r13; unsigned long r12; unsigned long bp; unsigned long bx; /* arguments: non interrupts/non tracing syscalls only save up to here*/ unsigned long r11; unsigned long r10; unsigned long r9; unsigned long r8; unsigned long ax; unsigned long cx; unsigned long dx; unsigned long si; unsigned long di; unsigned long orig_ax; /* end of arguments */ /* cpu exception frame or undefined */ unsigned long ip; unsigned long cs; unsigned long flags; unsigned long sp; unsigned long ss; /* top of stack page */ }; 2. PA-Risc CPU体系结构下 \linux-3.15.5\arch\parisc\include\uapi\asm\ptrace.h struct pt_regs { unsigned long gr[32]; /* PSW is in gr[0] */ __u64 fr[32]; unsigned long sr[ 8]; unsigned long iasq[2]; unsigned long iaoq[2]; unsigned long cr27; unsigned long pad0; /* available for other uses */ unsigned long orig_r28; unsigned long ksp; unsigned long kpc; unsigned long sar; /* CR11 */ unsigned long iir; /* CR19 */ unsigned long isr; /* CR20 */ unsigned long ior; /* CR21 */ unsigned long ipsw; /* CR22 */ };
我们知道,在IA-32体系结构的系统上,被引发中断的编号(中断号)保存在orig_eax的高8位中,这是个很重要的概念,即传统的"基于80中断的系统调用"、以及"外设引发的中断",都必须将中断号置入eax寄存器中,然后再触发trap陷门
0x2: 执行中断服务例程(ISR interrupt service routines)
在触发了CPU中断,ENTRY_32.S将寄存器集合的当前状态传递到do_IRQ
__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs) { //将一个指向寄存器集合的指针保存在一个全局的CPU变量中(中断发生之前,变量中保存的旧指针会保留下来,供后续使用)。需要访问寄存器集合的中断处理程序,可以从该变量中访问 struct pt_regs *old_regs = set_irq_regs(regs); /* high bit used in ret_from_ code */ unsigned vector = ~regs->orig_ax; unsigned irq; /* irq_enter负责更新一些统计量,对于具备动态时钟周期特性的系统,如果系统已经有很长一段时间没有发生时钟中断,则更新全局计时变量jiffiles 接下来,调用所述IRQ注册的ISR的任务委托给体系结构无关的函数generic_handle_irq,它调用irq_desc[irq]->handle_irq来激活电流控制处理程序 */ irq_enter(); exit_idle(); irq = __this_cpu_read(vector_irq[vector]); if (!handle_irq(irq, regs)) { ack_APIC_irq(); if (irq != VECTOR_RETRIGGERED) { pr_emerg_ratelimited("%s: %d.%d No irq handler for vector (irq %d)\n", __func__, smp_processor_id(), vector, irq); } else { __this_cpu_write(vector_irq[vector], VECTOR_UNDEFINED); } } /* irq_exit负责记录一些统计量,另外还要调用do_softirq来处理任何待决的软件IRQ */ irq_exit(); set_irq_regs(old_regs); return 1; }
在实现处理程序例程时,必须要注意一些要点,这些会极大地影响系统的性能和稳定性
1. 限制 需要注意的是,内核代码的运行会处于两种上下文状态 1) 中断上下文(interrupt context) 2) 常规上下文 在实现ISR(interrupt service routines)时,ISR一定是在"中断上下文(interrupt context)"中执行的,为了区分当前内核的这两种情况,内核提供了in_interrupt函数,用于指明当前是否在处理中断 /* 中断上下文和普通上下文的区别之处在于 0x1: 中断是异步执行的,它可以在任何时间发生。因而从用户空间来看,处理程序例程不是在一个明确定义的环境中执行。这种情况下,禁止访问用户空间,特别是与用户空间地址之间来回复制内存数据的行为。 例如:网络驱动程序不能将接收到的数据直接转发到等待的应用程序,因为内核无法确定等待数据的应用程序此时是否正在运行(是否获得CPU调度资源) 0x2: 中断上下文中不能调用调度器,因而不能自愿地放弃控制权 换句话说,这个时候不能使用自旋锁等操作,因为在关中断的情况下,CPU已经不响应外部的中断请求了,这个时候进行CPU调度会直接导致panic 0x3: 处理程序例程不能进入睡眠状态。只有在外部事件导致状态改变并唤醒进程时,才能解除睡眠状态。但中断上下文不允许中断,进程睡眠后,内核只能永远等待下去,因为也不能调用调度器,不能选择进程来执行。 要特别注意的是:只确保处理程序例程的直接代码不进入睡眠状态这是不够的,其中调用的所有过程和函数(以及这些函数的子函数/子过程,以此类推)都不能进入睡眠状态 */ 2. 实现处理程序 中断处理程序只能使用两种返回值: 1) 正确地处理了IRQ: 返回IRQ_HANDLED 2) ISR不负责该ISR: 返回IRQ_NONE
0x3: 退出路径
当ISR执行完毕后,代码逻辑继续回到ENTRY_32.S中,代码对当前内核栈和寄存器进行现场恢复,准备回到用户态继续运行
ENTRY(resume_userspace) LOCKDEP_SYS_EXIT DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don‘t miss an interrupt # setting need_resched or sigpending # between sampling and the iret TRACE_IRQS_OFF movl TI_flags(%ebp), %ecx andl $_TIF_WORK_MASK, %ecx # is there any work to be done on # int/exception return? jne work_pending jmp restore_all END(ret_from_exception)
Relevant Link:
深入linux内核架构(中文版).pdf 14章
3. Linux 硬件中断
Relevant Link:
http://blog.csdn.net/droidphone/article/details/7445825
4. Linux 软中断
回到之前的那张CPU中断示意图,CPU的中断机制是CPU提供的一种硬件机制,从中断源来说,有两种中断
1. 来自外设硬件的硬件中断,电气级别的中断触发 2. 由CPU自身执行中触发的软中断
软中断(software interrupt)是完全用软件实现的,内核借助软中断来获知异常情况的发生,而该情况将在稍后由专门的处理程序例程解决。内核在do_IRQ末尾处理所有待决软中断,因而可以确保软中断能够定期得到处理。从一个更抽象的角度来看,可以将软中断描述为一种延迟到稍后时刻执行的内核活动。
例如,网卡接收数据包,从网卡产生中断信号,CPU将网络数据包拷贝到内核,然后进行协议栈的处理,最后将数据部分传递给用户空间,这个过程都可以说是中断处理函数需要做的部分,但硬件中断处理仅仅做从网卡拷贝数据的工作,而协议栈的处理的工作就交给"可延迟执行"部分处理
而软中断,正是"可延迟处理"部分的一种机制,之所以叫软中断,是因为类似于硬件中断的过程
1. 产生中断信号 2. 维护软中断向量 3. 进行中断处理
0x1: 软中断的执行流程
1. 初始化: 定义一个新的可延迟函数,这个操作通常在内核自身初始化或加载过模块时进行 2. 激活: 标记一个可延迟函数为"挂起"状态(可延迟函数的下一轮调度中可执行),激活可以在任何时后进行(即是正在处理中断) 3. 屏蔽: 有选择的屏蔽一个可延迟函数,这样,即使它被激活,内核也不执行它 4. 执行: 执行一个挂起的可延迟函数和同类型的其他所有挂起的可延迟函数,执行是在特定的时间内进行的
0x2: 软中断的类型
enum { HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, BLOCK_IOPOLL_SOFTIRQ, TASKLET_SOFTIRQ, SCHED_SOFTIRQ, HRTIMER_SOFTIRQ, RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ NR_SOFTIRQS };
Linux内核需要将枚举值转化为字符串数组,\linux-3.15.5\kernel\softirq.c
const char * const softirq_to_name[NR_SOFTIRQS] = { "HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "BLOCK_IOPOLL", "TASKLET", "SCHED", "HRTIMER", "RCU" };
0x3: 软中断涉及的数据结构
软中断机制的核心部分是一个表(softirq_vec),它包含32个softirq_action类型的的数据项
\linux-3.15.5\include\linux\interrupt.h
struct softirq_action { //指向处理程序例程的指针,在软中断发生时由内核执行该处理程序例程 void (*action)(struct softirq_action *); };
该数据结构的定义是和体系结构无关的,即它和下层的CPU中断已经没有直接耦合关系了,而软中断机制的整个实现也是如此
0x4: 软中断的初始化
我们知道,内部中断(软中断)和外部中断(硬件中断)的区别,对于内部中断的初始化,主要是设置中断向量表(IDT)。而对于外部中断,除了中断向量表之外,还要初始化中断控制器(8259A),以及中断控制器相关的管理结构
软中断必须先注册,然后内核才能执行软中断
\linux-3.15.5\kernel\softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action *)) { softirq_vec[nr].action = action; }
open_softirq在softirq_vec表中指定的位置写入新的软中断,在softirq_init()函数中循环调用,对全局变量softirq_vec(软中断向量表)进行赋值设置
void __init softirq_init(void) { int cpu; for_each_possible_cpu(cpu) { per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head; per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head; } open_softirq(TASKLET_SOFTIRQ, tasklet_action); open_softirq(HI_SOFTIRQ, tasklet_hi_action); }
0x5: 软中断的激活
\linux-3.15.5\kernel\softirq.c
asmlinkage __visible void __do_softirq(void) { unsigned long end = jiffies + MAX_SOFTIRQ_TIME; unsigned long old_flags = current->flags; int max_restart = MAX_SOFTIRQ_RESTART; struct softirq_action *h; bool in_hardirq; __u32 pending; int softirq_bit; int cpu; /* * Mask out PF_MEMALLOC s current task context is borrowed for the * softirq. A softirq handled such as network RX might set PF_MEMALLOC * again if the socket is related to swap */ current->flags &= ~PF_MEMALLOC; /* 首先要确认当前不处于中断上下文中,如果处于中断上下文,则立即结束,因为软中断用于执行ISR中非时间关键部分,所以其代码本身一定不能在中断处理程序内调用 通过local_softirq_pending,确定当前CPU软中断位图中所置位的比特位 */ pending = local_softirq_pending(); account_irq_enter_time(current); __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET); in_hardirq = lockdep_softirq_start(); cpu = smp_processor_id(); restart: /* Reset the pending bitmask before enabling irqs */ set_softirq_pending(0); local_irq_enable(); h = softirq_vec; while ((softirq_bit = ffs(pending))) { unsigned int vec_nr; int prev_count; h += softirq_bit - 1; vec_nr = h - softirq_vec; prev_count = preempt_count(); kstat_incr_softirqs_this_cpu(vec_nr); trace_softirq_entry(vec_nr); h->action(h); trace_softirq_exit(vec_nr); if (unlikely(prev_count != preempt_count())) { pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n", vec_nr, softirq_to_name[vec_nr], h->action, prev_count, preempt_count()); preempt_count_set(prev_count); } rcu_bh_qs(cpu); h++; pending >>= softirq_bit; } local_irq_disable(); pending = local_softirq_pending(); if (pending) { if (time_before(jiffies, end) && !need_resched() && --max_restart) goto restart; wakeup_softirqd(); } lockdep_softirq_end(in_hardirq); account_irq_exit_time(current); __local_bh_enable(SOFTIRQ_OFFSET); WARN_ON_ONCE(in_interrupt()); tsk_restore_flags(current, old_flags, PF_MEMALLOC); }
softirq_vec中的action函数在一个while循环中针对各个待决的软中断被调用。在处理了所有标记出的软中断之后,内核检查在此期间是否有新的软中断标记到位图中,这种操作会一直持续下去,直至在执行所有处理程序之后没有新的未处理软中断未知
0x6: 软中断的屏蔽
0x7: 软中断的执行
Relevant Link:
http://rock3.info/blog/2013/11/20/%E8%BD%AF%E4%B8%AD%E6%96%AD%E5%8E%9F%E7%90%86/ http://blog.csdn.net/droidphone/article/details/7518428http://www.crashcourse.ca/wiki/index.php/Softirqs
5. 中断优先级
在CPU硬件这个概念级别上,CPU对中断的处理顺序如下
1. 对内核来说,假设当前有两个设备同时发出中断请求 2. CPU首先处理优先级最高的中断 3. 中断控制器向CPU报告优先级高的中断(中断A),低优先级的中断(中断B)被延迟 4. 内核把当前运行级别提升到高优先级的IRQL 5. 在高优先级的中断处理例程中,如果出现了一个比当前优先级更高优先级的IRQL请求(中断C),则将当前中断处理例程(中断A)放入就绪队列挂起 6. 当最高优先级的中断处理例程(中断C)处理完毕之后,然后处理延迟的调度请求(中断A、中断B) /* 中断A->中断C->中断A->中断B */ 7. 当全部中断都处理完毕后,就会处理延迟的软件中断(即在do_IRQ的末尾检测是否有软中断需要处理) 8. 最后再返回用户态
在CPU的中断处理过程中,有一个隐含的优先级,对于Linux内核我们同样可以模仿windows内核使用IRQL优先级的概念,把统一的优先级称为IRQL,每个级别有一个对应的IRQL,优先级别越高,对应的IRQL越高
0x1: 高级可编程中断控制器(Advanced Programmable Interrupt Controller APIC)
8259A只适用于单处理器,为了满足多处理器的需要,出现了高级可编程中断控制器
大多数x86平台都包含了APIC,每一个CPU内部都有一个本地APIC,本地APIC可以产生时钟中断,可以向其他的处理发送处理器间中断等。系统可以有一个或多个I/O APIC,每个I/O APIC支持24个中断出入信号,I/O APIC和Local APIC之间通过总线连接
APIC中的每个LINTn和IRQn分别有一个64位配置寄存器,被称为Interrupt Redirection Table,这些寄存器被映射到内存地址空间
详细的信息请参阅《Intel® 64 and IA-32 Architectures Developer‘s Manual: Vol. 3A》 http://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.html
0x2: APIC初始化
系统启动阶段需要根据配置,重新设置CPU的中断向量等相关信息,这些配置是硬件相关的,为此Intel指定了MultiProcessor Specification。它规定了主板设计者必须把硬件相关的信息按照统一的数据格式集成到BIOS中,这样操作系统就可以通过BIOS获取到相关信息
\linux-3.15.5\arch\x86\kernel\mpparse.c
void __init default_find_smp_config(void) { unsigned int address; /* * FIXME: Linux assumes you have 640K of base ram.. * this continues the error... * * 1) Scan the bottom 1K for a signature * 2) Scan the top 1K of base RAM * 3) Scan the 64K of bios */ if (smp_scan_config(0x0, 0x400) || smp_scan_config(639 * 0x400, 0x400) || smp_scan_config(0xF0000, 0x10000)) return; /* * If it is an SMP machine we should know now, unless the * configuration is in an EISA bus machine with an * extended bios data area. * * there is a real-mode segmented pointer pointing to the * 4K EBDA area at 0x40E, calculate and scan it here. * * NOTE! There are Linux loaders that will corrupt the EBDA * area, and as such this kind of SMP config may be less * trustworthy, simply because the SMP table may have been * stomped on during early boot. These loaders are buggy and * should be fixed. * * MP1.4 SPEC states to only scan first 1K of 4K EBDA. */ address = get_bios_ebda(); if (address) smp_scan_config(address, 0x400); }
\linux-3.15.5\arch\x86\include\asm\bios_ebda.h
/* * Returns physical address of EBDA. Returns 0 if there is no EBDA. */ static inline unsigned int get_bios_ebda(void) { /* * There is a real-mode segmented pointer pointing to the * 4K EBDA area at 0x40E. */ unsigned int address = *(unsigned short *)phys_to_virt(0x40E); address <<= 4; return address; /* 0 means none */ }
BIOS会提供一个Intel中规定的MP table,它的头4个字节为_MP_,smp_scan_config()就是根据在BIOS数据区中寻找这个表,如果找到就打印:Scan for SMP in [mem %#010lx-%#010lx]
/source/arch/x86/kernel/irqinit.c
static void __init apic_intr_init(void) { smp_intr_init(); #ifdef CONFIG_X86_THERMAL_VECTOR alloc_intr_gate(THERMAL_APIC_VECTOR, thermal_interrupt); #endif #ifdef CONFIG_X86_MCE_THRESHOLD alloc_intr_gate(THRESHOLD_APIC_VECTOR, threshold_interrupt); #endif #if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC) /* self generated IPI for local APIC timer */ alloc_intr_gate(LOCAL_TIMER_VECTOR, apic_timer_interrupt); /* IPI for X86 platform specific use */ alloc_intr_gate(X86_PLATFORM_IPI_VECTOR, x86_platform_ipi); #ifdef CONFIG_HAVE_KVM /* IPI for KVM to deliver posted interrupt */ alloc_intr_gate(POSTED_INTR_VECTOR, kvm_posted_intr_ipi); #endif /* IPI vectors for APIC spurious and error interrupts */ alloc_intr_gate(SPURIOUS_APIC_VECTOR, spurious_interrupt); alloc_intr_gate(ERROR_APIC_VECTOR, error_interrupt); /* IRQ work interrupts: */ # ifdef CONFIG_IRQ_WORK alloc_intr_gate(IRQ_WORK_VECTOR, irq_work_interrupt); # endif #endif }
在apic_intr_init()中,内核为Local APIC设置了一些新的中断处理函数,对Local APIC中的配置寄存器进行配置后,这样当Local APIC产生中断时,就会调用对应的中断处理函数了
6. CPU在关中断状态下编程要注意的事项
0x1: 关中断下内核内存申请的限制
典型的在kprobe的CPU关中断情况下,使用kmalloc、vmalloc需要注意一些限制
1. 不能使用vmalloc 1) vmalloc可能导致缺页中断 2) vmalloc在内部使用了kmalloc(.., PAGE_KERNEL),这可能导致在申请内存的时候的睡眠,因为这个使用CPU无法响应中断,随机导致kernel panic 2. 如果使用kmalloc 1) 不能使用PAGE_KERNEL,原因和vmalloc的"2)"是相同的 2) 必须使用GFP_ATOMIC
关于kmalloc、vmalloc的相关知识,请参阅另一篇文章
http://www.cnblogs.com/LittleHann/p/4113830.html
0x2: 关中断下内核中禁止使用信号量这种锁
由于信号量可能引起进程切换,但是在中断环境中是不允许进程切换的,否则会引起Kernel Panic
为了解决这个问题,应该采用自旋锁(spinlock)
关于Linux内核中锁的相关知识,请参阅另一篇文章
http://www.cnblogs.com/LittleHann/p/4116368.html
0x3: 关中断下内核中禁止使用copy_from_user、strncpy_from_user进行KernelSpace <-> UserSpace之间复制内存数据
在中断状态下,禁止使用copy_from_user、strncpy_from_user进行与用户空间地址之间来回复制内存数据的行为,因为
1. copy_from_user、strncpy_from_user都是在进行从用户态到内核态的内存复制 2. 内核无法确定等待数据的应用程序此时是否正在运行(是否获得CPU调度资源) 3. 在copy_from_user、strncpy_from_user运行中,有一定概率发生当前正在复制的UserSpace内存被Page Out,这个时候进行内存复制就会触发缺页中断(Page Fault),缺页中断会引发一个CPU中断 4. 在Kprobe关中断情况下,回调Handler函数中引发的CPU中断是无法得到响应的 5. copy_from_user、strncpy_from_user在复制过程中引发的Page Fault,不能得到响应,只会被自身汇编代码中的.fixup函数段捕获,并打印: do_page_fault....,但其实这个时候已经是不正常现象了 6. Linux在发生了Page Fault之后,不会立刻Kernel Panic,而是继续运行,但是此时系统已经处于不正常状态了,如果继续运行,最终可能导致Panic
copy_from_user的内核代码分析
1. http://lxr.free-electrons.com/source/arch/x86/include/asm/uaccess.h#L688 2. http://lxr.free-electrons.com/source/arch/x86/lib/usercopy_32.c#L681 3. http://lxr.free-electrons.com/source/arch/x86/include/asm/uaccess.h#L88 4. http://lxr.free-electrons.com/source/arch/x86/lib/usercopy_32.c#L583 5. http://lxr.free-electrons.com/source/arch/x86/lib/usercopy_32.c#L531
strncpy_from_user的内核源代码分析
http://www.cs.fsu.edu/~baker/devices/lxr/http/source/2.6.25.8/linux/arch/x86/lib/usercopy_64.c?v=. http://www.verydemo.com/demo_c378_i61436.html http://www.tuicool.com/articles/uYzAzy http://www.hep.by/gnu/kernel/kernel-api/API-strncpy-from-user.html http://blog.csdn.net/justlinux2010/article/details/8972754 https://www.kernel.org/doc/htmldocs/device-drivers/API-might-sleep.html
Relevant Link:
http://blog.csdn.net/yangdelong/article/details/5491097 http://oss.org.cn/kernel-book/ldd3/ch06s02.html http://blog.csdn.net/eroswang/article/details/3529750 http://blog.csdn.net/ce123_zhouwei/article/details/8454226
Copyright (c) 2014 LittleHann All rights reserved
Linux Kernel Interrupt、Interrupt Priority、Prohibit Things Whthin CPU In The Interrupt Off State
原文:http://www.cnblogs.com/LittleHann/p/4104598.html