发生 SLAB 内存泄漏该怎么办

通常应用程序主要通过类似 malloc 等标准函数来进行内存的分配使用, 不过在 Linux 中, 内核无法使用标准函数, 一般通过 SLAB Allocator 机制来进行内存的分配. 各个子系统, 驱动以及内核模块等都可以通过 SLAB Allocator 机制分配内存, 同时该机制还可以当作缓存来使用, 这点主要是针对经常分配并释放的对象. 从内核 2.6 版本开始, slab 分配器有两个替代分配器:

分配器 用途
slob 主要用于嵌入式系统, 代码量少, 算法简单
slub 主要用于服务器等大型系统, 优化内存开销

更多见: slab/slob/slub

通常的服务器系统都使用 slub 分配器, 内核, 模块或驱动用完内存后都需要释放掉内存, 如果一直占用着内存, 系统可能会频繁的 OOM(Out of Memory) 杀掉用户空间的进程, 更有可能耗尽系统内存引起内核崩溃. 在如下信息中, 系统已经没有多少内存可用, 其中 SUnreclaimslab 的一部分, 占据了大量的内存:

# 系统环境

Dell R620
MEM 64G
Centos 7
kernel-3.10.0-862.el7.x86_64


# less /proc/meminfo 
MemTotal:       65759220 kB
MemFree:          294356 kB
MemAvailable:     110732 kB
Buffers:            4264 kB
Cached:          1746492 kB
...
Slab:           63117636 kB
SReclaimable:      55500 kB
SUnreclaim:     63062136 kB

SUnreclaim 是不可回收状态, 它几乎占用了整个系统内存, 通过 slabtop 命令来看:

# slabtop -sc
 Active / Total Objects (% used)    : 63476486 / 63523309 (99.9%)
 Active / Total Slabs (% used)      : 1996655 / 1996655 (100.0%)
 Active / Total Caches (% used)     : 80 / 110 (72.7%)
 Active / Total Size (% used)       : 63100006.52K / 63116476.68K (100.0%)
 Minimum / Average / Maximum Object : 0.01K / 0.99K / 12.69K

  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME                   
62983672 62983275  99%    1.00K 1985241       32  63527712K kmalloc-1024
 25060  12262  48%    0.57K    895       28     14320K radix_tree_node
 17600  16523  93%    0.58K    320       55     10240K inode_cache

kmalloc-1024 即表示内核空间在分配 1024 字节大小的内存, 总共占据了接近 62G 的内存. 这可能意味着内核子系统, 驱动或者模块存在分配内存但不释放的行为. 如何诊断这种问题? 可以通过以下几种方式找到一些有用的线索:

通过 debugfs 查找线索

内核开发者应该对 debugfs 很熟悉, 我们可以将很多内核空间的状态信息暴露到用户空间. 通过 debugfs 我们可以很方便的找出哪些程序分配了 1024 字节的内存, 如下所示:

# mount -t debugfs none /sys/kernel/debug

// 增加过滤规则, 指定 bytes_alloc 为 1024 字节
# echo "bytes_alloc == 1024" > /sys/kernel/debug/tracing/events/kmem/kmalloc/filter

// 开启事件跟踪
# echo 1 > /sys/kernel/debug/tracing/events/kmem/kmalloc/enable

// 经过一段时间后关闭
echo 0 > /sys/kernel/debug/tracing/events/kmem/kmalloc/enable

// 获取信息
# cat /sys/kernel/debug/tracing/trace > /tmp/kmem.out

/tmp/kmem.out 即可发现不少信息, 如下所示:

