linux-0.11之内存管理思路并不难

内存管理的内容牵涉面比较广,它会联系到文件系统,进程等等。因此,写内存管理也脱离不了它们。这里想到哪里就写到哪里,因为linux-0.11内存管理掌握起来挺容易的。

先从内存管理与进程之间的关系来说。


前面有一篇文章专门写到进程,里面涉及到了gdt,ldt,tss等。先说该怎么设计它们的存放位置。


在gdt表按如下方式组织:0(位置空),内核代码段,内核数据段,系统段,tss0,ldt0,tss1,ldt1,...。换成寻址则为:0x7,0xf,0x17,0x1f,0x27...。


内核代码段和数据段的限长为16MB,基地址为0。


手工建立一个任务,称为任务0,对应tss0与ldt0,其代码与数据的限长为640KB。然后

它的内核栈可以定义一个数组,其代码段与数据段与内核共享,值分别为0x10与0x17,

在内核程序启动的时候也会有一个栈,任务0的用户栈可以与它共享。之后的按照下表tss格式定义出tss0。


linux-0.11之内存管理思路并不难

然后是从内核切换到任务0,需要按照下图所示:


linux-0.11之内存管理思路并不难


在内核栈先压入任务0的ss(内核ss),任务0的esp(内核esp),eflags,任务0的cs(内核cs),任务0的eip(内核eip),然后执行iret就可以恢复出任务0,让任务0按照eip继续执行下去。

这样就能让任务跳到应用态了。


任务0由于代码段,数据段等都与内核共享,所以,页目录与页表以及代码段数据段都不需要有拷贝操作。


那谈内存管理就从任务1开始。


任务1还是从任务0拷贝而来,但是它的拷贝操作与任务0拷贝内核代码有所不同。


任务1也需要设置ldt1,tss1,这不用多说,而增加内容则是需要拷贝任务0的页目录与页表项,然后还需要映射到页目录中。


怎么去操作这个过程?


1.把任务0的代码段与数据段的基地址与限长都取出来,做正确性检测。

2.任务1的线性地址设置为64MB,以后有n个任务,它的排序为n*64MB,然后将任务0的页目录与页表拷贝到这个任务1的64MB开始位置上。(从这里开始涉及到内存管理了)

3.如何拷贝?

计算拷贝的起始地址,也就是实际的线性地址x>>22,它就是页目录的索引,然后需要<<2,就变成页目录的实际地址,也就是x>>20。目的地址也是如此。总的来说,页目录有1024项,能索引4GB范围勒。


再计算需要拷贝多少个页目录内容,也就是多少个页表的内容。


由于刚开始只使用页目录项的4项,所以也需要检测拷贝目的剩余目录项中是否页表存在标志。之后则是进入页表处,由于源页表肯定存在了,目的页表则不存在,所以需要创建页表(这也需要内存管理①)。之后就是将源页表内容拷贝到目的页表中。


再之后,就需要将源页表与目的页表都设置为只读。


从上面可以看出,暂时就不需要拷贝任务0的代码和数据到任务1上。因为拷贝的页表内容自然指向了任务1的程序与数据,做到了天然共享。


4.怎么找到任务1执行?

tss1中的ldt1的代码段,数据段设置为新的地址的数据段和代码段与相应的基址。堆栈段设置为与任务0相同。任务开始运行,系统根据tss中的ldt序号找到存在与gdt中的ldt值,其包含了描述具体的ldt(包含空项,代码段,数据段的信息)。还是贴图看看基本上就很清楚了。


linux-0.11之内存管理思路并不难

1


linux-0.11之内存管理思路并不难

2


linux-0.11之内存管理思路并不难

3


所以,任务1就加载到基地址为64MB*1的位置上。例如代码执行就变成64MB:eip。


这种线性地址就会被转换,然后找到页目录与对应的页表项。然后发现与内核的代码与数据是对应的,所以也就是共享的。


上面所说的就如下所示:


linux-0.11之内存管理思路并不难

