加载中…
  
博文
标签:

杂谈

分类: 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的代码中继续执行了

 
阅读    收藏 
标签:

杂谈

分类: C
 带有默认参数值的虚函数或者纯虚函数
我先从一个简单例子说起:
#include <iostream>
using namespace std;
class CBase
{
public:
    virtual void Test(int iTest = 0) const = 0;
};
class CDerived : public CBase
{
public:
     virtual void Test(int iTest = 1) const
     {
         cout<<"iTest:"<<iTest<<endl;
     }
};
int main()
{
    CBase *p = new CDerived;
    p->Test();   
    delete p;
 
    return 0;
}
运行结果:iTest:0
=============================
刚在一个C++的qq群里有人问为什么不是iTest:1呢?
解释如下:虚函数是动态绑定的(即在运行时),但缺省参数是静态绑定的(即在编译时),默认参数在编译的时候已经写死了,不会动态的。
 
这意味着你最终可能调用的是一个定义在派生类,但使用了基类中的缺省参数值的虚函数。可能大家会说为什么不让缺省参数值被动态绑定呢?答案和运行效率有关。如果缺省参数值被动态绑定,编译器就必须想办法为虚函数在运行时确定合适的缺省值,这将比现在采用的在编译阶段确定缺省值的机制更慢更复杂。做出这种选择是想求得速度上的提高和实现上的简便,所以大家现在才能感受得到程序运行的高效。
阅读    收藏 
(2010-04-23 20:07)
标签:

杂谈

堆栈
在计算机领域,堆栈是一个不容忽视的概念,但是很多人甚至是计算机专业的人也没有明确堆栈其实是两种数据结构。

要点:

堆:顺序随意

栈:先进后出

堆和栈的区别  

一、预备知识—程序的内存分配  

一个由c/C++编译的程序占用的内存分为以下几个部分  

1、栈区(stack)—   由编译器自动分配释放   ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。  

2、堆区(heap)   —   一般由程序员分配释放,   若程序员不释放,程序结束时可能由OS回收   。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。  

3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,   未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。   -   程序结束后有系统释放  

4、文字常量区   —常量字符串就是放在这里的。   程序结束后由系统释放  

5、程序代码区—存放函数体的二进制代码。  

二、例子程序  

这是一个前辈写的,非常详细  

//main.cpp  

int   a   =   0;   全局初始化区  

char   *p1;   全局未初始化区  

main()  

{  

int   b;   栈  

char   s[]   =   "abc ";   栈  

char   *p2;   栈  

char   *p3   =   "123456 ";   123456\0在常量区,p3在栈上。  

static   int   c   =0;   全局(静态)初始化区  

p1   =   (char   *)malloc(10);  

p2   =   (char   *)malloc(20);  

分配得来得10和20字节的区域就在堆区。  

strcpy(p1,   "123456 ");   123456\0放在常量区,编译器可能会将它与p3所指向的 "123456 "优化成一个地方。  

}  

二、堆和栈的理论知识  

2.1申请方式  

stack:  

由系统自动分配。   例如,声明在函数中一个局部变量   int   b;   系统自动在栈中为b开辟空间  

heap:  

需要程序员自己申请,并指明大小,在c中malloc函数  

如p1   =   (char   *)malloc(10);  

在C++中用new运算符  

如p2   =   (char   *)malloc(10);  

但是注意p1、p2本身是在栈中的。  

2.2  

申请后系统的响应  

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。  

堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,  

会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。  
阅读    收藏 
  

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

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

新浪公司 版权所有