NotePublic/Software/System/Linux/Modules/Kernel/Linux_内核模块自动加载机制.md

9.8 KiB
Raw Permalink Blame History

Linux 内核模块自动加载机制

思考

如果想让内核启动过程中自动加载某个模块该怎么做呢?最容易想到的方法就是到/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为10ECd后面的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是怎么启动的呢

外部参考资料

  1. Essential Linux Device Drivers