Linux系统中堆栈的使用方法

本节内容概要描述了Linux内核从开机引导到系统正常运行过程中对堆栈的使用方式。这部分内容的说明与内核代码关系比较密切,可以先跳过。在开始阅读相应代码时再回来仔细研究。

  Linux0.11系统中共使用了四种堆栈。一种是系统初始化时临时使用的堆栈:一种是供内核程序自己使用的堆栈(内核堆栈),只有一个,位于系统地址空间的固定位置,也是后来任务0的用户态堆栈:另一种是每个任务通过系统调用,执行内核程序时使用的堆栈,我们称之为任务的内核态堆栈,每个任务都有自己独立的内核态堆栈;最后一种是任务在用户态执行的堆栈,位于任务(进程)地址空间的末端。下面分别对它们进行说明。

  2.7.1初始化阶段

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

  当bootsect代码被ROM BIOS引导加载到物理内存0x7c00处时,并没有设置堆栈段,当然程序也没有使用堆栈。直到bootsect被移动到0x9000:0处时,才把堆栈段寄存器SS设置为0x9000,堆栈指针esp寄存器设置为0xff00,也即堆栈顶端在0x9000:0xff00处,参见boot/bootsect.s第61,62行。setup.s程序中也沿用了bootsect中设置的堆栈段。这就是系统初始化临时使用的堆栈。

进入保护模式(head.s)

  从head.s程序起,系统开始正式在保护模式下运行。此时堆栈段被设置为内核数据段(0x10),堆栈指针esp设置成指向user_stack数组的顶端(head.s 31行),保留了一页内存(4K)作为堆栈使用。user_stack数组定义在sched.c的67--72行,共含有1024个长字。它在物理内存中的位置可参见下图2-12所示。此时该堆栈时内核程序自己使用的堆栈。

初始化时(main.c)

  在main.c中,在执行move_to_user_mode()代码之前,系统一直使用上述堆栈。而在执行过move_to_user_mode()之后,main.c的代码被"切换"成任务0中执行。通过执行fork()系统调用,main.c中的init()将在任务1中执行,并使用任务1的堆栈。而main()本身则在被"切换"成为任务0后,仍然继续使用上述内核程序自己的堆栈作为任务0的用户态堆栈。关于任务0所使用的堆栈的详细描述见后面说明。

  2.7.2任务的堆栈

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

在用户态运行时

  每个任务(出了任务0)有自己的64MB地址空间。当一个任务(进程)刚被创建时,它的用户态堆栈指针被设置在其地址空间的末端(64MB顶端),在其内核态堆栈则被设置称位于其任务数据结构所在页面的末端。应用程序在用户态下运行时就一直使用这个堆栈。堆栈实际使用的物理内存则由CPU分页机制确定。由于Linux实现了写时复制功能(Copy on Write),因此在进程被创建后,若该进程及其父进程没有使用堆栈,则两者共享同一堆栈对应的物理内存页面。

在内核态运行时

  每个任务有其自己的内核态堆栈,与每个任务的任务数据结构(task_struct)放在同一页面内。这是在建立新任务时,fork()程序在任务tss段的内核级堆栈字段(tss.esp0 和 tss.ss0 )中设置的,参见kernel/fork.c,93行

  p->tss.esp0 = PAGE_SIZE + (long)p:

  p->tss.ss0 = 0x10;

  其中p是新任务的任务数据结构指针,tss时任务状态段结构。内核为新任务申请内存用作保存其task_struct数据结构,而tss结构(段)是task_struct中的一个字段。该任务的内核堆栈段值tss.ss0也被设置成为0x10(即内核数据段),而tss.esp0则指向保存task_struct结构页面的末端。如下图

  为什么通过内存管理程序从主内存区分配的来的用于保存任务数据结构的一页内存也能被设置成内核数据段中的数据呢,也即tss.ss0为什么能被设置成0x10呢?这要从内核代码段的长度范围来说明。在head.s程序的末端,分别设置了内核代码段和数据段的描述符。其中段的长度被设置成了16MB。这个长度值时Linux0.11内核所能支持的最大物理内存长度(参见head.s ,110行开始的注释)。因此,内核代码可以寻址到整个物理内存范围中的任务位置,当然也包括主内存区。到Linux0.98版后内存段的限长被修改成了1GB。

  每当任务执行内核程序而需要使用其内核堆栈时,CPU就会利用TSS结构把它的内核态堆栈设置成由这两个构成。在任务切换时,老人物的内核堆栈指针(esp0)不会被保存。对CPU来讲,这两个值是只读的。因此每当一个任务进入内核态执行时,其内核态堆栈总是空的。

任务0的堆栈

  任务0的堆栈比较特殊,需要别予以说明。

  任务0的代码段和数据段相同,段基地址都是从0开始,限长也都是640KB。这个地址范围也就是内核代码和基本数据所在的地方。在执行了move_to_user_mode()之后,它的内核态堆栈位于其任务数据结构所在页面的末端,而它的用户态堆栈就是前面进入保护模式后所使用的堆栈,也即sched.c的user_stack数组的位置。任务0的内核态堆栈时在其人工设置的初始化任务数据结构中指定的,而它的用户态堆栈时再执行move_to_user_mode()时,在模拟iret返回之前的堆栈中设置的。在堆栈中,esp仍然是user_stack中原来的位置,而ss被设置成0x17,也即用户态局部表中的数据段,也即从内存地址0开始并且限长640KB的段。

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

  任务调用系统调用时就会进入内核,执行内核代码。此时内核代码就会使用该任务的内核态堆栈进行操作。当进入内核程序时,由于优先级别发生了改变(从用户态转到内核态),用户态堆栈的堆栈段和堆栈指针以及eflags会被保存在任务的内核态堆栈中。而在执行iret退出内核程序返回到用户程序时,将恢复用户态的堆栈和eflags。见下图

posted on 2011-07-21 20:44  不知道  阅读(6516)  评论(0)    收藏  举报

导航