操作系统导论读书笔记—虚拟化内存

本文深入探讨了操作系统中的内存管理,特别是虚拟化内存和分页机制。介绍了物理存储器的层次结构,强调了分段和分页在解决内存问题上的角色,如内部碎片、外部碎片和地址转换。讨论了内存分配策略,包括分段带来的问题、空闲空间管理方法如分割与合并、地址转换的硬件实现以及TLB缓存。此外,还涉及了页交换策略,如最优替换、FIFO和LRU等,以及脏页处理和其他内存策略。
摘要由CSDN通过智能技术生成

1. 物理存储组成

在这里插入图片描述
物理存储器容量由小到大依次为寄存器、缓存、主存、本地磁盘、远程存储服务器,其存取速度由快变慢,单位字节的价格由高变低。

2. 分段

操作系统提供了一个物理内存的抽象,这个抽象就叫地址空间,是运行程序所能看到的系统中的内存。进程的地址空间包含了运行程序的所有内存状态。比如:程序的代码、栈保存当前函数的调用信息,分配空间给局部变量,传递参数和函数返回值。最后,堆用于管理动态分配的、用户管理的内存(malloc或new申请的)。还有静态初始化的变量。操作系统在专门硬件的帮助下,通过每一个虚拟内存的索引,将其转换为物理地址,从而进行访问。虚拟内存空间

2.1 内存类型及API

2.1.1 栈内存

在运行一个C程序时,会分配两种内存。第一种称为栈内存,它的申请和释放操作是编译器来隐式管理的,所以有时也称为自动内存。当函数退出时,编译器会释放该内存。函数不能返回指向栈内存的指针。

2.1.2 堆内存

这种主要针对长期内存要求,所有申请和释放操作都由程序员完成。

	void *malloc(int num); //返回指向新内存的指针

在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。

	void free(void *address); 

该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。

2.1.3 TXT区域(代码段)

储存是存放二进制可执行代码的位置

2.1.4 Data区域

存放静态常量,从高地址开始向下增长

2.1.5 BSS区域

存放未初始化的静态变量

2.2 内存常见错误

忘记分配内存

char *src="hello";
char *dst;
strcpy(dst,src);//segfault

运行这段代码,可能会导致段错误。

没有分配足够的内存

char *src="hello";
char *dst=char*malloc(strlen(src));//too small
strcpy(dst,src);

这种方式可能会覆盖其他变量。

忘记初始化分配内存

程序进行未初始化的读取,它会从堆中读取一些未知的数据。可能对程序有害。

忘记释放内存

在长时间运行的程序中,内存泄露会是一个很棘手的问题。在有垃圾回收器的语言中,也是如此。如果你仍然拥有对某块内存的引用,垃圾收集器不会释放它。在程序的编写过程,要养成释放显性分配内存的习惯。

在用完之前释放内存

程序在用完之前释放内存,这种错误称为悬挂指针,随后的使用可能会导致程序崩溃或者覆盖有效的内存。

反复释放内存

程序可能不止一次释放内存,这种称为重复释放。内存分配库会困惑,做各种奇怪的事。

错误使用free()

free期望你只传入之前malloc得到的指针,如果传入非法值,就有可能产生危险的结果。

2.3 虚实地址转换

基于硬件的地址转换,简称为地址转换,这是实现内存虚拟化的核心技术。利用地址转换,硬件对每次内存访问进行处理,将指令中的虚拟地址转换为数据实际存储的物理地址。操作系统必须对内存进行管理,记录正在使用和空闲的内存位置。我们将CPU中负责地址转换的部分统称为内存管理单元。

2.3.1 动态(基于硬件)重定位

在最初的应用中,每个CPU需要两个硬件寄存器:基址寄存器和界限寄存器,有时称为限制寄存器。物理地址等于虚拟地址加基址寄存器的值。以基址寄存器为起点开始分配内存,上限为界限寄存器的值。
这种为每个进程分配固定物理内存的方式,使进程内存空间之间得到了隔离,但是这种固定内存划分方式效率低下。进程内的栈和堆区并不一定很大,会导致这片区域内存空间浪费,会产生内存碎片。

2.3.2 分段

通过基址寄存器和界限寄存器简单的为每个进程分配内存的这种方式,效率低下、容易产生很多内部碎片。我们采用更细化的内存管理方式,分段管理,减少内部碎片。
在MMU中引入多对基址界限寄存器,每个逻辑段一对。分段机制使得操作系统能够将不同段放在不同的物理内存区域,从而避免了虚拟地址空间中的未使用部分占有物理内存。
段错误
试图访问超出界限的非法内存地址,硬件发现该越界,会陷入操作系统,终止该出错进程。
反方向增长
针对栈段需要支持反方向增长。
支持共享
增加保护位,对段进行读写属性设置。对代码段进行读共享、写保护,这样多个程序就能运行同一代码。

