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 主内存块可能的最低端(
44 #define PAGING_MEMORY (15*1024*1024) 主内存区最多可以占用
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,}; 主内存块映射数组
页面被占用标志
//
//
//
1MB
)。
15M
。
//
//
mem_map
中每一项的内容表示物理内存被多少个的对象使用,所以对应项为 就表示对应物理内 存
0
页面空闲。
址为
关系
_map
可以看出当内核在定义映射数组
LOW_MEM mem_map
MAP_NR
,
mem_map
的第一项对应于物理内存的地址为
时是以主内存块最大可能大小
LOW_MEM
15M
来定义的,最低起始 地
,所以就有了第 行的映射
mem
46
。而当实际运行时主内存块却不一定是这么大,这就需要根据实际主内存块的大小对
的内容进行调整。对于不是属于实际主内存块的物理内存的对应项清除掉,
linux0.11
采用的做法 是
。这样在作管理时这些不属于主内存块的页面就不会通过主内存块的管理程序被分配出去使用了。
在初始化时将属于实际属于主内存块的物理内存的对应项的值清零,将不属于的置为一个相对较大的 值
USED
/init/main.c
下面就是主内存块初始化的代码:
当系统初启时,启动程序通过
58 #define EXT_MEM_K (*(unsigned short *)0x90002)
BIOS
1M
调用将 以后的扩展内存大小( )读入到内存
KB
0x90002
号单元
main()
中的内容
字节。
+
//
16M
=1Mb
内存大小
物理内存
最大支持
(k)*1024
字节 扩展内存
// linux0.11
下面是系统初始化函数
112 memory_end = (1<<20) + (EXT_MEM_K<<10); //
113 memory_end &= 0xfffff000; 以页面为单位取整。
//
114 if (memory_end > 16*1024*1024)
115 memory_end = 16*1024*1024;
116 if (memory_end > 12*1024*1024) 根据内存大小设置缓冲区末端的位置
117 buffer_memory_end = 4*1024*1024;
118 else if (memory_end > 6*1024*1024)
119 buffer_memory_end = 2*1024*1024;
120 else
121 buffer_memory_end = 1*1024*1024;
122 main_memory_start = buffer_memory_end; //
123 #ifdef RAMDISK 如果定义了虚拟盘,重新设置主内存块起始位置
//
//rs_init()
124 main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
125 #endif
126 mem_init(main_memory_start,memory_end); 初始化主内存块
主内存起始位置 缓冲区末端;
返回虚拟盘的大小
//
=
下面就是
的代码。
mem_init
399 void mem_init(long start_mem, long end_mem)
400 {
401 int i;
402
403 HIGH_MEMORY = end_mem; 设置物理内存最高端。
404 for (i=0 ; i
>= 12; 计算需要初始化的映射项数目
USED
//
//
//
//
//
409 while (end_mem-->0) 将实际主内存块对应的映射项置为 (空闲)
410 mem_map[i++]=0;
411 }
通过以上的操作之后,操作系统便可以了解主内存块中物理内存页面的使用情况了。
//
0
内存的分配与回收
3
分配
当内核本身或者进程需要一页新的物理页面时,内核就要给他分配一个空闲的物理页面。内核需要
查询相关信息,以尽量最优的方案分配一个空闲页面,尤其是在有虚存管理机制的操作系统中对于空 闲
页面的选取方案非常重要,如果选取不当将导致系统抖动。
linux0.11
没有实现虚存管理,也就不用考虑
这些,只需要考虑如何找出一个空闲页面。
知道了内核对主内存块中空闲物理内存页面的映射结构
mem_map
,查找空闲页面的工作就简单了。
只需要在
mem_map
找出一个空闲项,并将该项映射为对应的物理页面地址。算法如下:
mem_map
空闲项;
从最后一项开始查找
算法:get_free_page
输入:无
输出:空闲页面物理地址
{
if(
没有空闲项
renturn 0
(
return
}
将该物理页内容清零
数组下标
)
;
1
对应的物理地址;
将空闲项内容置 ,表示已经被占用;
将空闲项对应的下标转换为对应的物理页面的物理地址=
<<12
+
LOW_MEM)
获取首个 实际上是最后 个 空闲页面,并标记为已使用。如果没有空闲页面,
(
1
:-)
就返回 。
0
的源码如下:
get_free_page
/mm/memory.c
59 /*
60 * Get physical address of first (actually last :-) free page, and mark it
61 * used. If no free pages left, return 0.
62 */
/*
*
*
*/
//
%3
%4
//
63 unsigned long get_free_page(void)
64 {
65 register unsigned long __res asm( "ax");
66
67 __asm__( "std ; repne ; scasb\n\t" 置方向位,将
//
68 "jne 1f\n\t" //
69 "movb $1,1(%%edi)\n\t 将该内存映射项置 。
输入: 与 相同表示 ,初值为 ; 表示直接操作数
0 %2
PAGING PAGES
表示 初值为映射数组最后一项地址
表示 页面起始地址。 即
表示 ,初值为
al(0)
0
%1 %0
;搜索次数
输出:返回
ecx
edi
%0,
eax
eax
(LOW_MEM)
;
mem_map+PAGING_PAGES-1
__res
。
eax
与
(edi)
开始的反相 个字节的内容比较
ecx
如果没有等于 的字节,则跳转结束(返回 )。
//
1
0