操作系统与内存管理

操作系统内存管理

1. 虚拟地址空间

虚拟内存管理
malloc
page-fault 缺页错误
物理内存空间
page-table 页表结构
mmap 内存映射
swap 内存交换区

在这里插入图片描述

2. 内存地址空间含义及分配

  • 内存地址空间 ≠ 物理内存空间
    Virtual address space ≠ Physical memory space
    门牌号 ≠ 实际房屋
    门牌号数量 ≥ 实际房屋数量
  • 32bit vs 64bit
    2^32 = 4GB
    2^64 = 16EB = 16,384 PB = 1.7e17 TB
    处理器支持:
    ARMv7 (32bit), ARMv8 (64bit)
    x86, amd64 (x86_64)
    操作系统支持
    编译器支持
  • 具体内存划分如下图所示

物理内存 Physical Memory
DRAM: 泛指内存
DIMM: 特指一条内存

虚拟内存地址  物理内存地址
虚拟内存地址 Virtual Memory Address (VMA)
物理内存地址 Physical Memory Address (PMA)
保护模式下的代码(user/kernel)都使用VMA
CPU自动完成 VMA  PMA 转换
分配粒度:a memory page = 4KB

在这里插入图片描述

Page Table: 记录VMA --> PMA映射的数据结构
Page Table 保存在 Kernel Address Space,由操作系统维护
每个进程有一个独立的 Page Table,用户空间 VMA -->PMA 映射各不相同
所有进程共享同一内核空间中VMA–>PMA映射
TLB用于加速 VMA --> PMA 转换
在这里插入图片描述

3. 虚拟内存诞生的前世与今生?

MMU诞生之前:

在传统的批处理系统如DOS系统,应用程序与操作系统在内存中的布局大致如下图:

  • 应用程序直接访问物理内存,操作系统占用一部分内存区。
  • 操作系统的职责是“加载”应用程序,“运行”或“卸载”应用程序。
    在这里插入图片描述

如果我们一直是单任务处理,则不会有任何问题,也或者应用程序所需的内存总是非常小,则这种架构是不会有任何问题的。然而随着计算机科学技术的发展,所需解决的问题越来越复杂,单任务批处理已不能满足需求了。而且应用程序需要的内存量也越来越大。而且伴随着多任务同时处理的需求,这种技术架构已然不能满足需求了,早先的多任务处理系统是怎么运作的呢?

程序员将应用程序分段加载执行,但是分段是一个苦力活。而且死板枯燥。此时聪明的计算机科学家想到了好办法,提出来虚拟内存的思想。程序所需的内存可以远超物理内存的大小,将当前需要执行的留在内存中,而不需要执行的部分留在磁盘中,这样同时就可以满足多应用程序同时驻留内存能并发执行了。

从总体上而言,需要实现哪些大的策略呢?

  • 所有的应用程序能同时驻留内存,并由操作系统调度并发执行。需要提供机制管理I/O重叠,CPU资源竞争访问。
  • 虚实内存映射及交换管理,可以将真实的物理内存,有可变或固定的分区,分页或者分段与虚拟内存建立交换映射关系,并且有效的管理这种映射,实现交换管理。

这样,衍生而来的一些实现上的更具体的需求:

  • 竞争访问保护管理需求:需要严格的访问保护,动态管理哪些内存页/段/区,为哪些应用程序所用。这属于资源的竞争访问管理需求。
  • 高效的翻译转换管理需求:需要实现快速高效的映射翻译转换,否则系统的运行效率将会低下。
  • 高效的虚实内存交换需求:需要在实际的虚拟内存与物理内存进行内存页/段交换过程中快速高效。

总之,在这样的背景下,MMU应运而生,也由此可见,任何一项技术的发展壮大,都必然是需求驱动的。这是技术本身发展的客观规律。

3.1 内存管理的好处
  • 为编程提供方便统一的内存空间抽象,在应用开发而言,好似都完全拥有各自独立的用户内存空间的访问权限,这样隐藏了底层实现细节,提供了统一可移植用户抽象。
  • 以最小的开销换取性能最大化,利用MMU管理内存肯定不如直接对内存进行访问效率高,为什么需要用这样的机制进行内存管理,是因为并发进程每个进程都拥有完整且相互独立的内存空间。那么实际上内存是昂贵的,即使内存成本远比从前便宜,但是应用进程对内存的寻求仍然无法在实际硬件中,设计足够大的内存实现直接访问,即使能满足,CPU利用地址总线直接寻址空间也是有限的。
