Linux内存逆向映射(reverse mapping)技术的前世今生

2023-11-19

本文来自于微信公众号Linux阅码场

一、前言

数学大师陈省身有一句话是这样说的:了解历史的变化是了解这门学科的一个步骤。今天,我把这句话应用到一个具体的Linux模块:了解逆向映射的最好的方法是了解它的历史。本文介绍了Linux内核中的逆向映射机制如何从无到有,如何从笨重到轻盈的历史过程,通过这些历史的演进过程,希望能对逆向映射有更加深入的理解。

二、基础知识

在切入逆向映射的历史之前,我们还是简单看看一些基础的概念,这主要包括两个方面:一个是逆向映射的定义,另外一个是引入逆向映射的原因。

1、什么是逆向映射(reverse mapping)?

在聊逆向映射之前,我们先聊聊正向映射好了,当你明白了正向映射,逆向映射的概念也就易如反掌了。所谓正向映射,就是在已知虚拟地址和物理地址(或者page number、page struct)的情况下,为地址映射建立起完整的页表的过程。例如,进程分配了一段VMA之后,并无对应的page frame(即没有分配物理地址),直到程序访问了这段VMA之后,产生异常,由内核为其分配物理页面并建立起所有的各级的translation table。通过正向映射,我们可以将进程虚拟地址空间中的虚拟页面映射到对应的物理页面(page frame)。

逆向映射相反,在已知page frame的情况下(可能是PFN、可能是指向page descriptor的指针,也可能是物理地址,内核有各种宏定义用于在它们之间进行转换),找到映射到该物理页面的虚拟页面们。由于一个page frame可以在多个进程之间共享,因此逆向映射的任务是把分散在各个进程地址空间中的所有的page table entry全部找出来。

一般来说,一个进程的地址空间内不会把两个虚拟地址mapping到一个page frame上去,如果有多个mapping,那么多半是这个page被多个进程共享。最简单的例子就是采用COW的进程fork,在进程没有写的动作之前,内核是不会分配新的page frame的,因此父子进程共享一个物理页面。还有一个例子和c lib相关,由于c lib是基础库,它会file mapping到很多进程地址空间中,那么c lib中的程序正文段对应的page frame应该会有非常多的page table entries与之对应。

2、为何需要逆向映射?

之所以建立逆向映射机制主要是为了方便页面回收。当页面回收机制启动之后,如果回收的page frame是位于内核中的各种内存cache中(例如 slab内存分配器),那么这些页面其实是可以直接回收,没有相关的页表操作。如果回收的是用户进程空间的page frame,那么在回收之前,内核需要对该page frame进行unmapping的操作,即找到所有的page table entries,然后进行对应的修改操作。当然,如果页面是dirty的,我们还需要一些必要的磁盘IO操作。

可以给出一个实际的例子,例如swapping机制,在释放一个匿名映射页面的时候,要求对所有相关的页表项进行更改,将swap area和page slot index写入页表项中。只有在所有指向该page frame的页表项修改完毕后才可以将该页交换到磁盘,并且回收这个page frame。demand paging的场景是类似的,只不过是需要把所有的page table entry清零,这里就不赘述了。

三、史前文明

盘古开天辟地之前,宇宙混沌一片。对于逆向映射这个场景,我们的问题就是:没有逆向映射之前,混沌的内核世界是怎样的呢?这一章主要是回答这个问题的,分析的基础是2.4.18内核的源代码。

1、没有逆向映射,系统如何运作?

也许年轻的内核工程师很难想象没有逆向映射的内核世界,但实际上2.4时期的内核就是这样的。让我们想象一下,我们自己就是page reclaim机制的维护者,看看我们目前的困境:如果没有逆向映射机制,那么struct page中没有维护任何的逆向映射的数据。这种情况下,内核不可能通过简单的方法来找到page frame所对应的那些PTEs。当回收一个被多个进程共享的page frame,我们该怎么办呢?

本身回收用户进程的物理页帧并不复杂,这需要memory mapping和swapping机制的支持。这两种机制的工作原理类似,只不过一个用于file mapped page,另外一个用于anonymous page。不过对于页面回收而言,他们的工作原理类似:就是把某些进程不常使用的page frame交换到磁盘上去,同时解除进程和这个page frame的一切关系,完成这两步之后,这个物理页帧已经自由了,可以回收到伙伴系统中。

OK,了解了基本原理,现在需要看看如何具体实现:不常使用的page frame很好找(inactive lru链表),不过断绝page frame和进程们之间的关系很难,因为没有逆向映射。不过这难不倒Linux内核开发人员,他们选择了扫描整个系统的各个进程的地址空间的方法。

2、如何对进程地址空间进行扫描?

下图是一个对进程地址空间进行扫描的示意图:

系统中的所有进程地址空间(memory descriptor)被串成一个链表,链表头就是init_mm,系统中所有的进程地址空间都挂在了这个链表中。所谓scan当然就是沿着这条mm链表进行了。当然,页面回收算法尽量不scan整个系统的全部进程地址空间,毕竟那是一个比较笨的办法。回收算法可以考虑收缩内存cache,也可以遍历inactive_list来试图完成本次reclaim数目的要求(该链表中有些page不和任何进程相关),如果通过这些方法释放了足够多的page frame,那么一切都搞定了,不需要scan进程地址空间。当然,情况并非总是那么美好,有时候,必须启动进程物理页面回收过程才能满足页面回收的要求。

进程物理页面回收过程是通过调用swap_out函数完成的,而scan进程地址空间的代码也是开始于这个函数。该函数是一个三层嵌套结构:

