今天在跟同事看一个偶现的问题时,需要用更高版本的 linux 内核驱动进行测试。编译了一个高版本的内核驱动后发现出问题的内核驱动引用计数异常,导致内核模块不能正常卸载,rmmod -f 也无法卸载。
按照我的经验可能只剩下了重启,但是我们看的这个问题有一定的偶然性,一旦重启可能又需要过很长时间才能出现。
我们需要一种不重启也能加载新版本内核驱动的方法,我一开始觉得可能需要写个外部模块修改出问题的内核驱动的引用计数,但是仔细想了想这种方式可能实现不了。那难道只能重启机器了吗?
多次加载(同一)驱动
下班回家的路上,我想到了驱动加载的过程,之前我有系统的研究过,知道每个内核驱动由驱动名称唯一标识,突然灵机一动,想到或许改个名字重新编译一下应该就可以加载了,相当于让驱动改头换面,既换汤也换药。
不过考虑到一些驱动可能会用 EXPORT_SYMBOL、EXPORT_SYMBOL_GPL 来导出符号,这样如果已经加载了一个驱动,再次加载就会报符号重复的错误,对于这种情况,可能只有将这些 EXPORT 的符号名也修改下了。
回家后我使用虚拟机尝试多次加载 e1000e 网卡驱动,我对驱动源码进行了如下修改:
1. 修改 Makefile 中的模块名称
# SPDX-License-Identifier: GPL-2.0
# Copyright(c) 1999 - 2018 Intel Corporation.
#
# Makefile for the Intel(R) PRO/1000 ethernet driver
#
#obj-$(CONFIG_E1000E) += e1000e.o
obj-m += e1000e_1.o
e1000e_1-objs := 82571.o ich8lan.o 80003es2lan.o \
mac.o manage.o nvm.o phy.o \
param.o ethtool.o netdev.o ptp.o
上面的修改将 e1000e 修改为 e1000e_1。
2. 修改 pci 驱动注册中的驱动名称
char e1000e_driver_name[] = "e1000e_1";
这个名称在 pci_driver 结构体中被使用,相关的代码如下:
./netdev.c-7557-static struct pci_driver e1000_driver = {
./netdev.c:7558: .name = e1000e_driver_name,
./netdev.c-7559- .id_table = e1000_pci_tbl,
./netdev.c-7560- .probe = e1000_probe,
./netdev.c-7561- .remove = e1000_remove,
./netdev.c-7562- .driver = {
./netdev.c-7563- .pm = &e1000_pm_ops,
./netdev.c-7564- },
./netdev.c-7565- .shutdown = e1000_shutdown,
./netdev.c-7566- .err_handler = &e1000_err_handler
如果不修改这个名字,当加载时注册 pci 驱动时系统会根据名称查找系统中 pci 总线驱动链表,找到已经存在的项目后会报 pci 驱动已经注册的错误。
与这个逻辑相关的代码如下:
int driver_register(struct device_driver *drv)
{
int ret;
struct device_driver *other;
if (!drv->bus->p) {
pr_err("Driver '%s' was unable to register with bus_type '%s' because the bus was not initialized.\n",
drv->name, drv->bus->name);
return -EINVAL;
}
if ((drv->bus->probe && drv->probe) ||
(drv->bus->remove && drv->remove) ||
(drv->bus->shutdown && drv->shutdown))
printk(KERN_WARNING "Driver '%s' needs updating - please use "
"bus_type methods\n", drv->name);
other = driver_find(drv->name, drv->bus);
if (other) {
printk(KERN_ERR "Error: Driver '%s' is already registered, "
"aborting...\n", drv->name);
return -EBUSY;
}
...................
driver_register 在网卡驱动的 init 函数中被调用以注册驱动,driver_register 中会以驱动名称在总线中已经注册的驱动链表中查找,如果找到一个已经存在的驱动则会终止注册,如果没有找到则继续执行注册过程并将驱动最终添加到总线注册驱动链表中。
修改了以上两点后重新编译,后生成一个 e1000e_1.ko 的驱动,可以成功加载,也可以成功绑定驱动。
加载后 lsmod 查看 e1000e 驱动信息如下:
longyu@virt-debian10:~/linux-source-4.19/drivers/net/ethernet/intel/e1000e$ lsmod | grep e1000e
e1000e_1 282624 0
e1000e 282624 0
使用 dpdk 中提供的 dpdk-devbind.py 脚本绑定虚拟网卡到 e1000e_1 中,能够绑定成功,绑定成功后执行 dpdk-devbind.py -s 有如下输出信息:
longyu@virt-debian10:~/dpdk-stable-17.02.1/usertools$ python dpdk-devbind.py -s
Network devices using DPDK-compatible driver
============================================
<none>
Network devices using kernel driver
===================================
0000:01:00.0 'Virtio network device' if=enp1s0 drv=virtio-pci unused=virtio_pci *Active*
0000:04:00.0 '82574L Gigabit Network Connection' if=enp4s0 drv=e1000e unused=
0000:08:00.0 '82574L Gigabit Network Connection' if=enp8s0 drv=e1000e unused=
0000:09:00.0 '82574L Gigabit Network Connection' if=enp9s0 drv=e1000e_1 unused=e1000e *Active*
可以看到 09:00.0 绑定到了 e1000e_1 驱动,其它的两个 e1000e 的网口绑定到了 e1000e 驱动。
配置 ip 后能够看到接口正常收发包,ifconfig 查看接口信息得到了如下输出:
longyu@virt-debian10:~/linux-source-4.19/drivers/net/ethernet/intel/e1000e$ sudo ifconfig enp9s0
enp9s0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.122.30 netmask 255.255.255.0 broadcast 192.168.122.255
inet6 fe80::5054:ff:fe08:3fef prefixlen 64 scopeid 0x20<link>
ether 52:54:00:08:3f:ef txqueuelen 1000 (Ethernet)
RX packets 63777 bytes 6480718 (6.1 MiB)
RX errors 554 dropped 0 overruns 0 frame 554
TX packets 29 bytes 27028 (26.3 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device interrupt 23 memory 0xfc240000-fc260000
可以看出 tx 与 rx 都有数据,表明收发包正常。
总结
上面这种方法对于网卡驱动这种不会 export 任何符号的模块来说是可行的,对于那些需要 export 符号的驱动,需要修改符号的名称以避免重复导出符号的错误。
同时需要注意的是这里由于使用的是 pci 驱动,我们可以单独绑定某个 pci 设备接口到指定的驱动上,对于其它类型的驱动,这种方式就不适用了。