问题描述
某设备运行 dpdk 程序,free 的内存空间在逐渐下降。
第一次查看:
[root]# free -h
total used free shared buff/cache available
Mem: 3.6G 1.5G 425M 403M 1.7G 534M
Swap: 0B 0B 0B
几分钟后查看:
[root]# free -h
total used free shared buff/cache available
Mem: 3.6G 1.2G 425M 403M 1.4G 534M
Swap: 0B 0B 0B
持续观察确定 free 的内存在【持续降低】。
问题定位过程
- 使用 top 查看用户态程序的占用的内存空间,未发现明显异常
- 执行 echo 3 > /proc/sys/vm/drop_caches 清除缓存,free 的空间仍旧没有增加
- 观察 /proc/slabinfo 发现 kmalloc-8192 这种类型的 slab 空间一直在增加
第一次查看:
kmalloc-8192 128016 128016 8192 4 8 : tunables 0 0 0 : slabdata 32004 32004 0
几分钟后查看:
kmalloc-8192 135760 135760 8192 4 8 : tunables 0 0 0 : slabdata 33940 33940 0
怀疑就是 kmalloc-8192 导致的问题。
跟踪 kmalloc-8192 的 alloc 与 free 过程
执行如下命令开启内核 kmalloc-8192 类 slab 对象的 trace 功能:
echo 1 > /sys/kernel/slab/kmalloc-8192/trace
几十秒后查看 dmesg 信息,看到如下内容:
[90556.444309] TRACE kmalloc-8192 alloc 0xffff88001a0d8000 inuse=4 fp=0x (null)
.........
[90556.444342] Call Trace:
[90556.444355] [<ffffffff826cbd7c>] dump_stack+0x63/0x7f
[90556.444362] [<ffffffff826ca454>] alloc_debug_processing+0xb1/0x105
[90556.444368] [<ffffffff826ca84a>] __slab_alloc+0x3a2/0x40f
[90556.444375] [<ffffffff825a7d4c>] ? kzalloc+0xf/0x11
[90556.444383] [<ffffffff826d3792>] ? _raw_spin_unlock_irqrestore+0x1a/0x2d
[90556.444389] [<ffffffff8258eb55>] ? pci_conf1_read+0xdb/0xf3
[90556.444394] [<ffffffff825a7d4c>] ? kzalloc+0xf/0x11
[90556.444401] [<ffffffff82139fad>] __kmalloc+0xf5/0x156
[90556.444409] [<ffffffff825c4f0b>] ? eth_mac_addr+0x28/0x28
[90556.444413] [<ffffffff825a7d4c>] kzalloc+0xf/0x11
[90556.444419] [<ffffffff825ae850>] alloc_netdev_mqs+0x80/0x2d9
[90556.444425] [<ffffffff825c5075>] alloc_etherdev_mqs+0x1c/0x1e
[90556.444463] [<ffffffffa0058840>] igb_kni_probe+0x64/0xa56 [rte_kni]
[90556.444471] [<ffffffff82402fdf>] ? put_device+0x12/0x14
[90556.444478] [<ffffffff82305dda>] ? pci_get_dev_by_id+0x71/0x79
[90556.444483] [<ffffffff82305e16>] ? pci_get_subsys+0x34/0x3b
[90556.444507] [<ffffffffa003567e>] kni_ioctl_create+0x32d/0x5a1 [rte_kni]
[90556.444531] [<ffffffffa0035924>] kni_ioctl+0x32/0x3b [rte_kni]
[90556.444539] [<ffffffff82156a05>] do_vfs_ioctl+0x351/0x415
[90556.444546] [<ffffffff8215e905>] ? __fget+0x66/0x70
[90556.444552] [<ffffffff82156b14>] SyS_ioctl+0x4b/0x76
[90556.444558] [<ffffffff826d3f89>] system_call_fastpath+0x16/0x1b
上面的信息表明此处的 alloc 是在 kni 接口初始化时执行的。同时也看到了其它模块的打印,reiserfs 的一个打印信息如下:
90560.232045] TRACE kmalloc-8192 alloc 0xffff88001a0da000 inuse=4 fp=0x (null)
.........
[90560.232083] Call Trace:
[90560.232096] [<ffffffff826cbd7c>] dump_stack+0x63/0x7f
[90560.232103] [<ffffffff826ca454>] alloc_debug_processing+0xb1/0x105
[90560.232109] [<ffffffff826ca84a>] __slab_alloc+0x3a2/0x40f
[90560.232117] [<ffffffff821adca2>] ? fix_nodes+0x188/0x1b1d
[90560.232124] [<ffffffff8216bb01>] ? touch_buffer+0xd/0xf
[90560.232130] [<ffffffff8216c4eb>] ? __find_get_block+0x1e5/0x1f7
[90560.232136] [<ffffffff821adca2>] ? fix_nodes+0x188/0x1b1d
[90560.232143] [<ffffffff82139fad>] __kmalloc+0xf5/0x156
[90560.232149] [<ffffffff821adca2>] fix_nodes+0x188/0x1b1d
[90560.232156] [<ffffffff8210b43b>] ? mark_page_accessed+0xf/0xc5
[90560.232161] [<ffffffff8216bb01>] ? touch_buffer+0xd/0xf
[90560.232167] [<ffffffff8210b43b>] ? mark_page_accessed+0xf/0xc5
[90560.232172] [<ffffffff8216bb01>] ? touch_buffer+0xd/0xf
[90560.232178] [<ffffffff821bdd39>] ? journal_mark_dirty+0x62/0x25f
[90560.232185] [<ffffffff821b95f1>] reiserfs_insert_item+0x1ba/0x247
[90560.232196] [<ffffffff821b1675>] add_save_link+0x170/0x1b8
[90560.232202] [<ffffffff826d1ccf>] ? mutex_unlock+0x11/0x13
[90560.232209] [<ffffffff821a8a2b>] reiserfs_truncate_file+0x197/0x278
[90560.232215] [<ffffffff8210c341>] ? truncate_pagecache+0x4d/0x54
[90560.232221] [<ffffffff821abc4a>] reiserfs_setattr+0x2e2/0x311
[90560.232228] [<ffffffff8218c405>] ? __dquot_initialize+0x20/0x14c
[90560.232236] [<ffffffff820ac766>] ? preempt_count_add+0x75/0x88
[90560.232241] [<ffffffff820ac6c2>] ? get_parent_ip+0xd/0x3c
[90560.232248] [<ffffffff8215e092>] notify_change+0x1e2/0x2c6
[90560.232255] [<ffffffff82147096>] do_truncate+0x64/0x89
[90560.232262] [<ffffffff82153f2c>] do_last.isra.46+0x976/0x9a5
[90560.232269] [<ffffffff82154172>] path_openat+0x217/0x4ab
[90560.232275] [<ffffffff8215547d>] do_filp_open+0x35/0x7a
[90560.232281] [<ffffffff826d3765>] ? _raw_spin_unlock+0x12/0x25
[90560.232287] [<ffffffff8215ee1f>] ? __alloc_fd+0xe5/0xf4
[90560.232293] [<ffffffff8214830f>] do_sys_open+0x6b/0xfa
[90560.232298] [<ffffffff821483b7>] SyS_open+0x19/0x1b
[90560.232303] [<ffffffff826d3f89>] system_call_fastpath+0x16/0x1b
[90560.232321] TRACE kmalloc-8192 free 0xffff88001a0da000 inuse=3 fp=0xffff88001a0dc000
它与 kni 模块 alloc 空间打印的堆栈信息的区别在于最后一行,即 TRACE kmalloc-8192 free 0xffff88001a0da000 …,此行表明【释放】了 8192 大小的空间,而 kni 模块的打印中没有释放的打印,表明只在创建。
为什么 kni 模块在一直创建 netdev 呢?
使用 kni 模块的程序只在【初始化】的过程中才会创建 netdev,程序死亡后 netdev 会释放,那上面说的没有释放是否是因为程序一直在运行?
查看程序状态发现,程序竟然在一直重启,同时 kmalloc-8192 的数量在以固定的数目缓慢增长,重命名程序后不再增长,能够确定是 rte_kni 内部模块存在内存泄露问题。
rte_kni.ko 代码分析
阅读 rte_kni.ko 源码发现,在 igb_kni_probe 函数中会调用 alloc_etherdev_mq 创建一个 netdev,此外 kni_ioctl_create 中也会创建一个 netdev,而在【释放】逻辑中,kni_dev_remove 中只释放了 kni_ioctl_create 中创建的 netdev,【没有释放】 e1000、igb、ixgbe xxx_kni_probe 中创建的 netdev 结构,导致内存泄露。
解决方案
在 e1000、igb、ixgbe 的 xxx_kni_probe 中创建的 netdev 结构会被填充到 kni_dev 结构的 lad_dev 字段中,只需要在 kni_dev_remove 函数中判断 lad_dev 字段是否为空,不为空则调用 free_netdev 释放即可。
修改后的 kni_dev_remove 函数代码如下:
static int
kni_dev_remove(struct kni_dev *dev)
{
if (!dev)
return -ENODEV;
.........
if (dev->lad_dev) {
free_netdev(dev->lad_dev);
}
if (dev->net_dev) {
unregister_netdev(dev->net_dev);
free_netdev(dev->net_dev);
}
return 0;
}
验证方案
使用 ftrace 跟踪内核函数调用来验证修改有效,使用 ftrace 跟踪 kni 程序初始化时调用 alloc_netdev_mqs 以及死亡时调用 free_netdev 的过程来验证问题得到解决。
kni 程序运行命令如下:
[root]# ./kni -- -p0x3 --config="(0,0,1),(1,1,1)"
ftrace 跟踪到 alloc_netdev_mqs 函数调用情况如下:
# tracer: function
#
# entries-in-buffer/entries-written: 4/4 #P:4
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
kni-11436 [000] .... 1906.033365: alloc_netdev_mqs <-kni_ioctl_create
kni-11436 [000] .... 1906.033435: alloc_netdev_mqs <-alloc_etherdev_mqs
kni-11436 [000] .... 1906.220368: alloc_netdev_mqs <-kni_ioctl_create
kni-11436 [000] .... 1906.220440: alloc_netdev_mqs <-alloc_etherdev_mqs
杀死 kni 程序后,ftrace 跟踪到 free_netdev 调用情况如下:
Develop>cat ./trace
# tracer: function
#
# entries-in-buffer/entries-written: 2/2 #P:4
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
kni-11436 [000] .... 1990.960338: free_netdev <-kni_dev_remove
kni-11436 [000] .... 1990.986674: free_netdev <-kni_dev_remove
能够看到这里申请了四次,但是只释放了两次,泄露了两个 netdev 结构(每个口泄露一个),符合预期。
重新加载修改后的 rte_kni.ko 文件后测试记录如下:
ftrace 跟踪到 alloc_netdev_mqs 函数调用情况如下:
Develop>cat ./trace
# tracer: function
#
# entries-in-buffer/entries-written: 8/8 #P:4
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
kni-13861 [000] .... 2951.871499: alloc_netdev_mqs <-kni_ioctl_create
kni-13861 [000] .... 2951.871571: alloc_netdev_mqs <-alloc_etherdev_mqs
kni-13861 [000] .... 2952.059302: alloc_netdev_mqs <-kni_ioctl_create
kni-13861 [000] .... 2952.059372: alloc_netdev_mqs <-alloc_etherdev_mqs
杀掉 kni 程序后,ftrace 跟踪到 free_netdev 调用情况如下:
# tracer: function
#
# entries-in-buffer/entries-written: 4/4 #P:4
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
kni-13861 [000] .... 3000.154817: free_netdev <-kni_dev_remove
kni-13861 [000] .... 3000.165235: free_netdev <-kni_dev_remove
kni-13861 [000] .... 3000.181801: free_netdev <-kni_dev_remove
kni-13861 [000] .... 3000.190228: free_netdev <-kni_dev_remove
修复后申请了四次,释放了四次,符合预期。
总结
前期已经解决过一些 rte_kni.ko 模块的问题,但是这次暴露出的问题表明其中仍旧潜藏着一些问题。
高版本的趋势是先【剥离】 rte_kni.ko 模块的 ethtool 功能,最终【完全抛弃】,这是正确的方向。
rte_kni.ko 模块的 ethtool 功能本身就是一个【过渡功能】,【稳定性堪忧】,我们也应当追随潮流弃用 rte_kni.ko 的 ethtool 功能。