(1) 首先沿着init_mm,对每一个进程地址空间进行扫描

(2) 在扫描一个进程地址空间的时候,对属于该进程地址空间的每一个VMA进行扫描

(3) 在扫描每一个VMA的时候,对属于该VMA的每一个page进行扫描

在扫描过程中,如果命中了进程A的page frame0,由于该page只是被进程A 使用(即只是被A进程mapping),那么可直接unmap并回收该page。对于共享页面,我们不能这么处理了,例如上图中的page frame 1,但scan A进程的时候,如果条件符合,那么我们会unmap该page,解除它和进程A的关系,当然,这时候不能回收该page,因为进程X还在使用该page。直到scan过程历经千山万水来到进程X,完成对page frame 1的unmaping操作,该物理页面才可以真正会伙伴系统的怀抱。

3、地址空间扫描的细节问题

首先,第一个问题:到底scan多少虚拟地址空间才停止scan呢?当目标已经达到的时候,例如本次scan打算reclaim 32个page frame,如果目标达到,那么scan停止,不需scan全部虚拟地址空间。还有一种比较悲惨的情况,那就是scan了系统中所有的地址空间之后,仍然没有达成目标,这时候也就可以停止了,不过这属于OOM的处理了。为了确保系统中的进程被均匀的scan(毕竟swap out会影响进程性能,我们肯定不能只逮住部分进程薅其羊毛),每次scan完成后,记录当前scan的位置(保存在swap_mm变量),等下次又启动scan过程的时候,从swap_mm开始继续scan。

由于对性能有影响,swap out需要雨露均沾,各个进程都跑不掉。同样的道理,对于一个进程的地址空间,我们一样也是需要公平对待,因此需要保存每次scan的虚拟地址(mm->swap_address),这样,每次重启scan的时候,总是从swap_mm那个地址空间的mm->swap_address虚拟地址开始scan。

具体对一个page frame进行swap out的代码位于try_to_swap_out函数中,在这个函数中,如果条件满足,会解除该page frame的该进程之间的关系,完成必要的IO操作,该page reference count减一,对应的pte清零或者设定swap entry等。当然,swap out一个page之后,我们并非一定能够回收它,因为这个page很可能被多个进程共享。而在scan过程中,如果碰巧找到了该page对应的所有的页面表条目,那么说明该页面已经不被任何进程引用,这时候该page frame就会被逐出磁盘,从而完成一个页面的回收。

四、开天辟地

时间又回到2002年1月,那时VM大神Rik van Riel遭遇了人生中的一次重大挫折,他的耗费心血维护的代码被一个全新的VM子系统取代了。不过Rik van Riel并没有消沉下去,他在憋大招,也就是传说中的reverse mapping(后文简称rmap)。本章主要描述第一个版本的rmap,代码来自Linux 2.6.0。

1、设计概念

如何构建rmap?最直观的想法就是针对每一个page frame,我们维护一个链表,保存属于该page的所有PTEs。因此,Rik van Riel给struct page增加了一个pte chain的成员,以便把所有mapping到该page的pte entry指针给串起来。这样想要unmap一个page就易如反掌了,沿着这个pte chain就可以找到所有的mappings。一个基本的示意图如下,下面的小节会给出更详细的解释。

2、对Struct page的修改

Struct page的修改如下:

