中断 |
目 录
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 (星仔迷), 信区: SysInternals 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_vec[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。
硬件中断
硬件中断概述
中断可以用下面的流程来表示:
中断产生源 --> 中断向量表 (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 (the 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 "_interrupt:/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_irq_controller有多种:
1.在一般单cpu的机器上,通常采用两个8259芯片,因此hw_irq_controller指的就是i8259A_irq_type
2.在多CPU的机器上,采用APIC子系统来处理芯片,APIC有3个部分组成,一个是I/O APIC模块,其作用可比做8259芯片,但是它发出的中断信号会通过 APIC总线送到其中一个(或几个)CPU中的Local APIC模块,因此,它还起一个路由的作用;它可以接收16个中断。
中断可以采取两种方式,电平触发和边沿触发,相应的,I/O APIC模块的hw_irq_controller就有两种:
ioapic_level_irq_type
ioapic_edge_irq_type
(这里指的是intel的APIC,还有其它公司研制的APIC,我没有研究过)
3. Local APIC自己也能单独处理一些直接对CPU产生的中断,例如时钟中断(这和没有使用Local APIC模块的CPU不同,它们接收的时钟中断来自外围的时钟芯片),因此,它也有自己的 hw_irq_controller:
lapic_irq_type
struct hw_interrupt_type {
const char * typename;
unsigned int (*startup)(unsigned int irq);
void (*shutdown)(unsigned int irq);
void (*enable)(unsigned int irq);
void (*disable)(unsigned int irq);
void (*ack)(unsigned int irq);
void (*end)(unsigned int irq);
void (*set_affinity)(unsigned int irq, unsigned long mask);
};
typedef struct hw_interrupt_type hw_irq_controller;
startup 是启动中断芯片(模块),使得它开始接收中断,一般情况下,就是将 所有被屏蔽的引脚取消屏蔽
shutdown 反之,使得芯片不再接收中断
enable 设某个引脚可以接收中断,也就是取消屏蔽
disable 屏蔽某个引脚,例如,如果屏蔽0那么时钟中断就不再发生
ack 当CPU收到来自中断芯片的中断信号,给相应的引脚的处理,这个各种情况下 (8259, APIC电平,边沿)的处理都不相同
end 在CPU处理完某个引脚产生的中断后,对中断芯片(模块)的操作。
irqaction
将一个硬件处理函数挂到相应的处理队列上去(当然首先要生成一个irqaction结构):
-----------------------------------------------------
int request_irq(unsigned int irq,
void (*handler)(int, void *, struct pt_regs *),
unsigned long irqflags,
const char * devname,
void *dev_id)
-----------------------------------------------------
参数说明在源文件里说得非常清楚。
handler是硬件处理函数,在下面的代码中可以看得很清楚:
---------------------------------------------
do {
status |= action->flags;
action->handler(irq, action->dev_id, regs);
action = action->next;
} while (action);
---------------------------------------------
第二个参数就是action的dev_id,这个参数非常灵活,可以派各种用处。而且要保证的是,这个dev_id在这个处理链中是唯一的,否则删除会遇到麻烦。
第三个参数是在entry.S中压入的各个积存器的值。
它的大致流程是:
1.在slab中分配一个irqaction,填上必需的数据
以下在函数setup_irq中。
2.找到它的irq对应的结构irq_desc
3.看它是否想对随机数做贡献
4.看这个结构上是否已经挂了其它处理函数了,如果有,则必须确保它本身和这个队列上所有的处理函数都是可共享的(由于传递性,只需判断一个就可以了)
5.挂到队列最后
6.如果这个irq_desc只有它一个irqaction,那么还要进行一些初始化工作
7在proc/下面登记 register_irq_proc(irq)(这个我不太明白)
将一个处理函数取下:
void free_irq(unsigned int irq, void *dev_id)
首先在队列里找到这个处理函数(严格的说是irqaction),主要靠dev_id来匹配,这时dev_id的唯一性就比较重要了。
将它从队列里剔除。
如果这个中断号没有处理函数了,那么禁止这个中断号上再产生中断:
if (!desc->action) {
desc->status |= IRQ_DISABLED;
desc->handler->shutdown(irq);
}
如果其它CPU在运行这个处理函数,要等到它运行完了,才释放它:
#ifdef CONFIG_SMP
/* Wait to make sure it's not being used on another CPU */
while (desc->status & IRQ_INPROGRESS)
barrier();
#endif
kfree(action);
do_IRQ
asmlinkage unsigned int do_IRQ(struct pt_regs regs)
1.首先取中断号,并且获取对应的irq_desc:
int irq = regs.orig_eax & 0xff; /* high bits used in ret_from_ code */
int cpu = smp_processor_id();
irq_desc_t *desc = irq_desc + irq;
2.对中断芯片(模块)应答:
desc->handler->ack(irq);
3.修改它的状态(注:这些状态我觉得只有在SMP下才有意义):
status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
status |= IRQ_PENDING; /* we _want_ to handle it */
IRQ_REPLAY是指如果被禁止的中断号上又产生了中断,这个中断是不会被处理的,当这个中断号被允许产生中断时,会将这个未被处理的中断转为IRQ_REPLAY。
IRQ_WAITING 探测用,探测时,会将所有没有挂处理函数的中断号上设置IRQ_WAITING,如果这个中断号上有中断产生,就把这个状态去掉,因此,我们就可以知道哪些中断引脚上产生过中断了。
IRQ_PENDING , IRQ_INPROGRESS是为了确保:
同一个中断号的处理程序不能重入
不能丢失这个中断号的下一个处理程序
具体的说,当内核在运行某个中断号对应的处理程序(链)时,状态会设置成IRQ_INPROGRESS。如果在这期间,同一个中断号上又产生了中断,并且传给CPU,那么当内核打算再次运行这个中断号对应的处理程序(链)时,发现已经有一个实例在运行了,就将这下一个中断标注为IRQ_PENDING, 然后返回。这个已在运行的实例结束的时候,会查看是否期间有同一中断发生了,是则再次执行一遍。
这些状态的操作不是在什么情况下都必须的,事实上,一个CPU,用8259芯片,无论即使是开中断,也不会发生中断重入的情况,因为在这期间,内核把同一中断屏蔽掉了。
多个CPU比较复杂,因为CPU由Local APIC,每个都有自己的中断,但是它们可能调用同一个函数,比如时钟中断,每个CPU都可能产生,它们都会调用时钟中断处理函数。
从I/O APIC传过来的中断,如果是电平触发,也不会,因为在结束发出EOI前,这个引脚上是不接收中断信号。如果是边沿触发,要么是开中断,要么I/O APIC选择不同的CPU,在这两种情况下,会有重入的可能。
/*
* If the IRQ is disabled for whatever reason, we cannot
* use the action we have.
*/
action = NULL;
if (!(status & (IRQ_DISABLED | IRQ_INPROGRESS))) {
action = desc->action;
status &= ~IRQ_PENDING; /* we commit to handling */
status |= IRQ_INPROGRESS; /* we are handling it *//*进入执行状态*/
}
desc->status = status;
/*
* If there is no IRQ handler or it was disabled, exit early.
Since we set PENDING, if another processor is handling
a different instance of this same irq, the other processor
will take care of it.
*/
if (!action)
goto out;/*要么该中断没有处理函数;要么被禁止运行(IRQ_DISABLE);要么有一个实例已经在运行了*/
/*
* Edge triggered interrupts need to remember
* pending events.
* This applies to any hw interrupts that allow a second
* instance of the same irq to arrive while we are in do_IRQ
* or in the handler. But the code here only handles the _second_
* instance of the irq, not the third or fourth. So it is mostly
* useful for irq hardware that does not mask cleanly in an
* SMP environment.
*/
for (;;) {
spin_unlock(&desc->lock);
handle_IRQ_event(irq, ®s, action);/*执行函数链*/
spin_lock(&desc->lock);
if (!(desc->status & IRQ_PENDING))/*发现期间有中断,就再次执行*/
break;
desc->status &= ~IRQ_PENDING;
}
desc->status &= ~IRQ_INPROGRESS;/*退出执行状态*/
out:
/*
* The ->end() handler has to deal with interrupts which got
* disabled while the handler was running.
*/
desc->handler->end(irq);/*给中断芯片一个结束的操作,一般是允许再次接收中断*/
spin_unlock(&desc->lock);
if (softirq_active(cpu) & softirq_mask(cpu))
do_softirq();/*执行软中断*/
return 1;
}
软中断softirq
softirq简介
提出softirq的机制的目的和老版本的底半部分的目的是一致的,都是将某个中断处理的一部分任务延迟到后面去执行。
Linux内核中一共可以有32个softirq,每个softirq实际上就是指向一个函数。当内核执行softirq(do_softirq),就对这32个softirq进行轮询:
(1)是否该softirq被定义了,并且允许被执行?
(2)是否激活了(也就是以前有中断要求它执行)?
如果得到肯定的答复,那么就执行这个softirq指向的函数。
值得一提的是,无论有多少个CPU,内核一共只有32个公共的softirq,但是每个CPU可以执行不同的softirq,可以禁止/起用不同的softirq,可以激活不同的softirq,因此,可以说,所有CPU有相同的例程,但是
每个CPU却有自己完全独立的实例。
对(1)的判断是通过考察irq_stat[ cpu ].mask相应的位得到的。这里面的cpu指的是当前指令所在的cpu.在一开始,softirq被定义时,所有的cpu的掩码mask都是一样的。但是在实际运行中,每个cpu上运行的程序可以根据自己的需要调整。
对(2)的判断是通过考察irq_stat[ cpu ].active相应的位得到的.
虽然原则上可以任意定义每个softirq的函数,Linux内核为了进一步加强延迟中断功能,提出了tasklet的机制。tasklet实际上也就是一个函数。在第0个softirq的处理函数tasklet_hi_action中,我们可以看到,当执行这个函数的时候,会依次执行一个链表上所有的tasklet.
我们大致上可以把softirq的机制概括成:
内核依次对32个softirq轮询,如果遇到一个可以执行并且需要的softirq,就执行对应的函数,这些函数有可能又会执行一个函数队列。当执行完这个函数队列后,才会继续询问下一个softirq对应的函数。
挂上一个软中断
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
unsigned long flags;
int i;
spin_lock_irqsave(&softirq_mask_lock, flags);
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
for (i=0; i<NR_CPUS; i++)
softirq_mask(i) |= (1<<nr);
spin_unlock_irqrestore(&softirq_mask_lock, flags);
}
其中对每个CPU的softirq_mask都标注一下,表明这个softirq被定义了。
tasklet
在这个32个softirq中,有的softirq的函数会依次执行一个队列中的tasklet
tasklet其实就是一个函数。它的结构如下:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
next 用于将tasklet串成一个队列
state 表示一些状态,后面详细讨论
count 用来禁用(count = 1 )或者启用( count = 0 )这个tasklet.因为一旦一个tasklet被挂到队列里,如果没有这个机制,它就一定会被执行。 这个count算是一个事后补救措施,万一挂上了不想执行,就可以把它置1。
func 即为所要执行的函数。
data 由于可能多个tasklet调用公用函数,因此用data可以区分不同tasklet.
如何将一个tasklet挂上
首先要初始化一个tasklet,填上相应的参数
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->func = func;
t->data = data;
t->state = 0;
atomic_set(&t->count, 0);
}
然后调用schedule函数,注意,下面的函数仅仅是将这个tasklet挂到 TASKLET_SOFTIRQ对应的软中断所执行的tasklet队列上去, 事实上,还有其它的软中断,比如HI_SOFTIRQ,会执行其它的tasklet队列,如果要挂上,那么就要调用tasklet_hi_schedule(). 如果你自己写的softirq执行一个tasklet队列,那么你需要自己写类似下面的函数。
static inline void tasklet_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_vec[cpu].list;
/**/ tasklet_vec[cpu].list = t;
__cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
local_irq_restore(flags);
}
}
这个函数中/**/标注的句子用来挂接上tasklet,
__cpu_raise_softirq用来激活TASKLET_SOFTIRQ,这样,下次执行do_softirq就会执行这个TASKLET_SOFTIRQ软中断了
__cpu_raise_softirq定义如下:
static inline void __cpu_raise_softirq(int cpu, int nr)
{
softirq_active(cpu) |= (1<<nr);
}
tasklet的运行方式
我们以tasklet_action为例,来说明tasklet运行机制。事实上,还有一个函数tasklet_hi_action同样也运行tasklet队列。
首先值得注意的是,我们前面提到过,所有的cpu共用32个softirq,但是同一个softirq在不同的cpu上执行的数据是独立的,基于这个原则,tasklet_vec对每个cpu都有一个,每个cpu都运行自己的tasklet队列。
当执行一个tasklet队列时,内核将这个队列摘下来,以list为队列头,然后从list的下一个开始依次执行。这样做达到什么效果呢?在执行这个队列时,这个队列的结构是静止的,如果在运行期间,有中断产生,并且往这个队列里添加tasklet的话,将填加到tasklet_vec[cpu].list中, 注意这个时候,这个队列里的任何tasklet都不会被执行,被执行的是list接管的队列。
见/*1*//*2/之间的代码。事实上,在一个队列上同时添加和运行也是可行的,没这个简洁。
-----------------------------------------------------------------
static void tasklet_action(struct softirq_action *a)
{
int cpu = smp_processor_id();
struct tasklet_struct *list;
/*1*/ local_irq_disable();
list = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = NULL;
/*2*/ local_irq_enable();
while (list != NULL) {
struct tasklet_struct *t = list;
list = list->next;
/*3*/ if (tasklet_trylock(t)) {
if (atomic_read(&t->count) == 0) {
clear_bit(TASKLET_STATE_SCHED, &t->state);
t->func(t->data);
/*
* talklet_trylock() uses test_and_set_bit that imply
* an mb when it returns zero, thus we need the explicit
* mb only here: while closing the critical section.
*/
#ifdef CONFIG_SMP
/*?*/ smp_mb__before_clear_bit();
#endif
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
/*4*/ local_irq_disable();
t->next = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = t;
__cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
/*5*/ local_irq_enable();
}
}
-------------------------------------------------------------
/*3*/看其它cpu是否还有同一个tasklet在执行,如果有的话,就首先将这个tasklet重新放到tasklet_vec[cpu].list指向的预备队列(见/*4*/~/*5*/),而后跳过这个tasklet.
这也就说明了tasklet是不可重入的,以防止两个相同的tasket访问同样的变量而产生竞争条件(race condition)
tasklet的状态
在tasklet_struct中有一个属性state,用来表示tasklet的状态:
tasklet的状态有3个:
1.当tasklet被挂到队列上,还没有执行的时候,是 TASKLET_STATE_SCHED
2.当tasklet开始要被执行的时候,是 TASKLET_STATE_RUN
其它时候,则没有这两个位的设置
其实还有另一对状态,禁止或允许,tasklet_struct中用count表示,用下面的函数操作
-----------------------------------------------------
static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{
atomic_inc(&t->count);
}
static inline void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t);
tasklet_unlock_wait(t);
}
static inline void tasklet_enable(struct tasklet_struct *t)
{
atomic_dec(&t->count);
}
-------------------------------------------------------
下面来验证1,2这两个状态:
当被挂上队列时:
首先要测试它是否已经被别的cpu挂上了,如果已经在别的cpu挂上了,则不再将它挂上,否则设置状态为TASKLET_STATE_SCHED
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
... ...
}
为什么要这样做?试想,如果一个tasklet已经挂在一队列上,内核将沿着这个队列一个个执行,现在如果又被挂到另一个队列上,那么这个tasklet的指针指向另一个队列,内核就会沿着它走到错误的队列中去了。
tasklet开始执行时:
在tasklet_action中:
------------------------------------------------------------
while (list != NULL) {
struct tasklet_struct *t = list;
/*0*/ list = list->next;
/*1*/ if (tasklet_trylock(t)) {
/*2*/ if (atomic_read(&t->count) == 0) {
/*3*/ clear_bit(TASKLET_STATE_SCHED, &t->state);
t->func(t->data);
/*
* talklet_trylock() uses test_and_set_bit that imply
* an mb when it returns zero, thus we need the explicit
* mb only here: while closing the critical section.
*/
#ifdef CONFIG_SMP
smp_mb__before_clear_bit();
#endif
/*4*/ tasklet_unlock(t);
continue;
}
---------------------------------------------------------------
1 看是否是别的cpu上这个tasklet已经是 TASKLET_STATE_RUN了,如果是就跳过这个tasklet
2 看这个tasklet是否被允许运行?
3 清除TASKLET_STATE_SCHED,为什么现在清除,它不是还没有从队列上摘下来吗?事实上,它的指针已经不再需要的,它的下一个tasklet已经被list记录了(/*0*/)。这样,如果其它cpu把它挂到其它的队列上去一点影响都没有。
4 清除TASKLET_STATE_RUN标志
1和4确保了在所有cpu上,不可能运行同一个tasklet,这样在一定程度上确保了tasklet对数据操作是安全的,但是不要忘了,多个tasklet可能指向同一个函数,所以仍然会发生竞争条件。
可能会有疑问:假设cpu 1上已经有tasklet 1挂在队列上了,cpu2应该根本挂不上同一个tasklet 1,怎么会有tasklet 1和它发生重入的情况呢?
答案就在/*3*/上,当cpu 1的tasklet 1已经不是TASKLET_STATE_SCHED,而它还在运行,这时cpu2完全有可能挂上同一个tasklet 1,而且使得它试图运行,这时/*1*/的判断就起作用了。
软中断的重入
一般情况下,在硬件中断处理程序后都会试图调用do_softirq执行软中断,但是如果发现现在已经有中断在运行,或者已经有软中断在运行,则
不再运行自己调用的中断。也就是说,软中断是不能进入硬件中断部分的,并且软中断在一个cpu上是不可重入的,或者说是串行化的(serialize)
其目的是避免访问同样的变量导致竞争条件的出现。在开中断的中断处理程序中不允许调用软中断可能是希望这个中断处理程序尽快结束。
这是由do_softirq中的
if (in_interrupt())
return;
保证的.
其中,
#define in_interrupt() ({ int __cpu = smp_processor_id(); /
(local_irq_count(__cpu) + local_bh_count(__cpu) != 0); })
前者local_irq_count(_cpu):
当进入硬件中断处理程序时,handle_IRQ_event中的irq_enter(cpu, irq)会将它加1,表明又进入一个硬件中断
退出则调用irq_exit(cpu, irq)
后者local_bh_count(__cpu) :
当进入软中断处理程序时,do_softirq中的local_bh_disable()会将它加1,表明处于软中断中
local_bh_disable();
一个例子:
当内核正在执行处理定时器的软中断时,这期间可能会发生多个时钟中断,这些时钟中断的处理程序都试图再次运行处理定时器的软中断,但是由于 已经有个软中断在运行了,于是就放弃返回。
软中断调用时机
最直接的调用:
当硬中断执行完后,迅速调用do_softirq来执行软中断(见下面的代码),这样,被硬中断标注的软中断能得以迅速执行。当然,不是每次调用都成功的,见前面关于重入的帖子。
-----------------------------------------------------
asmlinkage unsigned int do_IRQ(struct pt_regs regs)
{
... ...
if (softirq_active(cpu) & softirq_mask(cpu))
do_softirq();
}
-----------------------------------------------------
还有,不是每个被标注的软中断都能在这次陷入内核的部分中完成,可能会延迟到下次中断。
其它地方的调用:
在entry.S中有一个调用点:
handle_softirq:
call SYMBOL_NAME(do_softirq)
jmp ret_from_intr
有两处调用它,一处是当系统调用处理完后:
ENTRY(ret_from_sys_call)
#ifdef CONFIG_SMP
movl processor(%ebx),%eax
shll $CONFIG_X86_L1_CACHE_SHIFT,%eax
movl SYMBOL_NAME(irq_stat)(,%eax),%ecx # softirq_active
testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx # softirq_mask
#else
movl SYMBOL_NAME(irq_stat),%ecx # softirq_active
testl SYMBOL_NAME(irq_stat)+4,%ecx # softirq_mask
#endif
jne handle_softirq
一处是当异常处理完后:
ret_from_exception:
#ifdef CONFIG_SMP
GET_CURRENT(%ebx)
movl processor(%ebx),%eax
shll $CONFIG_X86_L1_CACHE_SHIFT,%eax
movl SYMBOL_NAME(irq_stat)(,%eax),%ecx # softirq_active
testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx # softirq_mask
#else
movl SYMBOL_NAME(irq_stat),%ecx # softirq_active
testl SYMBOL_NAME(irq_stat)+4,%ecx # softirq_mask
#endif
jne handle_softirq
注意其中的irq_stat, irq_stat +4 对应的就是字段 active和mask
既然我们每次调用完硬中断后都马上调用软中断,为什么还要在这里调用呢?
原因可能都多方面的:
(1)在系统调用或者异常处理中同样可以标注软中断,这样它们在返回前就能得以迅速执行
(2)前面提到,有些软中断要延迟到下次陷入内核才能执行,系统调用和异常都陷入内核,所以可以尽早的把软中断处理掉
(3)如果在异常或者系统调用中发生中断,那么前面提到,可能还会有一些软中断没有处理,在这两个地方做一个补救工作,尽量避免到下次陷入内核才处理这些软中断。
另外,在切换前也调用。
bottom half
2.2.x中的bottom half :
2.2.x版本中的bottom half就相当于2.4.1中的softirq.它的问题在于只有32个,如果要扩充的话,需要task 队列(这里task不是进程,而是函数),还有一个比较大的问题,就是虽然bottom half在一个CPU上是串行的(由local_bh_count[cpu]记数保证),但是在多CPU上是不安全的,例如,一个CPU上在运行关于定时器的bottom half,另一个CPU也可以运行同一个bottom half,出现了重入。
2.4.1中的bottom half
2.4.1中,用tasklet表示bottom half, mark_bh就是将相应的tasklet挂到运行队列里tasklet_hi_vec[cpu].list,这个队列由HI_SOFTIRQ对应的softirq来执行。
另外,用一个全局锁来保证,当一个CPU上运行一个bottom half时,其它CPU上不能运行任何一个bottom half。这和以前的bottom half有所不同,不知道是否我看错了。
用32个tasklet来表示bottom half:
struct tasklet_struct bh_task_vec[32];
首先,初始化所有的bottom half:
void __init softirq_init()
{
... ...
for (i=0; i<32; i++)
tasklet_init(bh_task_vec+i, bh_action, i);
... ...
}
这里bh_action是下面的函数,它使得bottom half运行对应的bh_base。
static void bh_action(unsigned long nr)
{
int cpu = smp_processor_id();
/*1*/ 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);
}
/*1*/试图上锁,如果得不到锁,则重新将bottom half挂上,下次在运行。
当要定义一个bottom half时用下面的函数:
void init_bh(int nr, void (*routine)(void))
{
bh_base[nr] = routine;
mb();
}
取消定义时,用:
void remove_bh(int nr)
{
tasklet_kill(bh_task_vec+nr);
bh_base[nr] = NULL;
}
tasklet_kill确保这个tasklet被运行了,因而它的指针也没有用了。
激活一个bottom half,就是将它挂到队列中 :
static inline void mark_bh(int nr)
{
tasklet_hi_schedule(bh_task_vec+nr);
}
中断流程
中断可以用下面的流程来表示:
中断产生源 --------> 中断向量表 (idt) -----------> 中断入口 ( 一般简单处理后调用相应的函数) ---------> 后续处理
根据中断产生源,我们可以把中断分成两个部分 :
内部中断( CPU 产生)
外部中断( 外部硬件产生 )
这些中断经过一些处理后,会有一些后续处理。
后面分别讨论:
内部中断
外部中断
后续处理
内部中断
内部中断有两种产生方式:
CPU 自发产生的: 如除数为0 的中断, page_fault 等
程序调用 int : int 80h
CPU自发产生的中断对应 idt 向量表中确定的位置,例如除数为0的中断在对应idt中第0个向量,
因此,内核只需要在第0个向量中设定相应的处理函数即可。
程序调用 int 可以产生的任何中断, 因此,前者是后者的子集。 特别的有:
int 80h
这是系统调用的中断.( system call )是用户代码调用内核代码的入口。
这里面可以考察的专题至少有:
*系统调用
*其它内部中断
外部中断
1.
外部中断是: 外部硬件(如时钟) -----> 中断芯片 ----> 中断向量表 -----> 中断入口
完成一个完整的映射,有4件事情要做:
(1) 将外部设备和中断芯片相应的管脚接上
(2) 对中断芯片设置,使得特定管脚的中断能映射到CPU idt特定的位置
(3) 程序中包含了这些中断入口
(4) 在中断向量表里设置向量相应的入口地址
这些工作需要在外部中断流程里描述
2.
由于硬件设备可能共用一个中断,在统一的函数中会有相应的结构来处理,也就是有16个结构分别处理相应的16个中断
特定的硬件驱动需要将自己的处理函数挂接到特定的结构上.
3.
但是,有一个问题:驱动怎么知道自己的硬件产生哪个中断?
有一些是确定的,比如时钟是第0个, 软盘是第 5 个(right ??), 还有一些 PCI 设备是可以通过访问得到它们的中断号的,但是ISA设备需要通过探测(probe)来得到(详细情况可以参考 linux device driver )这涉及探测的工作
4.
因此,这里面要考察的工作至少包括:
1. i8259芯片的设置(包括上面的 (2) ), 以及一些其它属性的设置
2. 外部中断的流程
3. 处理外部中断的结构与相应的数据结构
下面是《LINUX系统分析...》中的一段,可供参考。
但有时一个设备驱动程序不知道设备将使用哪一个中断。在PCI结构中这不会成为一个问题,因为PCI的设备驱动程序总是知道它们的中断号。但对于ISA结构而言,一个设备驱动程序找到自己使用的中断号却并不容易。Linux系统通过允许设备驱动程序探测自己的中断来解决这个问题。
首先,设备驱动程序使得设备产生一个中断。然后,允许系统中所有没有指定的中断,这意味着设备挂起的中断将会通过中断控制器传送。Linux 系统读取中断状态寄存器然后将它的值返回到设备驱动程序。一个非0 的结果意味着在探测期间发生了一个或者多个的中断。设备驱动程序现在可以关闭探测,这时所有还未被指定的中断将继续被禁止。
一个ISA 设备驱动程序知道了它的中断号以后,就可以请求对中断的控制了。PCI 结构的系统中断比I S A 结构的系统中断要灵活得多。ISA设备使用中断插脚经常使用跳线设置,所以在设备驱动程序中是固定的。但PCI 设备是在系统启动过程中PCI初始化时由PCI BIOS或PCI子系统分配的。每一个PCI 设备都有可能使用A、B、C或者D这4 个中断插脚中的一个。缺省情况下设备使用插脚A。
每个PCI插槽的PCI中断A、B、C和D是通过路由选择连接到中断控制器上的。所以PCI插槽4的插脚A可能连接到中断控制器的插脚6 ,PCI 插槽4 的插脚B 可能连接到中断控制器的插脚7 ,以此类推。
PCI中断具体如何进行路由一般依照系统的不同而不同,但系统中一定存在PCI中断路由拓扑结构的设置代码。在Intel PC机中,系统的BIOS代码负责中断的路由设置。对于没有BIOS的系统,Linux系统内核负责设置。
PCI的设置代码将中断控制器的插脚号写入到每个设备的PCI设置头中。PCI的设置代码根据所知道的PCI中断路由拓扑结构、PCI设备使用的插槽,以及正在使用的PCI中断的插脚号来决定中断号,也就是IRQ号。
系统中可以有很多的PCI中断源,例如当系统使用了PCI-PCI桥时。这时,中断源的数目可能超过了系统可编程中断控制器上插脚的数目。在这种情况下,某些PCI设备之间就不得不共享一个中断,也就是说,中断控制器上的某一个插脚可以接收来自几个设备的中断。Linux系统通过让第一个中断源请求者宣布它使用的中断是否可以被共享来实现中断在几个设备之间共享的。中断共享使得irq_action数组中的同一个入口指向几个设备的irqaction结构。当一个共享的中断有中断发生时,Linux系统将会调用和此中断有关的所有中断处理程序。所有可以共享中断的设备驱动程序的中断处理程序都可能在任何时候被调用,即使在自身没有中断需要处理时。
后续处理
后续部分主要完成下面的工作
1. bottom_half
2. 是否能进程切换?
3.是否需要进程切换?是则切换
4.信号处理
特别的,有一个重要的下半部分就是时钟中断的下半部分。
bottom_half
正如许多书所说的,它们继续完成中断处理(在开中断的状态下), 因此中断中的处理函数需要在一个32位变量中设置特定的bit来告诉do_softirq要执行哪个bottom_half(我们不妨把这个32位数想象成一个新的中断向量表,设置bit相当于产生中断,下半部分相当于handler,也许这是被称为软中断的原因吧)bottom_half有的时候需要借助一个特殊的结构: task_queue 来完成更多的工作,
task_queue
task_queue 是一个链表,每个节点是一个函数指针,这样,一 个 bottom_half 就可以执行一个链表上的函数列了
当然 task_queue 不一定只在 bottom_half 中应用, 我查了一下, 在一些驱动中也直接调用 run_task_queue 来执行一个特定的队列.
如果内核需要在某个中断产生后执行它的函数,只需要在它下半部分调用的 task_queue 上挂上它的函数( Linux Device Driver 中有步进马达的例子)现在的内核有些变化,增加了softirq_action tasklet, 不十分清楚是什么原因
是否需要进行切换
因为 linux是非抢占的,所以如果返回的代码段是内核级的话,就不允许进行切换。如果能切换判断一下是否需要切换, 如果是就切换
信号处理
看是否有信号要处理,如果要调用 do_signal
时钟中断的下半部分
在上面许多的外部中断中,有一个特殊的中断的处理 timer_interrupt, 它的下半部分主要处理:时间计算和校准定时器工作
因此,我们有了下面的工作
*下半部分(包括softirq, tasklet, bottom_half )
*后续处理的流程
*时钟中断的下半部分
*定时器
[声明]
#define DECLARE_TASK_QUEUE(q) LIST_HEAD(q)
#define LIST_HEAD(name) /
struct list_head name = LIST_HEAD_INIT(name)
struct list_head {
struct list_head *next, *prev;
};
#define LIST_HEAD_INIT(name) { &(name), &(name) }
''DECLARE_TASK_QUEUE'' [include/linux/tqueue.h, include/linux/list.h]
DECLARE_TASK_QUEUE(q) 宏用来申明一个名叫"q"的结构管理任务队列
[标明(MARK)]
这里是mark_bh过程的结构 [include/linux/interrupt.h]
|mark_bh(NUMBER)
|tasklet_hi_schedule(bh_task_vec + NUMBER)
|insert into tasklet_hi_vec
|__cpu_raise_softirq(HI_SOFTIRQ)
|soft_active |= (1 << HI_SOFTIRQ)
''mark_bh''[include/linux/interrupt.h]
例如,当IRQ handler需要推迟一些工作, 它会mark_bh(NUMBER), 这里的NUMBER是BH的标志
[执行]
通过do_IRQ[arch/i386/kernel/irq.c]
|do_softirq
|h->action(h)-> softirq_vec[TASKLET_SOFTIRQ]->action -> tasklet_action
|tasklet_vec[0].list->func
"h->action(h);" 是队列向前.
.
一. 软中断概况
软中断是利用硬件中断的概念,用软件方式进行模拟,实现宏观上的异步执行效果。很多情况下,软中断和"信号"有些类似,同时,软中断又是和硬中断相对应的,"硬中断是外部设备对CPU的中断","软中断通常是硬中断服务程序对内核的中断","信号则是由内核(或其他进程)对某个进程的中断"(《Linux内核源代码情景分析》第三章)。软中断的一种典型应用就是所谓的"下半部"(bottom half),它的得名来自于将硬件中断处理分离成"上半部"和"下半部"两个阶段的机制:上半部在屏蔽中断的上下文中运行,用于完成关键性的处理动作;而下半部则相对来说并不是非常紧急的,通常还是比较耗时的,因此由系统自行安排运行时机,不在中断服务上下文中执行。bottom half的应用也是激励内核发展出目前的软中断机制的原因,因此,我们先从bottom half的实现开始。
二. bottom half
在Linux内核中,bottom half通常用"bh"表示,最初用于在特权级较低的上下文中完成中断服务的非关键耗时动作,现在也用于一切可在低优先级的上下文中执行的异步动作。最早的bottom half实现是借用中断向量表的方式,在目前的2.4.x内核中仍然可以看到:
static void (*bh_base[32])(void); /* kernel/softirq.c */
系统如此定义了一个函数指针数组,共有32个函数指针,采用数组索引来访问,与此相对应的是一套函数:
void init_bh(int nr,void (*routine)(void));
为第nr个函数指针赋值为routine。
void remove_bh(int nr);
动作与init_bh()相反,卸下nr函数指针。
void mark_bh(int nr);
标志第nr个bottom half可执行了。
由于历史的原因,bh_base各个函数指针位置大多有了预定义的意义,在v2.4.2内核里有这样一个枚举:
enum {
TIMER_BH = 0,
TQUEUE_BH,
DIGI_BH,
SERIAL_BH,
RISCOM8_BH,
SPECIALIX_BH,
AURORA_BH,
ESP_BH,
SCSI_BH,
IMMEDIATE_BH,
CYCLADES_BH,
CM206_BH,
JS_BH,
MACSERIAL_BH,
ISICOM_BH
};
并约定某个驱动使用某个bottom half位置,比如串口中断就约定使用SERIAL_BH,现在我们用得多的主要是TIMER_BH、TQUEUE_BH和IMMEDIATE_BH,但语义已经很不一样了,因为整个bottom half的使用方式已经很不一样了,这三个函数仅仅是在接口上保持了向下兼容,在实现上一直都在随着内核的软中断机制在变。现在,在2.4.x内核里,它用的是tasklet机制。
三. task queue
在介绍tasklet之前,有必要先看看出现得更早一些的task queue机制。显而易见,原始的bottom half机制有几个很大的局限,最重要的一个就是个数限制在32个以内,随着系统硬件越来越多,软中断的应用范围越来越大,这个数目显然是不够用的,而且,每个bottom half上只能挂接一个函数,也是不够用的。因此,在2.0.x内核里,已经在用task queue(任务队列)的办法对其进行了扩充,这里使用的是2.4.2中的实现。
task queue是在系统队列数据结构的基础上建成的,以下即为task queue的数据结构,定义在include/linux/tqueue.h中:
struct tq_struct {
struct list_head list; /* 链表结构 */
unsigned long sync; /* 初识为0,入队时原子的置1,以避免重复入队 */
void (*routine)(void *); /* 激活时调用的函数 */
void *data; /* routine(data) */
};
typedef struct list_head task_queue;
在使用时,按照下列步骤进行:
DECLARE_TASK_QUEUE(my_tqueue); /* 定义一个my_tqueue,实际上就是一个以tq_struct为元素的list_head队列 */
说明并定义一个tq_struct变量my_task;
queue_task(&my_task,&my_tqueue); /* 将my_task注册到my_tqueue中 */
run_task_queue(&my_tqueue); /* 在适当的时候手工启动my_tqueue */
大多数情况下,都没有必要调用DECLARE_TASK_QUEUE()定义自己的task queue,因为系统已经预定义了三个task queue:
tq_timer,由时钟中断服务程序启动;
tq_immediate,在中断返回前以及schedule()函数中启动;
tq_disk,内存管理模块内部使用。
一般使用tq_immediate就可以完成大多数异步任务了。
run_task_queue(task_queue *list)函数可用于启动list中挂接的所有task,可以手动调用,也可以挂接在上面提到的bottom half向量表中启动。以run_task_queue()作为bh_base[nr]的函数指针,实际上就是扩充了每个bottom half的函数句柄数,而对于系统预定义的tq_timer和tq_immediate的确是分别挂接在TQUEUE_BH和IMMEDIATE_BH上(注意,TIMER_BH没有如此使用,但TQUEUE_BH也是在do_timer()中启动的),从而可以用于扩充bottom half的个数。此时,不需要手工调用run_task_queue()(这原本就不合适),而只需调用mark_bh(IMMEDIATE_BH),让bottom half机制在合适的时候调度它。
四. tasklet
由上看出,task queue以bottom half为基础;而bottom half在v2.4.x中则以新引入的tasklet为实现基础。
之所以引入tasklet,最主要的考虑是为了更好的支持SMP,提高SMP多个CPU的利用率:不同的tasklet可以同时运行于不同的CPU上。在它的源码注释中还说明了几点特性,归结为一点,就是:同一个tasklet只会在一个CPU上运行。
struct tasklet_struct
{
struct tasklet_struct *next; /* 队列指针 */
unsigned long state; /* tasklet的状态,按位操作,目前定义了两个位的含义:
TASKLET_STATE_SCHED(第0位)或TASKLET_STATE_RUN(第1位) */
atomic_t count; /* 引用计数,通常用1表示disabled */
void (*func)(unsigned long); /* 函数指针 */
unsigned long data; /* func(data) */
};
把上面的结构与tq_struct比较,可以看出,tasklet扩充了一点功能,主要是state属性,用于CPU间的同步。
tasklet的使用相当简单:
定义一个处理函数void my_tasklet_func(unsigned long);
DECLARE_TASKLET(my_tasklet,my_tasklet_func,data); /*
定义一个tasklet结构my_tasklet,与my_tasklet_func(data)函数相关联,相当于DECLARE_TASK_QUEUE() */
tasklet_schedule(&my_tasklet); /*
登记my_tasklet,允许系统在适当的时候进行调度运行,相当于queue_task(&my_task,&tq_immediate)和mark_bh(IMMEDIATE_BH) */
可见tasklet的使用比task queue更简单,而且,tasklet还能更好的支持SMP结构,因此,在新的2.4.x内核中,tasklet是建议的异步任务执行机制。除了以上提到的使用步骤外,tasklet机制还提供了另外一些调用接口:
DECLARE_TASKLET_DISABLED(name,function,data); /*
和DECLARE_TASKLET()类似,不过即使被调度到也不会马上运行,必须等到enable */
tasklet_enable(struct tasklet_struct *); /* tasklet使能 */
tasklet_disble(struct tasklet_struct *); /* 禁用tasklet,只要tasklet还没运行,则会推迟到它被enable */
tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long); /* 类似DECLARE_TASKLET() */
tasklet_kill(struct tasklet_struct *); /* 清除指定tasklet的可调度位,即不允许调度该tasklet,但不做tasklet本身的清除 */
前面提到过,在2.4.x内核中,bottom half是利用tasklet机制实现的,它表现在所有的bottom half动作都以一类tasklet的形式运行,这类tasklet与我们一般使用的tasklet不同。
在2.4.x中,系统定义了两个tasklet队列的向量表,每个向量对应一个CPU(向量表大小为系统能支持的CPU最大个数,SMP方式下目前2.4.2为32)组织成一个tasklet链表:
struct tasklet_head tasklet_vec[NR_CPUS] __cacheline_aligned;
struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned;
另外,对于32个bottom half,系统也定义了对应的32个tasklet结构:
struct tasklet_struct bh_task_vec[32];
在软中断子系统初始化时,这组tasklet的动作被初始化为bh_action(nr),而bh_action(nr)就会去调用bh_base[nr]的函数指针,从而与bottom half的语义挂钩。mark_bh(nr)被实现为调用tasklet_hi_schedule(bh_tasklet_vec+nr),在这个函数中,bh_tasklet_vec[nr]将被挂接在tasklet_hi_vec[cpu]链上(其中cpu为当前cpu编号,也就是说哪个cpu提出了bottom half的请求,则在哪个cpu上执行该请求),然后激发HI_SOFTIRQ软中断信号,从而在HI_SOFTIRQ的中断响应中启动运行。
tasklet_schedule(&my_tasklet)将把my_tasklet挂接到tasklet_vec[cpu]上,激发TASKLET_SOFTIRQ,在TASKLET_SOFTIRQ的中断响应中执行。HI_SOFTIRQ和TASKLET_SOFTIRQ是softirq子系统中的术语,下一节将对它做介绍。
五. softirq
从前面的讨论可以看出,task queue基于bottom half,bottom half基于tasklet,而tasklet则基于softirq。
可以这么说,softirq沿用的是最早的bottom half思想,但在这个"bottom half"机制之上,已经实现了一个更加庞大和复杂的软中断子系统。
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
static struct softirq_action softirq_vec[32] __cacheline_aligned;
这个softirq_vec[]仅比bh_base[]增加了action()函数的参数,在执行上,softirq比bottom half的限制更少。
和bottom half类似,系统也预定义了几个softirq_vec[]结构的用途,通过以下枚举表示:
enum
{
HI_SOFTIRQ=0,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
TASKLET_SOFTIRQ
};
HI_SOFTIRQ被用于实现bottom half,TASKLET_SOFTIRQ用于公共的tasklet使用,NET_TX_SOFTIRQ和NET_RX_SOFTIRQ用于网络子系统的报文收发。在软中断子系统初始化(softirq_init())时,调用了open_softirq()对HI_SOFTIRQ和TASKLET_SOFTIRQ做了初始化:
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
open_softirq()会填充softirq_vec[nr],将action和data设为传入的参数。TASKLET_SOFTIRQ填充为tasklet_action(NULL),HI_SOFTIRQ填充为tasklet_hi_action(NULL),在do_softirq()函数中,这两个函数会被调用,分别启动tasklet_vec[cpu]和tasklet_hi_vec[cpu]链上的tasklet运行。
static inline void __cpu_raise_softirq(int cpu, int nr)
这个函数用来激活软中断,实际上就是第cpu号CPU的第nr号软中断的active位置1。在do_softirq()中将判断这个active位。tasklet_schedule()和tasklet_hi_schedule()都会调用这个函数。
do_softirq()有4个执行时机,分别是:从系统调用中返回(arch/i386/kernel/entry.S::ENTRY(ret_from_sys_call))、从异常中返回(arch/i386/kernel/entry.S::ret_from_exception标号)、调度程序中(kernel/sched.c::schedule()),以及处理完硬件中断之后(kernel/irq.c::do_IRQ())。它将遍历所有的softirq_vec,依次启动其中的action()。需要注意的是,软中断服务程序,不允许在硬中断服务程序中执行,也不允许在软中断服务程序中嵌套执行,但允许多个软中断服务程序同时在多个CPU上并发。
六. 使用示例
softirq作为一种底层机制,很少由内核程序员直接使用,因此,这里的使用范例仅对其余几种软中断机制。
1.bottom half
原有的bottom half用法在drivers/char/serial.c中还能看到,包括三个步骤:
init_bh(SERIAL_BH,do_serial_bh); //在串口设备的初始化函数rs_init()中,do_serial_bh()是处理函数
mark_bh(SERIAL_BH); //在rs_sched_event()中,这个函数由中断处理例程调用
remove_bh(SERIAL_BH); //在串口设备的结束函数rs_fini()中调用
尽管逻辑上还是这么三步,但在do_serial_bh()函数中的动作却是启动一个task queue:run_task_queue(&tq_serial),而在rs_sched_event()中,mark_bh()之前调用的则是queue_task(...,&tq_serial),也就是说串口bottom half已经结合task queue使用了。而那些更通用一些的bottom half,比如IMMEDIATE_BH,更是必须要与task queue结合使用,而且一般情况下,task queue也很少独立使用,而是与bottom half结合,这在下一节task queue使用示例中可以清楚地看到。
2.task queue
一般来说,程序员很少自己定义task queue,而是结合bottom half,直接使用系统预定义的tq_immediate等,尤以tq_immediate使用最频繁。看以下代码段,节选自drivers/block/floppy.c:
static struct tq_struct floppy_tq; //定义一个tq_struct结构变量floppy_tq,不需要作其他初始化动作
static void schedule_bh( void (*handler)(void*) )
{
floppy_tq.routine = (void *)(void *) handler;
//指定floppy_tq的调用函数为handler,不需要考虑floppy_tq中的其他域
queue_task(&floppy_tq, &tq_immediate);
//将floppy_tq加入到tq_immediate中
mark_bh(IMMEDIATE_BH);
//激活IMMEDIATE_BH,由上所述可知,
这实际上将引发一个软中断来执行tq_immediate中挂接的各个函数
}
当然,我们还是可以定义并使用自己的task queue,而不用tq_immediate,在drivers/char/serial.c中提到的tq_serial就是串口驱动自己定义的:
static DECLARE_TASK_QUEUE(tq_serial);
此时就需要自行调用run_task_queue(&tq_serial)来启动其中的函数了,因此并不常用。
3.tasklet
这是比task queue和bottom half更加强大的一套软中断机制,使用上也相对简单,见下面代码段:
1: void foo_tasklet_action(unsigned long t);
2: unsigned long stop_tasklet;
3: DECLARE_TASKLET(foo_tasklet, foo_tasklet_action, 0);
4: void foo_tasklet_action(unsigned long t)
5: {
6: //do something
7:
8: //reschedule
9: if(!stop_tasklet)
10: tasklet_schedule(&foo_tasklet);
11: }
12: void foo_init(void)
13: {
14: stop_tasklet=0;
15: tasklet_schedule(&foo_tasklet);
16: }
17: void foo_clean(void)
18: {
19: stop_tasklet=1;
20: tasklet_kill(&foo_tasklet);
21: }
这个比较完整的代码段利用一个反复执行的tasklet来完成一定的工作,首先在第3行定义foo_tasklet,与相应的动作函数foo_tasklet_action相关联,并指定foo_tasklet_action()的参数为0。虽然此处以0为参数,但也同样可以指定有意义的其他参数值,但需要注意的是,这个参数值在定义的时候必须是有固定值的变量或常数(如上例),也就是说可以定义一个全局变量,将其地址作为参数传给foo_tasklet_action(),例如:
int flags;
DECLARE_TASKLET(foo_tasklet,foo_tasklet_action,&flags);
void foo_tasklet_action(unsigned long t)
{
int flags=*(int *)t;
...
}
这样就可以通过改变flags的值将信息带入tasklet中。直接在DECLARE_TASKLET处填写flags,gcc会报"initializer element is not constant"错。
第9、10行是一种RESCHEDULE的技术。我们知道,一个tasklet执行结束后,它就从执行队列里删除了,要想重新让它转入运行,必须重新调用tasklet_schedule(),调用的时机可以是某个事件发生的时候,也可以是像这样在tasklet动作中。而这种reschedule技术将导致tasklet永远运行,因此在子系统退出时,应该有办法停止tasklet。stop_tasklet变量和tasklet_kill()就是干这个的。