一些概念
中断:一件可以改变处理器执行指令顺序的事件,对应于CPU内外部硬件电路产生的电信号。分为同步中断异步中断。
- 同步中断是CPU控制单元产生,只有在一条指令中止执行后CPU才会发出中断。
- 异步中断由其他硬件设备遵照CPU时钟信号随机产生。
Intel微处理器将同步和异步中断分别称为异常和中断。中断由间隔计时器和I/O设备产生。异常由程序错误产生,或由内核必须处理的异常条件产生,第一种情况,内核发送信号;第二种情况,内核执行恢复异常所需的所有步骤。
中断的作用
- 内核的目标是尽快的处理完中断或尽可能把更多的处理向后推迟,内核响应中断进行的操作分为两部分:关键部分立即执行,其余部分随后执行。
- 内核处理一个中断时,可能会有另一个中断发生,允许这种情况可以维持更多I/O设备处于忙状态。因此中断处理程序应当使内环控制路径能以嵌套方式执行。
*由于内核代码存在一些临界区,在临界区内中断必须被禁止。临界区应当尽可能限制。因为内核应该大多数时候以允许中断方式运行。
异常与中断
- 中断
*可屏蔽中断(maskable):I/O设备发出的所有中断请求(IRQ)都产生可屏蔽中断。又处于两种状态,屏蔽的,非屏蔽的。只要屏蔽的中断还是屏蔽的,控制单元就忽略它。
*不可屏蔽中断:极少数的关键事件才能产生,总是由CPU识别。
*异常
*处理器探测异常:CPU执行指令时探测到反常条件产生的异常。可分为3组。
*故障(fault):通常可以纠正,程序可以在不失连贯性的情况下重新开始。eip保存引发故障的指令地址。当异常处理程序终止时,那条指令会重新执行。
*陷阱(trap):陷阱指令执行后立即报告,内核交还控制权后不是连续性继续执行。eip保存下条指令地址。主要用途是调试程序。
*异常中止(abort):用于报告严重错误,eip不保存异常指令的确切位置。控制单元发送紧急中断信号,异常处理程序强制进程中止。
*编程异常:在程序员发出请求时产生,也称软中断,用于执行系统调用以及向调试程序发送特定事件。。由int, int3指令触发。控制单元将编程异常作为陷阱处理。
IRQ与中断
能发出中断请求的硬件设备至少有一条IRQ输出线。所有IRQ线都与可编程中断控制器(PIC)输入引脚相连,执行下列操作
- 监视IRQ,如果两条以上线产生信号,选择IRQ编号小的线。
- IRQ线上的信号:
a. 信号转化为对应的向量(0~255 8-bit标识符)
b. 将向量存放在中断控制器I/O端口,允许CPU读取向量
c. 信号发送到处理器INTR引脚,即产生中断
d. 等待(PIC与设备控制器阻塞)CPU将中断信号写进PIC的I/O端口以确认;清INTR线 - IRQ从0开始,IRQn关联的向量是n+32
可以禁止IRQ线,PIC停止对指定IRQ发布中断,但中断并未丢失。
异常
x86发布约20种异常,内核应为每种异常提供专门的异常处理程序。
中断描述符表(IDT),与每个中断或异常向量相联系,存有相应的中断或异常处理程序入口地址。内核激活中断前,适当初始化IDT。idtr指定IDT线性机制及最大长度,Intel包含3种类型描述符。
![a](4.2 4-2)
任务门:中断发生时,存放取代当前进程的新进程TSS选择符
中断门:包含中断异常处理程序的段选择符与段内偏移。当控制转移到适当的段,处理器重置IF标志,关闭可屏蔽中断
陷阱门:和中断门类似,但是不改变IF标志
中断和异常处理程序嵌套执行
每个终端或异常都会引发内和控制路径,或代表当前进程在内核态执行的单独指令序列。内和控制路径可以任意嵌套,即一个中断处理程序可以被另一个中断处理程序中断。处理中断时,第一部分指令是把寄存器内容保存到内核堆栈,后一部分是恢复寄存器内容并将CPU返回用户态。但是嵌套深度大于1时,最后一部分指令将执行上次被打断的内核控制路径。
![a](4.3 4-3)
这就要求中断处理程序运行期间不能发生进程切换。一个中断处理程序可以抢占其他的中断处理程序,也可以抢占异常处理程序。异常处理程序从不抢占中断处理程序。唯一内核态异常是缺页异常,中断处理程序不执行导致缺页(进程切换)操作。处理缺页异常时,内核可以挂起当前进程,使用另一个进程代替,直到请求的页可以使用。被挂起的进程获得处理器,相应的内核控制路径可以恢复执行。
交错执行内核控制路径可以:
- 提高中断控制器和设备控制器的吞吐量。内核正在处理一个中断也能发送应答,减少阻塞。
2.实现无优先级的中断模型,简化代码,提高可移植性。
多处理器系统中,几个内核控制路径可以并发执行。与异常相关的内核控制路径可以由于进程切换在不同的CPU上执行。
初始化IDT
Linux将中断描述符如下分类:
- 中断门:用户态进程不能访问的一个Intel中断门(DPL = 0)。激活所有Linux中断处理程序,限制在内核态
- 系统门:用户态进程可以访问的一个Intel陷阱门(DPL = 3)。激活3个Linux异常处理程序,分别是向量4,5,128,对应汇编指令的into,bound以及int $0x80
- 系统中断门:用户态进程可以访问的Intel中断门(GPL = 3)。激活向量3的异常处理程序,对应int3指令
- 陷阱门:用户态进程不能访问的一个Intel陷阱门(DPL = 0)。激活大部分Linux异常处理程序
- 任务门:用户态进程不能访问的Intel任务门(DPL = 0)。激活Linux的
Double Fault
异常处理程序
IDT存放在idt_table中,有256个表项。第一次初始化由内核将同一个中断门(指向ignore_int()中断处理程序)填充到256个表项。紧接着内核将进行第二次初始化,用有意义的程序替换。
异常处理
当异常发生时,内核向引发异常的进程发送一个信号,。由以下三部分组成:
- 在内核堆栈中保存大多数寄存器内容(汇编实现)
- 使用C函数处理异常
- 通过ret_from_exception()从异常处理函数退出
中断处理
向进程发送信号以通知中断的方法是无效的,因为进程可能被挂起。主要关注三种类型的中断:I/O中断,时钟中断,处理期间中断。
I/O中断
一般要求I/O中断处理程序具有足够的灵活性来同时给多个设备提供服务,实现方式有两种:
- IRQ共享:中断处理程序执行多个中断服务例程(ISR),每个ISR关联一个单独设备,设备间共享IRQ。无法知道那个设备产生IRQ,因此每个ISR都被执行以检验设备是否需要关注。
2.IRQ动态分配:IRQ在最后时刻才与设备关联。
中断发生时并不是所有操作具有相同的紧迫性。中断处理程序代表进程执行,因此总是处于TASK_RUNNING状态,因此不能有阻塞过程,必须由中断处理程序执行的操作称为top_half,其余的称为bottom-half。Linux把中断要执行的操作分为三类:
- 紧急的:立即在中断处理程序内执行,在禁止可屏蔽中断情况下。
- 非紧急的:立即被中断处理程序执行,在激活中断的情况下。
- 非紧急可延迟的:可以延迟较长时间而不影响内和操作。
I/O中断执行以下基础操作:
- 将IRQ值以及寄存器内容保存到内核栈
- 向对应的PIC应答,允许其进一步发生中断
- 检查共享IRQ设备的ISR
- 跳转到ret_from_intr(),终止
![a](4.6 4-4)
IRQ数据结构
每个中断向量有自己的irq_desc_t描述符,共同组成了irq_desc数组。
![a](4.6 4-5)
意外中断,与某个IRQ相关的ISR不存在,或者所有ISR都都无法辨别是否是自身设备发出中断。办法是禁用IRQ;对于共享IRQ,当意外中断达到一定次数才禁用IRQ。
软中断(softirq),tasklet和工作队列
用于处理中断的bottom-half工作。软中断是一套静态定义的bottom-halves(机制),在编译期间静态注册,可以在不同处理器上同时运行(即使类型相同);tasklets是基于softirq动态创建的具有灵活性的bottom_halves,可以动态注册,只有不同种类的tasklets可以在不同的处理器上同时运行,是性能和易用性的折衷。大多数情况,tasklet就够用了。
软中断
softirq由softirq_action结构表示:
struct softirq_action {
void (*action)(struct softirq_action *);
};
static struct softirq_action softirq_vec[NR_SOFTIRQS];
/*每个注册的softirq对应数组中的一项。在2.6内核中,一共有9个软中断*/
softirq不会抢占其他的softirq,只用中断处理程序才可以抢占softirq。已经注册的softirq在执行前必须被标记,也称为触发软中断,标记后处于pengding状态。一般情况下,中断处理程序再返回前标记它的软中断,软中断会在合适的时间运行,一般有三种情况:
- 从中断处理程序代码路径返回时
- 在ksoftirq内核线程
- 任何显式检查与执行挂起软中断的代码处
软中断的执行发生在__do_sofeirq()函数中,主体代码如下:
pending = local_softirq_pending(); /*32-bit mask of pending irqs,if bit n is set,the n-th irq is pending */
if (pending) {
struct softirq_action *h;
set_softirq_pending(0);/* reset the pending bitmask */
h = softirq_vec;/*first entry of softirq_vec*/
do {
if (pending & 1)
h->action(h); /*if bit is set,call action*/
h++;
pending >>= 1;
} while (pending);
}
softirq 用于时间敏感的bottom-half操作。2.6版本中,网络和块设备直接使用了softirq,内核计时器以及tasklet建立在softirq之上。
tasklets
Tasklets主要基于两种softirq,HI_SOFTIRQ和TASKLET_SOFTIRQ,前者有更高优先级。
struct tasklet_struct {
struct tasklet_struct *next; /* next tasklet in the list*/
unsigned long state; /* state TASKLET_STATE_SCHED(shed to run),TASKLET_STATE_RUN(running(only mprocessor))*/
atomic_t count; /* reference counter,if nonzero disabled cannot run;else enabled can run if marked pending*/
void (*func)(unsigned long); /* tasklet handler function */
unsigned long data; /* argument to the tasklet function */
};
Tasklets 调度
__tasklet_schedule() 和__tasklet_hi_schedule(),对应TASKLET_SOFTIRQ和HI_SOFTIRQ。
ksoftirq内核线程
软中断可以在运行时触发自身并重新运行。如果持续执行重复的软中断,其他用户态进程可能得不到处理;如果选择不执行重复的软中断,直到下次处理pending softirq时才处理,造成冲断处理不及时。最终采用了折衷的方案,重复的softirq不会立即执行。如果softirq数量太多,内核唤醒一族内核线程来处理负载,内核线程处于最低优先级,一个处理器对应一个线程:
for (;;) {
if (!softirq_pending(cpu))
schedule();
set_current_state(TASK_RUNNING);
while (softirq_pending(cpu)) {
do_softirq();
if (need_resched())
schedule();
}
set_current_state(TASK_INTERRUPTIBLE);
}
Work queues
Work queues将工作推迟到内核线程,总是运行在进程上下文,因此可以被调度及挂起。workqueue处理函数无法访问用户态内存,因为没有为内核线程映射用户态内存。
一般工作被推迟到缺省线程上,对性能要求较高的工作可以创建自己的线程。相关结构workqueue_struct,cpu_workqueue_struct, work_struct都在kernel/workqueue.c。缺省工作线程是events类型,每个工作线程对应一个cpu_workqueue_struct结构。每种工作线程对应一个workqueue_struct结构。
从异常和中断返回
返回阶段的主要目的是恢复某个程序的执行。但是需要使用一些标志来记录其他的请求,如挂起进程的切换请求,挂起的信号等等。存放在thread_info的flags字段中。完成返回任务的汇编代码不是一个函数,因为控制权不会返回给其调用者。有两个不同的入口,分别是ret_from_intr()和ret_from_exception()
![](4.9 4-6)