Skip to the content.

Analysis of the softlockup issue caused by the net_device refcnt not being 1

发表于 2025-09-11

网络设备的索引不为1,导致的softlockup的问题分析

1. 问题现象

测试发现设备在升级过程中,耗时较长,并且升级失败。检查发现系统出现crash。 原因是 “Kernel panic - not syncing: softlockup: netdev_wait_allrefs hung tasks”。 设备在unreigster过程中 因为 索引不为1,导致迟迟不能释放,最终触发softlockup告警,最终触发内核panic(代码中增加了超时3分钟触发panic处理)。

对于这类型问题,需要先从日志中获取 问题net_device的相关信息。

[ 1056.498243] unregister_netdevice: waiting for vlan1 to become free. Usage count = 7

从这条日志可以得出, net_device vlan1 当前有7个refcnt, 有6个异常refcnt没有释放(正常在这里refcnt应该为1) 。

然后取coredump文件,使用crash工具调试。

      KERNEL: vmlinux  [TAINTED]
    DUMPFILE: coredump-2025-09-04-10_59  [PARTIAL DUMP]
        CPUS: 2
        DATE: Thu Sep  4 10:59:21 PDT 2025
      UPTIME: 00:19:04
LOAD AVERAGE: 0.98, 0.89, 0.63
       TASKS: 78
    NODENAME: TESTMODEL
     RELEASE: 6.1
     VERSION: #1 SMP Tue Sep  2 16:39:22 PDT 2025
     MACHINE: x86_64  (2599 Mhz)
      MEMORY: 16 GB
       PANIC: "Kernel panic - not syncing: softlockup: netdev_wait_allrefs hung tasks"
crash> bt
PID: 4962     TASK: ffff8881025dc880  CPU: 0    COMMAND: "mdsvr"
 #0 [ffffc900022e7ae8] machine_kexec at ffffffff802857d3
 #1 [ffffc900022e7b68] __crash_kexec at ffffffff80394297
 #2 [ffffc900022e7c38] panic at ffffffff802a4da5
 #3 [ffffc900022e7cb8] netdev_run_todo at ffffffff80cb82bc
 #4 [ffffc900022e7d58] vlan_ioctl_handler at ffffffff810590d3
 #5 [ffffc900022e7df8] sock_ioctl at ffffffff80c6c8be
 #6 [ffffc900022e7e68] __x64_sys_ioctl at ffffffff8052e795
 #7 [ffffc900022e7f18] do_syscall_64 at ffffffff810942a9
 #8 [ffffc900022e7f50] entry_SYSCALL_64_after_hwframe at ffffffff812000dc
    RIP: 00007f95312478d8  RSP: 00007ffd66effb28  RFLAGS: 00000246
    RAX: ffffffffffffffda  RBX: 0000000000000010  RCX: 00007f95312478d8
    RDX: 00007ffd66effb60  RSI: 0000000000008983  RDI: 0000000000000010
    RBP: 0000000000000001   R8: 0000000000000004   R9: 0000000000000007
    R10: 00007ffd66effb1c  R11: 0000000000000246  R12: 00007ffd66effb60
    R13: 0000000000000000  R14: 00007ffd66f00260  R15: 0000000000000000
    ORIG_RAX: 0000000000000010  CS: 0033  SS: 002b

因为这种crash 没有保留当时的寄存器,给问题定位带来了一些困难。 这时通过 net命令已经看不到 vlan1 设备了,所以我们需要通过栈信息来获取到 vlan1的地址。

