锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

深入理解Linux中断机制

时间:2022-10-14 07:30:00 连接器poke

目录

  • 一是中断基本原理
    • 1.1 中断的定义
    • 1.2 中断的作用
    • 1.3 中断的产生
    • 1.4 中断的处理
    • 1.5 中断向量号
    • 1.6 框架结构中断
  • 二、中断过程
    • 2.1 保存现场
    • 2.2 查找向量表
  • 三、软件中断
    • 3.1 CPU异常
    • 3.2 指令中断
  • 四、硬件中断
    • 4.1 外设中断
    • 4.2 中断处理器之间
  • 五、中断处理
    • 5.1 异常处理
    • 5.2 硬中断(hardirq)
    • 5.3 软中断(softirq)
    • 5.4 微任务(tasklet)
    • 5.5 中断线程(threaded_irq)
    • 5.6 工作队列(workqueue)
  • 六、中断与同步
    • 6.1 CPU运行模型
    • 6.2 中断相关同步方法
  • 七、总结回顾


一是中断基本原理

中断是计算机中一个非常重要的功能,其重要性不亚于人类的神经系统和脉搏。虽然图灵机和冯诺依曼的结构没有中断,但如果计算机真的没有中断,那么计算机就相当于一半的残疾人。今天我们将谈谈中断。

1.1 中断的定义

让我们来看看中断的定义:

中断机制:CPU在执行指令时,收到某个中断信号转而去执行预先设定好的代码,然后再返回到原指令流中继续执行,这就是中断机制。

可以发现,中断的定义非常简单。我们根据中断的定义画一张图片:
图灵机运行模型
在图灵机模型中,计算机一直线性运行。中断后,计算机可以在流程执行流中透明地插入代码。那么这样做的目的是什么呢?

1.2 中断的作用

设计中断机制的目的是帮助操作系统实现自己的功能。这四个功能是:

1.外设异步通知CPU: 外设发生了什么,或者完成了什么任务或者有什么消息要告诉我CPU,可异步给予CPU发出通知。例如,网卡收到网络包,磁盘完成IO任务,定时器间隔到了,可以给CPU发送中断信号。

2.CPU之间发消息: 在SMP在系统中,一个CPU想给另一个CPU可以发送消息IPI(处理器间中断)。

3.处理CPU异常: CPU在执行指令的过程中,异常会发送中断信号来处理异常。例如,当进行整数除法操作时,发现被除数为0,当访问虚拟内存时,虚拟内存没有映射到物理内存上。

4.实现系统调用: 早期的系统调用就是靠中断指令来实现的,后期虽然开发了专用的系统调用指令,但是其基本原理还是相似的。关于系统调用的详细原理,请参看《深入理解Linux系统调用与API》。

1.3 中断的产生

那么如何产生中断信号呢?产生中断信号有以下四个来源:

1.外设,外设产生的中断信号是异步的,通常称为硬件中断(注意硬件中断是另一个概念)。硬件中断根据是否可以屏蔽分为可屏蔽中断和不可屏蔽中断。例如,网卡、磁盘生硬件中断。

2.CPU,这里指的是一个CPU向另一个CPU发送中断称为发送中断IPI(处理器间中断)。IPI也可以看出,这是一种特殊的硬件中断,因为它类似于硬件中断模式,是异步的。

3.CPU异常,CPU在执行指令时发现异常会向自己发送中断信号,这是同步的,通常称为软件中断(注意软中断是另一个概念)。CPU异常按照是否需要修复以及是否能修复分为3类:1.陷阱(trap),不需要修复,中断后继续执行下一个指令。2.故障(fault),也有可能修复需要修复。中断处理后,重新执行以前的指令。3.暂停(abort),需要修复但不能修复,中断后,过程或核心会崩溃。例如,缺页异常是一种故障,因此也称为缺页故障。缺页异常处理完成后,刚才的指令将重新执行。