# less /tmp/kmem.out 
# tracer: nop
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
...
       cmf-agent-3768  [010] .... 8824626.658432: kmalloc: call_site=ffffffff951b4836 ptr=ffff8c1be58e9000 bytes_req=936 bytes_alloc=1024 gfp_flags=GFP_KERNEL
            bash-844   [000] .... 8824683.769449: kmalloc: call_site=ffffffff95224c73 ptr=ffff8c16ef3bb800 bytes_req=640 bytes_alloc=1024 gfp_flags=GFP_KERNEL|GFP_ZERO
            bash-844   [000] .... 8824683.769468: kmalloc: call_site=ffffffff9512ee23 ptr=ffff8c16ef3be400 bytes_req=920 bytes_alloc=1024 gfp_flags=GFP_KERNEL|GFP_ZERO
       cmf-agent-3768  [000] .... 8824686.715482: kmalloc: call_site=ffffffff951b4836 ptr=ffff8c16ef3b9400 bytes_req=936 bytes_alloc=1024 gfp_flags=GFP_KERNEL
 java_version.sh-2526  [014] .... 8824686.805563: kmalloc: call_site=ffffffff9512ee23 ptr=ffff8c1727fbc400 bytes_req=920 bytes_alloc=1024 gfp_flags=GFP_KERNEL|GFP_ZERO
            java-2529  [015] .... 8824686.820292: kmalloc: call_site=ffffffff9512ee23 ptr=ffff8c21a214b800 bytes_req=920 bytes_alloc=1024 gfp_flags=GFP_KERNEL|GFP_ZERO
         systemd-1     [020] .... 8824687.815762: kmalloc: call_site=ffffffff9512ee23 ptr=ffff8c180afbfc00 bytes_req=920 bytes_alloc=1024 gfp_flags=GFP_KERNEL|GFP_ZERO
...

跟踪的时间越长, 发现线索的概率就越大, 上述的信息中可以看到有不少用户空间的进程, 包括systemd 都进行了内存分配. 如果从上述信息中找到了可疑的信息, 可以关闭这些 TASK 对应的程序或内核模块, 如果找不到则可能是内核子系统, 驱动引起的原因. 上述信息中由于已经没有多少可用内存, 跟踪的时间也比较短, 所以没有发现可疑的线索. 同类问题可以参考 kmalloc-1024 slab caches take all resources.

通过 slub 查找线索

可以通过 slub debug 来追踪 kmalloc-1024 的内存分配是否存在内存泄漏的行为. 目前大多数发行版都开启了编译选项 CONFIG_SLUB_DEBUG, 我们可以在运行时通过命令 echo 1 > /sys/kernel/slab/<leaking_slab>/trace 开启 debug 调试, 这种方式对将进程的堆栈也打印出来, 一般输出到系统 messages 或者 console, 如下所示:

# echo 1 > /sys/kernel/slab/kmalloc-1024/trace

// 经过一段时间后在关闭调试
# echo 0 > /sys/kernel/slab/kmalloc-1024/trace

备注: 该方式会输出大量的信息, 主机可能因为系统日志 buffer 的原因出现卡慢的现象, 实际使用中建议通过以下命令执行:

# echo 1 > /sys/kernel/slab/kmalloc-1024/trace  && sleep <n> && echo 0 > /sys/kernel/slab/kmalloc-1024/trace

n 为数字, 建议 30 以内, 表示 sleep 多少秒, 不建议设置的很大.

开启调试后, 可以在 console 或系统日志中看到类似下面的信息:

Sep 16 16:27:26 cztest kernel: CPU: 14 PID: 56451 Comm: bash Kdump: loaded Tainted: P           OE  ------------   3.10.0-862.14.4.el7.x86_64 #1
Sep 16 16:27:26 cztest kernel: Hardware name: Dell Inc. PowerEdge R620/0D2D5F, BIOS 2.5.4 01/22/2016
Sep 16 16:27:26 cztest kernel: Call Trace:
Sep 16 16:27:26 cztest kernel: [<ffffffff99f13754>] dump_stack+0x19/0x1b
Sep 16 16:27:26 cztest kernel: [<ffffffff99f108b8>] free_debug_processing+0x1ca/0x259
Sep 16 16:27:26 cztest kernel: [<ffffffff99a292f0>] ? free_pipe_info+0x90/0xa0
Sep 16 16:27:26 cztest kernel: [<ffffffff99a292f0>] ? free_pipe_info+0x90/0xa0
Sep 16 16:27:26 cztest kernel: [<ffffffff999fa000>] __slab_free+0x250/0x2f0                         --> 释放 slab
Sep 16 16:27:26 cztest kernel: [<ffffffff99c27080>] ? tty_check_change.part.10+0xf0/0x100
Sep 16 16:27:26 cztest kernel: [<ffffffff99a292f0>] ? free_pipe_info+0x90/0xa0
Sep 16 16:27:26 cztest kernel: [<ffffffff999fa766>] kfree+0x106/0x140
Sep 16 16:27:26 cztest kernel: [<ffffffff99a292f0>] free_pipe_info+0x90/0xa0
Sep 16 16:27:26 cztest kernel: [<ffffffff99a29359>] put_pipe_info+0x59/0x60
Sep 16 16:27:26 cztest kernel: [<ffffffff99a29400>] pipe_release+0xa0/0xb0
Sep 16 16:27:26 cztest kernel: [<ffffffff99a214fc>] __fput+0xec/0x260
Sep 16 16:27:26 cztest kernel: [<ffffffff99a2175e>] ____fput+0xe/0x10
Sep 16 16:27:26 cztest kernel: [<ffffffff998bab8b>] task_work_run+0xbb/0xe0
Sep 16 16:27:26 cztest kernel: [<ffffffff9982bc55>] do_notify_resume+0xa5/0xc0
Sep 16 16:27:26 cztest kernel: [<ffffffff99f25ae4>] int_signal+0x12/0x17

