不讲技术细节,也不讲代码,单纯讲理解
二次机会/二次看见
背景
page有2个标志位,要分清:
- PG_referenced:软件标志位,基本上扫描lru时才设置
- pte young(accessed),硬件标志位,在pte上,mmu访问了会自动在进程的pte上设置为1.
初始状态
匿名页
pagefault时,page放入inactive队头,此时 PG_referenced 为 0, accessed 为 1(pte young=1)
文件页
mmap
pagefault时(filemap_fault),放入inactive队头,PG_referenced 为 0, accessed 为 1(pte young=1)。和 匿名页一样。
read/write
page 只在pagecache ,没有pte。 PG_referenced 为 1 (预读的但是还没读到的为0)。
二次机会/二次看见 算法 轮转 细节
扫描 active
从尾部开始扫描。
扫描到的page,清除 accessed (rmap找到所有引用的进程的pte设置young=0) ,理论上,active的 accessed 必然为 1,然后挪动页page到 inactive 队头。
特例:如果是 VM_EXEC + 通过rmap看存在pte为young的 ,则 保留到active,并挪到active头,优先保护。注意这种情况 active的 accessed 可能为0,和上面描述有冲突,这是特例。
扫描 inactive
从尾部开始扫描。
注意刚从 active转移到 inactive的那一刻,accessed必然为0(当然如果硬件访问了则会设置为1),并且 PG_referenced 为0.
-
如果发现,accessed为0,即 (rmap找到所有引用的进程的pte都是0),且 PG_referenced 为 0,则淘汰。这意味着,从 active 挪动到inactive后,一直从队尾扫描到这个页的过程中,这个页面一直没有被访问。
-
如果发现,accessed非0,我们要抉择是保留在inactive还是挪到active
- 2.1 如果 PG_referenced 为0,并且 accessed 1(rmap结果只有一个进程的pte young为1),则保留在inactive,挪到inactive对头,同时设置PG_referenced为1,以及清楚accessed标志位。这说明,从 active 挪动到inactive后,第一次扫描到,并且有一个进程访问了这个页。我们认为热度中等。
- 2.2 如果 PG_referenced 为0,但是 accessed > 1,即 (rmap结果不止一个进程的pte young为1),同上,挪动到active,同时清楚accessed,设置 PG_referenced为1。可以理解为,从 active 挪动到inactive后,好几个进程访问了这个页,热度高。
- 2.3 如果 PG_referenced 为1,则挪到 active 队头,同时 会清除accessed,保留 PG_referenced为1。PG_reference 为1说明我们经过了 2.1,经过2.1我们清除了硬件accessed标志位,此次再扫描,发现accessed还是有值,说明热度高。
- 2.4 如果 PG_referenced 为1,但是 accessed 为 0,说明,这个page 是经过 2.1 过来的,即有人访问过一次,但是这次再次扫描到,硬件标志位还是为0,说明没人访问过,则淘汰。
最后
- 上述 2.1 有个例外,如果是 VM_EXEC,则直接挪到 ACTIVATE。代码页享受特权。
- 上面的"淘汰",对于文件页,脏页要先回写,干净的则直接回收,对于匿名页就是写到swap。
refault 算法
上面说了 内存的LRU算法,其实仔细想和LRU没有半毛钱关系,必然存在错误淘汰,错误淘汰,然后又pagefault,然后又错误淘汰,这种来回操作磁盘,会导致磁盘IO打爆,这叫 io thrashing。
内核refault算法核心目的是尽量避免io thrashing。怎么做呢,就是让pagefault进来的页,直接进入active链表。
在 active 链表的page,相比 inactive链表的page,至少可以有多一次机会,上面一节提到过。
在没有 refault算法时,一个页pagefault是直接进入 inactive 链表的,这是核心差别。
那 refault 算法怎么判别 一个被淘汰的页面,在pagefault的时候,需要进入active链表呢。
首先,当 page被swapout时,会有一个记录元信息的shadow xarry ,理解为一颗radix tree就行。它 以 page addr为key,value只是8字节的meta信息。虽然占用内存,但是毕竟不是page对应的数据,可以接受,而且本身其实也可能会被淘汰,这另说。
这个元信息是这么组成的
eviction 时间戳 \| memcg id \| node id \| workingset bit
核心是 "eviction 时间戳" ,内核 使用 lruvec->nonresident_age 维护,
nonresident_age只会在下述情况下自增
- 一个page被淘汰,nonresident_age 就会无脑++
这里需要考虑一个模型,就是内存满了(说的有点极端,但是好理解),就是内存满的时候,一个新申请的页,必然会导致一个老的页淘汰。
也就是 淘汰次数 ≈ 新页 fault 进来的次数 ≈ "发生了多少次首次访问" - 一个page被从inactive挪到active 链表,会无脑++
这表示一个page,第二次访问的统计计数,因为一个页在 inactive 中被 晋升到 active,肯定是page有 accessed. - 当一个 page 在pagefault时,经过 refault 算法判别,需要进入 active,++。正常页面进入inactive 不会++。
和2类似,只是错误淘汰,但是最终判别需要进入 active 。
总的来说,上述 nonresident_age 只表达了一个统计计数: 一个页,离开 inactive 链的事件数,可能是淘汰,可能是晋升到active。
理解了这个点,我们再来看 refualt 怎么用这个值的
一个pagefault时候,会获取到 老的 nonresident_age (从shadow中获取),然后计算 E = abs(nonresident_age - old_nonresident_age)
想要知道这个值表示什么,则必须回到我们refault的目的,核心就是不让page错误淘汰,page若不被淘汰,那就是让 inactive链表长点(NR_inactive表示),毕竟我们是从 inactive 队尾进行回收的。
那么E表示的是,我这个page不在内存的这个时间里,多少 数据从 inactive 轮转到了 active,即包含了这个page不在内存的时间段内,还有多少数据被"淘汰",如果这个page不想发生淘汰,这意味着 inactive 还需要增长 E 个容量。
注意有一点,上面说的"淘汰"其实不精确,就如 nonresident_age 中第二点所说,inactive挪到active的页,说明是热页,即热页其实变多了,此时我们肯定是希望很多的 inactive 长度,所以inactive->active这个值也是被加进去的。
如果这个E ≤ NR_active ,NR_active 缩 E个,即inactive增加E个,可行。
如果这个E > NR_active ,怎么缩 都没用。
另一种思考:
NR_inactive + E ≤ NR_inactive + NR_active 这个page 才有的救,即我们让这个page,经过active头到尾,再经过inactive头到尾,这样就避免了io thrashing。
结尾
refualt算法,包括二次看见算法在内,这完全是启发式编程,你不能说他准,也不能说它有理论支撑,但是实际上,他就是好使的,并且经过数以亿计的生成环境验证的结果。他是经过十数年累计的增量修改,才能达到的当前的稳态。