【Linux 0.12】Linux内核体系结构——堆栈的使用方法

  • bootsect 是什么?为什么会被移动?

Linux体系结构

堆栈的使用方法

Linux 0.12 系统共使用了 4 种堆栈:

  1. 系统引导初始化时临时使用的堆栈
  2. 进入保护模式之后提供内核程序初始化使用的堆栈
  3. 任务通过系统调用,执行内核程序时的堆栈,称之为任务的内核态堆栈(每个任务都有独立的内核态堆栈)
  4. 任务在用户态执行的堆栈。位于进程逻辑地址空间近末端处

为什么要使用多个堆栈?

  1. 从实模式进入保护模式使得 CPU 对内存寻址访问方式带来了变化,因此需要重新设置栈区域
  2. 为了解决不同 CPU 特权级共享堆栈带来的保护问题,执行 0 级的内核代码和执行 3 级的用户代码需要使用不同的栈。当一个任务进入内核态运行时,就会使用其 TSS 段中给出的特权级 0 的堆栈指针tss.ss0, tss.esp0,即内核栈。原用户指针会被保存在内核栈中,从而当从内核态返回用户态时,能直接回复用户态的堆栈。

初始化阶段

1. 开机初始化时 (bootsect.S, setup.s)

当 bootsect 代码被 ROM BIOS 引导加载到物理内存 0x7c00 处时,并没有设置堆栈段,当然程序也没有使用堆栈。直到 bootsect 被移动到 0x900:0 处时,才把堆栈段寄存器 ss 设置为 0x9000,ESP 设置为 0xff00,参见 boot/bootsect.s 第 61,62 行。

2. 进入保护模式时(head.s)

从 head.s 程序起,系统正式运行在保护模式下。此时堆栈段被设置为内核数据段 0x10,ESP 设置为指向 user_stack 数组的顶端,保留了 1 页内存 作为堆栈使用,参见 head.s 第 31 行。user_stack 数组定义在 sched.c 的67~72 行,共含有 1024 个长字。他在物理内存的位置参见下图。此时该堆栈时内核程序自己使用的堆栈,给出的地址也是大约值,与编译时设置的参数有关。

img

3. 初始化时(main.c)

在 init/main.c 中,执行 move_to_user_mode() 代码将控制权移交给任务 0 之前,系统一直使用上述堆栈。而在执行过 move_to_user_mode() 之后,main.c 的代码被切换成任务 0 中执行。通过执行 fork() 系统调用, main.c 中的 init() 将在任务 1 中执行,并使用任务 1 的堆栈。而 main 本身则在被切换为任务 0 之后,仍然继续使用上述内核程序自己的堆栈作为任务0的用户态堆栈。

任务的堆栈

每个任务都有两个堆栈,分别用于用户态和内核态程序的执行,并且分别称为用户态堆栈和内核态堆栈。除了处于不同 CPU 特权级中,这两个堆栈之间的主要区别在于任务的内核态堆栈很小,所保存的数据量最多不能超过 4096 - 任务数据结构块个字节,大约 3K。而任务的用户态堆栈可以在 64M 的空间延伸。

1. 在用户态运行时

每个任务有自己的 64M 的地址空间。当一个任务被创建时,它的用户态堆栈指针设置在其地址的靠近末端,实际上末端部分还要包括执行程序的参数和环境变量。应用程序在用户态下运行会一直使用这个堆栈。堆栈实际使用的物理内存则由 CPU 分页机制确定。栈内容对于物理内存也是 copy on write。

2. 在内核态运行时

每个人物都有自己的内核态堆栈,用于任务在内核代码中执行期间。其所在的线性地址中的位置由该任务 TSS 段中 ss0 和 esp0 两个字段指定。ss0 是段选择符,esp0 是堆栈栈底指针。每当任务从用户代码转移进入内核代码中执行时,任务的内核态栈总是空的。任务内核态堆栈被设置在位于其任务数据结构所在页面的末端,即与任务的任务数据结构放在同一页面内,参见 kernel/fork.c 第 92 行。

3. 任务 0 和任务 1 的堆栈

任务 0 (空闲进程 idle)和任务 1(初始化进程 init)的堆栈比较特殊,需要特别说明。任务 0 和任务 1 的代码段和数据段相同,限长也都是 640K,但他们被映射到不同的线性地址范围中。任务 0 的段基地址从线性地址 0 开始,而任务 1 的段基地址从 64M 开始。但是他们全部都映射到相同物理地址 0~640K 的范围中。这个范围也就是内核代码和基本数据存放的地方。

在执行了 move_to_user_mode() 之后,任务 0 和任务 1 的内核态堆栈分别位于各自任务数据结构所在页面的末端。而任务 0 的用户态堆栈,就是前面进入保护模式之后使用的堆栈,即 sched.c 的 user_stack[] 数组的位置。由于任务 1 在创建时复制了任务 0 的用户堆栈,因此刚开始时任务 0 和任务 1 共享使用同一个用户堆栈空间。但当任务 1 开始运行时,由于任务 1 映射到 user_stack[] 处的页表项被设定为只读,使得任务 1 在执行堆栈操作时会触发写页面异常,从而内核会使用写时复制技术为任务 1 另行分配主存页面。

任务 0 的内核态堆栈时人工设置的,而它的用户态堆栈是在执行 move_to_user_mode() 时,在模拟 iret 返回之前的堆栈中设置的。

任务内核态堆栈与用户态堆栈之间的切换

在 Linux 0.12 系统中,所有中断服务程序都属于内核代码。如果一个中断产生时任务正在用户代码中执行,那么该终端就会引起 CPU 特权级从 3 级到 0 级的变化,此时 CPU 就会进行用户态堆栈到内核态堆栈的切换操作。CPU 会从当前任务的任务状态段 TSS 中取得新堆栈的段选择符 tss.ss0 和偏移值 。然后,CPU首先将原用户态堆栈指针 ss 和 esp 压入内核态堆栈,然后将 eflags 的内容和 cs:eip 压入堆栈。