虚拟内存
防止内存运行多个程序时崩溃。
把进程所使用的地址隔离开,让操作系统为每个进程分配一套独立的虚拟地址。操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。
内存分段
程序时由若干逻辑分段组成的,不同的段有不同的属性,所以用分段的形式把这些段分离出来。
虚拟地址由段选择因子和段内偏移量组成。
带来的问题:内存碎片、内存交换的效率低。
内存碎片主要分为内部内存碎片和外部内存碎片,但一般不会出现内部内存碎片。用swap空间解决外部内存碎片,将内存和硬盘进行空间交换。但这样很慢导致效率低。
内存分页
分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。
分页不会产生外部内存碎片,但页内会出现内存浪费,导致内部内存碎片的现象。
分页机制下,虚拟地址分为页号和页内偏移两部分。
多级页表 节约空间 TLB 节约时间,把最常访问的页表项存储到访问更快的硬件中,成为页表缓存。
段页式内存管理
先把程序分为多个有逻辑意义的段,再把每个段分成固定大小的页。地址结构就由段号、段内页号和页内位移组成。
Linux内存主要采用页式内存管理,但同时不可避免的涉及了段机制。
内存分配
malloc()是c库里的函数,用于动态分配内存。
方式一通过brk()系统调用从堆分配内存,方式二通过mmap()系统调用在文件映射区域分配内存。
malloc分配的是虚拟内存,在分配内存时,会预分配更大的空间作为内存池。
malloc(1)实际分配132K字节的内存。
brk()方式申请的内存,free释放内存时,没有把内存归还给操作系统而是缓存在malloc内存池中了,待下次使用;而mmap方式申请的内存会归还给操作系统。
频繁使用mmap分配内存,每次都要进行运行态的切换,还会发生缺页中断,导致CPU消耗大。
频繁使用brk分配内存,堆内将产生越来越多不可用的碎片,导致内存泄漏。
free函数会对传进的内存地址向左偏移16字节,分析出内存块的大小。
内存回收
后台内存回收和直接内存回收;
后台内存回收过程是异步的,不会阻塞进程的执行;若后台异步回收跟不上就开始直接回收,这个过程是同步的,会阻塞进程的执行。若仍然无法满足内存需求,那么就会触发OOM机制。
OOM Killer机制根据算法选择占内存高的进程将其杀死。
可以回收的内存
文件页:内核缓存的磁盘数据和内核缓存的文件数据;回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘在释放内存。
匿名页:这部分内存没有实际载体。通过Swap机制先把内存写到磁盘,再释放内存。
回收都是基于LRU算法,也就是优先回收不常访问的内存。
调整文件页和匿名页的回收倾向:在Linux里把swappiness的数值设置为0,这样更倾向于文件页的回收,减少磁盘I/O的操作。
内核定义了三个内存阈值来衡量当前剩余内存的情况:页高阈值、页低阈值、页最小阈值。
橙色部分:执行内存回收,直到剩余内存大于高阈值。异步回收
红色部分:触发直接内存回收。
通过调节内存最小阈值类设置页低阈值和页高阈值。
NUMA架构下的内存回收策略
SMP架构:多个CPU处理器共享资源的电脑硬件架构,每个CPU地位平等。也称为一致存储访问结构(UMA),但用同一根总线访问内存,总线压力大。
NUMA架构将每个CPU进行了分组,每组用Node表示,每个Node有自己独立的资源,包括内存、IO等。每组之间可以通过互联模块总线通信。
在NUMA架构下,某个Node内存不足时,系统可以从其他Node寻找空闲内存。
保护进程
omm_badness()根据进程消耗的内存打分,消耗的内存越大越容易被杀掉,可以通过调整omm_score_adj的值改变得分结果。
将omm_score_adj设置为-1000,进程就不会被杀掉。
避免预读失效和缓存污染
Linux操作系统的缓存:对读取的文件数据缓存到页缓存中。
MySQL的缓存:Innodb存储引擎设计了一个缓冲池,其属于内存空间。
传统LRU算法:访问的页在内存中,把该页对应的LRU链表节点移动到链表头部;不在内存中,把该页放到LRU链表头部还要淘汰末尾的页。
预读机制:操作系统出于空间局部性原理,提前将磁盘块后续的数据都加载到内存中。好处是减少了磁盘I/O次数。
预读失效:被提前加载来的页没有被访问,反而占用链表头部位置,导致淘汰热点数据。
改进:
Linux实现了两个LRU表:活跃LRU链表(active list)和非活跃LRU链表
MySQL在LRU链表上划分了两个区域:young和old区域。
将预读页加入到inactive list的头部,当预读页被访问时,才将页插入active list的头部。
缓存污染
热点数据都被挤到非活跃链表了,就导致活跃链表被污染了。
改进:
提高进入活跃LRU链表的门槛,保证热点数据不会被轻易替换掉。
Linux在内存也被第二次访问时,才把页升级到活跃链表中。
MySQL在内存页被第二次访问同时时间间隔超过1秒时才会升级到young区。
深入理解虚拟内存管理
进程虚拟内存空间
存放进程程序二进制文件中的机器指令的代码段
定义全局变量和静态变量的数据段和BSS段
用于动态申请内存的堆
存放动态链接库以及内存映射区的文件映射与匿名映射
存放函数调用过程中的局部变量和函数参数的栈
Linux进程虚拟内存空间
进程虚拟内存空间的管理
进程在内核中的描述符task_struct结构,每个进程都有唯一的mm_struct结构体,保证虚拟地址空间都是独立互不干扰的。
通过fork()创建的子进程,其虚拟内存空间和父进程的虚拟内存空间是一模一样的,直接拷贝过来的。
通过vfork或clone系统调用创建的子进程,是将父进程的虚拟内存空间以及相关页表直接赋值给子进程,也就是父子的虚拟内存空间是共享的,一样的,并不是一份拷贝。这样子进程就变成了线程。
是否共享地址空间几乎是进程和线程的本质区别。
内核线程和用户态线程的区别是内核线程没有相关的内存描述符mm_struct;
划分用户态和内核态虚拟内存空间
进程的内存描述符mm_struct结构体中的task_size变量定义了用户态地址空间和内核态地址空间的分界线。
内核中用mm_struct结构体中的属性来定义虚拟内存空间的不同内存区域。
内核管理虚拟内存空间
结构体vm_area_struct描述了虚拟内存空间
虚拟内存区域的访问权限和行为规范
vm_page_prot偏向于定义页这一级别的访问控制权限,可直接应用在底层表中。
vm_flags偏向于定义整个虚拟内存空间的访问权限和行为规范。
内存映射中的映射关系
属性anon_vma关联匿名映射区;属性vm_file关联被映射的文件;vm_pgoff表示映射进虚拟内存中的文件内容在文件中的偏移。
结构体 vm_area_struct结构中有一个vm_ops用来指向针对虚拟内存区域的相关操作的函数指针。
组织关系
内核中通过一个双向 vm_area_struct结构中的双向链表将虚拟内存空间中的虚拟内存区域串联起来的。头指针存储在内存描述符struct mm_struct结构体中的mmap中;每个虚拟内存区域又通过struct vm_arrea_struct中的vm_mm指针指向所属的虚拟内存空间mm_struct;
还有一种红黑树用于高效的查找,每个VMA都是树的一个结点,红黑树的根结点存储在mm_rb中。
二进制文件映射到虚拟内库空间
通过函数load_elf_binary将ELF格式的二进制文件中的Section加载并映射到虚拟内存空降中。
内核虚拟内存空间
内核态虚拟内存空间是所有进程共享的。
32位体系内核虚拟内存空间布局
直接映射区:范围为 3G -- 3G + 896m这块连续的虚拟内存地址会映射到0-896M这块连续的物理内存上。
直接映射区存放的内容:前1M被系统占用,之后的存放内核代码段、数据段、BSS段。还有进程相关的数据结构、内核栈。
物理内存896M以上的区域被内核划分为ZONE_HIGHMEM区域,称为高端内存。通过动态映射的方式映射到128M大小的内核虚拟内存空间中。
动态映射区:动态的一部分一部分的分批映射。
永久映射区:建立与物理高端内存的长期映射关系。
固定映射区:虚拟地址是固定的,被映射的物理地址是可以改变的。相当于一个指针常量。
临时映射区:临时映射,用完就释放。
64位体系内核虚拟内存空间布局
64位下内核虚拟内存空间很大,不用精细管理,直接映射就可以了。
物理内存地址
内存也叫随机访问存储器(RAM)
静态RAM(SRAM):用于CPU高速缓存L1、L2、L3,访问速度快,容量小,造价高;
动态RAM(DRAM):用于常说的主存,访问速度较慢,容量较大,相对便宜。
主存由一个个存储器模块组成,多个存储器模块连接到存储控制器上,就聚合成了主存。
DRAM芯片的存储结构是二维矩阵,二维矩阵中存储的元素成为超单元。都有坐标地址。
包含两个地址引脚,八个数据引脚。
DRAM芯片的访问
CPU读写主存
CPU和主存之间的数据交互是通过总线完成的,数据在总线的传送过程称为总线事务。
总线上传输的信号有地址信号、数据信号、控制信号。总线传输的地址为物理内存地址。
IO bridge负责转换不同总线上的电子信号。
CPU从内存读数据
存储控制器将物理地址转换为二维坐标,并广播给所有存储器模块。并依次从DRAM0到DRAM7读取相应的supercell。
物理内存地址实际上是不连续的,因为这连续的八个字节实际上存储与不同的DRAM芯片上。
Linux物理内存管理
物理内存划分为内存页,在内核中用struct page结构体进行管理。每个结构体定义了一个索引编号PFN,与struct page 一 一 对应。
物理内存模型
FLATMEN平坦内存模型
假设内存是地址连续的内存空间,内核将内存空间分为一页一页的内存块struct page,用一个数组来组织连续的的物理内存页。
内核使用mem_map的全局数组来组织所有划分出来的内存页,计算逻辑就是基于mem_map数组进行偏移操作。
DISCONTIGMEM非连续内存模型
平坦模型管理非连续内存时会造成内存空洞。
在DISCONTIGMEM非连续内存模型中,内核将物理内存从宏观上划分成了一个个的结点node,每个结点管理一块连续的物理内存。
每个节点中还是采用FLATMEM平坦内存模型来组织管理物理内存页。
SPARSEMEM稀疏内存模型
DISCONTIGMEN内存模型中的每个node中的物理内存也不一定都是连续的。若node数目多了,开销变大。
SPARSEMEM稀疏内存模型核心思想是对粒度更小的连续内存块进行精细管理。管理单元称为section。
物理内存热插拔
热插拔分为两个阶段:物理热插拔阶段和逻辑热插拔阶段。
SPARSEMEM内存模型中的每个men_section都可以在系统运行时改变offline和online的状态,当offline时,内核把这部分内存隔离开,然后将内存页迁移到其他内存上。
内核将物理内存根据物理页是否可前移,划分为不可迁移页、可回收页、可迁移页。热拔出时,操作的都是可迁移的内存页,从而使内存顺利拔出。
物理内存架构
从CPU访问物理内存的角度观察物理内存的架构。
一致性内存访问UMA架构
多个CPU访问内存都要过总线,且距离相同,访问速度都是一样的,这种模式叫SMP,即对称多处理器。
优点:结构简单,访问速度一致。
缺点:总线压力会变大,CPU可用带宽减少;总线长度增加,访问延迟。
非一致性内存访问NUMA架构
内存被划分为一个个内存结点,每个CPU都有自己的本地内存节点,本地内存不足时,需要跨节点访问其他内存节点。
NUMA内存分配策略
内核管理NUMA节点
内核统一组织NUMA节点
内核使用了一个类型为struct pglist_data的全局数据node_data[]来管理所有的NUMA节点。
NUMA节点描述符pglist_data结构
node_id表示NUMA节点的id;
struct page 类型的数组node_men_map包含了NUMA节点内所有的物理内存页;
node_start_pfn指向NUMA节点内第一个物理页的PFN;
node_present_pages用于统计NUMA节点内所有真正可用的物理页面数量;
NUMA节点物理内存区域的划分
ZONE_MOVABLE 是一个虚拟内存区域,该区域的物理页都是可以迁移的,主要为了防止内存碎片和支持内存的热插拔。
NUMA节点之间所包含的物理内存区域个数是不一样的。
NUMA节点中内存规整与回收
kswapd进程用于回收不经常使用的页面;
kcompactd进程用于内存的规整避免内存碎片。
NUMA节点的状态
NUMA节点多余一个,内核会维护一个位图node_states,用于维护各个NUMA节点的状态信息。
内核管理NUMA节点中的物理内存区域
内核中描述和管理NUMA节点中物理内存区域的结构体是struct zone;
物理内存区域中的预留内存
每个物理内存区域struct zone为操作系统预留了一部分内存,用于内核的核心操作。
内核不允许高位内存对低位内存的无限制挤压,每个内存区域可以根据各自的lowmen_reserve_ratio计算各自区域中的预留内存大小;也可以进行动态调整。
物理内存区域中的水位线
内存资源紧张时应对方法:产生OOM、内存回收、内存规整。
内核为每个NUMA结点中的每个物理内存区域定制了三条指示内存容量的水线:WMARK_MIN(页最小阈值)、WMARK_LOW(页低阈值)、WMARK_HIGH(页高阈值)
水位线的数值是以min_free_kbytes为基准分别计算出来的,用户也可以通过sysctl来动态设置这个参数。
要保证kswapd进程活动范围大一些,这样就减少了直接回收内存的可能。调整watermark_scale_factor重新计算水位线之间的间距。
物理内存区域中的冷热页
热页:已经加载进CPU高速缓存中的物理内存页;
冷页:还未加载进CPU高速缓存中的物理内存页;
内核关于冷热页的管理全部封装在struct per_cpu_pageset结构中。两个pre_cpu_pages结构分别管理冷页和热页。
内核5.0版本直接使用struct pre_cpu_pages的链表来集中管理系统中所有CPU高速缓存冷热页。
每种迁移类型都对应一个冷热页链表。
内核描述物理内存页
Linux采用4KB作为标准物理内存页的大小:必须是2的整数次幂;4KB是磁盘块大小的整数倍,同时传输小块数据会更高效。
struct page结构是内核中访问最频繁的结构体,其中包含了大量的union结构,被用于同一块内存根据不同的场景保存不同类型数据的一种方式。
匿名页的反向映射
从物理内存映射到虚拟内存,用于某个物理内存页需要进行回收或迁移,此时就要去找到这个物理页被映射到了哪些进程的虚拟地址空间,并断开他们的映射。
内核可以直接检出page结构中的mapping指针的最低位来判断该物理内存到底是匿名页还是文件页。
内存回收相关属性
内核为了将页面的使用频率这个因素加入进来,就引入了active链表和inactive链表。
回收优先级:inactive 链表尾部 > inactive 链表头部 > active 链表尾部 > active 链表头部。
因为匿名页的换入换出代价大,所以要优先替换文件页。对active链表和inactive链表按照匿名页和文件页进行了归类。(匿名页的active链表,inactive链表和文件页的active链表,inactive链表)
swappiness设置为0时,内核只会置换文件页,不会置换匿名页。
物理内存页属性和状态的标志位flag
struct page中的flag字段的高8位用来表示struct page的定位信息,剩余低位表示特定的标志位。
复合页compound_page相关属性
巨型大页:通过两个或者多个物理上连续的内存页page组装成一个比普通内存页page更大的页。
可减少缺页中断提高性能;所需要的页表项比普通页要少,加速内存访问;拷贝父进程的页表时更快。
复合页中所有尾页都会指向首页从而组合成一个完整的复合页。
Slab对象池相关属性
slab好比一个对象池,内核中的数据结构对象都对应于一个slab对象池,用于分配固定类型对象所需要的内存。从伙伴系统申请一整页内存,然后划分为多个大小相等的小块内存被slab管理。