4.直接中断指令CPU该指令产生中断信号CPU同步异常也可称为软件中断。例如,中断指令int 0x80可用于实现系统调用。

中断信号的四个来源对应于中断的四个功能。前两种中断可称为硬件中断,均为异步;后两种中断可称为软件中断,均为同步。在许多书中,硬件中断也被称为中断,软件中断被称为异常。

1.4 中断的处理

那么如何处理中断信号呢?也许你会认为这不是很简单。前图不是画得很清楚吗?中断信号是将中断执行流插入正常执行流中。虽然这种中断处理简单直接,但还是有问题的。

执行场景(execute context)

在我们继续解释之前,我们首先介绍念,执行场景(execute context)。这个概念在中断之前就没有了,中断之后,CPU分为两个执行场景,过程执行场景(process context)中断执行场景(interrupt context)。那么哪些是过程执行场景,哪些是中断执行场景呢?进程的执行是进程执行场景,同步中断的处理也是进程执行场景,异步中断的处理是中断执行场景。有些人可能对同步中断的处理是过程执行场景感到困惑,但这也很容易理解,因为同步中断与当前指令有关,可以看作是过程执行的一部分。异步中断的处理与当前指令无关,因此不是过程执行场景。

流程执行场景和中断执行场景有两个区别:一是流程执行场景可以调度和休眠,中断执行场景不能调度或休眠;第二,流程执行场景可以接受中断信号,中断执行场景可以屏蔽中断信号。因此,如果中断执行场景的执行时间过长,会影响我们对新中断信号的响应,因此需要尽量缩短中断执行场景的时间。因此,处理异步中断有两种方法:

1.立即完全处理:

可立即完全处理简单易处理的异步中断。

2.立即预处理 以后完全处理:

处理耗时的中断可以通过立即预处理和以后完全处理来处理。

为了方便表达,我们称立即完全处理和立即预处理为中断预处理,以后完全处理为中断后处理。中断预处理只有一种方法,即直接处理。然而,中断后处理的方法有很多,可以在中断执行场景或过程执行场景中运行。前者称为直接中断后处理,后者称为线程中断后处理。

在Linux中断预处理称为上半部分,中断后处理称为下半部分。由于上半部和下半部的含义不明确,我们在本文中称之为中断预处理和中断后处理。中断预处理只有一种方法叫做hardirq(硬中断)。中断后处理的方法有很多,分为直接中断后处理两类softirq(软中断),tasklet(微任务)workqueue(工作队列),threaded_irq(中断线程)。

硬中断和软中断是什么意思?最初的异步中断处理是直接完成中断,整个过程是屏蔽中断,现在,整个过程分为两部分,前半部分或屏蔽中断,称为硬件中断,处理与硬件相关的紧急情况,后半部分不再屏蔽中断,称为软中断,处理剩余的事情。由于软中断不再屏蔽中断信号,因此提高了系统对中断的响应性。

硬件中断、软件中断、硬件中断、软件中断是不同的概念,分别指中断的来源和处理方法。

1.5 中断向量号

不同的中断信号需要不同的处理方法,那么系统如何区分不同的中断信号呢?它依赖于中断向量号。每个中断信号都有一个中断向量号,中断向量号是一个整数。CPU接收到中断信号时,根据信号中断的向量号查询中断向量表,并根据向量表中的指示调用相应的处理函数。

如何对应中断信号和中断向量号?CPU在异常情况下,其向量号是由CPU架构标准规定。对于外设,其向量号由设备驱动动态申请。IPI对于中断和指令中断,其向量号由核心规定。

那么,中断向量表的格式是什么,应该如何设置,我们稍后再谈。

1.6 框架结构中断

有了这么多以前的基础知识,我们来对中机制做个概览。