3.2 内存管理实现总体策略

从操作系统角度来看,虚拟内存的基本抽象由操作系统实现完成:

  • 处理器内存空间不必与真实的所连接的物理内存空间一致。
  • 当应用程序请求访问内存时,操作系统将虚拟内存地址翻译成物理内存地址,然后完成访问。

从应用程序角度来看,应用程序(往往是进程)所使用的地址是虚拟内存地址,从概念上就如下示意图所示,MMU在操作系统的控制下负责将虚拟内存实际翻译成物理内存。

img

从而这样的机制,虚拟内存使得应用程序不用将其全部内容都一次性驻留在内存中执行:

  • 节省内存:很多应用程序都不必让其全部内容一次性加载驻留在内存中,那么这样的好处是显而易见,即使硬件系统配置多大的内存,内存在系统中仍然是最为珍贵的资源。所以这种技术节省内存的好处是显而易见的。

  • 使得应用程序以及操作系统更具灵活性

    • 操作系统根据应用程序的动态运行时行为灵活的分配内存给应用程序。
    • 使得应用程序可以使用比实际物理内存多或少的内存空间。

细究 MMU(Memory Management Unit)内存管理单元 请参考:关于MMU那些事儿

4. 不同进程如何划分内存地址空间?

每个进程都独占整个用户态内存地址空间:

进程在用户态时,只能访问用户空间内存;只有进入内核态后,才可以访问内核空间内存。虽然每个进程的地址空间都包含了内核空间,但这些内核空间,其实关联的都是相同的物理内存,也就是共享动态链接库、共享内存等。当进程切换到内核态后,就可以很方便地访问内核空间内存。

并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的。内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表记录虚拟地址与物理地址的映射关系。

8418a8d7c30bad544ae12b9d1f6ff016.png不同进程存在于不

页表实际上存储在 CPU 的内存管理单元 MMU 中,这样,正常情况下,处理器就可以直接通过硬件,找出要访问的内存。而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。

**CPU 上下文切换中的TLB(**Translation Lookaside Buffer,转译后备缓冲器)是 MMU 中页表的高速缓存。由于进程的虚拟地址空间是独立的,而 TLB 的访问速度又比 MMU 快得多,所以,通过减少进程的上下文切换,减少 TLB 的刷新次数,就可以提高 TLB 缓存的使用率,进而提高 CPU 的内存访问性能。

关于MMU那些事儿

MMU(Memory Management Unit)内存管理单元: 规定了一个内存映射的最小单位,也就是页,通常是 4 KB 大小。这样,每一次内存映射,都需要关联 4 KB 或者 4KB 整数倍的内存空间。

4 KB大小的页,会导致整个页表会变得非常大,比如32位系统4GB/4KB=100多万个页表项。为了解决页表项过多的问题,Linux 提供了两种机制,也就是多级页表和大页(HugePage)。

多级页表就是把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移。由于虚拟内存空间通常只用了很少一部分,那么,多级页表就只保存这些使用中的区块,这样就可以大大地减少页表的项数。Linux 用四级页表来管理内存页,虚拟地址被分为 5 个部分,前 4 个表项用于选择页,而最后一个索引表示页内偏移。

296cf2e3491d64d271a38be5cd884bea.png

大页,就是比普通页更大的内存块,常见的大小有 2MB 和 1GB。大页通常用在使用大量内存的进程上,比如 Oracle、DPDK 等。

通过这些机制,在页表的映射下,进程就可以通过虚拟地址来访问物理内存了。

5 内存分配与回收

malloc() 是 C 标准库提供的内存分配函数,对应到系统调用上,有两种实现方式,即 brk() 和 mmap()。

ae42331b710a24aff9417be0cec92392.png

对小块内存(小于 128K),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。

对大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。

这两种方式的优缺点:

brk()方式的缓存,可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。

mmap()方式分配的内存,会在释放时直接归还系统,所以每次 mmap 都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是 malloc 只对大块内存使用 mmap 的原因。

