虚拟内存为程序提供了强隔离性,并基于保护了各个程序。
分段
和分页
va = 段号+偏移量
,段号索引段表
中的一个段项
,段项包含了该段实际的起始物理地址、权限、段大小等
将程序的虚拟地址分为四个分别物理连续
的段:代码+数据+堆+栈
,每个段在段表中都有一个段项
这并不会导致内部内存碎片
的问题,但会有外部内存碎片的问题。例如新的程序的一个段有200MB,但这里插不下:
为了解决这个问题,只好重排
内存,把一部分内存写入磁盘,然后重写加载到合适的位置,减少内存碎片,也即 内存交换
对于多进程的系统,很容易产生外部内存碎片,从而反复地进行 swap
,产生了大量浪费——且磁盘非常慢,严重拖慢
整个系统的速度
页
,通常一页大小为4KB
页表
进行va和pa的转换,每一个映射项称为PTEMMU
完成,MMU有缓存 TLB
,可以缓存PTE,并在切换页表时清空缓存多级页表
,不使用的地址部分的PT我们可以不分配
,只需要覆盖程序实际使用的部分即可例如这是一个四级页表,va
=四个页号
+实际数据页的offset
内存碎片
,但有内部内存碎片——程序没用完这一页。swap out
到磁盘上,在需要使用时再 swap in
加载回来——每次只写一部分页,很高效lazy allocation、cow、mmap
等等机制有的系统结合了两个机制,先把程序分段,然后每个段映射为多个页
每次使用地址都需要大量的查表操作,极为缓慢
早期intel处理器使用段式内存管理,后来就全是页式内存管理了。早期的处理器只能使用分段+分页混合式布局。目前的分段机制几乎只是为了兼容,Linux 为此将整个程序使用的部分分为一个虚拟段,然后用分页机制映射
很多内核对自己的地址空间和物理空间做了 恒等映射
,这样可以通过操纵虚拟地址的方式直接操纵物理地址
用户地址空间大多分布如下:
malloc() 并不是系统调用,而是 C 库里的函数,用于动态
分配内存。
分配器持有一片连续的内存,这个区域叫堆
。每次以 block
为单位分配内存,block 需要满足字对齐
要求,这些内存都是 虚拟内存
每次被 free
的block会被归还给这个分配器,而不是还给OS。mmap分配的 block 会还给OS
分段链表/分区堆
:每种类型大小的链表的内存块大小都一样,采用 first-fit
分配内存块,使用 brk
增长堆mmap
在内存映射文件区域映射一个匿名文件不全部使用mmap的原因是,系统调用会消耗大量的时间。mmap可能以cow的方式实现,这会触发handler,进行上下文切换,也需要耗费大量的时间
不全部使用 sbrk 的原因是,可能出现
大块内存碎片
的问题
分配的每个 block 都有一个 header,里面可能存储一些元数据
,比如块大小、块是否被分配、上一个/下一个块是否被分配、指向下一个块的指针等等
OS通常都实现了 COW
机制,也就是说申请的虚拟内存只有在你读写的时候才触发 page fault
,调用相应的 handler,并分配实际的物理内存,改写页表
这时候你发现物理页不够了。系统会进行以下尝试以找到足够的物理内存:
文件
缓存:直接丢弃(脏页
可能需要先写入磁盘)匿名
页:LRU 写入磁盘 swap
磁盘速度缓慢,会影响整个系统的性能,发生
有两个list存放着
活跃和不活跃
的内存页链表:active_list & inactive_list。可以查询它们的大小:活跃/不活跃,匿名/不匿名两两组合
系统的机制如下:
后台内存回收
机制,通过一个内核线程 kswapd 进行异步回收
内存直接内存回收
机制,阻塞当前进程尝试 同步回收
内存这时候要是内存还是不够,系统会根据算法选择并强制杀死一个物理内存占用较高的进程,直到有足够的物理内存
匿名页的回收全都要用到 swap
,发生磁盘I/O
,而文件页中除了脏页不需要 swap,通过更多的回收文件页
,我们减少swap的使用
Linux 有一个 swapiness
选项可以调整回收匿名页的倾向
Linux 内核使用了水位线
机制:
low
时开启后台内存回收机制,直到水位线回升至 highmin
时开启直接内存回收机制OOM Killer
Linux 中的 pages_min 叫 min_free_kbytes
,其他的水位线根据 min 计算出来,也可以自由调整
可以通过 sar -B 1
查看内存回收指标:
如果pgscand太大,通过增大min,可以提前触发后台内存回收,从而降低直接内存回收触发频率。但这会降低系统的速度
,带来一定的延迟
如果将最低阈值设置的太低
,可能导致频繁触发OOM。
SMP 又称为 UMA Uniform Memory Access 一致存储访问结构,每个CPU共享相同的物理资源。随着CPU的增多,总线压力越来越大,每个CPU的平均总线带宽会减少
而 NUMA Not UMA 则对CPU进行了分组,每个分组称为一个Node 节点
,共享相同的物理资源,节点之间可以通过 QPI
互联模块总线 进行通信
当一个节点的内存不足了,其内存回收策略有:
对于NUMA架构的服务器
,如果系统内存还有一半,某个节点却触发了内存回收,则可以调整他的策略,从远程节点共享内存
在触发OOM时,Linux 有一个函数 oom_badness()
,它会扫描所有可以被杀死的进程,为他们打分,分数最高的进程被杀死。打分算法如下:
可以通过配置校准值
到 -1000~1000 中的一个值,从而减少进程被杀死的概率
默认情况下,进程的校准值为0,也就是只根据使用的物理页数打分
我们最好将一些很重要的系统服务的 oom_score_adj 配置为 -1000,比如 sshd,因为这些系统服务一旦被杀掉,我们就很难再登陆进系统了。
但是,不建议将我们自己的业务程序的 oom_score_adj 设置为 -1000,因为业务程序一旦发生了内存泄漏,而它又不能被杀掉,这就会导致随着它的内存开销变大,OOM killer 不停地被唤醒,从而把其他进程一个个给杀掉。
对于32位机器,用户虚拟地址空间只有3GB,完全不够,申请失败
对于64位机器,用户虚拟地址空间有128TB,这个是够的,但如果没有开启 swap 机制,那么还是会OOM然后被杀死
Redis 没有预读失效的问题,它是内存型数据库,对于后者,它采用了LFU算法应对。
MySQL 和 Linux 通过改进 LRU来应对这两个问题
Linux 有文件页缓存,MySQL 有缓冲池 BufferPool
预读的数据可能没有被用到,却降低了缓存命中率,还可能淘汰热点数据。我们想要预读页在内存中停留尽量短的时间
我门可以把列表分为两个:活跃列表和不活跃列表
,预读的数据只被加到不活跃列表的队头,只有被真正使用到的页才会被加到活跃列表的队头
MySQL
的思路差不多,不过是把同一个链表分为了两个区域:young/old
Linux:active_list/inactive_list;MySQL:young/old
大量只被使用了一次的数据淘汰了热点数据,导致缓存命中率下降。其原因在于页进入活跃链表的门槛太低了
我们可以只在某个页在第二次访问
、或在一定时间后第二次访问时,才把它加入活跃列表