免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 913 | 回复: 0
打印 上一主题 下一主题

interrupt-linux [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2008-02-24 13:37 |只看该作者 |倒序浏览




中断
目 录
   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_C
PU0也是一个死循环:
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]中,只有soft
irq_vec[0],和softirq_vec[3]分别填入了tasklet_action()和 tasklet_hi_actio
n()的地址。其余的保留它用。
    当发生软中断时,系统并不急于处理,只是将相应的cpu的中断状态结构中的a
ctive 的相应的位置位,并将相应的处理函数挂到相应的队列,然后等待调度时机来
临(如: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_l
ock 不能被锁上(已被其它进程锁上),则转而执行rescue,将bh_base[nr]挂至tas
klet_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_ac
tion()的作用不过是在发现bh_base[nr]暂时无法处理时重返队列的方法。
    由此可推测tasklet_vec队列的挂接应与此相似,查看interrupt.h,找到task
let_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()也将会处理N
ET_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_a
ction.
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都有一个对应的ta
sklet队列,
    它在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是非常宝贵的,所以我们建议只有当设备需要中断的时候才申请占用一个I
RQ,或者是在申请IRQ时采用共享中断的方式,这样可以让更多的设备使用中断。无
论对IRQ的使用方式是独占还是共享,申请IRQ的过程都是一样的,分为3步:
1.将所有的中断线探测一遍,看看哪些中断还没有被占用。从这些还没有被占用的
中断中选一个作为该设备的IRQ。
2.通过中断申请函数申请选定的IRQ,这是要指定申请的方式是独占还是共享。
3.根据中断申请函数的返回值决定怎么做:如果成功了万事大吉,如果没成功则或
者重新申请或者放弃申请并返回错误。
    申请IRQ的过程,在参考书的配的源代码里有详细的描述,读者可以通过仔细阅
读源代码中的short一例对中断号申请由深刻的理解。
四、中断处理程序
    Linux中的中断处理程序很有特色,它的一个中断处理程序分为两个部分:上半
部(top half)和下半部(bottom half)。之所以会有上半部和下半部之分,完全是
考虑到中断处理的效率。
    上半部的功能是“登记中断”。当一个中断发生时,他就把设备驱动程序中中
断例程的下半部挂到该设备的下半部执行队列中去,然后就没事情了--等待新的中
断的到来。这样一来,上半部执行的速度就会很快,他就可以接受更多她负责的设
备产生的中断了。上半部之所以要快,是因为它是完全屏蔽中断的,如果她不执行
完,其它的中断就不能被及时的处理,只能等到这个中断处理程序执行完毕以后。
所以,要尽可能多得对设备产生的中断进行服务和处理,中断处理程序就一定要快

    但是,有些中断事件的处理是比较复杂的,所以中断处理程序必须多花一点时
间才能够把事情做完。可怎么样化解在短时间内完成复杂处理的矛盾呢,这时候 L
inux引入了下半部的概念。下半部和上半部最大的不同是下半部是可中断的,而上
半部是不可中断的。下半部几乎做了中断处理程序所有的事情,因为上半部只是将
下半部排到了他们所负责的设备的中断处理队列中去,然后就什么都不管了。下半
部一般所负责的工作是察看设备以获得产生中断的事件信息,并根据这些信息(一
般通过读设备上的寄存器得来)进行相应的处理。如果有些时间下半部不知道怎么
去做,他就使用著名的鸵鸟算法来解决问题--说白了就是忽略这个事件。
    由于下半部是可中断的,所以在它运行期间,如果其它的设备产生了中断,这
个下半部可以暂时的中断掉,等到那个设备的上半部运行完了,再回头来运行它。
但是有一点一定要注意,那就是如果一个设备中断处理程序正在运行,无论她是运
行上半部还是运行下半部,只要中断处理程序还没有处理完毕,在这期间设备产生
的新的中断都将被忽略掉。因为中断处理程序是不可重入的,同一个中断处理程序
是不能并行的。
    在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。具体细节请参看本文
指定参考书。
八、实现
    如何实现驱动程序的中断例程,是各位读者的事情了。只要你们仔细的阅读sh
ort例程的源代码,搞清楚编写驱动程序中断例程的规则,就可以编写自己的中断例
程了。只要概念正确,
    在正确的规则下编写你的代码,那就是符合道理的东西。我始终强调,概念是
第一位的,能编多少代码是很其次的,我们一定要概念正确,才能进行正确的思考

九、小结
    本文介绍了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 expires;
        unsigned long idx = expires - timer_jiffies;
        if (idx > TVR_BITS) & TVN_MASK;
                insert_timer(timer, tv2.vec, i);
        } else if (idx > (TVR_BITS + TVN_BITS)) & TVN_MASK;
                insert_timer(timer, tv3.vec, i);
        } else if (idx > (TVR_BITS + 2 * TVN_BITS)) & TVN_MAS
