CSAPP学习笔记——chapter9 虚拟内存

虚拟内存提供三个重要的功能。第一,它在主存中自动缓存最近使用的存放磁盘上的虚拟地址空间的内容。虚拟内存缓存中的块叫做页。对磁盘上页的引用会触发缺页,缺页将控制转移到操作系统中的一个缺页处理程序。缺页处理程序将页面从磁盘复制到主存缓存,如果必要,将写回被驱逐的页。第二,虚拟内存简化了内存管理,进而又简化了链接、在进程间共享数据、进程的内存分配以及程序加载。最后,虚拟内存通过在每条页表条目中加人保护位,从而了简化了内存保护。

这一章主要介绍了现代操作系统中虚拟内存的概念,先是介绍了虚拟内存的一般概念,这一部分我将在本文第一小节进行一个串联;第二部分介绍了内存映射,并以Linux为例,介绍了fork函数,execve函数的实现细节;第三部分则是介绍了动态内存分配,程序员通过如malloc, new, free, delete等语言特定的函数和操作符来控制,重点介绍了动态内存分配器如何维护进程的堆区域。

xv6上的进程结构:

// Per-process state
struct proc {
struct spinlock lock; // p->lock must be held when using these:
enum procstate state; // Process state
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID // wait_lock must be held when using this:
struct proc *parent; // Parent process // these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
};

一、端到端的地址翻译

地址翻译符号小结

这里回顾一下MMU地址翻译的流程图:

地址翻译一般流程

处理器生成一个虚拟地址,地址翻译器(MMU)根据虚拟页号和页表基址寄存器中的值得到一个PTE(页表项),如果有效位为1,则将物理页号(PPN)与虚拟地址中的VPO串联起来,得到相应的物理地址。下面介绍结合了TLB和高速缓存的系统的地址翻译流程。

在这一节里,我们通过一个具体的端到端的地址翻译示例,来综合一下我们刚学过的这些内容,这个示例运行在有一个 TLB 和 LI d-cache 的小系统上。为了保证可管理性,我们做出如下假设:

  • 内存是按字节寻址的。
  • 内存访问是针对 1字节的字的(不是4 字节的字)
  • 虚拟地址是 14 位长的(n=14)。
  • 物理地址是12 位长的(m=12)
  • 页面大小是 64 字节(P=64)。
  • TLB 是四路组相联的,总共有 16 个条目。
  • L1 d-cache 是物理寻址、直接映射的,行大小为 4 字节,而总共有 16 个组

根据上面的假设,可以得到虚拟地址和物理地址的格式:

因为虚拟地址的位数是14,一个页面大小是64即\(2^6\)字节,所以页表项一共有\(2^{14}/2^6=2^8\)个,因此需要8个二进制位作为虚拟页号,据此可以进而推出物理地址的格式。下面给出当前系统的一个快照:

下面我们逐一介绍各个名词:

  1. TLB(快表)相当于是页表的一个高速缓存,因为页表是存储在主存上的,使用TLB能加速地址翻译。因为 TLB 有4 个组,所以 VPN 的低2位就作为组索引(TLBI)。VPN 中剩下的高 6 位作为标记(TLBT),用来区别可能映射到同一个 TLB组的不同的VPN。
  2. 页表,这里可能有读者会发现,这里的PTE怎么只有PPN,没有PPO,这跟前面图9-12不一样啊;其实就是没有的,因为CPU生成的虚拟地址中的VPO和PPO是一样的,所以我们只需要取出页表中的物理页号和VPO组合就可以了。
  3. 高速缓存缓存的是磁盘的数据,。直接映射的缓存是通过物理地址中的字段来寻址的。因为每个块都是 4字节,所以物理地址的低 2 位作为块偏移(CO)。因为有 16 组,所以接下来的 4 位就用来表示组索引CI。剩下的 6 位作为标记(CT)。

下面假设CPU给了一个虚拟地址:

将其手动划分为VPN和VPO,MMU会根据VPN先查找TLB,先检查TLB是否缓存了PTE0x0f,这一步是根据TLBT和TLBI实现的。

根据TLB索引定位到第三行,然后再根据TLB标记得到PPN,将PPN和VPO相加便得到了物理地址0x354;这里如果TLB不命中的话,MMU就会去主存上的页表取出物理页号。

接下来,MMU会把物理地址发送给缓存:

根据标记位,发现命中,然后根据缓存偏移CO把数据0x36取出返回给CPU。

这里如果缓存不命中的话,就会根据物理地址去主存上取数据。

综上,我们介绍了在TLB命中,缓存命中时的地址翻译过程。

二、内存映射

内存映射(Memory Mapping)是操作系统提供的一种机制,允许进程将磁盘上的文件或匿名内存映射到它的虚拟地址空间。这个功能通常通过系统调用如mmap在类Unix系统中实现。