没有 __slab_free 的堆栈可能就是内存泄漏的线索. 同类问题的处理见 诊断 SLUB 问题

通过 kmemleak 查找线索

kmemleak 通过追踪 kmalloc(), kmem_cache_alloc(), vmalloc() 等函数来判断内核是否存在内存泄漏. 目前大多数的发行版并没有开启编译选项 CONFIG_DEBUG_KMEMLEAK , 我们可以通过安装对应内核版本的 debug 版本来使用此特性. 这里不做具体的介绍, 详细示例可参考 kmemleak 检测内核内存泄漏debug-kernel-space-memory-leak.

通过 perf 查找线索

可以通过 perf 来跟踪 slab 内存分配的信息, 如下所示, 单独指定探针函数 kmem_cache_alloc 进行跟踪:

// 初始化, 清理已存在的探针函数
# perf probe -d kmem_cache_alloc*

// Centos 7 中执行
# perf probe kmem_cache_alloc      's->name:string' 2>/dev/null  

// 记录数据, 时间可以自己指定
# perf record -a -g -e probe:kmem_cache_alloc --filter 'name == "kmalloc-1024"' sleep 10

// 查看信息
# perf script
WTOplog.lThread 99877 [000] 22544738.707289: probe:kmem_cache_alloc: (ffffffff999fadc0) name="kmalloc-1024"
                  3fadc1 kmem_cache_alloc (/usr/lib/debug/lib/modules/3.10.0-862.14.4.el7.x86_64/vmlinux)
                  45b963 bio_alloc_bioset (/usr/lib/debug/lib/modules/3.10.0-862.14.4.el7.x86_64/vmlinux)
                   1095f ext4_bio_write_page ([ext4])
                    6c47 mpage_submit_page ([ext4])
                    6d70 mpage_process_page_bufs ([ext4])
                    7c75 mpage_prepare_extent_to_map ([ext4])
                    c617 ext4_writepages ([ext4])
                  3a3b81 do_writepages (/usr/lib/debug/lib/modules/3.10.0-862.14.4.el7.x86_64/vmlinux)
                  398405 __filemap_fdatawrite_range (/usr/lib/debug/lib/modules/3.10.0-862.14.4.el7.x86_64/vmlinux)
                  398551 filemap_write_and_wait_range (/usr/lib/debug/lib/modules/3.10.0-862.14.4.el7.x86_64/vmlinux)
                    34da ext4_sync_file ([ext4])
                  453277 do_fsync (/usr/lib/debug/lib/modules/3.10.0-862.14.4.el7.x86_64/vmlinux)
                  453583 sys_fdatasync (/usr/lib/debug/lib/modules/3.10.0-862.14.4.el7.x86_64/vmlinux)
                  92579b system_call (/usr/lib/debug/lib/modules/3.10.0-862.14.4.el7.x86_64/vmlinux)
                   f51ad [unknown] (/usr/lib64/libc-2.17.so)
                  b5adae __wt_log_force_sync (/opt/mongodb-linux-x86_64-3.6.15/bin/mongod)
                  b6152b __wt_log_flush (/opt/mongodb-linux-x86_64-3.6.15/bin/mongod)
                  abd5ae __session_log_flush (/opt/mongodb-linux-x86_64-3.6.15/bin/mongod)
                  a51778 mongo::WiredTigerSessionCache::waitUntilDurable (/opt/mongodb-linux-x86_64-3.6.15/bin/mongod)
                  a3eaa8 mongo::WiredTigerOplogManager::_oplogJournalThreadLoop (/opt/mongodb-linux-x86_64-3.6.15/bin/mongod)
                 23422d0 execute_native_thread_routine (/opt/mongodb-linux-x86_64-3.6.15/bin/mongod)