struct page {

……

union {

struct pte_chain *chain;

pte_addr_t direct;

} pte;

……

当然,很多页面都不是共享的,只有一个pte entry,因此direct直接指向那个pte entry就OK了。如果存在页面共享的情况,那么chain成员则会指向一个struct pte_chain的链表。

3、定义struct pte_chain

struct pte_chain {

unsigned long next_and_idx;

pte_addr_t ptes[NRPTE];

} ____cacheline_aligned;

如果pte_chain只保存一个pte entry的指针那么就太浪费了,比较好的方法是把struct pte_chain对齐在cache line并让整个struct pte_chain占用一个cache line。除了next_and_idx用于指向下一个pte_chain,形成链表之外,其余的空间都用于保存pte entry指针。由于pte entry指针形成了数组,因此我们还需要一个index指示下一个空闲的pte entry pointer的位置,由于pte_chain对齐在cache line,因此next_and_idx的LSB的若干个bit是等于0的,可以复用做index。

4、页面回收算法的修改

在进入基于rmap的页面回收算法之前,让我们先回忆一下痛苦的过去。假设一个物理页面P被A和B两个进程共享,在过去,释放P这个物理页面需要扫描进程地址空间,首先scan到A进程,解除P和A进程的关系,但是这时候不能回收,B进程还在使用该page frame。当然扫描过程最终会来到B进程,只有在这时候才有机会回收这个物理页面P。你可能会问:如果scan B进程地址空间的时候,A进程又访问了P从而导致映射建立。然后scan A的时候,B进程又再次访问,如此反反复复,那么P不就永远无法回收了吗?这个怎么办呢?这个……理论上是这样的,别问我,其实我也很绝望。

有了rmap,页面回收算法顿时感觉轻松多了,只要是页面回收算法看中的page frame,总是能够通过try_to_unmap解除和所有进程的关联,从而将其回收到伙伴系统中。如果该page frame没有共享(page flag设定PG_direct flag),那么page->pte.direct直接命中pte entry,调用try_to_unmap_one来进行unmap的操作。如果映射到了多个虚拟地址空间,那么沿着pte_chain依次调用try_to_unmap_one来进行unmap的操作。

五、女娲补天

虽然Rik van Riel开辟了逆向映射的新天地,但是,天和地都有着巨大的窟窿,需要有人修补。首先让我们看看这个“巨大的窟窿”是什么?

在引入第一个版本的rmap之后,Linux的页面回收变得简单、可控了,但是这个简单的设计是有代价的:每一个struct page增加一个指针成员,在32bit的系统上也就是增加了4B。考虑到系统为了管理内存会为每一个page frame建立一个struct page对象,引入rmap而导致的内存开销也不是一个小数目啊。此外,share page需要建立pte_chain链表,也是一个不小的内存开销。除了内存方面的压力,第一个版本的rmap对性能也造成了一定的影响。例如:在fork操作的时候,父子进程共享了很多的page frame,这样,在copy page table的时候就会伴随大量的pte_chain的操作,从而让fork的速度变得缓慢。

本章就是带领大家看看object-based reverse mapping(后文简称objrmap)是如何填补那个“巨大的窟窿”。本章的代码来自2.6.11版本的内核。

1、问题的引入

推动rmap优化的动力来自内存方面的压力,与此相关的问题是:32-bit的Linux内核是否支持4G以上的memory。在1999年,Linus的决定是:32-bit的Linux内核永远也不会支持2G以上的内存。不过历史的洪流不可阻挡,处理器厂商设计了扩展模块以便寻址更多的内存,高端的服务器也配置了越来越多的内存。这也迫使Linus改变之前的思路,让Linux内核支持更大的内存。

红帽公司的Andrea Arcangeli当时正在做的工作就是让32-bit的Linux运行在配置超过32G内存的公司服务器上。在这些服务器上往往启动大量的进程,共享了大量的物理页帧,消耗了大量的内存。对于Andrea Arcangeli来说,内存消耗的真正元凶是明确的:逆向映射模块,这个模块消耗了太多的low memory,从而导致了系统的各种crash。为了让自己的工作继续推进,他必须解决rmap引入的内存扩展性(memory scalability)问题。

2、file mapped的优化

并非只有Andrea Arcangeli关注到了rmap的内存问题,在2.5版本的开发过程中,IBM公司的Dave McCracken就已经提交了patch,试图在保证逆向映射功能的基础上,同时又能修正rmap带来的各种问题。

Dave McCracken的方案是一种基于对象的逆向映射机制。在过去,通过rmap,我们可以从struct page直接获取其对应的ptes,objrmap的方法借助其他的数据对象来完成从struct page检索到其对应ptes的过程,这个过程的示意图如下:

对于objrmap而言,寻找一个page frame的mappings是一个比较长的路径,它借助了VMA(struct vm_area_struct)这个数据对象。我们知道对于某些page frame是有后备文件的,这种类型的页面和某个文件相关,例如进程的正文段和该进程的可执行文件相关。此外,进程可以调用mmap()对某个文件进行mapping。对于这些页帧我们称之file mapped page。

对于这些文件映射页面,其struct page中有一个成员mapping指向一个struct address_space,address_space是和文件相关的,它保存了文件page cache相关的信息。当然,我们这个场景主要关注一个叫做i_mmap的成员。一个文件可能会被映射到多个进程的多个VMA中,所有的这些VMA都被挂入到i_mmap指向的Priority search tree中。

当然,我们最终的目标是PTEs,下面这幅图展示了如何从VMA和struct page中的信息导出该page frame的虚拟地址的:

而在linux kernel中,函数vma_address可以完成这个功能:

static inline unsigned long

vma_address(struct page *page, struct vm_area_struct *vma)

{

pgoff_t pgoff = page->index << (PAGE_CACHE_SHIFT - PAGE_SHIFT);

unsigned long address;

address = vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT);

return address;

}

对于file mapped page,page->index表示的是映射到文件内的偏移(Byte为单位),而vma->vm_pgoff表示的是该VMA映射到文件内的偏移(page为单位),因此,通过vma->vm_pgoff和page->index可以得到该page frame在VMA中的地址偏移,再加上vma->vm_start就可以得到该page frame的虚拟地址。有了虚拟地址和地址空间(vma->vm_mm),我们就可以通过各级页表找到该page对应的pte entry。

3、匿名页面的优化

我们都知道,用户空间进程的页面主要有两种,一种是file mapped page,另外一种是anonymous mapped page。Dave McCracken的objrmap方案虽好,但是只是适用于file mapped page,对于匿名映射页面,这个方案无能为力。因此,我们必须为匿名映射页面也设计一种基于对象的逆向映射机制,最后形成full objrmap方案。

为了解决内存扩展性的问题,Andrea Arcangeli全力工作在full objrmap方案上,不过他还有一个竞争对手,Hugh Dickins,同时也提交了一系列full objrmap补丁,试图并入内核主线,显然,在匿名映射页面上,最后胜出的是Andrea Arcangeli,他的匿名映射方案如下图所示:

和file mapped类似,anonymous page也是通过VMA来寻找page frame对应的pte entry。由于文件映射页面的VMA数量可能非常大,因此我们采用Priority search tree这样的数据结构。对于匿名映射页面,其数量一般不会太大,所以使用链表结构就OK了。

为了节省内存,我们复用了struct page中的mapping指针:一个page frame如果是file mapped,其mapping指针指向对应文件的address_space数据结构。如果是anonymous page,那么mapping指针指向anon_vma数据结构。虽然节省了内存,但是降低了可读性,但是由于内核会为每一个page frame建立一个对应的struct page数据对象,该数据结构即便是增加4B对整个系统的内存消耗都是巨大的,因此内核还是采用了较为丑陋的方式来定义mapping这个成员。通过struct page中的mapping成员我们可以获得该page映射相关的信息,总结如下:

(1) 等于NULL,表示该page frame不再内存中,而是被swap out到磁盘去了。

