【README】
1.本文内容总结自 B站 《操作系统-哈工大李治军老师》,内容非常棒,墙裂推荐;
2.段与页
- 段: 用户程序采用分段结构;
- 页: 操作系统采用分页机制管理物理内存;
- 段页结合:程序员希望用段,物理内存希望用页,所以操作系统需要把段与页结合起来管理内存;
3.段页各自工作机制
问题:如何把 分段式内存管理 与 分页式内存管理 联系起来?
【1】虚拟内存
1)虚拟内存定义
- 操作系统用程序实现了虚拟内存,把段与页结合起来;如下图所示。
【图解】
- 从用户视角,程序分段存储;
- 从物理内存视角,内存按照分页管理;
2)段页同时存在:段面向用户/页面向硬件
【图解】
用户程序分段存储在虚拟内存,包括数据段,代码段等;
以用户代码段为例:
把用户代码段分为3个内存页进行存储,其中某段的虚拟地址为0x00345008,映射的物理地址是 0x7008;
补充:
- 虚拟内存的代码段基址存储在cs段寄存器中,偏移量存储在ip寄存器中;
【2】 段页同时存在时的重定位(地址翻译)
0)背景
- 上述介绍了通过分段,每段分页把程序存储到多个内存页;
- 为了让用户使用内存,还应该提供段页同时存在时的重定位功能(逻辑地址翻译为物理地址的功能)
1)段页同时存在时的重定位(重定位也称为地址翻译,逻辑地址翻译为物理地址)
【图解】
- 步骤1:获取程序逻辑地址,结构为 段号+偏移,分别用cs : ip 存储段号,偏移量;
- 步骤2:根据段号从段表中查询段基址,这个段基址是虚拟地址;
- 步骤3:根据虚拟地址计算出页号(如虚拟地址0x1756的页号为17);
- 步骤4:根据页号从页表中查询出页框号(物理页号),如页框号为3;
- 步骤5:根据页框号(物理页号)和页内偏移计算出物理内存地址,如0x0356;至此完成了逻辑地址到物理内存地址的翻译;
最后:操作系统把物理内存地址送入地址总线读写该内存地址上的数据;
【小结】段页同时存在时的重定位(地址翻译)
- 从逻辑地址映射到虚拟地址,虚拟地址映射到物理地址的过程;
- 操作系统基于以上2层地址映射,实现既面向用户支持分段,又面向物理内存支持分页管理;
- 所以构成了段页结合的内存管理模式,其核心在于虚拟内存;
【3】 段页时结合的内存管理示例
内存管理的核心就是内存分配,所以从程序放入内存,使用内存开始(使用内存就是取指执行);
【3.1】段页时内存管理具体措施
1)具体措施
- 分配段,建立段表;
- 分配页,建立页表;
- 进程带动内存使用的图谱(给进程分配了内存后,进程中的程序就可以放入内存);
- 从进程fork中的内存分配开始;(进程fork,创建一个新进程,也就需要为新进程分配内存空间,使得程序中的逻辑地址能够正确翻译到物理地址)
2)段页式内存下程序如何载入内存?
- 步骤1:建立段表:把用户程序段(如代码段,数据段)与虚拟地址中的段映射起来;如在虚拟内存上分割一段区域分配给用户数据段,以此建立映射关系,即建立段表;
- 如何分割? 采用分区算法对虚拟内存进行分割(只需要存储分区基址和长度);
- 步骤2:建立页表:把虚拟地址中的段分割为若干逻辑页,如3个逻辑页;3个逻辑页与物理内存的3个物理页进行映射,即建立页表;
补充:虚拟内存是不存在的,是操作系统用程序实现的;
3)段页式内存下程序载入内存的示意图如下:
【图解】
虚拟地址段表结构(采用分区算法分割虚拟内存,如最佳适合分区算法,分区算法需要存储基址和长度):
段号 | 基址(虚拟地址) | 长度 | 保护 |
0 | Xxx | Xx | xx |
1 | 0x45000 | 60K | R/W |
页表结构:
逻辑页号 | 页框号(物理页) | 保护 |
Xx | xx | xx |
...... | ...... | ...... |
0x45 | 7 | R/W |
3.1)逻辑地址:
- 段号+偏移地址,分别通过 cs 和 ip寄存器来给出;
3.2)逻辑地址(如数据段 : 0x300)翻译为物理地址的步骤如下:
- 根据cs获取段号(数据段的段号); (cs是代码段寄存器)
- 根据段号查询段表得到段基址(虚拟地址) ;
- 根据虚拟地址计算出逻辑页号;如虚拟地址0x45000,它的逻辑页号是0x45;
- 根据逻辑页号查询页表得到页框号(物理页号);如根据逻辑页号45得到物理页号7;
- 根据物理页号7和偏移地址(ip寄存器个给出,如0x300)得到最终的物理内存地址0x7300; 即数据段中的指令 mov [300],0 中的逻辑地址300翻译后的物理地址为 0x7300 (把0赋值给物理地址0x7300内存单元);
地址翻译(重定向)完成后,操作系统把物理地址送入地址总线以读写该地址上的数据;
【4】段页式内存管理代码实现
【4.1】分配虚拟内存,建立段表 (第1步)
【代码解说】
- copy_process函数: 创建新进程;
- copy_mem函数:新进程的内存与当前进程的内存分配情况相同;
- 补充:LDT表就存储了该程序多个段的段基址;
1)copy_mem代码(分配虚拟内存,建立段表):
int copy_mem(int nr, task_struct *p)
{
unsigned long new_data_base;
// new_data_base就是虚拟内存基址,实际上是对虚拟内存的分割;每个新进程的虚拟内存空间是64M;
// 0号进程的虚拟内存空间是0~64M-1;1号进程的虚拟内存空间是 64M~2*64M-1;......
new_data_base = nr * 0x4000000; // 64M * nr ,nr是第几个进程
// 建立段表,为段表的代码段设置基址;p是pcb;进程切换时,段表跟着切换;
set_base(p->ldt[1], new_data_base);
// 建立段表,为段表的数据段设置基址;p是pcb;进程切换时,段表跟着切换;
set_base(p->ldt[2], new_data_base);
}
1.1)各进程的虚拟地址
每个进程占用 64M 的虚拟地址空间,互不重叠;
【4.2】分配物理内存,建立页表 (第2步)
1)copy_mem 拷贝内存代码 (详细步骤)
// 拷贝内存
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分别是父进程,新进程的虚拟地址基址;
// 拷贝页表,修改虚拟地址,为新进程建立页表
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 ;
from_dir = (unsigned long *) ( (from>>20) & 0xffc );
// 位运算得到新(子)进程的页目录号指针 to_dir ;
to_dir = (unsigned long *) ( (to >> 20) & 0xffc );
// 拷贝的字节数
size = (unsigned long) (size + 0x3fffff) >> 22;
//
for (; size-- > 0; from_dir++, to_dir++) {
// from_page_table 是父进程页表基址
from_page_table = (0xfffff000 & *from_dir);
// 申请(分配)一个物理内存页
to_page_table = get_free_page();
// 把新的物理内存页基址赋值给 to_dir
*to_dir = ( (unsigned long) to_page_table ) | 7 ;
}
}
2)from_page_table 与 to_page_table
【图解】
- from_dir 是页目录号 1 的基址,是 父进程 的页目录号的基址;
- to_dir 是页目录号 7 的基址,是 子进程 的页目录号的基址;
- 接下来要做的就是,把 from_dir 页目录号1映射的页表内容 拷贝到 to_dir页目录号7的页表;
2)把from_dir 对应的页表内容 拷贝到 to_dir 对应的页表
【代码】父进程页表内容拷贝到子进程页表的代码如下:
// 父进程页表内容拷贝到子进程页表的代码
for(; nr-- >0; from_page_table++; to_page_table++)
{
// from_page_table 是父进程页表(基址)
this_page = *from_page_table;
this_page &= ~2; // 只读
// 把父进程页表 from_page_table 内容拷贝给子进程页表 to_page_table
// 即 父进程页表基址 与 子进程页表基址相同,两者指向了同一块物理内存
*to_page_table = this_page;
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >>= 12;
// 当前页被共享了,所以该页的mem_map 累加;
mem_map[this_page]++;
}
说明:
- 这里没有为子进程重新申请物理页,这里子进程使用了父进程的物理页(当然是可以重新分配 的);
【4.3】进程建立完成后样子(fork后)
【图解】
- 进程1是父进程,进程2是子进程,通过进程1 fork 出了 进程2;
- 进程2的虚拟内存和段表初始化好了;
- 进程2的物理内存和页表也有了,即使进程2与父进程(进程1)的页表内容相同,共享同一块物理内存;
- 根据图也可以看到,进程1的段表与进程2的段表指向了同一个物理内存基址;
- 也就可以说,一个子进程被载入到内存中了; 尽管子进程用的代码,数据都是父进程的;
【5】程序使用内存
1)MMU:
- 内存管理单元,是一个硬件,自动完成逻辑地址到物理地址的翻译,即完成了从逻辑地址0x300到虚拟地址0x400300,再到物理地址0x7300的地址转换(用MMU硬件代替软件来实现地址翻译);
2)进程1的逻辑地址 0x300 翻译为物理地址0x7300的步骤:
- 步骤1: 通过0x300所在程序的段号,从段表中查询出段基址0x400000(64M为基址),从而计算出0x300在虚拟内存的逻辑地址为 0x400300;
- 步骤2:通过段基址计算得到逻辑页号,通过逻辑页号查询页表得到物理内存页号为7;
- 步骤3: 通过物理内存页号7和偏移地址300得到物理地址 0x7300;
3)地址翻译完成后,进程1 fork出了进程2
其中,进程2 翻译逻辑地址 0x300 的步骤如下:
- 步骤1: 通过0x300所在程序的段号,从段表中查询出段基址0x800000(128M为基址),从而计算出0x300在虚拟内存的逻辑地址为 0x800300;
- 步骤2:通过段基址计算得到逻辑页号,通过逻辑页号查询页表得到物理内存页号为7(因为进程2与进程1的页表相同);
- 又在进程fork时,进程1(父进程)把物理内存页7设置为只读(参见父进程页表内容拷贝到子进程页表的代码)。所以进程2无法写入内存页7,只能重新申请一个新的物理内存页,如物理页8.
- 步骤3: 进程2得到的物理内存页号为8,与偏移量合并计算得到物理内存地址为 0x8300;
至此:父子进程通过不同的虚拟内存空间实现了相互分离,相互独立;
【补充】MMU介绍 from wikipedia
refer2 https://zh.m.wikipedia.org/zh-hans/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E5%8D%95%E5%85%83
内存管理单元(英语:memory management unit,缩写为MMU),有时称作分页内存管理单元(英语:paged memory management unit,缩写为PMMU)。
它是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件。它的功能包括虚拟地址到物理地址的转换(即虚拟内存管理)[1]、内存保护、中央处理器高速缓存的控制,在较为简单的计算机体系结构中,负责总线的仲裁以及存储体切换(bank switching,尤其是在8位的系统上)。