又一次开始土法炼钢了,测试部发现很多环境panic了,从黑匣子看到的信息是OOM了,虽然kill掉了占用比最大的进程,但是监控进程发现后reboot了单板。然后把meminfo收集到的信息,对比发现,slab增长的非常快,发现task_struct占用了很多,通过硬件中断和jprobe挂钩子发现是一个驱动访问所有进程的atomic_t usage,调用get_task_struct但没有put_task_struct释放usage,导致进行大量线程申请的时候不能自动释放,slab中的task_struct大量增加不释放。

具体代码如下:

static inline void put_task_struct(struct task_struct *t)
{
if (atomic_dec_and_test(&t->usage))
__put_task_struct(t);
}

前面这些都是前奏,解决问题后,测试部重新测试,仍发现有内存增长,拿来看发现task_struct增长的问题解决了,但size-512在大量增长。只好接着上jprobe抓取调用栈,把钩子挂到了slab分配函数kmem_cache_alloc,通过cachep->name过滤size-512打印调用栈,结果是直接将内核打爆了。

接着回头到/proc/slabinfo找线索,slab的增长体现在两个参数上,num_objs和num_slabs,num_objs是slab分配的对象个数,而num_slabs则是分配的slab个数,每个slab占用一个4K页。因为num_slab增长的较低,思考在这上面挂钩子处理,就要了解slab具体的分配机制。

slab本身是一种内存管理方法,更直接的概念是,它就是类似内存池的东西,由于内核进行大量的小内存(B级别)分配,如果每次都申请内存,进程延迟就非常厉害,那么就用内存池的方式提前分配好对应数据结构的内存,申请者只需要到内存池里面拿到对应指针即可。只是对应的每个内存池一般都是几个内存页的大小。

已size-512举例,size-512本身是内核申请通用的结构体(或者理解成内核slab没有特别标注的少数派),称为对象(obj),结构体大小在256~512B之间,所以通用slab本身存在内部碎片。那么一个内存页4K大小,可以容纳8个obj,那么预分配内存,初始化内存池的时候,此时称为slabs_empty,此时/proc/slabinfo中num_slab++;如果系统只需要申请一个obj,就会空闲7个obj,此时称为slabs_partial,此时/proc/slabinfo中num_objs++;如果系统又申请8个obj,原slab全部分配不够用,称为slabs_full,重新申请slab 4K,那么num_slab++。需要留意的是,slab obj所谓的释放,也只是将相关指针置空,并不将申请的物理内存释放,而且由于obj在内核中基本都是高频使用的,不释放的有很高复用率,对于内存申请有很高的效率。只有当drop_cache内存紧张的时候,slabs_empty才会被释放物理内存。

下面说一下slab的具体组织形式

struct kmem_cache {
/* 1) per-cpu data, touched during every alloc/free */struct array_cache *array[NR_CPUS];
/* 2) Cache tunables. Protected by cache_chain_mutex */unsigned int batchcount;
unsigned int limit;
unsigned int shared;

unsigned int buffer_size;
u32 reciprocal_buffer_size;
/* 3) touched by every alloc & free from the backend */
unsigned int flags;/* constant flags */unsigned int num;/* # of objs per slab */
/* 4) cache_grow/shrink *//* order of pgs per slab (2^n) */unsigned int gfporder;

/* force GFP flags, e.g. GFP_DMA */gfp_t gfpflags;

size_t colour;/* cache colouring range */unsigned int colour_off;/* colour offset */struct kmem_cache *slabp_cache;
unsigned int slab_size;
unsigned int dflags;/* dynamic flags */
/* constructor func */void (*ctor)(void *obj);

/* 5) cache creation/removal */const char *name;
struct list_head next;

/* 6) statistics *//*
 * We put nodelists[] at the end of kmem_cache, because we want to size
 * this array to nr_node_ids slots instead of MAX_NUMNODES
 * (see kmem_cache_init())
 * We still use [MAX_NUMNODES] and not [1] or [0] because cache_cache
 * is statically defined, so we reserve the max number of nodes.
 */struct kmem_list3 *nodelists[MAX_NUMNODES];
/*
 * Do not add fields after nodelists[]
 */};

