Linux0.11 下的内存管理
作者:袁镱
robertyi@163.com
QQ:30131195
愿借此结交广大 linux 爱好者
1 如何在保护模式下实现对物理内存的管理
保护模式在硬件上为实现虚拟存储创造了条件,但是内存的管理还是要由软件来做。操作系统作为
资源的管理者,当然要对内存的管理就要由他来做了。
在 386 保护模式下,对任何一个物理地址的访问都要通过页目录表和页表的映射机制来间接访问,
而程序提供的任何地址信息都会被当成线性地址进行映射,这就使得地址提供者不知道他所提供的线性
地址最后被映射到了哪个具体的物理地址单元。这样的措施使得用户程序不能随意地操作物理内存,提
高了系统的安全性,但是也给操作系统管理物理内存造成了障碍。而操作系统必须要了解物理内存的使
用情况才谈得上管理。
要能够在保护模式下感知物理内存,也就是说要能够避开保护模式下线性地址的影响,直接对物理
内存进行操作。如何避开呢?正如前面所说:在保护模式下对任何一个物理地址的访问都要通过对线性
地址的映射来实现。
不可能绕过这个映射机制,那只有让他对内核失效。如果让内核使用的线性地址和物理地址重合,比如:
当内核使用 0x0000 1000 这个线性地址时访问到的就是物理内存中的 0x00001000 单元。问题不就解决
了吗!linux0.11 中采用的正是这种方法。
在进入保护模式之前,要初始化页目录表和页表,以供在切换到保护模式之后使用,要实现内核线
性地址和物理地址的重合,必须要在这个时候在页目录表和页表上做文章。
在看代码之前首先说明几点:
由于 linus 当时编写程序时使用的机器只有 16M 的内存,所以程序中也只处理了 16M 物理内存的
情况,而且只考虑了 4G 线性空间的情况。一个页表可以寻址 4M 的物理空间,所以只需要 4 个页表,
一个页目录表可以寻址 4G 的线性空间,所以只需要 1 个页目录表。
程序将页目录表放在物理地址_pg_dir=0x0000 处,4 个页表分别放在 pg0=0x1000, pg1=0x2000,
pg2=0x3000, pg3=0x4000 处
下面是最核心的几行代码:在 linux/boot/head.s 中
首先对 5 页内存清零
198 setup_paging:
199 movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */
#设置填充次数 ecx=1024*5
200 xorl %eax,%eax #设置填充到内存单元中的数 eax=0
201 xorl %edi,%edi /* pg_dir is at 0x000 */
#设置填充的起始地址 0,也是页目录表的起始位置
202 cld;rep;stosl
下面填写页目录表的页目录项
对于 4 个页目录项,将属性设置为用户可读写,存在于物理内存,所以页目录项的低 12 位是 0000 0000
0111B
以第一个页目录项为例,$ pg0+7=0x0000 1007
表示第一个页表的物理地址是 0x0000 1007&0xffff f000=0x0000 1000;
权限是 0x0000 1007&0x0000 0fff=0x0000 0007
203 movl $pg0+7,_pg_dir /* set present bit/user r/w */
204 movl $pg1+7,_pg_dir+4 /* --------- " " --------- */
205 movl $pg2+7,_pg_dir+8 /* --------- " " --------- */
206 movl $pg3+7,_pg_dir+12 /* --------- " " --------- */
接着便是对页表的设置:
4 个页表×1024 个页表项×每个页表项寻址 4K 物理空间:4*1024*4*1024=16M
每个页表项的内容是:当前项所映射的物理内存地址 + 该页的权限
其中该页的属性仍然是用户可读写,存在于物理内存,即 0x0000 0007
具体的操作是从 16M 物理空间的最后一个页面开始逆序填写页表项:
最后一个页面的起始物理地址是 0x0xfff000,加上权限位便是 0x fff007,以后每减 0x1000(一个页面
的大小)便是下一个要填写的页表项的内容。
207 movl $pg3+4092,%edi # edi 指向第四个页表的最后一项 4096-4。
208 movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
#把第四个页表的最后一项的内容放进 eax
209 std # 置方向位,edi 值以 4 字节的速度递减。
210 1: stosl /* fill pages backwards - more efficient :-) */
211 subl $0x1000,%eax # 每填写好一项,物理地址值减 0x1000。
212 jge 1b # 如果 eax 小于 0 则说明全填写好了。
# 使页目录表基址寄存器 cr3 指向页目录表。
213 xorl %eax,%eax /* pg_dir is at 0x0000 */
令 eax=0x0000 0000(页目录表基址)
214 movl %eax,%cr3 /* cr3 - page directory start */
# 设置 cr0 的 PG 标志(位 31),启动保护模式
215 movl %cr0,%eax
216 orl $0x80000000,%eax # 添上 PG 标志位。
217 movl %eax,%cr0 /* set paging (PG) bit */
在分析完这段代码之后,应该对初始化后的页目录表和页表有了一个大概的了解了,当这段代
码运行完后内存中的映射关系应该如图所示:
接下来将内核代码段描述符 gdt 设置为
0x00c09a0000000fff /* 16Mb */ # 代码段最大长度 16M。
这样线性地址就和物理地址重合了。
下面用两个例子验证一下:
(1) 要寻找 pg_dir 的第 15 项的内容
这个地址应该是在页目录表的(15-1)*4=0x38 位置,把它写成 32 为地址使 0x0000 0038,当内
核使用这个地址时,仍然要通过映射:首先取高 10 位,0000 0000 00B,根据 203 行的代码,
页目录表第 0 项的内容是$pg0+7,得到页表地址是 pg0=0x0000 1000,CPU 将用这个地址加上偏
移量找到对应的页表项,偏移量=线性地址中间 10 位*4=0,根据 203~221 行执行的结果,
在 pg0 中偏移量为 0 的页表项为 0x0000 0007, CPU 得到页表地址是 0x0000 0000 加上线性地址
的最后 12 位,将找到 0x0000 0038 单元的内容。
(2)寻找任意物理单元 0x00f5 9f50
与第一个例子一样,用这个地址作为线性地址寻址,先用高 10 位寻找页表,页目录表第 0000
0000 11B 项指向 pg3,根据线性地址中间 10 位 11 0101 1001B 寻找页表项,pg3 的第 11 0101
1001B 应该是 0x00f5 9007,
取得页表基址 0x00f5 9000,加上页内偏移量 0x f50,最后得到的就是物理地址 0x00f5 9f50 的
内容。
从上面两个例子可以看出:内核中使用的线性地址实际上已经是物理地址,这样从现象上
看 386 的地址映射机制对内核失效了:-)
明白了这一点之后,对后面内存管理方面的的分析就容易得多了
内存初始化
2
当操作系统启动前期实现对于物理内存感知之后,接下来要做的就是对物理内存的管理,要合理的
使用。对于 这样一个操作系统而言,内存有以下一些使用:面向进程,要分配给进程用于执行所必
Linux
要的内存空间;面向文件系统,要为文件缓冲机制提供缓冲区,同时也要为虚拟盘机制提供必要的空
间。这三种对于内存的使用相对独立,要实现这一些,就决定了物理内存在使用时需要进行划分,而最
简单的方式就是分块,将内存划分为不同的块,各个块之间各司其职,互不干扰。 中就是这样
linux0.11
作的。
将内存分为内核程序、高速缓冲、虚拟盘、主内存四个部分(黑色部分是页目录表、几个
Linux0.11
页表,全局描述符表,局部描述符表。一般将他们看作内核的一部分)。为什么要为内核程序单独划出一
个块来呢?主要是为了实现上简单。操作系统作为整个计算机资源的管理者,内核程序起着主要的作
用,它的代码在操作系统运行时会经常被调用,需要常驻内存。所以将这部分代码与一般进程所使用的
空间区分开,为他们专门化出一块内存区域。专门划出一块区域还有一个好处,对于内核程序来说,对
于自己的的管理就简单了,内核不用对自己代码进行管理。比如:当内核要执行一个系统调用时,发现
相应的代码没有在内存,就必须调用相关的内核代码去将这个系统调用的代码加载到内存,在这个过程
中,有可能出现再次被调用的相关内核代码不在内存中的情况,最后就可能会导致系统崩溃。操作系统
为了避免这种情况,在内核的设计上就变得复杂了。如果将内核代码专门划一个块出来,将内核代码全
部载入这个块保护起来,就不会出现上面讲的情况了。
在 中内存管理主要是对主内存块的管理。
linux0.11
要实现对于这一块的管理,内核就必须对这一块中的每一个物理页面的状态很清楚。一个物理页面
应该有以下基本情况:是否被分配,对于它的存取权限(可读、可写),是否被访问过 是否被写过,被
,
多少个不同对象使用。对于 来说,后面几个情况可以通过物理页面的页表项的 、 、 三项
linux0.11 D A XW
得到,所以对于是否被分配,被多少个对象使用就必须要由内核建立相关数据结构来记录。在
linux0.11
定义了一个字符数组 用于对主内存区的页面分配和共享信息进行记录。
mem_map [ PAGING_PAGES ]
以下代码均在 中
/mm/memory.c
43 #define LOW_MEM 0x100000
主内存块可能的最低端( )。
// 1MB
44 #define PAGING_MEMORY (15*1024*1024)
主内存区最多可以占用 。
// 15M
45 #define PAGING_PAGES (PAGING_MEMORY>>12)
主内存块最多可以占用的物理页面数
//
46 #define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
将指定物理内存地址映射为映射数组标号。
//
页面被占用标志
47 #define USED 100 //
57 static unsigned char mem_map [ PAGING_PAGES ] = {0,};
主内存块映射数组
//
中每一项的内容表示物理内存被多少个的对象使用,所以对应项为 就表示对应物理内存
mem_map 0