linux内核---内存子系统
2025-04-20 17:44:33 0 举报
AI智能生成
linux内核内存子系统全景图,持续更新
作者其他创作
大纲/内容
虚拟内存&页表<br>
Why
假设现在没有虚拟内存地址,我们在程序中对内存的操作全都都是使用物理内存地址,在这种情况下,程序员就需要精确的知道每一个变量在内存中的具体位置,我们需要手动对物理内存进行布局,明确哪些数据存储在内存的哪些位置,除此之外我们还需要考虑为每个进程究竟要分配多少内存?内存紧张的时候该怎么办?如何避免进程与进程之间的地址冲突?等等一系列复杂且琐碎的细节。
虚拟内存引入之后,进程的视角就会变得非常开阔,每个进程都拥有自己独立的虚拟地址空间,进程与进程之间的虚拟内存地址空间是相互隔离,互不干扰的。每个进程都认为自己独占所有内存空间,自己想干什么就干什么。
背后原理
程序的局部性
时间局部性是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某块数据被访问,则不久之后该数据可能再次被访问。空间局部性是指一旦程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问。<br>
根据程序局部性原理,在某一段时间内,进程真正需要的物理内存其实是很少的一部分,我们只需要为每个进程分配很少的物理内存就可以保证进程的正常执行运转。
补充
高速缓存是计算机科学中唯一重要的思想。事实上,髙速缓存技术确实极大地影响了计算机系统的设计。“快表、 页高速缓存以及虚拟内存技术从广义上讲,都是属于高速缓存技术。
时间局部性是通过将近来使用的指令和数据保存到高速缓存存储器中,并使用高速缓存的层次结构实现。空间局部性通常是使用较大的高速缓存,并将预取机制集成到高速缓存控制逻辑中实现。虚拟内存技术实际上就是建立了 “内存一外存”的两级存储器的结构,利用局部性原理实现髙速缓存。
CPU地址翻译<br>(虚拟地址与物理地址之间的关系)<br>
地址翻译
MMU负责把CPU发出的虚拟地址翻译成物理地址
分段机制
好处就是能产生连续的内存空间,解决了程序本身不需要关心具体的物理内存地址的问题
问题
第一个就是内存碎片的问题。<br>第二个就是内存交换的效率低的问题。
内存分段管理可以做到段根据实际需求分配内存,所以有多少需求就分配多大的段,所以不会出现内部内存碎片。
但是由于每个段的长度不固定,所以多个段未必能恰好使用所有的内存空间,会产生了多个不连续的小物理内存,导致新的程序无法被装载,所以会出现外部内存碎片的问题。
<b>解决「外部内存碎片」的问题就是内存交换</b>。如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿。
分页机制
分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。在 Linux 下,每一页的大小为 4KB。
解决了分段的大部分问题
采用了分页,页与页之间是紧密排列的,所以不会有外部碎片;一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换的效率相对比较高。
但是,因为内存分页机制分配内存的最小单位是一页,即使程序不足一页大小,我们最少只能分配一个页,所以页内会出现内存浪费,所以针对内存分页机制会有内部内存碎片的现象。
映射方式
<ul><li>把虚拟内存地址,切分成页号和偏移量;</li><li>根据页号,从页表里面,查询对应的物理页号;</li><li>直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。</li></ul>
多级页表
单级页表的问题
在 32 位的环境下,虚拟地址空间共有 4GB,假设一个页的大小是 4KB(2^12),那么就需要大约 100 万 (2^20) 个页,每个「页表项」需要 4 个字节大小来存储,那么整个 4GB 空间的映射就需要有 4MB 的内存来存储页表。
二级分页:如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。做个简单的计算,假设只有 20% 的一级页表项被用到了,那么页表占用的内存空间就只有 4KB(一级页表) + 20% * 4MB(二级页表)= 0.804MB,这对比单级页表的 4MB 是不是一个巨大的节约?
<b>页表一定要覆盖全部虚拟地址空间</b>,不分级的页表就需要有 100 多万个页表项来映射,而二级分页则只需要 1024 个页表项(此时一级页表覆盖到了全部虚拟地址空间,二级页表在需要时创建)。 <b> 对局部性原理的充分应用。</b>
32位系统:二级分页 10+10+12
64位系统:四级页表 9+9+9+9+12
全局页目录项 PGD(Page Global Directory);<br>上层页目录项 PUD(Page Upper Directory);<br>中间页目录项 PMD(Page Middle Directory);<br>页表项 PTE(Page Table Entry);
子主题
页表项PTE
进程页表
立即映射与延迟绑定
按需页面分配
OS自身页表
TLB 页表缓存
多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了几道转换的工序,这显然就降低了这俩地址转换的速度,也就是带来了时间上的开销。<br><br>利用程序局部性原理 和 缓存思想,把最常访问的几个页表项存储到访问速度更快的硬件:MMU中的TLB<br><br>有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。TLB 的命中率其实是很高的,因为程序最常访问的页就那么几个。<br>
包括
L1:指令TLB、数据TLB
L2:指令、数据TLB
大页
进程 的虚拟内存地址空间
用户态空间的段
cat /proc/pid/maps 或者 pmap pid 来查看某个进程的实际虚拟内存布局。
32位机器
内核中使用 start_brk 标识堆的起始位置,brk 标识堆当前的结束位置。当堆申请新的内存空间时,只需要将 brk 指针增加对应的大小,回收地址时减少对应的大小即可。比如当我们通过 malloc 向内核申请很小的一块内存时(128K 之内),就是通过改变 brk 位置实现的。<br><br>堆空间的上边是一段待分配区域,用于扩展堆空间的使用。接下来就来到了文件映射与匿名映射区域。进程运行时所依赖的动态链接库中的代码段,数据段,BSS 段就加载在这里。还有我们调用 mmap 映射出来的一段虚拟内存空间也保存在这个区域。注意:在文件映射与匿名映射区的地址增长方向是从高地址向低地址增长。<br><br>接下来用户态虚拟内存空间的最后一块区域就是栈空间了,在这里会保存函数运行过程所需要的局部变量以及函数参数等函数调用信息。栈空间中的地址增长方向是从高地址向低地址增长。每次进程申请新的栈地址时,其地址值是在减少的。<br><br>在内核中使用 start_stack 标识栈的起始位置,RSP 寄存器中保存栈顶指针 stack pointer,RBP 寄存器中保存的是栈基地址。
64位机器
在低 128T 的用户态地址空间:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF F000 范围中,所以虚拟内存地址的高 16 位全部为 0 。<br><br>如果一个虚拟内存地址的高 16 位全部为 0 ,那么我们就可以直接判断出这是一个用户空间的虚拟内存地址。<br><br>同样的道理,在高 128T 的内核态虚拟内存空间:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 范围中,所以虚拟内存地址的高 16 位全部为 1 。<br><br>也就是说内核态的虚拟内存地址的高 16 位全部为 1 ,如果一个试图访问内核的虚拟地址的高 16 位不全为 1 ,则可以快速判断这个访问是非法的。<br><br>这个高 16 位的空闲地址被称为 canonical 。如果虚拟内存地址中的高 16 位全部为 0 (表示用户空间虚拟内存地址)或者全部为 1 (表示内核空间虚拟内存地址),这种地址的形式我们叫做 canonical form,对应的地址我们称作 canonical address 。<br><br>那么处于 <b>canonical address 空洞 :0x0000 7FFF FFFF F000 - 0xFFFF 8000 0000 0000</b> 范围内的地址的高 16 位 不全为 0 也不全为 1 。如果某个虚拟地址落在这段 canonical address 空洞区域中,那就是既不在用户空间,也不在内核空间,肯定是非法访问了。
虚拟内存空间管理
内核数据结构基础
task_struct → mm_struct这个结构体中包含了前边几个小节中介绍的进程虚拟内存空间的全部信息。
fork进程时mm_struct 结构会随着进程描述符 task_struct 的创建而创建。
子进程在新创建出来之后它的虚拟内存空间是和父进程的虚拟内存空间一模一样的,直接拷贝过来。
copy_mm 函数首先会将父进程的虚拟内存空间 current->mm 赋值给指针 oldmm。然后通过 dup_mm 函数将父进程的虚拟内存空间以及相关页表拷贝到子进程的 mm_struct 结构中。最后将拷贝出来的 mm_struct 赋值给子进程的 task_struct 结构。
是否共享地址空间几乎是进程和线程之间的本质区别。Linux 内核并不区别对待它们,线程对于内核来说仅仅是一个共享特定资源的进程而已。<br><br>内核线程和用户态线程的区别就是内核线程没有相关的内存描述符 mm_struct ,内核线程对应的 task_struct 结构中的 mm 域指向 Null,所以内核线程之间调度是不涉及地址空间切换的。<br><br><b>父进程与子进程的区别,进程与线程的区别,以及内核线程与用户态线程的区别其实都是围绕着这个 mm_struct 展开的。</b><br>
内核如何划分用户态和内核态虚拟内存空间
数据结构
32位
task_size 变量 = 0xC000 0000<br>
64位
task_size 为 0x0000 7FFF FFFF F000
内核如何布局进程虚拟内存空间
数据结构
start_code 和 end_code 定义代码段的起始和结束位置,程序编译后的二进制文件中的机器码被加载进内存之后就存放在这里。<br><br>start_data 和 end_data 定义数据段的起始和结束位置,二进制文件中存放的全局变量和静态变量被加载进内存中就存放在这里。<br><br>后面紧挨着的是 BSS 段,用于存放未被初始化的全局变量和静态变量,这些变量在加载进内存时会生成一段 0 填充的内存区域 (BSS 段), BSS 段的大小是固定的,<br><br>下面就是 OS 堆了,在堆中内存地址的增长方向是由低地址向高地址增长, start_brk 定义堆的起始位置,brk 定义堆当前的结束位置。<br><br>我们使用 malloc 申请小块内存时(低于 128K),就是通过改变 brk 位置调整堆大小实现的。<br><br>接下来就是内存映射区,在内存映射区内存地址的增长方向是由高地址向低地址增长,mmap_base 定义内存映射区的起始地址。进程运行时所依赖的动态链接库中的代码段,数据段,BSS 段以及我们调用 mmap 映射出来的一段虚拟内存空间就保存在这个区域。<br><br>start_stack 是栈的起始位置在 RBP 寄存器中存储,栈的结束位置也就是栈顶指针 stack pointer 在 RSP 寄存器中存储。在栈中内存地址的增长方向也是由高地址向低地址增长。<br><br>arg_start 和 arg_end 是参数列表的位置, env_start 和 env_end 是环境变量的位置。它们都位于栈中的最高地址处。<br>
在 mm_struct 结构体中除了上述用于划分虚拟内存区域的变量之外,还定义了一些虚拟内存与物理内存映射内容相关的统计变量,操作系统会把物理内存划分成一页一页的区域来进行管理,所以物理内存到虚拟内存之间的映射也是按照页为单位进行的。这部分内容我会在后续的文章中详细介绍,大家这里只需要有个概念就行。<br><br>mm_struct 结构体中的 total_vm 表示在进程虚拟内存空间中总共与物理内存映射的页的总数。<br><br>注意映射这个概念,它表示只是将虚拟内存与物理内存建立关联关系,并不代表真正的分配物理内存。<br><br>当内存吃紧的时候,有些页可以换出到硬盘上,而有些页因为比较重要,不能换出。locked_vm 就是被锁定不能换出的内存页总数,pinned_vm 表示既不能换出,也不能移动的内存页总数。<br><br>data_vm 表示数据段中映射的内存页数目,exec_vm 是代码段中存放可执行文件的内存页数目,stack_vm 是栈中所映射的内存页数目,这些变量均是表示进程虚拟内存空间中的虚拟内存使用情况。
内核如何管理虚拟内存区域
数据结构
<ul><li>vm_area_struct是描述进程地址空间的基本管理单元</li><li>vm_area_struct 结构描述的是 [vm_start,vm_end) 这样一段左闭右开的虚拟内存区域。</li></ul>
定义虚拟内存区域的访问权限和行为规范
<ul><li>vm_page_prot 和 vm_flags 都是用来标记 vm_area_struct 结构表示的这块虚拟内存区域的访问权限和行为规范。</li><li>vm_page_prot 偏向于定义底层内存管理架构中页这一级别的访问控制权限</li><li>vm_flags 则偏向于定于整个虚拟内存区域的访问权限以及行为规范</li></ul>
vm_flags 访问权限<br>------------------------<br>VM_READ 可读<br>VM_WRITE 可写<br>VM_EXEC 可执行<br>VM_SHARD 可多进程之间共享:设置这个值即为 mmap 的共享映射,不设置的话则为私有映射。<br>VM_IO 可映射至设备 IO 空间:通常在设备驱:动程序执行 mmap 进行 IO 空间映射时才会被设置。<br>VM_RESERVED 内存区域不可被换出:在内存紧张的时候,这块虚拟内存区域非常重要,不能被换出到磁盘中。<br>VM_SEQ_READ 内存区域可能被顺序访问:内核会根据实际情况决定预读后续的内存页数<br>VM_RAND_READ 内存区域可能被随机访问:内核则会根据实际情况减少预读的内存页数甚至停止预读。<br> 可以通过 posix_fadvise,madvise 系统调用来暗示内核是否对相关内存区域进行顺序读取或者随机读取<br>
关联内存映射中的映射关系
三个属性 anon_vma,vm_file,vm_pgoff 分别和虚拟内存映射相关,虚拟内存区域可以映射到物理内存上,也可以映射到文件中,映射到物理内存上我们称之为<b>匿名映射</b>,映射到文件中我们称之为<b>文件映射。</b>
当我们调用 malloc 申请内存时,如果申请的是小块内存(低于 128K)则会使用 do_brk() 系统调用通过调整堆中的 brk 指针大小来增加或者回收堆内存。
如果申请的是比较大块的内存(超过 128K)时,则会调用 mmap 在上图虚拟内存空间中的文件映射与匿名映射区创建出一块 VMA 内存区域(这里是匿名映射)。这块匿名映射区域就用 struct anon_vma 结构表示。
当调用 mmap 进行文件映射时,vm_file 属性就用来关联被映射的文件。这样一来<b>虚拟内存区域就与映射文件关联了起来</b>。vm_pgoff 则表示映射进虚拟内存中的文件内容,在文件中的偏移。
针对虚拟内存区域的相关操作
<ul><li>当指定的虚拟内存区域被加入到进程虚拟内存空间中时,open 函数会被调用</li><li>当虚拟内存区域 VMA 从进程虚拟内存空间中被删除时,close 函数会被调用</li><li>当进程访问虚拟内存时,访问的页面不在物理内存中,可能是未分配物理内存也可能是被置换到磁盘中,这时就会产生<b>缺页异常,fault 函数</b>就会被调用。</li><li>当一个只读的页面将要变为可写时,page_mkwrite 函数会被调用。<br>struct vm_operations_struct 结构中定义的都是对虚拟内存区域 VMA 的相关操作函数指针</li></ul><br>
虚拟内存区域在内核中是如何被组织的
内核中其实是通过一个 struct vm_area_struct 结构的双向链表将虚拟内存空间中的这些虚拟内存区域 VMA 串联起来的
程序编译后的二进制文件如何映射到虚拟内存空间中
磁盘文件中的段我们叫做 Section,内存中的段我们叫做 Segment,也就是内存区域。<br>那么这些 ELF 格式的二进制文件中的 Section 是如何加载并映射进虚拟内存空间的呢?<br><br>内核中完成这个映射过程的函数是 load_elf_binary ,这个函数的作用很大,加载内核的是它,启动第一个用户态进程 init 的是它,fork 完了以后,调用 exec 运行一个二进制程序的也是它。当 exec 运行一个二进制程序的时候,除了解析 ELF 的格式之外,另外一个重要的事情就是建立上述提到的内存映射。<br>
setup_new_exec 设置虚拟内存空间中的内存映射区域起始地址 mmap_base<br><br>setup_arg_pages 创建并初始化栈对应的 vm_area_struct 结构。置 mm->start_stack 就是栈的起始地址也就是栈底,并将 mm->arg_start 是指向栈底的。<br><br>elf_map 将 ELF 格式的二进制文件中.text ,.data,.bss 部分映射到虚拟内存空间中的代码段,数据段,BSS 段中。<br><br>set_brk 创建并初始化堆对应的的 vm_area_struct 结构,设置 current->mm->start_brk = current->mm->brk,设置堆的起始地址 start_brk,结束地址 brk。 起初两者相等表示堆是空的。<br><br>load_elf_interp 将进程依赖的动态链接库 .so 文件映射到虚拟内存空间中的内存映射区域<br><br>初始化内存描述符 mm_struct
内核 的虚拟内存地址空间
内核态虚拟内存空间是所有进程共享的,不同进程进入内核态之后看到的虚拟内存空间全部是一样的。
32位
直接映射区
3G -- 3G + 896m 这块 896M 大小的虚拟内存会直接映射到 0 - 896M 这块 896M 大小的物理内存上,这块区域中的虚拟内存地址直接减去 0xC000 0000 (3G) 就得到了物理内存地址。所以我们称这块区域为直接映射区。
虽然这块区域中的虚拟地址是直接映射到物理地址上,但是内核在访问这段区域的时候还是走的虚拟内存地址,内核也会为这块空间建立映射页表
保存的内容
前 1M 已经在系统启动的时候被系统占用,1M 之后的物理内存存放的是内核代码段,数据段,BSS 段
进程相关的数据结构
内核栈
内核栈容量小而且是固定的
X86 体系结构下,ISA 总线的 DMA (直接内存存取)控制器,只能对内存的前16M 进行寻址,这就导致了 ISA 设备不能在整个 32 位地址空间中执行 DMA,只能使用物理内存的前 16M 进行 DMA 操作。<br><br>因此直接映射区的前 16M 专门让内核用来为 DMA 分配内存,这块 16M 大小的(物理)内存区域我们称之为 ZONE_DMA。<br>直接映射区中剩下的部分也就是从 16M 到 896M(不包含 896M)这段区域,我们称之为 ZONE_NORMAL<br>
ZONE_HIGHMEM 高端内存
物理内存 896M 以上的区域被内核划分为 ZONE_HIGHMEM 区域,我们称之为高端内存。
物理内存假设为 4G,高端内存区域为 4G - 896M = 3200M<br>而内核剩余可用的虚拟内存空间只有 1G - 896M = 128M<br>
只能采用动态映射的方式映射到 128M 大小的内核虚拟内存空间中,也就是说只能动态的一部分一部分的分批映射,先映射正在使用的这部分,使用完毕解除映射,接着映射其他部分。
vmalloc 动态映射区
vmalloc 分配的内存在虚拟内存上是连续的,但是物理内存是不连续的。通过页表来建立物理内存与虚拟内存之间的映射关系
永久映射区
允许建立与物理高端内存的长期映射关系。比如内核通过 alloc_pages() 函数在物理内存的高端内存中申请获取到的物理内存页,这些物理内存页可以通过调用 kmap 映射到永久映射区中。
固定映射区
在固定映射区中的虚拟内存地址可以自由映射到物理内存的高端地址上,但是与动态映射区以及永久映射区不同的是,在固定映射区中虚拟地址是固定的,而被映射的物理地址是可以改变的。
在内核的启动过程中,有些模块需要使用虚拟内存并映射到指定的物理地址上,而且这些模块也没有办法等待完整的内存管理模块初始化之后再进行地址映射。因此,内核固定分配了一些虚拟地址,这些地址有固定的用途,使用该地址的模块在初始化的时候,将这些固定分配的虚拟地址映射到指定的物理地址上去。
临时映射区
64位
物理内存
架构
node、zone、page
物理页帧管理
node
zone
struct zone结构体
一个node下的典型区域
内核会根据各个物理内存区域的功能不同,将 NUMA 节点内的物理内存划分为:<br><b>ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGHMEM</b> 这几个物理内存区域。<br>
ZONE_DMA DMA使用的页 <16 ZONE_NORMAL 可正常寻址的页 16 ~896 ZONE_HIGHMEM 动态映射的页 >896 执行DMA操作的内存必须从ZONE_DMA区分配 一般内存,既可从ZONE_DMA,也可从ZONE_NORMAL分配,但不能同时从两个区分配;
内核会为每一个内存区域<b>分配一个伙伴系统</b>用于管理该内存区域下物理内存的分配和释放
预留内存
防止更高位的内存区域对自己的内存空间进行过多的侵占挤压
一些用于常规用途的物理内存则可以从多个物理内存区域中进行分配,当 ZONE_HIGHMEM 区域中的内存不足时,内核可以从 ZONE_NORMAL 进行内存分配,ZONE_NORMAL 区域内存不足时可以进一步降级到 ZONE_DMA 区域进行分配。<br><br>但内核不会允许高位内存区域对低位内存区域的无限制挤压占用,因为毕竟低位内存区域有它特定的用途,所以每个内存区域会给自己预留一定的内存,防止被高位内存区域挤压占用。而每个内存区域为自己预留的这部分内存就存储在 lowmem_reserve 数组中。
/proc/sys/vm/lowmem_reserve_ratio
page
和PFN(page frame number)一一对应
page_to_pfn 与 pfn_to_page
struct page
struct page 结构是内核中访问最为频繁的一个结构体,包含了大量的 union 结构
子主题
尽管处理器的最小可寻址单位通常为字或字节,但内存管理单元(MMU,把虚拟地址转换为物理地址的硬件设备)通常以页为单位处理。内核用struct page结构体表示每个物理页,struct page结构体占40个字节,假定系统物理页大小为4KB,对于4GB物理内存,1M个页面,故所有的页面page结构体共占有内存大小为40MB,相对系统4G,这个代价并不高。
* flags:页标志包含是不是脏的,是否被锁定等等,每一位单独表示一种状态,可同时表示出32种不同状态,定义在<linux/page-flags.h> * _count:计数值为-1表示未被使用。 * virtual:页在虚拟内存中的地址,对于不能永久映射到内核空间的内存(比如高端内存),该值为NULL;需要事必须动态映射这些内存。
两种映射
通常所说的内存映射是<b>正向映射</b>,即从虚拟内存到物理内存的映射。而<b>反向映射则是从物理内存到虚拟内存的映射</b>,用于当某个物理内存页需要进行回收或迁移时,此时需要去找到这个物理页被映射到了哪些进程的虚拟地址空间中,并断开它们之间的映射。
物理页属性 flag标志位
高 8 位用来表示 struct page 的定位信息(section、node、zone)
低位特定的标志位
PG_locked 表示该物理页面已经被锁定,如果该标志位置位,说明有使用者正在操作该 page , 则内核的其他部分不允许访问该页, 这可以防止内存管理出现竞态条件,例如:在从硬盘读取数据到 page 时。<br><br>PG_mlocked 表示该物理内存页被进程通过 mlock 系统调用锁定常驻在内存中,不会被置换出去。<br><br>PG_referenced 表示该物理页面刚刚被访问过。<br><br>PG_active 表示该物理页位于 active list 链表中。PG_referenced 和 PG_active 共同控制了系统使用该内存页的活跃程度,在内存回收的时候这两个信息非常重要。<br><br>PG_uptodate 表示该物理页的数据已经从块设备中读取到内存中,并且期间没有出错。<br><br>PG_readahead 当进程在顺序访问文件的时候,内核会预读若干相邻的文件页数据到 page 中,物理页 page 结构设置了该标志位,表示它是一个正在被内核预读的页。相关详细内容可回看我之前的这篇文章 《从 Linux 内核角度探秘 JDK NIO 文件读写本质》(opens new window)<br><br>PG_dirty 物理内存页的脏页标识,表示该物理内存页中的数据已经被进程修改,但还没有同步会磁盘中。我在 《从 Linux 内核角度探秘 JDK NIO 文件读写本质》 (opens new window)一文中也详细介绍过。<br><br>PG_lru 表示该物理内存页现在被放置在哪个 lru 链表上,比如:是在 active list 链表中 ? 还是在 inactive list 链表中 ?<br><br>PG_highmem 表示该物理内存页是在高端内存中。<br><br>PG_writeback 表示该物理内存页正在被内核的 pdflush 线程回写到磁盘中。详情可回看文章《从 Linux 内核角度探秘 JDK NIO 文件读写本质》 (opens new window)。<br><br>PG_slab 表示该物理内存页属于 slab 分配器所管理的一部分。<br><br>PG_swapcache 表示该物理内存页处于 swap cache 中。 struct page 中的 private 指针这时指向 swap_entry_t 。<br><br>PG_reclaim 表示该物理内存页已经被内核选中即将要进行回收。<br><br>PG_buddy 表示该物理内存页是空闲的并且被伙伴系统所管理。<br><br>PG_compound 表示物理内存页属于复合页的其中一部分。<br><br>PG_private 标志被置位的时候表示该 struct page 结构中的 private 指针指向了具体的对象。不同场景指向的对象不同。
除此之外内核还定义了一些标准宏,用来检查某个物理内存页 page 是否设置了特定的标志位,以及对这些标志位的操作,这些宏在内核中的实现都是原子的,命名格式如下:<br><br>PageXXX(page):检查 page 是否设置了 PG_XXX 标志位<br><br>SetPageXXX(page):设置 page 的 PG_XXX 标志位<br><br>ClearPageXXX(page):清除 page 的 PG_XXX 标志位<br><br>TestSetPageXXX(page):设置 page 的 PG_XXX 标志位,并返回原值
冷热页、页迁移
热页就是已经加载进 CPU 高速缓存中的物理内存页,所谓的冷页就是还未加载进 CPU 高速缓存中的物理内存页,冷页是热页的后备选项
内核为了最大程度的防止内存碎片,将物理内存页面按照是否可迁移的特性分为了多种迁移类型:<b>可迁移,可回收,不可迁移</b>。在 struct per_cpu_pages 结构中,每一种迁移类型都会对应一个冷热页链表
Slab 对象池相关属性
基本原理是从伙伴系统中申请一整页内存,然后划分成多个大小相等的小块内存被 slab 所管理。这样一来 <b>slab 就和物理内存页 page 发生了关联</b>,由于 slab 管理的单元是物理内存页 page 内进一步划分出来的小块内存,所以当 page 被分配给相应 slab 结构之后,<b>struct page 里也会存放 slab 相关的一些管理数据。</b>
物理内存模型
定义
内核中如何组织管理物理内存页 struct page 的方式
分类
FLATMEM平坦内存模型
只适合管理一整块连续的物理内存
DISCONTIGMEM 非连续内存模型
内核将物理内存从宏观上划分成了一个一个的节点 node <br>(微观上还是一页一页的物理页),每个 node 节点管理一块连续的物理内存<br>
在每个 node 节点中还是采用 FLATMEM 平坦内存模型的方式来组织管理物理内存页
避免了为内存地址空洞分配 struct page 结构,从而节省了内存资源的开销
SPARSEMEM 稀疏内存模型
每个 node 中的物理内存也不一定都是连续的<br>支持内存的热插拔(hotplug)功能。<br>
是对粒度更小的连续内存块进行精细的管理,用于管理连续内存块的单元被称作 section 。<br>物理页大小为 4k 的情况下, section 的大小为 128M <br>
物理内存架构
从 CPU 访问物理内存的角度来看一下物理内存的架构
分类
一致性内存访问 UMA 架构
同一个 CPU 对所有内存的访问的速度是一样的
非一致性内存访问 NUMA 架构
在 NUMA 架构下,只有 DISCONTIGMEM 非连续内存模型和<br> SPARSEMEM 稀疏内存模型是可用的<br>
每个 CPU 都有属于自己的本地内存节点,CPU 访问自己的本地内存不需要经过总线,因此访问速度是最快的。当 CPU 自己的本地内存不足时,CPU 就需要跨节点去访问其他内存节点,这种情况下 CPU 访问内存就会慢很多。
内存的物理结构
物理内存热插拔
子主题
分配器
内存分配器设计原则
更高的内存资源利用率
减少内外部碎片
更好的性能
1、基于位图的连续物理页分配
2、伙伴系统
管理空闲内存
分配连续的物理页
向下分裂;向上合并
3、SLAB(SLUB、SLOB)分配器
分配小内存,只分配固定大小内存块
4、CMA
内存申请、分配、释放
基本原则
按需分配 与 缺页异常
实际的物理内存只有当进程真的去访问新获取的虚拟地址时,才会由“请求页机制”产生“缺页”异常,从而进入分配实际页面的例程。 该异常是虚拟内存机制赖以存在的基本保证——它会告诉内核去真正为进程分配物理页,并建立对应的页表,这之后虚拟地址才实实在在地映射到了系统的物理内存上。(当然,如果页被换出到磁盘,也会产生缺页异常,不过这时不用再建立页表了)
brk()和mmap()只创建了地址空间,没有建立映射。缺页异常分配物理页,将物理页框号填入 PTE
用户态内存操作
栈内存分配
默认分配8MB
堆内存分配(动态分配)
malloc/free系列命令
C库调用系统调用
≤128K,brk()
通过 brk() 函数将「堆顶」指针向高地址移动,获得新的内存空间<br>
>128K,mmap()
mmap() 系统调用中「私有匿名映射」的方式,在文件映射区分配一块内存,也就是从文件映射区“偷”了一块内存
不全部用mmap的原因
频繁通过 mmap 分配的内存话,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。
brk机制释放后还缓存在内存池中,等下次在申请内存的时候,就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页
不全部用brk的原因
小块内存申请释放,堆内将产生越来越多不可用的碎片,申请大内存时只能继续申请新的大块堆内存
函数原型
void* malloc (size_t size);
以字节为单位。在使用malloc时,一般参数传递的形式为(sizeof(要开辟的变量名)*要开辟的个数)
如果在malloc()函数在开辟的过程中遇到了无法分配请求的内存块<br>(即遇到了开辟失败的情况),那么就会返回一个NULL指针,因此malloc的返回值一定要进行检查!<br>
C库每次分配一段内存,做二次分配
分配更大的空间作为内存池
malloc(1)实际可能分配132K字节
free
通过brk()申请的
释放内存后,堆内存还是存在的,放进malloc内存池里
通过mmap()申请的<br>
直接归还给操作系统
进程退出完全释放
brk与do_mmap
创建进程fork()、程序载入execve()、映射文件mmap()、动态内存分配malloc()/brk()等进程相关操作都需要分配内存给进程。不过这时进程申请和获得的还不是实际内存,而是虚拟内存,准确的说是“内存区域”。进程对内存区域的分配最终都会归结到do_mmap()函数上来(brk调用被单独以系统调用实现,不用do_mmap()), 内核使用do_mmap()函数创建一个新的线性地址区间。但是说该函数创建了一个新VMA并不非常准确,因为如果创建的地址区间和一个已经存在的地址区间相邻,并且它们具有相同的访问权限的话,那么两个区间将合并为一个。如果不能合并,那么就确实需要创建一个新的VMA了。但无论哪种情况, do_mmap()函数都会将一个地址区间加入到进程的地址空间中--无论是扩展已存在的内存区域还是创建一个新的区域。
文件映射与匿名映射区内存分配(动态分配)
mmap/munmap命令
分配匿名内存空间,映射文件,共享内存
内核态内存操作
kmalloc
vmalloc
alloc_pages()
在物理内存中分配一个或多个连续的物理页
伙伴系统或slab分配器
内存回收<br>reclaim<br>
内存资源紧张的策略
1、产生 OOM,内核直接将系统中占用大量内存的进程,将 OOM 优先级最高的进程干掉,释放出这个进程占用的内存供其他更需要的进程分配使用。<br><br>2、<b>内存回收</b>,将不经常使用到的内存回收,腾挪出来的内存供更需要的进程分配使用。<br><br>3、内存规整,将可迁移的物理页面进行迁移规整,消除内存碎片。从而获得更大的一片连续物理内存空间供进程分配。
what内存回收机制综述
基本概念
文件页
其物理内存页中的数据来自于磁盘中的文件,当我们进行文件读取的时候,内核会根据局部性原理将读取的磁盘数据缓存在 page cache 中,<b>page cache 里存放的就是文件页</b>。当进程再次读取读文件页中的数据时,内核直接会从 page cache 中获取并拷贝给进程,省去了读取磁盘的开销。
干净页和脏页
pdflush 内核线程
匿名页
匿名页就是它背后并没有一个磁盘中的文件作为数据来源,匿名页中的数据都是通过进程运行过程中产生的,比如我们应用程序中动态分配的堆内存。
堆,栈,数据段
kswapd 内核进程
也会回收文件页
两个线程的关系
https://blog.51cto.com/dog250/1274029
联系的原因和纽带
kswap的作用是管理内存,pdflush的作用是同步内存和磁盘,当然因为数据写入磁盘前可能会缓存在内存,这些缓存真正写 入磁盘由三个原因趋势:1.用户要求缓存马上写入磁盘;2.缓存过多,超过一定阀值,需要写入磁盘;3.内存吃紧,需要将缓存写入磁盘以腾出地方
内核中的lru链表
缓存管理的执行者 pdflush和内存管理的执行者kswap就不需要直接交互商量事情了,一个lru链表解除了它们的耦合。
磁盘缓存也是正在被使用的内存,因此,kswap需要将它们换出,这里的换出和匿名页面被换到交换分区是一样的概念,将磁盘缓存换到哪里呢?当然哪里来哪里去了。linux不区分匿名页面 对应的交换分区和真实文件的磁盘缓存对应的磁盘文件分区,实际上在将匿名页面写到交换分区的时候也是按照写文件的形式进行的
当linux系统内存压力就大时,就会对系统的每个压力大的zone进程内存回收,内存回收主要是针对匿名页和文件页进行的。<br><br>对于<b>匿名页</b>,内存回收过程中会筛选出一些不经常使用的匿名页,将它们写入到swap分区中,然后作为空闲页框释放到伙伴系统。<br><br>而对于<b>文件页</b>,内存回收过程中也会筛选出一些不经常使用的文件页,如果此文件页中保存的内容与磁盘中文件对应内容一致,说明此文件页是一个<b>干净的文件页,就不需要进行回写</b>,直接<font color="#e74f4c">将此页作为空闲页框释放到伙伴系统中</font>,相反,如果文件页保存的数据与磁盘中文件对应的数据不一致,则认定此文件页为<b>脏页,需要先将此文件页回写到磁盘中对应数据所在位置上</b>,<font color="#e74f4c">然后再将此页作为空闲页框释放到伙伴系统中</font>。这样当内存回收完成后,系统空闲的页框数量就会增加,能够缓解内存压力。<br><br>听起来很厉害,它也有一个弊端,就是在回收过程中会对系统的IO造成很大的压力。所以,在系统内,一般每个zone会设置一条线,当空闲页框数量不满足这条线时,就会执行内存回收操作,而系统空闲页框数量满足这条线时,系统是不会进行内存回收操作的。<br>
文件页和匿名页回收的比例控制
/proc/sys/vm/swappiness的取值范围为 0 到 100,默认为 60。<br><br>swappiness 用于表示 Swap 机制的积极程度,数值越大,Swap 的积极程度越高,内核越倾向于回收匿名页。数值越小,Swap 的积极程度越低。内核就越倾向于回收文件页。
swapness=0则意味着不再交换匿名页,除非当内存不足(free and file-backed pages < high watermark in a zone)的情况下才使用swap空间
<b>cgroup</b>的swapness优先级高些,如果一个cgroup的swapness关掉,全局的没关,那么这个cgroup里的进程的swap就是关掉的
when回收时机
水线水位(watermark)控制
分类
WMARK_MIN(页最小阈值), WMARK_LOW (页低阈值),WMARK_HIGH(页高阈值)
机制
1、当该物理内存区域的剩余内存容量高于 _watermark[WMARK_HIGH] 时,说明此时该物理内存区域中的内存容量非常充足,内存分配完全没有压力。<br><br>2、当剩余内存容量在 _watermark[WMARK_LOW] 与_watermark[WMARK_HIGH] 之间时,说明此时内存有一定的消耗但是还可以接受,能够继续满足进程的内存分配需求。<br><br>3、当剩余内容容量在 _watermark[WMARK_MIN] 与 _watermark[WMARK_LOW] 之间时,说明此时内存容量已经有点危险了,内存分配面临一定的压力,但是还可以满足进程的内存分配要求,当给进程分配完内存之后,就会<b>唤醒 kswapd 进程开始内存回收,直到剩余内存高于 _watermark[WMARK_HIGH] 为止</b>。在这种情况下,进程的内存分配会触发内存回收,但请求<b>进程本身不会被阻塞</b>,由内核的 kswapd 进程异步回收内存。<br><br>4、当剩余内容容量低于 _watermark[WMARK_MIN] 时,说明此时的内容容量已经非常危险了,<b>如果进程在这时请求内存分配,内核就会进行直接内存回收</b>,这时请求进程会<b>同步阻塞</b>等待,直到内存回收完毕。
计算方法和调整方式
/proc/sys/vm/min_free_kbytes 为基准分别计算出来的
/proc/sys/vm/watermark_scale_factor 参数来调节水位线之间的间距
使得 WMARK_MIN 与 WMARK_LOW 之间留有足够的缓冲余地,使得 kswapd 能够有时间回收足够的内存,从而解决直接内存回收导致的性能抖动问题
人为地主动地进行drop_cache
where回收依据
LRU链表
LRU,即最近最少使用的页会被回收,Linux内核一直在评估哪些是LRU的页面即最不活跃的页面。<br>LRU 算法更多的是在时间维度上的考量,突出最近最少使用,但是它<b>并没有考量到使用频率的影响</b><br>
cat /proc/meminfo看到的active和inactive的内存就是指lru算法里面去评估的一个页面的使用情况(有没有被访问过),inactive的页面中最inactive的页面最先被回收。如果inactive的页都回收了但内存仍然不够,也会从active的页中回收相对最不活跃的页面。
<b>active 链表用来存放访问非常频繁的内存页(热页), inactive 链表用来存放访问不怎么频繁的内存页(冷页)</b>,当内存紧张的时候,内核就会优先将 inactive 链表中的内存页置换出去。<br>内核在回收内存的时候,这两个列表中的回收优先级为:inactive 链表尾部 > inactive 链表头部 > active 链表尾部 > active 链表头部。
五种类型
<b>匿名页</b>的 active 链表,inactive 链表和<b>文件页</b>的active 链表, inactive 链表
进程可以通过 mlock() 等系统调用把内存页锁定在内存里,保证该内存页无论如何不会被置换出去,比如出于安全或者性能的考虑,页面中可能会包含一些敏感的信息不想被 swap 到磁盘上导致泄密,或者一些频繁访问的内存页必须一直贮存在内存中。<br><br>当这些被锁定在内存中的页面很多时,内核在扫描 active 链表的时候也不得不跳过这些页面,所以内核又将这些被锁定的页面<b>单独拎出来放在一个独立的链表中。</b>
文件页回收
page cache
buffer & cache
通过<b>free命令</b>可以看到当前page cache占用内存的大小,free命令中会打印buffers和cached(有的版本free命令将二者放到一起了)。<br><b>通过文件系统来访问文件(挂载文件系统,通过文件名打开文件)产生的缓存就由cached记录,而直接操作裸盘(打开/dev/sda设备去读写)产生的缓存就由buffers记录。</b><br>
实际上文件系统本身再读写文件就是操作裸分区的方式,用户态也可以直接操作裸盘,像dd命令操作一个设备名也是直接访问裸分区。<br>那么,<b>通过文件系统读写的时候,就会既有cached又有buffers</b>。从图中可以看到,文件名等元数据和文件系统相关,是进cached,实际的数据缓存还是在buffers。例如,read一个文件(如ext4文件系统)的时候,如果文件cache命中了,就不用走到ext4层,从vfs层就返回了。<br>在open的时候加上O_DIRECT标记,做直接IO,就连buffers都不进了,直接读写磁盘。<br>
典型产生方式:从磁盘上读文件
read 和 mmap<br>
人为主动释放
echo 3 > /proc/sys/vm/drop_cache” 来清cache
常见问题:在统计内存的时候,把tmpfs占的内存统计到page cache的,有时你在drop_cache后发现cache/buffers仍然很大,可能就是因为tmpfs的内存无法回收。
干净页
可以直接释放
脏页
写回的时机
<b>sync</b>是回写脏页,即page cache被修改后与磁盘原文件内容不同步的页,回写完后内存也不会回收,回收还是要等到kswapd或direct reclaim。进程打开并使用一个文件后调用close(),是不会回写脏页的,要显示地调用sync()/fsync()。
脏页回写的时机由时间(dirty_expire_centisecs/dirty_writeback_centisecs)和空间(dirty_ratio/dirty_background_ratio)两方面共同控制:<div><br></div><div>即使只有一个脏页,<b>那如果它超时了,也会被写回</b>。防止脏页在内存驻留太久。dirty_expire_centisecs这个值默认是3000,即30s,可以将其设置得短一些,这样掉电后丢失的数据会更少,但磁盘写操作也更密集。</div><div><b>不能有太多的脏页</b>,否则会给磁盘IO造成很大压力,例如在内存不够做内存回收时,还要先回写脏页,也会明显耗时。</div>
匿名页:swap换页
交换分区或文件
SWAP就是LINUX下的虚拟内存分区,它的作用是在物理内存使用完之后,将磁盘空间(也就是SWAP分区)虚拟成内存来使用;或者在磁盘上创建swap文件
开关控制
CONFIG_SWAP选项
swapoff命令、swapon命令
换页时机
高水位线
低水位线
小于时择机换页
最小水位线
小于时立即换页
性能损失的应对
预取机制
发生缺页错误前就换入,猜测进程需要哪些内存页
页替换策略
最小化缺页异常发生次数
进程虚拟页的四种状态
未分配
已分配,未分配物理页
缺页异常
已分配,且映射物理页
已分配,但对应物理页被换出
缺页异常
内存高级应用
主要目的
提升物理内存使用率,减少不必要的消耗
提升内存访问的效率
内存压缩zram
从内存里拿出一段内存空间(compressed block),<b>作为交换空间模拟硬盘的交换分区</b>,用来交换匿名页,并且让kernel看到的物理内存大小不包括这段内存。而<b>这段交换空间自带透明压缩功能</b>,即交换到这块zRAM分区时,Linux会自动将这块匿名页压缩存放。系统访问这块页面的内容时,产生page fault后从交换分区去拿,这时Linux给你透明解压再交换出来。<br>
好处,就是访存比访问硬盘或flash的速度提高很多,且不用考虑寿命问题,并且由于这段内存是压缩后存储的,因此可以存更多的数据,虽然占用了一段内存,但实际可以存更多的数据,也达到了增加内存的效果。缺点就是压缩要占用CPU时间。<br>
内存合并ksm
共享内存与写时拷贝shm
大页内存hugepage
保留内存
damon 冷热页识别
性能导向的内存分配扩展机制:CPU缓存
内存问题和维测
内存泄漏
内存碎片
内存溢出
内存使用过高
交换空间过度使用
内存分配算法不当
oom
内存操作命令
/proc/meminfo
/proc/zoneinfo
/proc/slubinfo
耦合机制
page cache
将件作为数据的持久存储,持久是个核心问题,它分这几个层面:<br>1.数据存入用户态缓存,例如 stdio 中 FILE buffer<div>2.数据存入操作系统 page cache,例如我们调用了 write</div><div>3.数据存入硬件设备,例如 fsync/fdatasync </div><div>4.数据存入硬件设备中的持久存储介质。</div>
CPU读写内存
总图
内存管理大图
参考
架构
收藏
0 条评论
下一页