2.4 分段解决的问题与带来的问题

2.4.1 分段解决的问题

分段解决了一些问题,帮助我们更好的实现了虚拟化内存。
分段能更好地支持稀疏地址空间,避免了逻辑段之间的潜在的内存浪费。
分段要求的算法比较容易,很适合硬件完成,地址转换的开销极小。
独立的分段支持代码共享。

2.4.2 分段带来的新的问题

上下文切换:各个段寄存器的内容也需要保存和恢复。
外部碎片:每个段的大小都不一样,申请释放就会使空闲内存分散。可能剩余的内存总量满足申请要求,但是段内存都是连续的内存,连续的空闲内存不满足要求,就会申请不到。这种情况就是内存外部碎片。
该问题的一种解决方案是紧凑内存,终止运行进程,将进程数据复制到连续内存区。这种方式成本比较高,拷贝需要大量CPU时间。
另一种是使用空闲列表管理算法,使内存在最合适的地方进行申请尽量减少内存碎片。这类算法包括伙伴算法、最优匹配算法等。

3. 空闲空间管理

当管理的空闲空间由不同大小的单元组成时,管理就变得困难。在用户级的内存分配库(malloc、free)就很容易出现外部碎片。
一个程序调用malloc,并获得一个指向堆中一块空间的指针,这块区域就属于这个程序了,直到程序调用free将它释放。因此,不可能进行紧凑空闲空间的操作,从而减少碎片。操作系统层在实现分段时可以采用紧凑来减少碎片。

3.1 底层机制

3.1.1 分割与合并

分割:找到一块可以满足要求的空闲空间,将其分割,第一块返回用户,第二块留在空闲列表中。
合并:分配程序会在释放一块内存时合并可用空间,查看归还内存块地址以及邻边的空闲内存块,如果归还内存与空闲块相邻,就将他们合并在一起。

3.1.2 如何追踪分配空间的大小

free(void *ptr)接口没有块大小的参数,仅有数据起始地址的位置

在申请内存时多申请一个内存块头的结构大小用来储存内存的大小信息和检查内存是否被改写的魔术字段。
内存块

3.1.3 嵌入空闲列表

空闲列表描述了堆中的空闲内存块,最初只有一大块内存块,随着内存申请和释放,空闲内存块的数量增多,块也变小。内存被分割成多段,碎片化。这时需要遍历列表,合并相邻块。完成以后,堆又成为一个整体。

3.1.4 让堆生长

当空闲列表中的内存不能满足要求后(每块的大小都小于需要申请的内存或已经耗尽),这时候会调用sbrk向操作系统进行申请,以获得更大的堆空间。

2.3 基本策略

2.3.1 最优匹配

遍历整个空闲列表,满足大小要求中的最小那块。想法是选择最接近用户请求的那块,从而避免空间浪费。缺点是遍历查找需要付出比较大的性能损失。

2.3.2 最差匹配

遍历整个空闲列表,满足大小要求中的最大那块。多数情况下,效果很差,开销高。

2.3.3 首次匹配

首次匹配策略就是找到第一块满足要求的内存块。首次匹配速度快,但有时会让列表开头部分出现很多小碎片。

2.3.4 下次查找

从上次申请内存的位置开始查找,避免堆列表开头频繁的分割。
性能与首次匹配接近,同时避免了遍历查找。

2.3.5 分离空闲列表

如果一个程序经常申请一种大小的内存,那就用一个独立的列表,只管理这样大小的对象。
Solaris厚块分配程序就是进行管理的。它为可能频繁申请的内核对象创建一些对象缓存(object cache),如锁和文件系统的inode等。为这些对象缓存每个分离了特定大小的空闲列表,因此能够很快的响应内存的申请和释放。如果某个缓存中的空闲空间快耗尽时,它就向通用分配程序申请一些内存厚块(slab)(总量是页大小或对象大小的公倍数)。当使用者数量为0时,通用的内存分配程序会回收这些内存。

2.3.6 伙伴系统