crash> rd -s ffffc900022e7c38 128
ffffc900022e7c38:  panic+326        0000003000000008 
ffffc900022e7c48:  ffffc900022e7cc0 ffffc900022e7c60 
ffffc900022e7c58:  _printk+89       schedule_timeout+210 
ffffc900022e7c68:  00000000ffffdfff ffffc900022e7bc0 
ffffc900022e7c78:  d66ccc72ee3a4e00 0000000000001fff 
ffffc900022e7c88:  _printk_rb_static_descs ffffc900022e7d10
ffffc900022e7c98:  ffffc900022e7d00 ffffc900022e7d00
ffffc900022e7ca8:  00000001000cdf18 ffffc900022e7d50
ffffc900022e7cb8:  netdev_run_todo+2220 0000000000000002   //    rsp-0x68
ffffc900022e7cc8:  0000000000000002 vlan_vid_del+285 
ffffc900022e7cd8:  0000000000000002 00000001000ce1f1 
ffffc900022e7ce8:  ffff8881025dc880 000000fa03a45440 
ffffc900022e7cf8:  00000001000cc919 ffff888102af84d0 
ffffc900022e7d08:  ffff888102af84d0 ffff888102af8000       //  net_device
ffffc900022e7d18:  0000000000000000 d66ccc72ee3a4e00 
ffffc900022e7d28:  ffff888102af8000 00007ffd66effb60        //rbx  r12 
ffffc900022e7d38:  ffff888103a45440 0000000000000000        //r13  r14
ffffc900022e7d48:  init_net         ffffc900022e7df0        //r15  rbp
ffffc900022e7d58:  vlan_ioctl_handler+1411 ffff8881025dc880 

从栈中得到 vlan1的net_device地址 ffff888102af8000

crash> net_device.name,pcpu_refcnt -x ffff888102af8000
  name = "vlan1\000\000\000\000\000",
  pcpu_refcnt = 0x607bd000b234,

crash> atomic_t 0x607bd000b234:a
[0]: ffffe8ffffc0b234
struct atomic_t {
  counter = -12
}
[1]: ffffe8ffffd0b234
struct atomic_t {
  counter = 19
}

refcnt=19-12 =7 内存信息和日志信息相互印证。

2. 初步分析

定位这种设备索引不为1的问题,相比其他crash要困难。因为此时你只能看到索引不为1,但是谁hold住了这个设备的索引,是没有地方可以查看的。

直接dev_hold和dev_put日志的话,会因为日志太多而无法分析。于是将日志限制在针对 vlan1 这个设备的 hold和put操作进行栈信息打印。 编译debug image,加载复现后。收集日志,进行逐一比对(有时候就是需要这种纯手工劳动)。

//部分省略
=====
[12927.660926] [__dev_hold:4073] dev:vlan1 refcnt: 27 		//dst_init * 15次 +

[12927.668936] [__dev_put:4057] dev:vlan1 refcnt: 29 		//dst_destroy	* 7次 -
[13293.869949] [__dev_put:4057] dev:vlan1 refcnt: 29 		//dst_dev_put * 8次 -
===
[12927.660965] neighbour: [__dev_hold:4073] dev:vlan1 refcnt: 28 		//___neigh_create * 4次 +

[13293.862636] neighbour: [__dev_put:4057] dev:vlan1 refcnt: 33 		//neigh_destroy		* 4次 -
====
[12927.625415] [__dev_hold:4073] dev:vlan1 refcnt: 12 	//fib_create_info+0x96f	* 1次 +
[12927.625483] [__dev_hold:4073] dev:vlan1 refcnt: 13 	//fib_check_nh+0xa31 * 3次 +
[12927.627085] [__dev_hold:4073] dev:vlan1 refcnt: 16 	//fib_check_nh+0x9c3	* 11次 +
[12927.627562] [__dev_hold:4073] dev:vlan1 refcnt: 18 	//fib_nh_dup+0x39d		* 8次  +

[12927.632924] [__dev_put:4057] dev:vlan1 refcnt: 24 		//fib_nh_common_release+0x29c * 17次 -
=======

经过筛选和汇总,发现 fib_info相关对 vlan1 引用了 23次,但是释放了17次,有6个引用没有释放!!!

3. fib为什么没有释放

问题缩小到 fib持有的refcnt没有释放。但是那些fib持有vlan1的refcnt没有释放呢?

因为索引fib_info都挂在fib_info_hash链表数组中。我们读取链表数组,看看那些fib_info中持有了vlan1的索引。