K;
                insert_timer(timer, tv4.vec, i);
        } else if (expires > (TVR_BITS + 3 * TVN_BITS)) & TVN_MAS
K;
                insert_timer(timer, tv5.vec, i);
        } else {
                /* Can only get here on architectures with 64-bit jiffie
s */
                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_t
imer_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 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的服务函数,并把该ti
mer从链表中移走。在执行服务函数的过程中,允许中断。
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  不仅在进程运行时计时,在系统为进程运作而运行时它也计时,与I
TIMER_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为cu
rrent进程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_in
cr、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,并重新设置c
urrent进程的 it_real_value和it_real_incr为j和i。若j等于0,goto 4;若不等
于0,则把当前jiffies的值加上定时器剩余时间j,得到触发时间。若i小于j,则表
明I已经溢出,应该重新设为ULONG_MAX。最后把current进程的real_timer的expir
es设为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_in
terval置为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 itimerva
l *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发送的是0x
20
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 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_WAIT
ING,如果这个中断号上有中断产生,就把这个状态去掉,因此,我们就可以知道哪
些中断引脚上产生过中断了。
IRQ_PENDING , IRQ_INPROGRESS是为了确保:
同一个中断号的处理程序不能重入
不能丢失这个中断号的下一个处理程序
具体的说,当内核在运行某个中断号对应的处理程序(链)时,状态会设置成IRQ_INP
ROGRESS。如果在这期间,同一个中断号上又产生了中断,并且传给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, &regs, 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,可以激活不同的softir
q,因此,可以说,所有CPU有相同的例程,但是
    每个CPU却有自己完全独立的实例。
    对(1)的判断是通过考察irq_stat[ cpu ].mask相应的位得到的。这里面的cpu
指的是当前指令所在的cpu.在一开始,softirq被定义时,所有的cpu的掩码mask都
是一样的。但是在实际运行中,每个cpu上运行的程序可以根据自己的需要调整。
    对(2)的判断是通过考察irq_stat[ cpu ].active相应的位得到的.
    虽然原则上可以任意定义每个softirq的函数,Linux内核为了进一步加强延迟
中断功能,提出了tasklet的机制。tasklet实际上也就是一个函数。在第0个softi
rq的处理函数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; ifunc = func;
t->data = data;
t->state = 0;
atomic_set(&t->count, 0);
}
    然后调用schedule函数,注意,下面的函数仅仅是将这个tasklet挂到 TASKLE