在这种系统中,空闲空间首先从概念上被看成大小为2的N次幂的大空间。当有一个内存分配请求时,空闲空间被一分为二,直到刚好可以满足请求的大小,这时返回请求的块。这种分配策略只允许分配2的整次幂大小的空闲块,因此会有内部碎片。申请7k内存会返回8k。伙伴算法的漂亮之处在于块被释放的时候。如果将这个8k内存块释放,分配程序会检查“伙伴”8k是否空闲。如果是,就合为一体,变成16k的块。然后检查16k的伙伴是否空闲,一直递归合并。

4. 分页

操作系统有两种方法,来解决大多数空间管理问题。第一种是将空间分割成不同长度的分片,就像是虚拟内存管理中的分段。这个方法存在固有问题。具体来说,将空间切成不同的分片以后,空间本身会碎片化。
第二种将空间分割成固定长度的分片。在虚拟内存中,我们称这种思想为分页。分页将虚拟内存分割成固定大小的单元,每个单元称为一页。相应地,我们把物理内存看成是定长槽块的阵列,叫作页帧。每个页帧对应一个虚拟页。

4.1 页表

为了记录地址空间的每个虚拟页放在物理内存中的位置,操作系统通常为每个进程保存一个数据结构,称为页表。页表的主要作用是为地址空间的每个虚拟页面保存地址转换,从而让我们知道每个页在物理内存中的位置。

4.1.1 地址转换

虚拟地址分为两段,虚拟面页号(VPN)和页内偏移量(offset)使用VPN查找页表获取物理帧号(PFN)。通过物理帧号获取物理页起始地址,再加上偏移量(offset)得到真实物理地址。

4.1.2 页表存储位置

页表可以很大,32位地址空间,4k大小的页(12位),20位VPN,意味着需要管理2^20个地址转换一个页面条目(PTE)需要4个字节,整个页表需要4M。100个进程就是400M,所以页表要能存储在磁盘了。

4.1.3 页表结构