所以,任务1和任务0的页目录与页表不同,但是它们的代码与数据是共享一套的。


因此,如果任务1对数据有写操作,为了不对任务0有影响,故需要创建新的页,而不能再共享了。


要想一个问题,要保护的应该是共享的代码和数据,为什么要保护的却是页面呢?

这是因为页面映射的物理空间对应到的就是代码和数据,所以如果对数据进行了写操作,也能对应到页面上。之后对要操作的数据的物理地址再复制一份,然后页面再指向新的物理地址,这就完成了写时复制的整个过程。


在硬件上,前面将页面设置为只读模式,后续如果有写操作,硬件需要产生中断,并且需要告知软件产生中断的地址,这样软件就好做出复制操作。


可以写一个函数,命名为:

<code>do_wp_page(address)/<code>

有一个参数为线性地址。


从线性地址可以找到物理地址。方法为从页目录中找到页面的基址。然后再从线性地址从找到页面的偏移值。


这样再写一个函数,命名为:

<code>un_wp_page(address)/<code>

这个参数为页面地址,指向的值为物理地址。


它要做的就是上面说的拷贝物理页面的工作(就是实际内核代码和数据的存储位置),然后将页面设置为可读写的。


讲完上面的内容,只是内存管理(写时复制)的一部分,下面还要说一种情况,那就是缺页了。


例如,一个应用程序设置的栈过大。一般页面存储的栈空间都是按照4KB存储的,如果发生这种情况,则由于栈空间不足而要继续扩充空间,所以就会发生缺页的情况。


写一个函数,命名为:

<code>do_no_page(线性地址address)/<code>

由于需要的是多增加一个页面存储栈空间,所以只需要增加一个页表和物理页面然后增加它们之间的映射(物理地址与页表映射)就可以了。


所以,定义一个函数,命名为:

<code>get_empty_page(线性地址address)/<code>

它首先就是获取物理地址,可以将实现这个过程的函数命名为get_free_page(),然后实现物理地址与线性地址的映射,命名为函数:put_page(物理地址tmp,线性地址address),它首先从线性地址中得到页目录,查看页目录对应的页表是否存在,如果存在,则将物理地址跟页表对应的项关联就可以了。如果不存在,就需要重新申请一个4KB的页表,页目录指向页表地址,页表再跟物理地址对应就好。


还有一种情况,例如:进程加载程序,也就是进程2执行shell程序:

编写一个函数,应用层名为:

<code>int execve(const char * filename, char ** argv, char ** envp);/<code>

它需要能陷入内核,陷入内核之后能执行到sys_execve,在它里面要有能力修改程序的指向,让它能执行一个新的程序,所以要保证系统调用返回后,eip要被修改。那可以按照下面这张图来做:


linux-0.11之内存管理思路并不难

在栈底最后位置填入硬件最先保存的eip位置值(指针值),然后再编写一个函数,命名为:

<code>int do_execve(unsigned long * eip,long tmp,char * filename, char ** argv, char ** envp);/<code>

这样参数eip就指向了最先的eip位置,修改它的值就能修改程序退出系统之后的执行位置。

还要修改esp值,这个函数还要实现文件节点的获取。


所以,这样就能执行新的程序,但是,新的程序并没有代码在内存中,所以就会产生缺页异常了,也就引出内存管理之缺页异常。


写一个函数,来管理缺页异常。命名为:

<code>void do_no_page(unsigned long error_code,unsigned long address);/<code>

第二个参数为线性地址。

如果该函数要实现的简单,则直接根据上面的文件节点找到文件内容,然后加载到物理内存中,之后使用put_page()完成物理地址和线性地址的映射,就完成了1个页面的数据加载了。


如果设计的复杂与合理,则可以找该线性地址对应的空间是否有被已经被加载,如果被加载了,则可以直接使用了,而不需要再加载这段内容了。


说完以上内容就是类似linux-0.11的内存管理了。这样看起来linux-0.11操作系统的内存管理思路比一般嵌入式rtos操作系统的内存管理要简单很多的。


分享到:


相關文章: