虚拟内存

  • 虚拟内存的作用
    • 运行内存超过物理内存大小(SWAP)
    • 多进程之间地址冲突
    • 安全性:页表中有一些属性,比如读写权限、标记是否存在
  • 分段 通过段表将虚拟地址和物理地址进行映射,缺点是产生内存碎片
  • 分页 用页表管理虚拟内存地址
    • 每个进程有一个页表,存在内核中
    • 多级页表:类似基数排序,如果有一段虚拟地址一直没有使用,则可以避免分配低级的页表
    • TLB:CPU 中用来存放最常用页表项的 Cache。寻址时先找 TLB,然后才找常规的页表

malloc

  • 当分配比较小的时候,用 brk,
    • 在堆空间分配,向上移动 brk 指针
    • free 之后不会释放,而是等待下次使用
    • 可能导致碎片化(释放后的空闲空间太小,导致无法被充分利用)
    • malloc 比直接调用 brk 快,因为他会一次用 brk 申请一块比较大的,用于后续的 malloc 调用
    • 兼容性(brk mmap + /dev/zero mmap + MAP_ANONYMOUS
  • 当分配比较大的时候(128K),用 mmap,free 之后会即时释放
    • POSIX
    • 每次分配必须产生一个系统调用
  • 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