一个对象可以被映射到虚拟内存上,要么作为共享对象,要么作为私有对象,例如,每个C程序都需要来自标准C库的printf这样的函数,但如果每个对象都保留一个这样的副本,岂不是太浪费空间了,因此根据下图的内存映射可以节省空间:

写时复制概念

写时复制(Copy-On-Write,COW)是一种资源管理技术,常用于操作系统中进行进程创建(如fork系统调用)和内存页管理。它的巧妙之处在于优化性能和内存使用,通过延迟复制直到真正需要时才进行,以此来减少不必要的资源消耗。

COW的核心思想是当多个进程或多个内存页需要相同的数据时,它们初始时共享同一份数据副本而不是直接创建多个拷贝。这份数据保持不变,直到某个进程试图修改它。在修改发生的那一刻,操作系统才会创建一份数据的真实拷贝给那个进程,而其他进程仍然指向原始数据。

再看fork函数

fork()函数在一个进程中调用,它创建一个新进程,这个新进程拥有与原进程(父进程)相同的虚拟内存布局。这是通过复制原进程的虚拟内存结构实现的,包括内存管理结构(如mm struct)、区域结构和页表。然而,这种复制是浅层的,意味着新创建的进程(子进程)并不立即获得物理内存页面的新副本。相反,这两个进程共享相同的物理页面,这些页面被标记为只读。

关键点在于,每个页面都被设置为"写时复制"(Copy-On-Write, COW)。这意味着,只要这些共享页面是被读取的,两个进程可以安全地共享它们。但一旦任一进程尝试修改其中的任何一个页面,操作系统的内存管理机制会介入,为该进程创建该页面的一个新的、物理上独立的副本。这个新页面随后会被标记为可写,并且是该进程的私有属性。

通过这种方式,fork()实现了对虚拟内存的高效复制,同时保持了每个进程对其虚拟地址空间的完全控制。这样的设计保证了虚拟内存的抽象概念不会被破坏,即使在进程复制的情况下。每个进程都感觉它拥有自己的独立地址空间,即使在物理层面上,很多内存页面在一开始是共享的。

三、动态内存分配

为什么使用动态内存分配?

对应本博客第一张图的堆区域,动态内存一般是程序员在编程时自己定义的,使用动态内存分配的最重要原因是程序真正运行时才知道某些数据结构的大小。

比如这段代码:

#include <stdio.h>

int main(){
int *array, i, n;
scanf("%d", &n);
array = (int *)Malloc(n*sizeof(int));
for(i = 0; i < n; i++)
scanf("%d", &array[i]);
free(array);
return 0;
}

比如这段代码,在代码运行之前并不知道array数组的大小,如果把array设置为固定的大小,则很有可能导致空间的浪费,或者溢出。

隐式动态链表

分配器将堆视为一组不同大小的块的集合。

头部编码了这个块的大小,以及这个块是已经被分配还是空闲的;载荷则是块中的有效内容,比如一个整数数组;填充则是为了保证地址的对齐。

定义

隐式空闲链表(Implicit Free List)是一种在堆内存管理中使用的数据结构,用于跟踪堆中的空闲(未分配)内存块。它是动态内存分配算法的一个组成部分,通常用于实现如malloc和free这样的函数。

在隐式空闲链表中,堆被视为一系列的块(blocks),每个块要么是已分配的,要么是空闲的。每个块通常包含一些管理信息(如块的大小和分配状态),以及实际的数据区域。这些块在物理上连续排列在堆内存中。

隐式空闲链表的特点是:

  1. 空闲块的发现:为了找到空闲块,内存分配器必须遍历堆中的块,从头到尾检查每个块的分配状态。这通常是通过读取块的头部信息来完成的,头部信息会指示块的大小和是否被分配。
  2. 链表的隐式性:与显式链表不同,隐式空闲链表中的块并不包含指向下一个空闲块的指针。相反,分配器通过读取每个块的头部信息来决定哪些块是空闲的,并找到下一个块的位置。
  3. 内存利用和碎片:虽然隐式空闲链表简化了空闲内存块的管理,但它可能导致效率低下和内存碎片问题。查找合适的空闲块(尤其是在堆较大时)可能需要遍历整个堆,这会降低分配和释放内存的效率。同时,由于块的大小固定,可能导致内部碎片。

总的来说,隐式空闲链表提供了一种相对简单的方式来管理堆内存中的空闲块,但可能在性能和内存利用率方面存在一些折衷。为了改善这些问题,可以使用更复杂的内存管理策略,如显式空闲链表、分离适配(segregated fit)或伙伴系统(buddy system)。

释放空闲块

当分配器释放一个已分配块的时候,可能有其他空闲块与这个新释放的空闲块,这个时候就要考虑合并这两个空闲块,不然可能会导致“假碎片现象”。

