Contents

内核堆栈和用户空间堆栈

1. 概述

内核堆栈和用户堆栈是使用堆栈数据结构实现的,它们一起用作调用堆栈 。在本文中,我们将讨论调用堆栈在用户和内核空间中的使用。我们还将简要探讨虚拟内存及其对进程的映射。

2. 堆栈

堆栈是一种 LIFO 数据结构。我们可以执行 push 以将项目添加到堆栈中,并执行 pop 以删除一个项目。堆栈数据结构可用于表达式评估/转换、语法检查、顺序反转、回溯和函数调用。

程序通常由许多函数组成,这些函数又可以调用其他函数。递归(可重入)函数也可以调用自身。无论哪种情况,我们都可以用调用者和被调用者的调用图 来描述程序执行流程  。

当我们跟随程序执行时,我们需要保存状态以在函数之间转移控制。为此,我们使用基于栈数据结构的调用栈。某些与架构相关的约定定义了调用者和被调用者在执行期间保存的状态。

**在调用堆栈中,我们可以找到一系列堆栈帧。**堆栈帧由与特定功能相关的数据组成。它通常包括函数参数、返回地址和本地数据。随着我们深入调用图,分配了更多的帧,这增加了调用堆栈的大小。

3. 内核空间和用户空间

现代系统中的内存不是直接访问的。使用由物理内存支持的虚拟地址空间。从概念上讲,虚拟内存和物理内存分为称为页的块。典型的页面大小为 4096 字节。

那么,这给我们带来了什么?嗯,很多,事实证明。一方面,在进程之间共享内存时内存管理的麻烦更少。这种模型也更安全,因为每个进程都有自己的虚拟内存空间(内存隔离)。它还可以产生几乎无限的内存,因为可以按需提供物理页面的支持(按需分页 )。此外,系统可以将非活动页面交换到硬盘驱动器。有关更多信息,请参阅我们关于管理交换空间 的文章。

虚拟内存空间分为用户空间和内核空间。**内核空间是虚拟内存地址空间的较高部分。**例如,在x86_64架构中,此映射0xffff800000000000开始。

除了内存区域之外,硬件架构还提供了对 I/O 端口和 CPU 指令的限制。例如,在 x86 中,我们有四个保护环  ,编号为 0 到 3,虽然在 Linux 中,我们只使用 ring-0(内核模式)和 ring-3(用户模式)。

**如果用户进程需要受限制的服务,它可以使用系统调用(syscalls )。**这些系统调用共同构成用户应用程序访问内核资源的接口。

4. 用户和内核栈

在用户空间中,我们可以找到向下增长到较低地址的用户堆栈,而动态分配(堆)向上增长到更高地址。用户堆栈仅在进程在用户模式下运行时使用。

**内核栈是内核空间的一部分。因此,它不能从用户进程直接访问。**每当用户进程使用系统调用时,CPU 模式就会切换到内核模式。在系统调用期间,使用正在运行的进程的内核堆栈。

内核堆栈的大小在编译期间配置并保持固定。这通常是每个线程的两个页面 (8KB)。此外,额外的每 CPU中断堆栈 用于处理外部中断。当进程在用户模式下运行时,这些特殊的堆栈没有任何有用的数据。

与内核堆栈不同,我们可以更改用户堆栈:

# ulimit -s
8192
# ulimit -s 32768
# ulimit -s
32768
# ulimit -s unlimited
# ulimit -s
unlimited
#

**使用ulimit 命令,我们可以检查当前限制。*我们可以使用-s选项设置或查看用户堆栈大小(以千字节为单位) 。值ulimited*意味着堆栈大小没有限制。

现在让我们执行一个需要大堆栈空间的自定义程序:

# ulimit -s 
8192
# ./ackermann.x 3 12
Ackermann(3,12) = 32765
# ./ackermann.x 3 15
Segmentation fault (core dumped)
# ulimit -s 1048576
# ulimit -s 
1048576
# ./ackermann.x 3 15
Ackermann(3,15) = 262141
#

我们的自定义程序实现了Ackermann 函数 ,它可以为某些输入使用大量堆栈空间。例如,在此示例中,我们将输入 3 和 15 的堆栈限制增加到 1 GB。