Linux 书籍中常说的 BottomHalf 已然不见了,它们被转成 tasklets,这是支持 SMP 的。但其思想基本 一致。
我们在此不会对中断及异常的原理和机制做深入的介绍。但必须要作出一些说明,因为这是理解 Linux 内核与其它嵌入式/实时操作系统的不同,以及理解网络协议栈收报文的基础。 Linux 支持 CPU 的外部硬件中断和内部中断。严格来说,内部中断包含系统调用陷入和异常,在一 般的嵌入式操作系统(比如 VxWorks)中是没有系统调用这个概念的,所以对于一直从事嵌入式软件开 发的人初次进入到大型操作系统(比如 Linux 和 Windows)开发环境中,会面临内核空间与用户空间概 念上的困惑。其实说到底,所谓系统调用就是软件有计划地调用 CPU 提供的特殊指令,触发 CPU 内部 产生一个中断,于是完成一次核内核外运行空间的切换,具体可以参考许多书籍。而所谓异常就是软件 无意的执行了一个非法指令(比如除 0)从而造成 CPU 内部引发一次中断。
外部中断特指外部设备发出的中断信号。但这几种中断的 CPU 处理过程基本相同,即:在执行完当 前指令后,或在执行当前指令期间,根据中断源所提供的“中断向量”,在内存中找到相应的 ISR(中断 服务例程)然后调用之。
不管是内部还是外部中断,系统都会根据接收到的中断信息,查询 idt 表。idt 表依照中断源的位置按序组成,并对应中断服务程序(以及异常处理程序)的入口地址。Linux 系统在初始化页式虚存管理的 初始化以后,便调用 trap_init 和 init_IRQ 两个函数进行中断机制的初始化。我们只介绍init_IRQ
。
中断向量?中断请求号?这是个问题。 现在分析试图给大家解释一下,看看是不是这样的:IRQ 是设备相关的号码,一般生产厂商都会使 自己的设备分配到一个合适的号码。而中断向量就纯粹是操作系统中关于如何处理中断的内存组织结构, 它们之间存在某种映射关系,这种关系是由 CPU 体系结构以及操作系统决定的。那么在 IA32 体系的 Linux 中,是一种直接映射的关系,所有的 IRQ 号产生的中断全部映射到从 INT_vec[32]
开始的内存中。为什么 要从第 32 个单元开始呢?
上图中彩色部分都是系统能处理的中断,intel CPU 保留使用的中断向量是 0~32,根本不可能有哪 一种设备会使用这个区域的中断向量,这一部分就是我们常说的异常处理函数,还有一个比较特殊的中 断向量号 0x80
(即 128)就是系统调用号,由于不可能由外部设备引发这类中断,它们就被统称为内部 中断,这就是为什么要从第 32 个单元开始的原因。内核用调用 trap_init
函数挂接与之相应的中断处理函 数。接着系统调用init_IRQ
函数来初始化外部中断向量,其中断处理函数的挂接由各驱动程序自己完成。 由上图可以看出中断向量和中断请求号是相关但却不是一个东西:前者是内核中存在的一块内存,专门 存放中断处理函数的地址(指逻辑上,具体实现比较复杂) ,而后者就是一个概念,在内核中不必存在这 么一种变量,它可能就是中断向量的下标。
在 2.6 的内核中,中断相关的宏已经变化了,2.4 内核中的中断概念请看《Linux 内核源代码情景分 析 》。 在 2.6 内核的 entry.S
文件中,有一 个 interrupt 的定义, 它 放在.data
节中,然 后 ,在 include/asm-i386/hw_irq.h 中引用这个变量,最后在 arch/i386/kernel/i8259.c 中初始化这个变量。
下面两个代码片断是 2.4 内核关于这个 interrupt 变量的初始化:
1 #define IRQ(x,y) \
2 IRQ##x##y##_interrupt
3
4 #define IRQLIST_16(x) \
5 IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \
6 IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \
7 IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \
8 IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)
9
10 void (*interrupt[NR_IRQS])(void) = {
11 IRQLIST_16(0x0),
12 ...
经过编译器的预处理,interrupt 这个函数指针数组变成:
1 void (*interrupt[NR_IRQS])(void) = {
2 IRQ0x00_interrupt,
3 IRQ0x01_interrupt,
4 IRQ0x02_interrupt,
5 ……
6 IRQ0x0f_interrupt,
7 }
从代码中看出,这样的初始化不太灵活,扩展性比较差。下面给出 2.6 内核关于 interrupt 的使用方式。 首先在 entry.S 中汇编代码如下:
在 hw_irq.h
中有这样的定义:extern void (*interrupt[NR_IRQS])(void);
在此,NR_IRQS
是 224
。具体 的初始化如下:
1 void __init init_IRQ(void)
2 {
3 int i;
4
5 /* all the set up before the call gates are initialised */
6 pre_intr_init_hook();
7
8 /*
9 * 扫描整个中断向量表
10 */
11 for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) {
12 int vector = FIRST_EXTERNAL_VECTOR + i;
13 if (i >= NR_IRQS)
14 break; //如果是系统调用中断号,就初始化这个中断号
15 if (vector != SYSCALL_VECTOR)
16 set_intr_gate(vector, interrupt[i]);
17 }
18 ......
19 /*
20 * Set the clock to HZ Hz, we already have a valid vector now:
21 */
22 setup_pit_timer ();
23
24 ......
25
26 irq_ctx_init(smp_processor_id());
27 }
下面是关于中断上下文的一些宏,说明中断处理到达一种什么样的程度:
在 include/linux/irq.h 文件中,有关于中断控制器的描述,中断控制器描述符. 它包含了所有低层硬件 的信息。
其中一个实例是 i8259A_irq_type
,它定义在 arch/i386/kernel/i8259.c 中:
下面这个数组的定义利用了 GCC 编译器的特点,只定义了一个单元的值,然后使用[0 … NR_IRQS-1] 使整个数组的值都初始化为同样的值:
现在让我们从整体上看中断和软中断的处理过程,do_IRQ
是直接被调用的:
每个外部中断都会调用 do_IRQ
,此函数根据当时的 EAX 寄存器(i386 体系)值来判断当前属于哪 个 IRQ 去调用__do_IRQ
。
__do_IRQ
把 action 传入了 handle_IRQ_event
,然后在其中执行action->handler
,此 handle
就是每个设备驱动程 序挂接的 ISR
asmlinkage int handle_IRQ_event(unsigned int irq,
struct pt_regs *regs, struct irqaction *action)
{
int status = 1; /* Force the "do bottom halves" bit */
int ret, retval = 0;
//如果没有设置SA_INTERRUPT.将CPU 中断打开
//应该尽量的避免CPU关中断的情况,因为CPU屏弊本地中断,会使
//中断丢失
if (!(action->flags & SA_INTERRUPT))
local_irq_enable();
//遍历运行中断处理程序
do {
ret = action->handler(irq, action->dev_id, regs);
if (ret == IRQ_HANDLED)
status |= action->flags;
retval |= ret;
action = action->next;
} while (action);
if (status & SA_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
//关中断
local_irq_disable();
return retval;
}
上面介绍的是硬件过来的中断,而传说中的软件中断(不是软中断)是怎么工作的呢?其实很简单, 下面这副图是否能满足您的求知欲呢?那些曲线箭头表示代码的执行路径。要注意的是在 socket( )函数的 实现过程中,那些 mov 指令就是告诉内核要跳转的系统调用函数以及用户待传入内核的参数地址。不同 的 CPU 结构上会使用不同的寄存器,这里就不详细说明了。
所以,软件中断的处理方式和硬件的处理路径是完全不一样的,它不必经过do_IRQ
这个函数,而是 直接跳转到内核中的代码执行 sys_socketcall
。在 2.6.18 的内核中,BSD 网络接口的方式已经变成了使用 sys_socketcall
来解复用不同的系统调用,这样做的好处是减少系统调用表的大小,可以集中管理网络方 面的 API:
asmlinkage long sys_socketcall(int call, unsigned long __user *args)
{
unsigned long a[6];
unsigned long a0, a1;
int err;
if (call < 1 || call > SYS_ACCEPT4)
return -EINVAL;
/* 调用copy_from_user函数,从用户空间的内存地址拷贝参数到内核空间 */
if (copy_from_user(a, args, nargs[call]))
return -EFAULT;
err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
if (err)
return err;
a0 = a[0];
a1 = a[1];
switch (call) {
case SYS_SOCKET:
err = sys_socket(a0, a1, a[2]);
break;
case SYS_BIND:
err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_CONNECT:
err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_LISTEN:
err = sys_listen(a0, a1);
break;
case SYS_ACCEPT:
err = sys_accept4(a0, (struct sockaddr __user *)a1,
(int __user *)a[2], 0);
break;
case SYS_GETSOCKNAME:
err =
sys_getsockname(a0, (struct sockaddr __user *)a1,
(int __user *)a[2]);
break;
case SYS_GETPEERNAME:
err =
sys_getpeername(a0, (struct sockaddr __user *)a1,
(int __user *)a[2]);
break;
case SYS_SOCKETPAIR:
err = sys_socketpair(a0, a1, a[2], (int __user *)a[3]);
break;
case SYS_SEND:
err = sys_send(a0, (void __user *)a1, a[2], a[3]);
break;
case SYS_SENDTO:
err = sys_sendto(a0, (void __user *)a1, a[2], a[3],
(struct sockaddr __user *)a[4], a[5]);
break;
case SYS_RECV:
err = sys_recv(a0, (void __user *)a1, a[2], a[3]);
break;
case SYS_RECVFROM:
err = sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
(struct sockaddr __user *)a[4],
(int __user *)a[5]);
break;
case SYS_SHUTDOWN:
err = sys_shutdown(a0, a1);
break;
case SYS_SETSOCKOPT:
err = sys_setsockopt(a0, a1, a[2], (char __user *)a[3], a[4]);
break;
case SYS_GETSOCKOPT:
err =
sys_getsockopt(a0, a1, a[2], (char __user *)a[3],
(int __user *)a[4]);
break;
case SYS_SENDMSG:
err = sys_sendmsg(a0, (struct msghdr __user *)a1, a[2]);
break;
case SYS_RECVMSG:
err = sys_recvmsg(a0, (struct msghdr __user *)a1, a[2]);
break;
case SYS_ACCEPT4:
err = sys_accept4(a0, (struct sockaddr __user *)a1,
(int __user *)a[2], a[3]);
break;
default:
err = -EINVAL;
break;
}
return err;
}
目前为止,我们我们讨论的都是真正的中断,那么什么是软中断呢?请读者回顾在中断即将退出的 时候会调用irq_exit
,它内部会判断是否还有中断要处理,如果已经没有了就调用 invoke_softirq
,这是一 个宏,它被定义成 do_softirq
,此函数最终调用__do_softirq
,这也就是说,实际上软中断是在处理完所有 中断之后才会处理的。而且处理软中断的时候还是处于中断上下文中。不过有一些限制,详见下面对 __do_softirq
代码的分析。
在目前 Linux 内核中定义了 6 种软中断,而且告诫我们不要轻易的再定义新的软中断,原话如下:
PLEASE, avoid to allocate new softirqs, if you need not _really_ high frequency threaded job scheduling. For almost all the purposes tasklets are more than enough. F.e. all serial device BHs et al. should be converted to tasklets, not to softirqs
软中断向量 0(即HI_SOFTIRQ
)用于实现高优先级的软中断,软中断向量 3(即 TASKLET_SOFTIRQ
) 则用于实现诸如 tasklet 这样的一般性软中断。
Tasklet 机制是一种较为特殊的软中断。Tasklet 一词的原意是“小片任务”的意思,这里是指一小段可 执行的代码,且通常以函数的形式出现。软中断向量 HI_SOFTIRQ
和TASKLET_SOFTIRQ
均是用 tasklet 机制来实现的。 从某种程度上讲,tasklet 机制是 Linux 内核对 BH 机制的一种扩展。在 2.4 内核引入了 softirq 机制后, 原有的 BH 机制正是通过 tasklet 机制这个桥梁来纳入 softirq 机制的整体框架中的。正是由于这种历史的 延伸关系,使得 tasklet 机制与一般意义上的软中断有所不同,而呈现出以下两个显著的特点:
1. 与一般的软中断不同,某一段 tasklet 代码在某个时刻只能在一个 CPU 上运行,而不像一般的软 中断服务函数(即softirq_action
结构中的 action 函数指针)那样??在同一时刻可以被多个 CPU 并发 地执行。
2. 与 BH 机制不同,不同的 tasklet 代码在同一时刻可以在多个 CPU 上并发地执行,而不像 BH 机制 那样必须严格地串行化执行(也即在同一时刻系统中只能有一个 CPU 执行 BH 函数)。 Bottom Half 机制在新的 softirq 机制中被保留下来,并作为 softirq 框架的一部分。其实现也似乎更为 复杂些,因为它是通过 tasklet 机制这个中介桥梁来纳入 softirq 框架中的。实际上,软中断向量 HI_SOFTIRQ 是内核专用于执行 BH 函数的。
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{ //nr 即软中断向量编号
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
}
我们会发现__do_softirq
内部最多只处理 10 个软中断,如果系统内部的软中断事件太多,那么就会 通知 ksoftirqd 内核线程处理软中断。这样,就不会占用太多的中断上下文执行时间。
图解:
asmlinkage void __do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;
pending = local_softirq_pending();
account_system_vtime(current);
/*软中断处理中,禁止软中断再次进入,软中断处理是不可重入的*/
__local_bh_disable((unsigned long)__builtin_return_address(0));
trace_softirq_enter();
cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs
下面首先清除pending,以便系统可以激活其它软件中断,
然后使能外部中断
系统在下面的处理中,将使能外部中断以提高系统的响应,
注意,必须先清除pending,再使能外部中断,否则死锁*/
set_softirq_pending(0);
local_irq_enable();
h = softirq_vec;
/*下面按照从高到低调用open_softirq注册的句柄*/
do {
if (pending & 1) {
h->action(h); //关键的一句,tasklet、内核timer、网络中断都是在这里搞的
rcu_bh_qsctr_inc(cpu);
}
h++;
pending >>= 1;
} while (pending);
local_irq_disable();
pending = local_softirq_pending();
if (pending && --max_restart)
goto restart;
if (pending)
wakeup_softirqd();
trace_softirq_exit();
account_system_vtime(current);
_local_bh_enable();
}
设备驱动程序要处理硬件中断,必须挂接 ISR,则挂接一个 ISR 可以用这个函数
要注意的是你在挂接 ISR 之前要正确的初始化你的设备,并且要保证用正确的顺序挂接中断。里面 调用 setup_irq
就是把创建的irqaction{}
挂接到对应中断的链表上,以至于handle_IRQ_event
能根据 irq 号直接找到对应handler
。