虚拟内存
- 虚拟内存的作用
- 运行内存超过物理内存大小(SWAP)
- 多进程之间地址冲突
- 安全性:页表中有一些属性,比如读写权限、标记是否存在
- 分段 ⇒ 通过段表将虚拟地址和物理地址进行映射,缺点是产生内存碎片
- 分页 ⇒ 用页表管理虚拟内存地址
- 每个进程有一个页表,存在内核中
- 多级页表:类似基数排序,如果有一段虚拟地址一直没有使用,则可以避免分配低级的页表
- TLB:CPU 中用来存放最常用页表项的 Cache。寻址时先找 TLB,然后才找常规的页表
malloc
- 当分配比较小的时候,用 brk,
- 在堆空间分配,向上移动 brk 指针
- free 之后不会释放,而是等待下次使用
- 可能导致碎片化(释放后的空闲空间太小,导致无法被充分利用)
- malloc 比直接调用 brk 快,因为他会一次用 brk 申请一块比较大的,用于后续的 malloc 调用
- 兼容性(
brk
→ mmap
+ /dev/zero
→ mmap
+ MAP_ANONYMOUS
)
- 当分配比较大的时候(128K),用 mmap,free 之后会即时释放
- malloc 实际还会在开头存放内存块的信息,这样 free 的时候可以知道释放多大的空间
内存回收
- 依次执行:后台回收 → 直接回收 → OOM
- 后台回收:剩余内存小于页低阈值
pages_low
时,kswapd 线程异步执行
- 直接回收:剩余内存小于最小阈值
pages_min
时,阻塞执行
- 被回收的内存类型
- 文件页
- 脏页 ⇒ 先写,然后释放内存,影响性能
- 干净页 ⇒ 直接释放内存,不影响性能
- 匿名页:swap 到硬盘,影响性能
- 算法:LRU,两个双向链表(FIFO)
- active:最近访问过的内存页
- inactive:很少被访问的内存页
- 用双向链表的目的:可以快速把刚刚使用的内存页移动到链表头
- LRU
- 匿名页/文件页,由于处理策略不同(是否 swap),需要分成两个链表
- active/inactive,由于精准跟踪每个内存页的访问情况非常慢,可以通过上次 check 之后是否被访问来近似,即分成两个链表
- 还有一个 unevictable 链表
- 五个一块叫做 lruvec
- 可以通过 OOM 校准值
/proc/<pid>/oom_score_adj
调整进程被 OOM 杀死的几率
LRU 的问题
- 预读失效
- 在读取时,把相邻的磁盘块都读进 page cache,但是这些数据可能从来没有访问
- Linux 通过 active_list/inactive_list 解决,预读数据只放入 inactive_list,实际访问才放入 active_list
- InnoDB 通过在 LRU 链表上划分 young/old 区域解决,策略类似
- 缓存污染
- 只访问一次的数据会把热点数据挤出 LRU 链表
- Linux 只在数据被访问第二次时才加入 active_list
- InnoDB 在第二次时,如果和第一次的时间差超过 1 秒,才会加入 active_list