(2) 如果不等于NULL,并且least signification bit等于1,表示该page frame是匿名映射页面,mapping指向了一个anon_vma的数据结构。

(3) 如果不等于NULL,并且least signification bit等于0,表示该page frame是文件映射页面,mapping指向了一个该文件的address_space数据结构。

通过anon_vma数据结构,我们可以得到映射到该page的所有的VMA,至此,匿名映射和file mapped汇合,进一步解决的问题仅仅是如何从VMA到pte entry而已。上一节,我们描述了vma_address函数如何获取file mapped page的虚拟地址,其实anonymous page的逻辑是一样的,只不过vma->vm_pgoff和page->index的基础点不一样了,对于file mapped的场景,这个基础点是文件起始位置。对于匿名映射,起始点有两种情况,一种是share anonymous mapping,起点位置是0。另外一种是private anonymous mapping,起点位置是mapping的虚拟地址(除以page size)。但是不管如何,从VMA和struct page得到对应虚拟地址的算法概念是类似的。

六、卷土重来

full objrmap进入内核之后,看起来一切都很完美了,比起她的前任,Rik van Riel的rmap方案,objrmap各方面的指标都是全面碾压rmap。首次将逆向映射引入内核的大神Rik van Riel遭受了第二次的打击,不过他依然斗志昂扬并试图东山再起。

Objrmap虽然完美,不过晴朗的天空中飘着一朵乌云。大神Rik van Riel敏锐的看到了逆向映射的那朵“乌云“,提出了自己的解决方案。本章主要描述新的anon_vma机制,代码来自4.4.6内核。

1、旧anon_vma机制有什么问题?

我们先一起来看看旧anon_vma机制下,系统是如何运作的。VMA_P是父进程的一个匿名映射的VMA,A和C都已经分配了page frame,而其他的page都还都没有分配物理页面。在fork之后,子进程copy了VMA_P,当然由于采用了COW技术,这时候父子进程的匿名页面会共享,同时在父子进程地址空间对应的pte entry中标注write protect的标记,如下图所示:

按理说不同进程的匿名页面(例如stack、heap)是私有的,不会共享,但是为了节省内存,在父进程fork子进程之后,父子进程对该页面执行写操作之前,父子进程的匿名页是共享的,所以这些page frame指向同一个anon_vma。当然,共享只是短暂的,一旦有write操作就会产生异常,并在异常处理中分配page frame,解除父子进程匿名页面的共享,具体如下图的page A所示:

这时候由于写操作,父子进程原本共享的page frame已经不再共享,然而,这两个page却仍然指向同一个anon_vma,不仅如此,对于B这样的页面,一开始就没有在父子进程之间共享,当首次访问的时候(无论是父进程还是子进程),通过do_anonymous_page函数分配的page frame也是同样的指向一个anon_vma。也就是说,父子进程的VMA共享一个anon_vma。

在这种情况下,我们看看unmap page frame1会发生什么。毫无疑问,page frame1对应的struct page的mapping成员指向了上图中的anon_vma,遍历anon_vma会命VMA_P和VMA_C,这里面,VMA_C是无效的VMA,本来就不应该匹配到。如果anon_vma的链表没有那么长,那么整体性能也OK。然而,在有些网路服务器中,系统非常依赖fork,某个服务程序可能会fork巨大数量的子进程来处理服务请求,在这种情况下,系统性能严重下降。Rik van Riel给出了一个具体的示例:系统中有1000进程,都是通过fork生成的,每个进程的VMA有 1000个匿名页。根据目前的软件架构,anon_vma链表中会有1000个vma 的节点,而系统中有一百万个匿名页面属于同一个anon_vma。

这样的系统会导致什么样的问题呢?我们一起来看看try_to_unmap_anon函数,其代码框架如下:

static int try_to_unmap_anon(struct page *page)

{……

anon_vma = page_lock_anon_vma(page);

list_for_each_entry(vma, &anon_vma->head, anon_vma_node) {

ret = try_to_unmap_one(page, vma);

}

spin_unlock(&anon_vma->lock);

return ret;

}

当系统中的一个CPU在执行try_to_unmap_anon函数的时候,需要遍历VMA链表,这时会持有anon_vma->lock这个自旋锁。由于anon_vma存有了很多根本无关的VMA,通过,page table的检索过程,你就会发现这个VMA根本和准备unmap的page无关,因此只能scan下一个VMA,整个过程需要消耗大量的时间,延长了临界区(复杂度是O(N))。与此同时,其他CPU在试获取这把锁的时候,基本会被卡住,这时候整个系统的性能可想而知了。更加糟糕的是内核中并非只有unmap匿名页面的时候会上锁、遍历VMA链表,还有一些其他的场景也会这样(例如page_referenced函数)。想象一下,一百万个页面共享这一个anon_vma,对anon_vma->lock自旋锁的竞争那是相当的激烈啊。

2、改进的方案

旧的方案的症结所在是anon_vma承载了太多进程的VMA了,如果能将其变成per-process的,那么问题就解决了。Rik van Riel的解决办法是为每一个进程创建一个anon_vma结构并通过各种数据结构把父子进程的anon_vma(后面简称AV)以及VMA链接在一起。为了链接anon_vma,内核引入了一个新的结构,称为anon_vma_chain(后面简称AVC):

struct anon_vma_chain {

struct vm_area_struct *vma;――指向该AVC对应的VMA

struct anon_vma *anon_vma;――指向该AVC对应的AV

struct list_head same_vma; ――链接入VMA链表的节点

struct rb_node rb;―――链接入AV红黑树的节点

unsigned long rb_subtree_last;

};

