Linux 中主要的堆栈有哪些?我的意思是,例如,当发生中断时,将使用什么堆栈,用户进程堆栈和内核进程堆栈之间有什么区别?
答案1
这是高度特定于平台的。除非您绑定到某个平台(甚至 x86-32 和 x86-64 之间的差异也是主要的),否则无法回答这个问题。但是,如果将其限制为 x86,根据您最后的评论,我可以建议一些信息。
从用户态到内核态的服务请求(“系统调用”)有两种主要风格:中断风格和系统输入风格。 (这些术语是我为了描述而发明的。)中断类型的请求是处理器以与外部中断完全相同的方式处理的请求。在 x86 保护模式中,这称为使用int 0x80
(较新的)或lcall 7,0
(最旧的变体,SysV 兼容)并使用所谓的实现门s(任务门、中断门等),配置为特殊段描述符。任务切换由处理器执行。在此切换期间,旧任务寄存器(包括堆栈指针)被存储到旧任务TSS,并且新任务寄存器(包括堆栈指针)从新任务TSS加载。换句话说,全部“通常”寄存器被存储和加载(所以这是一个非常长的动作)。 (FPU/SSE/等状态存在一个单独的问题,更改被推迟 - 有关详细信息,请参阅文档。)
为了处理此类服务请求,内核为每个线程(又名 LWP - 轻量级进程)准备一个单独的堆栈,因为可以在任何可阻止函数调用期间切换线程。这种堆栈通常具有较小的尺寸(例如4KB)。
一旦 x86 任务切换总是更改堆栈指针,就没有机会为内核重用用户态堆栈。另一方面,根本不允许这种重用(除了少量当前线程数据),因为用户进程页面可能不安全:另一个活动线程可以更改甚至取消映射它。这就是为什么简单地禁止使用用户态堆栈在内核中运行,因此,每个线程都应该有不同的用户区和内核区的堆栈;这对于现代的 sysenter 风格的处理来说仍然如此。 (另一方面,正如上面已经提到的,每个线程都应该有一个用于其内核空间的堆栈,而不是另一个线程的堆栈。)
Sysenter 风格的处理是很晚才设计出来的,并通过 SYSENTER 和 SYSCALL 处理器指令实现。它们的不同之处在于,它们在设计时没有考虑到旧的(太严格的)限制,即系统调用应保留所有寄存器。相反,它们的设计更接近于通常的函数调用 ABI,它允许函数可以任意更改某些寄存器(在大多数 ABI 中,这称为“临时”寄存器),仅更改少数寄存器并注意保留旧值由处理程序带来。 SYSENTER/SYSEXIT 指令对(32 位和 64 位)会破坏 RDX 和 RCX 的旧内容(以一种奇怪的方式 - 用户态应使用正确的值预先填充它们),并且新的 RIP 和 RSP 会从各自的 MSR 加载,因此,堆栈是立即切换到内核空间。与此相反,SYSCALL/SYSRET(仅限 64 位)使用 RCX 和 R11 作为返回地址和标志,并执行不是自己改变堆栈。随后,内核利用该堆栈的一部分来保存一些寄存器,然后切换到自己的堆栈,因为 1) 不能保证用户态堆栈足够大来保存所有需要的值,2) 出于安全原因(见上文) 。从这一点开始,我们又拥有了每线程内核堆栈。
除了用户态线程之外,还有许多仅限内核的线程(您可以在ps
输出中看到它们作为方括号内的名称)。每个这样的线程都有自己的堆栈。它们实现 1) 周期性例程,在某些事件或超时时启动,2) 瞬态操作或 3) 处理实际中断处理程序请求的操作。 (对于情况 3,他们在旧内核中命名为“bh”,在新内核中命名为“ksoftirqd”。)这些线程的大部分都附加到单个逻辑 CPU。一旦他们没有用户土地,他们就没有用户土地堆栈。
AFAIK,Linux 中的外部中断处理程序仅限于每个逻辑 CPU 最多同时执行一个处理程序;在此类处理程序执行期间,不允许 IO 中断。 (NMI 是一个可怕的例外,容易出现错误处理。)它们使用任务切换中断门,并且为每个逻辑 CPU 都有一个自己的堆栈,原因与上述相同。
正如已经指出的,其中大部分内容过于特定于 x86。在其他架构中很少看到强制堆栈指针替换的任务切换。例如,ARM32 每个特权级别都有一个堆栈指针,因此,如果在内核态期间出现外部中断,堆栈指针不会更改。
由于内核开发速度很快,这个答案中的一些细节可能会被废弃。仅将其视为一般建议,并根据您将探索的具体版本进行验证。有关 x86 中断处理和任务切换的更多说明,请参阅“Intel® 64 和 IA-32 架构软件开发人员手册第 3A 卷:系统编程指南,第 1 部分”(可在 Intel 网站上免费获取)。