对于隐式链表来说,可以很方便的访问到下一个空闲块的内容,当前块被释放之后,再检查下一个块,如果他也是空闲的,就采取立即合并的方式;但是这种方式并不一定适用于所有的情况。

如何合并上一个块?

按照隐式链表的定义,当前块是无法访问到上一个块的信息的,如何上一个块也空闲了,怎么合并呢?第一个思路是在每个块后面添加一个脚部,由于每个脚部的大小是固定的,所以当前块可以根据地址验算得到上一个块的信息。

显式空闲链表

显式空闲链表主要是为了解决隐式空闲链表分配与堆块的总数呈线性关系。一种更好的方法是将空闲块组织成某种数据结构,方便查找。

第一种方法是将空闲块组织成双向链表

这样查找合适的空闲块的时候就可以直接根据链表结构进行查找了。

第二种方法是分离存储

根据2的幂来划分空闲块的大小:

不过这段内容,书里介绍的比较粗略,先在这存个索引,以后用到了再深究。

四、C程序中常见的与内存有关的错误

这部分内容作者从间接引用坏指针、读未初始化的内存、允许栈缓冲区溢出 、假设指针和它们指向的对象是相同大小的 、造成错位错误引用指针,而不是它所指向的对象、误解指针运算、引用不存在的变量 、引用空闲堆块中的数据、引起内存泄漏这些方面介绍了常见的与内存有关的错误。

Q&A

  1. 第一节,如果主存也没有命中咋办怎么办?

地址翻译小节介绍了操作系统生成一个虚拟地址A,然后根据这个地址先检查TLB,如果没有再去检查主存的页表,如果页表也没有命中的话,就会触发一个缺页异常。随后这个异常导致控制从程序转到内核的缺页处理程序,处理程序首先检查这次访问是不是合法的,比如虚拟地址A是不是合法的,试图访问内存区域是不是合法的。如果合法的话,就会选择一个牺牲页,如果这个页面被修改过,就把这个页的内容写回磁盘,并将新的页面换入主存,并更新页表。执行完这些步骤,缺页处理程序返回,这时候程序就可以正常翻译虚拟地址A,从而不会 产生缺页中断了。

  1. 虚拟地址有啥用,感觉直接让CPU生成一个物理地址,然后访问主存看着也挺好的?

虚拟地址在现代计算机系统中有着非常重要的作用,其优势主要体现在以下几个方面:

  1. 内存保护与隔离:每个进程拥有自己的虚拟地址空间,这意味着一个进程不能直接访问或修改另一个进程的内存。这种隔离提供了更强的安全性和稳定性,因为一个程序的错误不会直接影响到其他程序的运行。
  2. 简化内存管理:虚拟内存使得每个进程看起来都有一个连续的、完整的内存空间,即使物理内存是碎片化的。这大大简化了程序的内存管理,因为程序员不需要关心物理内存的分布情况。
  3. 更有效的内存利用:虚拟内存允许操作系统进行更为灵活的内存管理,如页面置换(将不常用的内存页面移到磁盘上)和懒加载(只在需要时加载内存页面)。这样可以让系统更高效地利用有限的物理内存资源。
  4. 支持大型应用程序:使用虚拟内存,系统可以为应用程序提供远大于实际物理内存的地址空间。这使得可以运行大型应用程序,即使它们的内存需求超过了物理内存的容量。
  5. 简化多任务处理:在多任务操作系统中,虚拟内存允许每个进程操作在自己的独立地址空间内,从而简化了上下文切换的过程。

直接使用物理地址的主要问题是,它不支持上述提到的特性,如内存保护、灵活的内存管理和简化的多任务处理。此外,物理内存的直接使用在现代多任务和大型应用程序的环境下会导致管理上的复杂性和安全风险。因此,虚拟地址在现代计算机系统中被广泛采用。