crash> fib_info_hash
fib_info_hash = $2 = (struct hlist_head *) 0xffff8881448c2c00
crash> fib_info_hash_size
fib_info_hash_size = $3 = 64
crash> rd 0xffff8881448c2c00 64
ffff8881448c2c00:  0000000000000000 0000000000000000   ................
//部分忽略
ffff8881448c2cc0:  0000000000000000 ffff88810211f400   ................
ffff8881448c2cd0:  0000000000000000 0000000000000000   ................
ffff8881448c2ce0:  0000000000000000 0000000000000000   ................
ffff8881448c2cf0:  0000000000000000 0000000000000000   ................
ffff8881448c2d00:  ffff888100af8c00 0000000000000000   ................
ffff8881448c2d10:  0000000000000000 ffff888100af8400   ................
ffff8881448c2d20:  ffff888100afbc00 0000000000000000   ................
ffff8881448c2d30:  0000000000000000 ffff888100af6400   .........d......
ffff8881448c2d40:  0000000000000000 0000000000000000   ................
ffff8881448c2d50:  0000000000000000 0000000000000000   ................
ffff8881448c2d60:  0000000000000000 0000000000000000   ................
ffff8881448c2d70:  ffff88813ef2a200 0000000000000000   ...>............      //
ffff8881448c2d80:  0000000000000000 0000000000000000   ................
ffff8881448c2d90:  0000000000000000 0000000000000000   ................
ffff8881448c2da0:  0000000000000000 0000000000000000   ................
ffff8881448c2db0:  0000000000000000 ffff888142a8f800   ...........B....
ffff8881448c2dc0:  0000000000000000 0000000000000000   ................
ffff8881448c2dd0:  0000000000000000 0000000000000000   ................
ffff8881448c2de0:  0000000000000000 0000000000000000   ................
ffff8881448c2df0:  ffff888102a98400 0000000000000000   ................

//依次查看,最后发现
crash> list ffff88813ef2a200
ffff88813ef2a200
ffff88813ef2a800
ffff888145767a00
ffff888145767200
ffff888143de1400
ffff888143de1200

//依次查看 nhc_dev
crash> fib_info.fib_nh ffff88813ef2a200
  fib_nh = 0xffff88813ef2a2b0
crash> fib_nh.nh_common -x 0xffff88813ef2a2b0
  nh_common = {
    nhc_dev = 0xffff888102af8000,      //vlan1设备
    nhc_dev_tracker = {<No data fields>},
    nhc_oif = 0x16,
    nhc_flags = 0x11,
    nhc_scope = 0xfd,
    nhc_family = 0x2,
    nhc_gw_family = 0x2,
    nhc_lwtstate = 0x0,
    nhc_gw = {
      ipv4 = 0x4901020a,      //10.2.1.73
    },
//部分省略
  }

在 ffff88813ef2a200 这个链上找到6个持有 vlan1 索引的fib_info, 和 问题中 有6个 refcnt未释放 对应。

这些fib_info是什么时候创建的呢?

crash> fib_info ffff88813ef2a200             
struct fib_info {
//部分省略
  fib_treeref = {
    refs = {
      counter = 1
    }
  },
  fib_clntref = {
    refs = {
      counter = 1
    }
  },

  fib_tb_id = 4097,
  fib_priority = 0,
//部分省略
}

从fib_info中我们得知它的 route table id是 4097。 查看设备运行时的路由表 4097

/var/log/debug_sys# ip route show table 4097
default proto static 
	nexthop via 10.2.1.71 dev vlan1 weight 1 
	nexthop via 10.2.1.72 dev vlan1 weight 1 
	nexthop via 10.2.1.73 dev vlan1 weight 1 

难道是这个表在升级时没有清理导致么?

4. 猜想 + 验证

4.1 删除4097表中路由项

删除4097表中的 10.2.1.72 和 10.2.1.73 对应的路由项,然后升级image失败,有coredump产生。 查看coredump, 问题fib_info依然存在。 但是重启后,再次升级就OK了。

也就是说直接删除 路由项,并不能将异常 fib_info清理干净。但是启动时如果没有 路由项,问题fib_info就不会产生,也就没有问题了。

现在问题缩小到, 创建 10.2.1.72 和 10.2.1.73 对应的路由项时,会产生 问题fib_info,但是删除时,并不会清理这些问题fib_info。

4.2 fib_table_insert和fib_table_delete 添加详细日志

