linux内核中udevd的模块自动加载机制

本文探讨了 Linux 内核中 udev 如何自动加载设备驱动模块的过程。通过分析 PCI 设备 ID 和模块间的关系,揭示了内核启动时如何自动识别并加载硬件驱动。此外,还详细介绍了 udev 监听内核事件并响应的机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >



www.osplay.org
本文讨论了linux内核中udevd的模块自动加载机制。
标题
  1. 思 考
如果想让内核启动过程中自动加载某个模块 该怎么做呢?最容易想到的方法就是到/etc/init.d/中添加一个启动脚本,然后在/etc/rcN.d/目录下创建一个符号链接,这个链接的名字 以S开头,这内核启动时,就会自动运行这个脚本了,这样就可以在脚本中使用modprobe来实现自动加载。但是我们发现,内核中加载了许多硬件设备的驱 动,而搜索/etc目录,却没有发现任何脚本负责加载这些硬件设备驱动程序的模块。那么这些模块又是如何被加载的呢?

 

  • 每一个设备都有Verdon ID, Device ID, SubVendor ID等信息。而每一个设备驱动程序,必须说明自己能够为哪些Verdon ID, Deviece

ID, 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)。

  • 在模块安装的时候,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
......
v后面的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
  • 在内核启动过程中,总线驱动程序会会总线协议进行总线枚举(总线驱动程序总是集成在内核之中,不能够按模块方式加载,你可以通过make menuconfig进入Bus

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

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

试验

在你的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;
}

思考

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

参考资料: Essential Linux Device Drivers

<think>嗯,用户想了解Linux内核模块的详细信息和工作原理,特别是实现机制。首先,我需要回忆一下内核模块的基本概念。内核模块是动态加载内核的代码,可以扩展功能而无需重启系统,这点挺重要的。比如驱动程序、文件系统支持之类的,对吧? 用户提到了引用[1]和[2],里面涉及模块签名、版本检查,还有自动加载机制。我应该把这些内容整合进去。模块的结构应该包括入口和出口函数,许可证声明,还有模块信息比如作者、描述等。许可证部分如果不声明可能会有问题,比如GPL兼容性,这点需要说明。 然后,模块加载和卸载过程。加载的时候,内核会做版本检查,确保兼容性,防止不稳定。还有签名验证,引用[1]提到的,可能涉及安全机制,防止恶意模块自动加载机制在引用[2]里提到,设备驱动模块在系统检测到硬件时,通过MODULE_DEVICE_TABLE生成的信息,udev或systemd-udevd会根据这些信息加载对应的模块,这个流程需要解释清楚。 模块的工作原理部分,可能需要讲符号导出和依赖关系。内核通过导出符号表让其他模块使用自己的函数,比如导出`printk`函数。模块依赖的话,加载顺序很重要,先加载基础模块。 实现机制方面,ELF文件结构,模块的.init和.exit段,还有版本检查的具体过程。比如编译时生成的vermagic字符串,包含内核版本、编译器版本等,加载时会检查是否匹配。这部分引用[1]提到的kernel.release文件,需要关联起来。 安全性方面,签名验证流程,比如使用私钥签名,公钥在内核中验证,确保模块的完整性和来源可靠。这也是引用[1]提到的内容。 可能用户还会关心如何编写一个简单的内核模块,所以给出一个Hello World示例,用代码展示模块的基本结构,加载和卸载的命令,比如insmod和rmmod。这样用户能有个直观的认识。 最后,相关问题的部分,应该包括模块与静态编译的区别、安全性问题、调试方法以及自动加载的具体流程。这些问题能帮助用户进一步深入理解。 检查一下有没有遗漏的点,比如模块通信机制,不过可能属于更高级的内容,用户的问题主要是详细介绍和实现机制,暂时不需要深入。确保各部分逻辑连贯,引用正确,特别是来自用户提供的两个引用内容。还要注意使用中文,LaTeX格式的正确性,行内公式用$...$,独立公式用$$...$$,虽然目前内容里可能公式不多,但如果有的话要注意格式。</think>### Linux内核模块详细介绍及实现机制 #### 一、内核模块的定义与作用 内核模块是动态加载Linux内核的可执行代码,能够在不重启系统的前提下扩展内核功能,例如设备驱动、文件系统支持或网络协议等。其核心价值在于**灵活性**和**可维护性**[^1]。 #### 二、模块的代码结构 一个典型模块包含以下要素: 1. **入口函数** `module_init()`:模块加载时执行初始化 2. **出口函数** `module_exit()`:模块卸载时执行清理 3. **许可证声明** `MODULE_LICENSE()`:必须声明(如GPL) 4. **模块信息** `MODULE_AUTHOR()`/`MODULE_DESCRIPTION()` 示例模块代码: ```c #include <linux/init.h> #include <linux/module.h> static int __init demo_init(void) { printk(KERN_INFO "Module loaded\n"); return 0; } static void __exit demo_exit(void) { printk(KERN_INFO "Module unloaded\n"); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Developer"); ``` #### 三、模块加载与卸载机制 1. **加载过程**: - 通过`insmod`或`modprobe`触发系统调用`init_module` - 内核执行版本检查(`vermagic`校验) - 执行模块初始化函数 - 更新内核符号表 2. **卸载过程**: - 通过`rmmod`触发`delete_module` - 检查模块引用计数 - 执行清理函数并释放资源 $$校验公式:\text{模块CRC} \equiv \text{内核预期CRC} \ (\text{mod}\ 2^{32})$$ #### 四、自动加载机制 当检测到新硬件时: 1. 内核发送`uevent`事件 2. `udev`守护进程解析`MODULE_DEVICE_TABLE`信息[^2] 3. 通过`modprobe`加载匹配驱动模块 ```bash # 查看模块设备ID映射 modinfo -F alias <module_name> ``` #### 五、安全性实现 1. **版本验证**: - 编译时生成`vermagic`字符串,包含: - 内核版本`KERNELRELEASE` - 编译器版本 - 配置标志 $$校验值 = \text{SHA1}(内核版本|编译器标志|配置选项)$$ 2. **数字签名**: - 使用X.509证书对模块签名 - 内核启动时验证签名有效性 - 防止未授权模块加载 #### 六、模块工作原理 1. **符号导出机制**: - 通过`EXPORT_SYMBOL()`公开函数 - 形成动态符号表`/proc/kallsyms` 2. **依赖关系**: - `depmod`生成modules.dep依赖文件 - 确保加载顺序正确
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值