中断信号的产生有两类,分别是异步中断和同步中断,异步中断包括外设中断和IPI中断,同步中断包括CPU异常和指令中断。无论是同步中断还是异步中断,都要经过中断向量表进行处理。对于同步中断的处理是异常处理或者系统调用,它们都是进程执行场景,所以没有过多的处理方法,就是直接执行。对于异步中断的处理,由于直接调用处理是属于中断执行场景,默认的中断执行场景是会屏蔽中断的,这会降低系统对中断的响应性,所以内核开发出了很多的方法来解决这个问题。

下面的章节是对这个图的详细解释,我们先讲中断向量表,再讲中断的产生,最后讲中断的处理。

本文后面都是以x86 CPU架构进行讲解的。

二、中断流程

CPU收到中断信号后会首先保存被中断程序的状态,然后再去执行中断处理程序,最后再返回到原程序中被中断的点去执行。具体是怎么做呢?我们以x86为例讲解一下。

2.1 保存现场

CPU收到中断信号后会首先把一些数据push到内核栈上,保存的数据是和当前执行点相关的,这样中断完成后就可以返回到原执行点。如果CPU当前处于用户态,则会先切换到内核态,把用户栈切换为内核栈再去保存数据(内核栈的位置是在当前线程的TSS中获取的)。下面我们画个图看一下:

CPU都push了哪些数据呢?分为两种情况。当CPU处于内核态时,会push寄存器EFLAGS、CS、EIP的值到栈上,对于有些CPU异常还会push Error Code。Push CS、EIP是为了中断完成后返回到原执行点,push EFLAGS是为了恢复之前的CPU状态。当CPU处于用户态时,会先切换到内核态,把栈切换到内核栈,然后push寄存器SS(old)、ESP(old)、EFLAGS、CS、EIP的值到新的内核栈,对于有些CPU异常还会push Error Code。Push SS(old)、ESP(old),是为了中断返回的时候可以切换回原来的栈。有些CPU异常会push Error Code,这样可以方便中断处理程序知道更具体的异常信息。不是所有的CPU异常都会push Error Code,具体哪些会哪些不会在3.1节中会讲。

上图是32位的情况,64位的时候会push 64位下的寄存器。

2.2 查找向量表

保存完被中断程序的信息之后,就要去执行中断处理程序了。CPU会根据当前中断信号的向量号去查询中断向量表找到中断处理程序。CPU是如何获得当前中断信号的向量号的呢,如果是CPU异常可以在CPU内部获取,如果是指令中断,在指令中就有向量号,如果是硬件中断,则可以从中断控制器中获取中断向量号。那CPU又是怎么找到中断向量表呢,是通过IDTR寄存器。IDTR寄存器的格式如下图所示:

IDTR寄存器由两部分组成:一部分是IDT基地址,在32位上是32位,在64位上是64位,是虚拟内存上的地址;一部分是IDT限长,是16位,单位是字节,代表中断向量表的长度。虽然x86支持256个中断向量,但是系统不一定要用满256个,IDT限长用来指定中断向量表的大小。系统在启动时分配一定大小的内存用来做中断向量表,然后通过LIDT指令设置IDTR寄存器的值,这样CPU就知道中断向量表的位置和大小了。

IDTR寄存器设置好之后,中断向量表的内容还是可以再修改的。该如何修改呢,这就需要我们知道中断向量表的数据结构了。中断向量表是一个数组结构,数组的每一项叫做中断向量表条目,每个条目都是一个门描述符(gate descriptor)。门描述符一共有三种类型,不同类型的具体结构不同,三类门描述符分别是任务门描述符、中断门描述符、陷阱门描述符。任务门不太常用,后面我们都默认忽略任务门。中断门一般用于硬件中断,陷阱门一般用于软件中断。32位下的门描述符是8字节,下面是它们的具体结构:

Segment Selector是段选择符,Offset是段偏移,两个段偏移共同构成一个32的段偏移。p代表段是否加载到了内存。dpl是段描述符特权级。d为0代表是16位描述符,d为1代表是32位描述符。Type 是8 9 10三位,代表描述符的类型。

