您现在的位置是:首页 > Linux OSLinux OS
深入理解Linux中断机制(上)---程磊篇
转载2022-08-07【Linux OS】人已围观
简介作者简介:程磊,一线码农,在某手机公司担任系统开发工程师,日常喜欢研究内核基本原理。
作者简介:
程磊,一线码农,在某手机公司担任系统开发工程师,日常喜欢研究内核基本原理。
目录
一、中断基本原理
二、中断流程
三、软件中断
四、硬件中断
五、中断处理
六、中断与同步
七、总结回顾
一、中断基本原理
中断是计算机中非常重要的功能,其重要性不亚于人的神经系统加脉搏。虽然图灵机和冯诺依曼结构中没有中断,但是计算机如果真的没有中断的话,那么计算机就相当于是半个残疾人。今天我们就来全面详细地讲一讲中断。
1.1 中断的定义
我们先来看一下中断的定义:
中断机制:CPU在执行指令时,收到某个中断信号转而去执行预先设定好的代码,然后再返回到原指令流中继续执行,这就是中断机制。
可以发现中断的定义非常简单。我们根据中断的定义来画一张图:
在图灵机模型中,计算机是一直线性运行的。加入了中断之后,计算机就可以透明地在进程执行流中插入一段代码来执行。那么这么做的目的是什么呢?
1.2 中断的作用
设计中断机制的目的在于中断机制有以下4个作用,这些作用可以帮助操作系统实现自己的功能。这四个作用分别是:
1.外设异步通知CPU:外设发生了什么事情或者完成了什么任务或者有什么消息要告诉CPU,都可以异步给CPU发通知。例如,网卡收到了网络包,磁盘完成了IO任务,定时器的间隔时间到了,都可以给CPU发中断信号。
2.CPU之间发送消息:在SMP系统中,一个CPU想要给另一个CPU发送消息,可以给其发送IPI(处理器间中断)。
3.处理CPU异常:CPU在执行指令的过程中遇到了异常会给自己发送中断信号来处理异常。例如,做整数除法运算的时候发现被除数是0,访问虚拟内存的时候发现虚拟内存没有映射到物理内存上。
4.实现系统调用:早期的系统调用就是靠中断指令来实现的,后期虽然开发了专用的系统调用指令,但是其基本原理还是相似的。
1.3 中断的产生
那么中断信号又是如何产生的呢?中断信号的产生有以下4个来源:
1.外设,外设产生的中断信号是异步的,一般也叫做硬件中断(注意硬中断是另外一个概念)。硬件中断按照是否可以屏蔽分为可屏蔽中断和不可屏蔽中断。例如,网卡、磁盘、定时器都可以产生硬件中断。
2.CPU,这里指的是一个CPU向另一个CPU发送中断,这种中断叫做IPI(处理器间中断)。IPI也可以看出是一种特殊的硬件中断,因为它和硬件中断的模式差不多,都是异步的。
3.CPU异常,CPU在执行指令的过程中发现异常会向自己发送中断信号,这种中断是同步的,一般也叫做软件中断(注意软中断是另外一个概念)。CPU异常按照是否需要修复以及是否能修复分为3类:1.陷阱(trap),不需要修复,中断处理完成后继续执行下一条指令,2.故障(fault),需要修复也有可能修复,中断处理完成后重新执行之前的指令,3.中止(abort),需要修复但是无法修复,中断处理完成后,进程或者内核将会崩溃。例如,缺页异常是一种故障,所以也叫缺页故障,缺页异常处理完成后会重新执行刚才的指令。
4.中断指令,直接用CPU指令来产生中断信号,这种中断和CPU异常一样是同步的,也可以叫做软件中断。例如,中断指令int 0x80可以用来实现系统调用。
中断信号的4个来源正好对应着中断的4个作用。前两种中断都可以叫做硬件中断,都是异步的;后两种中断都可以叫做软件中断,都是同步的。很多书上也把硬件中断叫做中断,把软件中断叫做异常。
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来作为系统调用的指令。
四、硬件中断
硬件中断分为外设中断和处理器间中断(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)。其实硬中断严格来说不算是一种方法,因为它是中断处理的必经之路,它就是中断向量表里面设置的处理函数。为了和软中断进行区分,才把硬中断叫做硬中断。硬中断和软中断都是属于中断执行场景,而中断线程和工作队列是属于进程执行场景。把硬件中断的处理任务放到进程场景里面来做,大大提高了中断处理的灵活性。
由于软件中断的处理都是直接处理,都是内核本身直接写好了的,一般都接触不到,而硬件中断的处理和硬件驱动密切相关,所以很多书上所讲的中断处理都是指的硬件中断的处理。
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); idt_setup_done = true; } static const __initconst struct idt_data apic_idts[] = { #ifdef CONFIG_SMP INTG(RESCHEDULE_VECTOR, asm_sysvec_reschedule_ipi), INTG(CALL_FUNCTION_VECTOR, asm_sysvec_call_function), INTG(CALL_FUNCTION_SINGLE_VECTOR, asm_sysvec_call_function_single), INTG(IRQ_MOVE_CLEANUP_VECTOR, asm_sysvec_irq_move_cleanup), INTG(REBOOT_VECTOR, asm_sysvec_reboot), #endif #ifdef CONFIG_X86_THERMAL_VECTOR INTG(THERMAL_APIC_VECTOR, asm_sysvec_thermal), #endif #ifdef CONFIG_X86_MCE_THRESHOLD INTG(THRESHOLD_APIC_VECTOR, asm_sysvec_threshold), #endif #ifdef CONFIG_X86_MCE_AMD INTG(DEFERRED_ERROR_VECTOR, asm_sysvec_deferred_error), #endif #ifdef CONFIG_X86_LOCAL_APIC INTG(LOCAL_TIMER_VECTOR, asm_sysvec_apic_timer_interrupt), INTG(X86_PLATFORM_IPI_VECTOR, asm_sysvec_x86_platform_ipi), # ifdef CONFIG_HAVE_KVM INTG(POSTED_INTR_VECTOR, asm_sysvec_kvm_posted_intr_ipi), INTG(POSTED_INTR_WAKEUP_VECTOR, asm_sysvec_kvm_posted_intr_wakeup_ipi), INTG(POSTED_INTR_NESTED_VECTOR, asm_sysvec_kvm_posted_intr_nested_ipi), # endif # ifdef CONFIG_IRQ_WORK INTG(IRQ_WORK_VECTOR, asm_sysvec_irq_work), # endif INTG(SPURIOUS_APIC_VECTOR, asm_sysvec_spurious_apic_interrupt), INTG(ERROR_APIC_VECTOR, asm_sysvec_error_interrupt), #endif }; static __init void set_intr_gate(unsigned int n, const void *addr) { struct idt_data data; init_idt_data(&data, n, addr); idt_setup_from_table(idt_table, &data, 1, false); }
linux-src/arch/x86/include/asm/desc.h
static inline void init_idt_data(struct idt_data *data, unsigned int n, const void *addr) { BUG_ON(n > 0xFF); memset(data, 0, sizeof(*data)); data->vector = n; data->addr = addr; data->segment = __KERNEL_CS; data->bits.type = GATE_INTERRUPT; data->bits.p = 1; }
linux-src/arch/x86/include/asm/idtentry.h
SYM_CODE_START(irq_entries_start) vector=FIRST_EXTERNAL_VECTOR .rept NR_EXTERNAL_VECTORS UNWIND_HINT_IRET_REGS 0 : .byte 0x6a, vector jmp asm_common_interrupt nop /* Ensure that the above is 8 bytes max */ . = 0b + 8 vector = vector+1 .endr SYM_CODE_END(irq_entries_start)
linux-src/arch/x86/kernel/irq.c
DEFINE_IDTENTRY_IRQ(common_interrupt) { struct pt_regs *old_regs = set_irq_regs(regs); struct irq_desc *desc; /* entry code tells RCU that we're not quiescent. Check it. */ RCU_LOCKDEP_WARN(!rcu_is_watching(), "IRQ failed to wake up RCU"); desc = __this_cpu_read(vector_irq[vector]); if (likely(!IS_ERR_OR_NULL(desc))) { handle_irq(desc, regs); } else { ack_APIC_irq(); if (desc == VECTOR_UNUSED) { pr_emerg_ratelimited("%s: %d.%u No irq handler for vector\n", __func__, smp_processor_id(), vector); } else { __this_cpu_write(vector_irq[vector], VECTOR_UNUSED); } } set_irq_regs(old_regs); } static __always_inline void handle_irq(struct irq_desc *desc, struct pt_regs *regs) { if (IS_ENABLED(CONFIG_X86_64)) generic_handle_irq_desc(desc); else __handle_irq(desc, regs); }
linux-src/arch/x86/kernel/irqinit.c
DEFINE_PER_CPU(vector_irq_t, vector_irq) = { [0 ... NR_VECTORS - 1] = VECTOR_UNUSED, };
linux-src/arch/x86/include/asm/hw_irq.h
typedef struct irq_desc* vector_irq_t[NR_VECTORS];
linux-src/include/linux/irqdesc.h
static inline void generic_handle_irq_desc(struct irq_desc *desc) { desc->handle_irq(desc); }
从上面的代码可以看出,对硬件中断的设置分为两个部分,一部分就像前面的软件中断的方式一样,是从apic_idts数组设置的,设置的都是一些IPI和per CPU的中断。另一部分是把所有剩余的硬件中断的处理函数都设置为irq_entries_start,irq_entries_start会调用common_interrupt函数。在common_interrupt函数中会根据中断向量号去读取per CPU的数组变量vector_irq,得到一个irq_desc。最终会调用irq_desc中的handle_irq来处理这个中断。
对于外设中断为什么要采取这样的处理方式呢?有两个原因,1是因为外设中断和中断控制器相关联,这样可以统一处理与中断控制器相关的事物,2是因为外设中断的驱动执行比较晚,有些设备还是可以热插拔的,直接把它们放到中断向量表上比较麻烦。有个irq_desc这个中间层,设备驱动后面只需要调用函数request_irq来注册ISR,只处理与设备相关的业务就可以了,而不用考虑和中断控制器硬件相关的处理。
我们先来看一下vector_irq数组是怎么初始化的。
linux-src/arch/x86/kernel/apic/vector.c
void lapic_online(void) { unsigned int vector; lockdep_assert_held(&vector_lock); /* Online the vector matrix array for this CPU */ irq_matrix_online(vector_matrix); /* * The interrupt affinity logic never targets interrupts to offline * CPUs. The exception are the legacy PIC interrupts. In general * they are only targeted to CPU0, but depending on the platform * they can be distributed to any online CPU in hardware. The * kernel has no influence on that. So all active legacy vectors * must be installed on all CPUs. All non legacy interrupts can be * cleared. */ for (vector = 0; vector < NR_VECTORS; vector++) this_cpu_write(vector_irq[vector], __setup_vector_irq(vector)); } static struct irq_desc *__setup_vector_irq(int vector) { int isairq = vector - ISA_IRQ_VECTOR(0); /* Check whether the irq is in the legacy space */ if (isairq < 0 || isairq >= nr_legacy_irqs()) return VECTOR_UNUSED; /* Check whether the irq is handled by the IOAPIC */ if (test_bit(isairq, &io_apic_irqs)) return VECTOR_UNUSED; return irq_to_desc(isairq); }
linux-src/kernel/irq/irqdesc.c
struct irq_desc *irq_to_desc(unsigned int irq) { return radix_tree_lookup(&irq_desc_tree, irq); }
可以看出vector_irq数组的初始化数据是从irq_desc_tree来的,我们再来看一下irq_desc_tree是怎么初始化的。
linux-src/kernel/irq/irqdesc.c
int __init early_irq_init(void) { int i, initcnt, node = first_online_node; struct irq_desc *desc; init_irq_default_affinity(); /* Let arch update nr_irqs and return the nr of preallocated irqs */ initcnt = arch_probe_nr_irqs(); printk(KERN_INFO "NR_IRQS: %d, nr_irqs: %d, preallocated irqs: %d\n", NR_IRQS, nr_irqs, initcnt); if (WARN_ON(nr_irqs > IRQ_BITMAP_BITS)) nr_irqs = IRQ_BITMAP_BITS; if (WARN_ON(initcnt > IRQ_BITMAP_BITS)) initcnt = IRQ_BITMAP_BITS; if (initcnt > nr_irqs) nr_irqs = initcnt; for (i = 0; i < initcnt; i++) { desc = alloc_desc(i, node, 0, NULL, NULL); set_bit(i, allocated_irqs); irq_insert_desc(i, desc); } return arch_early_irq_init(); }
可以看到vector_irq数组的内容是在系统初始化的时候通过alloc_desc函数为每个irq进行分配的。在alloc_desc中对irq_desc的初始化会把handle_irq函数指针默认初始化为handle_bad_irq,这个函数代表还没有中断控制器注册这个函数,handle_bad_irq只是简单地确认一下中断,然后做个错误记录。
中断控制器注册handle_irq函数的代码如下:
linux-src/kernel/irq/chip.c
void __irq_set_handler(unsigned int irq, irq_flow_handler_t handle, int is_chained, const char *name) { unsigned long flags; struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, 0); if (!desc) return; __irq_do_set_handler(desc, handle, is_chained, name); irq_put_desc_busunlock(desc, flags); } static void __irq_do_set_handler(struct irq_desc *desc, irq_flow_handler_t handle, int is_chained, const char *name) { if (!handle) { handle = handle_bad_irq; } else { struct irq_data *irq_data = &desc->irq_data; #ifdef CONFIG_IRQ_DOMAIN_HIERARCHY /* * With hierarchical domains we might run into a * situation where the outermost chip is not yet set * up, but the inner chips are there. Instead of * bailing we install the handler, but obviously we * cannot enable/startup the interrupt at this point. */ while (irq_data) { if (irq_data->chip != &no_irq_chip) break; /* * Bail out if the outer chip is not set up * and the interrupt supposed to be started * right away. */ if (WARN_ON(is_chained)) return; /* Try the parent */ irq_data = irq_data->parent_data; } #endif if (WARN_ON(!irq_data || irq_data->chip == &no_irq_chip)) return; } /* Uninstall? */ if (handle == handle_bad_irq) { if (desc->irq_data.chip != &no_irq_chip) mask_ack_irq(desc); irq_state_set_disabled(desc); if (is_chained) desc->action = NULL; desc->depth = 1; } desc->handle_irq = handle; desc->name = name; if (handle != handle_bad_irq && is_chained) { unsigned int type = irqd_get_trigger_type(&desc->irq_data); /* * We're about to start this interrupt immediately, * hence the need to set the trigger configuration. * But the .set_type callback may have overridden the * flow handler, ignoring that we're dealing with a * chained interrupt. Reset it immediately because we * do know better. */ if (type != IRQ_TYPE_NONE) { __irq_set_trigger(desc, type); desc->handle_irq = handle; } irq_settings_set_noprobe(desc); irq_settings_set_norequest(desc); irq_settings_set_nothread(desc); desc->action = &chained_action; irq_activate_and_startup(desc, IRQ_RESEND); } }
不同的系统有不同的中断控制器,其在启动初始化的时候都会去注册irq_desc的handle_irq函数。
下面我们再来看一下具体的硬件驱动应该如何注册自己设备的ISR:
linux-src/include/linux/interrupt.h
static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev) { return request_threaded_irq(irq, handler, NULL, flags, name, dev); }
linux-src/kernel/irq/manage.c
int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id);
驱动程序使用request_irq接口来注册自己的ISR,ISR就是运行在硬中断的,参数handler代表的就是ISR。request_irq又调用request_threaded_irq来实现自己。request_threaded_irq是用来创建中断线程的函数接口,其中有两个参数handler、thread_fn,都是函数指针,handler代表的是ISR,是进行中断预处理的,thread_fn代表的是要创建的中断线程的入口函数,是进行中断后处理的。中断线程的细节我们在5.5中断线程中再细讲。
我们再来总结一下外设中断的处理方式。外设中断的向量表条目都被统一设置到同一个函数common_interrupt。在函数common_interrupt中又会根据irq参数去一个类型为irq_desc的vector_irq数组中寻找其对应的irq_desc,并用irq_desc的handle_irq来处理这个中断。vector_irq数组是在系统启动时初始化的,每个irq_desc的handle_irq都是中断控制器初始化时设置的,handle_irq的处理是和中断控制器密切相关的。具体的硬件驱动会通过request_irq接口来注册ISR,每个ISR都会生成一个irqaction,这个irqaction会挂在irq_desc的链表上。这样中断发生时handle_irq就可以去执行与irq相对应的每个ISR了。
Tags:
很赞哦! ()
随机图文
-
一文搞定 | Linux共享内存原理
在Linux系统中,每个进程都有独立的虚拟内存空间,也就是说不同的进程访问同一段虚拟内存地址所得到的数据是不一样的,这是因为不同进程相同的虚拟内存地址会映射到不同的物理内存地址上。 但有时候为了让不同进程之间进行通信,需要让不同进程共享相同的物理内存,Linux通过 共享内存 来实现这个功能。下面先来介绍一下Linux系统的共享内存的使用。 -
深入理解CPU的调度原理
前言软件工程师们总习惯把OS(Operating System,操作系统)当成是一个非常值得信赖的管家,我们只管把程序托管到OS上运行,却很少深入了解操作系统的运行原理。确实,OS作为一个通用的 -
Linux 中断的底裤之 workqueue
workqueue 是除了 softirq 和 tasklet 以外最常用的下半部机制之一。workqueue 的本质是把 work 交给一个内核线程,在进程上下文调度的时候执行。因为这个特点,所以 workqueue 允许重新调度和睡眠,这种异步执行的进程上下文,能解决因为 softirq 和 tasklet 执行时间长而导致的系统实时性下降等问题。 -
Linux 中断所有知识点
GIC,Generic Interrupt Controller。是ARM公司提供的一个通用的中断控制器。主要作用为:接受硬件中断信号,并经过一定处理后,分发给对应的CPU进行处理。 当前GIC 有四个版本,GIC v1~v4, 本文主要介绍GIC v3控制器。