文档库 最新最全的文档下载
当前位置:文档库 › 内核分析:中断

内核分析:中断

中断
目 录
1. 中断
1. 软中断
2. 硬中断
3. 定时器代码分析
4. from aka
1. 硬件中断
2. 软中断
5. from lisolog
1. index
2. 内部中断
3. 外部中断
4. 后续处理
6. 软中断代码线索
7. 2. 4软中断机制
________________________________________


中断

Linux系统中有很多不同的硬件设备。你可以同步使用这些设备,也就是说你可以发出一个请求,然后等待一直到设备完成操作以后再进行其他的工作。但这种方法的效率却非常的低,因为操作系统要花费很多的等待时间。一个更为有效的方法是发出请求以后,操作系统继续其他的工作,等设备完成操作以后,给操作系统发送一个中断,操作系统再继续处理和此设备有关的操作。
在将多个设备的中断信号送往CPU的中断插脚之前,系统经常使用中断控制器来综合多个设备的中断。这样即可以节约CPU的中断插脚,也可以提高系统设计的灵活性。中断控制器用来控制系统的中断,它包括屏蔽和状态寄存器。设置屏蔽寄存器的各个位可以允许或屏蔽某一个中断,状态寄存器则用来返回系统中正在使用的中断。
大多数处理器处理中断的过程都相同。当一个设备发出中段请求时,CPU停止正在执行的指令,转而跳到包括中断处理代码或者包括指向中断处理代码的转移指令所在的内存区域。这些代码一般在CPU的中断方式下运行。在此方式下,将不会再有中断发生。但有些CPU的中断有自己的优先权,这样,更高优先权的中断则可以发生。这意味着第一级的中断处理程序必须拥有自己的堆栈,以便在处理更高级别的中断前保存CPU的执行状态。当中断处理完毕以后,CPU将恢复到以前的状态,继续执行中断处理前正在执行的指令。
中断处理程序十分简单有效,这样,操作系统就不会花太长的时间屏蔽其他的中断。
[设置Softirq]
cpu_raise_softirq是一个轮训,唤醒ksoftirqd_CPU0内核线程, 进行管理
cpu_raise_softirq
|__cpu_raise_softirq
|wakeup_softirqd
|wake_up_process
?cpu_raise_softirq [kernel/softirq.c]
?__cpu_raise_softirq [include/linux/interrupt.h]
?wakeup_softirq [kernel/softirq.c]
?wake_up_process [kernel/sched.c]
[执行Softirq]
当内核线程ksoftirqd_CPU0被唤醒, 它会执行队列里的工作。当然ksoftirqd_CPU0也是一个死循环:
for (;;) {
if (!softirq_pending(cpu))
schedule();
__set_current_state(TASK_RUNNING);
while (softirq_pending(cpu)) {
do_softirq();
if (current->need_resched)
schedule
}
__set_current_state(TASK_INTERRUPTIBLE)
}
?ksoftirqd [kernel/softirq.c]
[目录]
________________________________________


软中断

发信人: fist (星仔迷), 信区: SysInte

rnals WWW-POST
标 题: 软中断
发信站: 武汉白云黄鹤站 (Thu Mar 22 14:12:46 2001) , 转信
软中断「一」
一、 引言
软中断是linux系统原“底半处理”的升级,在原有的基础上发展的新的处理方式,以适应多cpu 、多线程的软中断处理。要了解软中断,我们必须要先了原来底半处理的处理机制。
二、底半处理机制(基于2.0.3版本)
某些特殊时刻我们并不愿意在核心中执行一些操作。例如中断处理过程中。当中断发生时处理器将停止当前的工作, 操作系统将中断发送到相应的设备驱动上去。由于此时系统中其他程序都不能运行, 所以设备驱动中的中断处理过程不宜过长。有些任务最好稍后执行。Linux底层部分处理机制可以让设备驱动和Linux核心其他部分将这些工作进行排序以延迟执行。
系统中最多可以有32个不同的底层处理过程;bh_base是指向这些过程入口的指针数组。而bh_active和 bh_mask用来表示那些处理过程已经安装以及那些处于活动状态。如果bh_mask的第N位置位则表示bh_base的 第N个元素包含底层部分处理例程。如果bh_active的第N位置位则表示第N个底层处理过程例程可在调度器认 为合适的时刻调用。这些索引被定义成静态的;定时器底层部分处理例程具有最高优先级(索引值为0), 控制台底层部分处理例程其次(索引值为1)。典型的底层部分处理例程包含与之相连的任务链表。例如 immediate底层部分处理例程通过那些需要被立刻执行的任务的立即任务队列(tq_immediate)来执行。
--引自David A Rusling的《linux核心》。
三、对2.4.1 软中断处理机制
下面,我们进入软中断处理部份(softirq.c):
由softirq.c的代码阅读中,我们可以知道,在系统的初始化过程中(softirq_init()),它使用了两个数组:bh_task_vec[32],softirq_vec[32]。其中,bh_task_vec[32]填入了32个bh_action()的入口地址,但soft_vec[32]中,只有softirq_vec[0],和softirq_vec[3]分别填入了tasklet_action()和tasklet_hi_action()的地址。其余的保留它用。
当发生软中断时,系统并不急于处理,只是将相应的cpu的中断状态结构中的active 的相应的位置位,并将相应的处理函数挂到相应的队列,然后等待调度时机来临(如:schedule(),
系统调用返回异常时,硬中断处理结束时等),系统调用do_softirq()来测试active位,再调用被激活的进程在这处过程中,软中断的处理与底半处理有了差别,active 和mask不再对应bh_base[nr], 而是对应softirq_vec[32]。在softirq.c中,我们只涉及了softirq_vec[0]、softirq_vec[3]。这两者分别调用了tasklet_action()和tasklet_hi_action()来进行后续处理。这两个过程比较相似,大致如下:
1 锁cpu的tasklet_ve

c[cpu]链表,取出链表,将原链表清空,解锁,还给系统。
2 对链表进行逐个处理。
3 有无法处理的,(task_trylock(t)失败,可能有别的进程锁定),插回系统链表。至此,系统完成了一次软中断的处理。
接下来有两个问题:
1 bh_base[]依然存在,但应在何处调用?
2 tasklet_vec[cpu]队列是何时挂上的?