AVC是一个神奇的结构,每个AVC都有其对应的VMA和AV。所有指向相同VMA的AVC会被链接到一个链表中,链表头就是VMA的anon_vma_chain成员。而一个AV会管理若干的VMA,所有相关的VMA(其子进程或者孙进程)都挂入红黑树,根节点就是AV的rb_root成员。

这样的描述非常枯燥,估计第一次接触逆向映射的同学是不会明白的,不如我们一起来看看AV、AVC和VMA的“大厦”是如何搭建起来的。

3、当VMA和VA首次相遇

由于采用了COW技术,子进程和父进程的匿名页面往往是共享的,直到其中之一发起写操作。但是如果子进程执行了exec的系统调用,加载了自己的二进制image,这时候,子进程和父进程的执行环境(包括匿名页面)就分道扬镳了(参考flush_old_exec函数),我们的场景就是从这么一个全新的exec后的进程开始。当该进程的匿名映射VMA通过page fault分配第一个page frame的时候,内核会构建下图所示的数据关系:

上图中的AV0就是该进程的anon_vma,由于它是一个顶级结构,因此它的root和parent都是指向了自己。AV这个数据结构当然为了管理VMA了,不过新机制中,这是通过AVC进行中转的。上图中的AVC0搭建了该进程VMA和AV之间的桥梁,分别有指针指向了VMA0和AV0,此外,AVC0插入到AV的红黑树,同时也会插入到VMA的链表中。

对于这个新分配的page frame而言,它会mapping到VMA对应的某个虚拟地址上去,为了维护逆向映射的关系,struct page中的mapping指向了AV0,index成员指向了该page在整个VMA0中的偏移。图中没有画出这个关系,主要因为这是老生常谈了,相信大家都已经熟悉。

VMA0中随后可能会有若干的page frame被mapping到该VMA的某个虚拟页面,不过上面的结构不会变化,只不过每一个page中的mapping都指向了上图中的AV0。另外,上图中那个虚线绿色block的AVC0其实等于那个绿色实线的AVC0 block,也就是说这时候该VMA只有一个anon_vma_chain,即AVC0,上图只是方便表示该AVC也会被挂入VMA的链表,挂入anon_vma的红黑树而已。

如果想参考相关的代码可以仔细看看do_anonymous_page或者do_cow_fault。

4、在fork的时候,匿名映射的VMA经历了什么?

一旦fork,那么子进程会copy父进程的VMA(参考函数dup_mmap),子进程会有自己的VMA,同时也会分配自己的AV(旧的机制下,多个进程共享一个AV,而新的机制中,AV是per process的),然后建立父子进程之间的VMA、VA的“大厦”,主要的步骤如下:

(1) 调用anon_vma_clone函数,建立子进程VMA和“父进程们”VA的关系

(2) 建立子进程VMA和子进程VA的关系

怎样叫做建立VMA和VA的关系?其实就是anon_vma_chain_link函数的调用过程,步骤如下:

(1) 分配一个AVC结构,成员指针指向对应的VMA和VA

(2) 将该AVC加入VMA链表

(3) 将该AVC加入VA红黑树

我们一开始先别把事情搞得太复杂,先看看一个全新进程fork子进程的场景。这时候,内核会构建下图所示的数据关系:

首先看看如何建立子进程VMA1和父进程AV0的关系,这里需要遍历VMA0的anon_vma_chain链表,当然现在这个链表只有一个AVC0(link到AV0),为了建立和父进程的联系,我们分配了AVC_x01,它是一个桥梁,连接了父子进程。(注:AVC_x01中的x表示连接,01表示连接level 0和level 1)。通过这个桥梁,父进程可以找到子进程的VMA(因为AVC_x01插入AV0的红黑树中),而子进程也可以找到父进程的AV(因为AVC_x01插入VMA1的链表中)。

当然,自己的anon_vma也需要创建。在上图中,AV1就是子进程的anon_vma,同时分配一个AVC1来连接该子进程的VMA1和AV1,并调用anon_vma_chain_link函数将AVC1插入VMA1的链表和AV1的红黑树中。

父进程也会创建其他新的子进程,新创建的子进程的层次和VMA1、VA1的类似,这里就不描述了。不过需要注意的是:父进程每创建一个子进程,AV0的红黑树中会增加每一个起“桥梁”作用的AVC,以此连接到子进程的VMA。

5、构建三层大厦

上一节描述了父进程创建子进程的情况,如果子进程再次fork,那么整个VMA-VA的大厦将形成三层结构,具体如下图所示:

当然,首先要进行的仍然是建立孙进程VMA和“父进程们”VA的关系,这里的“父进程们”其实是泛指孙进程的上层的那些进程们。对于这个场景,“父进程们”指的就是上图中的A进程和B进程。如何建立?在fork的时候,我们进行VMA的拷贝:即分配VMA2并以VMA1为原型copy到VMA2中。Copy是沿着VMA1的AVC链表进行的,该链表有两个元素:AVC1和 AVC_x01,分别和父进程A和子进程B的AV关联。因此,在孙进程C中,我们会分配AVC_x02和AVC_x12两个AVC,并建立level 2层和level 0层以及level 1层之间的关系。

同样的,自己level的anon_vma也需要创建。在上图中,AV2就是孙进程C的anon_vma,同时分配一个AVC2来连接该孙进程的VMA2和AV2,并调用anon_vma_chain_link函数将AVC2插入VMA2的链表和AV2的红黑树中。

AV2中的root指向root AV,也就是进程A的AV。Parent成员指向其B进程(C的父进程)的AV。通过Parent这样的指针,不同level的AV建立了父子关系,而通过root指针,每一个level的AV都可以寻找找到root AV。

