今天看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

2.6.29的一个节省内存的补丁相关推荐

  1. 【MCU】一种单片机节省内存的方法(补充)

    1.聊一聊 以前听这首曲子内心会变得格外平静,然而现在却五味陈杂! 今天主要跟大家分享一个MCU省内存的办法,同时也欢迎大家在文末问答留言讨论. 2.读前必备 对于MCU节省内存办法大合集bug菌在很 ...

  2. java 内存_java节省内存的几条建议

    java节省内存的几条建议 引导语:Java的主要工作是通过编程语言来制作互联网页面.制作动态效果以及网站等技术,以下是小编整理的java节省内存的几条建议,欢迎参考阅读! 1. 尽量在合适的场合使用 ...

  3. 2020-12-11 keras通过model.fit_generator训练模型(节省内存)

    keras通过model.fit_generator训练模型(节省内存) 前言 前段时间在训练模型的时候,发现当训练集的数量过大,并且输入的图片维度过大时,很容易就超内存了,举个简单例子,如果我们有2 ...

  4. javafx阴影_JavaFX技巧来节省内存! 属性和可观察对象的阴影场

    javafx阴影 在 JavaFX的世界中, Properties API允许UI开发人员将值绑定到UI控件. 这种功能出奇的简单,但是当对象模型经常使用属性时,应用程序可能会很快耗尽内存. 我通常会 ...

  5. JavaFX技巧来节省内存! 属性和可观察物的阴影场

    在 JavaFX的世界中, Properties API允许UI开发人员将值绑定到UI控件. 这种功能非常容易,但是当对象模型经常使用属性时,应用程序可能会很快耗尽内存. 我通常会编写两个单独的对象, ...

  6. python类与对象-如何为创建大量实例节省内存

    如何为创建大量实例节省内存 问题举例 在网络游戏中,定义玩家类Player(id, name, level...), 每个玩家在线将创建一个Player实例,当在线人数很多时,将产生大量实例, 如何降 ...

  7. 节省内存的嵌入式软件设计技巧

    现在新买的安卓千元机都是2G内存的了,我们还要绞尽脑汁地省内存?是的,那是高端处理器的特色,咱们这里讲的是资源紧缺型的嵌入式系统设计方法.一般主控是单片机控制器的电子产品的成本跟内存的关系可是成正比的 ...

  8. 谷歌浏览器扩展程序XDM_这才是谷歌浏览器的正确打开方式,有效节省内存占用,流畅飞起...

    喜欢谷歌chrome浏览器,并不是仅仅因为他的速度快,安全.更多的是因为chrome浏览器拥有丰富的扩展程序,这些扩展程序让谷歌浏览器变得无所不能.如果你的chrome浏览器不安装扩展程序,那真的不如 ...

  9. 一个轻量级内存池的实现与细节

    引言 内存池作为一种的内存管理机制被广泛地运用于各种领域当中,内存池拥有快速的内存分配与更加健壮的管理机制,同时在不同的平台与环境当中也拥有不同的实现方式,本文提出一种轻量级的内存池实现,可以非常方便 ...

最新文章

  1. Shell基础命令之echo
  2. python四十三:静态属性,类方法
  3. html-其他常见标签的使用
  4. python pandas 排序_python – pandas:单独对每列进行排序
  5. Ubuntu16.04版安装VMwareTools的步骤和没法挂载目录问题的解决
  6. 使用TortoiseGit提交代码到github上
  7. 手眼标定(eye in hand)-步骤
  8. 软件项目管理 实验二
  9. NameNode故障处理之数据恢复
  10. 如何给论文添加参考文献
  11. 小波变换matlab程序,图像小波变换原理_图像小波变换的matlab实现详解
  12. Flutter动画Animation开发指南
  13. xmarks android,Xmarks Bookmark Sync
  14. 做IT项目经理什么要求?
  15. 长沙云栖谷交通事故_长沙含浦片区自发成立抗洪救灾志愿者服务队转移被困群众(组图)...
  16. js中数组filter过滤奇偶数_js--数组的filter()过滤方法的使用
  17. DolphinDB智臾科技CEO周小华:《从反向控制的终极目标谈时序数据库的架构设计》
  18. Python商品数据预处理与K-Means聚类可视化分析
  19. STATA 森林图 基于OR值和CI直接画的
  20. 如何消除USB共享网络导致的Windows系统中自动增加的网络设备序列号?

热门文章

  1. VB:您知道 Mid$ 函量可以放在 '=' 的左方吗
  2. 简单而常用的shell 命令
  3. VB小技巧:字符变量中双引号的输入
  4. MySQL(二):MySQL性能优化
  5. 模型参数太多怎么办?用谷歌高效训练库GPipe啊
  6. 科大讯飞携手华南理工,成立脑机接口公司,注册资本4000万
  7. 苹果、小米、荣耀智能手环都能测卫生纸心率?网友“测遍万物”玩坏了
  8. 美国重金投资3D芯片项目!MIT+美独资公司攻关,旨在继续领先中国
  9. 不到一年英伟达股价又翻番了,CFO说:感谢中国、感谢AI
  10. 每日两道前端面试题20190226