文章说明:
-
Linux内核版本:5.0
-
架构:ARM64
-
参考资料及图片来源:《奔跑吧Linux内核》
-
Linux 5.0内核源码注释仓库地址:
zhangzihengya/LinuxSourceCode_v5.0_study (github.com)
1. KSM定义
KSM指Kemel SamePage Merging,即内核同页合并,用于合并内容相同的页面。KSM的出现是为了优化虚拟化中产生的冗余页面,因为虚拟化的实际应用中在同一台主机上会有许多相同的操作系统和应用程序,许多内存页面的内容可能是相同的,所以它们可以合并,从而释放内存供其他应用程序使用。
KSM允许合并同—个进程或不同进程之间内容相同的匿名页面,这对应用程序来说是不可见的。把这些相同的页面合并成一个只读的页面,从而释放出多余的物理页面,当应用程序需要改变页面内容时,会发生写时复制。
2. 使能KSM
KSM只会处理通过madvise系统调用显式指定的用户进程地址空间,因此用户程序想使用这个功能就必须在分配地址空间时显式地调用madvise(addr,length,MADV_MERGEABLE)。如果用户想在KSM中取消某一个用户进程地址空间的合并功能,也需要显式地调用madvise(addr,length,MADV_UNMERGEABLE)。
KSM的sysfs节点在/sys/kernel/mm/ksm/目录下,如下图所示:
其中主要节点的描述如表所示:
KSM在初始化时会创建—个名为ksmd的内核线程
static int __init ksm_init(void)
{...ksm_thread = kthread_run(ksm_scan_thread, NULL, "ksmd");...
}
程序可以显式地调用madvise系统调用把用户进程地址空间添加到KSM系统中
<madvise()->ksm_madvise()->__ksm_enter()>
int __ksm_enter(struct mm_struct *mm)
{...// 把 mm 添加到 mm_slots_hash 哈希表中insert_to_mm_slots_hash(mm, mm_slot);...// 把 mm->slot 添加到 ksm_scan.mm_slot->mm_list 链表中list_add_tail(&mm_slot->mm_list, &ksm_scan.mm_slot->mm_list);...// 设置 mm->flags 中的 MMF_VM_MERGEABLE 标识位,表示这个进程已经被添加到 KSM 系统中set_bit(MMF_VM_MERGEABLE, &mm->flags);...
}
ksm_scan_thread()是ksmd内核线程的主干,它运行ksm_do_scan()函数,扫描和合并页面
static int ksm_scan_thread(void *nothing)
{...while (!kthread_should_stop()) {...if (ksmd_should_run())// ksm_do_scan() 函数在 while 循环中尝试合并页面ksm_do_scan(ksm_thread_pages_to_scan);...}return 0;
}
2. 旧版本KSM的基本实现
Linux 4.13 内核之前KSM的实现: KSM机制下采用两棵红黑树来管理扫描的页面和已经合并的页面,第一棵红黑树称为不稳定红黑树,里面存放了还没有合并的页面。在KSM里,扫描的页面会采用rmap_item数据结构来描述。第二棵红黑树称为稳定红黑树,已经合并的页面会生成—个节点,这个节点为稳定节点。如两个页面的内容是一样的,KSM扫描并发现了它们,因此这两个页面就可以合并成一个页面。对于这个合并后的页面,会设置只读属性,其中一个页面会作为稳定的节点挂载到稳定的红黑树中之后,另外一个页面就会被释放了。但是这两个页面的map_item数据结构会被添加到稳定节点中的hlist链表,如下图所示:
假设有3个VMA(表示进程地址空间),VMA的大小正好是—个页面的大小,分别有3个页面映射这3个VMA。这3个页面准备通过KSM来扫描和合并,这3个页面的内容是相同的。具体步骤如下图所示:
rmap_item 数据结构代表一个页面:
struct rmap_item {// 所有的 rmap_item 连接成一个链表,链表头在 mm_slot.rmap_liststruct rmap_item *rmap_list;union {// 当 rmap_item 加入稳定红黑树时,它指向 VMA 的 anon_vma 数据结构struct anon_vma *anon_vma; /* when stable */
#ifdef CONFIG_NUMA// 内存节点编号int nid; /* when node of unstable tree */
#endif};// 指向进程内存描述符struct mm_struct *mm;// rmap_item 所跟踪的用户空间虚拟地址unsigned long address; /* + low bits used for flags below */// 虚拟地址对应物理页面的旧校验值unsigned int oldchecksum; /* when unstable */union {// rmap_item 加入不稳定红黑树的节点struct rb_node node; /* when node of unstable tree */struct { /* when listed from stable tree */// 加入稳定红黑树的节点struct stable_node *head;// hlist 节点,用于添加到稳定节点的 hlist 链表中struct hlist_node hlist;};};
};
stable_node 数据结构用于描述稳定红黑树的节点,表示一个至少由两个页面合并而成的页面:
struct stable_node {union {// 红黑树节点,用于加入稳定红黑树的节点struct rb_node node; /* when node of stable tree */struct { /* when listed for migration */// 当 head 等于 &migrate_nodes 时表示这是一个临时的节点,主要用于 NUMA 系统struct list_head *head;struct {// 链表节点,用于添加到链式稳定节点的 hlist 链表中struct hlist_node hlist_dup;// 链表节点,用于添加到 migrate_nodes 链表中,等待迁移到合适的内存节点的稳定红黑树中struct list_head list;};};};// 链表头,共享这个 KSM 页面的 rmap_items 都添加到这个链表中struct hlist_head hlist;union {// KSM 页面的帧号unsigned long kpfn;// 上一次垃圾回收的时间unsigned long chain_prune_time;};
#define STABLE_NODE_CHAIN -1024// 挂到 hlist 链表成员的数量,若赋值为 STABLE_NODE_CHAIN,表示是一个链式的稳定节点int rmap_hlist_len;
#ifdef CONFIG_NUMA// 内存节点编号int nid;
#endif
};
3. 新版本KSM的新特性
新版本的KSM(如Linux 5.0 内核的KSM)比Linux 4.0内核的KSM新增了两项特性:
- 内容全是零的页面进行特殊处理
- 对稳定的节点的hlist链表进行改造,以防止大量的相同的页面聚集在一个稳定的节点中,导致页面迁移、内存规整等机制长时间等待这个页面。
上述新特性中,第—项实现起来比较简单,系统中已经存在了系统零页,我们可以对这个系统零页预先计算检查和。若在扫描页面时发现页面的检查和等于系统零页的检查和,那么直接修改这个页面的映射关系,让其映射到系统零页,这样可以释放这个页面。
第二项的实现是限制共享页面的数量在256以内,这样遍历256个页表项的时间将会限制在毫秒级别, 不会导致系统岩机。新的解决方案扩充了稳定节点的hlist链表结构,rmap_items超过256个之 后,扩展稳定的节点为链表。每个链表的成员都是一个稳定的节点,每个稳定的节点有一个hlist 链表,这个hlist链表中可以添加新的rmap_items。另外,还需要把稳定的节点和链表的头在红黑树中做—个交换。
新版本的稳定的节点包含两个形态:
- 传统的稳定的节点:兼容旧版本的稳定的节点格式
- 链式稳定的节点:新版本的链式节点格式
而且它们同时存放在稳定的红黑树中,如下图所示:
假设现在系统产生了1000个页面,并且这1000个页面的内容都是相同的,每个页面对应不同的VMA,这些VMA的大小正好是一个页面大小。接下来观察新版本的KSM如何处理这些页面,如何创建新版本的链式稳定的节点。
…
合并page2:
…
当扫描到第257个页面(page256)时,不稳定的红黑树没有成员,因此可以直接把page256对应的rmap_item添加到不稳定的红黑树中。
接下来,扫描第258个页面(page257),整个过程如下图所示:
合并第259个页面:
…