6、page frame是如何加入“大厦”中?

前面几个小节重点讨论了hierarchy AV的结构是如何搭建起来的,也就是描述fork的过程中,父子进程的VMA、AVC和AV是如何联系的。本小节我们将一起来看看父子进程之一访问页面,发生了page fault的处理过程。这个处理过程有两个场景,一个是父子进程都没有page frame,这时候,内核代码会调用do_anonymous_page分配page frame并调用page_add_new_anon_rmap函数建立该page和对应VMA的关系。第二个场景复杂一点,是父子共享匿名页面的场景,当发生write fault的时候,也是分配page frame并调用page_add_new_anon_rmap函数建立该page和对应VMA的关系,具体代码位于do_wp_page函数。无论哪一个场景,最终都是将该page的mapping成员指向了该进程所属的AV结构。

7、为何建立如此复杂的“大厦”?

如果你能坚持读到这里,那么说明你对枯燥文字的忍受能力还是很强的,哈哈。Page、VMA、VAC、VA组成了如此复杂的层次结构到底是为什么呢?是为了打击你学习内核的兴趣吗?非也,让我们还是用一个实际的场景来说明这个“大厦”的功能。

我们通过下面的步骤建立起上图的结构:

(1) P进程的某个VMA中有两类页面: 一类是有真实的物理页面的,另外一类是还没有配备物理页面的。上图中,我们分别跟踪有物理页面的A以及还没有分配物理页面的B。

(2) P进程fork了P1和P2

(3) P1进程fork了P12进程

(4) P1进程访问了A页面,分配了page frame2

(5) P12进程访问了B页面,分配了page frame3

(6) P2进程访问了B页面,分配了page frame1

(7) P2进程fork了P21进程

经过上面的这一些动作之后,我们来看看page frame共享的情况:对于P进程的page frame(是指该page 的mapping成员指向P进程的AV,即上图中的AV_P)而言,他可能会被任何一个level的的子进程VMA中的page所有共享,因此AV_P需要包括其子进程、孙进程……的所有的VMA。而对于P1进程而言,AV_P1则需要包括P1子进程、孙进程……的所有的VMA,有一点可以确认:至少父进程P和兄弟进程P2的VMA不需要包括在其中。

现在我们回头看看AV结构的大厦,实际上是符合上面的需求的。

8、页面回收的时候,如何unmap一个page frame的所有的映射?

搭建了那么复杂的数据结构大厦就是为了应用,我们一起看看页面回收的场景。这个场景需要通过page frame找到所有映射到该物理页面的VMAs。有了前面的铺垫,这并不复杂,通过struct page中的mapping成员可以找到该page对应的AV,在该AV的红黑树中,包含了所有的可能共享匿名页面的VMAs。遍历该红黑树,对每一个VMA调用try_to_unmap_one函数就可以解除该物理页帧的所有映射。

OK,我们再次回到这一章的开始,看看那个长临界区导致的性能问题。假设我们的服务器上有一个服务进程A,它fork了999个子进程来为世界各地的网友服务,进程A有一个VMA,有1000个page。下面我们就一起来对比新旧机制的处理过程。

首先,百万page共享一个anon_vma的情况在新机制中已经解决,每一个进程都有自己特有的anon_vma对象,每一个进程的page都指向自己特有的anon_vma对象。在旧的机制中,每次unmap一个page都需要扫描1000个VMA,而在新的机制中,只有顶层的父进程A的AV中有1000个VMA,其他的子进程的VMA的数目都只有1个,这大大降低了临界区的长度。

七、后记