T_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) |= (1next;
/*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在执行,如果有的话,就首先将这个t
asklet重新放到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 中断向量表 (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 ??), 还有一些 P
CI 设备是可以通过访问得到它们的中断号的,但是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是通过路由选择连接到中断控制器上的。所以PC
I插槽4的插脚A可能连接到中断控制器的插脚6 ,PCI 插槽4 的插脚B 可能连接到中
断控制器的插脚7 ,以此类推。
    PCI中断具体如何进行路由一般依照系统的不同而不同,但系统中一定存在PCI
中断路由拓扑结构的设置代码。在Intel PC机中,系统的BIOS代码负责中断的路由
设置。对于没有BIOS的系统,Linux系统内核负责设置。
PCI的设置代码将中断控制器的插脚号写入到每个设备的PCI设置头中。PCI的设置代
码根据所知道的PCI中断路由拓扑结构、PCI设备使用的插槽,以及正在使用的PCI中
断的插脚号来决定中断号,也就是IRQ号。
    系统中可以有很多的PCI中断源,例如当系统使用了PCI-PCI桥时。这时,中断
源的数目可能超过了系统可编程中断控制器上插脚的数目。在这种情况下,某些PC
I设备之间就不得不共享一个中断,也就是说,中断控制器上的某一个插脚可以接收
来自几个设备的中断。Linux系统通过让第一个中断源请求者宣布它使用的中断是否
可以被共享来实现中断在几个设备之间共享的。中断共享使得irq_action数组中的
同一个入口指向几个设备的irqaction结构。当一个共享的中断有中断发生时,Lin
ux系统将会调用和此中断有关的所有中断处理程序。所有可以共享中断的设备驱动
程序的中断处理程序都可能在任何时候被调用,即使在自身没有中断需要处理时。
[目录]
后续处理
后续处理
    后续部分主要完成下面的工作
1. bottom_half
2. 是否能进程切换?
3.是否需要进程切换?是则切换
4.信号处理
    特别的,有一个重要的下半部分就是时钟中断的下半部分。
bottom_half
    正如许多书所说的,它们继续完成中断处理(在开中断的状态下), 因此中断中
的处理函数需要在一个32位变量中设置特定的bit来告诉do_softirq要执行哪个bot
tom_half(我们不妨把这个32位数想象成一个新的中断向量表,设置bit相当于产生
中断,下半部分相当于handler,也许这是被称为软中断的原因吧)bottom_half有的
时候需要借助一个特殊的结构: task_queue 来完成更多的工作,
task_queue
    task_queue 是一个链表,每个节点是一个函数指针,这样,一 个 bottom_ha
lf 就可以执行一个链表上的函数列了
当然 task_queue 不一定只在 bottom_half 中应用, 我查了一下, 在一些驱动中
也直接调用 run_task_queue 来执行一个特定的队列.
    如果内核需要在某个中断产生后执行它的函数,只需要在它下半部分调用的 t
ask_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 action(h)-> softirq_vec[TASKLET_SOFTIRQ]->action -> tasklet_actio
n
      |tasklet_vec[0].list->func
"h->action(h);" 是队列向前.
.
[目录]
2. 4软中断机制
一. 软中断概况
    软中断是利用硬件中断的概念,用软件方式进行模拟,实现宏观上的异步执行
效果。很多情况下,软中断和"信号"有些类似,同时,软中断又是和硬中断相对应
的,"硬中断是外部设备对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_tas
k_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表示d
isabled */
        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_t
ask,&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),unsigne
d 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)组织成一个tas
klet链表:
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和TAS
KLET_SOFTIRQ是 softirq子系统中的术语,下一节将对它做介绍。
五. softirq
    从前面的讨论可以看出,task queue基于bottom half,bottom half基于task
let,而tasklet则基于softirq。
    可以这么说,softirq沿用的是最早的bottom half思想,但在这个"bottom ha
lf"机制之上,已经实现了一个更加庞大和复杂的软中断子系统。
struct softirq_action
{
        void    (*action)(struct softirq_action *);
        void    *data;
};
static struct softirq_action softirq_vec[32] __cacheline_aligned;
    这个softirq_vec[]仅比bh_base[]增加了action()函数的参数,在执行上,so
ftirq比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_S
OFTIRQ做了初始化:
void open_softirq(int nr, void (*action)(struct softirq_action*), void *
data)
    open_softirq()会填充softirq_vec[nr],将action和data设为传入的参数。T
ASKLET_SOFTIRQ填充为 tasklet_action(NULL),HI_SOFTIRQ填充为tasklet_hi_ac
tion(NULL),在do_softirq()函数中,这两个函数会被调用,分别启动tasklet_ve
c[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/kerne
l/entry.S::ENTRY (ret_from_sys_call))、从异常中返回(arch/i386/kernel/e
ntry.S::ret_from_exception 标号)、调度程序中(kernel/sched.c::schedule(
)),以及处理完硬件中断之后(kernel/irq.c::do_IRQ ())。它将遍历所有的so
ftirq_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()函数中的动作却是启动一个ta
sk queue:run_task_queue(&tq_serial),而在rs_sched_event()中,mark_bh()之
前调用的则是queue_task(...,&tq_serial),也就是说串口bottom half已经结合t
ask 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结构变量flo
ppy_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相关联,并指定f
oo_tasklet_action()的参数为0。虽然此处以0为参数,但也同样可以指定有意义的
其他参数值,但需要注意的是,这个参数值在定义的时候必须是有固定值的变量或
常数(如上例),也就是说可以定义一个全局变量,将其地址作为参数传给 foo_t
asklet_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_TASKLE
T处填写flags,gcc会报"initializer element is not constant"错。
    第9、10行是一种RESCHEDULE的技术。我们知道,一个tasklet执行结束后,它
就从执行队列里删除了,要想重新让它转入运行,必须重新调用 tasklet_schedul
e(),调用的时机可以是某个事件发生的时候,也可以是像这样在tasklet动作中。
而这种reschedule技术将导致tasklet永远运行,因此在子系统退出时,应该有办法
停止tasklet。stop_tasklet变量和tasklet_kill()就是干这个的。





本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u2/61808/showart_482878.html
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP