Linux内核设备驱动模块自动加载机制解析

在这里插入图片描述



摘要

    现在大多数硬件设备的驱动都是作为模块出现的,Linux启动过程中会自动加载这些模块,本文通过内核源码简要说明这个过程。

1 驱动模块本身包含设备商、设备 ID 号等详细信息

    如果想让内核启动过程中自动加载某个模块该怎么做呢?最容易想到的方法就是到 /etc/init.d/ 中添加一个启动脚本,然后在 /etc/rcN.d/ 目录下创建一个符号链接,这个链接的名字以 S 开头,这内核启动时,就会自动运行这个脚本了,这样就可以在脚本中使用 modprobe 来实现自动加载。但是我们发现,内核中加载了许多硬件设备的驱动,而搜索 /etc 目录,却没有发现任何脚本负责加载这些硬件设备驱动程序的模块。那么这些模块又是如何被加载的呢?每一个设备都有 Verdon ID, Device ID, SubVendor ID 等信息。而每一个设备驱动程序,必须说明自己能够为哪些 Verdon ID, DevieceID, SubVendor ID 的设备提供服务。以 PCI 设备为例,它是通过一个 pci_device_id 的数据结构来实现这个功能的。例如:RTL8139 的 pci_device_id 定义为:

static struct pci_device_id rtl8139_pci_tbl[] = {
	{0x10ec, 0x8139, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
	{0x10ec, 0x8138, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
	......
}
MODULE_DEVICE_TABLE (pci, rtl8139_pci_tbl);

    上面的信息说明,凡是 Verdon ID 为 0x10EC , Device ID 为 0x8139, 0x8138 的 PCI 设备(SubVendor ID 和 SubDeviceID 为 PCI_ANY_ID,表示不限制。),都可以使用这个驱动程序(8139too)。

2 模块安装过程提取设备商、设备 ID 信息,并写入 modules.alias 文件

    在模块安装的时候,depmod 会根据模块中的 rtl8139_pci_tbl 的信息,生成下面的信息,保存到 /lib/modules/uname-r/modules.alias 文件中,其内容如下:

alias pci:v000010ECd00008138sv*sd*bc*sc*i* 8139too
alias pci:v000010ECd00008139sv*sd*bc*sc*i* 8139too
......

   后面的 000010EC 说明其 Vendor ID 为 10EC ,d 后面的 00008138 说明 Device ID 为 8139,而 sv 和 sd 为 SubVendor ID 和 SubDevice ID ,后面的星号表示任意匹配。另外在 /lib/modules/uname-r/modules.dep 文件中还保存这模块之间的依赖关系,其内容如下:

# (这里省去了路径信息。)
8139too.ko:mii.ko

3 内核启动过程中,总线枚举时把读取的设备 ID 等信息发送到 udevd,udevd 根据 modules.alias 文件找到匹配的驱动模块,加载之。

    在内核启动过程中,总线驱动程序会会总线协议进行总线枚举,并且为每一个设备建立一个设备对象。每一个总线对象有一个 kset 对象,每一个设备对象嵌入了一个 kobject 对象,kobject 连接在 kset 对象上,这样总线和总线之间,总线和设备设备之间就组织成一颗树状结构。当总线驱动程序为扫描到的设备建立设备对象时,会初始化 kobject 对象,并把它连接到设备树中,同时会调用 kobject_uevent() 把这个(添加新设备的)事件,以及相关信息(包括设备的 VendorID, DeviceID 等信息。)通过 netlink 发送到用户态中。在用户态的 udevd 检测到这个事件,就可以根据这些信息,打开 /lib/modules/uname-r/modules.alias 文件,根据

alias pci:v000010ECd00008138svsdbcsci* 8139too 

得知这个新扫描到的设备驱动模块为 8139too 。于是 modprobe 就知道要加载 8139too 这个模块了,同时 modprobe 根据 modules.dep 文件发现,8139too 依赖于 mii.ko,如果 mii.ko 没有加载,modprobe 就先加载 mii.ko,接着再加载 8139too.ko。

4 实验

    在你的 shell 中,运行:

ps aux | grep udevd

root 25063 ...... /sbin/udevd --daemon
# 我们得到udevd的进程ID为25063,现在结束这个进程:

kill -9 25063
# 然后跟踪 udevd,在shell中运行:

strace -f /sbin/udevd --daemon
# 这时,我们看到udevd的输出如下:
......
close(8) = 0
munmap(0xb7f8c000, 4096) = 0
select(7, [3 4 5 6], NULL, NULL, NULL

# 我们发现udevd在这里被阻塞在select()函数中。
# select函数原型如下:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, 
			struct timeval *timeout);
第一个参数:nfds表示最大的文件描述符号,这里为7(明明是6 ?)。
第二个参数:readfds为读文件描述符集合,这里为3,4,5,6.
第三个参数:writefds为写文件描述符集合,这里为NULL。
第四个参数:exceptfds为异常文件描述符集合,这里为NULL。
第五个参数:timeout指定超时时间,这里为NULL。

# select函数的作用是:如果readfds中的任何一个文件有数据可读,或者witefds中的任何一个文件可以写入,
# 或者exceptfds中的任何一个文件出现异常时,就返回。否则阻塞当前进程,直到上诉条件满足,
# 或者因阻塞时间超过了timeout指定的时间,当前进程被唤醒,select返回。

# 所以,在这里udevd等待3,4,5,6这几个文件有数据可读,才会被唤醒。现在,到shell中运行:

ps aux | grep udevd
root 27615 ...... strace -o /tmp/udevd.debug -f /sbin/udevd --daemon
root 27617 ...... /sbin/udevd --daemon

# udevd的进程id为27617,现在我们来看看select等待的几个文件:

cd /proc/27615/fd
ls -l

# udevd的标准输入,标准输出,标准错误全部为/dev/null.
0 -> /dev/null
1 -> /dev/null
2 -> /dev/null

# udevd在下面这几个文件上等待。
3 -> /inotify
4 -> socket:[331468]
5 -> socket:[331469]
6 -> pipe:[331470]
7 -> pipe:[331470]

由于不方便在运行中插入一块8139的网卡,因此现在我们以一个U盘来做试验,当你插入一个U盘后,
你将会看到strace的输出,从它的输出可以看到 udevd在select返回后,调用了modprobe加载驱动模块,
并调用了sys_mknod,在dev目录下建立了相应的节点。

execve("/sbin/modprobe", ["/sbin/modprobe", "-Q", "usb:v05ACp1301d0100dc00dsc00dp00"...]
......
mknod("/dev/sdb", S_IFBLK|0660, makedev(8, 16)) = 0
......

# 这里modprobe的参数"usb:v05AC..."对应modules.alias中的某个模块。

# 可以通过udevmonitor来查看内核通过netlink发送给udevd的消息,在shell中运行:

udevmonitor --env

# 然后再插入U盘,就会看到相关的发送给udevd的消息。
# == 内核处理过程 ==:

这里我们以PCI总线为例,来看看在这个过程中,内核是如何处理的。当PCI总线驱动程序扫描到一个新的设备时,
会建立一个设备对象,然后调用 pci_bus_add_device()函数,这个函数最终会调用kobject_uevent()通过
netlink向用户态的udevd发送消息。
int pci_bus_add_device(struct pci_dev *dev)
{
	int retval;
	retval = device_add(&dev->dev);
	
	......
	
	return 0;
}

// device_add()代码如下:

int device_add(struct device *dev)
{
	struct device *parent = NULL;
	
	dev = get_device(dev);
	
	......
	
	error = bus_add_device(dev);
	if (error)
	goto BusError;
	kobject_uevent(&dev->kobj, KOBJ_ADD);
	......
}

// device_add()在准备好相关数据结构后,会调用 kobject_uevent(),
// 把这个消息发送到用户空间的 udevd。

int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
	return kobject_uevent_env(kobj, action, NULL);
}

int kobject_uevent_env(struct kobject *kobj, enum kobject_action action, char *envp_ext[])
{
	struct kobj_uevent_env *env;
	const char *action_string = kobject_actions[action];
	const char *devpath = NULL;
	const char *subsystem;
	struct kobject *top_kobj;
	struct kset *kset;
	struct kset_uevent_ops *uevent_ops;
	u64 seq;
	int i = 0;
	int retval = 0;
	
	......
	
	/* default keys */
	retval = add_uevent_var(env, "ACTION=%s", action_string);
	if (retval)
		goto exit;
	retval = add_uevent_var(env, "DEVPATH=%s", devpath);
	if (retval)
		goto exit;
	retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);
	if (retval)
		goto exit;
	
	/* keys passed in from the caller */
	if (envp_ext) {
		for (i = 0; envp_ext[i]; i++) {
			retval = add_uevent_var(env, envp_ext[i]);
			if (retval)
				goto exit;
		}
	}

	......

	/* 通过netlink发送消息,这样用户态的udevd进程就会从select()函数返回,并做相应的处理。 */
	#if defined(CONFIG_NET)
	/* send netlink message */
	if (uevent_sock) {
		struct sk_buff *skb;
		size_t len;
		
		/* allocate message with the maximum possible size */
		len = strlen(action_string) + strlen(devpath) + 2;
		skb = alloc_skb(len + env->buflen, GFP_KERNEL);
		if (skb) {
			char *scratch;
			
			/* add header */
			scratch = skb_put(skb, len);
			sprintf(scratch, "%s@%s", action_string, devpath);
			
			/* copy keys to our continuous event payload buffer */
			for (i = 0; i < env->envp_idx; i++) {
				len = strlen(env->envp[i]) + 1;
				scratch = skb_put(skb, len);
				strcpy(scratch, env->envp[i]);
			}
			
			NETLINK_CB(skb).dst_group = 1;
			netlink_broadcast(uevent_sock, skb, 0, 1, GFP_KERNEL);
		}
	}
	#endif
	
	......
	return retval;
}

5 思考

    现在我们知道 /dev 目录下的设备文件是由 udevd 负责建立的,但是在内核启动过程中,需要 mount 一个根目录,通常我们的根目录是在硬盘上,比如:/dev/sda1,但是硬盘对应的驱动程序没有加载前,/dev/sda1 是不存在的, 如果没有 /dev/sda1,就不能通过 mount /dev/sda1 / 来挂载根目录。另一方面 udevd 是一个可执行文件,如果连硬盘驱动程序到没有加载,根目录都不存在,udevd 就不能运行。如果 udevd 不能运行,那么就不会自动加载磁盘驱动程序,也就不能自动创建 /dev/sda1。这不是死锁了吗?那么你的 Linux 是怎么启动的呢?

原文作者:内核技术中文网 - 构建全国最权威的内核技术交流分享论坛

原文地址:Linux内核设备驱动模块自动加载机制解析 - 圈点 - 内核技术中文网 - 构建全国最权威的内核技术交流分享论坛(版权归原文作者所有,侵权联系删除)

   
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值