概述
操作系统中使用段页式内存管理策略的目的是为了更好地访问内存。那段页式内存管理策略究竟是什么样呢?先看一张有关这个策略的图片:
这张图片从左向右看,最开始的cs:ip(也被称为逻辑地址或者偏移地址),与中间的虚拟地址(也被称为线性地址)建立关联,然后将虚拟地址与右边的内存物理地址建立关联。
问题:我们使用内存的目的其实是把程序放入内存中,然后在必要的时候访问内存,读写内存。为什么把程序段放入内存页的过程中插进来一个虚拟地址的映射呢?
解释:老实说操作系统引导程序就是直接把程序段放入指定的物理内存地址中的,中间没有进行虚拟地址的映射,之后在操作系统上建立起的程序在访问内存的时候都会添加一步虚拟地址映射过程。之所以这样是因为操作系统上的程序可以有多个,编写时并不能确定实际的地址在哪里,需要在运行时确定。引入虚拟内存这个概念也是为了屏蔽物理层内存地址的限制,使得程序编写时更加方便。
内存管理之段表
程序段与虚拟地址建立的关联可以记录在一个表中,这个表就被称为段表:
其中段号就是程序段的编号,基址就是关联的虚拟地址,长度就是该段号下程序段的长度,保护就是该程序段的访问属性。
内存管理之页表
虚拟地址与物理内存页建立的关联可以记录在一个表中,这个表就称为页表:
这里的页号是一种逻辑页号,是由虚拟地址提供的,页框号是物理内存的页号,保护是该内存页的可读可写属性,有效是表示该内存页是否有数据。无论是逻辑页还是内存页,它们的每页的大小都是4k。
建立段页式内存管理
从上面讲述可知建立段页式内存管理可分为4大步:分配段,建段表,分配页,建页表。
1、程序在编译后,编译器已经为该程序分了文本段、数据段等程序段。
2、那如何建段表呢?可以结合代码分析:
// linux0.11/kernel/fork.c
int copy_process(int nr,long ebp...)
{...
if (copy_mem(nr,p)) { // 拷贝内存
task[nr] = NULL;
free_page((long) p);
return -EAGAIN;
}...
}
int copy_mem(int nr,struct task_struct * p)
{...
new_data_base = new_code_base = nr * 0x4000000; // 分配段表中的基地址
set_base(p->ldt[1],new_code_base); // 建立代码段的基地址,也就是段表
set_base(p->ldt[2],new_data_base); // 建立数据段的基地址,也就是段表
...
}
从上面代码可以看出在拷贝进程时会进入拷贝内存接口,此时上面的几句代码就建立了段表,建立段表的核心是建立基地址,这个基地址就是虚拟地址。
3、分配页也就是从物理内存分配一页空闲内存出来,不过在linux0.11系统中内存管理采用的是写时复制,所以进程拷贝时,连同父进程的物理内存页也拷贝了进来,所以此时不用分配页,当子进程对内存进行写操作时系统才会向内存申请一块新的空闲内存区,完成分配页。
4、建立页表就是拷贝父进程的页表信息:
// linux-0.11/kernel/fork.c
int copy_mem(int nr,struct task_struct * p)
{...
if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
printk("free_page_tables: from copy_mem\n");
free_page_tables(new_data_base,data_limit);
return -ENOMEM;
}
return 0;
}
// linux-0.11/mm/memroy.c
int copy_page_tables(unsigned long from,unsigned long to,long size)
{
from_dir = (unsigned long *) ((from>>20) & 0xffc); // 获取父进程页目录项指针
to_dir = (unsigned long *) ((to>>20) & 0xffc);// 获取子进程页目录项指针
size = ((unsigned) (size+0x3fffff)) >> 22;
for( ; size-->0 ; from_dir++,to_dir++) {
if (1 & *to_dir)
panic("copy_page_tables: already exist");
if (!(1 & *from_dir))
continue;
from_page_table = (unsigned long *) (0xfffff000 & *from_dir); // 获取父进程的页表
if (!(to_page_table = (unsigned long *) get_free_page())) // 申请一页空闲页存放子进程的页表
return -1; /* Out of memory, see freeing */
*to_dir = ((unsigned long) to_page_table) | 7; // 将子进程页目录项与页表关联
nr = (from==0)?0xA0:1024;
// 拷贝页表信息
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
this_page = *from_page_table;
if (!(1 & this_page))
continue;
this_page &= ~2;
*to_page_table = this_page; // 拷贝父进程页表的信息到子进程的页表中
if (this_page > LOW_MEM) { // 该页如果大于低端内存页的话,将该页的映射数量加1,防止被过度释放
*from_page_table = this_page; // 此时父进程也没有写权限
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
}
invalidate();
return 0;
}
从上述代码注释可以看出子进程的页表是现在内存中获取一页空闲页来存放页表,然后将这个页表对象关联到子进程目录项的指针上,接着拷贝父进程下目录项中的页表到子进程目录项的页表中。所以父的页表与物理页表框的映射信息也被复制到子的中,此时共享读这页内存,双方都没有写权限.
总结
内存管理代码相当复杂,下回着重分析怎样逻辑地址与虚拟地址,虚拟地址与物理地址之间如何映射的。