2.6.29的一个节省内存的补丁

简介:

今天看linux内核的maillist,发现了一个很有创意的补丁,叫做ksm,也就是kernel shared memory driver,读了之后感觉太有创意了,可是不知道到底有没有实际用处。这个补丁的大致思想就是,扫描系统中所有的页面,把内容一样的页面合并为一个,并且设置为只读,然后写时复制,如果系统中存在很多潜在的内容一模一样的页面,那么这个补丁显然可以节省大量的内存,但是问题是,第一,存在这种内容一样的页面的几率大吗?第二就是现在的内存都很不值钱,有必要这种时间换空间的行为吗?可是不管怎样,这个创意很值得欣赏,特别是它的一些数据结构个算法。首先先看一下它的主要数据结构。

struct ksm_memory_region { //每个可以被这个补丁管理的可以扫描的区域用这个结构体表示

__u32 npages; //该区域内的页面数量

__u32 pad;

__u64 addr; //起始的虚拟地址

__u64 reserved_bits;

};

以上的这个数据结构就好比一个vma,它其实是对进程有效的。

struct ksm_mem_slot {

struct list_head link; //系统中所有的这个slot连接成一个链表

struct list_head sma_link; //同时一个sma中的slot也连接成一个链表

struct mm_struct *mm; //这个slot所在的mm,也就是地址空间

unsigned long addr; //这个slot的起始地址

unsigned npages; //这个slot的页面数量

};

注意上面的两个数据结构的异同,它们其实很多地方时一样的,只不过它们有意义的层次不同,ksm_memory_region是用户进程注册用的,ksm内核接收到这个ksm_memory_region以后就会初始化一个slot,然后把这个slot链接到ksm系统,还要注意的就是,slot中的地址区间实际上就是vma中的区间,因此它也有一个mm,指明了使用的是哪一个地址空间,这个mm字段很有用,一会分析代码的时候就会分析到。

struct ksm_sma {

struct list_head sma_slots;

};

这个结构再也简单不过了,这个list_head代表的就是一串slot,哪一串呢?实际上这个结构体是每进程一个的,那么里面的这个list就是该进程的的slot的链表,每个进程都有一个ksm_sma,然后这个进程的所有的slot链接到这个ksm_sma的sma_slots链表中,链表的节点其实就是上面的ksm_mem_slot。

struct ksm_scan {

struct ksm_mem_slot *slot_index; //当前正在扫描的slot,注意肯定有一个mm上下文

unsigned long page_index; //当前的slot的当前页面,因为每个slot中可能有npages个页面,而这个数不一定为1

};

为何slot要有一个mm上下文呢?因为一会合并页面的时候需要找到要合并的页面,这是肯定的,这里给出的都是虚拟地址,而找到页面就需要物理地址,而找到物理地址就需要用MMU的方式,通过页目录->页表的方式,而mmu是基于地址空间的,mm恰恰就是一个地址空间的内核表征,其中存储有pgd。

struct tree_item {

struct rb_node node;

struct rmap_item *rmap_item;

};

这个结构是个树节点,怎么又联系到树了?其实在这个补丁中,要扫描就必须有效的找到需要扫描的页面,那么必须有一套很高效的方式存储这些需要定时扫描的页面,于是红黑树是一个不错的选择,那么这一棵红黑树的键值是什么呢?当然是页面的内容了,在没有更好的方式之前,用memcmp比较两个页面的内荣是一种方式,可是我倒是觉得可以变相用strcmp,因为memcmp必须比较所有的一个页面的内容,而strcmp只需要比较/0之前的就可以了,可是为何说变相使用呢?因为如果两个页面的前面几个都是0,后面的不同,难道strcmp会返回不同吗?很显然不能,因此就要改造这个strcmp,使得不必比较整个页面但是却不会漏掉任何字节。

struct rmap_item { //这是一个反向映射,因为我们不但需要从slot找到树节点,还要有相反的映射

struct hlist_node link;

struct mm_struct *mm;

unsigned long address;

unsigned int oldchecksum; //上一次的校验码

unsigned char stable_tree; //是否在“稳定树”中

struct tree_item *tree_item;

struct rmap_item *next;

struct rmap_item *prev;

};

这个rmap是一个很高效的数据结构,linux的虚拟内存就有一个反向映射,正向映射是从一个页表项映射到唯一一个页面,而反向映射就是从一个页面映射到可能很多的若干个页表项,也就是一对多的反向映射。这里的rmap_item也是这样,一个树节点可能有多个页面与之对应,因为这个补丁的作用就是促使很多的相同的物理页面合并为一个,也就是很多的slot对应一个树节点。下面开始动人心魄的算法。

static inline int PageKsm(struct page *page)

{

return !PageAnon(page);

}

这个函数很有意思,虽然这个补丁的设计者不能区分什么样的页面是共享页面,但是他可以断定,只要不是匿名的页面,那么一定是共享的,也就是说非匿名页是页面共享的必要条件而不是充分条件。

static inline u32 calc_checksum(struct page *page)

{

u32 checksum;

void *addr = kmap_atomic(page, KM_USER0); //临时映射到KM_USER0,注意,不要睡觉

checksum = jhash(addr, PAGE_SIZE, 17); //计算这个页面的内容的hash值

kunmap_atomic(addr, KM_USER0);

return checksum; //返回这个hash值

}

以上这个函数计算一个页面的hash值,最终将这个hash值存入rmap_item结构的oldchechsum中,如果再次计算的时候这个值变了,那么就说明页面被写了。

static struct rmap_item *get_rmap_item(struct mm_struct *mm, unsigned long addr)

{

struct rmap_item *rmap_item;

struct hlist_head *bucket;

struct hlist_node *node;

bucket = &rmap_hash[addr % nrmaps_hash]; //得到这个地址的hash桶

hlist_for_each_entry(rmap_item, node, bucket, link) {

if (mm == rmap_item->mm && rmap_item->address == addr) {

return rmap_item;

}

}

return NULL;

}

以上这个函数得到一个反向映射的结构,也就是说,给一个虚拟地址和一个地址空间,然后返回一个反向映射,要知道既然是反向映射,那么就不止一个,那么这些相同的反向映射就连接进一个hash表中,也就是说,红黑树的每一个节点存有一个共享的物理页面,所有共享这个物理页面的slot链接进一个hash链表。

static struct rmap_item *create_new_rmap_item(struct mm_struct *mm, unsigned long addr, unsigned int checksum)

{

struct rmap_item *rmap_item;

struct hlist_head *bucket;

rmap_item = alloc_rmap_item();

if (!rmap_item)

return NULL;

rmap_item->mm = mm;

rmap_item->address = addr;

rmap_item->oldchecksum = checksum;

rmap_item->stable_tree = 0;

rmap_item->tree_item = NULL;

bucket = &rmap_hash[addr % nrmaps_hash]; //计算hash桶

hlist_add_head(&rmap_item->link, bucket); //加入hash链表

return rmap_item;

}

以上函数创建一个新的反向映射节点,这个反向映射节点有两个用途,一个就是计算hash值之后,然后链接进相应hash桶的链表,另外一个用途就是链接进红黑树,如果已经可以在红黑树中找到,那么就连接入该找到的节点的反向映射的链表。一共有两棵树,一棵是stable树,表述已经被共享的页面,一棵是unstable树,表述还没有被共享的,但是可能被共享的页面,凡是扫描到一个页面,就会先在stable树种寻找可能和这个页面一样的页面节点,如果找到的话,那么将这两个页面合并,在合并之前就将合并后的页面设置为一个写保护的写时复制页面,然后将这个反向映射加入树,既然找了可以合并的节点,那么只需要将这个反向映射连接到这个找到的节点的反向映射的链表就可以了;如果没有找到,那么无论如何先将这个被扫描的页面和unstable树的每个节点比较,如果找到相同的,那么尝试合并,如果合并成功则退出unstable树而加入stable树,如果不成功,那么最起码加入了unstable树,等到下次扫描的时候,如果没有变化,那么就有可能加入stable树,注意,查找hash的时候是根据mm和addr进行的,这一切都在下面的这个函数中:

static int cmp_and_merge_page(struct ksm_scan *ksm_scan, struct page *page)

{

struct page *page2[1];

struct ksm_mem_slot *slot;

struct tree_item *tree_item;

struct rmap_item *rmap_item;

struct rmap_item *tree_rmap_item;

unsigned int checksum;

unsigned long addr;

int wait = 0;

int ret;

slot = ksm_scan->slot_index;

addr = slot->addr + ksm_scan->page_index * PAGE_SIZE;

rmap_item = get_rmap_item(slot->mm, addr);

if (rmap_item) {

if (update_tree(rmap_item, &wait))

rmap_item = NULL;

} //以下先在stable树中寻找和这个page对应的反向映射,并且初始化page2,如果找到,那就说明page2就是一个已经被共享的页面,这个page就可以和page2合并,并且也不用再将这个反向映射插入到树中了,而只需要连接入树节点的吊链就可以

tree_rmap_item = stable_tree_search(page, page2, rmap_item);

if (tree_rmap_item) {

ret = try_to_merge_two_pages(slot->mm, page, tree_rmap_item->mm, page2[0], addr, tree_rmap_item->address);

put_page(page2[0]);

if (!ret) {

if (!rmap_item)

rmap_item = create_new_rmap_item(slot->mm, addr, 0); //如果还没有一个反向映射,那么构造一个并且连接到相应hash桶的hlist链表

...

rmap_item->next = tree_rmap_item->next; //既然已经在stable树中找到了这个tree_item,那么就不用插入了,而是在相应的节点的反向映射的链表中连接入就可以了,这个和文件缓存页面的优先级树的反向映射思想一样,都是树吊链结构,每个节点吊一条链子

rmap_item->prev = tree_rmap_item;

if (tree_rmap_item->next)

tree_rmap_item->next->prev = rmap_item;

tree_rmap_item->next = rmap_item;

rmap_item->stable_tree = 1;

rmap_item->tree_item = tree_rmap_item->tree_item;

}

ret = !ret;

goto out;

}

if (rmap_item) { //在已经找到反向映射的情况下,说明上一次加入stable树没有成功,那么如果这次还是不成功,则仍然放弃

checksum = calc_checksum(page); //如果校验码变化了,那么就说明这个页面是变的,就不必要继续了,它即使和别的页面合并,那么由于写时复制而被踢出的可能性也是很大。

if (rmap_item->oldchecksum != checksum) {

rmap_item->oldchecksum = checksum;

goto out;

}

}

tree_item = unstable_tree_search_insert(page, page2, rmap_item);

if (tree_item) {

rmap_item = tree_item->rmap_item;

ret = try_to_merge_two_pages(slot->mm, page, rmap_item->mm, page2[0], addr, rmap_item->address);

if (!ret) { //如果合并成功,那么就将这个反向映射插入到stable树中,其实也就是在这里有机会将反向映射插入stable树

rb_erase(&tree_item->node, &root_unstable_tree);

stable_tree_insert(page2[0], tree_item, rmap_item);

}

put_page(page2[0]);

ret = !ret;

goto out;

}

...

}

最后,我们看一下扫面的过程,其实很简单,就是将slots链表的节点挨个扫描,每一个slot的每一个页面对齐的地址都要按照get_user_pages页面都要根据slot结构中的mm字段用get_user_pages来的到页面,用这个页面按照stable树->unstable树的顺序每个节点比较,如果找到相同的,那么尝试合并,如果比较过程中,发现有些页面被换出或者由于cow改变了属性,那么就在树中删除它。系统每隔一段时间就会扫描整个系统的所有slots尝试发现可以被合并的页面。整个过程十分简单,就是基于两棵树,可是这个算法的思想非常不错,可以说极端的不错,虽然显得有些没有什么用处,但是我就是觉得好,一棵stable树维持了一个稳定的已经被合并的地址反向映射集合,而一棵unstable树提供了候选的节点,这就是一个阶梯状的体系,一个朴素的选择优先级的体现。最后的问题就是用户接口,这个补丁用的是vfs接口,ioctl控制。