有效位:虚拟内存空间不是所以地址都是有效的,栈和堆之间未使用的位置都被标记为无效,不需要分配物理内存。
保护位:设置读写权限。
存在位表示该页表是在物理存储器还是磁盘(即被换出swapped out)。交互允许系统将很少使用的页表移除磁盘。
脏位:页表带入内存后是否被改写。
访问位:页表是否被访问,页面替换有关,访问次数多的应该留着内存中。
对于x86-32位架构会有两级页表映射,页目录项和页表项。
页目录项
页目录项
页表项
页表项
页基址——位12~位32。页基址决定了4K大小的页的第一个字节的地址。如果是页目录项,表示的是项表的起始地址,如果是页表项,表示的是页的起始地址。
Present(P)flag,位0。P位为1,表示该页在物理内存中,可以进行页转换。P位为0,表示该页不在内存中,如果访问该页,则产生一个缺页异常(#PF)。处理器并不设置/清除该位,设置/清除的操作由操作系统进行。
如果产生一个缺页异常,操作系统一般完成如下操作:
1、从磁盘中将页拷贝到物理内存。
2、将页地址写入页表或者是页目录表项,并且置P位,dirty位和accesed位也在这个时候设置。
3、在TLB表中清空当前页表项。
4、从缺页处理例程中返回,重新执行被中断的程序。
读/写(R/W)位,位1
决定页的读/写权限。如果页目录项指向一个页表,则决定的是一组页的读/写权限。如果该位为0,则只读,如果为1,则可写。该标志位同U/S位和CR0中的WP位相互作用。
User/supervisor(U/S)位,第2位
决定页的user-supervisor权限。当该位为0,页有超级用户权限。当该位为1,页有用户权限。该标志位同R/W位和CR0中的WP位相互作用。
Page-level write-through(PWT)位,第3位
PWT位控制write-through和write-back特性。PWT为1,表示write-through,PWT为0,表示write-back。
Page-level cache disable(PCD)位,第4位
控制页的cache属性。如果PCD位为1,cache关闭,如果为0,cache打开。
Accessed(A)flag,第5位
该位表明页/页表是否被存取过。当页初次装入物理内存的时候,由内存管理内存将该位清0。当处理器访问该页时,将此位置1。处理器永远不会去清零,只有软件能够将该位清零。内存管理软件使用A位和后面将要讲到的D位,来决定页在物理内存中的切入和切出。
Dirty(D)flag,第6位
该位表明页是否被写过(该位对页目录无效)。当页初次装入物理内存的时候,置此位为0。当处理器写该页时,置此位为1。处理器不会对该位进行清零操作。
Page size(PS)flag,第7位
该位为零,表示页尺寸为4K字节,页目录项指向一个页表。如果该位为1,则页尺寸为4M字节。
在页目录项中,第9、10和11位用来指向页表,第6位保留,并且置为零。

4.2 快速地址转换(TLB)

分页不会导致外部碎片,非常灵活,支持稀疏虚拟地址空间。然而实现分页支持要小心考虑,分页会带来额外的内存访问(需要查表)和内存浪费(需要内存存储页表)。
慢是无法接受的,想让某些东西更快,需要一些硬件的支持。地址转换旁路缓冲存储器(TLB),它是频繁发生的虚拟到物理地址转换的硬件缓存。对每次内存访问,硬件先检查TLB,看看是否有转换映射,如果有,就完成转换。TLB带来了巨大的性能提升。

4.2.1 缓存

硬件缓存背后的思想是利用指令和数据引用的局限性,时间局限性和空间局限性。时间局限性是指最近被访问的指令和数据很快会被再次访问。空间局限性是指,最近访问的内存周围的地址可能很快也会被访问。访问一块内存数据,会先在缓存中查找内存的副本,没有再查找TLB,再访问内存。
页表项中可以设置缓存相关属性
Write-through(直写模式)在数据更新时,同时写入缓存Cache和后端存储。此模式的优点是操作简单;缺点是因为数据修改需要同时写入存储,数据写入速度较慢。
Write-back(回写模式)在数据更新时只写入缓存Cache。只在数据被替换出缓存时,被修改的缓存数据才会被写到后端存储。此模式的优点是数据写入速度快,因为不需要写存储;缺点是一旦更新后的数据未被写入存储时出现系统掉电的情况,数据将无法找回。

4.2.2 TLB管理

硬件管理
CISC架构的处理器采用这种方式,通过页表基址寄存器以及页表的确切格式,发生未命中时,遍历查找页表,取出映射更新TLB,比较典型的就是X86架构采用多级页表如之前描述。
软件管理
RISC采用软件方式管理TLB,当发生TLB未命中时,硬件系统抛出一个异常,这时暂停当前的指令流,将特权级提升到内核模式,跳转至陷阱处理程序。该陷阱处理函数用于处理TLB未命中,他会查找页表中的转换映射,然后用特权指令更新TLB,并从陷阱返回。此时,硬件重试该指令(导致TLB命中)。
软件的方式主要在于灵活性:操作系统可以用任意数据结构来实现页表,不需要改变硬件。另一个优势是简单,硬件不需要做太多工作,它抛出异常,操作系统负责剩余工作。

5. 交换空间

为了支持更大的地址空间,利用当前各种物理存储器特性,提升软件性能。使用交换空间来满足不同场景存储需求,频繁访问的数据放入存储量小但速度快的内存,长时间不使用的数据移出到磁盘;
交换空间:在硬盘上开辟一部分空间用于物理页的移入移出。

5.1 页交换策略目标

我们的目标是尽量让程序访问内存。减少访问磁盘的次数,避免程序以磁盘的速度运行。

5.2 替换策略

5.2.1 最优替换策略

其原理是替换内存中在最远将来才会被访问到的页,可以达到缓存未命中率最低。这是一种很难实现的方法,程序运行存在不确定性,你无法知道未来哪些页很长时间不会被访问。因此,最优策略只能作为比较,评估我们的策略有多接近完美。

5.2.2 FIFO

一种简单的实现方式,用一个队列实现,最早进入的页最先被换出。缺点是无法确定页的重要性,多次被使用的页仍然会被换出。

5.2.2 随机

随机挑选一个也换出,也是一种简单的实现方式,效果看运气。

5.2.3 利用历史数据:LRU

FIFO和随机这样的策略都会存在一个相同的问题:它可能会踢出一个重要的页,而这个页马上要被使用。为了提高后续命中率,需要通过历史的访问情况作为参考。
页替换策略可以使用频率,近期的访问频率越高,越有价值。程序有两种局部性,空间局部性和时间局部性。
空间局部性:一个页被访问,其附近也会被访问;
时间局部性:近期访问的页不久后会被再次访问;
在某些特定的场景下,LRU效果并不好,比如内存页循环使用,会造成0命中的状态。

5.2 考虑脏页

页表项中有一个位标识该页是否被修改,踢出它需要写回磁盘,代价昂贵,很多策略都倾向于踢出干净页。

5.2 其他内存策略

除了页交换策略还有页选择策略,决定何时怎样选择页面。操作系统可能会猜测一个页面可能会被使用,从而提前载入。
页写入策略,可以简单的单个页面写入,然而为了提高效率,操作系统会聚集写入,单次大的写入比多次小写入效率高。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值