需要注意的是:当这两种调用发生后,其实并没有真正分配内存。这些内存,都只在首次访问时才分配,也就是通过缺页异常进入内核中,再由内核来分配内存。

整体来说,Linux 使用伙伴系统来管理内存分配。前面我们提到过,这些内存在 MMU 中以页为单位进行管理,伙伴系统也一样,以页为单位来管理内存,并且会通过相邻页的合并,减少内存碎片化(比如 brk 方式造成的内存碎片)。

但在实际系统运行中,会有大量比页还小的对象,如不到1K,如果为它们也分配单独的页,会浪费大量的内存,那该怎么分配内存呢?

在用户空间,malloc 通过 brk() 分配的内存,在释放时并不立即归还系统,而是缓存起来重复利用。

在内核空间,Linux 则通过 slab 分配器来管理小内存。你可以把 slab 看成构建在伙伴系统上的一个缓存,主要作用就是分配并释放内核中的小对象。

内存回收:对内存来说,如果只分配而不释放,就会造成内存泄漏,甚至会耗尽系统内存。所以,在应用程序用完内存后,还需要调用 free() 或 unmap() ,来释放这些不用的内存。当然,系统也不会任由某个进程用完所有内存。在发现内存紧张时,系统就会通过一系列机制来回收内存,比如下面这三种方式:

(1)回收缓存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面。

(2)回收不常访问的内存,把不常用的内存通过交换分区(Swap)直接写到磁盘中。Swap 其实就是把一块磁盘空间当成内存来用。它可以把进程暂时不用的数据存储到磁盘中(这个过程称为换出),当进程访问这些内存时,再从磁盘读取这些数据到内存中(这个过程称为换入)。Swap 把系统的可用内存变大了,但通常只在内存不足时,才会发生 Swap 交换,并且由于磁盘读写的速度远比内存慢,Swap 会导致严重的内存性能问题。

(3)杀死进程,内存紧张时系统还会通过 OOM(Out of Memory,内核的一种保护机制),直接杀掉占用大量内存的进程.。OOM 监控进程的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分:

一个进程消耗的内存越大,oom_score 就越大;

一个进程运行占用的 CPU 越多,oom_score 就越小。

这样,进程的 oom_score 越大,代表消耗的内存越多,也就越容易被 OOM 杀死,从而可以更好保护系统。

当然,为了实际工作的需要,管理员可以通过 /proc 文件系统,手动设置进程的 oom_adj,从而调整进程的 oom_score。oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死;数值越小,表示进程越不容易被 OOM 杀死,其中 -17 表示禁止 OOM。如用下面的命令,你就可以把 sshd 进程的 oom_adj 调小为 -16,这样, sshd 进程就不容易被 OOM 杀死。

a2ddc12b55823a169cccfbcfa9297231.png

5.1 buffer和cache

free命令中buffer和cache都表示缓存,但用途不一样

1、 Buffer,是内核缓冲区用到的内存,对应的是/proc/meminfo中的Buffer值

2、 Cache,是内核页缓存和Slab用到的内存,对应的是/proc/meminfo中的Cache和SReclaimable之和

简单来说,Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中。

cache(缓存)从CPU角度考虑,是为了提高cpu和内存之间的数据交换速度而设计的,例如平常见到的一级缓存、二级缓存、三级缓存。 cpu在执行程序所用的指令和读数据都是针对内存的,也就是从内存中取得的。由于内存读写速度慢,为了提高cpu和内存之间数据交换的速度,在cpu和内存之间增加了cache,它的速度比内存快,但是造价高,又由于在cpu内不能集成太多集成电路,所以一般cache比较小,以后intel等公司为了进一步提高速度,又增加了二级cache,甚至三级cache,它是根据程序的局部性原理而设计的,就是cpu行的指令和****访问的数据往往在集中的某一块,所以把这块内容放入cache后,cpu就不用在访问内存了,这就提高了访问速度。当然若cache中没有cpu所需要的内容,还是要访问内存的。

从内存读取与磁盘读取角度考虑,cache可以理解为操作系统为了更高的读取效率,更多的使用内存来缓存可能被再次访问的数据。

