概述
内存回收,是内核在内存不足的情况下,一种释放内存的方法,在4.4.198内核里,主要有两个函数涉及内存回收,一个是kswapd,一个是drop_cache;
先说说kswapd,这是一个内核线程,每个内存节点都有一个这样的内核线程,名字是kswapd%d;在alloc_page函数中用低水位分配内存失败后,进入慢速路径的第一件事情
static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
struct alloc_context *ac)
{
..............
if (gfp_mask & __GFP_KSWAPD_RECLAIM)
wake_all_kswapds(order, ac);
..............
`}
我们知道,内核线程的函数类型是int (*fun)(void *),那么给kswapd的传参是什么呢?
int kswapd_run(int nid)
{
pg_data_t *pgdat = NODE_DATA(nid);
int ret = 0;
if (pgdat->kswapd)
return 0;
pgdat->kswapd = kthread_run(kswapd, pgdat, "kswapd%d", nid);
嗯,把pgdat传给了kswapd线程 ,kswapd主要用到了pg_data_t结构体的两个成员
typedef struct pglist_data {
...
int kswapd_max_order;
enum zone_type classzone_idx;
...
} pg_data_t;
这两个成员,一个是alloc_page时传入的order值,一个是第一个最适合分配的zone序号。kswpad内核线程会根据这两个参数,去判断现在是否要继续进行内存回收,详情见pgdat_balanced函数,kswapd函数要回收到内存高水位之上才停止回收动作也在这个函数里面体现出来了。
有意思的是,swapd的内存回收方向(假设我的gfp_mask是GFP_HIGHUSER_MOVABLE),是从zone_normal开始,到zone_highmem结束,而alloc_page函数是从zone_highmem开始,到zone_normal结束,方向是相反的,这样做的好处是,可以减少获取锁的冲突;试想,如果两者方向相同,那必然导致争抢同一把锁的概率大大提升。
kswapd主要使用三个手段去回收清理内存空间:
- 回收lru链表,具体请查看shrink_lruvec函数
- 调用shrink_slab从而调用注册进入系统的shrinker以回收内存
- 可能会调用compact_pgdat进行内存规整操作
看到后面就会知道,kswapd实现内存回收的方法和drop_cache是不一样的。
kswapd的实现,这里就不讲解了,网上已经有非常多写的很好的文章,主要过程就是,要么让出CPU进行睡眠;要么从ZONE_HIGHMEM->ZONE_NORMAL(依然假设我的gfp_mask是GFP_HIGHUSER_MOVABLE)找到第一个不平衡的zone,然后从ZONE_NORMAL一直进行内存回收直到这个zone,内存回收的操作有shrink_lruvec和shrink_slab,对每个zone进行了内存回收的操作后,可能会调用compact_pgdat进行一个异步的内存规整操作。
下面来聊聊drop_cache,具体实现的函数是drop_caches_sysctl_handler,他能够接收的参数是1-4,嗯,4是个神奇的参数,我之前一直以为只是能传1-3,具体4是干嘛用的,自己看代码吧。
int drop_caches_sysctl_handler(struct ctl_table *table, int write,
void __user *buffer, size_t *length, loff_t *ppos)
{
int ret;
ret = proc_dointvec_minmax(table, write, buffer, length, ppos);
if (ret)
return ret;
if (write) {
static int stfu;
if (sysctl_drop_caches & 1) {
iterate_supers(drop_pagecache_sb, NULL);
count_vm_event(DROP_PAGECACHE);
}
if (sysctl_drop_caches & 2) {
drop_slab();
count_vm_event(DROP_SLAB);
}
if (!stfu) {
pr_info("%s (%d): drop_caches: %dn",
current->comm, task_pid_nr(current),
sysctl_drop_caches);
}
stfu |= sysctl_drop_caches & 4;
}
return 0;
}
我们知道,当我们给drop_cache传入参数1时,释放的是pagecache,那么,具体是怎么释放的呢?内核是这样做的,遍历每个super_block实例,每个super_block实例又有一个链表把该super_block实例下的inode链在了一起,然后,遍历每个inode,每个inode有成员变量i_mapping指向该inode的address_space,address_space里面有一颗基数树,用于存放属于这个inode的pagecache,内核通过这个方法找到了所有pagecache,然后把可以释放的pagecache就给释放掉了;与kswapd线程对比,这个方法显然暴力了很多,毕竟通过lru链表进行释放,至少是用的两次机会法,不见得一次性就把这些pagecache给回收回来。
实现的代码也很清晰
static void drop_pagecache_sb(struct super_block *sb, void *unused)
{
struct inode *inode, *toput_inode = NULL;
spin_lock(&sb->s_inode_list_lock);
list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
spin_lock(&inode->i_lock);
/*
* We must skip inodes in unusual state. We may also skip
* inodes without pages but we deliberately won't in case
* we need to reschedule to avoid softlockups.
*/
if ((inode->i_state & (I_FREEING|I_WILL_FREE|I_NEW)) ||
(inode->i_mapping->nrpages == 0 && !need_resched())) {
spin_unlock(&inode->i_lock);
continue;
}
__iget(inode);
spin_unlock(&inode->i_lock);
spin_unlock(&sb->s_inode_list_lock);
cond_resched();
invalidate_mapping_pages(inode->i_mapping, 0, -1);
iput(toput_inode);
toput_inode = inode;
spin_lock(&sb->s_inode_list_lock);
}
spin_unlock(&sb->s_inode_list_lock);
iput(toput_inode);
}
当我们给drop_cache传入参数2时,内核实际上就调用了注册进来的shrinker,调一把回调,就把内存释放了。值得一提的是,对于inode_cache和dentry_cache,都向系统注册了shrinker,这时候就会把inode_cache和dentry_cache给清理了。
最后
以上就是糟糕高山为你收集整理的谈谈自己对内存回收的理解的全部内容,希望文章能够帮你解决谈谈自己对内存回收的理解所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复