0%

Linux Swap机制

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
struct swap_info_struct {
unsigned long flags; /* SWP_USED etc: see above */
signed short prio; /* swap priority of this type */
struct plist_node list; /* entry in swap_active_head */
struct plist_node avail_lists[MAX_NUMNODES];/* entry in swap_avail_heads */
signed char type; /* strange name for an index */
unsigned int max; /* extent of the swap_map */
unsigned char *swap_map; /* 一个非常大的数组,其中的每项都对应了一个交换区的一个槽(交换项)的引用计数 */
struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
struct swap_cluster_list free_clusters; /* free clusters list */
unsigned int lowest_bit; /* index of first free in swap_map */
unsigned int highest_bit; /* index of last free in swap_map */
unsigned int pages; /* total of usable pages of swap */
unsigned int inuse_pages; /* number of those currently in use */
unsigned int cluster_next; /* 交换文件当前的偏移量 */
unsigned int cluster_nr; /* 交换区中已经分配的页面数 */
struct percpu_cluster __percpu *percpu_cluster; /* per cpu's swap location */
struct swap_extent *curr_swap_extent;
struct swap_extent first_swap_extent;
struct block_device *bdev; /* swap device or bdev of swap file */
struct file *swap_file; /* seldom referenced */
unsigned int old_block_size; /* seldom referenced */
#ifdef CONFIG_FRONTSWAP
unsigned long *frontswap_map; /* frontswap in-use, one bit per page */
atomic_t frontswap_pages; /* frontswap pages in-use counter */
#endif
spinlock_t lock; /*
* protect map scan related fields like
* swap_map, lowest_bit, highest_bit,
* inuse_pages, cluster_next,
* cluster_nr, lowest_alloc,
* highest_alloc, free/discard cluster
* list. other fields are only changed
* at swapon/swapoff, so are protected
* by swap_lock. changing flags need
* hold this lock and swap_lock. If
* both locks need hold, hold swap_lock
* first.
*/
spinlock_t cont_lock; /*
* protect swap count continuation page
* list.
*/
struct work_struct discard_work; /* discard worker */
struct swap_cluster_list discard_clusters; /* discard clusters list */
};

系统会把这些 swap_info_struct 存放在一个 swap_info 数组中:

1
struct swap_info_struct *swap_info[MAX_SWAPFILES];

每个交换区在磁盘上都划分出大量一页大小的槽,第一个槽存放有交换区的基本信息,在内核中由一个 swap_header 联合体进行表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
union swap_header {
struct {
char reserved[PAGE_SIZE - 10];
char magic[10]; /* SWAP-SPACE or SWAPSPACE2 */
} magic;
struct {
char bootbits[1024]; /* Space for disklabel etc. */
__u32 version;
__u32 last_page;
__u32 nr_badpages;
unsigned char sws_uuid[16];
unsigned char sws_volume[16];
__u32 padding[117];
__u32 badpages[1];
} info;
};

Linux 将磁盘中的 SWAPFILE_CLUSTER 个页分配到一个簇中

1
2
3
4
5
6
#ifdef CONFIG_THP_SWAP
#define SWAPFILE_CLUSTER HPAGE_PMD_NR

#define swap_entry_size(size) (size)
#else
#define SWAPFILE_CLUSTER 256
  • swap_info_struct->cluster_nr 中记录“交换区中已经分配的页面数”
  • swap_info_struct->cluster_next 中记录“交换文件当前的偏移量”

映射页表项到交换区

当一个页面被交换出时,Linux 使用相应的页表项 PTE 来存放用于再次从磁盘上定位该页的信息:

  • PTE 本身不能直接精准保存交换页面的位置信息
  • PTE 只需要存放交换槽在 swap_info 数组的下标,以及在 swap_map 中的偏移量

通过如下两个函数进行转换:

1
2
static inline swp_entry_t pte_to_swp_entry(pte_t pte)
static inline pte_t swp_entry_to_pte(swp_entry_t entry)

交换区高速缓存 Swap Tcache

Linux 无法快速完成从 PTE 引用到页面结构的转化,因此,多线程共享页面不能简单地取出

为了解决这个问题,共享页会在内存中保留一个槽,作为高速缓存的一部分

Linux 回收平衡

Linux 页面回收,并不是回收得越多越好,而是力求达到一种平衡(内存 Swap 是需要代价的)

物理内存在 Kernel 中主要有这么几个层次的划分:

  • 全体内存
  • 一个 NUMA 节点的内存
  • 一个 NUMA 节点中的一个 zone 的内存

维护空闲页面的伙伴系统和维护可回收页面的 LRU 都工作在 zone 这一层,所以具体的内存回收操作也是在 zone 层进行的

Kernel 中追求的平衡并不针对全体内存,而是针对每一个 NUMA 节点的内存而言的:

  • NUMA 系统中的每一个节点都是并列的,统一考虑整体的平衡其实没什么意义
  • 所以对应于系统中的每个 NUMA 节点都会有一个 kswapd 线程来进行平衡

单个 NUMA 节点的内存的平衡由 pgdat_balanced 函数来判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static bool pgdat_balanced(pg_data_t *pgdat, int order, int classzone_idx)
{
int i;
unsigned long mark = -1;
struct zone *zone;

for (i = 0; i <= classzone_idx; i++) {
zone = pgdat->node_zones + i;

if (!managed_zone(zone))
continue;

mark = high_wmark_pages(zone);
if (zone_watermark_ok_safe(zone, order, mark, classzone_idx))
return true;
}

if (mark == -1)
return true;

return false;
}
  • 函数 pgdat_balanced 需要两个重要的指标:
    • classzone_idx:用于确定一种类型的 zone
    • order:用于确定一对伙伴页的大小

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 每次工作都有一个 orderclasszone_idx 作为目标
  • kswapd 如果主动工作,order 总是“0”,classzone_idx 总是最大的 zone

其实 kswapd 的任务就是让每一个 zone 的空闲内存都超过高水位,至于页面在各个 order 间的平衡分布就不用管

对于 kswapd 拿到的 order,所以如果达不到平衡分布,就还得继续回收以及尝试小 order 向大 order 的组装

参考:linux kswapd浅析