内存是计算机系统中至关重要的组成部分,它不仅储存了运行中的程序和数据,还直接关系到系统的性能和稳定性。让我们一起深入探讨Linux系统下内存管理的核心原理,揭开它的神秘面纱。
基础概念
物理地址
- 概念:物理地址是指计算机内存中实际的硬件地址,它对应着计算机中的物理存储单元(如RAM),物理地址是唯一的。内存的一个地址的容量是一个字节(Byte)
- 特点:?物理地址是唯一的,每个物理存储单元都有一个对应的物理地址。
虚拟地址
- 概念:虚拟地址是在程序执行过程中由操作系统提供的地址空间,它不直接对应物理硬件,而是经过虚拟内存系统的映射,最终映射到物理地址上。每个运行的进程都有自己的虚拟地址空间,这使得每个进程认为它拥有整个系统的内存。
- 特点:?虚拟地址具有抽象性,它使得程序无需关心实际的硬件细节,而是可以使用一个相对于程序自身的地址空间。
内存布局
- 32位操作系统:支持32位的地址空间,最多可以寻址2^32个地址,即4GB的内存。
- 64位操作系统:?支持64位的地址空间,最多可以寻址的地址数量为2^64,即128TB。
- 不同位宽的操作系统地址空间的范围也不同,下面的两张图来分别表示它们的虚拟地址空间:
- 每个进程的虚拟内存空间都包括用户空间和内核空间,每个进程都认为它拥有整个系统的内存资源。
- 每个进程的内核空间,其实关联的都是相同的物理内存(公用的)。
内存映射
既然每个进程都有一个这么大的地址空间,那么所有进程的虚拟内存加起来,自然要比实际的物理内存大得多。所以,并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,内存分配的机制是通过内存映射来管理的,内存映射支持按段分配和按页分配。
按段分配
分段是比较早提出的,它将整个物理内存划分为若干个不同用途的段,每个段用于存放特定类型的数据,这些逻辑分段包括只读段、数据段、堆段、栈段组成。
- 只读段:包括代码和常量等。
- 数据段:包括全局变量等。
- 堆段:包括动态分配的内存,从低地址开始向上增长。
- 文件映射段: 包括动态库、共享内存等,从高地址开始向下增长。
- 栈段:包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB
存在的问题:
- 外部内存碎片:因为分段机制分配的是连续的内存空间,假设有 1G 的物理内存,A程序占用了512MB,B程序占用了128MB,C程序占用了256MB,空闲128MB,B程序关闭了,因为内存不连续,导致没有足够空间在打开一个200MB的程序,就会交换到磁盘,从磁盘换入、唤出效率低下(多个不连续的物理内存空间)。
- 复杂性:?程序员需要管理多个内存段,增加了编程的复杂性。
- 不同段的交叉访问:?由于段之间的独立性,跨越多个段的访问会更加复杂。
按页分配
- 将物理内存和虚拟内存划分为固定大小的页(通常为4KB)
- 操作系统维护一个页表,将虚拟内存的页映射到物理内存的页上。
- 页表(快速、高效)。
- MMU:页表实际上存储在 CPU 的内存管理单元 MMU 中。
- TLB?是MMU 中页表的高速缓存,加速虚拟地址到物理地址的转换,减少对主存(RAM)的访问次数,提高系统性能。
- 多级页表:页的大小是4K,随着内存的增大,页表记录会特别多,为了解决页表项过多的问题,Linux 提供了两种机制,也就是多级页表和大页(HugePage)。
优点
- 消除外部碎片:?由于页是固定大小的,减少了外部碎片的产生。
- 简化内存管理:?操作系统负责页的映射,程序员无需关心具体的内存分配和释放。
- 更好的内存共享:?易于实现页面的共享,不同进程可以共享相同的页。
内存分配与回收
内存分配
进程可以通过调用malloc等函数在堆上动态分配内存。这些内存块的管理由C库提供,但最终涉及到系统调用,如brk和mmap
brk
- 作用:用于调整进程的数据段的结束地址,即扩展或缩小堆的大小。
- 操作对象:操作的是堆空间,对整个数据段的结束地址进行调整。
- 分配粒度:分配的内存是以页为单位的,较大的内存请求可能会导致内部碎片。
- 适用场景:适用于较小的内存分配,比如动态内存分配。
mmap
- 作用:用于在进程的地址空间中映射文件或匿名内存区域。
- 操作对象:可以操作文件映射,也可以用于匿名内存映射,即映射到无关联文件的内存。
- 分配粒度:可以以页为单位进行内存分配,也支持更细粒度的映射。
- 适用场景:适用于大块的内存分配,比如映射大文件、共享内存、内存映射 I/O 等。
内存回收
- 手动回收:调用?free() 或 unmap()?来释放这些不用的内存。
- 自动回收(内存紧张时系统触发)。
- 回收缓存:比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面。
- 回收不常访问的内存:把不常用的内存通过交换分区直接写到磁盘中(Swap)。
- 杀死进程:内存紧张时系统还会通过OOM(Out of Memory)直接杀掉占用大量内存的进程。
- 一个进程消耗的内存越大,oom_score 就越大。
- 一个进程运行占用的 CPU 越多,oom_score 就越小。
# oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死
# -17 表示禁止 OOM
echo -16 > /proc/$(pidof sshd)/oom_adj
结束语
今天我们从概念开始,一点点的展开讲了关于内存映射的两种内存分配机制,以及特点,讲了内存分配和回收,留下几个问题,系统大家一起讨论学习:
- 按页分配下会存在内存碎片吗?为什么?
- Linux 操作系统采用了哪种方式来管理内存呢?