下面看一下64位门描述符的格式:

可以看到64位和32位最主要的变化是把段偏移变成了64位。

关于x86的分段机制,这里就不展开讨论了,简介地介绍一下其在Linux内核中的应用。Linux内核并不使用x86的分段机制,但是x86上特权级的切换还是需要用到分段。所以Linux采取的方法是,定义了四个段__KERNEL_CS、__KERNEL_DS、__USER_CS、__USER_DS,这四个段的段基址都是0,段限长都是整个内存大小,所以在逻辑上相当于不分段。但是这四个段的特权级不一样,__KERNEL_CS、__KERNEL_DS是内核特权级,用在内核执行时,__USER_CS、__USER_DS是用户特权级,用在进程执行时。由于中断都运行在内核,所以所有中断的门描述符的段选择符都是__KERNEL_CS,而段偏移实际上就是终端处理函数的虚拟地址。

CPU现在已经把被中断的程序现场保存到内核栈上了,又得到了中断向量号,然后就根据中断向量号从中断向量表中找到对应的门描述符,对描述符做一番安全检查之后,CPU就开始执行中断处理函数(就是门描述符中的段偏移)。中断处理函数的最末尾执行IRET指令,这个指令会根据前面保存在栈上的数据跳回到原来的指令继续执行。

三、软件中断

对中断的基本概念和整个处理流程有了大概的认识之后,我们来看一下软件中断的产生。软件中断有两类,CPU异常和指令中断。我们先来看CPU异常:

3.1 CPU异常

CPU在执行指令的过程中遇到了异常就会给自己发送中断信号。注意异常不一定是错误,只要是异于平常就都是异常。有些异常不但不是错误,它还是实现内核重要功能的方法。CPU异常分为3类:1.陷阱(trap),陷阱并不是错误,而是想要陷入内核来执行一些操作,中断处理完成后继续执行之前的下一条指令,2.故障(fault),故障是程序遇到了问题需要修复,问题不一定是错误,如果问题能够修复,那么中断处理完成后会重新执行之前的指令,如果问题无法修复那就是错误,当前进程将会被杀死。3.中止(abort),系统遇到了很严重的错误,无法修改,一般系统会崩溃。

CPU异常的含义和其向量号都是架构标准提前定义好的,下面我们来看一下。

x86一共有256个中断向量号,前32个(0-31)是Intel预留的,其中0-21(除了15)都已分配给特定的CPU异常。32-255是给硬件中断和指令中断保留的向量号。

3.2 指令中断

指令中断和CPU异常有很大的相似性,都属于同步中断,都是属于因为执行指令而产生了中断。不同的是CPU异常不是在执行特定的指令时发生的,也不是必然发生。而指令中断是执行特定的指令而发生的中断,设计这些指令的目的就是为了产生中断的,而且一定会产生中断或者有些条件成立的情况下一定会产生中断。其中指令INT n可以产生任意中断,n可以取任意值。Linux用int 0x80来作为系统调用的指令。关于系统调用的详细情况,请参看《深入理解Linux系统调用与API》。

四、硬件中断

硬件中断分为外设中断和处理器间中断(IPI),下面我们先来看一下外设中断。

4.1 外设中断

外设中断和软件中断有一个很大的不同,软件中断是CPU自己给自己发送中断,而外设中断是需要外设发送中断给CPU。外设想要给CPU发送中断,那就必须要连接到CPU,不可能隔空发送。那么怎么连接呢,如果所有外设都直接连到CPU,显然是不可能的。因为一个计算机系统中的外设是非常多的,而且多种多样,CPU无法提前为所有外设设计和预留接口。所以需要一个中间设备,就像秘书一样替CPU连接到所有的外设并接收中断信号,再转发给CPU,这个设备就叫做中断控制器(Interrupt Controller )。

