初识 Linux 内存管理
2022-08-11 22:25:15 阿炯

Linux 内存管理模型非常直接明了,因为 Linux 的这种机制使其具有可移植性并且能够在内存管理单元相差不大的机器下实现管理,下面就来认识一下 Linux 内存管理是如何实现的。下文是转自cxuaner的个人空间,感谢原作者。

基本概念

每个 Linux 进程都会有地址空间,这些地址空间由三个段区域组成:text 段、data 段、stack 段。下面是进程地址空间的示例。



数据段(data segment) 包含了程序的变量、字符串、数组和其他数据的存储。数据段分为两部分,已经初始化的数据和尚未初始化的数据。其中尚未初始化的数据就是我们说的 BSS。数据段部分的初始化需要编译就期确定的常量以及程序启动就需要一个初始值的变量。所有 BSS 部分中的变量在加载后被初始化为 0 。

和 代码段(Text segment) 不一样,data segment 数据段可以改变。程序总是修改它的变量。而且,许多程序需要在执行时动态分配空间。Linux 允许数据段随着内存的分配和回收从而增大或者减小。为了分配内存,程序可以增加数据段的大小。在 C 语言中有一套标准库 malloc 经常用于分配内存。进程地址空间描述符包含动态分配的内存区域称为 堆(heap)。

第三部分段是 栈段(stack segment)。在大部分机器上,栈段会在虚拟内存地址顶部地址位置处,并向低位置处(向地址空间为 0 处)拓展。举个例子来说,在 32 位 x86 架构的机器上,栈开始于 0xC0000000,这是用户模式下进程允许可见的 3GB 虚拟地址限制。如果栈一直增大到超过栈段后,就会发生硬件故障并把页面下降一个页面。

当程序启动时,栈区域并不是空的,相反,它会包含所有的 shell 环境变量以及为了调用它而向 shell 输入的命令行。举个例子,当你输入:
cp cxuan lx

时,cp 程序会运行并在栈中带着字符串 cp cxuan lx ,这样就能够找出源文件和目标文件的名称。

当两个用户运行在相同程序中,例如编辑器(editor),那么就会在内存中保持编辑器程序代码的两个副本,但是这种方式并不高效。Linux 系统支持共享文本段作为替代。下面图中我们会看到 A 和 B 两个进程,它们有着相同的文本区域。



数据段和栈段只有在 fork 之后才会共享,共享也是共享未修改过的页面。如果任何一个都需要变大但是没有相邻空间容纳的话,也不会有问题,因为相邻的虚拟页面不必映射到相邻的物理页面上。

除了动态分配更多的内存,Linux 中的进程可以通过内存映射文件来访问文件数据。这个特性可以使我们把一个文件映射到进程空间的一部分而该文件就可以像位于内存中的字节数组一样被读写。把一个文件映射进来使得随机读写比使用 read 和 write 之类的 I/O 系统调用要容易得多。共享库的访问就是使用了这种机制。如下所示



我们可以看到两个相同文件会被映射到相同的物理地址上,但是它们属于不同的地址空间。

映射文件的优点是,两个或多个进程可以同时映射到同一文件中,任意一个进程对文件的写操作对其他文件可见。通过使用映射临时文件的方式,可以为多线程共享内存提供高带宽,临时文件在进程退出后消失。但是实际上,并没有两个相同的地址空间,因为每个进程维护的打开文件和信号不同。

Linux 内存管理系统调用

下面我们探讨一下关于内存管理的系统调用方式。事实上,POSIX 并没有给内存管理指定任何的系统调用。然而,Linux 却有自己的内存系统调用,主要系统调用如下:



如果遇到错误,那么 s 的返回值是 -1,a 和 addr 是内存地址,len 表示的是长度,prot 表示的是控制保护位,flags 是其他标志位,fd 是文件描述符,offset 是文件偏移量。

brk 通过给出超过数据段之外的第一个字节地址来指定数据段的大小。如果新的值要比原来的大,那么数据区会变得越来越大,反之会越来越小。

mmap 和 unmap 系统调用会控制映射文件。mmp 的第一个参数 addr 决定了文件映射的地址。它必须是页面大小的倍数。如果参数是 0,系统会分配地址并返回 a。第二个参数是长度,它告诉了需要映射多少字节。它也是页面大小的倍数。prot 决定了映射文件的保护位,保护位可以标记为 可读、可写、可执行或者这些的结合。第四个参数 flags 能够控制文件是私有的还是可读的以及 addr 是必须的还是只是进行提示。第五个参数 fd 是要映射的文件描述符。只有打开的文件是可以被映射的,因此如果想要进行文件映射,必须打开文件;最后一个参数 offset 会指示文件从什么时候开始,并不一定每次都要从零开始。

Linux 内存管理实现

内存管理系统是操作系统最重要的部分之一。从计算机早期开始,我们实际使用的内存都要比系统中实际存在的内存多。内存分配策略克服了这一限制,并且其中最有名的就是 虚拟内存(virtual memory)。通过在多个竞争的进程之间共享虚拟内存,虚拟内存得以让系统有更多的内存。虚拟内存子系统主要包括下面这些概念。

大地址空间


操作系统使系统使用起来好像比实际的物理内存要大很多,那是因为虚拟内存要比物理内存大很多倍。

保护

系统中的每个进程都会有自己的虚拟地址空间。这些虚拟地址空间彼此完全分开,因此运行一个应用程序的进程不会影响另一个。并且,硬件虚拟内存机制允许内存保护关键内存区域。

内存映射

内存映射用来向进程地址空间映射图像和数据文件。在内存映射中,文件的内容直接映射到进程的虚拟空间中。

公平的物理内存分配


内存管理子系统允许系统中的每个正在运行的进程公平分配系统的物理内存。

共享虚拟内存


尽管虚拟内存让进程有自己的内存空间,但是有的时候你是需要共享内存的。例如几个进程同时在 shell 中运行,这会涉及到 IPC 的进程间通信问题,这个时候你需要的是共享内存来进行信息传递而不是通过拷贝每个进程的副本独立运行。

下面我们就正式探讨一下什么是虚拟内存。

虚拟内存的抽象模型

在考虑 Linux 用于支持虚拟内存的方法之前,考虑一个不会被太多细节困扰的抽象模型是很有用的。

处理器在执行指令时,会从内存中读取指令并将其解码(decode),在指令解码时会获取某个位置的内容并将他存到内存中。然后处理器继续执行下一条指令。这样,处理器总是在访问存储器以获取指令和存储数据。

在虚拟内存系统中,所有的地址空间都是虚拟的而不是物理的。但是实际存储和提取指令的是物理地址,所以需要让处理器根据操作系统维护的一张表将虚拟地址转换为物理地址。

为了简单的完成转换,虚拟地址和物理地址会被分为固定大小的块,称为 页(page)。这些页有相同大小,如果页面大小不一样的话,那么操作系统将很难管理。Alpha AXP系统上的 Linux 使用 8 KB 页面,而 Intel x86 系统上的 Linux 使用 4 KB 页面。每个页面都有一个唯一的编号,即页面框架号(PFN)。



上面就是 Linux 内存映射模型了,在这个页模型中,虚拟地址由两部分组成:偏移量和虚拟页框号。每次处理器遇到虚拟地址时都会提取偏移量和虚拟页框号。处理器必须将虚拟页框号转换为物理页号,然后以正确的偏移量的位置访问物理页。

上图中展示了两个进程 A 和 B 的虚拟地址空间,每个进程都有自己的页表。这些页表将进程中的虚拟页映射到内存中的物理页中。页表中每一项均包含:
有效标志(valid flag) :表明此页表条目是否有效
该条目描述的物理页框号
访问控制信息,页面使用方式,是否可写以及是否可以执行代码


要将处理器的虚拟地址映射为内存的物理地址,首先需要计算虚拟地址的页框号和偏移量。页面大小为 2 的次幂,可以通过移位完成操作。

如果当前进程尝试访问虚拟地址,但是访问不到的话,这种情况称为 缺页异常,此时虚拟操作系统的错误地址和页面错误的原因将通知操作系统。

通过以这种方式将虚拟地址映射到物理地址,虚拟内存可以以任何顺序映射到系统的物理页面。

按需分页

由于物理内存要比虚拟内存少很多,因此操作系统需要注意尽量避免直接使用低效的物理内存。节省物理内存的一种方式是仅加载执行程序当前使用的页面(这何尝不是一种懒加载的思想呢?)。例如,可以运行数据库来查询数据库,在这种情况下,不是所有的数据都装入内存,只装载需要检查的数据。这种仅仅在需要时才将虚拟页面加载进内中的技术称为按需分页。

交换

如果某个进程需要将虚拟页面传入内存,但是此时没有可用的物理页面,那么操作系统必须丢弃物理内存中的另一个页面来为该页面腾出空间。

如果页面已经修改过,那么操作系统必须保留该页面的内容,以便以后可以访问它。这种类型的页面被称为脏页,当将其从内存中移除时,它会保存在称为交换文件的特殊文件中。相对于处理器和物理内存的速度,对交换文件的访问非常慢,并且操作系统需要兼顾将页面写到磁盘的以及将它们保留在内存中以便再次使用。

Linux 使用最近最少使用(LRU)页面老化技术来公平的选择可能会从系统中删除的页面,这个方案涉及系统中的每个页面,页面的年龄随着访问次数的变化而变化,如果某个页面访问次数多,那么该页就表示越 年轻,如果某个呃页面访问次数太少,那么该页越容易被换出。

物理和虚拟寻址模式


大多数多功能处理器都支持 物理地址模式和虚拟地址模式的概念。物理寻址模式不需要页表,并且处理器不会在此模式下尝试执行任何地址转换。Linux 内核被链接在物理地址空间中运行。

Alpha AXP 处理器没有物理寻址模式。相反,它将内存空间划分为几个区域,并将其中两个指定为物理映射的地址。此内核地址空间称为 KSEG 地址空间,它包含从 0xfffffc0000000000 向上的所有地址。为了从 KSEG 中链接的代码(按照定义,内核代码)执行或访问其中的数据,该代码必须在内核模式下执行。链接到 Alpha 上的 Linux内核以从地址 0xfffffc0000310000 执行。

访问控制


页面表的每一项还包含访问控制信息,访问控制信息主要检查进程是否应该访问内存。

必要时需要对内存进行访问限制。例如包含可执行代码的内存,自然是只读内存;操作系统不应允许进程通过其可执行代码写入数据。相比之下,包含数据的页面可以被写入,但是尝试执行该内存的指令将失败。大多数处理器至少具有两种执行模式:内核态和用户态。你不希望访问用户执行内核代码或内核数据结构,除非处理器以内核模式运行。

访问控制信息被保存在上面的 Page Table Entry ,页表项中,上面这幅图是 Alpha AXP的 PTE。位字段具有以下含义

V
表示 valid ,是否有效位

FOR
读取时故障,在尝试读取此页面时出现故障

FOW
写入时错误,在尝试写入时发生错误

FOE
执行时发生错误,在尝试执行此页面中的指令时,处理器都会报告页面错误并将控制权传递给操作系统,

ASM
地址空间匹配,当操作系统希望清除转换缓冲区中的某些条目时,将使用此选项。

GH
当在使用单个转换缓冲区条目而不是多个转换缓冲区条目映射整个块时使用的提示。

KRE
内核模式运行下的代码可以读取页面

URE
用户模式下的代码可以读取页面

KWE
以内核模式运行的代码可以写入页面

UWE
以用户模式运行的代码可以写入页面

页框号
对于设置了 V 位的 PTE,此字段包含此 PTE 的物理页面帧号(页面帧号)。对于无效的 PTE,如果此字段不为零,则包含有关页面在交换文件中的位置的信息。

除此之外,Linux 还使用了两个位

_PAGE_DIRTY
如果已设置,则需要将页面写出到交换文件中

_PAGE_ACCESSED
Linux 用来将页面标记为已访问。

缓存

上面的虚拟内存抽象模型可以用来实施,但是效率不会太高。操作系统和处理器设计人员都尝试提高性能。但是除了提高处理器,内存等的速度之外,最好的方法就是维护有用信息和数据的高速缓存,从而使某些操作更快。在 Linux 中,使用很多和内存管理有关的缓冲区,使用缓冲区来提高效率。

缓冲区缓存

缓冲区高速缓存包含块设备驱动程序使用的数据缓冲区。

还记得什么是块设备么?这里回顾下

块设备是一个能存储固定大小块信息的设备,它支持以固定大小的块,扇区或群集读取和(可选)写入数据。每个块都有自己的物理地址。通常块的大小在 512 - 65536 之间。所有传输的信息都会以连续的块为单位。块设备的基本特征是每个块都较为对立,能够独立的进行读写。常见的块设备有 硬盘、蓝光光盘、USB 盘

与字符设备相比,块设备通常需要较少的引脚。



缓冲区高速缓存通过设备标识符和块编号用于快速查找数据块。如果可以在缓冲区高速缓存中找到数据,则无需从物理块设备中读取数据,这种访问方式要快得多。

页缓存

页缓存用于加快对磁盘上图像和数据的访问

它用于一次一页地缓存文件中的内容,并且可以通过文件和文件中的偏移量进行访问。当页面从磁盘读入内存时,它们被缓存在页面缓存中。

交换区缓存


仅仅已修改(脏页)被保存在交换文件中

只要这些页面在写入交换文件后没有修改,则下次交换该页面时,无需将其写入交换文件,因为该页面已在交换文件中。可以直接丢弃。在大量交换的系统中,这节省了许多不必要的和昂贵的磁盘操作。

硬件缓存


处理器中通常使用一种硬件缓存。页表条目的缓存。在这种情况下,处理器并不总是直接读取页表,而是根据需要缓存页的翻译。这些是转换后备缓冲区 也被称为 TLB,包含来自系统中一个或多个进程的页表项的缓存副本。

引用虚拟地址后,处理器将尝试查找匹配的 TLB 条目。如果找到,则可以将虚拟地址直接转换为物理地址,并对数据执行正确的操作。如果处理器找不到匹配的 TLB 条目,它通过向操作系统发信号通知已发生 TLB 丢失获得操作系统的支持和帮助。系统特定的机制用于将该异常传递给可以修复问题的操作系统代码。操作系统为地址映射生成一个新的 TLB 条目。清除异常后,处理器将再次尝试转换虚拟地址。这次能够执行成功。

使用缓存也存在缺点,为了节省精力,Linux 必须使用更多的时间和空间来维护这些缓存,并且如果缓存损坏,系统将会崩溃。

Linux 页表


Linux 假定页表分为三个级别。访问的每个页表都包含下一级页表。



图中的 PDG 表示全局页表,当创建一个新的进程时,都要为新进程创建一个新的页面目录,即 PGD。

要将虚拟地址转换为物理地址,处理器必须获取每个级别字段的内容,将其转换为包含页表的物理页的偏移量,并读取下一级页表的页框号。这样重复三次,直到找到包含虚拟地址的物理页面的页框号为止。

Linux 运行的每个平台都必须提供翻译宏,这些宏允许内核遍历特定进程的页表。这样,内核无需知道页表条目的格式或它们的排列方式。

页分配和取消分配

对系统中物理页面有很多需求。例如,当图像加载到内存中时,操作系统需要分配页面。

系统中所有物理页面均由 mem_map 数据结构描述,这个数据结构是 mem_map_t 的列表。它包括一些重要的属性
count :这是页面的用户数计数,当页面在多个进程之间共享时,计数大于 1
age:这是描述页面的年龄,用于确定页面是否适合丢弃或交换
map_nr :这是此mem_map_t描述的物理页框号。


页面分配代码使用 free_area向量查找和释放页面,free_area的每个元素都包含有关页面块的信息。

页面分配


Linux 的页面分配使用一种著名的伙伴算法来进行页面的分配和取消分配。页面以 2 的幂为单位进行块分配。这就意味着它可以分配 1页、2 页、4页等等,只要系统中有足够可用的页面来满足需求就可以。判断的标准是nr_free_pages> min_free_pages,如果满足,就会在 free_area 中搜索所需大小的页面块完成分配。free_area 的每个元素都有该大小的块的已分配页面和空闲页面块的映射。

分配算法会搜索请求大小的页面块。如果没有任何请求大小的页面块可用的话,会搜寻一个是请求大小二倍的页面块,然后重复,直到一直搜寻完 free_area 找到一个页面块为止。如果找到的页面块要比请求的页面块大,就会对找到的页面块进行细分,直到找到合适的大小块为止。

因为每个块都是 2 的次幂,所以拆分过程很容易,因为你只需将块分成两半即可。空闲块在适当的队列中排队,分配的页面块返回给调用者。



如果请求一个 2 个页的块,则 4 页的第一个块(从第 4 页的框架开始)将被分成两个 2 页的块。第一个页面(从第 4 页的帧开始)将作为分配的页面返回给调用方,第二个块(从第 6 页的页面开始)将作为 2 页的空闲块排队到 free_area 数组的元素 1 上。

页面取消分配


上面的这种内存方式最造成一种后果,那就是内存的碎片化,会将较大的空闲页面分成较小的页面。页面解除分配代码会尽可能将页面重新组合成为更大的空闲块。每释放一个页面,都会检查相同大小的相邻的块,以查看是否空闲。如果是,则将其与新释放的页面块组合以形成下一个页面大小块的新的自由页面块。每次将两个页面块重新组合为更大的空闲页面块时,页面释放代码就会尝试将该页面块重新组合为更大的空闲页面。通过这种方式,可用页面的块将尽可能多地使用内存。

例如上图,如果要释放第 1 页的页面,则将其与已经空闲的第 0 页页面框架组合在一起,并作为大小为 2页的空闲块排队到 free_area 的元素 1 中。

内存映射


内核有两种类型的内存映射:共享型(shared)和私有型(private)。私有型是当进程为了只读文件,而不写文件时使用,这时,私有映射更加高效。但是,任何对私有映射页的写操作都会导致内核停止映射该文件中的页。所以,写操作既不会改变磁盘上的文件,对访问该文件的其它进程也是不可见的。

按需分页


一旦可执行映像被内存映射到虚拟内存后,它就可以被执行了。因为只将映像的开头部分物理的拉入到内存中,因此它将很快访问物理内存尚未存在的虚拟内存区域。当进程访问没有有效页表的虚拟地址时,操作系统会报告这项错误。

页面错误描述页面出错的虚拟地址和引起的内存访问(RAM)类型。

Linux 必须找到代表发生页面错误的内存区域的 vm_area_struct 结构。由于搜索 vm_area_struct 数据结构对于有效处理页面错误至关重要,因此它们以 AVL(Adelson-Velskii和Landis)树结构链接在一起。如果引起故障的虚拟地址没有 vm_area_struct 结构,则此进程已经访问了非法地址,Linux 会向进程发出 SIGSEGV 信号,如果进程没有用于该信号的处理程序,那么进程将会终止。

然后,Linux 会针对此虚拟内存区域所允许的访问类型,检查发生的页面错误类型。如果该进程以非法方式访问内存,例如写入仅允许读的区域,则还会发出内存访问错误信号。现在,Linux 已确定页面错误是合法的,因此必须对其进行处理。

说说Linux的用户内存上限

首先提个问题:64bit x86 Linux中用户空间最大可以使用多少物理内存?根据Kernel的定义,这个值是64T,Redhat官方给的是12T,但含糊的说了64T的限制。那64bit x86的寻址位宽是多少?答:48bit,理论上应该是支持2<<48=256T才是,Linux kernel为什么会强制限定了一个64T的最大内存?

避免复杂化,在此一律不谈大页内存;一律只针对当下主流48bit内存空间和IA32e地址扩展的x86_64 CPU。还是先从逼死强迫症的48bit内存地址位宽说起吧。按道理讲,64bit的CPU,内存地址就应该是64bit/8byte。但这48bit地址是一个x86平台上很有名的历史包袱之一,其中的0~47位为物理内存地址,48~63这16位必须是与第47位保持一致。在编程的层面上表现为所有的内存指针的前16位都是0。X86_64本身并不是一个完全意义上的64位地址空间,考虑向下兼容,内存采用了IA32的扩展方式IA32e,这诡异的设定源自于IA32的多级页表。

先开始讲页表跳转。Intel SDM vol3 figure 4-8 给了一张图。 (4-9,4-10分别是3层转换的2M大页和两层转换的1G大页)



当前内存的管理是通过页表转换的模式实现的,64bit系统当前是4级(跳)页表。48bit带宽的前36bit分为4段,每段9bit页表,后12bit对应的正是一个内存页(即4K)大小。

又一个逼死强迫症的8bit+1bit,又是32bit时代的遗传病。这9bit类似于指针操作,不断地指向的下一级内存地址。亲身经历,为了调试一个底层的bug,当时一个会议室15个人的组了hackthon,在完全解读了转换算法的情况下,一个小时里愣是没有一个人通过人肉的计算成功的完成地址页表转换,可见算法之反人类。


几十年前比尔盖兹说过一句:“PC只要640K的内存就够了!”成为了他一生的黑点。当时的8088CPU只支持20bit的寻址0xFFFFF,正巧是一个8位和一个12位的组合。这48bit的强迫症种子从这个时候就埋下了。

等一下!20bit不明明是1M吗,为什是640K内存?——2位保留地址!8088将20bit中的两个bit作为保留地址,当时用来映射BIOS和外设。当时这两个bit带来的仅仅只是被这384K的划分的不连续空间,当这种历史包袱遗传到了现在的48bit,保留的地址空间范围一样存在。同样,48bit空间也要满足“两头顶格”的习惯,整个可用地址范围变成了0~0x7FFF FFFF FFFF和0x8000 0000 0000~0xFFFF FFFF FFFF两个不连续的地址空间上的的几个更加离散的小岛。以首位区分或者理解为正负符号,Linux Kernel使用“1”作为系统地址空间,使用“0”作为用户地址空间(小于47bit可分配给用户空间)。贴图来自SDM vol3 fuigure 3-3局部,实模式即8088模式也可以在此找到。



另一方面就是Linux kernel是用移位的方式确定用户可用内存地址空间范围,相比x86,Linux表现出了必须是2的n次方强迫症逻辑——既然已经注定凑不齐47bit(128T),甚至目前连64T内存的主机都没有,keep easy!那就索性再扣除一个bit,46bit = 64T。mm.txt如是说:
Current X86-64 implementations support up to 46 bits of address space (64 TB), which is our current limit. This expands into MBZ space in the page tables.
–Andi Kleen(Intel.)

眼看着64T内存的主机也越来越近了,再加一级页表,48bit变57bit是一种最偷懒的办法(0b111001,比48还迫害强迫症患者),但如果考虑到云计算的普及,虚拟主机的应用越来越多的成为了主流,页表转换到了VM场景下还要增加额外的虚拟页表跳转,更多的开销已经无法接受了,看来日后只有大页才是趋势。

“向下兼容”这四个字让x86这个平台充斥着各种不可理解;而Linux,每一行代码都有一个故事……

小结

1)、Linux 内存寻址
Linux 内核主要通过虚拟内存管理进程的地址空间,内核进程和用户进程都只会分配虚拟内存,不会分配物理内存,通过内存寻址将虚拟内存与物理内存做映射。Linux 内核中有三种地址:
a、逻辑地址,每个逻辑地址都由一段 (segment) 和偏移量 (offset) 组成,偏移量指明了从段开始的地方到实际地址之间的距离。
b、线性地址,又称虚拟地址,是一个 32 个无符号整数,32 位机器内存高达 4GB,通常用十六进制数字表示,Linux 进程的内存一般说的都是这个内存。
c、物理地址,用于内存芯片级内存单元寻址。它们与从 CPU 的地址引脚发送到内存总线上的电信号对应。

Linux 中的内存控制单元 (MMU) 通过一种称为分段单元 (segmentation unit) 的硬件电路把一个逻辑地址转换成线性地址,接着,第二个称为分页单元 (paging unit) 的硬件电路把线性地址转换成一个物理地址。

2)、Linux 分页机制
分页单元把线性地址转换成物理地址。线性地址被分成以固定长度为单位的组,称为页 (page)。页内部连续的线性地址被映射到连续的物理地址中。一般 "页" 既指一组线性地址,又指包含这组地址中的数据。分页单元把所有的 RAM 分成固定长度的页框 (page frame),也成物理页。每一页框包含一个页 (page),也就是说一个页框的长度与一个页的长度一致。页框是主存的一部分,因此也是一个存储区域。区分一页和一个页框是很重要的,前者只是一个数据块,可以存放任何页框或者磁盘中。把线性地址映射到物理地址的数据结构称为页表 (page table)。页表存放在主存中,并在启用分页单元之前必须有内核对页表进行适当的初始化。

x86_64 的 Linux 内核采用 4 级分页模型,一般一页 4K,4 种页表分别为:
a、页全局目录
b、页上级目录
c、页中间目录
d、页表

页全局目录包含若干页上级目录,页上级目录又依次包含若干页中间目录的地址,而页中间目录又包含若干页表的地址。每个页表项指向一个页框。线性地址被分成 5 部分。

3)、NUMA 架构
随着 CPU 进入多核时代,多核 CPU 通过一条数据总线访问内存延迟很大,因此 NUMA 架构应运而生,NUMA 架构全称为非一致性内存架构 (Non Uniform Memory Architecture),系统的物理内存被划分为几个节点 (node),每个 node 绑定不同的 CPU 核,本地 CPU 核直接访问本地内存 node 节点延迟最小。可以通过 lscpu 命令查看 NUMA 与 CPU 核的关系。

$ lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                32
...
NUMA node0 CPU(s):     0-7,16-23      #这些核绑定在numa 0
NUMA node1 CPU(s):     8-15,24-31     #这些核绑定在numa 1

4)、伙伴关系算法
Linux 内核通过著名伙伴关系算法为分配一组连续的页框而建立一种健壮、稳定的内存分配策略,是内核中一种内存分配器,并解决了内存管理外碎片的问题,外碎片是指频繁地请求和释放不同大小的一组连续页框,必然导致在已分配的页框的块分散了许多小块的空闲页框。

5)、Slab 机制
slab 机制的核心思想是以对象的观点来管理内存,主要是为了解决内部碎片,内部碎片是由于采用固定大小的内存分区,即以固定的大小块为单位来分配,采用这种方法,进程所分配的内存可能会比所需要的大,这多余的部分便是内部碎片。slab 也是内核中一种内存分配器,slab 分配器基于对象进行管理的,所谓的对象就是内核中的数据结构(例如:task_struct,file_struct 等)。相同类型的对象归为一类,每当要申请这样一个对象时,slab 分配器就从一个 slab 列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免内部碎片。上面中说到的 dentry 对象就是通过 slab 分配器分配的一种对象。slab 和伙伴系统是上下级的调用关系,伙伴关系按照页管理内存,slab 按照字节管理,slab 先从伙伴系统获取数个页的内存,然后切成分成固定的小块(称为 object),然后再按照声明的对象数据结构分配对象。

6)、进程内存分布
所有进程都必须占用一定数量的内存,这些内存用来存放从磁盘载入的程序代码,或存放来自用户输入的数据等。内存可以提前静态分配和统一回收,也可以按需动态分配和回收。对于普通进程对应的内存空间包含 5 种不同的数据区:
a、代码段 (text):程序代码在内存中的映射,存放函数体的二进制代码,通常用于存放程序执行代码 (即 CPU 执行的机器指令)。
b、数据段 (data):存放程序中已初始化且初值不为 0 的全局变量和静态局部变量。数据段属于静态内存分配 (静态存储区),可读可写。
c、BSS 段 (bss):未初始化的全局变量和静态局部变量。
d、堆 (heap):动态分配的内存段,大小不固定,可动态扩张 (malloc 等函数分配内存),或动态缩减 (free 等函数释放)。
e、栈 (stack):存放临时创建的局部变量。

Linux 内核是操作系统中优先级最高的,内核函数申请内存必须及时分配适当的内存,用户态进程申请内存被认为是不紧迫的,内核尽量推迟给用户态的进程动态分配内存:
a、请求调页,推迟到进程要访问的页不在 RAM 中时为止,引发一个缺页异常。
b、写时复制 (COW),父、子进程共享页框而不是复制页框,但是共享页框不能被修改,只有当父/子进程试图改写共享页框时,内核才将共享页框复制一个新的页框并标记为可写。

7)、Linux 内存检测工具
a、free 命令可以监控系统内存
$ free -h
              total        used        free      shared  buff/cache   available
Mem:           31Gi        13Gi       8.0Gi       747Mi        10Gi        16Gi
Swap:         2.0Gi       321Mi       1.7Gi

b、top 命令查看系统内存以及进程内存
(1).VIRT Virtual Memory Size (KiB):进程使用的所有虚拟内存,包括代码(code)、数据(data)、共享库(shared libraries),以及被换出(swap out)到交换区和映射了(map)但尚未使用(未载入实体内存)的部分。
(2).RES Resident Memory Size (KiB):进程所占用的所有实体内存(physical memory),不包括被换出到交换区的部分。
(3).SHR Shared Memory Size (KiB):进程可读的全部共享内存,并非所有部分都包含在 RES 中。它反映了可能被其他进程共享的内存部分。

c、smaps 文件
通过 more /proc/$pid/smaps 查看某进程虚拟内存空间的分布情况。

d、vmstat
vmstat 是 Virtual Meomory Statistics(虚拟内存统计)的缩写,可实时动态监视操作系统的虚拟内存、进程、CPU 活动。

## 每秒统计3次
$ vmstat 1 3
procs -----------memory---------------- ---swap-- -----io---- --system-- -----cpu-----
...

e、meminfo 文件
Linux 系统中 /proc/meminfo 这个文件用来记录了系统内存使用的详细情况。

curl-7.19.7 版本在发送 HTTPS 请求时,所依赖的 NSS 库存在 dentry 泄漏的 bug;通过查看/proc/meminfo 中的 Slab 内核内存是否有泄漏,通过 slabtop 命令分析 slab 发现内核中 dentry 对象占比高,考虑到 dentry 对象跟文件有关,Linux 中一切皆可以为文件,这个可能跟 socket 文件有关。可以设置环境变量 NSS_SDB_USE_CACHE 修复这个 bug。