操作系统内存地址转换

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 就表示对应物理内存
页面空闲。
可以看出当内核在定义映射数组时是以主内存块最大可能大小mem_map 15M 来定义的,最低起始地
址为LOW_MEM,mem_map 的第一项对应于物理内存的地址为LOW_MEM,所以就有了第46 行的映射
关系MAP_NR。而当实际运行时主内存块却不一定是这么大,这就需要根据实际主内存块的大小对mem
_map 的内容进行调整。对于不是属于实际主内存块的物理内存的对应项清除掉,linux0.11 采用的做法是
在初始化时将属于实际属于主内存块的物理内存的对应项的值清零,将不属于的置为一个相对较大的值
USED。这样在作管理时这些不属于主内存块的页面就不会通过主内存块的管理程序被分配出去使用了。
下面就是主内存块初始化的代码:
/init/main.c
当系统初启时,启动程序通过BIOS 调用将1M 以后的扩展内存大小(KB)读入到内存0x90002 号单元
58 #define EXT_MEM_K (*(unsigned short *)0x90002)
下面是系统初始化函数main() 中的内容
112 memory_end = (1<<20) + (EXT_MEM_K<<10); // 内存大小=1Mb 字节+ 扩展内存(k)*1024 字节。
113 memory_end &= 0xfffff000; // 以页面为单位取整。
114 if (memory_end > 16*1024*1024) // linux0.11 最大支持16M 物理内存
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<PAGING_PAGES ; i++) // 将主内存块映射数组所有项置为USED
405 mem_map[i] = USED;
406 i = MAP_NR(start_mem); // 计算实际主内存块物理地址起始位置对应的映射项
407 end_mem - = start_mem; // 计算实际主内存块大小
408 end_mem >>= 12; // 计算需要初始化的映射项数目
409 while (end_mem-->0) 将实际主内存块对// 应的映射项置为0(空闲)
410 mem_map[i++]=0;
411 }
通过以上的操作之后,操作系统便可以了解主内存块中物理内存页面的使用情况了。
3 内存的分配与回收
分配
当内核本身或者进程需要一页新的物理页面时,内核就要给他分配一个空闲的物理页面。内核需要
查询相关信息,以尽量最优的方案分配一个空闲页面,尤其是在有虚存管理机制的操作系统中对于空闲
页面的选取方案非常重要,如果选取不当将导致系统抖动。linux0.11 没有实现虚存管理,也就不用考虑
这些,只需要考虑如何找出一个空闲页面。
知道了内核对主内存块中空闲物理内存页面的映射结构mem_map,查找空闲页面的工作就简单了。
只需要在mem_map 找出一个空闲项,并将该项映射为对应的物理页面地址。算法如下:
算法:get_free_page
输入:无
输出:空闲页面物理地址
{
从最后一项开始查找mem_map 空闲项;
if( 没有空闲项)
renturn 0;
将空闲项内容置1,表示已经被占用;
将空闲项对应的下标转换为对应的物理页面的物理地址=
( 数组下标<<12 + LOW_MEM)
将该物理页内容清零
return 对应的物理地址;
}
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 */
/*
* 获取首个(实际上是最后1 个:-) 空闲页面,并标记为已使用。如果没有空闲页面,
* 就返回0。
*/
// 输入:%1与%0 相同表示eax,初值为0;%2 表示直接操作数(LOW_MEM);
%3 表示ecx,初值为PAGING PAGES;搜索次数
%4 表示edi 初值为映射数组最后一项地址mem_map+PAGING_PAGES-1。
// 输出:返回%0, 表示eax 页面起始地址。eax 即__res
63 unsigned long get_free_page(void)
64 {
65 register unsigned long __res asm( "ax");
66
67 __asm__( "std ; repne ; scasbnt" // 置方向位,将al(0) 与(edi) 开始的反相ecx 个字节的内容比较
68 "jne 1fnt" // 如果没有等于0 的字节,则跳转结束(返回0)。
69 "movb $1,1(%%edi)nt // 将该内存映射项置1。
70 "sall $12,%%ecxnt" 相对于// LOW_MEM 的页面起始地址。
71 "addl %2,%%ecxnt" // 加上LOW_MEM = > 页面实际物理起始地址。
72 "movl %%ecx,%%edxnt" // 保存页面实际物理起始地址。
73 "movl $1024,%%ecxnt" // 置计数值1024
74 "leal 4092(%%edx),%%edint" // 使edi 指向该物理页末端
75 "rep ; stoslnt" // 沿反方向将该页清零。
76 "movl %%edx,%%eaxn" // 将页面实际物理起始地址放入eax(返回值)。
77 "1:"
78 : "=a" (__res)
79 : "" (0), "i" (LOW_MEM), "c" (PAGING_PAGES),
80 "D" (mem_map+PAGING_PAGES-1)
81 : "di", "cx", "dx");
82 return __res; // 返回空闲页面实际物理起始地址(如果无空闲也则返回0)。
83 }
84
这个函数返回的只是物理页面的物理地址,下一节将具体讲如何将物理地址映射为线性地址。
回收:
当内核使用完一个物理页面或者进程退出时内核归还申请了的物理页面。这时就需要更改相应的信
息,以便下一次使用。在归还页面时可能会出现下面几种情况:
1)页面物理地址低于主内存块可能的最低端,这种情况不需要处理直接退出,因为这部分内存空
间被用于内核程序和缓冲,没有作为分配页面的内存空间。还有一种情况会出现这种情况,当内存操作
失败时,会调用回收页面过程回收已经分配了的物理页,如果因为内存分配失败造成的,就不需要真正
的回收操作,调用回收过程时会以0 为输入参数。
2)页面物理地址高于实际物理内存最高地址。这种情况是不允许的,内核将使调用对象进入死循
环,这是一种简单而有效的方法,因为这种情况要判断出错原因是很困难的。
3)调用对象试图释放一块空闲物理内存。出现这种情况可能是因为多个对象共享该物理页,在释
放时出现了重复释放。比如:进程A、B共享物理页170,由于系统的原因A将该页释放了两次,当B
释放该页时就会出现这种情况。这种情况也是不允许的,一般意味着内核出错,内核将使调用对象进入
死循环以避免错误扩散。
4)要释放的页面正确。因为可能是共享内存,所以要将该页对应的映射项的值减1,表示减少了
一个引用对象。如果引用数减到0了,并不对物理页面的内容清0,等到被分配时再做,因为可能这个页
面不会在被使用,同时在分配时用汇编代码来做效率会很高。
这样下面的代码就很好理解了:
85 /*
86 * Free a page of memory at physical address 'addr'. Used by
87 * 'free_page_tables()'
88 */
/*
* 释放物理地址'addr' 开始的一页内存。用于函数'free_page_tables()'。
*/
89 void free_page(unsigned long addr)
90 {
91 if (addr < LOW_MEM) return; 如果物理地址小于// addr 主内存块可能的最低端,则返回。
92 if (addr >= HIGH_MEMORY)
// 如果物理地址addr>= 实际内存大小,则显示出错信息,调用对象死机。
93 panic( "trying to free nonexistent page");
94 addr - = LOW_MEM; // 将物理地址换算为对应的内存映射数组下标。
95 addr >>= 12;
96 if (mem_map[addr]--) return; // 如果对应内存映射数组项不等于0,则减1,返回
97 mem_map[addr]=0; // 否则置对应映射项为0,并显示出错信息,调用对象死机。
98 panic( "trying to free free page");
99 }
100

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值