内核内存回收原理简介

2023-11-04

页框回收与交换

概念

内核在为进程服务的过程中会分配大量的页,但是这些页对应的虚拟地址在进程的生命周期里一直会被断断续续的访问,所以当内核同时为大量进程服务时,内存终究会耗尽。所有页框回收就是在内核未耗尽内存之前(因为回收与交换也会使用内存),将在使用过程标记未访问不频繁的部分内存换出到磁盘,释放所占用的内存补给系统,以维持内核的正常运转,待被换出的页对应的虚拟地址再次被访问时,内核又通过缺页系统将换出的页再次分配新的内存进行换入,这样周而复始形成良性的循环。

组成

每个zone中都有一套lrubuddy system

  1. 页:可回收的页
  2. 最近使用链表lru:页描述符的标记和放置使用页的活动与不活动链表,使用过程中通过对页的访问,判断页是否是活动的,从而在活动与非活动链表中来回移动。
  3. 算法和守护线程:内存分配子系统中的页的直接回收路径与守护线程,他们负责计算应该将什么也标记为可回收状态、回收的数量是多少,然后触发或执行回收过程(扫描非活动链表开始回收)。
  4. 交换分区:交换分区页高速缓存与交换分区磁盘(或文件),他们负责暂存页引用和存储页的数据。
  5. 文件和设备:文件页高速缓存与文件(或设备),他们负责暂存页引用和回写脏页数据。

参与页框回收的页类型

type comments reclaimed operation
不可回收 1. 活动系统中的空闲页
2. 保留页(PG_reserved置位,比如内核镜像页)
3. 内核使用的动态分配的页
4. 进程内核态堆栈页
5. 临时锁定的页(PG_locked置位)
不允许回收或无需回收
可交换页 1. 用户态进程匿名页
2. tmpfs文件系统的映射页(比如IPC共享内存页)
将页交换到交换分区
可同步页 1. 用户态进程映射页
2. 存有磁盘文件数据页且在文件页高速缓存中
3. 块设备缓冲区页
4. 磁盘高速缓存页(索引节点高速缓存)
必要时,与磁盘同步这些页
可丢弃页 1. 内存高速缓存中未使用的页(比如slab分配器的未使用对象缓存)
2. 目录项高速缓存的未使用页
释放这些页,压缩缓存

页的转换图

  1. 每个zone中都有一套lrubuddy system
  2. 每个CPU有一套lru-cache
  1. 页的转换
                          +--------------------+
        +---->>>-----+--->| inactive lru cache |--->-+
        ^            ^    +--------------------+     |
        |            |                               |
        |            |                               v
        |      +-----+------+               +--------+-----+
        |      | active lru |               | inactive lru |
        |      +-----+------+               +--------------+
        |            ^                               |      
        |            |                               |      
        +---->>>-----+--<<<--+                       |      
        |                    |                       |      
    +---+-------+      +-----+------------+          v      
    | lru cache |      | active lru cache |          v      
    +---+-------+      +-----+------------+          v      
        ^                    |                       |      
        |                    |                       |      
        |                    |         re-active     |      
   PAGE |                    +-----<<<<<-------------+       
        |                                            |      
  +-----+-----+                     start reclaimed  |        +-----------------+
  | buddy sys |                 +--------<<<<<<------+------->| unevictable lru |
  +-----+-----+                 |                             +-----------------+
        ^            FILE sync  v    ANON swap
        |            +----<<----+----->>---+
        |            |                     |
        |            v                     v
        |      +-----+-----+         +-----+-----+
        |      | page cache|         | swap cache|
        |      +-----+-----+         +-----+-----+
        |            |                     |
        |            |      reused page    |
        +----<<<-----+---------<<<---------+
                     v                     v
               +-----+-----+         +-----+-----+
               | file      |         | swap      |
               +-----+-----+         +-----+-----+
                     |                     |
                     +---->>----+----<<----+
                                |
                                v
                           +----+----+
                           |disk  dev|
                           +---------+
                           
  1. lru链表分类
type comments
LRU_INACTIVE_ANON 非活动的匿名映射页的lru
LRU_INACTIVE_FILE 非活动的文件映射页的lru
LRU_ACTIVE_ANON 活动的匿名映射页的lru
LRU_ACTIVE_FILE 活动的文件映射页的lru
LRU_UNEVICTABLE 不可回收页的lru
  1. lruper-CPU缓存分类
type comments

页高速缓存

数据结构

页高速缓存核心数据结构是address_space对象,被嵌入在页所有者的索引节点对象中。每个页描述符中使用mapping(索引节点的address_space对象)和index(页大小的磁盘镜像偏移)字段关联到页高速缓存中。

  1. 基树

struct radix_tree_root address_space.page_tree是基树的根,由深度、节点概念构成。每个节点可以存储64个槽位,如果是叶子节点则存储内容为页描述符,否则为其他的子树节点的指针。假设有页号为0、4、131三页数据,那么在树中的映射为:

                             +-------+
                             | h = 2 |
                             +-------+
                             | rnode |
                             +---+---+
                                 | 
                                 |
                                 v
                       +------------------+   
                       |   count = 2      |   
                       +------------------+   
                       | 0 | | 2 |...| 63 |   
                       +-+-----+----------+
                         |     |
                 +-------+     +-------+
                 |                     |
                 v                     v
         +------------------+  +------------------+    
         |   count = 2      |  |   count = 1      |    
         +------------------+  +------------------+    
         | 0 | | 4 |...| 63 |  | 0 | | 3 |...| 63 |    
         +-+-----+----------+  +-------+----------+ 
           |     |                     |
           |     +-------+             |
           |             |             |
           v             v             v
        +-----+       +-----+       +-----+
        |page |       |page |       |page |
        +-----+       +-----+       +-----+
  1. 优先搜索树

  2. 反向匿名映射

  3. 操作方法接口
    页高速缓存中的页通过const struct address_space_operations *a_ops操作对外部子系统提供操作,主要方法如下:

    • int (*writepage)(struct page *page, struct writeback_control *wbc) 写操作
    • int (*readpage)(struct file *, struct page *) 读操作
    • int (*write_begin)(struct file *, struct address_space *mapping, loff_t pos, unsigned len, unsigned flags, struct page **pagep, void **fsdata) 为写操作做准备
    • int (*write_end)(struct file *, struct address_space *mapping, loff_t pos, unsigned len, unsigned copied, struct page *page, void *fsdata) 完成写操作

内部操作API

API page comments
find_get_page() get_page() 根据偏移查找一页,然后增加页的引用
find_get_pages() get_page() 根据起始偏移和请求数量,查找一组连续的页,然后对每一页都增加引用
find_lock_page() 1. get_page()
2. lock_page()
根据偏移查找一页,然后增加页的引用,并试图锁定改页(可能发生阻塞)
add_to_page_cache_lru() 1. get_page()
2. __SetPageLocked()
3. page.mapping = mapping
4. page.index = offset
5. lru_cache_add()
1. 从外部分配新页。
2. 锁定页,因为是新页内容无效,防止其他的路径进行访问
3. 插入到页高速缓存中,并初始化页的高速缓存相关字段
4. 新缓存的可回收的页,插入lru链表中
add_to_page_cache() / add_to_page_cache_lru() 类似,除了不加入lru链表中
delete_from_page_cache() 1. page.mapping = NULL
2. put_page()
1. 删除的页必须已锁定
2. 如果页还被页表映射,则要作出调整
3. 从页高速缓存中删除
4. 重置相关字段
read_cache_page() 1. get_page()
2. alloc_page()
3. __SetPageLocked()
4. SetPageUptodate()
5. unlock_page()
6. lru_cache_add()
7. mark_page_accessed()
1. 从缓存中查找页
2. 如果没有找到,分配新页,载入数据,标记更新标志,添加到lru链表中
3. 如果找到,等待页的更新操作
4. 标记最近访问标志

页标记

如果我们想回写高速缓存中所有的脏页和正在被回写的页,那么依次遍历这个基树中所有的节点是非常耗时的,为了快速搜索脏页,基树中每个非叶子节点都包含一个标记,这样我们可以从顶层节点可以很清楚的知道哪些叶子节点包含脏页,然后跳过没有标记的子树节点,然后往下遍历。向页高速缓存插入页时,不会主动的标记,因为页的状态还是空的;而当从中删除一页时,会自动的清理路径上的各个标记。

  1. 数据结构

    • ulong radix_tree_node.tag[PAGECACHE_TAG_DIRTY][1] 标记脏页
    • ulong radix_tree_node.tag[PAGECACHE_TAG_WRITEBACK][1] 标记正在回写的页
    • ulong radix_tree_node.tag[PAGECACHE_TAG_TOWRITE][1] to_write ?
  2. 操作API

API comments
radix_tree_tag_set() 设置指定偏移和类型的标记
radix_tree_tag_clear() 清除指定偏移和类型的标记
radix_tree_tag_get() 获取指定偏移和类型的标记
set_page_writeback() 高级应用,当页标志不是回写状态,设置高速缓存中的页标记和页标志为回写,
且如果页标志不是脏页,则清除高速缓存的页标记的脏状态

反向匿名映射

概念

反向匿名映射用于通过页描述符快速定位所有映射该页的进程的页表,从解除页与页表的映射。已普通的私有mmap()来说,当在fork()调用dup_mmap()时,原来的vma结构被拷贝一份,且被链接到同一个动态分配的struct anon_vma,然后再增加关联页的引用,并设置到新进程的页表中;在内核无关的进程中试图回收这些共享的页时,就需要通过反向匿名映射找到所有的vma,从而找到所有的页表。

数据结构

匿名线性区是起止是4K对齐的,而页描述符的index存储的是页在vma的页数偏移(参考__page_set_anon_rmap()),通过vma_address()就可以算出页则对应的用户空间的虚拟地址,从而找到页表项,进行映射解除。

    
                                    rb_root
                                 +----------+
                                 | anon_vma |
                                 +-+---+--+-+
                                  /    ^   \
                  rb_node        /     |    \     rb_node
                 +--------------+-+    |  +--+-------------+  
             +---| anon_vma_chain |    |  | anon_vma_chain |----+
             |   +--------+-------+    |  +--------+-------+    |    
             |            |            |           |            |
             |            v            |           v            |
             |   +--------+-------+    |  +--------+-------+    |
             |   | vma_area_struct|    ^  | vma_area_struct|    |
             |   +--------+-------+    ^  +--------+-------+    |
             |            |            ^           |            |
             |            v            ^           v            |
             |   +--------+--+         |       +---+-------+    |
             |   | mm_struct |         |       | mm_struct |    |
             |   +--+--------+         |       +--------+--+    |
             |      |                  |                |       |
             |      v                  |                v       |
             |   +--+--+    +-----+    |  +-----+    +--+--+    |
             |   | pgd |--->| pte |    |  | pte |<---| pgd |    |
             |   +-----+    +-----+    |  +-----+    +-----+    |
             |                    |    |  |                     |
             |                    v    |  v                     |
             |                    +----+--+                     |
             |                    | page  |                     |
             |                    +-------+                     |
             |           index    |       |    index            |
             |              +-----+       +-----+               |
             |<------------>|                   |<------------->|
             v              v                   v               v
             +----------+---+--+--+       +--+--+---+-----------+
             |//| PAGE |//|       |\\| PAGE |\\\\\\\\\\\|
    vm_start +----------+------+--+       +--|------+-----------+ vm_start
             匿名线性区                     匿名线性区

操作API

API struct page comment
page_add_new_anon_rmap() 首次将页添加到反向匿名映射数据结构中
page_lock_anon_vma_read() 如果页被映射,则锁住页反向映射的struct anon_vma数据结构,然后返回这个数据结构的指针,关系到rcu问题

页框回收

概念