这个补丁的实现依赖于一个帮助函数,就是replace_page,这个函数按照其作者的解释就是:this function is needed in cases you want to change the userspace virtual mapping into diffrent physical page,this function is working by removing the oldpage from the rmap and

calling put_page on it, and by setting the virtual address pte to point into the new page.

2.6.29的一个节省内存的补丁(续)

ksm补丁巧妙的用rmap_item这个数据结构来作为键值来索引红黑树中的元素,我们看一下红黑树中吊链的查找代码: 
while (found_rmap_item) { 


 本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1273501


相关文章
|
8月前
|
安全 API 定位技术
6.3 应用动态内存补丁
动态内存补丁可以理解为在程序运行时动态地修改程序的内存,在某些时候某些应用程序会带壳运行,而此类程序的机器码只有在内存中被展开时才可以被修改,而想要修改此类应用程序动态补丁将是一个不错的选择,动态补丁的原理是通过`CreateProcess`函数传递`CREATE_SUSPENDED`将程序运行起来并暂停,此时程序会在内存中被解码,当程序被解码后我们则可以通过内存读写实现对特定区域的动态补丁。
55 2
6.3 应用动态内存补丁
|
存储 测试技术
《vSphere性能设计:性能密集场景下CPU、内存、存储及网络的最佳设计实践》一3.1.2 测试新的应用和补丁
本节书摘来华章计算机《vSphere性能设计:性能密集场景下CPU、内存、存储及网络的最佳设计实践》一书中的第3章 ,第3.1.2节,[美] 克里斯托弗·库塞克(Christopher Kusek) 著 吕南德特·施皮斯(Rynardt Spies)姚海鹏 刘韵洁 译, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1115 0
|
17天前
|
Linux
Linux rsyslog占用内存CPU过高解决办法
该文档描述了`rsyslog`占用内存过高的问题及其解决方案。
40 4
|
1月前
|
移动开发 运维 监控
掌握Linux运维利器:查看CPU和内存占用,轻松解决性能问题!
掌握Linux运维利器:查看CPU和内存占用,轻松解决性能问题!
|
1月前
|
监控 Python
【python】实现cpu/内存监控的功能(非常简单)
【python】实现cpu/内存监控的功能(非常简单)
|
1月前
|
Linux
Linux 查看进程PID和线程CPU和内存占用情况
Linux 查看进程PID和线程CPU和内存占用情况
37 0
|
2月前
|
运维 监控 网络协议
JAVA 线上故障排查完整套路,从 CPU、磁盘、内存、网络、GC
JAVA 线上故障排查完整套路,从 CPU、磁盘、内存、网络、GC
74 0
|
1月前
|
移动开发 Linux
Linux下如何查看哪些进程占用的CPU内存资源最多
Linux下如何查看哪些进程占用的CPU内存资源最多
|
22天前
|
机器学习/深度学习 缓存 监控
linux查看CPU、内存、网络、磁盘IO命令
`Linux`系统中,使用`top`命令查看CPU状态,要查看CPU详细信息,可利用`cat /proc/cpuinfo`相关命令。`free`命令用于查看内存使用情况。网络相关命令包括`ifconfig`(查看网卡状态)、`ifdown/ifup`(禁用/启用网卡)、`netstat`(列出网络连接,如`-tuln`组合)以及`nslookup`、`ping`、`telnet`、`traceroute`等。磁盘IO方面,`iostat`(如`-k -p ALL`)显示磁盘IO统计,`iotop`(如`-o -d 1`)则用于查看磁盘IO瓶颈。
|
11天前
|
存储 弹性计算 固态存储
阿里云服务器CPU内存配置详细指南,如何选择合适云服务器配置?
阿里云服务器配置选择涉及CPU、内存、公网带宽和磁盘。个人开发者或中小企业推荐使用轻量应用服务器或ECS经济型e实例,如2核2G3M配置,适合低流量网站。企业用户则应选择企业级独享型ECS,如通用算力型u1、计算型c7或通用型g7,至少2核4G配置,公网带宽建议5M,系统盘可选SSD或ESSD云盘。选择时考虑实际应用需求和性能稳定性。
122 6