本文代码基于linux 4.19.195
继之前出现了业务运行一段时间后,从meminfo中看到slab占用的内存增加的问题后,最近同事又发现了业务运行一段时间,percpu这一项占用的内存增多的问题。
根据同事提供的数据,未运行业务时,percpu占用140M内存,运行一段时间业务然后终止业务,从meminfo中看到percpu占用了460M的内存。
percpu原理可以参考文献。
因为是系统启动后运行一段时间业务,导致的percpu内存占用增加,我们重点关注动态percpu变量的申请和释放的过程。
动态percpu变量的内存,通过函数pcpu_alloc()进行分配。
代码比较长,其基本逻辑是这样的:首先确认是给ko中定义的percpu变量分配内存还是给动态percpu变量分配内存,若是给动态percpu变量分配内存,则跳转到restart便签处。
从restart标签处,会遍历slot数组中对应需要分配的内存size的链表上的chunk,寻找哪个chunk有足够的空间。其中,slot是一个链表头数组,每个数组项是一个链表头,链表上挂着一个个chunk。chunk是用来管理percpu变量内存的结构体。
找到了chunk之后,便会goto area_found,从chunk上分配动态percpu内存给应用使用。
若没找到适合的chunk,则会创建一个新的chunk,新创建出来的chunk上的所有内存都是未被分配的,所以肯定可以从这个chunk中分配到需要的内存大小(实际上,新建chunk获得的是一片vmalloc内存段中的虚拟地址空间,后续从chunk分配内存时,才会从伙伴系统中获取到物理内存并将其映射到相应的虚拟内存,这里是为了简单起见才这么说)。