当在从伙伴系统中分配页时,如果分配请求不能得到满足,那么分配将进入慢速分配路径,唤醒交换守护线程(由kswapd_init()初始化),然后从活动的lru向非活动的lru补充可以回收的页,最后在交换分区中分配一个可用的交换标识(由类型和槽位组成),将页在页表中的页号替换为交换标识,并添加到交换页高速缓存中,再解除对应的映射引用,如果页被回写(当前路径直接回写或交换守护线程回写),则就可以释放内核引用(高速缓存或局部缓存),从而完成页的回收(返回到伙伴系统中),最后再次尝试分配请求的页数。

处理流程

  1. 慢速分配
                                        +---------------+
                                        | alloc_pages() |
                                        +-------+-------+
                                                |
                                                v
                                        +-------+-------+
                                        |      ...      |
                                        +-------+-------+
                                                |
                                                v
                                  +-------------+------------+       
                                  | __alloc_pages_slowpath() | 
                                  +-------------+------------+
                                                |
                                                v
                                +---------------+----------------+
                                |                                |
                                v                                v
                      +---------+----------+             +-------+-------+
                      | wake_all_kswapds() |             |      ...      |
                      +---------+----------+             +-------+-------+
                                |                                |
                                v                                v
                        +-------+-------+                +-------+-------+
                        |      ...      |                |out_of_memory()|
                        +-------+-------+                +---------------+
                                |
                                v
                     +----------+-----------+
                     | try_to_free_pages () |
                     +----------+-----------+
                                |
                                v 
API comments
alloc_pages() 分配主入口函数
__alloc_pages_slowpath() 当内核在遍历的所有zone后,发现仍然没有满足请求的页可用,就会进入该流程。
先尝试回收非活动的页,如果不行则杀死一个用户进程来回收页,这个是最后的手段。
wake_all_kswapds() 唤醒交换守护线程,尝试从另一个路径回收页,并可以回写已交换的页
try_to_free_pages() 主要是扫描活动lru,将不活动的页移入非活动lru,然后开尝试回收这些页。
out_of_memory() 最后的手段,选择一个内存使用最多、静态优先级最低的进程,将其杀死,从而回收所占用的页。这时一个耗时的过程。
  1. 尝试回收非活动页
  +------------------------+                                                          +----------+
  | try_to_free_pages()    |                                                          | kswapd() |
  +-----------+------------+                                                          +-----+----+
              |                                                                             |
              |                                                                             v
              |                                      +----------------------+      +--------+--------+
              |                           +----<<----| kswapd_shrink_node() |<-----| balance_pgdat() |
              |                           |          +----------------------+      +-----------------+
              v                           v
      +-------+--------+          +-------+-------+     
      | shrink_zones() |--------->| shrink_node() |---+
      +-------+--------+          +---------------+   |
              |                                       |
              v                                       v
 +------------+-------------+              +----------+----------+        +---------------+
 | wakeup_flusher_threads() |              | shrink_node_memcg() |------->| shrink_list() |
 +------------+-------------+              +----------+----------+        +-------+-------+
              |                                       |                           |
              v                                       v                           v
              +                               +-------+-------+        +----------+-----------+
                                              | shrink_slab() |        | shrink_active_list() |
                                              +---------------+        +----------+-----------+
                                                                                  |
                                                                                  v
                                             +-----------------+       +----------+-----------+
                                             | lru_add_drain() |<------|shrink_inactive_list()|
                                             +--------+--------+       +----------------------+
                                                      |
                                                      v
                                            +---------+----------+
                                            | shrink_page_list() |
                                            +---------+----------+
                                                      |
                                                      v
                                         +------------+-------------+
                                         | putback_inactive_pages() |
                                         +------------+-------------+
                                                      |
                                                      v
                                         +------------+--------------+
                                         | free_hot_cold_page_list() | 
                                         +------------+--------------+
API comments
try_to_free_pages() 构造回收控制参数,以一种优先级多次扫描各个zone,直到回收到足够的页。
优先级越低,说明回收的压力越大,越会采取一些特殊措施,比如尝试写磁盘,为了尽快从分配函数返回,本来这个工作应该守护线程来做的。
shrink_zones() 开始遍历请求node上的按优先级排序的所有zone(可以包括其他nodezone),调用shrink_node()开始回收
shrink_node() 调用shrink_node_memcg()shrink_slab()开始尝试回收当前zone
然后调用should_continue_reclaim()检查是否当前的zone需要继续尝试回收。
should_continue_reclaim() 判断是否应该再次尝试,比如还有一定的满足某个阈值的非活动页,则可以再次尝试。
shrink_slab() 扫描注册的其他可回收函数,尝试回收,注意此处并不是扫描kmem_cache中的slab,而是其他高速缓存,比如目录对象高速缓存
shrink_node_memcg() 遍历当前zone上的所有可回收的活动lru
shrink_list() 判断阈值是否扫描活动lru,然后扫描非活动lru开始扫描
shrink_active_list() 扫描非活动页,然后补充到非活动的lru
shrink_inactive_list() 扫描非活动lru开始尝试回收,没有回收的再次移入活动lru里,并释放回收的页
lru_add_drain() 将各个lru缓存的页刷新到对应的lru链表中
shrink_page_list() 尝试回收,执行解除映射、尝试写出到磁盘等工作,返回可以不能立即回收的页
putback_inactive_pages() 按条件将不能回收的页再次移动到适当的lru
free_hot_cold_page_list() 释放最后不能回到lru的页,比如尝试回收时正在被其他路径使用,在调用putback_inactive_pages()时又被put_page()遗弃的页
  1. 周期压缩SLAB占用的页

kmem_cache初始化之后,注册(见start_cpu_timer()函数)一个延迟工作队列,定时的调用cache_reap()函数进行slab空闲对象链表的回收。

                          +--------------+
                          | cache_reap() |
                          +--------------+



  1. 杀死进程回收占用页

如果交换分区已满或所有的slab都被压缩之后,还是没有可用的页,那么内核调用select_bad_process()尝试按以下规则选出一个拥有大量的页、静态优先级低、不拥有root权限、不直接访问硬件的进程,然后杀死这个进程,回收他占用的页。

                          +-----------------+
                          | out_of_memory() |
                          +-----------------+

交换子系统

概念

交换主要用于回收页框时,存储在磁盘中没有映像的页的内容,从而可以回收该页。过程如下:

