Linux 内核|中断

本文最后更新于:1 个月前

1.中断

中断是系统用来响应硬件设备请求的一种机制,操作系统收到硬件的中断请求,会打断正在执行的进程,然后调用内核中的中断处理程序来响应请求。

举例:敲击键盘时,键盘控制器会发送一个中断,通知操作系统有键按下。

  • 中断处理请求应该短且快。

    响应中断请求的程序,也就是中断处理程序,要尽可能快的执⾏完,这样可以减少对正常进程运⾏调度地影响。

  • 否则会造成 临时关闭中断。

    当前中断处理程序未执行完,系统内其他设备的中断请求无法被响应,即其他中断会丢失。

    举例:当一个电话打进来,如果不迅速沟通完,其他人打电话给你,你就无法接到。

    类比到系统中,例如网卡有数据包进来,系统需要迅速将数据包先拷到内存中,避免网卡因为缓存不够,发生丢包。

2.同步中断和异步中断

改变处理器执行指令的顺序的一个事件都是中断,分为同步中断和异步中断。

  • 同步中断(synchronous):即异常,一条指令终止后,CPU 才发生中断,所以是同步。异常通常可分为错误(Faults)、陷阱(Traps)和中止(Aborts)。

  • 异步中断(asynchronous):即通常所说的中断(interrupt),由其他硬件设备依照CPU时钟信号随机产生。

3.硬中断与软中断

中断实际上有两种:

  • 外部或硬件引起的中断,硬中断。
  • 软件引起的中断,软中断。

硬件引起的中断,由 Local APIC 或与 Local APIC 连接的处理器针脚接收。

软件引起的中断,由处理器自身的特殊情况引起(有时使用特殊架构的指令)。一个常见特殊情况的例子就是 除数为零,另一个例子就是使用 系统调用(system call)。

4.为何需要软中断

主要是为解决 中断处理程序执行时间过长 和 中断丢失 的问题,将中断分为两个阶段,分别是 上半部分(硬中断完成)下半部分(软中断完成)

  • 上半部,由硬件触发中断,用来快速处理中断。

    一般会暂时关闭中断请求,主要负责处理跟硬件紧密相关或者时间敏感的事情。硬中断处理程序完成耗时短的工作,快速执行。

  • 下半部,由内核触发中断,用来异步处理上半部未完成的工作。

    一般以「内核线程」的方式运行。软中断处理程序完成耗时长的工作,延迟执行。

硬中断会打断 CPU 正在执行的任务,然后立即执行中断处理程序;而软中断以内核线程的方式执行,每个 CPU 对应一个软中断内核线程,名称通常为 「ksoftirqd/CPU编号」,如 0 号 CPU 对应的软中断内核线程名称是 「ksoftirqd/0」。

软中断除了包含硬件设备中断处理程序的下半部分,一些内核自定义事件也是软中断,比如:内核调度、RCU锁(内核中常用的一种锁)。

5.查看软中断

  • Linux 中,在 /proc/softirqs 中可查看软中断的运行情况;

  • /porc/interrupts 中可查看硬中断的运行情况。

porc:一个指向内核数据结构的接口,通过它能够查看和改变各种系统属性。

  • 第一列表示软中断的类型, 比如 NET_RX 表示网络接收中断,NET_TX 表示网络发送中断,TIMER 表示
    定时中断、 RCU 表示 RCU 锁中断、SCHED 表示内核调度中断。
  • 数值显示的是每个 CPU 对应的不同类型的软中断的 累计运行次数。同一种类型的软中断在不同 CPU 的累计次数相差不多。
  • 累计次数无需关注,中断次数的变化速率 是重点。watch -d cat /proc/softirqs 查看中断次数的变化速率。

内核线程的名字外面都有有中括号,这说明 ps 无法获取它们的命令行参数。一般来说,名字在中括号里,都可以认为是内核线程。

6.软中断CPU使用率过高

top 查看 CPU 使用情况,按 1 即可显示 CPU 核心。倒数第 2 列,si 表示 CPU 在软中断上的使用率。

如何判断是由哪个类型软中断导致的 CPU 使用率过高?

使用 watch -d cat /proc/softirqs 查看每个软中断类型的中断次数的变化速率。

  1. 对于网络 I/O 比较高的 Web 服务器,NET_RX 网络接收中断的变化速率相比其他中断类型快很多。

  2. NET_RX 网络接收中断次数的变化速率过快,需要使用 sar -n DEV 查看网卡的网络包接收速率情况,然后分析是哪个网卡有大量的网络包进来。

  3. 再通过 tcpdump 抓包,分析这些包的来源,如果是非法的地址,可以考虑加防火墙,如果是正常流量,则要考虑硬件升级等。