明明创建时和删除时有对应的ib_table_insert和fib_table_delete操作,为什么fib_info没有释放? 为了追踪fib_info信息,在创建时对 fib_table,fib_alias, fib_info等地址进行跟踪。用来观察问题fib_info的创建和释放。 验证发现创建时产生的fib_info并没有挂到fib_alias上。

随后增加日志,验证发现 问题fib_info 在一个表merge操作后,没有再使用,但是没释放。产生了孤儿fib_info。 找到原因后,修复就水到渠成。添加对应的release操作即可。

5. 复盘和思考

虽然描述中轻描淡写,但实际上费了很大功夫。所以想着能不能有一些好的办法,帮助尽快定位问题。在分析时注意到内核有 CONFIG_NET_DEV_REFCNT_TRACKER 选项。

它的定义在 “net/Kconfig.debug”

# SPDX-License-Identifier: GPL-2.0-only

config NET_DEV_REFCNT_TRACKER
        bool "Enable net device refcount tracking"
        depends on DEBUG_KERNEL && STACKTRACE_SUPPORT && NET
        select REF_TRACKER
        default n
        help
          Enable debugging feature to track device references.
          This adds memory and cpu costs.

config NET_NS_REFCNT_TRACKER
        bool "Enable networking namespace refcount tracking"
        depends on DEBUG_KERNEL && STACKTRACE_SUPPORT && NET
        select REF_TRACKER
        default n
        help
          Enable debugging feature to track netns references.
          This adds memory and cpu costs.

config DEBUG_NET
        bool "Add generic networking debug"
        depends on DEBUG_KERNEL && NET
        help
          Enable extra sanity checks in networking.
          This is mostly used by fuzzers, but is safe to select.

为了测试这个选项是否好用,我特地编译了一版 打开 “CONFIG_NET_DEV_REFCNT_TRACKER=y” 的image,加载复现后。

查看core日志,发现有如下记录:

[  386.186291] unregister_netdevice: waiting for vlan1 to become free. Usage count = 3
[  386.186478] leaked reference.
[  386.186538]  fib_create_info+0x655/0xaa0
[  386.186548]  fib_table_insert+0xa7/0xa00
[  386.186553]  inet_rtm_newroute+0x8d/0xe0
[  386.186557]  rtnetlink_rcv_msg+0x2f4/0x5e0
[  386.186562]  netlink_rcv_skb+0xc1/0x170
[  386.186568]  rtnetlink_rcv+0x15/0x20
[  386.186571]  netlink_unicast+0x257/0x550
[  386.186576]  netlink_sendmsg+0x3c2/0x5c0
[  386.186581]  ____sys_sendmsg+0x28f/0x2e0
[  386.186586]  __sys_sendmsg+0x2a8/0x300
[  386.186590]  __x64_sys_sendmsg+0x1c/0x30
[  386.186594]  do_syscall_64+0x49/0xa0
[  386.186602]  entry_SYSCALL_64_after_hwframe+0x64/0xce

[  386.186608] leaked reference.
[  386.186666]  fib_create_info+0x655/0xaa0
[  386.186670]  fib_table_insert+0xa7/0xa00
[  386.186675]  inet_rtm_newroute+0x8d/0xe0
[  386.186678]  rtnetlink_rcv_msg+0x2f4/0x5e0
[  386.186682]  netlink_rcv_skb+0xc1/0x170
[  386.186686]  rtnetlink_rcv+0x15/0x20
[  386.186690]  netlink_unicast+0x257/0x550
[  386.186694]  netlink_sendmsg+0x3c2/0x5c0
[  386.186699]  ____sys_sendmsg+0x28f/0x2e0
[  386.186703]  __sys_sendmsg+0x2a8/0x300
[  386.186707]  __x64_sys_sendmsg+0x1c/0x30
[  386.186711]  do_syscall_64+0x49/0xa0
[  386.186717]  entry_SYSCALL_64_after_hwframe+0x64/0xce


直接将问题定位到是fib_info部分, 可以说是相当精确。 如果早开启这个选项,我就不用在前面费那么多功夫了。 这个选项,5星推荐!!!

本文访问次数:...