/*** pcpu_alloc - the percpu allocator* @size: size of area to allocate in bytes* @align: alignment of area (max PAGE_SIZE)* @reserved: allocate from the reserved chunk if available* @gfp: allocation flags** Allocate percpu area of @size bytes aligned at @align.  If @gfp doesn't* contain %GFP_KERNEL, the allocation is atomic. If @gfp has __GFP_NOWARN* then no warning will be triggered on invalid or failed allocation* requests.** RETURNS:* Percpu pointer to the allocated area on success, NULL on failure.*/
static void __percpu *pcpu_alloc(size_t size, size_t align, bool reserved,gfp_t gfp)
{*******restart:/* search through normal chunks */for (slot = pcpu_size_to_slot(size); slot < pcpu_nr_slots; slot++) { //根据需要分配内存块的大小索引slot数组找到对应链表list_for_each_entry(chunk, &pcpu_slot[slot], list) {off = pcpu_find_block_fit(chunk, bits, bit_align,is_atomic);if (off < 0)//在该链表中进一步寻找符合尺寸要求的chunkcontinue;off = pcpu_alloc_area(chunk, bits, bit_align, off); //从该chunk分配出size大小的空间,返回该size空间在chunk中的偏移量off;然后重新将该chunk挂到slot数组对应链表中if (off >= 0)goto area_found;}}spin_unlock_irqrestore(&pcpu_lock, flags);/** No space left.  Create a new chunk.  We don't want multiple* tasks to create chunks simultaneously.  Serialize and create iff* there's still no empty chunk after grabbing the mutex.*///看上面注释,No space left.  Create a new chunk.if (is_atomic) {err = "atomic alloc failed, no space left";goto fail;}if (list_empty(&pcpu_slot[pcpu_nr_slots - 1])) {chunk = pcpu_create_chunk(pcpu_gfp); //创建一个新的chunk,这里进行的是虚拟地址空间的分配if (!chunk) {err = "failed to allocate new chunk";goto fail;}spin_lock_irqsave(&pcpu_lock, flags);pcpu_chunk_relocate(chunk, -1); //把一个全新的chunk挂到slot数组对应链表中} else {spin_lock_irqsave(&pcpu_lock, flags);}goto restart;area_found:pcpu_stats_area_alloc(chunk, size);spin_unlock_irqrestore(&pcpu_lock, flags);/* populate if not all pages are already there */if (!is_atomic) {int page_start, page_end, rs, re;page_start = PFN_DOWN(off);page_end = PFN_UP(off + size);pcpu_for_each_unpop_region(chunk->populated, rs, re, //遍历unpopulate的regionpage_start, page_end) {WARN_ON(chunk->immutable);ret = pcpu_populate_chunk(chunk, rs, re, pcpu_gfp); //populate该段区域spin_lock_irqsave(&pcpu_lock, flags);if (ret) {pcpu_free_area(chunk, off);err = "failed to populate";goto fail_unlock;}pcpu_chunk_populated(chunk, rs, re, true);/* 将chunk->populated已映射过物理内存的区域设置为1 */spin_unlock_irqrestore(&pcpu_lock, flags);}mutex_unlock(&pcpu_alloc_mutex);}if (pcpu_nr_empty_pop_pages < PCPU_EMPTY_POP_PAGES_LOW)pcpu_schedule_balance_work();/* clear the areas and return address relative to base address */for_each_possible_cpu(cpu)memset((void *)pcpu_chunk_addr(chunk, cpu, 0) + off, 0, size);/*
//chunk->base_addr + off表示分配该size空间的起始percpu内存地址//最终返回的地址即__per_cpu_start+off,即得到该动态分配percpu变量在内核镜像中的一个虚拟内存地址。//实际上该动态分配percpu变量并不在此地址上,只是为了以后通过per_cpu(var, cpu)引用该变量时,//与静态percpu变量一致,因为静态percpu变量在内核镜像中是有分配内存虚拟地址的(在.data..percpu段中)。//使用per_cpu(var, cpu)时,该动态分配percpu变量的内核镜像中的虚拟地址(假的地址,为了跟静态percpu变量一致),加上本cpu所在percpu空间与.data..percpu段的偏移量,//即得到该动态分配percpu变量在本cpu副本中的内存地址
*/ptr = __addr_to_pcpu_ptr(chunk->base_addr + off);kmemleak_alloc_percpu(ptr, size, gfp);trace_percpu_alloc_percpu(reserved, is_atomic, size, align,chunk->base_addr, off, ptr);return ptr;fail_unlock:spin_unlock_irqrestore(&pcpu_lock, flags);
fail:trace_percpu_alloc_percpu_fail(reserved, is_atomic, size, align);if (!is_atomic && do_warn && warn_limit) {pr_warn("allocation failed, size=%zu align=%zu atomic=%d, %s\n",size, align, is_atomic, err);dump_stack();if (!--warn_limit)pr_info("limit reached, disable warning\n");}if (is_atomic) {/* see the flag handling in pcpu_blance_workfn() */pcpu_atomic_alloc_failed = true;pcpu_schedule_balance_work();} else {mutex_unlock(&pcpu_alloc_mutex);}return NULL;
}

虽然系统在启动早期会预留部分内存给percpu变量使用,但是预留的内存随着系统的运行而不足,从而会触发新的chunk的申请操作,以获得新的percpu内存变量的空间。
那么,动态percpu变量在释放的时候,是否会把相应的物理内存还给os呢?
我们来看动态percpu内存的释放函数。

/*** free_percpu - free percpu area* @ptr: pointer to area to free** Free percpu area @ptr.** CONTEXT:* Can be called from atomic context.*/
void free_percpu(void __percpu *ptr)
{void *addr;struct pcpu_chunk *chunk;unsigned long flags;int off;bool need_balance = false;if (!ptr)return;kmemleak_free_percpu(ptr);addr = __pcpu_ptr_to_addr(ptr);spin_lock_irqsave(&pcpu_lock, flags);chunk = pcpu_chunk_addr_search(addr);off = addr - chunk->base_addr;pcpu_free_area(chunk, off);/* if there are more than one fully free chunks, wake up grim reaper */if (chunk->free_bytes == pcpu_unit_size) {struct pcpu_chunk *pos;list_for_each_entry(pos, &pcpu_slot[pcpu_nr_slots - 1], list)if (pos != chunk) {need_balance = true;break;}}trace_percpu_free_percpu(chunk->base_addr, off, ptr);spin_unlock_irqrestore(&pcpu_lock, flags);if (need_balance)pcpu_schedule_balance_work();
}
EXPORT_SYMBOL_GPL(free_percpu);

乍一眼看过去,好像并没有归还物理内存的动作。不过,在函数的最后,发现函数pcpu_schedule_balance_work()会唤醒一个工作,其执行的函数是pcpu_balance_workfn()。
省略不必要的代码,我们只看重点。
首先,函数注释的第二段第一句话,Reclaim all fully free chunks except for the first one,说明是会有条件将物理内存还给伙伴系统的,什么条件呢?

  1. fully free chunk
  2. except for the first one
    也就是说,会将除了第一个之外的fully free chunk的物理内存还给os。代码逻辑很简单,这里就不再多讲了。
/*** pcpu_balance_workfn - manage the amount of free chunks and populated pages* @work: unused** Reclaim all fully free chunks except for the first one.  This is also* responsible for maintaining the pool of empty populated pages.  However,* it is possible that this is called when physical memory is scarce causing* OOM killer to be triggered.  We should avoid doing so until an actual* allocation causes the failure as it is possible that requests can be* serviced from already backed regions.*/
static void pcpu_balance_workfn(struct work_struct *work)
{*********struct list_head *free_head = &pcpu_slot[pcpu_nr_slots - 1];list_for_each_entry_safe(chunk, next, free_head, list) {WARN_ON(chunk->immutable);/* spare the first one */ //跳过第一个if (chunk == list_first_entry(free_head, struct pcpu_chunk, list))continue;list_move(&chunk->list, &to_free); //加入到to_free链表}spin_unlock_irq(&pcpu_lock);list_for_each_entry_safe(chunk, next, &to_free, list) { //遍历to_free链表int rs, re;pcpu_for_each_pop_region(chunk->populated, rs, re, 0,chunk->nr_pages) {pcpu_depopulate_chunk(chunk, rs, re); //depopulatespin_lock_irq(&pcpu_lock);pcpu_chunk_depopulated(chunk, rs, re); //修改统计信息spin_unlock_irq(&pcpu_lock);}pcpu_destroy_chunk(chunk);cond_resched();}*********
}

根据上面的分析,我们知道,动态percpu的内存,在应用需要的时候会从buddy去分配,在应用释放的时候,并没有直接释放内存,而是将其缓存在内存中,后续按照“保留一个完全free的chunk”的策略来将某些完全free的chunk使用的内存进行释放。因而,和slab内存占用的问题类似,若系统运行时间较长,出现了内存碎片,则有可能出现多个动态percpu对象零散的分散在多个chunk上,导致chunk上所有的内存都无法被回收,进而导致meminfo中percpu占用内存上升的问题。
那么,是否有接口可以像slab的shrink接口一样,将所有未使用的物理内存释放给os呢?作者找了一下内核代码,并没有发现有相关接口。
从这两个问题的经验来看,内核在避免以4K位单位的内存碎片做了巨大的努力,比如cma、内存compact、将内存按可移动性分区等等,但是,对于这些小内存的碎片,似乎没有花费太大的精力。另外,我们从alloc_page接口申请内存时,在内存不足的情况下,内核可能会通过页迁移、回收用户态程序的文件页、匿名页,以及调用shrinker接口来释放一定的内存,但是,并没有去做unreclaim slab内存以及percpu内存的回收。按笔者的环境来做实验,笔者16G内存的环境上slab内存占用达到了1.1G,通过写ko的方式遍历了系统中的所有slab(percpu的没去看了,感觉free的内存应该也不少),发现至少有300M内存是可以还给伙伴系统的。试想,如果在接近oom时,能拿这300M中一半内存来急救一下,大概率可以避免OOM的发生,但是不知道内核的设计者为什么没把这块内存在oom前进行回收利用。
每天进步一点点~~~

一个疑似percpu内存泄漏问题排查相关推荐

  1. 一次疑似 JVM native 内存泄漏的排查实录

    最近开发同学反馈,某定时任务服务疑似有内存泄漏,整个进程的内存占用比 Xmx 内存大不少,而且看起来是缓慢上升的,做了下面这次分析,包括下面的内容: 分析 JVM native 内存的一些常见思路 内 ...

  2. 一次完整的JVM堆外内存泄漏故障排查记录

    前言 记录一次线上JVM堆外内存泄漏问题的排查过程与思路,其中夹带一些JVM内存分配机制以及常用的JVM问题排查指令和工具分享,希望对大家有所帮助. 在整个排查过程中,我也走了不少弯路,但是在文章中我 ...

  3. Java内存泄漏的排查

    1.内存溢出 一种通俗的说法. 1.内存溢出:你申请了10个字节的空间,但是你在这个空间写入11或以上字节的数据,出现溢出. 2.内存泄漏:你用new申请了一块内存,后来很长时间都不再使用了(按理应该 ...

  4. python 内存泄漏的排查

    python 内存泄漏的排查 判断该次上线或发版的内容,排查到具体上线了那些接口或修改了那些接口 单起一个服务,使用ps -aux |grep pid 查看该进程占用的内存大小 work@xxx:~$ ...

  5. 一个跨平台的 C++ 内存泄漏检测器

    From:http://www.ibm.com/developerworks/cn/linux/l-mleak2/index.html 内存泄漏对于C/C++程序员来说也可以算作是个永恒的话题了吧.在 ...

  6. 一次 Java 内存泄漏的排查

    由来 前些日子小组内安排值班,轮流看顾我们的服务,主要做一些报警邮件处理.Bug 排查.运营 issue 处理的事.工作日还好,无论干什么都要上班的,若是轮到周末,那这一天算是毁了. 不知道是公司网络 ...

  7. jstat 内存泄漏_一次Java内存泄漏的排查!要了自己的老命!

    点击上方"Java之间",选择"置顶或者星标" 你关注的就是我关心的! 作者:枕边书 来源:https://zhenbianshu.github.io 一.由来 ...

  8. java 内存泄露对象排查_记录一次 java内存泄漏的排查

    1.问题:jar进程会随着时间由 30% 上涨到 70% 直到虚机报警.重启过后,还是会缓慢上涨: 进程数也在上涨: 2.在排查内存问题时,可能会使用的命令 1)查看 java 进程:jps -l 可 ...

  9. 分享一次 Java 内存泄漏的排查

    由来 前些日子小组内安排值班,轮流看顾我们的服务,主要做一些报警邮件处理.Bug 排查.运营 issue 处理的事.工作日还好,无论干什么都要上班的,若是轮到周末,那这一天算是毁了. 不知道是公司网络 ...

最新文章

  1. 使用OpenCV与sklearn实现基于词袋模型的图像分类预测与搜索
  2. NAT的完全分析及其UDP穿透的完全解决方案
  3. 神经网络分类支持子文件夹
  4. KMP算法的来龙去脉
  5. 神策数据司沛:加速车企数字化转型,搭建高质量用户数据平台实战
  6. CDHtmlDialog 与 网页交互技巧
  7. 计算机基础知识教程算法,快速掌握!计算机二级公共基础知识教程:算法
  8. 内联函数及4种显示转化
  9. ACDsee_14中文许可证秘钥
  10. php smarty配置,PHP中使用Smarty模板目录结构配置
  11. deep-text-recognition-benchmark 项目训练data.mdb数据集,运行日志中,只显示训练了英文和数字
  12. eleme 分页组件更新
  13. Hive 内置函数及自定义函数
  14. Spark-NLP:大规模自然语言理解
  15. 中国移动云mas短信http协议对接
  16. 容器内存溢出排障思路
  17. ArcGIS 发布GP服务
  18. 对苹果输入法产品评价
  19. 计算机专业报考小学语文老师,各位大神,我是今年的专科应届毕业生,计算机应用专业,想当小学语文老师,考教师资格证对专业有限制吗?...
  20. 夜光带你走进python开发 (三十九)传奇语言

热门文章

  1. 接口测试框架3之httprunnerV3入门以及HttpRunner安装详解
  2. 深度学习:2016年的进展综述及2017年的预测
  3. 周瑜-曲有误,周郎顾
  4. stm32开发3D打印机(六)——使用FATFS文件系统读取打印文件 获取信息 执行转换 转换坐标(上)
  5. 类似货拉拉的货运APP开发软件怎么做
  6. IOS 京东相关app 出现“网络请求失败,请检查您的网络设置”的解决办法
  7. 金山网盾3.6率先支持阿里旺旺 护航网购用户
  8. Android zxing扫码截彩色图
  9. JavaScript利用onblur事件实现文本框中英文字母全部转换成大写
  10. SPI FLASH 读取指令read data和fast read data的区别