在x86上,在UP时代的时候,有一个中断控制器叫做PIC(Programmable Interrupt Controller )。所有的外设都连接到PIC上,PIC再连接到CPU的中断引脚上。外设给PIC发中断,PIC再把中断转发给CPU。由于PIC的设计问题,一个PIC只能连接8个外设,所以后来把两个PIC级联起来,第二个PIC连接到第一个PIC的一个引脚上,这样一共能连接15个外设。

到了SMP时代的时候,PIC显然不能胜任工作了,于是Intel开发了APIC(Advanced PIC)。APIC分为两个部分:一部分是Local APIC,有NR_CPU个,每个CPU都连接一个Local APIC;一部分是IO APIC,只有一个,所有的外设都连接到这个IO APIC上。IO APIC连接到所有的Local APIC上,当外设向IO APIC发送中断时,IO APIC会把中断信号转发给某个Local APIC。有些per CPU的设备是直接连接到Local APIC的,可以通过Local APIC直接给自己的CPU发送中断。

外设中断并不是直接分配中断向量号,而是直接分配IRQ号,然后IRQ+32就是其中断向量号。有些外设的IRQ是内核预先设定好的,有些是行业默认的IRQ号。

关于APIC的细节这里就不再阐述了,推荐大家去看《Interrupt in Linux (硬件篇)》,对APIC讲的比较详细。

4.2 处理器间中断

在SMP系统中,多个CPU之间有时候也需要发送消息,于是就产生了处理器间中断(IPI)。IPI既像软件中断又像硬件中断,它的产生像软件中断,是在程序中用代码发送的,而它的处理像硬件中断,是异步的。我们这里把IPI看作是硬件中断,因为一个CPU可以把另外一个CPU看做外设,就相当于是外设发来的中断。

五、中断处理

终于讲到中断处理了,我们再把之前的中间机制图搬过来,再回顾一下:

无论是硬件中断还是软件中断,都是通过中断向量表进行处理的。但是不同的是,软件中断的处理程序是属于进程执行场景,所以直接把中断处理程序设置好就行了,中断处理程序怎么写也没有什么要顾虑的。而硬件中断的处理程序就不同了,它是属于中断执行场景。不仅其中断处理函数中不能调用会阻塞、休眠的函数,而且处理程序本身要尽量的短,越短越好。所以为了使硬件中断处理函数尽可能的短,Linux内核开发了一大堆方法。这些方法包括硬中断(hardirq)、软中断(softirq)、微任务(tasklet)、中断线程(threaded irq)、工作队列(workqueue)。其实硬中断严格来说不算是一种方法,因为它是中断处理的必经之路,它就是中断向量表里面设置的处理函数。为了和软中断进行区分,才把硬中断叫做硬中断。硬中断和软中断都是属于中断执行场景,而中断线程和工作队列是属于进程执行场景。把硬件中断的处理任务放到进程场景里面来做,大大提高了中断处理的灵活性。

由于软件中断的处理都是直接处理,都是内核本身直接写好了的,一般都接触不到,而硬件中断的处理和硬件驱动密切相关,所以很多书上所讲的中断处理都是指的硬件中断的处理。但是我们今天把软件中断的处理也讲一讲,这里只讲异常处理,系统调用部分请参看《深入理解Linux系统调用与API》。

5.1 异常处理

x86上的异常处理是怎么设置的呢?我们把前面的图搬过来看一下:

我们对照着这个图去捋代码。首先我们需要分配一片内存来存放中断向量表,这个是在如下代码中分配的。
linux-src/arch/x86/kernel/idt.c

/* Must be page-aligned because the real IDT is used in the cpu entry area */
static gate_desc idt_table[IDT_ENTRIES] __page_aligned_bss;

linux-src/arch/x86/include/asm/desc_defs.h

struct idt_bits { 
        
	u16 ist	: 3,
      zero	: 5,
      type	: 5,
      dpl	: 2,
        p	: 1;
} __attribute__((packed));

