本帖最后由 yin_wu_qing 于 2024-1-7 20:21 编辑
- 1. page数据结构中的_refcount和_mapcount有什么区别?
_refcount和_mapcount是page数据结构中非常重要的两个引用计数,且都是atomic_t类型的变量。
A _refcount表示内核中引用该页面的次数。
- 当_refcount的值为0时,表示该页面为空闲页面或即将要被释放的页面。
- 当_refcount的值大于0时,表示该页面已经被分配且内核正在使用,暂时不会被释放。内核中提供加/减_refcount的接口函数,读者应该使用这些接口函数来使用_refcount引用计数。
get_page():_refcount加1;put_page():_refcount减1。若_refcount减1后等于0,那么会释放该页面。
get_page()函数调用page_ref_inc()来增加引用计数,最后使用atomic_inc()函数原子地增加引用计数。
put_page()首先使用put_gage_testzero()函数来使_refcount减1并且判断其是否为0。如果_refcount减1之后等于0,就会调用_put_page()来释放这个页面。
B _mapcount表示这个页面被进程映射的个数,即已经映射了多少个用户PTE。每个用户进程都拥有各自独立的虚拟空间(256TB)和一份独立的页表,所以可能出现多个用户进程地址空间同时映射到一个物理页面的情况,RMAP系统就是利用这个特征来实现的。_mapcount主要用于RMAP系统中。
- 若_mapcount等于-1,表示没有PTE映射到页面。
- 若_mapcount等于0,表示只有父进程映射到页面。匿名页面刚分配时,_mapcount初始化为0。例如,当do_anonymous_page()产生的匿名页面通过page_add_new_anon_map()添加到rmap系统中时,会设置_mapcount为0,这表明匿名页面当前只有父进程的PTE映射到页面。
- 若_mapcount大于0,表示除了父进程外还有其他进程映射到这个页面。同样以创建子进程时共享父进程地址空间为例,设置父进程的PTE内容到子进程中并增加该页面的_mapcount。
- 2.匿名页面和高速缓存页面有什么区别?
匿名页面的产生:从内核的角度来看,在如下情况会产生匿名页面。
- 用户空间通过malloc()/mmap()接口函数来分配内存,在内核空间中发生缺页中断时,do_anonymous_page()会产生匿名页面。
- 发生写时复制。当缺页中断出现写保护错误时,新分配的页面是匿名页面,下面又分两种情况。
a 调用do_wp_page().
分配只读的特殊映射的页面,如映射到零页面的页面。
分配非单身匿名页面(有多个映射的匿名页面,即page->_mapcount>0)。
分配只读的私有映射的内容缓存页面。
分配KSM页面。
b 调用do_cow_page()共享的匿名映射(Shared Anonymous Mapping,SHMM)页面。
上述这些情况在发生写时复制时会新分配匿名页面。
- do_swap_page(),从交换分区读回数据时会分配匿名页面。
- 迁移页面。以do_anonymous_page()分配一个匿名页面为例,匿名页面刚分配时的状态如下。
page->_refcount = 1。
page->_mapcount = 0。
设置PG_swapbacked标志位。
加入LRU_ACTIVE_ANON链表中,并设置PG_lru标志位。
page->mapping指向VMA中的anon_vma数据结构。
匿名页面在缺页中断中分配完成之后,就建立了进程虚拟地址空间和物理页面的映射关系,用户进程访问虚拟地址即访问匿名页面的内容。
高速缓存页面
假设现在系统内存紧张,需要回收一些页面来释放内存,匿名页面刚分配时会加入活跃LRU链表(LRU_ACTIVE_ANON)的头部,在活跃LRU链表移动一段时间后,该匿名页面到达活跃LRU链表的尾部,shrink_active_list()函数把该页面加入不活跃LRU链表(LRU_INACTIVE_ANON)。Linux内核为页面迁移提供了一个系统调用migrate_pages,它可以迁移一个进程的所有页面到指定内存节点上。该系统调用最早是为了在UMA系统中提供一种迁移进程到任意内存节点的能力。现在内核除了为NUMA系统提供页面迁移能力外,其他的一些模块也可以利用页面迁移功能做一些事情。如内存规整和内存热插拔等。页面迁移的设计初衷是在UMA系统中提高内存访问性能,把一些页面从一个内存节点迁移到另外一个内存节点。它还有一个应用场景(内存规整)。这些迁移的页面都是LRU链表上的页面。但是,最近几年Linux内核引入了一些新的特性,如zsmalloc和virtio-balloon页面。以virtio-balloon页面为例,它也有页面迁移的需求,之前的做法是在virtio-balloon驱动中进行迁移操作和相应的逻辑。如果其他的驱动也想做类似的页面迁移,那么它们就不能复用与virtio-balloon驱动相关的代码,必须重新写一套代码,这样会造成很多代码的重复与冗余。为了解决这个问题,内存管理的页面迁移机制提供相应的接口来支持这些非LRU页面的迁移。因此,页面迁移机制支持两大类内存页面。
传统LRU页面,如匿名页面和文件映射页面。
非LRU页面,如zsmalloc或者virtio-balloon页面。
- 3. page数据结构中有一个锁,我们称为页锁,请问trylock_page()和lock_page()有什么区别?
Page数据结构中的成员flags定义了一个标志PG_locked,内核通常利用PG_locked来设置一个页锁。lock_page()函数用于申请页锁,如果页锁被其他进程占用了,那么它会睡眠等待。从lock_page()函数的声明和实现代码来看,lock_page()函数首先会调用trylock_page()函数,然后调用__lock_page()函数。trylock_page()和lock_page()这两个函数看起来很相似,但有很大的区别。trylock_page()定义在include/linux/pagemap.h文件中,它使用test_and_set_bit_lock()尝试为page的flags设置PG_locked标志位,并且返回原来标志位的值。如果page的PG_locked位已经置位了,那么当前进程调用trylock_page()时返回false,说明有其他进程已经锁住了page。因此,若trylock_page()返回false,表示获取锁失败;若返回true,表示获取锁成功。
trylock_page()尝试给页面加锁。若trylock_page()返回false,表示别的进程已持有了这个页面的锁;否则,表示当前进程已经成功获取锁。
- 如果尝试获取页面不成功,当前不是强制迁移(force=0)或迁移模式为MIGRATE_ASYNC,则会直接忽略这个页面,因为这种情况下没有必要睡眠等待页面释放锁。
- 如果当前进程设置了PF_MEMALLOC标志位,表示当前进程可能处于直接内存压缩的内核路径上,通过睡眠等待页锁是不安全的,所以直接忽略该页面。例如,在文件预读中,预读的所有页面都会加锁并被添加到LRU链表中,等到预读完成后,这些页面会标记PG_uptodate并释放锁,这个过程中块设备层会把多个页面合并到一个BIO设备中。如果在分配第2个或者第3个页面时发生内存短缺,内核会运行到直接内存压缩的内核路径上,导致一个页面加锁之后又等待这个锁,产生死锁,因此直接内存压缩的内核路径会标记PF_MEMALLOC。PF_MEMALLOC标志位一般在直接内存压缩、直接内存回收以及kswapd中设置,这些场景下可能会有少量的内存分配行为,因此若设置PF_MEMALLOC标志位,表示允许它们使用系统预留的内存,即不用考虑zone水位问题,可以参见__perform_reclaim()、__alloc_pages_direct_compact()和kswapd()等函数。除了上述情况外,其余情况下只能调用lock_page()函数来等待页锁被释放。
- 4. 请画出page数据结构中flags成员的布局示意图。
- num pageflags {
- PG_locked, /* Page is locked. Don't touch. */ //表示页面已经上锁;如果该比特位置位,说明已经被锁,内存管理其他模块不能访问这个页面,防止竞争
PG_error, //表示页面操作过程中发生错误时会设置该位;
PG_referenced, //同PG_active一起,用于控制页面的活跃程度,在kswapd页面回收中使用;
PG_uptodate, //表示页面的数据已经从块设备成功读取到内存页面;
PG_dirty, //表示页面内容发生改变,这个页面为脏的,即页面内容被改写,还没同步到外部存储器
PG_lru, //表示页面加入了LRU链表中,内核使用LRU链表来管理活跃和不活跃页面;
PG_active,
PG_workingset,
PG_waiters, /* Page has waiters, check its waitqueue. Must be bit #7 and in the same byte as "PG_locked" */
PG_slab, //页面用于slab分配器
PG_owner_priv_1, /* Owner use. If pagecache, fs may use*/
PG_arch_1,
PG_reserved,
PG_private, /* If pagecache, has fs-private data */
PG_private_2, /* If pagecache, has fs aux data */
PG_writeback, /* Page is under writeback */ //表示页面的内容正在向块设备进行会写
PG_compound,
PG_swapcache,
PG_mappedtodisk, /* Has blocks allocated on-disk */
PG_reclaim, /* To be reclaimed asap */ //表示这个页面马上要被回收
PG_swapbacked, /* Page is backed by RAM/swap */ //表示页面具有swap缓存功能,通过匿名页面才可以写回swap分区
PG_unevictable, /* Page is "unevictable" */ //表示这个页面不能回收
#ifdef CONFIG_MMU
PG_mlocked, /* Page is vma mlocked */ //表示页面对应的vma处于mlocked状态;
#endif
__NR_PAGEFLAGS,
};
- 5. 请列举page数据结构中_refcount和_mapcount计数的使用案例。
_refcount通常在内核中用于跟踪页面的使用情况,常见的用法归纳总结如下:
- 初始状态下,空闲页面的_refcount是0
- 分配页面时,_refcount会变成1。页面分配接口函数alloc_pages()在成功分配页面后,_refcount应该为0,这里使用VM_BUG_ON_PAGE()来判断,然后设置这些页面的_refcount为1。
- 加入LRU链表时,页面会被kswapd内核线程使用,因此_refcount会加1。以malloc为用户程序分配内存为例,发生缺页中断后,do_anonymous_page()函数成功分配出来一个页面,在设置硬件PTE之前,调用lru_cache_add()函数把这个匿名页面添加到LRU链表中。在这个过程中,使用page_cache_get()宏来增加_refcount。当页面已经添加到LRU链表后,_refcount会减1,这样做的目的是防止页面在添加到LRU链表过程中被释放。
- 被映射到其他用户进程的PTE时,_refcount会加1。如在创建子进程时共享父进程的地址空间,设置父进程的PTE内容到子进程中并增加该页面的_refcount,详见do_fork()->copy_process()->copy_mm()->dup_mmap()->copy_pte_range()->copy_one_pte()函数。
- 在copy_one_pte()函数中,通过vm_normal_page()找到父进程的PTE对应的页面,然后增加这个页面的_refcount。
- 页面的private成员指向私有数据。
对于PG_swapable的页面,__add_to_swap_cache()函数会增加_refcount。
对于PG_private的页面,主要在块设备的buffer_head中使用,如buffer_migrate_page()函数中会增加_refcount。
- 内核对页面进行操作等关键路径上也会使_refcount加1,如内核的follow_page()函数和get_user_pages()函数。以follow_page()函数为例,调用者通常需要设置FOLL_GET标志位来使其增加_refcount。如KSM中获取可合并的页面函数get_mergeable_page(),另一个例子是DIRECT_IO,详见write_protect_page()函数。
_mapcount表示这个页面被进程映射的个数,即已经映射了多少个用户PTE。每个用户进程都拥有各自独立的虚拟空间(256TB)和一份独立的页表,所以可能出现多个用户进程地址空间同时映射到一个物理页面的情况,RMAP系统就是利用这个特性来实现的。_mapcount主要用于RMAP系统中。
若_mapcount等于-1,表示没有PTE映射到页面。
若_mapcount等于0,表示只有父进程映射到页面。匿名页面刚分配时,_mapcount初始化为0。例如,当do_anonymous_page()产生的匿名页面通过page_add_new_anon_rmap()添加到rmap系统中时,会设置_mapcount为0,这表明匿名页面当前只有父进程的PTE映射到页面。
若_mapcount大于0,表示除了父进程外还有其他进程映射到这个页面。同样以创建子进程时共享父进程地址空间为例,设置父进程的PTE内容到子进程中并增加该页面的_mapcount,详见do_fork()->copy_process()->copy_mm()->dup_mmap()->copy_pte_range()->copy_one_pte()函数。