**缓冲(buffers)**是为了提高内存和硬盘(或其他I/O设备)之间的数据交换的速度而设计的。把分散的写操作集中进行,减少磁盘碎片和硬盘的反复寻道,从而提高系统性能。linux有一个守护进程定期清空缓冲内容(即写入磁盘),也可以通过sync命令手动清空缓冲。

简单来说,buffer是即将要被写入磁盘的,而cache是被从磁盘中读出来的。 buffer是由各种进程分配的,被用在如输入队列等方面。一个简单的例子如某个进程要求有多个字段读入,在所有字段被读入完整之前,进程把先前读入的字段放在buffer中保存。

cache经常被用在磁盘的I/O请求上,如果有多个进程都要访问某个文件,于是该文件便被做成cache以方便下次被访问,这样可提高系统性能。

5.2 malloc背后发生了什么?

在这里插入图片描述

为什么可以运行
因为malloc分配的是虚拟内存地址空间
分配小于 MMAP_THRESHOLD (128KB),从 Heap 分配
分配大于 MMAP_THRESHOLD (128KB),从 Memory Mapping Segment 分配
物理内存在第一次访问的时候才分配(demand paging)
每次分配一个物理内存页(4KB)

内存结构如下图所示:

在这里插入图片描述

5.3 第一次访问刚分配的内存时,会发生什么?

Page Fault 缺页错误

  • 第一次访问一个新malloc出来的地址时,该Virtual Memory Address (VMA) 还未有与其对应的物理内存页
  • CPU中的Memory Management Unit (MMU)单元会触发一个缺页错误的硬件中断
  • 操作系统内核收到该中断后会去检查该VA是否处在一个合法的VMA区间
  • 如果VMA不合法,则抛出段错误 (Segmentation fault) ,然后杀掉该进程,如果合法,则在空余的物理内存池中寻找一个内存页,通过修改页表 (Page table) 来完成 VMA到物理内存的对应关系

如何判断 VMA 是否合法?

  • malloc() 会调用 brk() 或 mmap() 来向kernel注册合法的虚拟内存地址空间 Virtual Memory Area (VM Area),详细键 5.0开头。
    这些VM Area由一棵红黑树来维护

在这里插入图片描述

5.4 内存缓冲区 Swap
  • 当物理内存耗尽时,操作系统会将一部分访问不频繁的数据从内存搬移到硬盘中

  • -硬盘上用于保存这些数据的区域称为“内存缓冲区”(Swap)

  • 这理论上可以让应用程序可以使用的内存和硬盘一样大

  • 但是使用硬盘缓冲区会导致访问速度减慢100倍(20GB/s vs 200MB/s)
    top 实例分析
    在这里插入图片描述

    VIRT (Virtual): 所声明的虚拟内存空间(包括分配未使用)
    RES (Resident): 所使用的物理内存空间(不包括swap)
    SHR (Shared): 和其他进程共享的内存空间
    判断一个进程占用内存空间大小,关键看RES
    在这里插入图片描述

5.5 内存碎片处理
  • 内部碎片

    • 已分配给某进程,但不能被该进程利用的物理内存空间

    • 内部:物理内存页的内部

    • 内存分配尽可能采用同样大小(或整数倍大小)
      在这里插入图片描述

  • 外部碎片

    • 未分配给任一进程,但由于太小了无法分配给申请内存空间的进程的物理内存空间
    • 通常发生在需要分配一段连续的物理内存空间时(例如 kmalloc,DMA)
    • 外部碎片导致内存不足的情况相对罕见

内存碎片整理工具有用吗?

在这里插入图片描述

简单回答:没用
原因:内存碎片整理工具无法知道每一块内存被谁引用,直接移动会导致程序指针失效,程序崩溃

市面上的内存碎片整理工具干了啥?

  • step 1:申请一大块虚拟内存空间

  • step 2:访问这块虚拟内存空间,触发page fault

  • step 3:操作系统为该虚拟内存空间分配物理内存,进而将其他进程内存中的数据写到swap

  • step 4:碎片整理工具释放刚申请的内存

  • step 5:新启动的程序即时拥有可使用的内存

    作用:

    • 减少了新启动程序触发swap的耗时
    • 一定程度上缓解了内存泄漏

    副作用:让已经在运行的程序频繁触发swap

    • Managed environments 自带内存碎片整理功能
    • JVM / C# / Go / Rust
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大江东去浪淘尽千古风流人物

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值