当内核在物理内存紧张的时候,内核内存回收机制将用户进程的特定的部分物理页通过交换分区写入磁盘或文件(比如在alloc_pages()时触发交换机制),然后将该页在交换分区的标记写入原页表项代替页号,并将页表项的驻留内存属性清零,当再次访问该页对应的虚拟地址时,CPU发现页表项驻留内存属性没有设置(内核通过pte_present()为假进行判断),则发生交换缺页,内核分配新页给进程并通过页表项携带的交换标记从磁盘将数据读入新页,从而完成缺页异常,并且如果仅有当前进程使用该页,则从交换分区删除该页。由于被交换出的物理页,可能会在短期又会发生缺页,这极有可能是由于几个进程共享该页导致,所以为了不频繁的读写磁盘影响性能,所有在交换分区之上构建了交换页高速缓存,在交换的换入换出中都先操作缓存,然后再周期的将没有被任何人引用的页写入磁盘。

组成

  1. 交换页高速缓存

交换页高速缓存就是基树

  1. 交换分区

内核用一个静态数组存储所有交换分区的描述符swap_info_struct的指针,这些描述都有一个优先级,然后同等级的描述符被链接在优先级链表swap_avail_head的同一个节点之下。每个文件或设备都被初始化为4K大小的块(称之为页槽),并在描述符中有与之对应的引用计数(swap_info_strcut.swap_map[])。内核调用sys_swapon()来初始化一个交换分区,初始化完成之后可用页槽计数为0、不可用的为SWAP_MAP_BAD。每个交换分区的第一个页槽都用于存放交换分区的元数据(使用mkswap系统调用初始化),所有为了辨别,则将0号页槽的引用计数初始化为SWAP_MAP_BAD

 swap_avail_head plist_node
    +----+        +-----+      +-----+
    |    |------->|     |----->|     |
    +----+        +--+--+      +-----+
                     |
                     |
   swap_info[]       v
    +----+        +--+--+   swap_file*    +----+----+----+----+----+----+----+
    |    |------->|     |---------------->|    |    |    |    |    |    |    |
    +----+        +-----+   swap_map[]    +----+----+----+----+----+----+----+
    |    |        |     |---------------->|    |    |    |    |    |    |    |
    +----+        +--+--+                 +----+----+----+----+----+----+----+
    |    |           |  swap_info_struct*
    +----+           |
    |    |           v
    +----+        +--+--+
    |    |------->|     |
    +----+        +-----+
                  |     |
                  +-----+
                        swap_info_struct*
  1. 交换标识符

交换标识符标识在交换分区中页的位置,由交换描述符在swap_info[]数组中的位置(标识类型)偏移一定位数后,或上描述符中文件页槽位置(标识偏移)组成。

0                   63
+------------+----------+
| 57 bit     | 7 bit    |
+------------+----------+
|<- offset ->|<- type ->|
API comments
swap_entry() 合成交换标识符
swap_type() 获取标识符的类型
swap_offset() 获取标识符的偏移
  1. 换出页标识符

换出页标识符通过交换标识符转换而来,因为这个标识符要设置在页表中,所以不能简单的将typeoffset组合了事,应该匹配页表项本身的一些标志位(和CPU架构相关)。X86架构如下:

0                                 63
+-------+-------------+-------+--------+
|1 bit  | 8 bit       | 5 bit | 50 bit | 
+-------+-------------+-------+--------+
|present| other flags | type  | offset |
API comments
pte_to_swp_entry() 换出页标识符转换为交换标识符
swp_entry_to_pte() 交换标识符转换为换出页标识符

交换分区的操作

API comments
get_swap_page() 用于在所有可用的交换分区中获取一个空闲页槽,在增加对页槽的引用后,以交换标识符的形式返回给调用者。在扫描可用的槽位时,优先使用优先级高的(值越小优先级越高)交换分区,然后再以轮训的方式使用同优先级的交换分区,直到找到可用槽位或没有任何可用交换分区为止。
swap_free() 释放交换标识符对应的页槽,当页槽因为为0时,调整所在交换分区的属性

交换流程

  1. 简介

