Stack Switching in Linux
基于 Linux 5.7-rc5
源码简单介绍了 Linux 中栈的切换。
Linux 中常见的栈
在 Linux 中,常见的栈分为 3 类:
用户栈:线程在用户态使用的栈,位于用户态地址空间顶部,栈底由 Linux 宏
STACK_TOP
宏定义,但 Linux 在加载进程时,会对栈进行随机化,因此栈底的值往往不等于STACK_TOP
,用户栈是一类 per-task 的栈。用户栈在加载可执行程序时进行初始化,以 elf 文件加载为例,在函数load_elf_binary
中函数randomize_stack_top
会将宏STACK_TOP
进行随机化后作为用户栈的栈底地址,然后函数setup_arg_pages
会在栈里填充进程的参数等内容。1
2
3
4
5
6
7
8
9/* fs/binfmt_elf.c */
static int load_elf_binary(struct linux_binprm *bprm)
{
...
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
executable_stack);
...
}
List.1 用户栈的初始化 用户栈的大小可以通过
ulimit -s
获取,一般默认为 8MB。1
2$ ulimit -s // ulimit是shell built-in command
8192 // 单位为kbytes内核栈:线程在内核态使用的栈,在进行 fork 时,新线程的栈会由内核动态分配,大小为由宏
THREAD_SIZE
定义,在x86_64
下为 4 个页面(不开启 KASAN 的情况下),即 16KB,内核栈是一类 per-task 的栈。内核栈在 fork 时进行初始化,在函数dup_task_struct
中进行栈的分配和初始化。1
2
3
4
5
6
7
8
9/* kernel/fork.c */
static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{
...
stack = alloc_thread_stack_node(tsk, node); /* 分配栈 */
...
tsk->stack = stack; /* 初始化task_struct中的栈指针 */
...
}
中断栈:内核在处理硬中断时使用的栈,大小为
IRQ_STACK_SIZE
,在x86_64
下为 4 个页面(不开启 KASAN 的情况下),即 16KB,中断栈也是一类 per-cpu 的栈。中断栈的初始化以及与具体架构有关。x86_64
中断栈的是静态分配的栈,在 CPU 启动过程中初始化相应的指针,初始化完成后就不再改变。1
2
3
4
5
6
7
8
9
10/* arch/x86/kernel.c */
DEFINE_PER_CPU_PAGE_ALIGNED(struct irq_stack, irq_stack_backing_store) __visible; /* 静态分配中断栈 */
DECLARE_INIT_PER_CPU(irq_stack_backing_store);
...
int irq_init_percpu_irqstack(unsigned int cpu)
{
if (per_cpu(hardirq_stack_ptr, cpu))
return 0;
return map_irq_stack(cpu); /* 映射中断栈 */
}List.3 x86_64 中断栈的初始化 ARM64
中断栈的初始化由函数init_irq_stacks
完成,对于没有使用CONFIG_VMAP_STACK
的情况,栈是静态分配的,当使用了CONFIG_VMAP_STACK
时,栈会进行动态分配。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/* arch/arm64/kernel/irq.c */
#ifdef CONFIG_VMAP_STACK
static void init_irq_stacks(void)
{
int cpu;
unsigned long *p;
for_each_possible_cpu(cpu) {
p = arch_alloc_vmap_stack(IRQ_STACK_SIZE, cpu_to_node(cpu)); /* 动态分配中断栈 */
per_cpu(irq_stack_ptr, cpu) = p;
}
}
#else
/* irq stack only needs to be 16 byte aligned - not IRQ_STACK_SIZE aligned. */
DEFINE_PER_CPU_ALIGNED(unsigned long [IRQ_STACK_SIZE/sizeof(long)], irq_stack); /* 静态分配中断栈 */
static void init_irq_stacks(void)
{
int cpu;
for_each_possible_cpu(cpu)
per_cpu(irq_stack_ptr, cpu) = per_cpu(irq_stack, cpu);
}
#endif
x86_64
的栈切换
x86_64
总是使用 RSP
寄存器作为栈指针,因此,当要进行栈切换时,总是需要去更新 RSP
寄存器的值。
除了前文介绍的 3 种常用的栈以外,x86_64
Linux 还引入了 trampoline stack。Trampoline stack 是 x86_64
Linux 在从用户态切换到内核态时临时使用的栈,大小为 512 字节,是一类 per-cpu 的栈。Trampoline stack 的指针保存在 Task State Segment (TSS
) 中,trampoline stack 的切换是由硬件自动完成的。
此外,对于 x86_64
,Intel 还提供了 Interrupt Stack Table (IST
) 硬件特性 [1]。IST 提供了 7 个新的栈,这些栈的切换方式与 trampoline stack 类似,是由硬件自动完成的。当发生中断 / 异常时,硬件通过 interrupt-descriptor table (IDT
) 中的 descriptor 的一个域(即下图红框内的 3-bit)确定需要使用哪个栈,如果全都不使用,则会硬件使用默认的行为。x86_64
Linux 目前定义了 4 种 IST
栈,包括用于处理 Double fault 的 ESTACK_DF
,用于处理 Non-Maskable Interrupt 的 ESTACK_NMI
,用于处理 Hardware Debug Interrupt 的 ESTACK_DB
以及用于处理 Machine Check Exception 的 ESTACK_MCE
[2]。
同时,如果开启了 Shadow Stack,每个 IST
中的栈都会有一个对应的 Shadow Stack。
为了简化,不讨论 Shadow Stack 以及 IST 中的栈,故后续讨论的中断和异常不包括 Double fault,Non-Maskable Interrupt (NMI),Hardware Debug Interrupt 以及 Machine Check Exception。
x86_64
在硬件上有四个特权级,ring0,ring1,ring2,ring3
,但有时为了在特殊情况下进一步区分特权级,还有 ring -1
(用于虚拟化)以及 ring -2
(System Management Mode[3],为 BIOS 和 UEFI 而设计)的说法。一般情况下,用户态使用 ring3
,内核态使用 ring0
,并且为了简化,不讨论使用虚拟化的情况。
Trampoline stack 的初始化和内核栈指针
Trampoline stack 在 CPU 初始化过程中进行初始化,将 trampoline stack 的地址加载到 TSS
中,即 tss.sp0
中。如果没有开启 XEN 的半虚拟化,则 tss.sp0
在加载后就不再变化。
1 |
|
Trampoline stack 是静态分配的,且大小为 512 字节。
1 |
|
虽然 Linux 在初始化内核栈的时候在 task_struct
中的保存了内核栈的指针,但 x86_64
Linux 在发生异常或中断时并不是直接从 task_struct
中读取内核栈指针,而是从 cpu_current_top_of_stack
(实际上是 tss.sp1
的别名)这个 per-cpu 变量中获取。因此,在每次进行进程切换时,内核会在__switch_to
函数中更新 cpu_current_top_of_stack
变量。
1 |
|
syscall 的栈切换
syscall
指令是 x86_64
为了加快系统调用速度而提供的一条指令。当硬件执行 syscall
指令时,硬件将切换到 ring0
,将 syscall
的下一条指令的地址加载到 RCX
寄存器中,并加载 IA32_LSTAR MSR
的值作为切换到 ring0
后的入口地址。同时,硬件会将 RFLAGS
的内容保存到 R11
寄存器里,并根据 IA32_FMASK MSR
的设置更新 RFLAGS
寄存器。此外,硬件会从 IA32_STAR MSR
中获取 SS
和 CS
段寄存器的值,但硬件会将固定值加载到 descriptor 的 cache 中,而不是加载 GDT
或 LDT
中对应的 descriptor[4]。
sysret
是与 syscall
成对使用的指令,用于从系统调用返回,除了 SS
和 CS
段寄存器的值是从 IA32_STAR MSR
与 syscall
指令不同的域获得的以外,其他处理与 syscall
指令相反。这里需要记住的是,无论是 syscall
还是 sysret
指令,都没有改变栈指针寄存器(即 RSP
的值),也不会向内存中写入任何内容 [4]。
syscall
指令避免了需要通过中断的方式进行系统调用。x86_64
Linux 系统调用处理函数入口为 entry_SYSCALL_64
,在函数入口处,内核会将用户栈的地址临时保存在 tss.sp2
中(第 4 行),然后从 cpu_current_top_of_stack
中获取当前线程的内核栈地址(第 6 行),并切换到内核栈,然后再从 tss.sp2
中取出用户栈的指针保存到内核栈上(第 10 行)。因此在 syscall 的处理入口处,x86_64
Linux 并没有使用 trampoline stack。
1 |
|
当 syscall 处理完毕后(即函数 do_syscall_64
执行完毕后),x86_64
Linux 会首先读取 tss.sp0
中存放的 trampoline stack 地址,并切换到 trampoline stack(第 18 行)。最后加载用户态栈指针(第 23 行)后回到用户态。因此,在 syscall 处理过程中,x86_64
Linux 进行了 3 次栈切换:用户栈 -> 内核栈 -> trampoline stack -> 用户栈
。
中断的栈切换
x86_64
发生中断时,硬件会进行一些列操作,并将一些寄存器的内容保存在栈上。
当中断发生在用户态时,硬件会从 TSS
中取出 ring0
的栈指针(即 tss.sp0
)。而 tss.sp0
在初始化时,加载了 trampoline stack 的指针。因此,在发生中断时,硬件会自动切换到 trampoline stack,而 SS
段寄存器的值将变为 0。同时,由于硬件自动切换了栈,为了防止中断发生前的栈指针丢失,硬件会把发生中断前的段寄存器 SS
、栈指针 RSP
、标志寄存器 RFLAGS
、段寄存器 CS
以及指令指针寄存器 EIP
依次压入 trampoline stack。如果该中断需要保存 error code
,硬件还会在最后将 error code
压栈 [5]。
如果中断发生在内核态,则硬件不会切换栈,但在 x86_64
下,硬件同样会保存段寄存器 SS
的值和栈指针 RSP
的值,而不像 32 位模式下的 x86
只会保存标志寄存器 RFLAGS
、段寄存器 CS
和指令指针寄存器 RIP
,以及 error code
(如果存在)[5]。
x86_64
Linux 在发生中断后会跳转到函数 common_interrupt
中,common_interrupt
主要包含三个部分:①中断处理的准备工作,包括保存中断上下文,进行栈切换等,由 interrupt_entry
函数完成。②中断处理,由函数 do_IRQ
完成。③中断退出,从 Label ret_from_intr
处开始。
1 |
|
x86_64
在用户态发生中断时,实际上在处理中断前做了 3 次栈切换,第一次栈切换由硬件进行,将当前的 RSP
切换为 TSS
中的 sp0
的值,即切换到 trampoline stack,同时硬件会把部分寄存器(SS
,RSP
,RFLAGS
,CS
以及 RIP
) 的值保存在 trampoline stack 上。第二次切换发生在函数 interrupt_entry
中,interrupt_entry
是 x86_64
Linux 在执行中断处理函数之前需要执行的函数,为中断处理做一些准备工作。在 interrupt_entry
中,内核会把硬件保存在 trampoline stack 中的内容压入内核栈中,同时把其他未保存的通用寄存器 (GPR) 保存在内核栈中,构成中断上下文 (pt_regs
) 结构体。因此,中断上下文实际上是保存在内核栈里面。
1 |
|
第三次切换是从内核栈切换到中断栈,中断栈的切换通过宏 ENTER_IRQ_STACK
实现。ENTER_IRQ_STACK
会将 per-cpu
变量 irq_count
的值加一,如果 irq_count
变为 0,则要切换到中断栈中(即未发生中断时,irq_count
的值为 - 1),如果 irq_count
大于 0,说明之前已经在使用中断栈了,无需在切换到中断栈。此外,ENTER_IRQ_STACK
还会将将原本的栈指针保存在中断栈上以便处理完中断后恢复。
1 |
|
中断处理完成后,Linux 会通过宏 LEAVE_IRQ_STACK
回到原本的栈上。LEAVE_IRQ_STACK
的动作比较简单,一个是将 RSP
恢复到调用 ENTER_IRQ_STACK
之前的值,另一个是将 irq_count
减一,以便 ENTER_IRQ_STACK
判断是否当前正在使用中断栈。
1 |
|
由于中断发生前在用户态,因此最后还需要返回用户态,此时内核会再次从切换到 trampoline stack,当返回用户态时,硬件会自动从 trampoline stack 中读取用户态的栈顶指针,并设置 RSP
。
1 |
|
因此,如果中断发生在用户态,则将进行 6 次栈切换:用户栈 -> trampoline stack -> 内核栈 -> 中断栈 -> 内核栈 -> trampoline stack ->用户栈
。
如果中断发生在内核态,则硬件不会进行栈切换,即不会切换到 trampoline stack,因此 Linux 也无需再从 trampoline stack 切换到内核栈,故只进行 2 次栈切换(原本使用内核栈):内核栈 -> 中断栈 -> 内核栈
。
异常的栈切换
异常发生时,硬件的行为与中断发生时一致,不再重复介绍。
x86_64
Linux 的异常入口主要由 idtentry
宏定义,最终会跳转到 idtentry_part
宏中。idtentry_part
同样包括 3 个部分:①异常处理的准备工作,paranoid_entry
和 error_entry
。②异常处理函数 \do_sym
。③异常退出 paranoid_exit
和 error_exit
。由于 paranoid_entry
和 paranoid_exit
都与使用 IST 的中断 / 异常有关,因此这里只讨论 error_entry
和 error_exit
。
1 |
|
用户态引发的异常的处理主要位于 error_entry
,异常发生前位于用户态时,硬件会自动切换从用户栈切换到 trampoline stack,然后再由内核从 trampoline stack 切换到对应的内核栈,sync_regs
函数将 pt_regs
拷贝到内核栈后的会返回内核栈的栈顶指针,然后内核再将该栈顶指针写入 RSP
完成 trampoline stack 到内核栈的切换。
1 |
|
用户态发生异常时,异常处理完毕的返回用户态时的栈的处理流程与中断一致,这里不再重复介绍。对于发生在用户态的异常,需要进行 4 次栈切换:用户栈 -> trampoline stack -> 内核栈 -> trampoline stack -> 用户栈
。如果异常发生前位于内核态,则不进行栈切换(一直使用内核栈)。虽然 Linux 现在已经不支持中断的抢占,但异常仍然是可以被中断所抢占的,如果异常处理过程中发生了中断抢占的情况,还有可能会切换到中断栈上。
进程切换时的栈切换
在进程切换过程中,栈切换发生在__switch_to_asm
函数中,内核会把栈指针寄存器 RSP
的值放在当前进程 task_struct
的 thread
成员的 sp
成员中,同时在另一个进程的 task_struct
的同一位置取出新的栈指针,并写入到 RSP
寄存器中。
1 |
|
ARM64
的栈切换
相比于 x86_64
,ARM64
的栈切换机制要简单的多。ARM64
将特权级分为了 4 种:EL0, EL1, EL2, EL3
,一般情况下,用户态为 EL0
,内核态为 EL1
,EL2
用于管理虚拟机,运行 hypervisor,EL3
用于管理 TrustZone。ARM64
提供了两种栈,一种非特权状态下的栈 sp_el0
,另一种特权状态下的栈 sp_elx
(x 可以是 1,2,3,取决于硬件是否实现了 EL2
,EL3
特权级)。为了简化,这里只讨论硬件只实现了最低要求,即只实现了 EL1
和 EL0
[6]。
在不同的特权级下,通用寄存器 sp
(即栈寄存器) 会引用 sp_el0
或 sp_el1
(即 sp
相当于 sp_el0
或 sp_el1
的别名),ARM64
允许 EL0
使用 sp_el0
,而 EL1
使用 sp_el0
或 sp_el1
(由 PSTATE.SP
控制),分别记作 EL1t
和 EL1h
,由于 Linux 不使用 EL1t
(可以从如下代码中看到,EL1t
的入口都是无效的)所以这里只讨论 EL1h
[7]。
1 |
|
对于 ARM64
而言,当从 EL0
发生中断或异常时,通用寄存器 sp
将自动引用 sp_el1
寄存器,即从用户栈到内核栈的切换是由硬件自动完成的,当从 EL1
退回到 EL0
时,从内核栈到用户栈的切换同样由硬件完成。
异常 / 中断的栈切换
ARM64
Linux 的异常 / 中断处理可以分为 3 个部分:①异常 / 中断处理的准备工作,由 kernel_entry
宏定义。②异常 / 中断的处理。③异常 / 中断 /syscall 的退出,由 kernel_exit
宏定义。因此,对于 ARM64
Linux,异常 / 中断的入口和出口相同。
对于用户态的异常 / 中断处理而言,仅发生 2 次栈切换,即 sp_el0(用户栈) -> sp_el1(内核栈) -> sp_el0(用户栈)
且都由硬件完成,对于内核态的异常 / 中断,则没有进行栈切换。
不同于 x86_64
由硬件自动将发生异常 / 中断时的栈顶指针保存在栈上,ARM64
在发生异常 / 中断时,Linux 会先将之前的栈顶指针保存在 x21
寄存器中,然后再存入内核栈中。
1 |
|
当异常 / 中断处理完后,如果是来自用户态的异常 / 中断,则将栈顶指针从内核栈中取出放入 sp_el0
中,同时将内核栈顶指针恢复到发生异常 / 中断前的值(即内核栈变成空栈),如果是来自内核态的异常中断,则只需要恢复内核栈顶指针即可。
1 |
|
ARM64
Linux 的中断处理主要由 irq_handler
宏定义,需要先切换到中断栈再进行中断的处理。
1 |
|
中断栈的切换通过宏 irq_stack_entry
实现,Linux 会将内核栈顶指针保存在 x19
寄存器中,然后从根据当前栈的高位和内核栈的高位是否相等判断是否正在使用中断栈,如果相等,表示没有使用中断栈,因此需要切换到中断栈,不相等表示已经处于中断栈中,不需要再进行切换了。因此,不同于 x86_64
Linux 通过一个计数器判断是否在使用中断栈,ARM64
Linux 是通过当前栈的高位来判断是否在使用中断栈的。
1 |
|
中断栈的退出通过宏 irq_stack_exit
实现,即加载 x19
的内容,ARM64
Linux 在实现中断处理时,保证在调用 irq_stack_entry
到调用 irq_stack_exit
过程中 x19 的内容不会改变。
1 |
|
因此,对于来自用户的中断处理,需要进行 4 次栈切换:sp_el0(用户栈) -> sp_el1(内核栈) -> sp_el1(中断栈) -> sp_el1(内核栈) -> sp_el0(用户栈)
,对于来自内核的中断,仅进行最多 2 次栈切换:sp_el1(内核栈) -> sp_el1(中断栈) -> sp_el1(内核栈)
。
进程切换时的栈切换
进程切换时,当前进程的内核栈指针将被保存在 task_struct
的 thread
成员的 cpu_context
成员中,并从下个进程的 task_struct
的对应地址处加载内核栈指针,而 sp_el0
将被加载为下一个进程的 task_struct
指针。
1 |
|
RISCV64
的栈切换
RISCV64
有三种模式:Machine Mode
,Supervisor Mode
,User Mode
。一般情况下,Machine Mode
一般用于运行 Supervisor binary interface (SBI),Supervisor Mode
用于运行内核,User Mode
则用于运行用户程序。在 ARM64
中,EL2
和 EL3
并不是必须的,但在 RISCV64
中,Machine Mode
则是一个必须要实现的模式,这是由于 RISCV64
发生中断时,总是会交给 Machine Mode
去判断,只有在 Machine Mode
下通过一定配置将中断委派给 Supervisor Mode
时,运行在 Supervisor Mode
下的内核才有机会处理中断。为了简化,后面假设 Machine Mode
已经将中断全部委派给内核处理,不再讨论 Machine Mode
相关的内容 [8]。
异常 / 中断的栈切换
RISCV64
比 ARM64
更加精简,既没有像 x86_64
那样有自动切换栈的机制,也没有 ARM64
那样存在多个栈寄存器的设计。RISCV64
只有一个栈寄存器 sp
,一切栈切换都是通过软件方式实现的,因此更易于理解。RISCV64
并没有中断栈,只有用户栈与内核栈,初始化与 x86_64
和 ARM64
一致。在 RISCV64
Linux 的中,中断 / 异常的入口是相同的,都以 handle_exception
为入口,由于只有用户栈和内核栈,因此在发生中断 / 异常时,相应的栈切换也非常简单。
1 |
|
当 RISCV64
发生中断 / 异常时,首先根据系统寄存器 CSR_SCRATCH
的值判断中断 / 异常来自于内核还是用户,如果来自内核则恢复 CSR_SCRATCH
的值并将当前的栈指针保存到 task_struct
中,使得后续可以利用相同的方法去处理来自于内核和来自于用户的中断 / 异常。RISCV64
会将发生中断 / 异常前的栈指针保存在 TASK_TI_USER_SP(tp)
中(这里的 TASK_TI_USER_SP
并不一定保存的是用户栈的栈指针,也可能是内核栈的栈指针),在后续处理中,会将发生异常前的栈指针读取出来并保存到栈上,通过这种方式,使得无论中断来自于内核还是来自于用户,都可以从栈上读取发生异常前的栈指针地址。
在中断 / 异常处理结束后,RISCV64
将从栈上恢复中断 / 异常发生前的所有的寄存器的值,并从中断 / 异常返回。
1 |
|
调度时的栈切换
RISCV64
在进程调度时的栈切换与 x86_64
和 ARM64
类似,内核会把栈指针放在当前进程 task_struct
的 thread
成员的 sp
成员中,同时在另一个进程的 task_struct
的对应位置取出新的栈指针。
1 |
|
Discussion
为什么 x86_64
有 trampoline stack?
最初引入 x86 trampoline stack 的 patch 是 [9]。从 LKML 里可以看出来,之所以引入 trampoline stack 是为了实现 KASIER
(现在叫 KPTI
),KASIER
是什么以及引入 KASIER
的原因可以从 [10] 里面找到,这里不再展开。
KPTI
为每个进程分配了两个页表,第一个页表和没有引入 KPTI
之前并没有任何区别,称为原始页表,第二个页表被称为 “shadow” 页表,里面包含了用户地址空间的全部的映射以及需要支持内核运行的最小映射,“shadow” 页表里面不包含内核栈的映射。对于 x86_64
Linux, 如果开启了 KPTI
,在分配顶级页表(即 pgd)的时候,内核会一次性分配 2 个页,一个作为原始页表,一个作为 “shadow” 页表,由于页表分配的时候两个页是 8KB
对齐的,因此,只要知道其中任何一个页表的地址,都可以通过翻转地址的第 12 位来获取另一个页表的地址。
开启 KPTI
后,当运行在内核态时,内核会使用原始页表,而运行在用户态时,内核会使用 “shadow” 页表。由于内核栈不再映射在用户态页表中,对于 x86_64
,当发生中断或异常时,如果 tss.sp0
为内核页表,则硬件在保存 SS
,RSP
等寄存器时,会因为无法从”shadow” 页表中找到对应的映射而触发异常,导致内核崩溃。因此,需要引入一段在”shadow” 页表中有映射的内核内存作为一个临时的栈,才能使得硬件在保存现场时不会触发异常。x86_64
在使用 syscall
指令时,硬件并不会自动切换栈和保存现场,所以在 syscall
的处理流程中并没有切换到 trampoline stack 这一步。虽然 trampoline stack 是由 KPTI
引入的,但无论内核是否开启了 KPTI
,都会使用 trampoline stack,在不开启 KPTI
的情况下,使用 trampoline stack 似乎是一种低效的做法。
不同于 x86_64
只使用 CR3
寄存器来存放页表基地址,ARM64
拥有两个页表基地址寄存器 TTBR0
和 TTBR1
,前者用于存放低一半的虚拟地址空间(即用户地址空间的映射)的页表基地址,后者用于存放高一半地址空间(即内核地址空间的映射)的页表基地址,其用户态和内核态的页表本身是分开的。在使用 KPTI
后,虽然在 EL0
运行时,KPTI
同样会将内核的映射隐藏,但由于 ARM64
在发生中断 / 异常时,硬件并不会向内存里写入内容,因此软件只需要在保存上下文时切换到原始页表后再将上下文写入内核栈即可,并不需要 trampoline stack。此外 ARM8.5 提供了禁止非特权访问由 TTBR0
或 TTBR1
翻译的地址的硬件特性 FEAT_E0PD
,或许可以取代 KPTI
[11]。
为什么要有中断栈?
Linux 很早就加入了中断栈,中断栈至少在 Linux 2.6
的时候就已经存在。中断是一个异步的事件,意味着中断可能在任何时刻发生。早期的 Linux 利用内核栈来同时处理中断,这导致在分配内核栈的内存时,必须考虑到最坏的情况,即内核栈的大小必须大于 $ 内核在没有发生中断的条件下所使用的最大栈空间 + 中断处理所需的最大栈空间 $,否则,内核就会在最坏情况下崩溃,影响内核的稳定性。而内核栈是每个进程都需要的资源,共用内核栈处理中断会导致浪费大量的内存,因而通过分配一个专用于处理中断栈的栈,可以有效降低内核栈所需要分配的内存空间大小。
共用内核栈处理中断只在极端情况下会导致栈溢出,而在用户态发生中断时,内核栈为空栈,由于内核栈和中断栈一样大,此时即便用内核栈处理中断也不会产生栈溢出。因此,x86_64
Linux 从 5.8 开始在发生中断时将不再无条件从内核栈切换到中断栈了,只有中断发生在内核的情况下才会切换到中断栈。
Reference
[1] kernel-stacks.rst - Documentation/x86/kernel-stacks.rst - Linux source code (v5.7-rc5) - Bootlin
[2] Intel® 64 and IA-32 Architectures Software Developer Manuals - Volume 3 - Charpter 6.14.5
[3] System Management Mode - Wikipedia
[4] Intel® 64 and IA-32 Architectures Software Developer Manuals - Volume 2 - Charpter 4.3
[5] Intel® 64 and IA-32 Architectures Software Developer Manuals - Volume 3 - Charpter 6.12.1
[6] Arm Architecture Reference Manual Armv8, for Armv8-A architecture profile - D1.1
[7] Arm Architecture Reference Manual Armv8, for Armv8-A architecture profile - D1.6.2
[8] RISC-V ISA Specification - Volume 2, Privileged Spec v. 20190608
[9] x86 entry-stack and Kaiser series
[10] KAISER: hiding the kernel from user space
[11] Arm Architecture Reference Manual Armv8, for Armv8-A architecture profile - A2.8.1