问题
首先来回顾下,Linux设备驱动模型中bus、device和driver三者的关系:
- bus是物理总线的抽象。
- device是设备抽象,存在于bus之上。
- driver是驱动抽象,注册到bus上,用于驱动bus上的特定device。
- device和driver通过bus提供的match方法来匹配(通常是使用设备ID进行匹配)。
- driver匹配到device后,调用driver的probe接口驱动device。
- 一个driver可以驱动多个相同的设备或者不同的设备。
一个driver匹配并驱动多个device的情形比较常见,比如一个igb驱动可以驱动多块intel网卡(可以是相同型号,也可以是不同型号),这是由驱动的id_table和驱动的处理逻辑决定的。那么自然而然的一个问题是:如果系统有多个driver都可以匹配到一个device,系统会怎么处理?换句话说,Linux是否允许两个driver服务同一个device?
实验
带着这样的疑问,我做了下面这个实验。
首先,查看下系统的PCI设备驱动情况,挑选一个测试对象。
$ lspci
00:00.0 Host bridge: Intel Corporation Skylake Host Bridge/DRAM Registers (rev 08)
00:02.0 VGA compatible controller: Intel Corporation HD Graphics 520 (rev 07)
00:14.0 USB controller: Intel Corporation Sunrise Point-LP USB 3.0 xHCI Controller (rev 21)
00:14.2 Signal processing controller: Intel Corporation Sunrise Point-LP Thermal subsystem (rev 21)
00:16.0 Communication controller: Intel Corporation Sunrise Point-LP CSME HECI #1 (rev 21)
00:17.0 SATA controller: Intel Corporation Sunrise Point-LP SATA Controller [AHCI mode] (rev 21)
00:1c.0 PCI bridge: Intel Corporation Sunrise Point-LP PCI Express Root Port (rev f1)
00:1c.2 PCI bridge: Intel Corporation Sunrise Point-LP PCI Express Root Port (rev f1)
00:1f.0 ISA bridge: Intel Corporation Sunrise Point-LP LPC Controller (rev 21)
00:1f.2 Memory controller: Intel Corporation Sunrise Point-LP PMC (rev 21)
00:1f.3 Audio device: Intel Corporation Sunrise Point-LP HD Audio (rev 21)
00:1f.4 SMBus: Intel Corporation Sunrise Point-LP SMBus (rev 21)
00:1f.6 Ethernet controller: Intel Corporation Ethernet Connection I219-V (rev 21)
02:00.0 Unassigned class [ff00]: Realtek Semiconductor Co., Ltd. RTS522A PCI Express Card Reader (rev 01)
04:00.0 Network controller: Intel Corporation Wireless 8260 (rev 3a)
可以看到00:1f.6
设备是一块Intel网卡,我们就拿它试验。看看它的驱动情况:
$ lspci -v
00:1f.6 Ethernet controller: Intel Corporation Ethernet Connection I219-V (rev 21)
Subsystem: Lenovo Ethernet Connection I219-V
Flags: bus master, fast devsel, latency 0, IRQ 128
Memory at f1200000 (32-bit, non-prefetchable) [size=128K]
Capabilities: <access denied>
Kernel driver in use: e1000e
Kernel modules: e1000e
可以看到当前正在使用的驱动是e1000e
模块。
然后,构造一个可以匹配选中网卡的PCI设备驱动,观察驱动加载时系统的行为。
代码直接从ldd3的pci_skel样例修改,只需要把id_table修改成目标网卡的vendor id和device id即可。
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/init.h>
static struct pci_device_id ids[] = {
{ PCI_DEVICE(0x8086, 0x1570), },
{ 0, }
};
MODULE_DEVICE_TABLE(pci, ids);
static unsigned char skel_get_revision(struct pci_dev *dev)
{
u8 revision;
pci_read_config_byte(dev, PCI_REVISION_ID, &revision);
return revision;
}
static int probe(struct pci_dev *dev, const struct pci_device_id *id)
{
/* Do probing type stuff here.
* Like calling request_region();
*/
printk("pci_e1000e probing\n");
pci_enable_device(dev);
if (skel_get_revision(dev) == 0x42)
return -ENODEV;
return 0;
}
static void remove(struct pci_dev *dev)
{
/* clean up any allocated resources and stuff here.
* like call release_region();
*/
}
static struct pci_driver pci_driver = {
.name = "pci_e1000e",
.id_table = ids,
.probe = probe,
.remove = remove,
};
static int __init pci_skel_init(void)
{
printk("%s:%d\n", __func__, __LINE__);
return pci_register_driver(&pci_driver);
}
static void __exit pci_skel_exit(void)
{
pci_unregister_driver(&pci_driver);
}
MODULE_LICENSE("GPL");
module_init(pci_skel_init);
module_exit(pci_skel_exit);
Makefile如下:
obj-m := pci_e1000e.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD)
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
我在init和probe接口中增加了打印,跟踪流程。
直接make生成pci_e1000e.ko模块,加载测试。
$ make
$ sudo insmod pci_e1000e.ko
dmesg查看驱动加载情况,发现内核只新增了一行打印:
pci_skel_init:52
说明驱动成功加载,但是压根没有进入probe接口。由此看来,内核应该不会让一个device同时被两个driver匹配上。
分析
直接从源码分析,可以看到设备驱动加载后,pci_register_driver
调用流程大致如下:
其核心代码是driver_attatch
,其作用是driver binding,它调用bus_for_each_dev
来遍历总线上的所有设备,然后对每一个设备调用__driver_attach
函数。
int driver_attach(struct device_driver *drv)
{
return bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);
}
我们想要的答案就在__driver_attach
函数的实现里:
static int __driver_attach(struct device *dev, void *data)
{
struct device_driver *drv = data;
int ret;
ret = driver_match_device(drv, dev);
if (ret == 0) {
/* no match */
return 0;
} else if (ret == -EPROBE_DEFER) {
dev_dbg(dev, "Device match requests probe deferral\n");
driver_deferred_probe_add(dev);
} else if (ret < 0) {
dev_dbg(dev, "Bus failed to match device: %d", ret);
return ret;
} /* ret > 0 means positive match */
if (dev->parent) /* Needed for USB */
device_lock(dev->parent);
device_lock(dev);
if (!dev->driver)
driver_probe_device(drv, dev);
device_unlock(dev);
if (dev->parent)
device_unlock(dev->parent);
return 0;
}
可以看到,在driver_match_device
匹配成功以后,并不是直接进行probe的,而是先要判断dev->driver
是否为空,如果不为空就不进行driver_probe_device
了。也就是说,如果一个device已经绑定了一个driver,就不允许尝试绑定第二个driver了。