交换子系统的核心入口函数为shrink_page_list(),它被页分配函数和交换守护线程调用。在扫描非活动链表后,内核将可能被回收的页从非活动lru链表中剥离到局部的一个链表中,然后扫描这个链表,尝试回收里面的页,然后将本次不能回收的页按照页的属性和状态再次的移动到各个lru链表中,使用这种方式可以有效的减少对lru链表的并发访问。在整个回收的过程中,必须对处理的页进行trylock_page()加锁,如果不能加锁,说明其他路径正在使用该页(大都是磁盘系统正在回写该页,因为正在回写的页会临时的放置在非活动的lru链表中),因此不能回收这样的页。

  1. 核心流程
    流程图说明
    • 双线表示主干逻辑
    • YES和NO表示条件(或调用)成功或失败
    • active 表示暂时不会能被回收且需要激活页,极有可能在尝试回收后加入到活动的lru链表中
    • unevictable 表示这个页从此不能回收,加入到LRU_UNEVICTABLE类型的lru链表中
    • keep old status 表示暂时不会能被回收且需要但不需要激活页,极有可能在尝试回收后加入到非活动的lru链表中
   +--------------------+
   | shrink_page_list() | 
   +---------++---------+
             ||
             vv
     +-------++------+  NO      +--------+
     | trylock_page()|--------->| active |
     +-------++------+          +--------+                   
             ||                                               
             || YES                                           
             vv                                              
    +--------++--------+   NO   +-------------+              
    | page_evictable() |------->| unevictable |              
    +--------++--------+        +-------------+              
             ||                                               
             || YES                                           
             vv                                              
     +-------++------+  NO      +-----------------+          
     | page_mapped() |--------->| keep old status |
     +-------++------+          +-------+---------+          
             ||                         ^                    
             || YES                     |                    
             vv                         |                    
    +--------++-------+     YES         |                    
    | PageWriteback() |-------->>>------+                    
    +--------++-------+                 ^                    
             ||                         |                    
             ||                         |                    
             vv                         |  PAGEREF_KEEP
    +--------++------+   NO     +-------+-----------------+  
    | force reclaim? |---->>----| page_check_references() |
    +--------++------+          +-----------+----------+--+
             ||                             |          |
             ||YES          PAGEREF_RECLAIM v          | PAGEREF_ACTIVATE
             ++------------<<<--------------+          |
             ||                                        |
             vv                                        |
    +--------++--------+        +---------------+      v      +--------+
    | PageAnon() and   |--->>---| add_to_swap() |-->>--+---->>| active |
    | !PageSwapCache() |  NO    +-------+-------+ NO   ^      +--------+
    +------------------+                |              |
             ||                         |              |
             || NO                      |              |
             ++-------------------------+    +---------+
             ||                              ^ SWAP_FAIL
             vv                              | 
     +-------++------+          +------------+---+   SWAP_AGAIN   +-----------------+
     | page_mapped() |---->>----| try_to_unmap() |----+------->>--| keep old status |
     +-------++------+   YES    +------------+---+    ^           +-----------------+
             ||                              |        |
             || NO            SWAP_SUCCESS   v        |
             ++------------------------------+        |
             ||                                       |
             vv                                       | PAGE_KEEP
      +------++-----+            +--------------------+------+                +--------+
      | PageDirty() |----->>---->| async to pageout() by blk |---->>----------| active |
      +------++-----+    YES     +--+-----+------------------+ PAGE_ACTIVATE  +--------+
             ||                     |     |
             || NO                  |     |               +-------------------+
             ||      PAGE_CLEAR     |     | PAGE_SUCCESS  | PageWriteback()   |      +-----------------+
             ++--------<<<----------+     +------>>>------| or PageDirty()    |-->>--| keep old status |
             ||                                           | or !trylock_page()|  NO  +-----------------+
             ||                                           +---------+---------+
             ||                                                     |               
             ||                                         YES         v               
             ++--------<<<------------------------------<<<---------+               
             ||
             vv
   +---------++---------+   YES  +-----------------------+  NO    +--------+
   | page_has_private() |------->| try_to_release_page() |------->| active |
   +---------++---------+        +-----------+-----------+        +--------+
             ||                              |
             || NO                           |
             ||                       YES    |
             ++------------------<<<---------+
             ||
             ||
             ||                        +----------------------------+
             vv                  +---->| __delete_from_swap_cache() |----->+
   +---------++---------+        |     +----------------------------+      |  NO   +-----------------+
   | __remove_mapping() |------->+                                         |------>| keep old status |
   +---------++---------+        |     +----------------------------+      |       +-----------------+
             ||                  +---->| __delete_from_page_cache() |----->+
             || YES                    +----------------------------+
             ||                       
             ||
  +----------++---------------+
  | free_hot_cold_page_list() |
  +----------++---------------+
  1. 函数说明
