参考:程序员闪客的认认真真的聊聊中断
为什么要中断?
现代操作系统为了高效使用CPU资源,1.多程序并行执行,需要切换时间片给不同的程序,这个时候就需要时钟中断,告诉操作系统进行下一个程序使用CPU资源。2.还有如果进程需要io交互,此时交出cpu执行权,io完成后需要中断告诉cpu可以被调度使用了。等等
整个操作系统就是一个中断驱动的死循环,其他所有事情都是由操作系统提前注册的中断机制和其对应的中断处理函数完成。
while(true) {
doNothing();//不断循环判断是否需要处理的中断事件
}
硬中断的工作流程大致:
①中断源发出中断请求;
②cpu判断是否有中断,如有获取中断号,再去中断描述表中获取中断程序;
③CPU进行压栈操作,保护当前线程执行现场,方便中断执行完毕后恢复;
④执行中断服务程序
⑤恢复被保护的状态,执行“中断返回”指令回到被中断的程序或转入其他程序。
一、中断的分类
1.1 Intel 手册上的分类
中断可以分:
中断:中断是一个异步事件,通常由 IO 设备触发。比如点击一下鼠标、敲击一下键盘等。
异常:异常又可以分为故障、陷阱、中止。异常是一个同步事件,是 CPU 在执行指令时检
测到的反常条件。比如除法异常、错误指令异常,缺页异常等。
INT中断:直接给CPU一个中断号 n,比如触发了 Linux 的系统调用,实际上就是执行了INT 0x80 指令,那么 CPU 收到的就是一个 0x80 中断号。
1.2 触发机制
软件中断:INT n 这种方式,因为他是由软件程序主动触发的。如:系统调用int 80中断
硬件中断:中断和异常叫做硬件中断,因为他们都是硬件自动触发的。如:除0异常,缺页异常,鼠标事件,时钟中断
1.3实现机制
硬中断: Intel CPU 这个硬件实现的中断机制。
软中断:纯粹由软件实现的一种类似中断的机制,实际上它就是模仿硬件。在内存中有维护的软中断的标志位
的数组,然后由内核的一个线程不断轮询这些标志位,如果有哪个标志位有效,则去寻找对应的中断程序。
软中断是 Linux 实现中断的下半部的一种非常常见的方式。
二、硬中断处理流程(CPU硬件实现机制)
Intel 手册上的分类(中断和异常)和触发机制分类( 软件中断和硬件中断),殊途同归,都是让 CPU 收到一个
中断号,CPU 收到这个中断号之后,寻找对应的程序做处理。
2.1 拿得到中断号(Intel 手册上的分类)
2.1.1获取中断的中断号
电脑主板上有可编程中断控制器,它有很多的 IRQ 引脚线,接入了一堆能发出中断请求的硬件设备,当这些硬件设备给 IRQ 引脚线发一个信号时,由于可编程中断控制器提前被设置好了 IRQ 与中断号的对应关系,所以就转化成了对应的中断号,把这个中断号存储在自己的一个端口上,然后给 CPU 的 INTR 引脚发送一个信号,CPU 收到 INTR 引脚信号后去刚刚的那个端口读取到这个中断号的值。
最终的目标,就是让 CPU 知道,有中断了,并且也知道中断号是多少。
2.1.2获取异常中断号
CPU 自己执行指令时检测到的一些反常情况,然后自己给自己一个中断号即可,无需外界给。
比如:CPU 执行到了一个无效的指令,则自己给自己一个中断号 0x06,这个中断号是 Intel 的 CPU 提前就规定好写死了的硬布线逻辑。
2.1.3获取INT 指令中断号
INT 指令后面跟一个数字,就相当于直接用指令的形式,告诉 CPU 一个中断号。
比如 :INT 0x80,就是告诉 CPU 中断号是 0x80。Linux 内核提供的系统调用,就是用了 INT 0x80 这种指令。
2.1.4获取中断号总结
无论是中断还是异常,最终都是通过各种方式,让 CPU 得到一个中断号。只不过中断是通过外部设备给 CPU 的 INTR 引脚发信号,异常是 CPU 自己执行指令的时候发现特殊情况触发的,自己给自己一个中断号。
2.2得到中断号,执行中断程序
2.2.1 根据中断号,查找中断程序
2.2.1.1流程
CPU 收到一个中断号n后,会去中断向量表中寻找第n个中断描述符,从中断描述符中找到中断处理程序的地址,然后跳过去执行。
2.2.1.2中断描述符表
就是一个在内存中的数组而已,以 linux-2.6.0 源码为例。
struct desc_struct idt_table[256] = { {0, 0}, };
是一个大小为 256 的数组。idt_table 这个名字就是 Interrupt Descriptor Table,逐字翻译过来确实就是中断描述符表。
2.2.1.3中断描述符
中断描述符表这个数组里的存储的数据结构,通过刚刚的源码也可以看出来,是一个叫 desc_struct 的结构。
struct desc_struct {
unsigned long a,b;
};
Linux 源码里就这么简单粗暴表示,一个中断描述符的大小为 64 位。
断描述符分类:
Task Gate:任务门描述符,任务门描述符 Linux 中几乎没有用到。
Interrupt Gate:中断门描述符,不允许中断嵌套。
Trap Gate:陷阱门描述符,允许中断嵌套。
程序地址 = 线性地址(段选择子+段内偏移) --> 分页机制(mmu)–>程序的真实物理地址
2.2.1.4cpu如何拿到中断描述表
中断描述符表在哪里,全凭各个操作系统的喜好,想放在哪里就放在哪里。
CPU 提前预留了一个寄存器叫 IDTR 寄存器,这里面存放的就是中断描述符表的起始地址,以及中断描述符表的大小。
IDTR 寄存器里的值一共 48 位,前 16 位是中断描述符表大小(字节数),后 32 位是中断描述符表的起始内
存地址,就是这个 idt_table 的位置。
linux-2.6.0源码:
#1.初始化
struct desc_struct idt_table[256] = { {0, 0}, };
#2.结构
idt_descr:
.word 256 * 8 - 1 大小
.long idt_table 数组
#3.紧接着,一个 LIDT 指令把这个结构放到 IDTR 寄存器中
lidt idt_descr
2.2.1.5中断描述符表这个结构写在内存
操作系统初始化的时候调用trap_init方法初始化中断
Linux-2.6.0 内核源码的 traps.c 文件
void __init trap_init(void) {
set_trap_gate(0, ÷_error);
...
set_trap_gate(6, &invalid_op);
...
set_intr_gate(14, &page_fault);
...
set_system_gate(0x80, &system_call);
}
除法异常、非法指令异常、缺页异常,以及之后可能通过 INT 0x80 触发系统调用的中断处理函数 system_call,就是这样被写到了中断描述符表里。
2.2.2 中断程序执行
1.CPU 执行中断函数前做了压栈操作,2.中断执行 ,3.然后又提供了 iret 指令做弹栈操作,恢复调用程序代码处。
压栈操作
目的,就是保护现场(原来的程序地址、原来的程序堆栈、原来的标志位)和传递信息(错误码)
1. 如果发生了特权级转移,压入之前的堆栈段寄存器 SS 及栈顶指针 ESP 保存到栈中,并将堆栈切换为 TSS 中的堆栈。
2. 压入标志寄存器 EFLAGS。
3. 压入之前的代码段寄存器 CS 和指令寄存器 EIP,相当于压入返回地址。
4. 如果此中断有错误码的,压入错误码 ERROR_CODE
5. 跳转到中断程序
6. 恢复调用程序代码处。
具体的压栈工作,以及如何利用这些栈的信息达到结束中断并返回原程序的效果,Intel 手册中也写得很清楚。通过配合 IRET 或 IRETD 指令返回。
这个指令会依次弹出栈顶的三个元素,把它们分别赋值给 EIP,CS 和 EFLAGS,而栈顶的三个元素,又恰好是 EIP,CS 和 EFLAGS 这样的顺序
Linux-0.11 版源码
#除法异常的中断处理函数,在 asm.s
_divide_error:
push dword ptr _do_divide_error ;
no_error_code: ;
xchg [esp],eax ;
push ebx
push ecx
push edx
push edi
push esi
push ebp
push ds ;
push es
push fs
push 0 ;
lea edx,[esp+44] ;
push edx
mov edx,10h ;
mov ds,dx
mov es,dx
mov fs,dx
call eax ;
add esp,8 ;
pop fs
pop es
pop ds
pop ebp
pop esi
pop edi
pop edx
pop ecx
pop ebx
pop eax ;// 弹出原来eax 中的内容。
iretd//这个指令会依次弹出栈顶的三个元素,把它们分别赋值给 EIP,CS 和 EFLAGS,而栈顶的三个元素,又恰好是 EIP,CS 和 EFLAGS 这样的顺序
三、软中断处理流程(纯软件实现机制)
软中断的微观层面,简单说就是有一个单独的守护进程,不断轮询一组标志位,如果哪个标志位有值了,那去这个标志位对应的软中断向量表数组的相应位置,找到软中断处理函数,然后跳过去。
3.1 linux系统实现机制
1.操作系统启动初始化:
#各种初始化
asmlinkage void __init start_kernel(void) {
...
trap_init();
sched_init();
time_init();
...
rest_init();
}
2.rest_init() 这个方法—>开启内核软中断守护进程
static void rest_init(void) {
kernel_thread(init, NULL, CLONE_KERNEL);
}
static int init(void * unused) {
do_pre_smp_initcalls();
}
static void do_pre_smp_initcalls(void) {
spawn_ksoftirqd();//开启内核软中断守护进程
}
3.前面一些方法忽略,直接到最重要的方法do_softirq()
// 这就是软中断处理函数表(软中断向量表)
// 和硬中断的中断向量表一样
static struct softirq_action softirq_vec[32];
asmlinkage void do_softirq(void) {
// h = 软中断向量表起始地址指针
h = softirq_vec;
// 这个是软中断标志位们,一次性拿到所有的软中断标志位,32 位
pending = local_softirq_pending();
do {
// 此时的软中断标志位有值(说明有软中断)
if (pending & 1) {
// 去对应的软中断向量表执行对应的处理函数
h->action(h);
// 软中断向量表指针向后移动
h++;
// 同时软中断处理标志位也向后移动
pending >>= 1;
} while (pending);
}
不断遍历 pending 这个软中断标志位的每一位,如果是 0 就忽略,如果是 1,那从上面的 h 软中断向量表中找到对应的元素,然后执行 action 方法,action 就对应着不同的软中断处理函数。
内核软中断处理守护进程,在 Linux 启动后,会自动跑起来,那也就代表了,软中断机制生效了。
4.注册软中断向量表
就是给softirq_vec 这个软中断向量表,也是一个数组,里面的每一个元素的 action 附上值,赋的就是软中断处理函数的函数地址。
softirq_vec[0].action = NULL;
softirq_vec[1].action = run_timer_softirq;
softirq_vec[2].action = net_tx_action;
...
softirq_vec[31].action = xxx;
下面我们看看网络子系统初始化:
subsys_initcall(net_dev_init);
static int __init net_dev_init(void) {
...
// 网络发包的处理函数
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
// 网络收包的处理函数
open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
...
}
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
softirq_vec[nr].data = data;
// 简直完全一样
softirq_vec[nr].action = action;
}
3.2 软中断触发流程
1.软中断标志位p对应的标志位改成 1,就触发了软中断了。
pending = local_softirq_pending(); //32 位的 int 值
比如我们触发2号软中断
2.Linux网络接受包处理流程
static inline void __netif_rx_schedule(struct net_device *dev) {
list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list);
// 发出软中断
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
static inline void __netif_rx_schedule(struct net_device *dev) {
list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list);
// 发出软中断
local_softirq_pending() |= 1UL << (NET_RX_SOFTIRQ)
}
四、Linux实现中断的上下半场(硬中断和软中断配合)
这里给大家用网络收到包的流程说明一下,会涉及到上下场部分,上部分是靠硬中断实现的,下部分是靠软中断实现的。
4.1 从网线到网卡
数据包从别的设备发送过来,通过网线,我们设备收到这个包。
数据包过来是一堆电信号(0011001…),然后交给网卡处理硬件处理。
实质上就是把网线中的高低电平,转换到网卡上的一个缓冲区中存储着。
4.2 从网卡到内存
数据到达了网卡这个硬件的缓冲区中,现在要把它弄到内存中的缓冲区。
这个复制的过程不需要cpu,只需要DMA 这个硬件设备既可完成。
复制的过程前,需要提前网卡驱动需要在内存中申请一个缓冲区叫 sk_buffer,然后把这个 sk_buffer 的地址告诉网卡,这样 DMA 才知道等网卡的缓冲区有数据到来时,把它拷贝到内存的什么位置上。
4.2.1 注册硬中断处理程序
1.网卡内的缓冲区,然后通过 DMA 的方式,拷贝到了内存中的 sk_buffer 这个结构中。
2.由于这个过程完全是由硬件完成的,所以下一步网卡该做的最后一件事,就是通知内核,让内核去处理这个数据。
3.怎么通知呢?就是中断。网卡向 CPU 发起中断信号,CPU 打断当前的程序,根据中断号找到中断处理程序,开始执行。
e1000 这个网卡驱动说明:
e1000_main.c
request_irq(netdev->irq, &e1000_intr, ...);
// 注册的硬中断处理函数
static irqreturn_t e1000_intr(int irq, void *data, struct pt_regs *regs) {
__netif_rx_schedule(netdev);
}
include\linux\netdevice.h
static inline void __netif_rx_schedule(struct net_device *dev) {
list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list);
// 发出软中断
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
当数据包从网卡缓冲区到内存中的 sk_buffer 后发出中断,将会执行到 e1000_intr 这个中断处理函数。
1.硬中断:将网卡设备 dev 放入 poll_list 里。
2.然后立刻发起了一次软中断,然后就结束了。
硬中断设计尽可能的短,以便计算机可以尽快处理下一个硬中断,毕竟鼠标点击、键盘敲击等需要响应特别及
时。而像网络包到来后的拷贝和解析过程,在硬中断面前优先级没那么高,所以就触发一个软中断等着内核线
程去执行就好了。
4.2.2 注册软中断处理程序
硬中断触发了一个值为 NET_RX_SOFTIRQ 的软中断。
网络子系统初始化的过程中,把这个软中断对应的处理函数注册好了
net\core\dev.c
static int __init net_dev_init(void) {
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
}
// transmit 发送
static void net_tx_action(struct softirq_action *h) {...}
// receive 接收
static void net_rx_action(struct softirq_action *h) {...}
软中断 net_rx_action函数
static void net_rx_action(struct softirq_action *h) {
struct softnet_data *queue = &__get_cpu_var(softnet_data);
while (!list_empty(&queue->poll_list)) {
struct net_device dev = list_entry(
queue->poll_list.next, struct net_device, poll_list);
dev->poll(dev, &budget);
}
}
遍历 poll_list 取出一个个的设备 dev(之前),然后调用其 poll 函数。
当前有数据包到来的这个网卡设备 dev 放入了这个 poll_list,现在又取出来了。
网卡相应驱动的 poll 函数,那网卡初始化时,e1000 这款网卡的 poll 函数被附上了这个函数地址。
netdev->poll = &e1000_clean;
static int e1000_clean(struct net_device *netdev, int *budget) {
struct e1000_adapter *adapter = netdev->priv;
e1000_clean_tx_irq(adapter);
e1000_clean_rx_irq(adapter, &work_done, work_to_do);
}
// drivers\net\e1000\e1000_main.c
e1000_clean_rx_irq(struct e1000_adapter *adapter) {
...
netif_receive_skb(skb);
...
}
// net\core\dev.c
int netif_receive_skb(struct sk_buff *skb) {
...
list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type)&15], list) {
...
deliver_skb(skb, ptype, 0);
...
}
...
}
static __inline__ int deliver_skb(
struct sk_buff *skb, struct packet_type *pt_prev, int last) {
...
return pt_prev->func(skb, skb->dev, pt_prev);
}
一路跟来,执行了 pt_prev 的 func 函数。这个函数具体的实现指涉及到协议栈的注册。
协议栈的注册
IP 协议的注册
// net\ipv4\ip_output.c
static struct packet_type ip_packet_type = {
.type = __constant_htons(ETH_P_IP),
.func = ip_rcv,
};
void __init ip_init(void) {
dev_add_pack(&ip_packet_type);
}
// net\core\dev.c
void dev_add_pack(struct packet_type *pt) {
if (pt->type == htons(ETH_P_ALL)) {
list_add_rcu(&pt->list, &ptype_all);
} else {
hash = ntohs(pt->type) & 15;
list_add_rcu(&pt->list, &ptype_base[hash]);
}
}
func 被赋值为了 ip_rcv,那上一步自然就执行到了这个函数,其实就是网络层交给谁来负责解析的意思。
// net\ipv4\ip_input.c
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt) {
...
return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);
}
static inline int ip_rcv_finish(struct sk_buff *skb) {
...
if (skb->dst == NULL) {
if (ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, dev))
goto drop;
}
...
return dst_input(skb);
}
// include\net\dst.h
// rth->u.dst.input= ip_local_deliver;
static inline int dst_input(struct sk_buff *skb) {
...
skb->dst->input(skb);
...
}
// net\ipv4\ip_input.c
int ip_local_deliver(struct sk_buff *skb) {
...
return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,
ip_local_deliver_finish);
}
static inline int ip_local_deliver_finish(struct sk_buff *skb) {
...
ipprot = inet_protos[hash];
ipprot->handler(skb);
...
}
ip_rcv 这个函数处理完必然交给传输层继续处理。
module_init(inet_init);
static struct inet_protocol tcp_protocol = {
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
};
static struct inet_protocol udp_protocol = {
.handler = udp_rcv,
.err_handler = udp_err,
.no_policy = 1,
};
static int __init inet_init(void) {
inet_add_protocol(&udp_protocol, IPPROTO_UDP);
inet_add_protocol(&tcp_protocol, IPPROTO_TCP);
ip_init();
tcp_init();
}