四、再探讨
再次考查softirq.c 的bh_action()部份,发现有两个判断:
A:if(!spin_trylock(&global_bh_lock))goto:rescue 指明如果global_bh_lock 不能被锁上(已被其它进程锁上),则转而执行rescue,将bh_base[nr]挂至tasklet_hi_vec[cpu]队列中。等候中断调度。
B:if(!hardirq_trylock(cpu)) goto tescue unlock 此时有硬中断发生,放入队列推迟执行。若为空闲,现在执行。
由此可见,这部分正是对应底半处理的程序,bh_base[]的延时处理正是底半处理的特点,可以推测,如果没有其它函数往tasklet_hi_vec[cpu]队列挂入,那tasklet_hi_vec[cpu]正完全对应着bh_base[]底半处理
在bh_action()中,把bh_ation()挂入tasklet_hi_vec[cpu]的正是mark_bh(),在整个源码树中查找,发现调用mark_bh()的函数很多,可以理解,软中断产生之时,相关的函数会调用mark_bh(),将bh_action挂上tasklet_hi_vec队列,而bh_action()的作用不过是在发现bh_base[nr]暂时无法处理时重返队列的方法。
由此可推测tasklet_vec队列的挂接应与此相似,查看interrupt.h,找到tasklet_schedule()函数:
157 static inline void tasklet_schedule(struct tasklet_struct *t)
158 {
159 if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
160 int cpu = smp_processor_id();
161 unsigned long flags;
162
163 local_irq_save(flags);
164 t->next = tasklet_vec[cpu].list;
165 tasklet_vec[cpu].list = t; /*插入队列。
166 __cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
167 local_irq_restore(flags);
168 }
169 }
正是它为tasklet_vec[cpu]队列的建立立下了汗马功劳,在源码树中,它亦被多个模块调用,来完成它的使命。
至此,我们可以描绘一幅完整的软中断处理图了。
现在,再来考查do_softirq()的softirq_vec[32],在interrupt.h中有如下定义:
56 enum
57 {
58 HI_SOFTIRQ=0,
59 NET_TX_SOFTIRQ,
60 NET_RX_SOFTIRQ,
61 TASKLET_SOFTIRQ
62 };
这四个变量应都是为softirq_vec[]的下标,那么,do_softirq()也将会处理NET_TX_SOFTIRQ和NET_RX_SOFTIRQ,是否还处理其它中断,这有待探讨。也许,这个do_softirq()有着极大的拓展性,等着我们去开发呢。
主要通过__cpu_raise_softirq来设置
在hi_tasklet(也就是一般用于bh的)的处理里面,在处理完当前的队列后,会将补充的队列重新挂上,然后标记(不管是否补充队列里面有tasklet):
local_irq_disable();
t->next = tasklet_hi_vec[cpu].list;
tasklet_hi_vec[cpu].list

= t;
__cpu_raise_softirq(cpu, HI_SOFTIRQ);
local_irq_enable();
因此,对mark_bh根本不用设置这个active位。对于一般的tasklet也一样:
local_irq_disable();
t->next = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = t;
__cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
local_irq_enable();
其它的设置,可以检索上面的__cpu_raise_softirq
bottom half, softirq, tasklet, tqueue
[bottom half]
bh_base[32]
|
\/
bh_action();
|
\/
bh_task_vec[32];
| mark_bh(), tasklet_hi_schedule()
\/
task_hi_action
bh_base对应的是32个函数,这些函数在bh_action()中调用
static void bh_action(unsigned long nr)
{
int cpu = smp_processor_id();
if (!spin_trylock(&global_bh_lock))
goto resched;
if (!hardirq_trylock(cpu))
goto resched_unlock;
if (bh_base[nr])
bh_base[nr]();
hardirq_endlock(cpu);
spin_unlock(&global_bh_lock);
return;
resched_unlock:
spin_unlock(&global_bh_lock);
resched:
mark_bh(nr);
}
在软中断初始化时,将bh_action()放到bh_task_vec[32]中,bh_task_vec[32]中元素的类型是tasklet_struct,系统使用mark_bh()或task_hi_schedule()函数将它挂到task_hi_vec[]的对列中,在系统调用do_softirq()时执行。
static inline void mark_bh(int nr)
{
tasklet_hi_schedule(bh_task_vec+nr);
}
static inline void tasklet_hi_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
int cpu = smp_processor_id();
unsigned long flags;
local_irq_save(flags);
t->next = tasklet_hi_vec[cpu].list;
tasklet_hi_vec[cpu].list = t;
__cpu_raise_softirq(cpu, HI_SOFTIRQ);
local_irq_restore(flags);
}
}
[softirq]
softirq_vec[32];
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
软中断对应一个softirq_action的结构,在do_softirq()中调用相应的action()做处理。
软中断初始化时只设置了0,3两项,对应的action是task_hi_action和task_action.
1: task_hi_action
/\
|
tasklet_hi_vec[NR_CPU]
struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned;
struct tasklet_head
{
struct tasklet_struct *list;
} __attribute__ ((__aligned__(SMP_CACHE_BYTES)));
task_hi_action处理的对象是一个tasklet的队列,每个cpu都有一个对应的tasklet队列,
它在tasklet_hi_schedule中动态添加。
3: task_action
/\
|
tasklet_vec[NR_CPU]
[tasklet]
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
从上面的分析来看tasklet只是一个调用实体,在do_softirq()中被调用。softirq的组织和结构才是最重要的。

[目录]
________________________________________


硬中断

标题 Linux设备驱动程序的中断
作者 coly (journeyman)
时间 07/02/01 11:24 AM
Linux设备驱动程序的中断 Coly V0.1
指定参考书:《Linux设备驱动程序》(第一版)
这里总结一下Linux

设备驱动程序中涉及的中断机制。
一、前言
Linux的中断宏观分为两种:软中断和硬中断。声明一下,这里的软和硬的意思是指和软件相关以及和硬件相关,而不是软件实现的中断或硬件实现的中断。软中断就是“信号机制”。软中断不是软件中断。Linux通过信号来产生对进程的各种中断操作,我们现在知道的信号共有31个,其具体内容这里略过,感兴趣读者可参看相关参考文献[1]。
一般来说,软中断是由内核机制的触发事件引起的(例如进程运行超时),但是不可忽视有大量的软中断也是由于和硬件有关的中断引起的,例如当打印机端口产生一个硬件中断时,会通知和硬件相关的硬中断,硬中断就会产生一个软中断并送到操作系统内核里,这样内核就会根据这个软中断唤醒睡眠在打印机任务队列中的处理进程。
硬中断就是通常意义上的“中断处理程序”,它是直接处理由硬件发过来的中断信号的。当硬中断收到它应当处理的中断信号以后,就回去自己驱动的设备上去看看设备的状态寄存器以了解发生了什么事情,并进行相应的操作。
对于软中断,我们不做讨论,那是进程调度里要考虑的事情。由于我们讨论的是设备驱动程序的中断问题,所以焦点集中在硬中断里。我们这里讨论的是硬中断,即和硬件相关的中断。
二、中断产生
要中断,是因为外设需要通知操作系统她那里发生了一些事情,但是中断的功能仅仅是一个设备报警灯,当灯亮的时候中断处理程序只知道有事情发生了,但发生了什么事情还要亲自到设备那里去看才行。也就是说,当中断处理程序得知设备发生了一个中断的时候,它并不知道设备发生了什么事情,只有当它访问了设备上的一些状态寄存器以后,才能知道具体发生了什么,要怎么去处理。
设备通过中断线向中断控制器发送高电平告诉操作系统它产生了一个中断,而操作系统会从中断控制器的状态位知道是哪条中断线上产生了中断。PC机上使用的中断控制器是8259,这种控制器每一个可以管理8条中断线,当两个8259级联的时候共可以控制15条中断线。这里的中断线是实实在在的电路,他们通过硬件接口连接到CPU外的设备控制器上。
三、IRQ
并不是每个设备都可以向中断线上发中断信号的,只有对某一条确定的中断线勇有了控制权,才可以向这条中断线上发送信号。由于计算机的外部设备越来越多,所以15条中断线已经不够用了,中断线是非常宝贵的资源。要使用中断线,就得进行中断线的申请,就是IRQ(Interrupt Requirement),我们也常把申请一条中断线成为申请一个IRQ或者是申请一个

中断号。
IRQ是非常宝贵的,所以我们建议只有当设备需要中断的时候才申请占用一个IRQ,或者是在申请IRQ时采用共享中断的方式,这样可以让更多的设备使用中断。无论对IRQ的使用方式是独占还是共享,申请IRQ的过程都是一样的,分为3步:
1.将所有的中断线探测一遍,看看哪些中断还没有被占用。从这些还没有被占用的中断中选一个作为该设备的IRQ。
2.通过中断申请函数申请选定的IRQ,这是要指定申请的方式是独占还是共享。
3.根据中断申请函数的返回值决定怎么做:如果成功了万事大吉,如果没成功则或者重新申请或者放弃申请并返回错误。
申请IRQ的过程,在参考书的配的源代码里有详细的描述,读者可以通过仔细阅读源代码中的short一例对中断号申请由深刻的理解。
四、中断处理程序
Linux中的中断处理程序很有特色,它的一个中断处理程序分为两个部分:上半部(top half)和下半部(bottom half)。之所以会有上半部和下半部之分,完全是考虑到中断处理的效率。
上半部的功能是“登记中断”。当一个中断发生时,他就把设备驱动程序中中断例程的下半部挂到该设备的下半部执行队列中去,然后就没事情了--等待新的中断的到来。这样一来,上半部执行的速度就会很快,他就可以接受更多她负责的设备产生的中断了。上半部之所以要快,是因为它是完全屏蔽中断的,如果她不执行完,其它的中断就不能被及时的处理,只能等到这个中断处理程序执行完毕以后。所以,要尽可能多得对设备产生的中断进行服务和处理,中断处理程序就一定要快。
但是,有些中断事件的处理是比较复杂的,所以中断处理程序必须多花一点时间才能够把事情做完。可怎么样化解在短时间内完成复杂处理的矛盾呢,这时候Linux引入了下半部的概念。下半部和上半部最大的不同是下半部是可中断的,而上半部是不可中断的。下半部几乎做了中断处理程序所有的事情,因为上半部只是将下半部排到了他们所负责的设备的中断处理队列中去,然后就什么都不管了。下半部一般所负责的工作是察看设备以获得产生中断的事件信息,并根据这些信息(一般通过读设备上的寄存器得来)进行相应的处理。如果有些时间下半部不知道怎么去做,他就使用著名的鸵鸟算法来解决问题--说白了就是忽略这个事件。
由于下半部是可中断的,所以在它运行期间,如果其它的设备产生了中断,这个下半部可以暂时的中断掉,等到那个设备的上半部运行完了,再回头来运行它。但是有一点一定要注意,那就是如果一个设备中断处理程序正在运行,无论

她是运行上半部还是运行下半部,只要中断处理程序还没有处理完毕,在这期间设备产生的新的中断都将被忽略掉。因为中断处理程序是不可重入的,同一个中断处理程序是不能并行的。
在Linux Kernel 2.0以前,中断分为快中断和慢中断(伪中断我们这里不谈),其中快中断的下半部也是不可中断的,这样可以保证它执行的快一点。但是由于现在硬件水平不断上升,快中断和慢中断的运行速度已经没有什么差别了,所以为了提高中断例程事务处理的效率,从Linux kernel 2.0以后,中断处理程序全部都是慢中断的形式了--他们的下半部是可以被中断的。
但是,在下半部中,你也可以进行中断屏蔽--如果某一段代码不能被中断的话。你可以使用cti、sti或者是save_flag、restore_flag来实现你的想法。至于他们的用法和区别,请参看本文指定参考书中断处理部分。
进一步的细节请读者参看本文指定参考书,这里就不再所说了,详细介绍细节不是我的目的,我的目的是整理概念。
五、置中断标志位
在处理中断的时候,中断控制器会屏蔽掉原先发送中断的那个设备,直到她发送的上一个中断被处理完了为止。因此如果发送中断的那个设备载中断处理期间又发送了一个中断,那么这个中断就被永远的丢失了。
之所以发生这种事情,是因为中断控制器并不能缓冲中断信息,所以当前一个中断没有处理完以前又有新的中断到达,他肯定会丢掉新的中断的。但是这种缺陷可以通过设置主处理器(CPU)上的“置中断标志位”(sti)来解决,因为主处理器具有缓冲中断的功能。如果使用了“置中断标志位”,那么在处理完中断以后使用sti函数就可以使先前被屏蔽的中断得到服务。
六、中断处理程序的不可重入性
上一节中我们提到有时候需要屏蔽中断,可是为什么要将这个中断屏蔽掉呢?这并不是因为技术上实现不了同一中断例程的并行,而是出于管理上的考虑。之所以在中断处理的过程中要屏蔽同一IRQ来的新中断,是因为中断处理程序是不可重入的,所以不能并行执行同一个中断处理程序。在这里我们举一个例子,从这里子例中可以看出如果一个中断处理程序是可以并行的话,那么很有可能会发生驱动程序锁死的情况。当驱动程序锁死的时候,你的操作系统并不一定会崩溃,但是锁死的驱动程序所支持的那个设备是不能再使用了--设备驱动程序死了,设备也就死了。
A是一段代码,B是操作设备寄存器R1的代码,C是操作设备寄存器R2的代码。其中激发PS1的事件会使A1产生一个中断,然后B1去读R1中已有的数据,然后代码C1向R2中写数据。而激发

PS2的事件会使A2产生一个中断,然后B2删除R1中的数据,然后C2读去R2中的数据。
如果PS1先产生,且当他执行到A1和B1之间的时候,如果PS2产生了,这是A2会产生一个中断,将PS2中断掉(挂到任务队列的尾部),然后删除了R1的内容。当PS2运行到C2时,由于C1还没有向R2中写数据,所以C2将会在这里被挂起,PS2就睡眠在代码C2上,直到有数据可读的时候被信号唤醒。这是由于PS1中的B2原先要读的R1中的数据被PS2中的B2删除了,所以PS1页会睡眠在B1上,直到有数据可读的时候被信号唤醒。这样一来,唤醒PS1和PS2的事件就永远不会发生了,因此PS1和PS2之间就锁死了。
由于设备驱动程序要和设备的寄存器打交道,所以很难写出可以重入的代码来,因为设备寄存器就是全局变量。因此,最简洁的办法就是禁止同一设备的中断处理程序并行,即设备的中断处理程序是不可重入的。
有一点一定要清楚:在2.0版本以后的Linux kernel中,所有的上半部都是不可中断的(上半部的操作是原子性的);不同设备的下半部可以互相中断,但一个特定的下半部不能被它自己所中断(即同一个下半部不能并)。
由于中断处理程序要求不可重入,所以程序员也不必为编写可重入的代码而头痛了。以我的经验,编写可重入的设备驱动程序是可以的,编写可重入的中断处理程序是非常难得,几乎不可能。
七、避免竞争条件的出现
我们都知道,一旦竞争条件出现了,就有可能会发生死锁的情况,严重时可能会将整个系统锁死。所以一定要避免竞争条件的出现。这里我不多说,大家只要注意一点:绝大多数由于中断产生的竞争条件,都是在带有中断的
内核进程被睡眠造成的。所以在实现中断的时候,一定要相信谨慎的让进程睡眠,必要的时候可以使用cli、sti或者save_flag、restore_flag。具体细节请参看本文指定参考书。
八、实现
如何实现驱动程序的中断例程,是各位读者的事情了。只要你们仔细的阅读short例程的源代码,搞清楚编写驱动程序中断例程的规则,就可以编写自己的中断例程了。只要概念正确,
在正确的规则下编写你的代码,那就是符合道理的东西。我始终强调,概念是第一位的,能编多少代码是很其次的,我们一定要概念正确,才能进行正确的思考。
九、小结
本文介绍了Linux驱动程序中的中断,如果读者已经新痒了的话,那么打开机器开始动手吧!
Time for you to leave!
参考文献:
1.Linux网络编程
2.编程之道
3.Linux设备驱动程序
4.Mouse drivers
5.Linux Kernel Hacking Guide
6.Unreliable Guide To Hacking The Linux Kernel

[目录]
_______________

_________________________


定时器代码分析

时钟和定时器中断
IRQ 0 [Timer]
|
\|/
|IRQ0x00_interrupt // wrapper IRQ handler
|SAVE_ALL ---
|do_IRQ | wrapper routines
|handle_IRQ_event ---
|handler() -> timer_interrupt // registered IRQ 0 handler
|do_timer_interrupt
|do_timer
|jiffies++;
|update_process_times
|if (--counter <= 0) { // if time slice ended then
|counter = 0; // reset counter
|need_resched = 1; // prepare to reschedule
|}
|do_softirq
|while (need_resched) { // if necessary
|schedule // reschedule
|handle_softirq
|}
|RESTORE_ALL
?IRQ0x00_interrupt, SAVE_ALL [include/asm/hw_irq.h]
?do_IRQ, handle_IRQ_event [arch/i386/kernel/irq.c]
?timer_interrupt, do_timer_interrupt [arch/i386/kernel/time.c]
?do_timer, update_process_times [kernel/timer.c]
?do_softirq [kernel/soft_irq.c]
?RESTORE_ALL, while loop [arch/i386/kernel/entry.S]
系统启动核心时,调用start_kernal()继续各方面的初始化,在这之前,各种中断都被禁止,只有在完成必要的初始化后,直到执行完Kmalloc_init()后,才允许中断(init\main.c)。与时钟中断有关的部分初始化如下:
调用trap_init()设置各种trap入口,如system_call、GDT entry、LDT entry、call gate等。其中0~17为各种错误入口,18~47保留。
调用init_IRQ()函数设置核心系统的时钟周期为10ms,即100HZ,它是以后按照轮转法进行CPU调度时所依照的基准时钟周期。每10ms产生的时钟中断信号直接输入到第一块8259A的INT 0(即irq0)。初始化中断矢量表中从0x20起的17个中断矢量,用bad_IRQ#_interrupt函数的地址(#为中断号)填写。
调用sched_init()函数,设置启动第一个进程init_task。设置用于管理bottom_half机制的数据结构bh_base[],规定三类事件的中断处理函数,即时钟TIMER_BH、设备TQUEUE_BH和IMMEDIATE_BH。
调用time_init()函数,首先读取当时的CMOS时间,最后调用setup_x86_irq(0,&irq0)函数,把irq0挂到irq_action[0]队列的后面,并把中断矢量表中第0x20项,即timer中断对应的中断矢量改为IRQ0_interrupt函数的地址,在irq0中,指定时间中断服务程序是timer_interrupt,
static struct irqaction irq0 = { timer_interrupt, 0, 0, "timer", NULL, NULL}
结构irqaction的定义如下:
struct irqaction {
void (*handler)(int, void *, struct pt_regs *); /* 中断服务函数入口 */
unsigned long flags; /* 服务允中与否标记 */
unsigned long mask;
const char *name;
void *dev_id;

struct irqaction *next;
};
其中,若flag==SA_INTERRUPT,则中断矢量改为fast_IRQ#_interrupt,在执行中断服务的过程中不允许出现中断,若为其它标记,则中断矢量为IRQ#_interrupt,在执行中断服务的过程中,允许出现中断。
Irq_action的定义与初始化如下:
static void (*interrupt[17])(void) = {IRQ#_interrupt};
static void (*fast_interrupt[16])(void) = {fast_IRQ#_interrupt};
static void (*bad_interrupt[16])(void) = {bad_IRQ#_interrupt};(以上#为中断号)
static struct irqaction *irq_action[16] = {
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL
};
irq_action是一个全局数组,每个元素指向一个irq队列,共16个irq队列,时钟中断请求队列在第一个队列,即irq_action[0]。当每个中断请求到来时,都调用setup_x86_irq把该请求挂到相应的队列的后面。
以后,系统每10ms产生一次时钟中断信号,该信号直接输入到第一块8259A的INT 0(即irq0)。CPU根据中断矢量表和中断源,找到中断矢量函数入口IRQ0_interrupt(程序运行过程中允许中断)或者fast_IRQ0_interrupt(程序运行过程中不允许中断)或者bad_IRQ0_interrupt(不执行任何动作,直接返回),这些函数由宏BUILD_TIMER_IRQ(chip, nr, mask)展开定义。
宏BUILD_TIMER_IRQ(chip, nr, mask)的定义如下:
#define BUILD_TIMER_IRQ(chip,nr,mask) \
asmlinkage void IRQ_NAME(nr); \
asmlinkage void FAST_IRQ_NAME(nr); \
asmlinkage void BAD_IRQ_NAME(nr); \
__asm__( \
"\n"__ALIGN_STR"\n" \
SYMBOL_NAME_STR(fast_IRQ) #nr "_interrupt:\n\t" \
SYMBOL_NAME_STR(bad_IRQ) #nr "_interrupt:\n\t" \
SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \
"pushl $-"#nr"-2\n\t" \
SAVE_ALL \
ENTER_KERNEL \
ACK_##chip(mask,(nr&7)) \
"incl "SYMBOL_NAME_STR(intr_count)"\n\t"\ /* intr_count为进入临界区的同步信号量 */
"movl %esp,%ebx\n\t" \
"pushl %ebx\n\t" \
"pushl $" #nr "\n\t" \ /* 把do_irq函数参数压进堆栈 */
"call "SYMBOL_NAME_STR(do_IRQ)"\n\t" \
"addl $8,%esp\n\t" \
"cli\n\t" \
UNBLK_##chip(mask) \
"decl "SYMBOL_NAME_STR(intr_count)"\n\t" \
"incl "SYMBOL_NAME_STR(syscall_count)"\n\t" \
"jmp ret_from_sys_call\n");
其中nr为中断请求类型,取值0~15。在irq.c中通过语句BUILD_TIMER_IRQ(first, 0, 0x01)调用该宏,在执行宏的过程中处理时钟中断响应程序do_irq()。
函数do_irq()的第一个参数是中断请求队列序号,时钟中断请求传进来的该参数是0。于是程序根据参数0找到请求队列irq_action[0],逐个处理该队列上handler所指的时钟中断请求的服务函数。由于已经指定时钟中断请求

的服务函数是timer_interrupt,在函数timer_interrupt中,立即调用do_timer()函数。
函数do_timer()把jiffies和lost_ticks加1,接着就执行mark_bh(TIMER_BH)函数,把bottom_half中时钟队列对应的位置位,表示该队列处于激活状态。在做完这些动作后,程序从函数do_irq()中返回,继续执行以后的汇编代码。于是,程序在执行语句jmp ret_from_sys_call后,跳到指定的位置处继续执行。
代码段jmp ret_from_sys_call及其相关的代码段如下:
ALIGN
.globl ret_from_sys_call
ret_from_sys_call:
cmpl $0,SYMBOL_NAME(intr_count)
jne 2f
9: movl SYMBOL_NAME(bh_mask),%eax
andl SYMBOL_NAME(bh_active),%eax
jne handle_bottom_half
#ifdef __SMP__
cmpb $(NO_PROC_ID), SYMBOL_NAME(saved_active_kernel_processor)
jne 2f
#endif
movl EFLAGS(%esp),%eax # check VM86 flag: CS/SS are
testl $(VM_MASK),%eax # different then
jne 1f
cmpw $(KERNEL_CS),CS(%esp) # was old code segment supervisor ?
je 2f
1: sti
orl $(IF_MASK),%eax # these just try to make sure
andl $~NT_MASK,%eax # the program doesn't do anything
movl %eax,EFLAGS(%esp) # stupid
cmpl $0,SYMBOL_NAME(need_resched)
jne reschedule
#ifdef __SMP__
GET_PROCESSOR_OFFSET(%eax)
movl SYMBOL_NAME(current_set)(,%eax), %eax
#else
movl SYMBOL_NAME(current_set),%eax
#endif
cmpl SYMBOL_NAME(task),%eax # task[0] cannot have signals
je 2f
movl blocked(%eax),%ecx
movl %ecx,%ebx # save blocked in %ebx for signal handling
notl %ecx
andl signal(%eax),%ecx
jne signal_return
2: RESTORE_ALL
ALIGN
signal_return:
movl %esp,%ecx
pushl %ecx
testl $(VM_MASK),EFLAGS(%ecx)
jne v86_signal_return
pushl %ebx
call SYMBOL_NAME(do_signal)
popl %ebx
popl %ebx
RESTORE_ALL
ALIGN
v86_signal_return:
call SYMBOL_NAME(save_v86_state)
movl %eax,%esp
pushl %eax
pushl %ebx
call SYMBOL_NAME(do_signal)
popl %ebx
popl %ebx
RESTORE_ALL
handle_bottom_half:
incl SYMBOL_NAME(intr_count)
call SYMBOL_NAME(do_bottom_half)
decl SYMBOL_NAME(intr_count)
jmp 9f
ALIGN
reschedule:
pushl $ret_from_sys_call
jmp SYMBOL_NAME(schedule) # test
另外,一些与时钟中断及bottom half机制有关的数据结构介绍如下:
#define HZ 100
unsigned long volatile jiffies=0;
系统每隔10ms自动把它加1,它是核心系统计时的单位。
enum {
TIMER_BH = 0,
CONSOLE_BH,
TQUEUE_BH,
DIGI_BH,
SERIAL_BH,
RISCOM8_BH,
SPECIALIX_BH,
BAYCOM_BH,

NET_BH,
IMMEDIATE_BH,
KEYBOARD_BH,
CYCLADES_BH,
CM206_BH
};
现在只定义了13个bottom half队列,将来可扩充到32个队列。
unsigned long intr_count = 0;
相当于信号量的作用。只有其等于0,才可以do_bottom_half。
int bh_mask_count[32];
用来计算bottom half队列被屏蔽的次数。只有某队列的bh_mask_count数为0,才能enable该队列。
unsigned long bh_active = 0;
bh_active是32位长整数,每一位表示一个bottom half队列,该位置1,表示该队列处于激活状态,随时准备在CPU认为合适的时候执行该队列的服务,置0则相反。
unsigned long bh_mask = 0;
bh_mask也是32位长整数,每一位对应一个bottom half队列,该位置1,表示该队列可用,并把处理函数的入口地址赋给bh_base,置0则相反。
void (*bh_base[32])(void);
bottom half服务函数入口地址数组。定时器处理函数拥有最高的优先级,它的地址存放在bh_base[0],总是最先执行它所指向的函数。
我们注意到,在IRQ#_interrupt和fast_IRQ#_interrupt中断函数处理返回前,都通过语句jmp ret_from_sys_call,跳到系统调用的返回处(见irq.h),如果bottom half队列不为空,则在那里做类似:
if (bh_active & bh_mask) {
intr_count = 1;
do_bottom_half();
intr_count = 0;
}(该判断的汇编代码见Entry.S)
的判断,调用do_bottom_half()函数。
在CPU调度时,通过schedule函数执行上述的判断,再调用do_bottom_half()函数。
总而言之,在下列三种时机:
CPU调度时
系统调用返回前
中断处理返回前
都会作判断调用do_bottom_half函数。Do_bottom_half函数依次扫描32个队列,找出需要服务的队列,执行服务后把对应该队列的bh_active的相应位置0。由于bh_active标志中TIMER_BH对应的bit为1,因而系统根据服务函数入口地址数组bh_base找到函数timer_bh()的入口地址,并马上执行该函数,在函数timer_bh中,调用函数run_timer_list()和函数run_old_timers()函数,定时执行服务。
TVECS结构及其实现
有关TVECS结构的一些数据结构定义如下:
#define TVN_BITS 6
#define TVR_BITS 8
#define TVN_SIZE (1 << TVN_BITS)
#define TVR_SIZE (1 << TVR_BITS)
#define TVN_MASK (TVN_SIZE - 1)
#define TVR_MASK (TVR_SIZE - 1)
#define SLOW_BUT_DEBUGGING_TIMERS 0
struct timer_vec {
int index;
struct timer_list *vec[TVN_SIZE];
};
struct timer_vec_root {
int index;
struct timer_list *vec[TVR_SIZE];
};
static struct timer_vec tv5 = { 0 };
static struct timer_vec tv4 = { 0 };
static struct timer_vec tv3 = { 0 };
static struct timer_vec tv2 = { 0 };
static struct timer_vec_root tv1 = { 0 };
static struct timer_vec * const tvecs[] = {
(struct timer_vec *)&tv1, &

tv2, &tv3, &tv4, &tv5
};
#define NOOF_TVECS (sizeof(tvecs) / sizeof(tvecs[0]))
static unsigned long timer_jiffies = 0;
TVECS结构是一个元素个数为5的数组,分别指向tv1,tv2,tv3,tv4,tv5的地址。其中,tv1是结构timer_vec_root的变量,它有一个index域和有256个元素的指针数组,该数组的每个元素都是一条类型为timer_list的链表。其余四个元素都是结构timer_vec的变量,它们各有一个index域和64个元素的指针数组,这些数组的每个元素也都是一条链表。
函数internal_add_timer(struct timer_list *timer)
函数代码如下:
static inline void internal_add_timer(struct timer_list *timer)
{
/*
* must be cli-ed when calling this
*/
unsigned long expires = timer->expires;
unsigned long idx = expires - timer_jiffies;
if (idx < TVR_SIZE) {
int i = expires & TVR_MASK;
insert_timer(timer, tv1.vec, i);
} else if (idx < 1 << (TVR_BITS + TVN_BITS)) {
int i = (expires >> TVR_BITS) & TVN_MASK;
insert_timer(timer, tv2.vec, i);
} else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) {
int i = (expires >> (TVR_BITS + TVN_BITS)) & TVN_MASK;
insert_timer(timer, tv3.vec, i);
} else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) {
int i = (expires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK;
insert_timer(timer, tv4.vec, i);
} else if (expires < timer_jiffies) {
/* can happen if you add a timer with expires == jiffies,
* or you set a timer to go off in the past
*/
insert_timer(timer, tv1.vec, tv1.index);
} else if (idx < 0xffffffffUL) {
int i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK;
insert_timer(timer, tv5.vec, i);
} else {
/* Can only get here on architectures with 64-bit jiffies */
timer->next = timer->prev = timer;
}
}
expires

在调用该函数之前,必须关中。对该函数的说明如下:
取出要加进TVECS的timer的激发时间(expires),算出expires与timer_jiffies的差值idx,用来决定该插到哪个队列中去。
若idx小于2^8,则取expires的第0位到第7位的值I,把timer加到tv1.vec中第I个链表的第一个表项之前。
若idx小于2^14,则取expires的第8位到第13位的值I,把timer加到tv2.vec中第I个链表的第一个表项之前。
若idx小于2^20,则取expires的第14位到第19位的值I,把timer加到tv3.vec中第I个链表的第一个表项之前。
若idx小于2^26,则取expires的第20位到第25位的值I,把timer加到tv4.vec中第I个链表的第一个表项之前。
若expires小于timer_jiffies,即idx小于0,则表明该timer到期,应该把timer放入tv1.vec中tv1.index指定的链表的第

一个表项之前。
若idx小于2^32,则取expires的第26位到第32位的值I,把timer加到tv5.vec中第I个链表的第一个表项之前。
若idx大等于2^32,该情况只有在64位的机器上才有可能发生,在这种情况下,不把timer加入TVECS结构。
函数cascade_timers(struct timer_vec *tv)
该函数只是把tv->index指定的那条链表上的所有timer调用internal_add_timer()函数进行重新调整,这些timer将放入TVECS结构中比原来位置往前移一级,比如说,tv4上的timer将放到tv3上去,tv2上的timer将放到tv1上。这种前移是由run_timer_list函数里调用cascade_timers函数的时机来保证的。然后把该条链表置空,tv->index加1,若tv->index等于64,则重新置为0。
函数run_timer_list()
函数代码如下:
static inline void run_timer_list(void)
{
cli();
while ((long)(jiffies - timer_jiffies) >= 0) {
struct timer_list *timer;
if (!tv1.index) {
int n = 1;
do {
cascade_timers(tvecs[n]);
} while (tvecs[n]->index == 1 && ++n < NOOF_TVECS);
}
while ((timer = tv1.vec[tv1.index])) {
void (*fn)(unsigned long) = timer->function;
unsigned long data = timer->data;
detach_timer(timer);
timer->next = timer->prev = NULL;
sti();
fn(data);
cli();
}
++timer_jiffies;
tv1.index = (tv1.index + 1) & TVR_MASK;
}
sti();
}
对run_timer_list函数的说明如下:
关中。
判断jiffies是否大等于timer_jiffies,若不是,goto 8。
判断tv1.index是否为0(即此时系统已经扫描过整个tv1的256个timer_list链表,又回到的第一个链表处,此时需重整TVECS结构),若是,置n为1;若不是,goto 6。
调用cascade_timers()函数把TVECS[n]中由其index指定的那条链表上的timer放到TVECS[n-1]中来。注意:调用cascade_timers()函数后,index已经加1。
判断TVECS[n]->index是否为1,即原来为0。如果是(表明TVECS[n]上所有都已经扫描一遍,此时需对其后一级的TVECS[++n]调用cascade_timers()进行重整),把n加1,goto 4。
执行tv1.vec上由tv1->index指定的那条链表上的所有timer的服务函数,并把该timer从链表中移走。在执行服务函数的过程中,允许中断。
timer_jiffies加1,tv1->index加1,若tv1->index等于256,则重新置为0,goto 2。
开中,返回。
Linux提供了两种定时器服务。一种早期的由timer_struct等结构描述,由run_old_times函数处理。另一种“新”的服务由timer_list等结构描述,由add_timer、del_timer、cascade_time和run_timer_list等函数处理。
早期的定时器服务利用如下数据结构:
struct timer_struct {
unsigned long expires; /*本定时器被唤醒的时刻 */
void (*fn)(void)

; /* 定时器唤醒后的处理函数 */
}
struct timer_struct timer_table[32]; /*最多可同时启用32个定时器 */
unsigned long timer_active; /* 每位对应一定时器,置1表示启用 */
新的定时器服务依靠链表结构突破了32个的限制,利用如下的数据结构:
struct timer_list {
struct timer_list *next;
struct timer_list *prev;
unsigned long expires;
unsigned long data; /* 用来存放当前进程的PCB块的指针,可作为参数传
void (*function)(unsigned long); 给function */
}

表示上述数据结构的图示如下:

在这里,顺便简单介绍一下旧的timer机制的运作情况。
系统在每次调用函数do_bottom_half时,都会调用一次函数run_old_timers()。
函数run_old_timers()
该函数处理的很简单,只不过依次扫描timer_table中的32个定时器,若扫描到的定时器已经到期,并且已经被激活,则执行该timer的服务函数。
间隔定时器itimer
系统为每个进程提供了三个间隔定时器。当其中任意一个定时器到期时,就会发出一个信号给进程,同时,定时器重新开始运作。三种定时器描述如下:
ITIMER_REAL 真实时钟,到期时送出SIGALRM信号。
ITIMER_VIRTUAL 仅在进程运行时的计时,到期时送出SIGVTALRM信号。
ITIMER_PROF 不仅在进程运行时计时,在系统为进程运作而运行时它也计时,与ITIMER_VIRTUAL对比,该定时器通常为那些在用户态和核心态空间运行的应用所花去的时间计时,到期时送出SIGPROF信号。
与itimer有关的数据结构定义如下:
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
struct timeval {
int tv_sec; /* seconds */
int tv_usec; /* microseconds */
};
struct itimerspec {
struct timespec it_interval; /* timer period */
struct timespec it_value; /* timer expiration */
};
struct itimerval {
struct timeval it_interval; /* timer interval */
struct timeval it_value; /* current value */
};
这三种定时器在task_struct中定义:
struct task_struct {
……
unsigned long timeout;
unsigned long it_real_value,it_prof_value,it_virt_value;
unsigned long it_real_incr,it_prof_incr,it_virt_incr;
struct timer_list real_timer;
……
}
在进程创建时,系统把it_real_fn函数的入口地址赋给real_timer.function。(见sched.h)
我们小组分析了三个系统调用:sys_getitimer,sys_setitimer,sys_alarm。
在这三个系统调用中,需用到以下一些函数:
函数static int _getitimer(int which, struct itimerval *value)
该函数的运行过程大致如下:
根据传进的参数which按三种itimer分别处理:
若是ITIMER_REAL

,则设置interval为current进程的it_real_incr,val设置为0;判断current进程的real_timer有否设置并挂入TVECS结构中,若有,设置val为current进程real_timer的expires,并把real_timer重新挂到TVECS结构中,接着把val与当前jiffies作比较,若小等于当前jiffies,则说明该real_timer已经到期,于是重新设置val为当前jiffies的值加1。最后把val减去当前jiffies的值,goto 2。
若是ITIMER_VIRTUAL,则分别设置interval,val的值为current进程的it_virt_incr、it_virt_value,goto 2。
若是ITIMER_PROF,则分别设置interval,val的值为current进程的it_prof_incr、it_prof_value,goto 2。
(2)调用函数jiffiestotv把val,interval的jiffies值转换为timeval,返回0。
函数 int _setitimer(int which, struct itimerval *value, struct itimerval *ovalue)
该函数的运行过程大致如下:
调用函数tvtojiffies把value中的interval和value转换为jiffies i 和 j。
判断指针ovalue是否为空,若空,goto ;若不空,则把由which指定类型的itimer存入ovalue中,若存放不成功,goto 4;
根据which指定的itimer按三种类型分别处理:
若是ITIMER_REAL,则从TVECS结构中取出current进程的real_timer,并重新设置current进程的it_real_value和it_real_incr为j和i。若j等于0,goto 4;若不等于0,则把当前jiffies的值加上定时器剩余时间j,得到触发时间。若i小于j,则表明I已经溢出,应该重新设为ULONG_MAX。最后把current进程的real_timer的expires设为i,把设置过的real_timer重新加入TVECS结构,goto 4。
若是ITIMER_VIRTUAL,则设置current进程的it-_virt_value和it_virt_incr为j和i。
若是ITIMER_PROF,则设置current进程的it-_prof_value和it_prof_incr为j和i。
(4)返回0。
函数verify_area(int type, const void *addr, unsigned long size)
该函数的主要功能是对以addr为始址的,长度为size的一块存储区是否有type类型的操作权利。
函数memcpy_tofs(to, from, n)
该函数的主要功能是从以from为始址的存储区中取出长度为n的一块数据放入以to为始址的存储区。
函数memcpy_fromfs(from, to, n)
该函数的主要功能是从以from为始址的存储区中取出长度为n的一块数据放入以to为始址的存储区。
函数memset((char*)&set_buffer, 0, sizeof(set_buffer))
该函数的主要功能是把set_buffer中的内容置为0,在这里,即把it_value和it_interval置为0。
现在,我简单介绍一下这三个系统调用:
系统调用sys_getitimer(int which, struct itimerval *value)
首先,若value为NULL,则返回-EFAULT,说明这是一个bad address。
其次,把which类型的itimer取出放入get_buffer。
再次,若存放成功,再确认对value的写权利。
最后,则把get_buffer中的itimer取出,拷入value。
系统调用sys_setitimer(int which, struct itimerval *value,struct itimerval *ovalue)

先,判断value是否为NULL,若不是,则确认对value是否有读的权利,并把set_buffer中的数据拷入value;若value为NULL,则把set_buffer中的内容置为0,即把it_value和it_interval置为0。
其次,判断ovalue是否为NULL,若不是,则确认对ovalue是否有写的权利。
再次,调用函数_setitimer设置由which指定类型的itimer。
最后,调用函数memcpy_tofs把get_buffer中的数据拷入ovalue,返回。
系统调用sys_alarm(unsigned int seconds)
该系统调用重新设置进程的real_itimer,若seconds为0,则把原先的alarm定时器删掉。并且设interval为0,故只触发一次,并把旧的real_timer存入oldalarm,并返回oldalarm。
[目录]
________________________________________


from aka

[目录]
________________________________________


硬件中断

硬件中断
硬件中断概述
中断可以用下面的流程来表示:
中断产生源 --> 中断向量表 (idt) --> 中断入口 ( 一般简单处理后调用相应的函数) --->do_IRQ--> 后续处理(软中断等工作)
具体地说,处理过程如下:
中断信号由外部设备发送到中断芯片(模块)的引脚
中断芯片将引脚的信号转换成数字信号传给CPU,例如8259主芯片引脚0发送的是0x20
CPU接收中断后,到中断向量表IDT中找中断向量
根据存在中断向量中的数值找到向量入口
由向量入口跳转到一个统一的处理函数do_IRQ
在do_IRQ中可能会标注一些软中断,在执行完do_IRQ后执行这些软中断。
下面一一介绍。
8259芯片
本文主要参考周明德《微型计算机系统原理及应用》和billpan的相关帖子
1.中断产生过程
(1)如果IR引脚上有信号,会使中断请求寄存器(Interrupt Request Register,IRR)相应的位置位,比如图中, IR3, IR4, IR5上有信号,那么IRR的3,4,5为1
(2)如果这些IRR中有一个是允许的,也就是没有被屏蔽,那么就会通过INT向CPU发出中断请求信号。屏蔽是由中断屏蔽寄存器(Interrupt Mask Register,IMR)来控制的,比如图中位3被置1,也就是IRR位3的信号被屏蔽了。在图中,还有4,5的信号没有被屏蔽,所以,会向CPU发出请求信号。
(3)如果CPU处于开中断状态,那么在执行指令的最后一个周期,在INTA上做出回应,并且关中断.
(4)8259A收到回应后,将中断服务寄存器(In-Service Register)置位,而将相应的IRR复位:
8259芯片会比较IRR中的中断的优先级,如上图中,由于IMR中位3处于屏蔽状态,所以实际上只是比较IR4,I5,缺省情况下,IR0最高,依次往下,IR7最低(这种优先级可以被设置),所以上图中,ISR被设置为4.
(5)在CPU发出下一个INTA信号时,8259将中断号送到数据线上,从而能被CPU接收到,这里有个问题:比如在上图中,8259获得的是数4,但是CPU需要的是中断号(并不为4),从而

可以到idt找相应的向量。所以有一个从ISR的信号到中断号的转换。在Linux的设置中,4对应的中断号是0x24.
(6)如果8259处于自动结束中断(Automatic End of Interrupt AEOI)状态,那么在刚才那个INTA信号结束前,8259的ISR复位(也就是清0),如果不处于这个状态,那么直到CPU发出EOI指令,它才会使得ISR复位。
2.一些相关专题
(1)从8259
在x86单CPU的机器上采用两个8259芯片,主芯片如上图所示,x86模式规定,从8259将它的INT脚与主8259的IR2相连,这样,如果从8259芯片的引脚IR8-IR15上有中断,那么会在INT上产生信号,主8259在IR2上产生了一个硬件信号,当它如上面的步骤处理后将IR2的中断传送给CPU,收到应答后,会通过CAS通知从8259芯片,从8259芯片将IRQ中断号送到数据线上,从而被CPU接收。
由此,我猜测它产生的所有中断在主8259上优先级为2,不知道对不对。
(2)关于屏蔽
从上面可以看出,屏蔽有两种方法,一种作用于CPU, 通过清除IF标记,使得CPU不去响应8259在INT上的请求。也就是所谓关中断。
另一种方法是,作用于8259,通过给它指令设置IMR,使得相应的IRR不参与ISR(见上面的(4)),被称为禁止(disable),反之,被称为允许(enable).
每次设置IMR只需要对端口0x21(主)或0xA1(从)输出一个字节即可,字节每位对应于IMR每位,例如:
outb(cached_21,0x21);
为了统一处理16个中断,Linux用一个16位cached_irq_mask变量来记录这16个中断的屏蔽情况:
static unsigned int cached_irq_mask = 0xffff;
为了分别对应于主从芯片的8位IMR,将这16位cached_irq_mask分成两个8位的变量:
#define __byte(x,y) (((unsigned char *)&(y))[x])
#define cached_21 (__byte(0,cached_irq_mask))
#define cached_A1 (__byte(1,cached_irq_mask))
在禁用某个irq的时候,调用下面的函数:
void disable_8259A_irq(unsigned int irq){
unsigned int mask = 1 << irq;
unsigned long flags;
spin_lock_irqsave(&i8259A_lock, flags);
cached_irq_mask |= mask; /*-- 对这16位变量设置 */
if (irq & 8) /*-- 看是对主8259设置还是对从芯片设置 */
outb(cached_A1,0xA1); /*-- 对从8259芯片设置 */
else
outb(cached_21,0x21); /*-- 对主8259芯片设置 */
spin_unlock_irqrestore(&i8259A_lock, flags);
}

(3)关于中断号的输出

8259在ISR里保存的只是irq的ID,但是它告诉CPU的是中断向量ID,比如ISR保存时钟中断的ID 0,但是在通知CPU却是中断号0x20.因此需要建立一个映射。在8259芯片产生的IRQ号必须是连续的,也就是如果irq0对应的是中断向量0x20,那么irq1对应的就是0x21,...
在i8259.c/init_8259A()中,进行设置:
outb_p(0x11, 0x20); /* ICW1: select 8259A-1 init */
outb_p(0x20 + 0, 0x21); /* ICW2: 8259A-1 IR0-7 mapped to 0x20-0x27 */
outb_p(0x04, 0x21); /* 8259A-1 (t

he master) has a slave on IR2 */
if (auto_eoi)
outb_p(0x03, 0x21); /* master does Auto EOI */
else
outb_p(0x01, 0x21); /* master expects normal EOI */
outb_p(0x11, 0xA0); /* ICW1: select 8259A-2 init */
outb_p(0x20 + 8, 0xA1); /* ICW2: 8259A-2 IR0-7 mapped to 0x28-0x2f */
outb_p(0x02, 0xA1); /* 8259A-2 is a slave on master's IR2 */
outb_p(0x01, 0xA1); /* (slave's support for AEOI in flat mode is to be investigated) */

这样,在IDT的向量0x20-0x2f可以分别填入相应的中断处理函数的地址了。
i386中断门描述符
段选择符和偏移量决定了中断处理函数的入口地址
在这里段选择符指向内核中唯一的一个代码段描述符的地址__KERNEL_CS(=0x10),而这个描述符定义的段为0到4G:
---------------------------------------------------------------------------------
ENTRY(gdt_table) .quad 0x0000000000000000 /* NULL descriptor */
.quad 0x0000000000000000 /* not used */
.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
... ...
---------------------------------------------------------------------------------
而偏移量就成了绝对的偏移量了,在IDT的描述符中被拆成了两部分,分别放在头和尾。
P标志着这个代码段是否在内存中,本来是i386提供的类似缺页的机制,在Linux中这个已经不用了,都设成1(当然内核代码是永驻内存的,但即使不在内存,推测linux也只会用缺页的标志)。
DPL在这里是0级(特权级)
0D110中,D为1,表明是32位程序(这个细节见i386开发手册).110是中断门的标识,其它101是任务门的标识, 111是陷阱(trap)门标识。
Linux对中断门的设置
于是在Linux中对硬件中断的中断门的设置为:
init_IRQ(void)
---------------------------------------------------------
for (i = 0; i < NR_IRQS; i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[ i]);
}
----------------------------------------------------------
其中,FIRST_EXTERNAL_VECTOR=0x20,恰好为8259芯片的IR0的中断门(见8259部分),也就是时钟中断的中断门),interrupt[ i]为相应处理函数的入口地址
NR_IRQS=224, =256(IDT的向量总数)-32(CPU保留的中断的个数),在这里设置了所有可设置的向量。
SYSCALL_VECTOR=0x80,在这里意思是避开系统调用这个向量。