name就是slab obj的名字,如size-512;next指向下一个slab类型,如task_struct;kmem_list3则是slab的三个链表,即

struct kmem_list3 {
struct list_head slabs_partial;/* partial list first, better asm code */struct list_head slabs_full;
struct list_head slabs_free;
unsigned long free_objects;
unsigned int free_limit;
unsigned int colour_next;/* Per-node cache coloring */spinlock_t list_lock;
struct array_cache *shared;/* shared per node */struct array_cache **alien;/* on other nodes */unsigned long next_reap;/* updated without locking */int free_touched;/* updated without locking */};

那么就可轻易看到slab的组织形式如下:

slab-oenhan

下面继续看问题,从上了解到监控num_slab的打印估计比监控num_objs少上7倍,决定在num_slab上挂钩子。

排查代码发现,obj通过kmem_cache_alloc函数申请,通过__cache_alloc >> __do_cache_alloc >> ____cache_alloc依次进入到正题,通过PER_CPU变量array,传递给ac_get_obj获取obj,如果obj没有获取到,此时是array cache中获取不到obj了,则通过cache_alloc_refill重新分配slab到array cache,cache_alloc_refill遍历kmem_list3中半满和空闲链表,发现都没有节点可以供
分配,则必须进行slab内存重新申请物理内存。

if (entry == &l3->slabs_partial) {
l3->free_touched = 1;
entry = l3->slabs_free.next;
if (entry == &l3->slabs_free)
goto must_grow;
}

slab重新申请内存由cache_grow完成,具体交给kmem_getpages处理,那么挂钩子就在kmem_getpages上,没有export,只能通过kallsyms_lookup_name函数获取函数地址。

此时调用栈打印出来了,绝大部分都是相同的一个栈,但排查了一下,对应的A函数根本没有内存泄露,百思不得其解中,经过同事提醒,才发觉A函数会高频申请obj,而隐藏的泄露函数B的频率较低,每次B申请size-512都是obj供给,而A则很多次都没有命中slab,则kmem_getpages抓取的都是A函数。

只好还是回头抓取kmem_cache_alloc,手动降低打印频率,和kmem_cache_free对比看,才发现了问题点,ipmi的一个驱动有内存泄露,原来用户态的一个C进程每次都通过ipmi发消息给D端,而D端有个低概率的问题导致返回报文超长,C通过ipmi算出报文异常,则不接受,该异常报文则存储在内存中,仍排在ipmi消息队列前面,后面仍有报文来临,C仍取第一个异常报文,导致所有返回报文阻塞在ipmi侧,体现为ipmi内存泄露。

修改方法则是解决D的bug,将C设置为截断获取报文的方式。

另外谈内存泄露,加上自己的一些土方法,在meminfo中,有效使用的物理内存=total-free-buffer-cache,虽然不完全准确,也可以此判断内存泄露情况,如果匿名页增长,则对比ps看哪进程RSS占用高,slab变化大,则要看哪一个slab变化大,然后用jprobe等三八大盖解决,当然,有条件的还是用美式武器kmemcheck搞定。

总结一下对jprobe等工具的使用感想:

抄袭一下初中课本的一段,福特汽车公司的一台马达发生故障,怎么也修不好,只好请一个叫斯坦曼的人来修。他看了一会,指着电机的某处说:“这儿的线圈多了16圈。”果然,把16圈线去掉后,电机马上运转正常。福特公司经理问斯坦门茨要多少酬金,斯坦门茨说:“1万美元。”1万美元?就只简简单单画了一条线!斯坦门茨看大家迷惑不解,转身开了个清单:画一条线,1美元;知道在哪儿画线,9999美元。福特公司经理看了之后,不仅照价付酬,还邀请斯坦曼到自己公司来。

工具就是划线而已,真正的价值在于知道在哪里划线。


从size-512内存泄露看slab分配来自于OenHan

链接为:https://oenhan.com/size-512-slab-kmalloc

发表回复