7.中断的发生流程

假设每一个物理硬件都有一根中断线,设备可通过它对 CPU 发起中断信号,中断信号先通过中断控制器,然后发到 CPU 上执行。中断控制器:Advanced Programmable Interrupt Controller,简称 APIC。一个 APIC 包括两个独立的设备:

  • Local APIC
  • I/O APIC

Local APIC 存在于每个 CPU 核心中,Local APIC 负责处理特定于 CPU 的中断配置,常被用于管理来自 APIC 时钟(APIC-timer)、热敏元件和其他与 I/O 设备连接的设备的中断。

I/O APIC 提供了多核处理器的中断管理。它被用来在所有的 CPU 核心中分发外部中断。

中断的发生流程:

  1. 外部设备给中断控制器发送物理中断信号。

  2. 中断控制器将物理中断信号转换成为中断向量 interrupt vector,发给各个 CPU。

  3. 每个 CPU 都会有一个中断向量表,根据 interrupt vector 调用一个 IRQ 处理函数。

  4. IRQ 处理函数中,将 interrupt vector 转化为抽象中断层的中断信号 irq,调用中断信号 irq 对应的中断描述结构(IDT)里面的 irq_handler_t

    IDT 表 :中断描述符表,CPU执行中断指令时,会去 IDT 表中查找对应的中断服务程序(interrupt service routine ,ISR)。

8.中断结构体

对于每一个中断,我们都有一个对应的描述结构体irq_desc,有一个重要的成员变量是 struct action *action`,用于表示处理这个中断的动作。

1
2
3
4
5
6
7
8
9
10
11
12
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;
......
struct irqaction *action; /* IRQ action list */
......
int parent_irq;
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;

每一个中断处理动作的结构 struct irqaction,都有以下成员:

  • 中断处理函数 handler
  • 设备 id void *dev_id
  • 中断信号irq
  • 如果中断处理函数在单独的线程运行,则有 thread_fn 是线程的执行函数,thread 是线程的 task_struct

多个 irqaction 组成一个链表,对于这个中断的所有处理动作,都串在这个链表上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* struct irqaction - per interrupt action descriptor
* @handler: interrupt handler function
* @name: name of the device
* @dev_id: cookie to identify the device
* @percpu_dev_id: cookie to identify the device
* @next: pointer to the next irqaction for shared interrupts
* @irq: interrupt number
* @flags: flags (see IRQF_* above)
* @thread_fn: interrupt handler function for threaded interrupts
* @thread: thread pointer for threaded interrupts
* @secondary: pointer to secondary irqaction (force threading)
* @thread_flags: flags related to @thread
* @thread_mask: bitmask for keeping track of @thread activity
* @dir: pointer to the proc/irq/NN/name entry
*/
struct irqaction {
irq_handler_t handler;
void *dev_id;
void __percpu *percpu_dev_id;
struct irqaction *next;
irq_handler_t thread_fn;
struct task_struct *thread;
struct irqaction *secondary;
unsigned int irq;
unsigned int flags;
unsigned long thread_flags;
unsigned long thread_mask;
const char *name;
struct proc_dir_entry *dir;
} ____cacheline_internodealigned_in_smp;

众多的中断 irq_desc 则采取类似于内存管理中所用到的基数树 radix tree 的方式进行管理。这种结构对于从某个整型 key 找到 value 速度很快,中断信号 irq 是这个整数。通过它,我们很快就能定位到对应的 irq_desc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifdef CONFIG_SPARSE_IRQ
static RADIX_TREE(irq_desc_tree, GFP_KERNEL);
struct irq_desc *irq_to_desc(unsigned int irq)
{
return radix_tree_lookup(&irq_desc_tree, irq);
}
#else /* !CONFIG_SPARSE_IRQ */
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[0 ... NR_IRQS-1] = {
}
};
struct irq_desc *irq_to_desc(unsigned int irq)
{
return (irq < NR_IRQS) ? irq_desc + irq : NULL;
}
#endif /* !CONFIG_SPARSE_IRQ */

本博客所有文章均个人原创,除特别声明外均采用 CC BY-SA 4.0协议,转载请注明出处!