Linux Swap机制
在 Linux 下,当物理内存不足时,拿出部分硬盘空间当 Swap 分区(也被称为“虚拟内存”,从硬盘中划分出的一个分区),从而解决内存容量不足的情况
- Swap 意思是交换, 当物理内存不够用的时候,内核就会释放缓存区(buffers/cache)里一些长时间不用的程序,然后将这些程序临时放到 Swap 中
- Swap Out:当某进程向OS请求内存发现不足时,OS会把内存中暂时不用的数据交换出去,放在 SWAP 分区中
- Swap In:当某进程又需要这些数据且OS发现还有空闲物理内存时,又会把 Swap 分区中的数据交换回物理内存中
Linux 操作系统使用如下这几种机制来检查系统内存是否需要进行页面回收:
- 周期回收:
- 这是由后台运行的守护进程 kswapd 完成的
- 该进程定期检查当前系统的内存使用情况,当发现系统内空闲的物理页面数目少于特定的阈值时,该进程就会发起页面回收的操作
- 内存紧缺回收:
- 操作系统忽然需要通过伙伴系统为用户进程分配一大块内存,或者需要创建一个很大的缓冲区,而当时系统中的内存没有办法提供足够多的物理内存以满足这种内存请求
- 这时候,操作系统就必须尽快进行页面回收操作,以便释放出一些内存空间从而满足上述的内存请求
- 这种页面回收方式也被称作“直接页面回收”
- 睡眠回收:
- 在进程进入
suspend-to-disk
(休眠)状态时,内核必须释放内存
- 在进程进入
如果 Linux 在进行了内存回收操作之后仍然无法回收到足够多的页面以满足上述内存要求,那么操作系统只有最后一个选择,那就是使用 OOM(out of memory) killer,它从系统中挑选一个最合适的进程杀死它,并释放该进程所占用的所有页面
Linux 内核的页面回收算法为 PFRA
- PFRA 采取从用户态进程和内核高速缓存“窃取”页框的办法补充伙伴系统的空闲块列表
- PFRA 的目标之一就是保存最少的空闲页到磁盘,以便内核可以安全地从“内存紧缺”的情形中恢复过来
PFRA 需要做的第一件事情就是明确:哪些页面可以被 Swap,哪些又不能
于是 PFRA 按照页框所含内容,以不同的方式处理页框:
- 不可回收页:不允许也无需回收
- 空闲页(包含在子伙伴系统表列中)
- 保留页(PG_reserved 标志置位)
- 内核动态分配页
- 进程内核态堆栈页
- 临时锁定页(PG_locked 标志置位)
- 内存锁定页(VM_LOCKED 标志置位,并且在先行区中)
- 可回收页:可以将该页的内存保存在 Swap 交换区(作为 “虚拟内存” 的磁盘区域)
- 用户态地址空间的匿名页
- tmpfs 文件系统的映射页(例如:POSIX 接口中 IPC 共享内存的页)
- 可同步页:必要时,与磁盘镜像同步这些页
- 用户态地址空间的映射页
- 存有磁盘文件数据,并且在页高速缓存中的页
- 块设备缓冲区页
- 某些磁盘的高速缓存页(例如:索引节点高速缓存)
- 可丢弃页:无需操作
- 内存高速缓存中的未使用页(例如:Slab 分配器高速缓存)
- 目录中高速缓存的未使用页
Swap 基础结构
每一个活动的交换区都会由一个 swap_info_struct
结构体进行描述:
1 | struct swap_info_struct { |
系统会把这些 swap_info_struct
存放在一个 swap_info
数组中:
1 | struct swap_info_struct *swap_info[MAX_SWAPFILES]; |
每个交换区在磁盘上都划分出大量一页大小的槽,第一个槽存放有交换区的基本信息,在内核中由一个 swap_header
联合体进行表示:
1 | union swap_header { |
Linux 将磁盘中的 SWAPFILE_CLUSTER 个页分配到一个簇中
1 |
- 在
swap_info_struct->cluster_nr
中记录“交换区中已经分配的页面数” - 在
swap_info_struct->cluster_next
中记录“交换文件当前的偏移量”
映射页表项到交换区
当一个页面被交换出时,Linux 使用相应的页表项 PTE 来存放用于再次从磁盘上定位该页的信息:
- PTE 本身不能直接精准保存交换页面的位置信息
- PTE 只需要存放交换槽在
swap_info
数组的下标,以及在swap_map
中的偏移量
通过如下两个函数进行转换:
1 | static inline swp_entry_t pte_to_swp_entry(pte_t pte) |
交换区高速缓存 Swap Tcache
Linux 无法快速完成从 PTE 引用到页面结构的转化,因此,多线程共享页面不能简单地取出
为了解决这个问题,共享页会在内存中保留一个槽,作为高速缓存的一部分
Linux 回收平衡
Linux 页面回收,并不是回收得越多越好,而是力求达到一种平衡(内存 Swap 是需要代价的)
物理内存在 Kernel 中主要有这么几个层次的划分:
- 全体内存
- 一个 NUMA 节点的内存
- 一个 NUMA 节点中的一个 zone 的内存
维护空闲页面的伙伴系统和维护可回收页面的 LRU 都工作在 zone 这一层,所以具体的内存回收操作也是在 zone 层进行的
Kernel 中追求的平衡并不针对全体内存,而是针对每一个 NUMA 节点的内存而言的:
- NUMA 系统中的每一个节点都是并列的,统一考虑整体的平衡其实没什么意义
- 所以对应于系统中的每个 NUMA 节点都会有一个 kswapd 线程来进行平衡
单个 NUMA 节点的内存的平衡由 pgdat_balanced
函数来判断
1 | static bool pgdat_balanced(pg_data_t *pgdat, int order, int classzone_idx) |
- 函数
pgdat_balanced
需要两个重要的指标:classzone_idx
:用于确定一种类型的 zoneorder
:用于确定一对伙伴页的大小
classzone_idx
:
在 Kernel 中,一个 NUMA 节点的内存被分成若干个 zone(区分依据为簇到处理器的“距离”),一个 zone 用于表示内存中的某个范围,在 Linux 中由下标 classzone_idx
来进行索引:
- 这些 zone 下标越大,zone 里的内存使用范围就越小
- 小下标 zone 的内存用途总是包含大下标 zone 的
- 因此大下标的 zone 显得更加“不通用”
于是在进行内存分配的时候,总是会优先尝试在 classzone_idx
最大的 zone 里面的去分配,不行再尝试 classzone_idx
更小的 zone
order
:
zone 里面的内存是用伙伴系统来管理的,伙伴系统里面有很多个 freelist
,分别是 2^n 个连续页面的空闲链表
order
就代表这里的 n,order
越大,意味着需要越多的连续页面- 大
order
对小order
也是有包含关系的 - 大
order
的连续内存其实并不是那么容易就回收到的
于是伙伴系统会尽量避免分裂大 order
的连续内存,碎片化的小 order
内存会成为优先分配的目标
kswapd 线程
kswapd 是 Linux 中用于页面回收的内核线程,拥有如下特性:
- kswapd 线程每100毫秒起来工作一次,或者由于别的进程分配内存失败,而被唤醒
- kswapd 每次工作都有一个
order
和classzone_idx
作为目标 - kswapd 如果主动工作,
order
总是“0”,classzone_idx
总是最大的 zone
其实 kswapd 的任务就是让每一个 zone 的空闲内存都超过高水位,至于页面在各个 order 间的平衡分布就不用管
对于 kswapd 拿到的 order
,所以如果达不到平衡分布,就还得继续回收以及尝试小 order
向大 order
的组装