往期内容
本专栏往期内容,interrtupr子系统:
- 深入解析Linux内核中断管理:从IRQ描述符到irq domain的设计与实现
- Linux内核中IRQ Domain的结构、操作及映射机制详解
- 中断描述符irq_desc成员详解
- Linux 内核中断描述符 (irq_desc) 的初始化与动态分配机制详解
- 中断的硬件框架
- GIC介绍
- GIC寄存器介绍
pinctrl和gpio子系统专栏:
专栏地址:pinctrl和gpio子系统
编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用
– 末片,有专栏内容观看顺序
input子系统专栏:
- 专栏地址:input子系统
- input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
– 末片,有专栏内容观看顺序I2C子系统专栏:
- 专栏地址:IIC子系统
- 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
– 末篇,有专栏内容观看顺序总线和设备树专栏:
- 专栏地址:总线和设备树
- 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
– 末篇,有专栏内容观看顺序
目录
- 往期内容
- 1.回顾中断的发生、处理过程
- 2.异常向量表的安装
- 2.1 复制向量表
- 2.2 向量表在哪
- 3.中断向量
- 4.处理流程
- 5.处理函数
1.回顾中断的发生、处理过程
- 中断发生的硬件过程
-
中断处理的软件处理流程
- CPU执行完当前指令,检查到发生了中断,跳到向量表
- 保存现场、执行GIC提供的处理函数、恢复现场
中断的硬件框架-CSDN博客
2.异常向量表的安装
对于arm架构,异常向量表有两个位置:0和0xffff0000。前者一般在裸机中会用到;后者是上了操作系统后的,并且这个地址是虚拟地址,需要在物理地址上存放好vector向量表后,将虚拟地址0xffff0000映射到该存放好向量表的物理地址
2.1 复制向量表
- 汇编代码
// arch\arm\kernel\head.S
1. bl __lookup_processor_type......
2. bl __create_page_tables //创建页表:建立虚拟地址和物理地址之间的映射关系
3. ldr r13, =__mmap_switched //address to jump to after mmu has been enabled 当使能mmu后会跳到mmap_switched函数,将该函数地址保存到r13
4. b __enable_mmub __turn_mmu_onmov r3, r13ret r3
5. __mmap_switched: // arch\arm\kernel\head-common.S
6. b start_kernel //这里是跳转指令,也就是跳转去仔细start_kernel函数
- 创建新向量表,将向量表的地址复制到新分配的vectors中,主要是在early_trap_init函数中实现的。
start_kernel // init\main.csetup_arch(&command_line); // arch\arm\kernel\setup.cpaging_init(mdesc); // arch\arm\mm\mmu.cdevicemaps_init(mdesc); // arch\arm\mm\mmu.cvectors = early_alloc(PAGE_SIZE * 2); // 1.分配新向量表 -- 物理内存early_trap_init(vectors); // 2.在代码中将vectors中的向量表复制到新向量表,具体看下图// 3. 映射新向量表到虚拟地址0xffff0000//存放向量表的物理地址和虚拟地址0xffff0000之间的关联/** Create a mapping for the machine vectors at the high-vectors* location (0xffff0000). If we aren't using high-vectors, also* create a mapping at the low-vectors virtual address.*/map.pfn = __phys_to_pfn(virt_to_phys(vectors));map.virtual = 0xffff0000;map.length = PAGE_SIZE;#ifdef CONFIG_KUSER_HELPERSmap.type = MT_HIGH_VECTORS;#elsemap.type = MT_LOW_VECTORS;#endifcreate_mapping(&map);
下面是devicemaps_init
函数中具体的内容:
static void __init devicemaps_init(const struct machine_desc *mdesc)
{struct map_desc map; // 用于定义映射区域的结构体unsigned long addr; // 循环使用的地址变量void *vectors; // 向量表的虚拟地址指针/** 1. 提前分配异常向量页的内存,通常大小为两个页面。* 异常向量用于处理处理器的各种异常(如中断、错误等)。*/vectors = early_alloc(PAGE_SIZE * 2);// 提前初始化异常向量,将 vectors 地址传递给异常初始化函数early_trap_init(vectors);/** 2. 清除页表,排除顶层 PMD 页面,以便稍后使用 early_fixmaps。* VMALLOC_START 表示虚拟内存的起始地址。*/for (addr = VMALLOC_START; addr < (FIXADDR_TOP & PMD_MASK); addr += PMD_SIZE)pmd_clear(pmd_off_k(addr));/** 3. 如果内核是 XIP (Execute In Place) 模式,则在 modulearea 中映射内核代码。* XIP 模式允许直接从 ROM 中执行代码,不必将代码拷贝到 RAM 中。*/
#ifdef CONFIG_XIP_KERNELmap.pfn = __phys_to_pfn(CONFIG_XIP_PHYS_ADDR & SECTION_MASK); // XIP 的物理基地址map.virtual = MODULES_VADDR; // 模块区的起始虚拟地址map.length = ((unsigned long)_exiprom - map.virtual + ~SECTION_MASK) & SECTION_MASK; // 映射长度map.type = MT_ROM; // 映射类型为只读create_mapping(&map); // 创建内核的只读映射
#endif/** 4. 映射缓存刷新区域。此区域用于缓存清除和同步 CPU 缓存。*/
#ifdef FLUSH_BASEmap.pfn = __phys_to_pfn(FLUSH_BASE_PHYS); // 映射物理地址map.virtual = FLUSH_BASE; // 缓存刷新区域的虚拟地址map.length = SZ_1M; // 映射长度为 1 MBmap.type = MT_CACHECLEAN; // 设置为缓存清理类型create_mapping(&map); // 创建映射
#endif
#ifdef FLUSH_BASE_MINICACHEmap.pfn = __phys_to_pfn(FLUSH_BASE_PHYS + SZ_1M); // 小缓存物理地址map.virtual = FLUSH_BASE_MINICACHE; // 小缓存虚拟地址map.length = SZ_1M; // 映射长度 1 MBmap.type = MT_MINICLEAN; // 小缓存清理类型create_mapping(&map); // 创建映射
#endif/** 5. 为处理器的高向量位置(0xffff0000)创建一个映射。* 如果不使用高向量,也在低向量位置(0x00000000)创建映射。*/map.pfn = __phys_to_pfn(virt_to_phys(vectors)); // 计算向量的物理页帧号map.virtual = 0xffff0000; // 设置向量的虚拟地址为高向量位置map.length = PAGE_SIZE; // 设置映射大小
#ifdef CONFIG_KUSER_HELPERSmap.type = MT_HIGH_VECTORS; // 高向量类型
#elsemap.type = MT_LOW_VECTORS; // 低向量类型
#endifcreate_mapping(&map); // 创建高向量映射// 如果不使用高向量,则在低向量地址(0x00000000)创建映射if (!vectors_high()) {map.virtual = 0;map.length = PAGE_SIZE * 2; // 为两个页面创建映射map.type = MT_LOW_VECTORS; // 低向量类型create_mapping(&map); // 创建低向量映射}/** 6. 为内核创建一个只读映射,用于保护高向量的内核页面。*/map.pfn += 1; // 下一个物理页面map.virtual = 0xffff0000 + PAGE_SIZE; // 设置为高向量第二页面的虚拟地址map.length = PAGE_SIZE; // 映射一个页面map.type = MT_LOW_VECTORS; // 低向量类型create_mapping(&map); // 创建只读映射/** 7. 如果机器描述结构中包含 map_io 函数,则调用它映射静态映射设备;* 否则使用默认的 debug_ll_io_init() 进行初始化。*/if (mdesc->map_io)mdesc->map_io();elsedebug_ll_io_init();// 填补 PMD 缺口fill_pmd_gaps();/** 8. 为 VMALLOC 区域保留固定的 I/O 空间,用于 PCI 设备。*/pci_reserve_io();/** 9. 刷新缓存和 TLB 确保所有内存操作都一致。* 此外确保写缓存中的向量页已写回。*/local_flush_tlb_all(); // 刷新整个 TLBflush_cache_all(); // 刷新缓存/** 10. 启用异步中止异常处理。*/early_abt_enable();
}
- 向量页分配与初始化:提前分配两个页面的大小用于异常向量表,然后调用
early_trap_init
初始化异常向量。 - 页表清理:清除页表,以便为早期的固定映射区域做准备。
- XIP 模式下的内核映射:如果配置了 XIP(即在 ROM 中直接执行内核代码),则为 XIP 内核创建只读映射。
- 缓存刷新区域映射:创建缓存清理区域的映射,用于 CPU 缓存同步。
- 向量表映射:如果使用高向量位置,则创建映射在
0xffff0000
地址;若不使用高向量,还会在0x00000000
地址创建低向量映射。 - 只读映射创建:为向量表的只读区域创建映射,提供额外保护。
- 静态映射设备的 I/O 映射:根据机器描述结构
mdesc
中的map_io
函数映射 I/O 设备,否则使用默认方法初始化。 - VMALLOC 区域 I/O 空间保留:为 PCI 设备保留固定的 I/O 空间。
- 缓存与 TLB 刷新:刷新缓存和 TLB 确保内存一致性,防止写缓存中的向量页未写回而影响异常处理。
- 启用异步异常:开启处理器的异步异常(如数据预取异常)。
而该函数中的 early_trap_init(vectors);
就是将vectors段进行初始化,比如向量表起始地址设置为__vectors_start
:
void __init early_trap_init(void *vectors_base)
{
#ifndef CONFIG_CPU_V7Munsigned long vectors = (unsigned long)vectors_base; // 向量页的起始地址extern char __stubs_start[], __stubs_end[]; // 存根代码的起始和结束地址extern char __vectors_start[], __vectors_end[]; // 向量表的起始和结束地址unsigned i; // 用于遍历向量页的循环变量// 将传入的向量页基地址存入全局变量 vectors_page,供其他地方使用vectors_page = vectors_base;/** 1. 初始化向量页,使所有未定义的指令都跳转到同一个未定义指令。* 这里使用的指令 0xe7fddef1 在 ARM 和 Thumb 两种指令集下都是未定义的。* 这种处理方式确保任何未捕获的异常都将被捕获。*/for (i = 0; i < PAGE_SIZE / sizeof(u32); i++)((u32 *)vectors_base)[i] = 0xe7fddef1;/** 2. 将向量表和存根代码复制到向量页(地址 0xffff0000 处);* 向量表用于异常处理入口,存根代码提供了具体的异常处理逻辑。*/memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start); // 复制向量表memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start); // 复制存根代码// 初始化用户空间的辅助代码(kuser helpers),用于用户态和内核态的交互kuser_init(vectors_base);/** 3. 刷新指令缓存确保向量表和存根代码对处理器指令流可见。* 刷新范围为向量页的两个页面大小(即 8 KB)。*/flush_icache_range(vectors, vectors + PAGE_SIZE * 2);#else /* ifndef CONFIG_CPU_V7M *//** 在 ARM Cortex-M 处理器(如 Cortex-M3 和 Cortex-M4)上,向量表位置可以通过配置寄存器指定,* 因此不需要复制向量表到专用内存区域。直接在内核镜像中使用即可。*/
#endif
}
-
指令未定义初始化:对整个向量页填充一条未定义的指令
0xe7fddef1
。该指令在 ARM 和 Thumb 指令集中都是未定义的,因此任何未捕获的异常都会导致未定义的指令异常,从而统一跳转到异常处理逻辑中。这种方法在初始化阶段为所有异常提供一个默认处理入口。 -
向量表与存根代码复制:
- 将向量表和存根代码从内核镜像复制到向量页。向量表用于存放异常入口地址,例如中断、系统调用等入口。
- 存根代码是实际的异常处理逻辑,它们与向量表中的入口地址配合实现对异常的处理。
-
用户辅助功能初始化:
kuser_init
函数用于初始化 kuser helpers。kuser helpers 是一组用于用户空间与内核进行低级交互的辅助函数。 -
指令缓存刷新:调用
flush_icache_range
函数刷新指令缓存,使得新的向量表和存根代码对处理器的指令流可见。这确保处理器在异常发生时能够正确获取最新的异常处理代码。 -
Cortex-M 特殊处理:在 Cortex-M 系列处理器(如 ARMv7-M 架构)上,向量表的位置可以直接配置,而不需要复制向量表到一个特定的内存区域。
2.2 向量表在哪
上面说到在代码中就有将向量表的起始地址__vectors_start
复制到新分配的vectors段中,vectros
段的内容怎么去找到它??就是靠__vectors_start
上面代码中可以看到代码中向量表位于__vectors_start
处( 向量表起始地址),它在arch/arm/kernel/vmlinux.lds
中定义, 也就是下面的连接脚本,指定 .vectors
段的加载位置(0xffff0000),确保在 CPU 触发异常时可以正确找到中断向量表的位置(也就是找到vectors
段中的向量表) :
__vectors_start = .; // 定义向量表起始地址
.vectors 0xffff0000 : AT(__vectors_start) { // 将 .vectors 段加载到物理地址0xffff0000*(.vectors) // 匹配名为 .vectors 的段
}
. = __vectors_start + SIZEOF(.vectors); // 更新当前地址指针到 .vectors 结束
__vectors_end = .; // 定义向量表结束地址
__stubs_start = .; // 定义 stubs 区域起始地址
.stubs ADDR(.vectors) + 0x1000 : AT(__stubs_start) { // 将 .stubs 段放置在偏移 0x1000 处*(.stubs) // 匹配名为 .stubs 的段
}
__vectors_start = .;
:定义.vectors
段的起始地址为当前地址。**.vectors 0xffff0000 : AT(__vectors_start) { \*(.vectors) }**
:将.vectors
段分配到内存地址0xffff0000
,并将__vectors_start
设置为该段的物理加载地址。*(.vectors)
指令将所有.vectors
名称的段匹配到这里。**. = __vectors_start + SIZEOF(.vectors);**
:更新当前地址到.vectors
段结束处,计算.vectors
段大小。.stubs ADDR(.vectors) + 0x1000 : AT(__stubs_start) { \*(.stubs) }
:将.stubs
段放置在.vectors
段的偏移 0x1000 位置,为跳转指令提供目的地。
在arch\arm\kernel\entry-armv.S里搜.vectors
,可以找到vectors
段的内容。 这段汇编代码定义了一个 .vectors
段,其中包含 ARM 处理器的中断向量表。ARM 处理器在发生异常时,会跳转到固定的地址来执行特定的中断向量代码:
.section .vectors, "ax", %progbits
.L__vectors_start: // 向量表起始地址标签W(b) vector_rst // 重置向量W(b) vector_und // 未定义指令异常向量W(ldr) pc, .L__vectors_start + 0x1000 // 软件中断向量,跳转到偏移0x1000的位置W(b) vector_pabt // 预取指令异常向量W(b) vector_dabt // 数据访问异常向量W(b) vector_addrexcptn // 地址异常向量W(b) vector_irq // 外部中断(IRQ)向量W(b) vector_fiq // 快速中断(FIQ)向量
其中每一行的 W(b)
或 W(ldr)
指令表示一个 ARM 指令:
W(b)
:生成一个跳转指令b
,用于跳转到指定的中断向量处理函数。W(ldr)
:生成一个加载指令ldr
,将.L__vectors_start + 0x1000
处的地址加载到pc
寄存器,实现对软件中断的处理。
中断向量定义如下:
vector_rst
:复位向量,发生复位时跳转执行。vector_und
:未定义指令异常向量。vector_pabt
:预取指令异常向量,指令预取时发生异常。vector_dabt
:数据访问异常向量,数据访问时发生异常。vector_addrexcptn
:地址异常向量。vector_irq
:外部中断(IRQ)向量。vector_fiq
:快速中断(FIQ)向量。
关联
.vectors
段包含了 ARM 异常中断向量,指向特定异常的处理函数。- 连接脚本将
.vectors
段放置在0xffff0000
地址,该地址是 ARM 处理器默认的异常向量位置。 ldr
指令将.stubs
段(偏移0x1000
)作为跳转目标,从而执行不同的异常处理代码。
3.中断向量
发生中断时,CPU跳到向量表去执行b vector_irq
。
vector_irq函数使用宏来定义: