[笨叔点滴2] 为啥子ARM32体系结构中每个处理模式都有一个单独的栈?
“ 各位小伙伴,我是小笨叔。笨叔尽量每天给大家分享一点点小东西,可能是笨笨的、傻傻的、甜甜的、酸酸的小点滴,记录这每一刻每一天的小感悟,就像小雨点一样,它会慢慢汇合到大江大海!”
上次提到LinuxCon大会,这种大会正如笨叔说的
“听君一席话,胜读十年书”
笨叔有幸在北京拜访了Linux社区里面一位老前辈,聆听他当年在80~90年代做CPU处理器体系结构和Linux操作系统的鲜为人知的历史和对人生和技术的感悟,让笨叔大受启发。
在闲谈中,他提到为什么ARM32的处理器需要7种处理器模式,而且每一种处理器模式都需要一个单独的栈空间?比如irq模式,为什么ARM32的设计里需要一个单独的irq模式,而且这个irq模式只有12个字节?为什么不和SVC模式公用一个栈呢?比如x86等传统经典的处理器架构里,是没有为irq单独开辟一个栈的。
这些是非常好的问题,引发了笨叔回到上海继续思考。本文抛砖引玉,希望有兴趣的小伙伴可以在文章后面留言发表您的观点。
01 ARM32上的设计
—
我们来看一下ARM32上,如果发生了一个irq中断,ARM32处理器是怎么处理的?我们知道ARM32里面有一个奇葩的7个工作模式:
如上图所示,在ARMV6之前,ARM的处理器模式有7个。正常的内核是跑在SVC模式的,用户态app是跑在User模式,而其他几个就是我们常说的IRQ中断,FIQ中断和几个异常模式。如果对比x86的话,x86只有ring0 ~ ring3这4个特权级别,内核跑着ring 0里,而用户态跑在ring 3里,而没有区分SVC模式,IRQ模式等。
我们在看看ARMv7上改进。
抛开secure模式不谈,在non-secure即normal world来说,ARMv7在继续沿用7个模式的基础上又区分了PL0模式和PL1和PL2模式, PL是privilege level的意思。简单来说:
PL0模式:用户模式,等同于以前的USER模式
PL1操作系统模式:包含以前的SVC模式,IRQ模式,FIQ模式
PL2虚拟化模式:新增的模式,有点类似x86上虚拟化扩展的root模式
如果说ARM32的设计的合理的话,我们看看ARM v8里的设计。
ARMv8里面已经完全抛弃ARMv7之前的做法了,连名字都该了,现在叫做EL,也就是exception level,而且每一个EL级别的异常也变了。其中
名字改了之后,每个EL级别管辖的范围和权利就不一样了,是不是很像x86里面的ring0~ring3呢,谁叫你x86在PC和服务器上这么火呢?所以,x86上的优点,ARM当然要好好学习啦。
除了这个之外,还完全抛弃了SVC, IRQ, FIQ等等那7个模式了,这和ARMv7又有很多不同了。现在ARMv8上,异常分成两种:
同步异常:比如MMU的一些访问权限问题
异步异常:这个就大家常见的IRQ, FIQ, ERR
而且还有一点,ARMv8里面,不再为每个异常类型设置一个专门的栈,比如IRQ已经没有单独的栈了,现在的栈,只有每个EL才有栈。
02 ARM32中断栈
—
ARM32发生中断之后,IRQ栈和SVC栈的情况如下:
上述这个图就是ARM32在Linux内核中,发生IRQ中,栈的变化情况,大家可以看《奔跑吧Linux内核》第621~626页。总的来说,IRQ模式下面的栈值保存了发生中断那个现场的SPSR, LR, SP_IRQ三个寄存器的内容。然后切换模式到SVC模式,进一步保存通用寄存器到SVC的栈空间里。所以在ARM32里,中断发生之后,其实是使用了两个模式的栈,一个是IRQ的栈,另外一个SVC模式的栈,这个SVC的栈,在Linux内核里其实就是内核栈。
那在X86里,是怎么样的呢?x86处理器里有一个TSS(Task State Segment),当中断发生时,用户进程或者处于用户态(特权级3)或者处于内核态(特权级0),如果是在用户态,那么会发生栈的切换问题,也就是会切换到内核态的栈,如果是在内核态,那么就没有栈切换的问题。但是x86处理器在特权级0上只有一个ESP,这意味着中断发生后,只能使用一个栈,这个栈就是内核栈(kernel stack)。处理器的硬件逻辑会将被中断进程的下条指令(CS,EIP)以及EFLAG压入栈,当然如果发生用户态栈向内核态栈的切换,处理器还会把用户态的(SS, ESP)也压入栈,此时使用的就是内核栈。这个行为属于处理器的硬件逻辑范畴,不是系统软件的行为。
另外一方面,现在Linux内核在x86或者其他体系结构里,已经支持内核栈和中断栈分离的方法了。当然,这里说的中断栈不是本文说的那个CPU内部的中断栈,而是和内核栈类似的一个栈,也就是在中断处理程序中,和SVC模式使用的内核栈分离。这是内核设计的问题了,总之,中断栈可与内核栈共享,也可重新分配一个独立的中断栈。但是负面因素是中断栈如果发生嵌套,可能破坏内核栈的一些数据,因为毕竟共享,所以栈空间有时候难免会捉襟见肘。所以在x86的Linux中,总是使用分离的内核栈设计。系统中每个进程都会拥有属于自己的内核栈,而系统中每个CPU都将为中断处理准备了两个独立的中断栈,分别是hardirq栈和softirq栈。注意,这是OS的设计问题,不是今天我们想讨论的CPU硬件设计上的中断栈分离。
下面是Linux内核里面x86架构使用分离的中断栈的补丁。
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=7974891db234467eaf1fec613ec0129cb4ac2332
那为啥子ARM32要为IRQ模式设置单独一个12字节的栈呢,而不是复用SVC模式的栈呢?
笨叔翻阅了ARM相关的技术文档也没找到答案,我猜测可能是因为一个重要的原因是ARM32这个架构设计的时候是90年代,那个年代的ARM处理器是相当慢的,单独一个IRQ栈可以提高中断的响应速度,特别是早期的ARM处理器主要应用场景是嵌入式系统,支持中断嵌套的方式。从上面的x86的分离的中断栈的patch来看,分离的中断栈的确有不少优势,至少可以保证不会导致SVC模式的栈发生overflow。那究竟是ARM32这种设计先进呢还是ARMv8和x86的设计理念先进呢,是进步还是倒退呢?我有点confuse了!
笨叔抛砖引玉,希望感兴趣的同学可以在后面留言,大家一起讨论。