本文节选自《实验指导手册》第二版第16.6章
实验指导手册是奔跑吧Linux内核入门篇第二版配套实验书,pdf版本已经release,可以免费下载和自由打印!
下载方法:
登陆“奔跑吧linux社区”微信公众号,输入“奔跑吧2”获取下载地址。
本文是《奔跑吧Linux内核 入门篇》第二版中第16章的实验16-4:中断实验。在实现操作系统基本功能之前,我们先实现中断处理功能,我们以树莓派上ARM Core里面的generic timer为中断源,把中断这条路径先打通,以后在做进程创建和进程切换的时候也是需要用的,可以作为时钟中断来使用。
1.实验目的
1)了解和熟悉ARM64汇编语言。
2)了解和熟悉ARM64的异常等级处理。
3)了解和熟悉ARM64的中断处理流程。
4)了解和熟悉树莓派中系统定时器(system timer)的用法。
2.实验要求
1)在boot.s中实现对ARM64异常向量表的支持。
2)将树莓派中的系统定时器作为中断源,编写中断处理程序,每当有定时器中断到来时输出“Timer interrupt occured”。
3.实验详解
本实验对初学者有一定难度,读者需要熟悉如下几个方面。
ARMv8体系结构中的中断处理过程。
树莓派3B以及树莓派4B上的中断控制器。
树莓派上的定时器外设
ARMv8上的generic timer
中断上下文保存与恢复
3.1 中断处理过程
ARM64处理器核心一般都有两个和中断相关的管脚:nIRQ 和 nFIQ,并且是每个处理器核心都有一对这样的管脚。
另外,PSTATE状态中有两个比特位和中断相关:
I:用来屏蔽IRQ中断
F:用来屏蔽FIQ中断
下图是ARM64处理器发生中断的处理流程。
3.2 树莓派上的中断控制器
ARM公司提供了标准的GIC控制器,例如树莓派4b上支持GIC-400,树莓派3b上支持传统的中断方式(legacy interrupt)。
树莓派4b上提供和支持两种中断控制器,而树莓派3b只支持传统的中断控制器。
GIC-400 (默认)
传统的中断控制器(legacy interrupt controller)
树莓派3b和4b支持多种中断源,如图所示。
ARM Core N:ARM Core本身的中断源,例如Core里面的generic timer
ARMC:可以被VPU和CPU访问的中断源,例如mailbox等
ARM_LOCAL:只能被CPU访问的中断源,例如本地timer等
下面这个图是树莓派传统中断控制器的路由情况。
从图上可以看到,ARM core N和ARM_LOCAL的中断都会路由到ARM_LOCAL routing的硬件单元,而ARMC中断会被路由到ARMC routing的硬件单元。
下面这个图是中断状态的路由情况。
中断状态寄存器 路由过程:
先读SOURCEn中断状态寄存器
SOURCEn寄存器 bit 8 是否置位,若置位,读取PENDING2寄存器
若PENDING2的bit24置位,则读PENDING0寄存器
若PENDING2的bit25置位,则读PENDING1寄存器
树莓派4b上还支持GIC-400的中断控制器,这个留给读者自己去学习。
3.3 ARM Core的generic timer
我们以ARM core上的generic timer为例来说明,如何在树莓派上使用timer中断。
Cortex-A72支持4个ARM Core的generic timer:
CNT_PS_IRQ:Secure EL1 Physical Timer Event interrupt
CNT_PNS_IRQ:Nonsecure EL 1 Physical Timer Event interrupt
CNT_HP_IRQ:Hypervisor Physical Timer Event interrupt, EL2
CNT_V_IRQ:Virtual Timer Event interrupt EL3
其中,P是physical的意思,S是secure,指的是是不是安全世界还是非安全世界,armv8支持 trustzone,在trustzone的软件跑在安全世界里,trustzone外面的软件跑在非安全世界。第二个是,PNS,NS是non-secure的意思,非安全世界的timer,第三个HP timer,HP指的是hypervisor的意思,也就是A72手册里说的ANon-secure EL2 physical timer,第4个是v timer,指的是virtual timer。
这4个generic timer的中断相关设置是在ARM LOCAL中断组的寄存器里。它有4个IRQ_SOURCE寄存器,每个CPU一个,表示中断源的状态,4个FIQ_SOURCE寄存器,并且每个CPU一个。4个TIMER_CNTRL寄存器,每个CPU一个,用来enable对应的中断源。
我们这个实验以Nonsecure EL 1 Physical Timer为例,也就是树莓派手册上说的PNS timer,相关初始化的寄存器是在ARM Core中,见armv8.6手册。和他相关的寄存器也就2~3个,所以timer是非常简单的设备。
第一个寄存器:CNTP_CTL_EL0, Counter-timer Physical Timer Control register. (第D13.8.16章)。比较相关的是第bit0,enable bit,表示打开这个timer。Imask表示中断是mask。ISTATES是一个中断状态位。
第二寄存器:CNTP_TVAL_EL0, Counter-timer Physical Timer TimerValue register (第D13.8.18章)
这里是timervalue,timervalue的方式,就是初始化一个值,当递减到0的时候,timer中断就触发。
64位的 comparevalue的方式
最简单的方式就是timervalue的方式,就是你初始化一个值,让它递减,然后递减到0,timer中断就触发了,我们实验也用这种方式。
3.4 例子
下面是EL1的Nonsecure generic timer的中断处理流程:
a) 初始化timer,设置cntp_ctl_el0寄存器的enable域为1
b) 给timer的TimeValue一个初值,设置cntp_tval_el0寄存器
c) 打开树莓派中断控制器中和timer相关的中断,设置TIMER_CNTRL0寄存器中的CNT_PNS_IRQ为1.
d) 打开PSTATE寄存器中的IRQ中断总开关
e) Timer中断发生
f) 跳转到el1_irq汇编函数
g) 保存中断上下文(使用kernel_entry宏)
h) 跳转到中断处理函数
i) 读取ARM_LOCAL中中断状态寄存器IRQ_SOURCE0
j) 判断是否CNT_PNS_IRQ中断发生
k) 如果是,重新设置TimeValue
l) 返回到el1_irq汇编函数
m) 恢复中断上下文
n) 返回中断现场
3.5 中断上下文
中断现场是发生中断时候那个瞬间的现场,包括CPU处理器的一些状态,对于ARM64处理器来说,中断上下文包括如下内容:
PSTATE寄存器
PC值
SP值
X0~x30寄存器
使用一个栈框数据结构来描述需要保存的中断现场(struct pt_regs)。
中断发生时,中断上下文保存到当前进程的内核栈里,使用栈框来协助保存中断上下文的内容。
本实验的保存中断上下文的参考代码是在arch/arm64/kernel/entry.S文件里。
/*
保存异常发生时候的上下文
保存x0~x29,x30(lr),sp, elr, spsr保存到 栈中
*/
.macro kernel_entry el
/*
SP指向了栈底, S_FRAME_SIZE表示一个栈框的大小.
定义一个struct pt_regs来描述一个栈框,
用在异常发生时保存上下文.
*/
sub sp, sp, #S_FRAME_SIZE
/*
保存通用寄存器x0~x29到栈框里pt_regs->x0~x29
*/
stp x0, x1, [sp, #16 *0]
stp x2, x3, [sp, #16 *1]
stp x4, x5, [sp, #16 *2]
stp x6, x7, [sp, #16 *3]
stp x8, x9, [sp, #16 *4]
stp x10, x11, [sp, #16 *5]
stp x12, x13, [sp, #16 *6]
stp x14, x15, [sp, #16 *7]
stp x16, x17, [sp, #16 *8]
stp x18, x19, [sp, #16 *9]
stp x20, x21, [sp, #16 *10]
stp x22, x23, [sp, #16 *11]
stp x24, x25, [sp, #16 *12]
stp x26, x27, [sp, #16 *13]
stp x28, x29, [sp, #16 *14]
/* x21: 栈顶 的位置*/
add x21, sp, #S_FRAME_SIZE
mrs x22, elr_el1
mrs x23, spsr_el1
/* 把lr保存到pt_regs->lr, 把sp保存到pt_regs->sp位置*/
stp lr, x21, [sp, #S_LR]
/* 把elr_el1保存到pt_regs->pc中
把spsr_elr保存到pt_regs->pstate中*/
stp x22, x23, [sp, #S_PC]
.endm
中断返回时,从当前进程的内核栈中恢复中断现场。
本实验的恢复中断上下文的参考代码是在arch/arm64/kernel/entry.S文件里。
/*
恢复异常发生时保存下来的上下文
*/
.macro kernel_exit el
/* 从pt_regs->pc中恢复elr_el1,
从pt_regs->pstate中恢复spsr_el1
*/
ldp x21, x22, [sp, #S_PC] // load ELR, SPSR
msr elr_el1, x21 // set up the return data
msr spsr_el1, x22
ldp x0, x1, [sp, #16 * 0]
ldp x2, x3, [sp, #16 * 1]
ldp x4, x5, [sp, #16 * 2]
ldp x6, x7, [sp, #16 * 3]
ldp x8, x9, [sp, #16 * 4]
ldp x10, x11, [sp, #16 * 5]
ldp x12, x13, [sp, #16 * 6]
ldp x14, x15, [sp, #16 * 7]
ldp x16, x17, [sp, #16 * 8]
ldp x18, x19, [sp, #16 * 9]
ldp x20, x21, [sp, #16 * 10]
ldp x22, x23, [sp, #16 * 11]
ldp x24, x25, [sp, #16 * 12]
ldp x26, x27, [sp, #16 * 13]
ldp x28, x29, [sp, #16 * 14]
/* 从pt_regs->lr中恢复lr*/
ldr lr, [sp, #S_LR]
add sp, sp, #S_FRAME_SIZE // restore sp
eret // return to kernel
.endm
4.实验步骤
本实验的参考代码可以在QEMU以及树莓派3b上运行。
除了ARM core里面的generic timer之后,树莓派3b和4b还额外支持System Timer and ARM side Timer这两个外设,不过由于QEMU模拟器并不支持这两个外设,所以本实验采用ARM core里面的generic timer。
4.1 QEMU上模拟
我们先在QEMU上模拟。由于本实验的参考代码还没有实现对GIC-400中断控制器的支持,因此,在QEMU里我们只能模拟树莓派3b了。
在Ubuntu Linux主机中,进入参考实验代码目录。
rlk@rlk :$ cd /home/rlk/rlk/runninglinuxkernel_5.0/kmodules/rlk_lab/rlk_basic/chapter_16_benos/lab04_generic_timer/
进入make menuconfig菜单。
rlk@master:lab04_generic_timer $ make menuconfig
其中:
Board selection (Raspberry 3B) -> 选树莓派3b
Uart for Pi (pl_uart) -> 选PL串口
编译并运行。
rlk@master:lab04_generic_timer $ make
rlk@master:lab04_generic_timer $ make run
4.2 在树莓派3b上运行
把4.1章编译的benos.bin文件拷贝到MicroSD里(可以参考第16.1章介绍的VMWare Player和Windows主机共享的方法来拷贝到主机,然后在拷贝到MicroSD卡里),并且修改名字为benos3.bin。
把MicroSD插回树莓派3b上,然后上电运行。在串口中可以看到如下日志信息。
5. 在树莓派4b上运行
我们想在树莓派4b上运行本实验的参考代码。
如果刚才已经编译过的话,首先make clean一遍。
rlk@master:lab04_generic_timer $ make clean
进入make menuconfig菜单。
rlk@master:lab04_generic_timer $ make menuconfig
其中:
Board selection (Raspberry 4B) -> 选树莓派4b
Uart for Pi (pl_uart) -> 选PL串口
然后重新编译。
rlk@master:lab04_generic_timer $ make
按照之前的方法,把编译的benos.bin文件拷贝到MicroSD里。然后,把MicroSD卡插回树莓派4B上。启动树莓派。
我们从串口里发现打印了启动信息,但是没有打印“Core0 Timer interrupt received”。
那是什么原因导致的?为什么树莓派3b能跑,而树莓派4b就不能跑呢?
我们在前面介绍过,树莓派3b和树莓派4b的中断控制器有一点不一样。树莓派3b默认只支持传统的中断控制器,而树莓派4b支持两种:传统的中断控制器和GIC-400中断控制器。而且树莓派4b默认支持GIC-400。我们的实验参考代码只是对传统中断控制器进行了支持,暂时还没有支持GIC-400。
因此,这里有两种解决方法。
方案一:修改配置,让树莓派4b默认使用传统的中控制器。
方案二:修改代码,让其支持GIC-400。
下面给大家一个方案一的hack做法。
1.需要修改MicroSD卡boot分区里的bcm2711-rpi-4-b-nogic.dtb文件。使用hexedit工具来修改,把里面的“arm,gic-400”字符串暴力修改为“not,gic-400”。其实,只要把这个字符串修改了就行。这样,树莓派4b的固件就不能找到设备树上关于GIC的描述,它就会自动切换到传统的中断控制器中。
2.修改MicroSD卡boot分区的config.txt文件,新增一个选项“enable_gic=0”。
[pi4]
kernel=benos4.bin
enable_gic=0
上述两个文件在:
/home/rlk/rlk/runninglinuxkernel_5.0/kmodules/rlk_lab/rlk_basic/chapter_16_benos/lab04_generic_timer/hack_rpi4目录
把MicroSD卡重新插入到树莓派4b上,重新上电,就会看到树莓派4b板子上能打印“Core0 Timer interrupt received”。
关于方案二,我们留给学有余力的读者自行完成。
实验代码
实验参考代码runninglinuxkenrel_5.0的git repo。
国内访问:
https://benshushu.coding.net/...
github:
https://github.com/figozhang/...
本文对应的参考代码在如下目录:kmodules/rlk_lab/rlk_basic/chapter_16_benos/lab04_generic_timer
我们提供配置好的实验环境,基于ubuntu 20.04的VMware/Vbox镜像,可以通过如下方式获取下载地址:
登陆“奔跑吧linux社区”微信公众号,输入“奔跑吧2”获取下载地址。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。