19.段页结合的实际内存管理

Posted PacosonSWJTU

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了19.段页结合的实际内存管理相关的知识,希望对你有一定的参考价值。

【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位的系统上)。

 

以上是关于19.段页结合的实际内存管理的主要内容,如果未能解决你的问题,请参考以下文章

存储管理-段页式管理

段页式存储管理思想

操作系统的内存管理——页式段式管理段页式管理

linux段页式内存管理技术

Linux内存管理——段页式访问

我是如何学习写一个操作系统:内存管理和段页机制