段页结合的实际内存管理
如何让操作系统既支持段又支持页?
虚拟内存
:我们让应用程序分为段,然后映射到一段虚拟内存中,再让虚拟内存映射到物理内存中的页中,这样就完成了段和页的结合
段、页同时存在时的重定位(地址翻译)
- 根据段号找到偏移求出虚拟地址
- 根据虚拟地址求出页号找到物理地址
一个实际的段、页式内存管理
内存管理核心:内存分配
- 分配段、建段表:分配页、建页表
- 进程带动内存使用的图谱
- 从进程fork中的内存分配开始
使用内存分为五步
- 分配段
- 建段表
- 分配页
- 建页表
- 重定位
1.分配段、建段表
fork()->sys_fork->copy_process
//在linux/kernel/fork.c中
int copy_process(int nr, long ebp, ...)
{
...
copy_mem(nr, p);
...
}
int copy_mem(int nr, task_struct *p) //nr表示第几个进程,p为PCB
{
unsigned long new_data_base;
new_data_base = nr*0x4000000; // 64M*nr ,这一步完成了虚拟内存的分割
set_base(p->ldt[1], new_data_base);
set_base(p->ldt[2], new_data_base);
/*上面两句完成了段表的初始化,这里将64M的内存既用作代码段又用作数据段,因此基址是相同的*/
}
在上面的程序中,每个进程占64M的虚拟地址空间,互不重叠,因此,他们可以共用一套页表,但是实际中页号是可能重叠的,因此需要有单独的页表
2.分配内存、建页表
int copy_mem(int nr, task_struct *p)
{
unsigned long old_data_base;
old_data_base = get_base(current->ldt[2]);
copy_page_tables(old_data_base, new_data_base, data_limit);
/*子进程与父进程共用一套页表,子进程不需要分配内存,因为父进程已经分配好了,子进程只需要跟父进程拷贝一份相同的页表,传递两个虚拟地址,重新建一个页表,页表内容和父进程相同*/
}
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 long)(size+0x3fffff)>>22;
for(; size-->0; from_dir++,to_dir++)
{
from_page_table=(0xfffff000&from_dir);
to_page_table = get_free_page();
}
}
我们解释一下int copy_page_tables(...)
函数
from_dir = (unsigned long *)((from>>20) & 0xffc);
from
是32位虚拟地址,我们将它右移22位就得到页目录号了,再乘于4,因为一个页目录大小为4个字节,因此相当于右移20位
from_dir
就是一个指向页目录号的指针,接下来,就是根据这个指针找到每一个页号和对应的页框号
for(; size-->0; from_dir++,to_dir++)
{
to_page_table = get_free_page(); /
*分配一个物理内存页来保存页表,就是在mem_map中找一段没有被用过的内存*/
*to_dir = ((unsigned long)to_page_table) | 7;
}
接下来,就是将父进程的页表拷贝到子进程中
// nr = 1024;
for(; nr-->0; from_page_table++; to_page_table++)
{
this_page = *from_page_table;
this_page &=~2; //只读,父进程子进程共享一个页
*to_page_table = this_page;
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >> = 12;
mem_map[this_page]++; //这一页被共享了,当其中一个释放,还有其他的在使用,因此要+1
}
这里如果你想给子进程单独设置一个页,只要调用get_free_page()
得到一个空闲的内存地址,再将其赋给子进程的页表就可以了。
3.重定位
- 通过逻辑地址找到虚拟地址(MMU完成)
- 通过虚拟地址找到物理地址(MMU完成)
在上面的程序中,如果我们对父进程指向p=0x300, *p=7
,那么父进程就会在通过重定位找到物理地址,然后将7写入,然后父进程fork()一个子进程,因为公用的是一套页表,并且将页表置位只读,因此子进程指向p=0x300, *p=8
时,就会重新申请一段内存,修改页表,然后MMU重新计算,然后执行*p = 8
,这样就实现了进程之间的分离。