struct gate_struct { 
        
	u16		offset_low;
	u16		segment;
	struct idt_bits	bits;
	u16		offset_middle;
#ifdef CONFIG_X86_64
	u32		offset_high;
	u32		reserved;
#endif
} __attribute__((packed));

typedef struct gate_struct gate_desc;

linux-src/arch/x86/include/asm/segment.h

#define IDT_ENTRIES 256

可以看到我们的中断向量表idt_table是门描述符gate_desc的数组,数组大小是IDT_ENTRIES 256。门描述符gate_desc的定义和前面画的图是一致的,注意x86是小端序。

寄存器IDTR内容包括IDT的基址和限长,为此我们专门定义一个数据结构包含IDT的基址和限长,然后就可以用这个变量通过LIDT指令来设置IDTR寄存器了。
linux-src/arch/x86/kernel/idt.c

static struct desc_ptr idt_descr __ro_after_init = { 
        
	.size		= IDT_TABLE_SIZE - 1,
	.address	= (unsigned long) idt_table,
};

linux-src/arch/x86/include/asm/desc.h

#define load_idt(dtr) native_load_idt(dtr)
static __always_inline void native_load_idt(const struct desc_ptr *dtr)
{ 
        
	asm volatile("lidt %0"::"m" (*dtr));
}

有一点需要注意的,我们并不是需要把idt_table完全初始化好了再去load_idt,我们可以先初始化一部分的idt_table,然后再去load_idt,之后可以不停地去完善idt_table。

我们先来看一下内核是什么时候load_idt的,其实内核有多次load_idt,不过实际上只需要一次就够了。

调用栈如下:
start_kernel
  setup_arch
    idt_setup_early_traps

代码如下:
linux-src/arch/x86/kernel/idt.c

void __init idt_setup_early_traps(void)
{ 
        
	idt_setup_from_table(idt_table, early_idts, ARRAY_SIZE(early_idts), true);
	load_idt(&idt_descr);
}

这是内核在start_kernel里第一次设置IDTR,虽然之前的代码里也有设置过IDTR,我们就不考虑了。load_idt之后,IDT就生效了,只不过这里IDT还没有设置全,只设置了少数几个CPU异常的处理函数,我们来看一下是怎么设置的。

linux-src/arch/x86/kernel/idt.c

static __init void
idt_setup_from_table(gate_desc *idt, const struct idt_data *t, int size, bool sys)
{ 
        
	gate_desc desc;

	for (; size > 0; t++, size--) { 
        
		idt_init_desc(&desc, t);
		write_idt_entry(idt, t->vector, &desc);
		if (sys)
			set_bit(t->vector, system_vectors);
	}
}

static inline void idt_init_desc(gate_desc *gate, const struct idt_data *d)
{ 
        
	unsigned long addr = (unsigned long) d->addr;

	gate->offset_low	= (u16) addr;
	gate->segment		= (u16) d->segment;
	gate->bits		= d->bits;
	gate->offset_middle	= (u16) (addr >> 16);
#ifdef CONFIG_X86_64
	gate->offset_high	= (u32) (addr >> 32);
	gate->reserved		= 0;
#endif
}

#define write_idt_entry(dt, entry, g) native_write_idt_entry(dt, entry, g)
static inline void native_write_idt_entry(gate_desc *idt, int entry, const gate_desc *gate)
{ 
        
	memcpy(&idt[entry], gate, sizeof(*gate));
}

在函数idt_setup_from_table里会定义一个gate_desc的临时变量,然后用idt_data来初始化这个gate_desc,最后会把gate_desc复制到idt_table中对应的位置中去。这样中断向量表中的这一项就生效了。

下面我们再来看看idt_data数据是怎么来的:
linux-src/arch/x86/kernel/idt.c

static const __initconst struct idt_data early_idts[] = { 
        
	INTG(X86_TRAP_DB,		asm_exc_debug),
	SYSG(X86_TRAP_BP,		asm_exc_int3),
};