而set_intr_gate的定义是这样的:
----------------------------------------------------
void set_intr_gate(unsigned int n, void *addr){
_set_gate(idt_table+n,14,0,addr);
}
----------------------------------------------------
其中,需要解释的是:14是标识指明这个是中断门,注意上面的0D110=01110=14;另外,0指明的是DPL.
中断入口

以8259的16个中断为例:
通过宏BUILD_16_IRQS(0x0), BI(x,y),以及
#define BUILD_IRQ(nr) \
asmlinkage void IRQ_NAME(nr); \
__asm__( \
"\n"__ALIGN_STR"\n" \
SYMBOL_NAME_STR(IRQ) #nr "_inte

rrupt:\n\t" \
"pushl $"#nr"-256\n\t" \
"jmp common_interrupt");
得到的16个中断处理函数为:

IRQ0x00_interrupt:
push $0x00 - 256
jump common_interrupt
IRQ0x00_interrupt:
push $0x01 - 256
jump common_interrupt
... ...

IRQ0x0f_interrupt:
push $0x0f - 256
jump common_interrupt

这些处理函数简单的把中断号-256(为什么-256,也许是避免和内部中断的中断号有冲突)压到栈中,然后跳到common_interrupt

其中common_interrupt是由宏BUILD_COMMON_IRQ()展开:
#define BUILD_COMMON_IRQ() \
asmlinkage void call_do_IRQ(void); \
__asm__( \
"\n" __ALIGN_STR"\n" \
"common_interrupt:\n\t" \
SAVE_ALL \
"pushl $ret_from_intr\n\t" \
SYMBOL_NAME_STR(call_do_IRQ)":\n\t" \
"jmp "SYMBOL_NAME_STR(do_IRQ));
.align 4,0x90common_interrupt:
SAVE_ALL展开的保护现场部分
push $ret_from_intrcall
do_IRQ:
jump do_IRQ;
从上面可以看出,这16个的中断处理函数不过是把中断号-256压入栈中,然后保护现场,最后调用do_IRQ .在common_interrupt中,为了使do_IRQ返回到entry.S的ret_from_intr标号,所以采用的是压入返回点ret_from_intr,用jump来模拟一个从ret_from_intr上面对do_IRQ的一个调用。
和IDT的衔接
为了便于IDT的设置,在数组interrupt中填入所有中断处理函数的地址:
void (*interrupt[NR_IRQS])(void) = {
IRQ0x00_interrupt,
IRQ0x01_interrupt,
... ...
}
在中断门的设置中,可以看到是如何利用这个数组的。
硬件中断处理函数do_IRQ
do_IRQ的相关对象
在do_IRQ中,一个中断主要由三个对象来完成
其中, irq_desc_t对象构成的irq_desc[]数组元素分别对应了224个硬件中断(idt一共256项,cpu自己前保留了32项,256-32=224,当然这里面有些项是不用的,比如x80是系统调用).
当发生中断时,函数do_IRQ就会在irq_desc[]相应的项中提取各种信息来完成对中断的处理。
irq_desc有一个字段handler指向发出这个中断的设备的处理对象hw_irq_controller,比如在单CPU,这个对象一般就是处理芯片8259的对象。为什么要指向这个对象呢?因为当发生中断的时候,内核需要对相应的中断进行一些处理,比如屏蔽这个中断等。这个时候需要对中断设备(比如8259芯片)进行操作,于是可以通过这个指针指向的对象进行操作。
irq_desc还有一个字段action指向对象irqaction,后者是产生中断的设备的处理对象,其中的handler就是处理函数。由于一个中断可以由多个设备发出,Linux内核采用轮询的方式,将所有产生这个中断的设备的处理对象连成一个链表,一个一个执行。
例如,硬盘1,硬盘2都产生中断IRQx,在do_IRQ中首先找到irq_desc[x],通过字段handler对产生中断IRQx的设备进行处理(对8259而言,就是屏蔽以后的中断IRQx),然后通过action先后运行硬盘1和硬盘2的处理函数。

hw_irq_controller
hw_

相关文档