CSAPP学习笔记——chapter9 虚拟内存的更多相关文章

  1. CSAPP学习笔记—虚拟内存

    CSAPP学习笔记—虚拟内存 符号说明 虚拟内存地址寻址 图9-12展示了MMU如何利用页表来实现这种映射.CPU中的一个控制寄存器,页表基址寄存器(Page Table Base Register, ...

  2. 《深入理解计算机系统》学习笔记整理(CSAPP 学习笔记)

    简介 本笔记目前已包含 CSAPP 中除第四章(处理器部分)外的其他各章节,但部分章节的笔记尚未整理完全.未整理完成的部分包括:ch3.ch11.ch12 的后面几小节:ch5 的大部分. 我在整理笔 ...

  3. Linux学习笔记5——虚拟内存

    一.为什么要有虚拟内存 虚拟内存的提出,是为了禁止用户直接访问物理存储设备,有助于系统稳定. 二.为什么一个程序不能访问另外一个程序的地址指向的空间 1:每个程序的开始地址0x80084000 2:程 ...

  4. CSAPP学习笔记 第一章 计算机系统漫游

    Ch 1.0 1.计算机系统是由硬件和系统软件组成的 2.本书阐述了计算机组件是如何工作的以及执行组件是如何影响程序正确性和性能的. 3.通过跟踪hello程序的生命周期来开始对系统的学习. #inc ...

  5. redis学习笔记之虚拟内存

    首先说明下redis的虚拟内存与os的虚拟内存不是一码事,但是思路和目的都是相同的.就是暂时把不经常访问的数据从内存交换到磁盘中,从而腾出宝贵的 内存空间用于其他需要访问的数据.尤其是对于redis这 ...

  6. CSAPP学习笔记(异常控制流1)

    1:诸如子进程结束之后父进程需要被告知,有时候应用程序需要系统调用,内核通过上下文切换将控制从一个进程切换到另一个进程,还有一个进程发送信号到另一个进程时接收者转而到它的信号处理函数去执行等等,我们的 ...

  7. CSAPP学习笔记(第一,二章)

    1:文本文件指的是ASCII码文件,二进制文件指的是除文本文件以外,其他文件. 2:区分数据对象的唯一判别方法是数据的上下文. 3:描述一下一个hello.c文件的处理过程.首先hello.c文件我们 ...

  8. 《Linux内核设计与实现》课本第五章学习笔记——20135203齐岳

    <Linux内核设计与实现>课本第五章学习笔记 By20135203齐岳 与内核通信 用户空间进程和硬件设备之间通过系统调用来交互,其主要作用有三个. 为用户空间提供了硬件的抽象接口. 保 ...

  9. 孙鑫VC学习笔记:多线程编程

    孙鑫VC学习笔记:多线程编程 SkySeraph Dec 11st 2010  HQU Email:zgzhaobo@gmail.com    QQ:452728574 Latest Modified ...

  10. 《Linux内核设计与实现》课本第一章&第二章学习笔记

    <Linux内核设计与实现>课本学习笔记 By20135203齐岳 一.Linux内核简介 Unix内核的特点 Unix很简洁,所提供的系统调用都有很明确的设计目的. Unix中一切皆文件 ...

随机推荐

  1. c# Lamda表达式 简化语法例子

    看到一个老代码里的方法,是判断两个string 数组是否存在相同的元素: 快一百行代码了..... public bool HasRole(string[] roleList) { bool resu ...

  2. atomic 包底层实现原理

    一.概念介绍(一)volatile关键字 Java 因为指令重排序,优化我们的代码,让程序运行更快,也随之带来了多线程下,指令执行顺序的不可控. 1.volatile关键字的作用: 内存可见性,修饰的 ...

  3. 如何快速的开发一个完整的iOS直播app(推流篇)

    开发一款直播app,肯定需要流媒体服务器,本篇主要讲解直播中流媒体服务器搭建,并且讲解了如何利用FFMPEG编码和推流,并且介绍了FFMPEG常见命令. 效果 一.安装Homebrew Homebre ...

  4. Linux blkid命令

    Linux blkid命令:显示块设备属性. Linux blkid命令 功能描述 使用blkid命令可以用来查询系统的块设备(包括交换分区)所使用的文件系统类型.卷标.UUID等信息. Linux ...

  5. Java中StringBuilder类常用的几个方法

    StringBuilder类 StringBuilder 类是 Java 中用于处理可变字符串的类,它提供了在字符串内部进行修改的方法,相比之下,String 类是不可变的,每次对字符串做修改都会创建 ...

  6. 多方安全计算(6):MPC中场梳理

    学习&转载文章:多方安全计算(6):MPC中场梳理 前言 诚为读者所知,数据出域的限制约束与数据流通的普遍需求共同催生了数据安全计算的需求,近一两年业界又统将能够做到多方数据可用不可见的技术归 ...

  7. C++:Boost库

    今日安装一个PSI库时,需要boost库,在此认识一下boost库,转载:macOS 中Boost的安装和使用 介绍 Boost是一个功能强大,构造精良,跨越平台,代码开源,完全免费的C++程序库. ...

  8. VMware的快照原理

    本文分享自天翼云开发者社区<VMware的快照原理>,作者:m****n VMware的快照是基于数据块的快照.快照也是以一个文件方式存在的,缺省位置和虚拟机在同一目录下,它是一个Delt ...

  9. OpenAPI 简介

    本文分享自天翼云开发者社区<OpenAPI 简介>,作者:蔡****钊 一.什么是open API API的全称是应用编程接口(Application Programming Interf ...

  10. “翼”鸣惊人,天翼云两篇论文被ACM ICPP 2024收录!

    *日,由天翼云科技有限公司弹性计算产品线天玑实验室撰写的两篇论文<PheCon: Fine-Grained VM Consolidation with Nimble Resource Defra ...