#define G(_vector, _addr, _ist, _type, _dpl, _segment) \ { 
           \ .vector = _vector, \ .bits.ist = _ist, \ .bits.type = _type, \ .bits.dpl = _dpl, \ .bits.p = 1, \ .addr = _addr, \ .segment = _segment, \ }

/* Interrupt gate */
#define INTG(_vector, _addr) \ G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL0, __KERNEL_CS)

/* System interrupt gate */
#define SYSG(_vector, _addr) \ G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)

linux-src/arch/x86/kernel/traps.c

DEFINE_IDTENTRY_DEBUG(exc_debug)
{ 
        
	exc_debug_kernel(regs, debug_read_clear_dr6());
}

EFINE_IDTENTRY_RAW(exc_int3)
{ 
        
	/* * poke_int3_handler() is completely self contained code; it does (and * must) *NOT* call out to anything, lest it hits upon yet another * INT3. */
	if (poke_int3_handler(regs))
		return;

	/* * irqentry_enter_from_user_mode() uses static_branch_{,un}likely() * and therefore can trigger INT3, hence poke_int3_handler() must * be done before. If the entry came from kernel mode, then use * nmi_enter() because the INT3 could have been hit in any context * including NMI. */
	if (user_mode(regs)) { 
        
		irqentry_enter_from_user_mode(regs);
		instrumentation_begin();
		do_int3_user(regs);
		instrumentation_end();
		irqentry_exit_to_user_mode(regs);
	} else { 
        
		irqentry_state_t irq_state = irqentry_nmi_enter(regs);

		instrumentation_begin();
		if (!do_int3(regs))
			die("int3", regs, 0);
		instrumentation_end();
		irqentry_nmi_exit(regs, irq_state);
	}
}

early_idts是idt_data的数组,在这里定义了两个中断向量表的条目,分别是X86_TRAP_DB和X86_TRAP_BP,它们的中断处理函数分别是asm_exc_debug和asm_exc_int3。这里只是设置了两个中断向量表条目,并且把IDTR寄存器设置好了,后来就不需要再设置IDTR寄存器了。

下面我们看一下所有CPU异常的处理函数是怎么设置的。

先看调用栈:
start_kernel
  trap_init
    idt_setup_traps

代码如下:
linux-src/arch/x86/kernel/idt.c

void __init idt_setup_traps(void)
{ 
        
	idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);
}

static const __initconst struct idt_data def_idts[] = { 
        
	INTG(X86_TRAP_DE,		asm_exc_divide_error),
	ISTG(X86_TRAP_NMI,		asm_exc_nmi, IST_INDEX_NMI),
	INTG(X86_TRAP_BR,		asm_exc_bounds),
	INTG(X86_TRAP_UD,		asm_exc_invalid_op),
	INTG(X86_TRAP_NM,		asm_exc_device_not_available),
	INTG(X86_TRAP_OLD_MF,		asm_exc_coproc_segment_overrun),
	INTG(X86_TRAP_TS,		asm_exc_invalid_tss),
	INTG(X86_TRAP_NP,		asm_exc_segment_not_present),
	INTG(X86_TRAP_SS,		asm_exc_stack_segment),
	INTG(X86_TRAP_GP,		asm_exc_general_protection),
	INTG(X86_TRAP_SPURIOUS,		asm_exc_spurious_interrupt_bug),
	INTG(X86_TRAP_MF,		asm_exc_coprocessor_error),
	INTG(X86_TRAP_AC,		asm_exc_alignment_check),
	INTG(X86_TRAP_XF,		asm_exc_simd_coprocessor_error),

#ifdef CONFIG_X86_32
	TSKG(X86_TRAP_DF,		GDT_ENTRY_DOUBLEFAULT_TSS),
#else
	ISTG(X86_TRAP_DF,		asm_exc_double_fault, IST_INDEX_DF),
#endif
	ISTG(X86_TRAP_DB,		asm_exc_debug, IST_INDEX_DB),

#ifdef CONFIG_X86_MCE
	ISTG(X86_TRAP_MC,		asm_exc_machine_check, IST_INDEX_MCE),
#endif

#ifdef CONFIG_AMD_MEM_ENCRYPT
	ISTG(X86_TRAP_VC,		asm_exc_vmm_communication, IST_INDEX_VC),
#endif

	SYSG(X86_TRAP_OF,		asm_exc_overflow),
#if defined(CONFIG_IA32_EMULATION)
	SYSG(IA32_SYSCALL_VECTOR,	entry_INT80_compat),
#elif defined(CONFIG_X86_32)
	SYSG(IA32_SYSCALL_VECTOR,	entry_INT80_32),
#endif
};

