linux内存管理_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。

605804cc549cf91ca4c5e4c4a41e79d7.png

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

e601ab06c394156e8364a720c6368bf1.png

在内核栈先压入任务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(包含空项,代码段,数据段的信息)。还是贴图看看基本上就很清楚了。

c4c57a82bce5e26db7df0172f2d8c9b6.png

1

5c3f9d4b9d0da480595cbea477b4b73f.png

2

ad55b27666676044231925427ecd9d16.png

3

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

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

上面所说的就如下所示:

9d1cc08f144e8c2ec1f5a3d928ad7e54.png

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

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

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

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

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

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

do_wp_page(address)

有一个参数为线性地址。

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

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

un_wp_page(address)

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

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

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

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

写一个函数,命名为:

do_no_page(线性地址address)

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

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

get_empty_page(线性地址address)

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

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

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

int execve(const char * filename, char ** argv, char ** envp);

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

42e8c110a501e70ba9d6fa2bdf95e433.png

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

int do_execve(unsigned long * eip,long tmp,char * filename, char ** argv, char ** envp);

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

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

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

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

void do_no_page(unsigned long error_code,unsigned long address);

第二个参数为线性地址。

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值