[本博客连载并且会持续修正,转载请注明出处:http://blog.chinaunix.net/uid-26583089-id-5730536.html]

    call 08048368 段式映射过程
    C语言“fun();”这样一行调用函数的语句,经过编译链接,就会得到类似“call 08048368”这样一条汇编指令(如果希望深刻理解程序的编译、运行过程,需要学习汇编、elf格式、静态链接、动态链接原理等),CPU执行这条指令时,是如何通过虚拟地址08048368,找到要调用的指令在实际内存中什么位置的呢?

    Linux预备知识(2)“80386段式内存管理机制(保护模式)”已经介绍过虚拟地址→线性地址的映射过程。对于“call 08048368”这条指令,它期望目标在代码段,所以使用CS段寄存器+08048368寻找物理地址。
    内核在创建用户进程时,为切换到该进程时,每个寄存器(regs)该赋的值都做好了准备,其中希望为CS赋__USER_CS,为DS、ES、SS赋__USER_DS。
    include/asm-i386/processor.h,408~417:
    Linux存储管理(2)-LMLPHP

    __KERNEL_CS、__KERNEL_DS、__USER_CS、__USER_DS宏解释:
  Linux存储管理(2)-LMLPHP
   Linux存储管理(2)-LMLPHP

    提前说明一下,Linux内核只使用GDTR,不使用LDTR,所以这些index都对应全局描述符表下标:
    Linux存储管理(2)-LMLPHP

    将第2、3、4、5个描述符展开成二进制得到:
  Linux存储管理(2)-LMLPHP

    回忆Linux预备知识(2)“80386段式内存管理机制(保护模式)”对于段描述符的解释,主要可以得到如下总结:
    __KERNEL_CS、__KERNEL_DS、__USER_CS、__USER_DS都是从0开始长度为0xffffffff的整个段;
    __KERNEL_XX:DPL=0,__USER_XX:DPL=3;
    __XX_CS:type=1010,表示代码段、可读可执行,__XX_DS:type=0010,表示数据段,可读可写。
    
    可以看出,Linux内核对于段描述符的安排,一下子违背了80386的2个期望:80386期望每个进程拥有自己独立的段(即位置不重叠),Linux内核将所有进程安排在同一个段;80386期望每个用户进程用LDTR保存自己独立的描述符表的位置,Linux内核按排所有用户进程使用全局描述符表,并都使用4、5下标处的描述符。
    之所以能“违背”,是因为硬件本身为了兼容问题将段式管理保留下来的,软件并不需要通过它获取到什么“好处”,反而需要做一些应付工作。
    这样,GDT中还剩8180个表项是空闲的。后面学习第3章“中断”的时候,会知道内核还要为每个进程准备一个“TSS段”,描述符也要放全局描述符表里,所以理论上系统中最大的进程数量为4090。

    经过以上分析,虚拟地址0x08048368经过段式映射后,仍然是0x08048368,为了体现它已经经过段式映射,称“线性地址”。

    call 08048368 页式映射过程
    虚拟地址→线性地址映射,只是“敷衍”一下80386,并没有起到实质的映射作用,所以接下来要将线性地址映射为物理地址。

    在切换进程时,内核会设置CR3寄存器为即将切换到的进程的目录表地址:
  Linux存储管理(2)-LMLPHP

    由于进程切换肯定是在内核态完成(第4章“进程与进程调度”),而Linux存储管理(1)已经介绍,每个进程的目录表的后1/4部分,都指向同一份记录内核空间映射关系的页表,所以这一行代码前后,内核所在的空间保持不变,不会有什么问题。

    具体的页式映射过程已经Linux内核预备知识(2)介绍过(当然还有可能涉及到从交换分区换入的过程,但目前不用关心)。
01-19 05:16