可以看到这次设置非常简单,就是调用了一下idt_setup_from_table,并没有调用load_idt。主要是数组def_idts里面包含了大部分的CPU异常处理。但是没缺页异常,缺页异常是单独设置。设置路径如下:

调用栈:
start_kernel
  setup_arch
    idt_setup_early_pf

代码如下:
linux-src/arch/x86/kernel/idt.c

void __init idt_setup_early_pf(void)
{ 
        
	idt_setup_from_table(idt_table, early_pf_idts,
			     ARRAY_SIZE(early_pf_idts), true);
}

static const __initconst struct idt_data early_pf_idts[] = { 
        
	INTG(X86_TRAP_PF,		asm_exc_page_fault),
};

现在CPU异常的中断处理函数就全部设置完成了,想要研究具体哪个异常是怎么处理的同学,可以去跟踪研究一下相应的函数。

5.2 硬中断(hardirq)

硬件中断的中断处理和软件中断有一部分是相同的,有一部分却有很大的不同。对于IPI中断和per CPU中断,其设置是和软件中断相同的,都是一步到位设置到具体的处理函数。但是对于余下的外设中断,只是设置了入口函数,并没有设置具体的处理函数,而且是所有的外设中断的处理函数都统一到了同一个入口函数。然后在这个入口函数处会调用相应的irq描述符的handler函数,这个handler函数是中断控制器设置的。中断控制器设置的这个handler函数会处理与这个中断控制器相关的一些事物,然后再调用具体设备注册的irqaction的handler函数进行具体的中断处理。

我们先来看一下对中断向量表条目的设置代码。

调用栈如下:
start_kernel
  init_IRQ
    native_init_IRQ
      idt_setup_apic_and_irq_gates

代码如下:
linux-src/arch/x86/kernel/idt.c

/** * idt_setup_apic_and_irq_gates - Setup APIC/SMP and normal interrupt gates */
void __init idt_setup_apic_and_irq_gates(void)
{ 
        
	int i = FIRST_EXTERNAL_VECTOR;
	void *entry;

	idt_setup_from_table(idt_table, apic_idts, ARRAY_SIZE(apic_idts), true);

	for_each_clear_bit_from(i, system_vectors, FIRST_SYSTEM_VECTOR) { 
        
		entry = irq_entries_start + 8 * (i - FIRST_EXTERNAL_VECTOR);
		set_intr_gate(i, entry);
	}

#ifdef CONFIG_X86_LOCAL_APIC
	for_each_clear_bit_from(i, system_vectors, NR_VECTORS) { 
        
		/* * Don't set the non assigned system vectors in the * system_vectors bitmap. Otherwise they show up in * /proc/interrupts. */
		entry = spurious_entries_start + 8 * (i - FIRST_SYSTEM_VECTOR);
		set_intr_gate(i, entry);
	}
#endif
	/* Map IDT into CPU entry area and reload it. */
	idt_map_in_cea();
	load_idt(&idt_descr);

	/* Make the IDT table read only */
	set_memory_ro((unsigned long)&idt_table, 1);

相关文章