linux内核中udevd的模块自动加载机制。
2011-05-02 11:15
423 查看
来源:
www.osplay.org
摘要:
本文讨论了linux内核中udevd的模块自动加载机制。
标题
思
考
如果想让内核启动过程中自动加载某个模块
该怎么做呢?最容易想到的方法就是到/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定义为:
上面的信息说明,凡是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
文件中,其内容如下:
v后面的000010EC说明其Vendor ID为10EC,d后面的00008138说明Device
ID为8139,而sv,和sd为SubVendor ID和SubDevice ID,后面的星号表示任意匹配。
另外在/lib/modules/uname-r
/modules.dep文件中还保存这模块之间的依赖关系,其内容如下:
在内核启动过程中,总线驱动程序会会总线协议进行总线枚举(总线驱动程序总是集成在内核之中,不能够按模块方式加载,你可以通过make
menuconfig进入Bus
options,这里面的各种总线,你只能够选择Y或N,而不能选择M.),并且为每一个设备建立一个设备对象。每一个总线对象有一个kset对
象,每一个设备对象嵌入了一个kobject对象,kobject连接在kset对象上,这样总线和总线之间,总线和设备设备之间就组织成一颗树状结构。
当总线驱动程序为扫描到的设备建立设备对象时,会初始化kobject对象,并把它连接到设备树中,同时会调用kobject_uevent()把这个
(添加新设备的)事件,以及相关信息(包括设备的VendorID,DeviceID等信息。)通过netlink发送到用户态中。在用户态的udevd
检测到这个事件,就可以根据这些信息,打开/lib/modules/uname-r
/modules.alias文件,根据
得知这个新扫描到的设备驱动模块为8139too。于是modprobe就知道要加载8139too这个模块了,同时modprobe根据
modules.dep文件发现,8139too依赖于mii.ko,如果mii.ko没有加载,modprobe就先加载mii.ko,接着再加载
8139too.ko。
我们得到udevd的进程ID为25063,现在结束这个进程:
然后跟踪udevd,在shell中运行:
这时,我们看到udevd的输出如下:
我们发现udevd在这里被阻塞在select()函数中。
select函数原型如下:
select函数的作用是:如果readfds中的任何一个文件有数据可读,或者witefds中的任何一个文件可以写入,或者exceptfds
中的任何一个文件出现异常时,就返回。否则阻塞当前进程,直到上诉条件满足,或者因阻塞时间超过了timeout指定的时间,当前进程被唤
醒,select返回。
所以,在这里udevd等待3,4,5,6这几个文件有数据可读,才会被唤醒。现在,到shell中运行:
udevd的进程id为27617,现在我们来看看select等待的几个文件:
由于不方便在运行中插入一块8139的网卡,因此现在我们以一个U盘来做试验,当你插入一个U盘后,你将会看到strace的输出,从它的输出可以
看到udevd在select返回后,调用了modprobe加载驱动模块,并调用了sys_mknod,在dev目录下建立了相应的节点。
这里modprobe的参数"usb:v05AC..."对应modules.alias中的某个模块。
可以通过udevmonitor来查看内核通过netlink发送给udevd的消息,在shell中运行:
然后再插入U盘,就会看到相关的发送给udevd的消息。
== 内核处理过程 ==:
这里我们以PCI总线为例,来看看在这个过程中,内核是如何处理的。当PCI总线驱动程序扫描到一个新的设备时,会建立一个设备对象,然后
调用pci_bus_add_device()函数,这个函数最终会调用kobject_uevent()通过netlink向用户态的udevd发送消
息。
device_add()代码如下:
device_add()在准备好相关数据结构后,会调用kobject_uevent(),把这个消息发送到用户空间的udevd。
比如:/dev/sda1, 但是硬盘对应的驱动程序没有加载前,/dev/sda1是不存在的, 如果没有/dev/sda1,就不能通过mount
/dev/sda1
/来挂载根目录。另一方面udevd是一个可执行文件,如果连硬盘驱动程序到没有加载,根目录都不存在,udevd就不能运行。如果udevd不能运行,
那么就不会自动加载磁盘驱动程序,也就不能自动创建/dev/sda1。这不是死锁了吗?那么你的Linux是怎么启动的呢?
参考资料: Essential Linux Device Drivers
www.osplay.org
摘要:
本文讨论了linux内核中udevd的模块自动加载机制。
标题
思
考
如果想让内核启动过程中自动加载某个模块
该怎么做呢?最容易想到的方法就是到/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
相关文章推荐
- linux内核中udevd的模块自动加载机制
- Linux内核设备驱动模块自动加载机制
- Linux内核(6) - 模块机制与“Hello World!
- php自动加载机制的深入分析
- 详解composer的自动加载机制
- Linux如何在系统启动时自动加载内核模块
- YII框架的类自动加载机制
- 突破SafeSEH机制之三——利用加载模块之外的地址绕过SafeSEH
- Linux如何在系统启动时自动加载模块
- PHP面向对象自动加载机制原理与用法分析
- nodeJS基础--模块加载机制
- nodejs 模块以及加载机制,主要讨论找不到模块的问题
- Linux内核动态加载模块(详细)
- Linux如何在系统启动时自动加载内核模块
- 内核模块开机自动加载和黑名单
- linux内核及其模块的查询,加载,卸载
- 嵌入式 Linux 与linux启动时自动加载模块 分类: arm-linux-Ubuntu 2013-07-22 16:30 242人阅读 评论(0) 收藏
- linux下自动加载设备驱动程序模块 分类: arm-linux-Ubuntu 2015-05-15 17:40 325人阅读 评论(0) 收藏
- Atitit. 木马病毒的外部class自动加载机制------加载class的方法总结
- Nodejs之require加载机制(模块可以污染全局空间)