本文带领大家一起简略的了解了逆向映射的发展过程。当然,时间的车轮永不停息,逆向映射机制还在不断的修正,如果你愿意,也可以了解其演进过程的基础上,提出自己的优化方案,在其历史上留下自己的印记。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux内存逆向映射(reverse mapping)技术的前世今生 的相关文章

  • 有谁知道在哪里定义硬件、版本和序列号。 /proc/cpuinfo 的字段?

    我想确保我的 proc cpuinfo 是准确的 目前它输出 Hardware am335xevm Revision 0000 Serial 0000000000000000 我可以在代码中的哪里更改它以给出实际值 这取决于 Linux 的
  • Linux 中什么处理 ping?

    我想覆盖 更改 linux 处理 ping icmp echo 请求数据包的方式 这意味着我想运行自己的服务器来回复传入的 icmp 回显请求或其他 数据包 但为了使其正常工作 我想我需要禁用 Linux 的默认 ping icmp 数据包
  • C语言中如何通过内存地址映射函数名和行号?

    如何用 GCC 中的内存地址映射回函数名称和行号 即假设一个 C 语言原型 void func Get the address of caller maybe this could be avoided MemoryAddress get
  • SSE:跨页边界的未对齐加载和存储

    我在页面边界旁边执行未对齐加载或存储之前读过某处 例如使用 mm loadu si128 mm storeu si128内在函数 代码应首先检查整个向量 在本例中为 16 个字节 是否属于同一页 如果不属于同一页 则切换到非向量指令 我知道
  • 如何确保应用程序在 Linux 上持续运行

    我试图确保脚本在开发服务器上保持运行 它会整理统计数据并提供网络服务 因此它应该会持续存在 但一天中有几次 它会因未知原因而消失 当我们注意到时 我们只需再次启动它 但这很麻烦 并且某些用户没有权限 或专有技术 来启动它 作为一名程序员 我
  • Linux 上有关 getBounds() 和 setBounds() 的 bug_id=4806603 的解决方法?

    在 Linux 平台上 Frame getBounds 和 Frame setBounds 的工作方式不一致 这在 2003 年就已经有报道了 请参见此处 http bugs java com bugdatabase view bug do
  • 删除 Git 存储库,但保留所有文件

    在我使用 Linux 的过程中的某个时刻 我决定将我的主目录中的所有内容都放入源代码管理中是个好主意 我不是在问这是否是一个好主意 我是在问如何撤销它 删除存储库的原因是我最近安装了 Oh My Zsh 而且我非常喜欢它 问题是我的主目录有
  • php exec 返回的结果比直接进入命令行要少

    我有一个 exec 命令 它的行为与通过 Penguinet 给 linux 的相同命令不同 res exec cd mnt mydirectory zcat log file gz echo res 当将命令直接放入命令行时 我在日志文件
  • 使用 Grep 查找两个短语之间的文本块(包括短语)

    是否可以使用 grep 来高亮所有以以下内容开头的文本 mutablePath CGPathCreateMutable 并以以下内容结尾 CGPathAddPath skinMutablePath NULL mutablePath 这两个短
  • 确定我可以向文件句柄写入多少内容;将数据从一个 FH 复制到另一个 FH

    如何确定是否可以将给定数量的字节写入文件句柄 实际上是套接字 或者 如何 取消读取 我从其他文件句柄读取的数据 我想要类似的东西 n how much can I write w handle n read r handle buf n a
  • 快速像素绘图库

    我的应用程序以每像素的方式生成 动画 因此我需要有效地绘制它们 我尝试过不同的策略 库 但结果并不令人满意 尤其是在更高分辨率的情况下 这是我尝试过的 SDL 好的 但是慢 OpenGL 像素操作效率低下 xlib 更好 但仍然太慢 svg
  • 如何在c linux中收听特定接口上的广播?

    我目前可以通过执行以下操作来收听我编写的简单广播服务器 仅广播 hello int fd socket PF INET SOCK DGRAM 0 struct sockaddr in addr memset addr 0 sizeof ad
  • Fortran gfortran linux 中的“分段错误(核心转储)”错误

    我正在创建一个程序 该程序将分析目录中的文件 fits 然后它将在另一个目录中创建另一个文件 txt 它只是一个转换器 当我尝试执行该程序 编译正常 时 它给了我一条错误消息 程序收到信号 SIGSEGV 分段错误 无效的内存引用 此错误的
  • 监控子进程的内存使用情况

    我有一个 Linux 守护进程 它分叉几个子进程并监视它们是否崩溃 根据需要重新启动 如果父进程可以监视子进程的内存使用情况 以检测内存泄漏并在超出一定大小时重新启动子进程 那就太好了 我怎样才能做到这一点 您应该能够从 proc PID
  • 为什么C Clock()返回0

    我有这样的事情 clock t start end start clock something else end clock printf nClock cycles are d d n start end 我总是得到输出 时钟周期是 0
  • Awk - 计算两个文件之间的每个唯一值和匹配值

    我有两个文件 首先 我尝试获取第 4 列中每个唯一字段的计数 然后匹配第二个文件的第二列中的唯一字段值 File1 第 4 列的每个唯一值和 File2 第 2 列包含我需要在两个文件之间匹配的值 所以本质上 我试图 gt 如果 file2
  • 限制 Imagemagick 使用的空间和内存

    我在 Rails 应用程序上使用 Imagemagick 使用 rmagick 但我的服务器 Ubuntu 不是很大 当我启动转换进程时 Imagemagick 占据了我的服务器 30GB HDD 的所有位置 内存 我想限制内存和 tmp
  • vagrant ssh -c 并在连接关闭后保持后台进程运行

    我正在编写一个脚本来启动和后台流浪机器内的进程 似乎每次脚本结束和 ssh 会话结束时 后台进程也会结束 这是我正在运行的命令 vagrant ssh c cd vagrant src nohup python hello py gt he
  • Linux 上的 RTLD_LOCAL 和dynamic_cast

    我们有一个由应用程序中的一些共享库构成的插件 我们需要在应用程序运行时更新它 出于性能原因 我们在卸载旧插件之前加载并开始使用新插件 并且只有当所有线程都使用旧插件完成后 我们才卸载它 由于新插件和旧插件的库具有相同的符号 我们dlopen
  • 让 TeXstudio 在 linux mint 中工作:找不到文件“url.sty”。

    刚刚切换到 Linux Mint 以前的顽固 Windows 用户 我在尝试安装 TeXstudio 时遇到一些问题 Sudo apt get install texstudio 给了我一个正确的安装 至少 我是这么认为的 但是当我尝试构建