......

更多参考: track-slab-using-perf.

通过 systemtap 查找线索

在文章 [linux-dynamic-trace]/introduction_to_linux_dynamic_tracing/) 中, 我们提到了 systemtap 这个强悍的工具, 这里我们也可以使用 systemtap 来跟踪 slab 内存分配的堆栈情况. 如下所示增加脚本跟踪 kmem_cache_alloc 函数:

# cat kmem.stap
# This script displays the number of given slab allocations and the backtraces leading up to it.
# used with centos 7

global slab = @1
global stats, stacks
probe kernel.function("kmem_cache_alloc") {
        if (kernel_string($s->name) == slab) {
                stats[execname()] <<< 1
                stacks[execname(),kernel_string($s->name),backtrace()] <<< 1
        }
}
# Exit after 10 seconds
# probe timer.ms(10000) { exit () }
probe end {
        printf("Number of %s slab allocations by process\n", slab)
        foreach ([exec] in stats) {
                printf("%s:\t%d\n",exec,@count(stats[exec]))
        }
        printf("\nBacktrace of processes when allocating\n")
        foreach ([proc,cache,bt] in stacks) {
                printf("Exec: %s Name: %s  Count: %d\n",proc,cache,@count(stacks[proc,cache,bt]))
                print_stack(bt)
                printf("\n-------------------------------------------------------\n\n")
        }
}

执行输出如下:

# stap -v --all-modules kmem.stp kmalloc-1024
Pass 1: parsed user script and 476 library scripts using 273956virt/69332res/3516shr/65876data kb, in 710usr/70sys/795real ms.
Pass 2: analyzed script: 2 probes, 9 functions, 4 embeds, 3 globals using 426840virt/223480res/4820shr/218760data kb, in 3080usr/1510sys/4696real ms.
Pass 3: translated to C into "/tmp/stapd1lZps/stap_cf01a87fb02d496c7f93f7cfbd8898a7_7397_src.c" using 426840virt/223888res/5228shr/218760data kb, in 5860usr/260sys/6344real ms.
Pass 4: compiled C into "stap_cf01a87fb02d496c7f93f7cfbd8898a7_7397.ko" in 17840usr/2710sys/19353real ms.
Pass 5: starting run.
WARNING: Missing unwind data for a module, rerun with 'stap -d (unknown; retry with -DDEBUG_UNWIND)'
^CNumber of kmalloc-1024 slab allocations by process
xfsaild/sda2:   33
systemd-udevd:  2
gpg-agent:      2
kworker/u97:1:  1
......
......
-------------------------------------------------------

Exec: gpg-agent Name: kmalloc-1024  Count: 1
 0xffffffff8a01d630 : kmem_cache_alloc+0x0/0x1f0 [kernel]
 0xffffffff8a41eb99 : sk_prot_alloc+0x39/0x190 [kernel]
 0xffffffff8a41f82c : sk_alloc+0x2c/0xd0 [kernel]
 0xffffffff8a4f649d : unix_create1+0x4d/0x1a0 [kernel]
 0xffffffff8a4fa0fa : unix_stream_connect+0x9a/0x4a0 [kernel]
 0xffffffff8a41b96d : SYSC_connect+0xed/0x130 [kernel]
 0xffffffff8a41d5be : sys_connect+0xe/0x10 [kernel]
 0xffffffff8a576ddb : system_call_fastpath+0x22/0x27 [kernel]
 0x7ff070008d50 : 0x7ff070008d50

可以看到我们获取了进程及相应的堆栈信息, 提供了不少诊断的线索. 完整示例见: track-slab-with-systemtap.

总结说明

上述介绍的几种方式不一定能找出内存泄漏的根本原因, 不过会帮助我们获取一些有用的线索. 值得一提的是, 如果找到了相关的线索, 关闭对应的任务, 模块或驱动后, 内核不一定会释放这些内存, 可能只能观察到 kmalloc-1024 的数量不再增长, 这种情况下就只能靠重启系统释放内存.

参考

slab-allocator
kmalloc-1024 slab caches take all resources
keep track of slab leaks
track slab allocations using perf
track slab allocations with systemtap
debug-kernel-space-memory-leak
kmemleak 检测内核内存泄漏
诊断 SLUB 问题
诊断 SLAB 问题