加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

内核栈是干什么用的?TSS作用,进程上下文,精选一个解释

(2011-03-10 21:02:34)
标签:

杂谈

分类: Linux学习
假设一个进程正在用户态下运行,各积存器(eax,ebx,ecx,esi,edi……)的值都是用户态进程的上下文,这时发生了一个中断(如时钟中断或执行了一个系统调用),那么中断处理程序会改这些积存器,当中断执行完后这些积存器怎么恢复? 

靠的是核心栈,中断程序的一开始我们执行一个SAVEALL,把这些积存器保存在核心栈中,中断结束返回前我们执行一个POPALL将保存在栈中的值弹出到各寄存器。 

中断发生时会进行一个栈的切换,会将esp置成tss->esp0,在2.4内核中就是task struct中的thread->esp0,也就是我们所说的核心栈。 

中断完成后返回时也会进行一个栈切换,将esp置成tss->esp,这个是用户栈
。 
以上两个切换都是由硬件完成的。 

也许你认为核心栈在上面这个例子中发挥的作用并不是很大,可以用其他办法来模拟。 

但是中断发生后有可能进行进程切换,想象一下如果有100个进程切来切去是不是就不好管理了,如果每个进程都将有自己的硬件上下文保存在核心栈中,管理起来就方便多了,很容易就能恢复一个进程的执行。
 
TSS:
 
struct tss_struct tss 是进程的任务状态段TSS(Task State Segment)信息结构。在任务从执行中被切换出时tss_struct结构保存了当前处理器的所有寄存器值。当任务又被CPU重新执行时,CPU就会利用这些值恢复到任务被切换出时的状态,并开始执行。
当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换(switch)至另一个进程时,它就需要保存当前进程的所有状态,也即保存当前进程的上下文,以便在再次执行该进程时,能够恢复到切换时的状态执行下去。在Linux中,当前进程上下文均保存在进程的任务数据结构中。在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中断服务结束时能恢复被中断进程的执行。
 
 
4.8.2 任务的堆栈(http://www.ylmf.net/zhuanti/zt02/2010/1108/8811.html)

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

  (1)在用户态运行时

  每个任务(除了任务0和任务1)有自己的64MB地址空间。当一个任务(进程)刚被创建时,它的用户态堆栈指针被设置在其地址空间的靠近末端(64MB顶端)部分。实际上末端部分还要包括执行程序的参数和环境变量,然后才是用户堆栈空间,如图5-24所示。应用程序在用户态下运行时就一直使用这个堆栈。堆栈实际使用的物理内存则由CPU分页机制确定。由于Linux实现了写时复制功能(Copy on Write),因此在进程被创建后,若该进程及其父进程都没有使用堆栈,则两者共享同一堆栈对应的物理内存页面。只有当其中一个进程执行堆栈写操作(如 push操作)时内核内存管理程序才会为写操作进程分配新的内存页面。而进程0和进程1的用户堆栈比较特殊,见后面说明。

  

图5-24 逻辑空间中的用户态堆栈

  (2)在内核态运行时

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

  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结构页面的末端。如图5-25所示。实际上tss.esp0被设置成指向该页面(外)上一字节处(图中堆栈底处)。这是因为 Intel CPU执行堆栈操作时是先递减堆栈指针esp值,然后在esp指针处保存入栈内容。

/uploads/allimg/101108/20391A593-1.jpg

图5-25 进程的内核态堆栈示意图

  为什么从主内存区申请得来的用于保存任务数据结构的一页内存也能被设置成内核数据段中的数据呢,即tss.ss0为什么能被设置成0x10呢?这是因为用户内核态栈仍然属于内核数据空间。我们可以从内核代码段的长度范围来说明。在head.s程序的末端,分别设置了内核代码段和数据段的描述符,段长度都被设置成了16MB。这个长度值是Linux 0.12内核所能支持的最大物理内存长度(参见head.s,110行开始的注释)。因此,内核代码可以寻址到整个物理内存范围中的任何位置,当然也包括主内存区。每当任务执行内核程序而需要使用其内核栈时,CPU就会利用TSS结构把它的内核态堆栈设置成由tss.ss0和tss.esp0这两个值构成。在任务切换时,老任务的内核栈指针esp0不会被保存。对CPU来讲,这两个值是只读的。因此每当一个任务进入内核态执行时,其内核态堆栈总是空的。

  (3)任务0和任务1的堆栈

  任务0(空闲进程idle)和任务1(初始化进程init)的堆栈比较特殊,需要特别予以说明。任务0和任务1的代码段和数据段相同,限长也都是 640KB,但它们被映射到不同的线性地址范围中。任务0的段基地址从线性地址0开始,而任务1的段基地址从64MB开始。但是它们全都映射到物理地址 0~640KB范围中。这个地址范围也就是内核代码和基本数据所存放的地方。在执行了move_to_user_mode()之后,任务0和任务1的内核态堆栈分别位于各自任务数据结构所在页面的末端,而任务0的用户态堆栈就是前面进入保护模式后所使用的堆栈,即sched.c的user_stack[] 数组的位置。由于任务1在创建时复制了任务0的用户堆栈,因此刚开始时任务0和任务1共享使用同一个用户堆栈空间。但是当任务1开始运行时,由于任务1映射到user_stack[]处的页表项被设置成只读,使得任务1在执行堆栈操作时将会引起写页面异常,从而内核会使用写时复制机制(关于写时复制技术的说明请参见第13章)为任务1另行分配主内存区页面作为堆栈空间使用。只有到此时,任务1才开始使用自己独立的用户堆栈内存页面。因此任务0的堆栈需要在任务1实际开始使用之前保持"干净",即任务0此时不能使用堆栈,以确保复制的堆栈页面中不含有任务0的数据。

  任务0的内核态堆栈是在其人工设置的初始化任务数据结构中指定的,而它的用户态堆栈是在执行move_to_user_mode()时,在模拟 iret返回之前的堆栈中设置的,参见图5-22所示。我们知道,当进行特权级会发生变化的控制权转移时,目的代码会使用新特权级的堆栈,而原特权级代码堆栈指针将保留在新堆栈中。因此这里先把任务0用户堆栈指针压入当前处于特权级0的堆栈中,同时把代码指针也压入堆栈,然后执行IRET指令即可实现把控制权从特权级0的代码转移到特权级3的任务0代码中。在这个人工设置内容的堆栈中,原esp值被设置成仍然是user_stack中原来的位置值,而原 ss段选择符被设置成0x17,即设置成用户态局部表LDT中的数据段选择符。然后把任务0代码段选择符0x0f压入堆栈作为栈中原CS段的选择符,把下一条指令的指针作为原EIP压入堆栈。这样,通过执行IRET指令即可"返回"到任务0的代码中继续执行了

 

0

阅读 收藏 喜欢 打印举报/Report
前一篇:list_entry
后一篇:回调函数
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有