linux进程回收机制,linux内存回收机制-爱代码爱编程
1回收哪些页面
Page cache;
用户地址空间的内存映射页面;
Slab缓存:如dentry和inode cache;
匿名页:进程堆栈和mmap匿名映射内存区;回收前先换置到swap;
2何时回收
Kswapd定期唤醒:当系统空闲内存小于阈值则进行页面回收;
直接页面回收:假设操作系统需要通过伙伴系统为用户进程分配一大块内存,或者需要创建一个很大的缓冲区,而当时系统中的内存没有办法提供足够多的物理内存以满足这种内存请求,这时候,操作系统就必须尽快进行页面回收操作;
OS尝试内存回收后仍无法获取足够页面,则调用find_bad_process并进行OOM kill;
3如何回收
基于LRU算法;每个zone维护两个LRU链表
struct zone {
……
spinlock_t lru_lock;
struct list_head active_list;
struct list_head inactive_list;
unsigned long nr_active;
unsigned long nr_inactive;
……
}
PG_active/PG_referenced用于标识页面活跃度,前者标识页面时活跃的;后者表示页面最近是否被访问过,每访问一次便会置位;
注:假如只是用一个标志符,在页面被访问时,置位该标志符,之后该页面一直处于活跃状态,如果操作系统不清除该标志位,那么即使之后很长一段时间内该页面都没有或很少被访问过,该页面也还是处于活跃状态。为了能够有效清除该标志位,需要有定时器的支持以便于在超时时间之后该标志位可以自动被清除。然而,很多Linux支持的体系结构并不能提供这样的硬件支持,所以Linux中使用两个标志符来判断页面的活跃程度。
Linux依据这两个字段将page在active_list和inactive_list之间移动;
注:1表示函数mark_page_accessed(),2表示函数page_referenced(),3表示函数activate_page(),4表示函数shrink_active_list()
不管是kswapd还是直接页面回收,最终都调用shrink_slab和shrink_zone;
直接页面回收:反复调用这两个函数,若特定循环次数内没能成功释放N个page,则调用OOM killer;
Kswapd:对每个zone都调用shrink_zone();
3.1 Shrink_slab原理
先向操作系统内核注册shrinker函数,会在内存较少的时候主动释放一些该磁盘缓存占用的空间。
函数shrink_slab()会遍历shrinker链表,从而对所有注册了shrinker函数的磁盘缓存进行处理。
注册shrinker是通过函数set_shrinker()实现的,解除shrinker注册是通过函数remove_shrinker()实现的。当前,Linux操作系统中主要的shrinker函数有如下几种:
shrink_dcache_memory():该shrinker函数负责dentry缓存。
shrink_icache_memory():该shrinker函数负责inode缓存。
mb_cache_shrink_fn():该shrinker函数负责用于文件系统元数据的缓存。
3.2 Shrink_zone原理
1通过shrink_active_list()将页面从active移到inactive
list;
2调用shrink_inactive_list()将inactive
list的页放入临时链表,最终调用shrink_page_list()回收
3.2.1
Swappiness的意义
上文提到的shrink_zone()会调用shrink_lruvec(),而active/inactive list又各分为anon匿名页和file cache映射页链表,总计4个LRU;
而swappines只针对anon page,即便其为0也有可能执行swap。
vmscan.c中的get_scan_coun()
1.首先如果系统禁用了swap或者没有swap空间,则只扫描file
based的链表,即不进行匿名页链表扫描
代码:
if (!sc->may_swap || (get_nr_swap_pages() <= 0)) {
scan_balance = SCAN_FILE;
goto out;
}
2.如果当前进行的不是全局页回收(cgroup资源限额引起的页回收),并且swappiness设为0,则不进行匿名页链表扫描,
代码:
if (!global_reclaim(sc) && !vmscan_swappiness(sc)) {
scan_balance = SCAN_FILE;
goto out;
}
3.如果进行链表扫描前设置的priority(这个值决定扫描多少分之一的链表元素)为0,且swappiness非0,则可能会进行swap
代码:
if (!sc->priority && vmscan_swappiness(sc)) {
scan_balance = SCAN_EQUAL;
goto out;
}
4.如果是全局页回收,并且当前空闲内存和所有file based链表page数目的加和都小于系统的high watermark,则必须进行匿名页回收,则必然会发生swap
代码:
anon = get_lru_size(lruvec,
LRU_ACTIVE_ANON) +
get_lru_size(lruvec,
LRU_INACTIVE_ANON);
file = get_lru_size(lruvec,
LRU_ACTIVE_FILE) +
get_lru_size(lruvec,
LRU_INACTIVE_FILE);
if (global_reclaim(sc)) {
free = zone_page_state(zone,
NR_FREE_PAGES);
if (unlikely(file + free <=
high_wmark_pages(zone))) {
scan_balance = SCAN_ANON;
goto out;
}
}
5.如果系统inactive file链表比较充足,则不考虑进行匿名页的回收,即不进行swap
代码:
if (!inactive_file_is_low(lruvec)) {
scan_balance = SCAN_FILE;
goto out;
}
注:每个zone有min/low/high 3个值,而high watermark指的是最后一个,这3个值依据vm.min_free_kbytes设置
3.2.2反向映射
回收物理页前需要解除所有关联该页的页表项,而共享内存中的页可能被多个进程引用,因此需要一种机制快速定位页表项;
2.4要遍历所有进程;
2.5引入反向映射,每个物理页维护一个页表项链表;
2.6引入基于对象的反向映射,每个物理页设置一个反向映射链表,链表节点为vm_area_struct结构,其通过mm_struct找到pgd进而找到相应页表项;
struct page {
atomic_t _mapcount; --初始值是-1,每增加一个使用者,该计数器加1
union {
……
struct {
……
struct address_space *mapping; --如果最低位置位,为指向anon_vma结构(用于匿名页面)的指针;否则为address_space指针(用于基于文件映射的页面)。
};
……
};
对于匿名页面来说,页面虽然可以是共享的,但是一般情况下,共享匿名页面的使用者的数目不会很多;而对于基于文件映射的页面来说,共享页面的使用者的数目可能会非常多,使用优先级搜索树这种结构可以更加快速地定位那些引用了该页面的虚拟内存区域。操作系统会为每一个文件都建立一个优先级搜索树,其根节点可以通过结构address_space中的i_mmap字段获取。
注:LRU缓存
页面根据其活跃程度会在active链表和inactive链表之间来回移动,如果要将某个页面插入到这两个链表中去,必须要通过自旋锁以保证对链表的并发访问操作不会出错。为了降低锁的竞争,Linux提供了一种特殊的缓存:LRU缓存,用以批量地向LRU链表中快速地添加页面。有了LRU缓存之后,新页不会被马上添加到相应的链表上去,而是先被放到一个缓冲区中去,当该缓冲区缓存了足够多的页面之后,缓冲区中的页面才会被一次性地全部添加到相应的LRU链表中去。
LRU缓存用到了pagevec结构,如下所示:
struct pagevec {
unsigned long nr;
unsigned long cold;
struct page *pages[PAGEVEC_SIZE];
};
lru_cache_add()和lru_cache_add_active()。前者用于延迟将页面添加到inactive链表上去,后者用于延迟将页面添加到active链表上去。这两个函数都会将要移动的页面先放到页向量pagevec中,当pagevec满了(已经装了14个页面的描述符指针),pagevec结构中的所有页面才会被一次性地移动到相应的链表上去。
参考资料