API struct page comments
trylock_page() page->flags \= PG_locked 不阻塞,试图锁定页
page_evictable() / 1. 检查页关联映射的地址空间address_space->flags 是否设置AS_UNEVICTABLE标志,或者页描述符page->flags是否设置PG_mlocked标志。
2. 因为在用户调用系统函数创建一个映射页时可以指定该虚拟地址空间或文件内容映射的页可以不被换出到磁盘。(参见mlock()系统调用)
page_mapped() / 检查页描述符的page->_mapcount映射计数字段是否大于-1,从而判断是否被映射。(该计数器的初始值为-1
PageWriteback() / 检查页描述符的page->flags是否设置PG_writeback标志,从而判断页是否正在通过VFS文件系统正在排队回写数据,因为上次回收页时执行的pageout()是异步的,在为成功回写之前,页描述符一直停留在非活动链表中,这样不必浪费资源时刻关注这些成功回写的、可以释放的页
page_check_references() 1. page->flags \= PG_referenced
2. page->flags \= PG_swapbacked
如果上层调用不是强迫回收当前状态是非活动的页,那么在明确回收之前,会调用此函数再次检测页是否最近又被访问过,从而在最近下个时刻避免不必要的缺页处理
PageAnon() / 检查页描述符page->mapping是否设置PAGE_MAPPING_ANON,从而判断是否是匿名映射
PageSwapCache() / 检查页描述符page->flags是否设置PG_swapcache,从而判断是否已经添加到交换高速缓存中(前提是页必须是匿名页),只有匿名页被添加到缓存中才能被换出
add_to_swap() 1. page->flags \= PG_swapcache
2. page->_refcount += 1
将匿名映射页添加到交换高速缓存中。
1. 如果有多个交换分区,使用优先级高的交换分区,然后分配一个页槽并它的增加引用计数,然后将这个槽位和交换分区的位置组合成交换标识符。
2. 将交换标识符和页作为键值添加到交换分区对应的页高速缓存中(一棵基树)。
3. 如果所有的交换分区槽位不足,则可能失败。
try_to_unmap() 1. page->_refcount -= 1
2. page->_mapcount -= 1
尝试解除映射,这里有一个关键条件:如果没有其他路径使用该页,那么page->_refcount = page->_mapcount + 2(因为页在lru链表和局部链表中,他们各有一个内核引用;且每个页的映射引用有一个内核引用)。
1. 如果是匿名页,则遍历反向匿名映射中的节点,尝试解除页在进程的页表中的映射,并将交换标识符设置到页表。
2. 如果是文件映射,则遍历地址空间中的优先级树中的节点,尝试解除每个文件描述符对齐的引用(猜的?)。
PageDirty() / 检查页描述符page->flags是否设置PG_dirty,从而判断是否为脏页(页的内容是否改变过)。
1. 作为匿名页,如果自从上次换入,页的内容都为改变过,且页还在交换分区中,则不必再写磁盘,直接尝试回收。
2. 作为文件页,与匿名页一样。
pageout() 1. page->flags \= PG_writeback
2. page->flags \= PG_reclaim 3. page->_refcount += 1
使用页对应的地址空间(匿名页是交换分区抽象的VFS方法集合,文件页则是对应的文件系统的VFS方法集合)的方法分配一个块缓存排队写出请求到磁盘,然后将页写出,这个过程是异步的。
1. 在写出请求之前设置对应的页高速缓存的脏标记(只有脏页、非写出的页才需要请求写出)。
2. 请求之后设置页描述符的PG_writebacePG_reclaim标志。
3. 在请求写出的方法中,如果请求成功,则标记页高速缓存的写出标记,同时清除脏标记。
page_has_private() page->_refcount -= 1 判断页在写磁盘队列中是否有元数据缓存(在这里仅文件映射有)。如果页完成写,那么存储在page->flags中的PG_private标记被清除。
try_to_release_page() / 尝试释放写页到磁盘时关联的元数据缓存
__remove_mapping() / 如果写磁盘异步的完成,在这里就被检查到,然后解除可回写到磁盘的页在相应的页高速缓存的引用
__delete_from_file_cache() / 直接从文件页高速缓存释放占用的节点
__delete_from_swap_cache() / 直接从交换页高速缓存释放占用的节点
free_hot_cold_page_list() / 在页被解除所有的引用后,使用该函数将其直接释放到伙伴系统中

换出与换入举例

先举例一种情景:当两个进程的匿名映射共享同一个物理页,并已准备将两个进程的匿名映射解除映射换出他们引用的物理页。假设开始时shrink_page_list()已完成add_to_swap()调用,在解除其中一个进程的映射后,这个进程立马又调用do_swap_page()进行换入操作。在这个过程中观察交换槽位引用、交换页高速缓存、页描述符、页表项的竞争时序情况:

file comments
mapcount 页的映射计数
refcount 页的引用计数,在不考虑其内核引用时,进程匿名页的mapcount == refcount
swapcount 交换槽位的引用,如果进程的页被交换,那么该计数为进程数量,或再加一(如果还仍被高速缓存引用的话)
  1. 初始化状态,有进程请求换出页(假设不是两个匿名页的宿主进程)操作相关结构,在调用add_to_swap()后,调用try_to_unmap()之前。从换出进程的视角观察:
            +-----+-----+-----+
            |  1  |  2  |  3  |  swap slot
            +-----+--+--+-----+
                     |
            +-----+--+--+-----+
            |  1  |  2  |  3  |  swap cache
            +-----+--+--+-----+
                     |
                     +-------+
            +-----+          |
            | pte |          v
            +-----+------>+--+--+      +-----+
                          |page |<-----| lru |
            +-----+------>+-----+      +-----+
            | pte |
            +-----+

1. mapcount = 2
2. refcount = 4 ,由于操作换出的页还被`lru`引用和高速缓存引用
3. swapcount = 1 ,暂时只被高速缓存引用
  1. 解除引用try_to_unmap()后,在写页pageout()之前发生缺页do_swap_page() ,从各自的视角来看 :
                task swapping out                    |               task' swapping in
                                                     |                                   
            +-----+-----+-----+                      |           +-----+-----+-----+ 
            |  1  |  2  |  3  |  swap slot           |           |  1  |  2  |  3  |
            +-----+--+--+-----+                      |           +-----+--+--+-----+
                     |                               |                    | 
         +---------->+                               |        +---------->+
         |           |                               |        |           | 
         |  +-----+--+--+-----+                      |        |  +-----+--+--+-----+
         |  |  1  |  2  |  3  |  swap cache          |        |  |  1  |  2  |  3  |
         |  +-----+--+--+-----+                ============>  |  +-----+--+--+-----+
         |           |                               |        |           |
         |           +-------+                       |        |           +-------+
         |  +-----+          |                       |        |  +-----+          |
         +--| pte |          v                       |        +--| pte |          v
         |  +-----+       +--+--+      +-----+       |           +-----+       +--+--+
         |                |page |<-----| lru |       |                         |page |
         |  +-----+       +-----+      +-----+       |           +-----+------>+-----+
         +--| pte'|                                  |           | val |     
            +-----+                                  |           +-----+
                                                     |     
1. mapcount = 0                                      |     1. mapcount = 0
2. refcount = 2                                      |     2. refcount = 3    
3. swapcount = 3                                     |     3. swapcount = 3

解除引用之后,交换槽位计数增加到页表项对其的引用计数,而引用计数被清零。页的引用计数减少了映射计数,但如果没有其他路径引用这个页,那么一定是为2,因为仅高速缓存和lru对其有引用,这也是很多相关的函数中的表达式有加减2的判断。但是解除映射释放页表锁之后,pageout()之前;缺页do_swap_page()可以进入lookup_swap_cache()流程,并从高速缓存查找到页,并用局部变量(上图中用val表示)引用了页。但是由于换出shrink_page_list()持有这个页的锁,缺页下一步了的lock_page()会阻塞挂起(这也是页锁的重要作用),那么这个缺页进程的页表项还是引用页槽,而不是将页号设置到页表,然后解除对页槽的引用。

  1. 在执行pageout()时(缺页进程正在被挂起),is_page_cache_freeable()检查页的引用计数时,发现了缺页进程的存在,所以写出不会发生,进而页在本次也不会被回收掉,然后调用unlock_page()唤醒缺页进程,继续尝试回收下一个进程。而缺页进程完成页表的设置和页槽引用的释放等。从内核的视角观察:
            +-----+-----+-----+
            |  1  |  2  |  3  |  swap slot
            +-----+--+--+-----+
                     |
         +---------->+
         |           |
         |  +-----+--+--+-----+
         |  |  1  |  2  |  3  |  swap cache
         |  +-----+--+--+-----+
         |           |
         |           +-------+
         |  +-----+          |
         |  | pte |          v
         |  +-----+------>+--+--+      +-----+
         |                |page |<-----| lru |
         |  +-----+       +-----+      +-----+
         +--| pte'|
            +-----+
            

1. mapcount = 1 ,只换入了一次
2. refcount = 3 ,除了会被`lru`引用和换入页表项引用外,还会被高速缓存一直引用,因为有换出的进程还未换入。只有swapcount为1时,即仅高速缓存引用时,才会解除与页的映射。
3. swapcount = 2 ,只被高速缓存引用和另一个已解除映射的页表项引用
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

内核内存回收原理简介 的相关文章

  • 2.6内核的通用的编译步骤

    2 6内核的通用的编译步骤 1 下载源码并解压 虽然我们可以将内核源码存放在任何自己找得到的地方 但通常还是会将内核源码下载到 usr src目录并解压 cd usr src wget ftp kernel org pub linux ke
  • Linux内核——cli()和sti()——标志寄存器的中断标志

    cli 和sti 有点类似于汇编指令中的CLI和STL 当某个任务在执行的过程中不想被中断 则可以在任务的开始出执行cli 在任务的结束处执行sti 恢复中断的执行 为了避免竞争条件和中断对临界代码区的干扰 在Linux 0 12内核代码中
  • KVM内核代码结构

    KVM内核代码结构 因为KVM的源代码已经包含在了Linux的内核树中 因此我们只需直接从www kernel org下载代码即可 内核源码包打开较大 解开后目录结构大概是这个样子 涉及KVM的主要有两个目录 virt和arch x86 k
  • linux的自旋锁struct spinlock_t的使用

    在linux中提供了一些机制用来避免竞争条件 最简单的一个种就是自旋锁 例如 当一个临界区的数据在多个函数之间被调用时 为了保护数据不被破坏 可以采用spinlock来保护临界区的数据 当然还有一个就是信号量也是可以实现临界区数据的保护的
  • linux的dirty page回写磁盘过程中是否允许并发写入更新page?

    概述 众所周知Linux内核write系统调用采用pagecache机制加速写入过程 避免write系统调用长时间block应用进程 用户态进程执行write调用的时候 内核只是将用户态buffer copy到内核的pagecache当中
  • ARM架构内核启动分析-head.S(1.1、vmlinux.lds 链接脚本分析)

    ARM架构内核启动分析 一 start kernel之前 首先需要明确的是 内核镜像在被解压之后执行 是执行哪段代码 这是个重要的问题 平时在编译生成应用程序或内核模块时 我们无需考虑链接的具体细节 如代码和数据放在哪里 代码执行入口在哪等
  • chroot命令

    转载 理解 chroot 什么是 chroot chroot 即 change root directory 更改 root 目录 在 linux 系统中 系统默认的目录结构都是以 即是以根 root 开始的 而在使用 chroot 之后
  • linux kerne新版本编号?

    今天看到linux内核版本号都到3 4了 心中非常惊讶 为什么现在版本飞这么快了 于是一番google 终于找到了两篇文章 大家可以看看 Linux kernel version bumped up to 3 0 as 20th birth
  • 虚拟文件系统 (VFS)-基于linux3.10

    引言 虚拟文件系统 VFS VirtualFileSystem 介于具体的文件系统和C库之间 其用提供一个统一的方法来操作文件 目录以及其它对象 其能够很好的抽象具体的文件系统 在linux上具体的文件系统主要分为三类 l 基于非易失性的存
  • 带外数据

    定义带 外 数据 想 像一下在银行人们排起队等待处理他们的帐单 在这个队伍中每个人最后都会移到前面由出纳员进行服务 现在想像一下一个走入银行 越过整个队伍 然后用枪抵 住出纳员 这个就可以看作为带 外 数据 这个强盗越过整个队伍 是因为这把
  • bootloader详解

    一 bootloader介绍 bootloader是硬件在加电开机后 除BIOS固化程序外最先运行的软件 负责载入真正的操作系统 可以理解为一个超小型的os 目前在Linux平台中主要有lilo grub等 在Windows平台上主要有nt
  • file_operations 结构体

    file operations 结构体中的成员函数是字符设备驱动程序设计的主体内容 这些函数实际会在应用程序进行 Linux 的 open write read close 等系统调用时最终被调用 file operations 结构体目前
  • 【xenclient】 使用小结 -- ubuntu的千百bug

    说道多系统 不能不提下ubuntu 以前redhat似乎是linux的领头羊 但在桌面领域 跟windows还是差得太远 在linux最弱的桌面特性上 ubuntu算是第一个以桌面特效全面超越windows的系统了 因此我的系统 除了保留做
  • 为什么linux kernel默认的页面大小是4K,而不是4M或8M?

    相信很多人在看内核内存管理部分的时候 都有这样一个疑问 为什么物理页面的大小选择4K 而不是大一些或者小一些呢 这个问题没有固定的答案 仁者见仁智者见智 每个人的关注点不一样 所以这篇文章不是说给出一个固定的答案 更多的只是一篇讨论性的文章
  • 七种Linux设备驱动模型之——Device

    前言 Linux将所有的设备统一抽象为struct device结构 同时将所有的驱动统一抽象为struct device driver结构 这样设计之后就方便驱动开发工程师编写驱动 只需要将具体的设备包含struct device结构 具
  • Linux内核文件系统知识大总结

    1 文件系统特点 文件系统要有严格的组织形式 使得文件能够以块为单位进行存储 文件系统中也要有索引区 用来方便查找一个文件分成的多个块都存放在了什么位置 如果文件系统中有的文件是热点文件 近期经常被读取和写入 文件系统应该有缓存层 文件应该
  • 慢慢欣赏linux pud_offset解析

    typedef struct pudval t pud pud t gt typedef u64 pudval t dir表示L0页表索引的指针 指向PUD页表的基地址 define pud offset dir addr pud t va
  • Linux中select poll和epoll的区别

    select的本质是采用32个整数的32位 即32 32 1024来标识 fd值为1 1024 当fd的值超过1024限制时 就必须修改FD SETSIZE的大小 这个时候就可以标识32 max值范围的fd 对于单进程多线程 每个线程处理多
  • Linux内核源码学习(1)

    一 内核简介 1 在安装好的Linux系统中 内核的源代码位于 usr src linux 2的10次方就是1K 1024 16位CPU的地址空间是64K X86结构的80386是32位CPU 段描述结构伪代码 typedef struct
  • linux内核驱动开发笔试题

    linux内核驱动开发笔试题 一 一些常规中举的C考题 第一题 写出下述程序结果 int m 3 1 4 7 2 5 8 3 6 9 int i j k 2 for i 0 i lt 3 i printf d m k i 问题所在 本题考点

随机推荐

  • C#网络编程TCP通信实例程序简单设计

    用TcpClient和TcpListener设计一个Tcp通信的例子 通信程序截图 2个客户端链接服务端测试截图 服务端 客户端 运行动态图 C 程序设计代码 BenXHSocket dll主要代码设计 SocketObject类 Proj
  • 京东自动抢茅台脚本(亲测可用,文末有新年礼物)

    点击上方 程序IT圈 选择 置顶公众号 关键时刻 第一时间送达 2021年第一天 祝大家新年快乐 文末给大家送个新款AirPods Pro 大家图个喜庆 这期为大家继续分享个GitHub上面的大神开源的项目 大家可以认真看看 然后把对自己刚
  • 线程的同步与互斥

    线程的同步与互斥 互斥 当一个公共资源同一时刻只能被一个进程或线程使用 多个进程或线程不能同时使用公共资源 如 当线程A在使用打印机时 其他线程都需要等待 同步 两个或两个以上的进程或线程在运行过程中协同步调 按预定的先后次序运行 如 A任
  • RK3588 烧写固件

    首先先安装驱动DriverInstall 上电 配置串口调试工具 一般使用MobaXterm rk3588 波特率1500000 串口软件有信息打印说明成功 进行下一步操作 升级固件里 选固件 选择updata img文件 再点升级 串口软
  • 【微信小程序】解决微信小程序textarea层级过高穿透问题

    先来张完美的效果图 说下遇到的问题 之前做过的一个项目改版碰到的病例上传页面发布按钮上一版本是在底部放置的 这一版改为了顶部固定 由于上传页面顶部有两个textarea输入框所以问题就产出了 之前使用的button和view标签布的局页面上
  • 2020浙江省赛(ZJCPC)赛后总结

    引言 2020注定是特殊的一年 其时间线受疫情影响 本该在上半年举办的活动全部放到了下半年 虽然能够在2020结束前能够举办已经很感谢主办方 然后10 17就在线上参加了2020浙江省大学生程序设计大赛 得益于参加过计量大学的模拟赛以及省赛
  • 【博客687】k8s informer的list-watch机制剖析

    k8s informer的list watch机制剖析 1 list watch场景 client go中的reflector模块首先会list apiserver获取某个资源的全量信息 然后根据list到的rv来watch资源的增量信息
  • python中object的用法_【Python】【基础知识】【内置函数】【object的使用方法】

    原英文帮助文档 classobject Return a new featureless object object is a base for all classes It has the methods that are common
  • 【笔记】CPU的结构和功能(一)

    一 CPU的结构 1 CPU的功能 2 CPU结构框图 3 CPU的寄存器 用户可见寄存器 控制和状态寄存器 4 控制单元和中断系统 二 指令周期 1 指令周期的基本概念 2 指令周期的数据流 取指周期数据流 间址周期的数据流 执行周期的数
  • 命令计算机执行指定的操作,计算机如何执行一条机器指令

    计算机如何执行一条机器指令 计算机如何执行一条机器指令 文章目录指令运行过程 微程序控制基本概念 几个周期区别 寻址方式 指令运行过程 在上篇我们谈到 计算机处理一段程序 就会将程序翻译成机器指令 然后执行完成相应的任务 执行指令的过程分为
  • 为什么Python没有main函数?

    作者 豌豆花下猫 来源 Python猫 ID python cat 众所周知 Python中没有所谓的main函数 但是网上经常有文章提到 Python的main函数 和 建议编写main函数 其实 可能他们是想模仿真正的main函数 但是
  • AD18的覆铜技巧

    AD18的覆铜技巧 设置覆铜的安全距离 从工具中选择覆铜管理器 设置覆铜的安全距离 进入设计 规则 创建一个新的Clearance如下图 从工具中选择覆铜管理器 从Create NEW polygon from 中选择Board Outli
  • 37.cuBLAS开发指南中文版--cuBLAS中的Level-2函数her()

    2 6 20 cublasher cublasStatus t cublasCher cublasHandle t handle cublasFillMode t uplo int n const float alpha const cuC
  • Caused by: java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available产生原因

    在项目部署时需要将代码打包放到服务器上 打包的时候报了如下的错误 但是在idea上却是能正常运行的 java lang IllegalStateException Failed to load ApplicationContext at o
  • OpenGL学习——第十课:纹理映射(1)实例

    这里使用第九课中的Texture h和Texture cpp来实现对一个正方体的六面的纹理效果 需要注意的代码就是相关纹理映射的部分 1 代码部分省去了Texture h和Texture cpp 因此运行时候需要先把这两个加入到工程目录下
  • 猿创征文

    猿创征文 国产数据库之在k8s环境下部署RadonDB MySQL集群 一 RadonDB MySQL介绍 1 RadonDB MySQL简介 2 RadonDB MySQL的应用场景 3 RadonDB MySQL核心功能 4 Radon
  • 用Python求三角形面积

    题目描述 三角形面积 SQRT S S a S b S c 其中S a b c 2 a b c为三角形的三边 定义两个带参的宏 一个用来求area 另一个宏用来求S 写程序 在程序中用带实参的宏名来求面积area 输入 a b c三角形的三
  • esp32 怎么分配freertos 堆栈大小_spiffs 文件系统在esp32中的应用

    spiffs 介绍 SPIFFS 是一个开源文件系统 用于 SPI NOR flash 设备的嵌入式文件系统 支持磨损均衡 文件系统一致性检查等功能 spiffs 源码地址 github com spiffs 特点 而我们知道乐鑫的esp3
  • Qss之QTabWidget美化

    直接上代码吧 QTabWidget QTabWidget pane border none QTabWidget tab bar left 5px QTabBar tab background gray border 2px solid C
  • 内核内存回收原理简介

    页框回收与交换 概念 内核在为进程服务的过程中会分配大量的页 但是这些页对应的虚拟地址在进程的生命周期里一直会被断断续续的访问 所以当内核同时为大量进程服务时 内存终究会耗尽 所有页框回收就是在内核未耗尽内存之前 因为回收与交换也会使用内存