当前位置: 首页 > 科技观察

深入解析Linux内核反向映射机制

时间:2023-03-20 14:53:37 科技观察

作者简介Cheetah,曾为U-boot社区和Linux内核社区提交过多个补丁,主要从事Linux相关系统软件开发,负责Soc芯片BringUp和系统软件开发,喜欢阅读Kernel源码,通过不断的学习和工作深入了解内存管理、进程调度、文件系统、设备驱动等内核子系统。为了系统的安全,Linux内核将每个用户进程都运行在自己独立的虚拟地址空间中。用户进程之间通过虚拟地址空间相互隔离,不能相互访问。一个进程的崩溃不会影响整个系统的异常。它不会干扰系统和其他进程的运行。Linux内核通过共享内存可以为系统节省大量内存。比如fork子进程时,父子进程以只读方式共享所有私有页面。再比如通过IPC共享内存的方式,各个无关进程可以直接共享一块物理内存等等。我们都知道,操作系统开启mmu后,CPU访问的都是虚拟地址。CPU访问虚拟地址时,需要通过mmu将虚拟地址转换为物理地址。这称为正向映射。与本文相关的是反向映射,主要是利用物理页来查找共享该页的所有vma对应的页表项。这就是本文要讨论的问题。本文内容:1.反向映射开发2.反向映射应用场景3.匿名页面反向映射4.文件页面反向映射5.ksm页面反向映射5.总结注:反向映射机制是难点Linux内核虚拟内存管理,理解内存管理的关键技术之一!!1.反向映射的发展其实在早期的Linux内核版本中并没有反向映射的概念。当时为了找到一个物理页对应的页表项需要遍历系统中所有由mm组成的链表,然后对每个mm遍历每个vma,然后检查vma是否映射到这一页。这个过程极其漫长且效率低下。有些时候你必须遍历所有的mm才能找到所有映射到这个页面的pte。后来人们发现这个问题,就在物理页的页结构中添加了一个指针来解决。通过这个指针,他们可以找到一个数组结构来描述映射这个页面的所有pte,这对于反向映射查找所有pte很容易,但是带来了浪费内存的问题。然后在2.6内核中,内核高手们想到了复用page结构中的mapping域,然后通过红黑树把所有映射这个page的vmas组织起来,形成匿名页和文件页的反向映射机制。下面是匿名页面的反向映射图:下面是文件页面的反向映射图:但是匿名页面的反向映射遇到了效率和锁竞争激烈的问题,促使目前使用avc联系每一层反向映射结构,然后通过这种方式减少锁的粒度。可见,逆向映射的发展伴随着Linux内核的发展,是一个不断优化和进化的过程。2、反向映射应用场景那么Linux内核为什么需要反向映射机制呢?它会产生什么样的问题?设想以下场景:(1)一个物理页面被vma映射的多个进程使用,系统进程内存不足,需要回收部分页面。正好这个页面适合我们回收。能不能直接把这个页面返回给合作伙伴系统?答案肯定不是。因为这个页面是被很多进程共享的,所以我们要做的就是打破这个页面的映射关系,也就是反向映射所做的事情。(2)在某些情况下,我们需要将一个页面迁移到另一个页面,但这会牵一发而动全身。可能有一些进程已经将即将迁移的页面映射到了自己的vma中,所以我们也需要知道这个时候是怎么回事,这个页面映射到了哪个vma中呢?这也是反向映射所做的。其实反向映射的主要应用场景是内存回收和页面迁移。当系统发生内存回收和页面迁移时,Linux内核会判断每个候选页面是否为映射页面。如果是,就会调用try_to_unmap来解除页表映射关系,本文主要从try_to_unmap函数来解读反向映射机制。如果我们细看其他的内核子系统,就会发现在内存回收、内存碎片整理、CMA、大页面、页面迁移等各种场景中都可以找到反向映射的关键工作。Linux内核中映射机制的实现是理解和掌握这些子系统的基础和关键,否则你将无法理解这些技术背后的脊髓,所以理解逆向映射机制对于理解Linux内核内存至关重要管理。重要的!!!3、匿名页面的反向映射匿名页面的共享主要发生在父进程fork子进程时。parentfork子进程时,会将所有vma复制给子进程,并调用dup_mmap->anon_vma_fork建立子进程的rmap以及与elder进程的rmap关系结构:主要是通过红黑树在anon_vma数据结构来链接共享父进程page的所有子进程的vma(通过anon_vma_chain连接对应的vma和av),当然这种关系的建立比较复杂,涉及到vma、avc等数据结构和av。.关联page和vma时出现pagefault异常do_anonymous_page。当内存回收或页面迁移时,内核路径最终会调用:try_to_unmap//mm/rmap.c->rmap_walk->rmap_walk_anon->anon_vma_interval_tree_foreach(avc,&anon_vma->rb_root,pgoff_start,pgoff_end)->rwc->for候选页,rmap_one->try_to_unmap_one会得到候选页关联的anon_vma,然后从anon_vma的红黑树遍历到共享该页的所有vma,再通过try_to_unmap_one为每个vma处理对应的页表项,取消映射关系。4、文件页的反向映射文件页的共享主要发生在多个进程共享libc库时。同一个库文件只能读入pagecache一次,然后通过各个进程的页表映射到各个进程的vma。vma是通过address_space的区间树来管理共享文件页的。在mmap或fork时,将vma添加到这个区间树中:当发生文件映射页面错误时,将页面与address_space相关联。当内存回收或页面迁移时,内核路径最终会调用:try_to_unmap//mm/rmap.c->rmap_walk->rmap_walk_file->vma_interval_tree_foreach(vma,&mapping>i_mmap,pgoff_start,pgoff_end)->rwc->rmap_one对于每一个候选文件页,如果是映射页,则遍历该页对应的address_space区间树,对每一个满足条件的vma,调用try_to_unmap_one寻找pte,释放映射关系。5、ksm页面反向映射的ksm机制是内核合并页面内容相同的页面(ksm管理匿名页面),将映射到该页面的页表项标记为只读,然后释放原来的页表达到节省大量内存的目的,这对于宿主机开启多个虚拟机的应用场景非常有用。ksm机制会管理两棵红黑树,一棵是稳定树,一棵是不稳定树。stable_node中各个节点管理的页面都是页面内容完全相同的页面(称为kpage),sharedkpage页面的页表项会被标记为只读,并且会有rmap_item用于描述原始候选页面其反向映射(anon_vma成员的红黑树是描述该候选页面映射的所有vma的集合),合并时会加入到对应的稳定树节点和链表中。当内存回收或页面迁移时,内核路径最终会调用:try_to_unmap//mm/rmap.c->rmap_walk->rmap_walk_ksm//mm/ksm.c->hlist_for_each_entry(rmap_item,&stable_node->hlist,hlist)->anon_vma_interval_tree_foreach(vmac,&anon_vma->rb_root,0,ULONG_MAX)->rwc->rmap_one对于一个ksm页面,反向映射时,会得到ksm页面对应的节点,然后遍历该节点的hlist链表得到的每一个anon_vma和上面介绍的匿名页面的反向映射是一样的。从anon_vma的红黑树中找到所有的vma,最后try_to_unmap_one找到pte,释放映射关系。6.小结我们介绍了三种反向映射,匿名页、文件页和ksm页的反向映射,通过页对应的vma、address_space、stable_node结构找到vma。当然,我们只是介绍了Linux内核中反向映射的冰山一角,主要是try_to_unmap函数。其实每一个反向映射的每一个数据结构的建立过程都是错综复杂的,不是一篇文章三言两语说清楚的。它们分散在Linux内核源代码中。代码的过程创建fork、内存映射mmap、缺页异常处理、文件系统等角落。诚然,如果我们不知道各种反正映射对应的各种数据结构之间的关系,或者只是有一些概念上的了解,并没有真正掌握这种机制的实现原理,那么理解Linux内核虚拟内存管理是一个障碍。很多反向映射内存管理的问题不理解,是看不懂的!本文转载自微信♂“Linux代码阅读田”,可通过以下二维码关注。转载本文请联系Linux代码阅读领域公众号。