Linux0.12源码阅读 —— 需求加载和写时复制

参考资料

1.Linux内核完全注释 v5.0修正版

实验环境

bochs模拟x86硬件平台下的Linux0.12操作系统
实验环境地址:http://www.oldlinux.org/Linux.old/bochs/ ,该路径下选择一个Linux0.12版本就可进行实验。

需求加载

当通过fork函数新建一个任务,并调用execve函数加载执行文件后,此时并没有任何实际的执行文件内容被加载到内存。但是,execve函数已经为执行文件的执行做了以下准备:

  • 为执行文件的执行准备好了参数和环境变量

       例如:当我们在命令行上执行一个可执行文件 >./test par1 par2

         参数就是:“./test” “par1” “par2”字符串。这些参数其实就是执行文件./test里的main(int argc,char **argv,char **envp)方法里的第二参数argv所指向的内容,而envp所指向的环境变量是由shell传入的,envp的所指向的数据排列格式和argv是一致的。在栈中的排列如下:

同样,我们执行的脚本文件也一样,只是其执行文件是脚本文件里指定的。例如有个脚本文件,其文件名为shell,其内容如下:   

#!./x_shell abc d e

.........(不关心剩余内容)

其中./x_shell执行文件源码如下,通过gcc -o x_shell x_shell.c编译链接生成:

/* @file x_shell.c */
void main(int argc,char **argv)
{
    char *p;
    for(;p=*argv;argv++)
    {
        printf("%s\n",p);
    }
}

当我们在命令行上执行该脚本文件时> ./shell par1 par2 

结果如下:

Linux0.12源码阅读 —— 需求加载和写时复制_第1张图片

透过打印结果,可以知道传入到main函数的参数顺序是这样的  "x_shell" "abc d e" "./shell" "par1" "par2"。也就是说执行文件是#!后面指定的x_shell而不是shell,shell只是作为一个字符串参数给到x_shell,x_shell对shell文件内容进行解析。这也就是为什么把shell这种文件叫作脚本文件。

  • 设置了执行文件的入口地址,修改了本任务的执行文件i节点为这里execve函数指定的文件i节点
  • 对执行文件对应任务的线性地址空间进行了清理

      主要是将execve函数执行前用到的页面释放,同时将本任务的线性地址空间内所有页目录项和页表项清0。这样本任务继续执行(用户态下执行)时,其所要访问(指令执行,数据读写都是要访问内存的)的页面一定是不存在的,于是会引起页故障(故障:引发故障的指令会被重新执行)。页故障会把引发故障的线性地址存入CR2寄存器,在页故障处理程序中通过CR2判断页故障的类型是缺页还是写保护。由于此时本任务对应的所有页目录项和页表项都被清0了,所以一定是缺页。在缺页处理中,内核会根据该引起异常的线性地址反向推出逻辑地址,再根据该逻辑地址结合可执行文件(当然,还有可能是库文件)的格式得出对应内容在可执行文件内的偏移。于是,根据该偏移把相应的页面加载进内存页,同时把该内存页映射到引发故障的线性地址处。页故障处理到此结束,本任务继续执行,刚才引发故障的指令被重新执行,此时相应的页面已经在内存中,所以不会再引发页故障,程序正常执行。而此时,内存中也仅仅是加载了该引发故障的一页执行文件内容,其余内容也仍旧未加载到内存。那些未加载到的执行文件内容,如果以后没有被访问到,那么将永远不会被加载到内存,如果访问到就会重复上述页故障处理。这样一来就实现了哪里需要,哪里才被加载进内存,也就是需求加载。

  • 其他关于当前任务task_struct结构的相关设置

写时复制

写时复制机制和页面共享有着密切的关系,页面共享(前提:不同任务有着相同的执行文件或库文件)如下:

Linux0.12源码阅读 —— 需求加载和写时复制_第2张图片

其中内核对每一页物理内存页都进行了维护,内核会标记占用每一页内存页的任务数,上图中"2"表明该内存页正被两个任务占用。最为关键的一点是,当内核为任务进行页面共享操作时,会把A任务和B任务中共享同一物理页的线性页标记为"只读"。这样当A或B任何一个任务对该共享页进行写操作时就会引发页故障(故障:引发故障的指令会被重新执行),同时把引发页故障的线性地址存入CR2寄存器。在页故障处理程序中,通过CR2寄存器判断出本次页故障是由写保护引发的而不是缺页,于是内核接着判断相应的物理页是否被共享,如果被共享那么就为该引发页故障的线性地址申请一页空闲物理内存页,并把该物理内存页映射到该故障线性地址处。例如,B任务先进行了写操作:

Linux0.12源码阅读 —— 需求加载和写时复制_第3张图片

这样,A和B就各有一页物理内存页了,接着内核会把原物理页内容复制到新物理页并修改A和B任务对应的线性地址为“读写”。故障处理到此结束,返回后引发故障的指令重新执行,这时相应的线性地址已经是“可写”的状态,因此将不再引发故障,程序正常执行。

你可能感兴趣的:(Linux0.12源码阅读 —— 需求加载和写时复制)