随机推荐

  • MySQL基础(非常全)

    MySQL基础 一 MySQL概述 1 什么是数据库 答 数据的仓库 如 在ATM的示例中我们创建了一个 db 目录 称其为数据库 2 什么是 MySQL Oracle SQLite Access MS SQL Server等 答 他们均是
  • 【统一身份认证】详细讲解

    一 什么是统一身份认证 二 统一身份认证的构成 1 角色模型 用户 角色 权限 2 实现 前言
  • win10使用vscode+anaconda+Python环境配置(解决无法加载文件 \WindowsPowerShell\profile.ps1,因为在此系统上禁止运行脚本)

    win10使用vscode anaconda Python环境配置 解决无法加载文件 WindowsPowerShell profile ps1 因为在此系统上禁止运行脚本 安装anaconda 为conda设置环境变量 conda 环境初
  • Linux文件权限

    Linux用户分为 拥有者 组群 Group 其他 other Linux系統中 预设的情況下 系統中所有的帐号与一般身份使用者 以及root的相关信息 都是记录在 etc passwd文件中 每个人的密码则是记录在 etc shadow文
  • C++11 标准新特性: 右值引用与转移语义

    新特性的目的 右值引用 Rvalue Referene 是 C 新标准 C 11 11 代表 2011 年 中引入的新特性 它实现了转移语义 Move Sementics 和精确传递 Perfect Forwarding 它的主要目的有两个
  • Linux服务器中查看进程的四种方法

    查看进程的四种方法 1 使用ps aux命令来查看 能以简单列表的形式显示出进程信息 ps aux用于报告当前系统的进程状态 可以搭配kill指令随时中断 删除不必要的程序 ps命令是最基本同时也是非常强大的进程查看命令 使用该命令可以确定
  • PTA乙级题目练习-卡拉兹(Callatz)猜想

    卡拉兹 Callatz 猜想 对任何一个正整数 n 如果它是偶数 那么把它砍掉一半 如果它是奇数 那么把 3n 1 砍掉一半 这样一直反复砍下去 最后一定在某一步得到 n 1 卡拉兹在 1950 年的世界数学家大会上公布了这个猜想 传说当时
  • 静态编译 java_动态编译和静态编译及Java执行

    静态编译就是在编译的时候把你所有的模块都编译进exe里去 当你启动这个exe的时候所有模块都加载进来了 你写小程序没问题 但程序一大 加载的过程 就是当你运行程序时初始化的过程 就比较费力了 大多数ppc的硬件配置还是很一般的 动态编译就不
  • 新书《活用UML-需求分析高手》详细大纲(持续更新中)

    本书目前正在编写中 大纲可能会随时调整 欢迎各位朋友提出宝贵意见 欢迎到umlonline网站学习 活用UML 需求分析高手 课程在线版本 http www umlonline org school forum 26 1 html 目 录第
  • vs添加对dll的引用

    我们在使用vs进行开发调试的时候经常会遇到一个问题 就是当我们的主工程引用到其他工程更新的dll 我们经常采用copy到工程目录的方法 亦或者当我们的多个工程引用到同一个dll文件的时候 我们怎么来配置 1 将dll配置到环境变量 这种方法
  • 以渲染和ue独立游戏为接下来的主要学习任务。(独立游戏就是为单干做准备,不为跳槽涨薪,目标就是单干,找另外一种可能。)

    这段时间工作不好找 即使招聘网站上找我聊的 薪水也没有什么吸引力 所以不考虑跳槽 直接把精力放在渲染上 说不准还能涨涨薪 拿个年终奖 这条被哥们否定了 内部涨薪很困难 即使以前确实是从1万七涨到2万 也可能是因为人力看过流水 知道我是从两万
  • 微信小程序游戏怎么开发入门教程

    微信小程序游戏开发是现在比较热门的小程序类型开发项目 对于开发人员而言 怎么开发微信小程序游戏呢 今天小编分享一篇小游戏的入门开发教程 希望对微信小程序制作开发人员提供参考 第一步 注册一个小程序账号 在官方注册一个微信小程序账号 注册申请
  • springboot+thymeleaf+mybatis简单获取数据库数据

    1 数据库准备 建好需要的表 这里我的表是info list 2 文件创建 实体类Info public class Info private int id private String name public int getId retu
  • Android平台RTMP

    我们需要怎样的直播播放器 很多开发者在跟我聊天的时候 经常问我 为什么一个RTMP或RTSP播放器 你们需要设计那么多的接口 真的有必要吗 带着这样的疑惑 我们今天聊聊Android平台RTMP RTSP播放器常规功能 如软硬解码设置 实时
  • 5V转±12V无变压器双boost电路

    最近有个新项目 需要 10V范围的模拟量输出 非隔离 对于5V以下供电的控制板而言单端输出绝对没问题 可现在需要有正负输出 是少不了正负电源的 因此准备设计一个5V转 12V的电源 然后选择一个双向供电的运放 来实现单端模拟量信号向双向模拟
  • Blob类型介绍以及查看

    最近看到一个字段的类型是Blob有点懵逼 从Navigate上看只能看到一个 Blob 实际你去仔细看的话 分线字段大小不为空 而且每个大小不一样 说明该字段不仅不为空而且还有值且不一样 那这个Blob是什么呢 大名叫做binary lar
  • 程序员如何乘风破浪?从数据库历史看技术人发展

    2009 年我国数据库软件市场规模为 35 03 亿元 2017 年我国数据库软件市场规模增长至 120 00 亿元 8年时间内 我国数据库软件市场始终保持平稳增长 年均复合增长率为 17 5 且增速呈现递增趋势 根据中研产业研究院估计 到
  • 深入浅出Spring AOP面向切面编程实现原理方法

    1 什么是AOP AOP Aspect Oriented Programming 意为 面向切面编程 通过预编译方式和运行期动态代理实现在不修改源代码的情况下 给程序动态统一添加功能的一种技术 可以理解成动态代理 是Spring框架中的一个
  • Web前端学习(六)HTML5列表标签

    列表标签 ul li 语法 ul li 精彩少年 li li 美丽突然出现 li li 触动心灵的旋律 li ul 有序列表 ol li 语法 ol li 前端开发面试心法 li li 零基础学习html li li JavaScript全
  • Linux内存逆向映射(reverse mapping)技术的前世今生

    本文来自于微信公众号Linux阅码场 一 前言 数学大师陈省身有一句话是这样说的 了解历史的变化是了解这门学科的一个步骤 今天 我把这句话应用到一个具体的Linux模块 了解逆向映射的最好的方法是了解它的历史 本文介绍了Linux内核中的逆