热插拔机制之udev和mdev

热插拔是内核和用户空间之间,通过调用用户空间程序(如hotplug、udev 和 mdev)的交互。 当需要通知用户内核发生了某种热插拔事件时,内核才调用这个用户空间程序。

1. 3种热插拔机制

Linux内核支持热插拔的部件有USB设备、PCI设备甚至CPU。Linux的热插拔支持是一个连接底层硬件、内核空间和用户空间程序的机制,且一直在变化。而设备文件系统有devfs,mdev,udev这三种。

在对待设备文件这块,Linux改变了几次策略。在Linux早期,设备文件仅仅是是一些带有适当的属性集的普通文件,它由mknod命令创建,文件存放在/dev目录下。后来,采用了devfs, 一个基于内核的动态设备文件系统,他首次出现在2.3.46内核中。Mandrake,Gentoo等Linux分发版本采用了这种方式。devfs创建 的设备文件是动态的。但是devfs有一些严重的限制,从2.6.13版本后移走了。目前取代他的是udev(PC机上的linux中)和mdev(嵌入式linux系统)。

在需要控制上千个硬盘或热插拔设备(比如USB摄像头和MP3播放器)时一般建议采用udev机制。实际上,你不需要修改这些为磁盘或热插拔终端设备的标准配置文件,而仅需要了解udev的配置方法来使用这些新的外设,如果不修改配置, Linux可能会采用不恰当的名字,属组或权限来创建这些设备文件。

udev和devfs的一个显著的区别在于:采用devfs,当一个并不存在的/dev节点被打开的时候,devfs能自动加载对应的驱动,而udev是在设备被发现的时候才加载驱动模块,而不是当它被访问的时候。


1.1hotplug

这个程序是一个典型的 bash 脚本, 当用户向系统添加或删除设备时,内核会产生一个热插拔事件,并在 /proc/sys/kernel/hotplug 文件里查找处理设备连接的用户空间程序hotplug。它只传递执行权给一系列位于 /etc/hot-plug.d/ 目录树的程序。hotplug 脚本搜索所有的有 .hotplug 后缀的可能对这个事件进行处理的程序并调用它们, 并传递给它们许多不同的已经被内核设置的环境变量。(目前基本已被淘汰)

其工作前提是硬件本身会告诉计算机自己是做什么的(就算没有,它也会告诉内核自己的生厂商代码和独一无二的产品代码);驱动程序清楚自己是驱动哪一类设备的;内核通过总线底层代码清楚什么时间什么样的设备被接入或移出计算机。

/sbin/hotplug的本质是一个脚本。脚本中解析相关参数并调用modprobe和rmmod完成加载和卸载操作。但是,/sbin/hotplug本身是被谁调用的呢?设备驱动程序一般不会和这些太底层的kobject/kset家伙打交道,因为更高层次的device,bus和driver把kobject/kset那一层的细节实现都给封装了起来。

以device_add为起点,uevent事件被这样产生和传递:

device_add

=> kobject_uevent(&dev->kobj, KOBJ_ADD)

        => /* send netlink message */

                ...

                        /* 准备参数 */

                        argv [0] = uevent_helper;

                        argv [1] = (char *)subsystem;

                        argv [2] = NULL;

                ...

                        /* 内核空间调用用户空间的程序 */

                        call_usermodehelper(argv[0], argv,env->envp, UMH_WAIT_EXEC);

                    …

下面看看uevent_helper[0]来自何处:

char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH;

CONFIG_UEVENT_HELPER_PATH其实是空值。可以通过向sysfs接口/sys/kernel/uevent_helper写入应用空间程序路径。

1.2 udev

用于linux2.6.13或更高版本的内核上,为用户空间提供使用固定设备名的动态/dev目录的解决方案。它通过在 sysfs 的 /class/ 和/block/ 目录树中查找一个称为 dev 的文件,以确定所创建的设备节点文件的主次设备号。所以要使用udev,驱动必须为设备在sysfs中创建类接口及其dev属性文件,方法和sculld模块中创建dev属性相同。

udev的工作原理: 当系统内核发现系统中添加或者删除了某个新的设备时,内核检测到后会产生一个hotplug event并查找/proc/sys/kernel/hotplug去找出管理设备连接的用户空间程序。若udev已经启动,内核会通知udev去检测sysfs中关于这个新设备的信息并创建设备节点。udev就会去执行udevd,以便让udevd可以产生或者删除硬件的设备文件。 接着udevd会通过libsysfs读取sys文件系统,以便取得该硬件设备的信息(如/dev/vcs,在/sys/class/tty/vcs/dev存放的是”7:0”,既/dev/vcs的主次设备号);然后再向namedev查询该外部设备的设备文件信息,例如文件的名称、权限等。最后,udevd就依据上述的结果,在/dev/目录中自动建立该外部设备的设备文件,同时在/etc/udev/rules.d下检查有无针对该设备的使用权限。

当设备插入或移除时,hotplug机制会让内核会通过netlink socket通讯(内核调用kobject_uevent函数发送netlink message给用户空间,该功能由内核的统一设备模型里的子系统这一层实现)向用户传递一个事件的发生,Udevd通过标准的socket机制,创建socket连接来获取内核广播的uevent事件 并解析这些uevent事件。

运行udevd以后,使用udevtrigger的时候,会把内核中已存在的设备的节点创建出来,其具体过程为:udevtrigger通过向/sysfs 文件系统下现有设备的uevent节点写"add"字符串,从而触发uevent事件,使得udevd能够接收到这些事件,并创建buildin的设备驱动的设备节点连同任何已insmod的模块的设备节点。
所以,我们也能够手工用命令行来模拟这一过程:

# echo "add" > /sys/block/mtdblock2/uevent

实际上,不管您往uevent里面写什么,都会触发add事件,这个从kernel内部对uevent属性的实现函数能够看出来.

而udevstart的实现方式和udevtrigger就不同了,他基本上是重复实现了udevd里面的机制,通过遍历sysfs,自己完成设备节点的创建,不通过udevd来完成。

Uevent_seqnum 用来标识当前的uevent事件的序号(已产生了多少uevent事件),您能够通过如下操作来查看:

$ cat /sys/kernel/uevent_seqnum
2673

/sbin/hotplug接收到内核的热插拔事件后会执行一系列脚本,其中一个脚本执行了/sbin/udevsend,从而让udev的守护进程知悉这一事件[3]。不过现在,有些发行版中/sbin/目录已经不存在hotplug和udevsend了。热插拔事件通过netlink由udevd直接接收并全权负责。通过下面这条命令可以查看系统中传递给udevd的热插拔事件:

udevadm monitor
  • udev实现自动加载:

注意:请区别于开机自动加载:

开机自动加载:

将模块.ko文件复制到/lib/modules/uname -r/kernel/modulename.ko 目录并更新 /etc/modules文件即可实现booting阶段自动加载模块。(这里没有用到热插拔)

下面介绍如何实现设备节点的自动创建及u盘或sd卡的自动挂载:

  1. 若文件系统中默认是没有对udev进行支持的,我们还需要移植一个udev:

    1. 下载udev源码udev-100.tar.bz2,并解压(网址:http://www.us.kernel.org/pub/linux/utils/kernel/hotplug

    2. 设置交叉编译选项:修改makefile中的交叉编译工具:cross = arm-linux-,保存后执行make进行编译;

    3. 执行命令make进行编译,然后执行arm-linux-strip udev udevd udevstart udevinfo udevtest,并拷贝这些文件到目标板根文件/bin目录下面。

    4. 添加udev的支持(以下3种方法任选其一即可):

      1. 修改etc/init.d/rcs脚本,然后添加如下命令:

        1. /bin/mount -t sysfs sysfs /sys 
          /bin/mount -t tmpfs tmpfs /dev
          
          /bin/udevd --daemon
          /bin/udevstart
          
      2. 修改linuxrc文件(如果linuxrc是二进制文件的话 ,则删除后再创建文本文件)

        1. /bin/mount -t sysfs sysfs /sys
          /bin/mount -t tmpfs tmpfs /dev
          
          /bin/udevd --daemon
          /bin/udevstart
          exec /sbin/init
          
      3. 修改/etc/fstab为:

        1. #device mount-point type options dump fsck order
          
          proc  /proc proc defaults 0 0
          tmpfs /tmp tmpfs defaults 0 0
          sysfs /sys sysfs defaults 0 0
          tmpfs /dev tmpfs defaults 0 0
          
        2. 修改/etc/init.d/rcs,添加如下内容:

          /bin/udevd --daemon

          /bin/udevstart

    5. 编译内核时确保配置了如下选项:

      1.   CONFIG_HOTPLUG=y
          CONFIG_UEVENT_HELPER_PATH=""
          CONFIG_NET=y
          CONFIG_UNIX=y
          CONFIG_SYSFS=y
          CONFIG_SYSFS_DEPRECATED*=n
          CONFIG_PROC_FS=y
          CONFIG_TMPFS=y
          CONFIG_INOTIFY_USER=y
          CONFIG_SIGNALFD=y
          CONFIG_TMPFS_POSIX_ACL=y (user ACLs for device nodes)
          CONFIG_BLK_DEV_BSG=y (SCSI devices)
        
  1. 在/etc下创建udev目录

  2. 在/etc/udev下创建目录rules.d和文件udev.conf

  3. 在udev.conf中添加如下内容

    1. # udev.conf
      # the initial syslog(3) priority: "err", "info", "debug" or its
      # numerical equivalent. for runtime debugging, the daemons internal
      # state can be changed with: "udevcontrol log_priority=<value>".
      
      udev_log="err"
      
    2. udev_root:udev 产生的设备所存放的目录,默认值是 /dev/。建议不要修改该参数,因为很多应用程序默认会从该目录调用设备文件。
      udev_db:udev 信息存放的数据库或者所在目录,默认值是 /dev/.udev.tdb。
      udev_rules:udev 规则文件的名字或者所在目录,默认值是 /etc/udev/rules.d/。
      udev_permissions:udev 权限文件的名字或者所在目录,默认值是 /etc/udev/permissions.d/。
      default_mode/ default_owner/ default_group:如果设备文件的权限没有在权限文件里指定,就使用该参数作为默认权限,默认值分别是:0600/root/root。
      udev_log:是否需要 syslog记录 udev 日志的开关,默认值是 no。

  4. 在目录rules.d/下创建.config规则文件

    1. 如实现u盘自动挂载,则创建11-add-usb.rules文件,内容为:

      1. action!="add",goto="farsight"
        
        kernel=="sd[a-z][0-9]",run+="/sbin/mount-usb.sh %k"
        
        label="farsight"
        
      2. action后是说明是什么事件,kernel后是说明是什么设备比如sda1,mmcblk0p1等,run这个设备插入后去执行哪个程序%k是传入这个程序的参数,这里%k=kernel的值也就是sda1等

      3. 在/sbin/下创建mount-usb.sh文件,添加如下内容:

        1.  #!/bin/sh
          
          /bin/mount -t vfat /dev/$1 /tmp
          sync
          
      4. 修改文件权限为其添加可执行的权限:chmod u+x mount-usb.sh

      5. 至此,就实现了u盘的自动挂载。

    2. 如实现u盘自动卸载,则创建11-remove-usb.rules文件,内容为:

      1. action !="remove",goto="farsight"
        subsystem!="block",goto="farsight"
        kernel=="sd[a-z][0-9]",run+="/sbin/umount-usb.sh"
        label="farsight"
        
      2. 在/sbin/下创建umount-usb.sh文件,添加如下内容:

        1. #!/bin/sh
          
          sync
          umount /tmp/
          
    3. 如实现SD卡自动挂载,则创建12-add-sd.rules文件,内容为:

      1. action!="add",goto="farsight"
        kernel=="mmcblk[0-9]p[0-9]",run+="/sbin/mount-sd.sh %k"
        label="farsight"
        
      2. 在/sbin/下创建mount-sd.sh文件,添加如下内容:

        1. #!/bin/sh
          
          /bin/mount -t vfat /dev/$1 /tmp
          sync
          
    4. 如实现sd卡自动卸载,则创建12-remove-sd.rules文件,内容为:

      1. action !="remove",goto="farsight"
        subsystem!="block",goto="farsight"
        kernel=="mmcblk",run+="/sbin/umount-sd.sh"
        label="farsight"
        
      2. 在/sbin/下创建umount-sd.sh文件,添加如下内容:

        1. #!/bin/sh
          
          sync
          /bin/umount /tmp/
          
  • 下面对规则文件的规则进行简要的价绍:

udev的规则文件放在/lib/udev/rules.d/etc/udev/rules.d两个目录中,后者的优先权较高:后者目录中的规则文件会覆盖前者中同名文件。这个文件通常很短,他可能只是包含几行#开头的注释,然后有几行选项:

udev_root = "/dev/"
udev_rules = "/etc/udev/rules.d/"
udev_log = "err"

上面的第二行非常重要,因为他表示udev规则存储的目录,这个目录存储的是以.rules结束的文件。每一个文件处理一系列规则来帮助udev分配名字给设备文件以保证能被内核识别。在/etc/udev/rules.d下面会有好几个udev规则文件,这些文件一部分是udev包安装的,另外一部分则是可能是别的硬件或者软件包 生成的。这些规则文件的文件名通常是两个数字开头,它表示系统应用该规则的顺序。

规则文件里的规则有一系列的键/值对组成,键/值对之间用逗号(,)分割。每一个键或者是用户匹配键,或者是一个赋值键。匹配键确定规则是否被应用,而赋 值键表示分配某值给该键。这些值将影响udev创建的设备文件。赋值键可以处理一个多值列表。匹配键和赋值键操作符解释见下表:

操作符 匹配/赋值 涵义
== 匹配 相等
!= 匹配 不相等
= 赋值 覆盖赋值
+= 赋值 追加赋值
:= 赋值 赋值后,后面的规则不能覆盖它

常用键列举如下:

ACTION              一个事件的名字,比如add,当设备增加的时候
KERNEL              在内核里看到的设备名字,比如sd*表示任意SCSI磁盘设备
DEVPATH             内核设备目录,比如/devices
SUBSYSTEM           子系统名字,比如sound,net
BUS                 总线的名字,比如IDE,USB
DRIVER              设备驱动的名字,比如ide-cdrom
ID                  独立于内核名字的设备名字
SYSFS{ value}       sysfs属性值,他可以表示任意
ENV{ key}           环境变量,可以表示任意
PROGRAM             可执行的外部程序,如果程序返回0值,该键则认为为真(true)
RESULT              上一个PROGRAM调用返回的标准输出。
NAME                根据这个规则创建的设备文件的文件名。注意:仅仅第一行的NAME描述是有效的,
                    后面的均忽略。 想使用两个以上的名字访问一个设备的话,可以考虑SYMLINK键。
SYMLINK             根据规则创建的字符连接名
OWNER               设备文件的属主。
GROUP               设备文件所在的组。
MODE                设备文件的权限,采用8进制
RUN                 为设备而执行的程序列表
LABEL               在配置文件里为内部控制而采用的名字标签(同下面的GOTO服务)
GOTO                跳到匹配的规则(通过LABEL来标识),有点类似程序语言中的GOTO
IMPORT{ type}       导入一个文件或者一个程序执行后而生成的规则集到当前文件
WAIT_FOR_SYSFS      等待一个特定的设备文件的创建。主要是用作时序和依赖问题。
PTIONS              特定的选项: last_rule 对这类设备终端规则执行; ignore_device 忽略当前规则; ignore_remove 忽略接下来的并移走请求。all_partitions 为所有的磁盘分区创建设备文件

一些规则的例子:

/*匹配任意被内核识别到的设备,然后设定这些设备的属组是root,组是root,
 *访问权限模式是(-rw——-)。这也是一个安全的缺省设置,保证所有的设备在默
 *认情况下只有root可以读写
 */
KERNEL=="*", OWNER="root" GROUP="root", MODE="0600"
    
/*匹配终端设备(tty),然后设置新的权限为0666,所在的组是tty。它也设置了
 *一个特别的设备文件名:%K,代表设备的内核名字。意味着内核识别出这些设备是
 *什么名字,就创建什么样的设备文件名。
 */
KERNEL=="tty", NAME="%k", GROUP="tty", MODE="0666", OPTIONS="last_rule"
    
/*scd[0-9]表示 SCSI CD-ROM 驱动. 它创建一对设备符号连接:cdrom和cdrom-%k。*/
KERNEL=="scd[0-9]*", SYMLINK+="cdrom cdrom-%k"
    
/*hd[a-z]表示ATA CDROM驱动器。这个规则创建和上面的规则相同的符号连接。
 *ATA CDROM驱动器需要sysfs值来区别别的ATA设备,而SCSI CDROM可以被内核唯一识别。
 */
KERNEL=="hd[a-z]", BUS=="ide", SYSFS{removable}=="1", SYSFS{device/media}=="cdrom", SYMLINK+="cdrom cdrom-%k"
    
    
/*告诉udev增加/sbin/modprobe sg 到命令列表,当任意SCSI设备增加到系统后,这些命令将执行。
 *其效果就是当有新的SCSI设备插到计算机中时会自动加载sg模块。
 */    
ACTION=="add", SUBSYSTEM=="scsi_device", RUN+="/sbin/modprobe sg"

在修改udev配置之前,我们通常的考虑是:不要修改系统预置的规则,特别是那些影响非常广泛的配置,比比如上面例子中的第一行。我们正确的做法应该是在/etc/udev/rules.d/下创建一个新的规则文件,并确定该文件的文件名包含的数字序列应该比标准配置文件高。

比如,你要修改floppy设备的所在组,并创建一个新的符号连接/dev/floppy,那你可以这么写:

KERNEL=="fd[0-9]*", GROUP="users", SYMLINK+="floppy"

再如,让USB设备不用root权限访问,则需在/etc/udev/rules.d/目录下新建一个文件,取名可以是90-tofu.rules, 内容如下:

SUBSYSTEM=="usb", ATTRS{idProduct}=="f408", ATTRS{idVendor}=="040e", GROUP="tofu", MODE="0666"

然后重新插拔设备,即可。

再如,想自动加载键盘:

为udev键盘规则文件60-keyboard.rules开头增加:

ACTION=="add",RUN+="/lib/udev/hello.sh"     //当键盘接入时,自动运行hello.sh脚本
ACTION=="remove",RUN+="/lib/udev/bye.sh"    //当键盘拔出时,自动运行bye.sh脚

在脚本文件中添加装卸载命令:

/*  
 * /lib/udev/hello.sh:
 */

#!/bin/bash
sudo -H insmod /hello.ko


/*
 *  /lib/udev/bye.sh:
 */
    
#!/bin/bash
sudo -H rmmod hello

使用udev规则修改rules.d目录下的特定规则文件并创建相应的脚本文件后,插拔USB、PCI等设备时时就能自动加载模块了。

但这只能针对特定的设备,而且过程中需要修改和创建各种文件,太繁琐了。如果我想针对某一类设备时又该怎么办呢?

  • MODULE_DEVICE_TABLE实现自动加载

这是一种单纯地仅需在模块的源代码级实现自动加载:

首先,使用MODULE_DEVICE_TABLE宏注册模块。

接着,编译模块并将编译后产生的.ko文件拷贝至/lib/modules/ `uname -r`/目录下。

然后,使用sudo depmod -a命令将新的模块信息加入/lib/modules/ `uname -r`/目录下的modules.alias和modules.dep文件中。

#define USB_KEYBOARD_VENDOR_ID 0x093a
#define USB_KEYBOARD_PRODUCT_ID 0x2510

static struct usb_device_id usb_kbd_id_table[] = 
{
    {USB_DEVICE(USB_KEYBOARD_VENDOR_ID, USB_KEYBOARD_PRODUCT_ID) },
    {},
};

MODULE_DEVICE_TABLE(usb, usb_kbd_id_table);

上面的代码是实现一块键盘连接上计算机后自动加载模块这个功能所需在模块中添加的部分。VENDOR_ID和PRODUCT_ID每个键盘是不一样的,可以把键盘连接在计算机后,使用lsusb命令确定键盘的这两个值。如果你需要对每个 USB 设备都响应而不是特定的VENDOR_ID和PRODUCT_ID值, 那么需要创建一个只设置这个 driver_info 成员的入口项:

static struct usb_device_id usb_ids[] =
{
    {.driver_info = 42},
    {},
};

MODULE_DEVICE_TABLE(usb, usb_ids);

如果只想对所有的USB键盘做响应,那么是这样的:

static struct usb_device_id usb_kbd_id_table[] =
{
    { USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID,
        USB_INTERFACE_SUBCLASS_BOOT,
        USB_INTERFACE_PROTOCOL_KEYBOARD) },
    {},
};

MODULE_DEVICE_TABLE(usb, usb_kbd_id_table);

如果,想在一个设备上使用sysfs信息来唯一标识一个设备。这些信息最好通过udevinfo命令来获取:

你把设备插入系统后,系统为设备产生了设备名(如/dev/sda)。那样的
话,你先用udevinfo -q path -n/dev/sda,命令会产生一个该设备名对应的在sysfs下的路径,如/block/sda。然后,你再用udevinfo -a -p/sys/block/sda,这个命令会显示一堆信息,信息分成很多块。这些信息实际来自于操作系统维护的sysfs链表,不同的块对应不同的路径。你就可以用这些信息来作为udev规则文件中的匹配项。但需要注意的是,同一个规则只能使用同一块中显示的信息,不能跨块书写规则。

[root@localhost rules.d]# udevinfo -a -p $(udevinfo -q path        -n      /dev/hda1)
Udevinfo starts with the device specified by the devpath and then walks up the chain of
parent devices. It prints for every device found,all possible attributes in the udev rules
key format. A rule to match, can be composed by the attributes of the device and the
attributes from one single parent device.

looking at device '/block/hda/hda1':    
KERNEL=="hda1"     SUBSYSTEM=="block"     DRIVER==""       
ATTR{stat}=="        1133         2268            2            4"         ATTR{size}=="208782"
ATTR{start}=="63"         ATTR{dev}=="3:1"        looking at parent device '/block/hda':  

KERNELS=="hda"     SUBSYSTEMS=="block"     DRIVERS==""       
ATTRS{stat}=="28905 18814 1234781 302540 34087 133247 849708 981336 0 218340 1283968"
ATTRS{size}=="117210240"         ATTRS{removable}=="0"       
ATTRS{range}=="64"         ATTRS{dev}=="3:0"

looking at parent device '/devices/pci0000:00/0000:00:1f.1/ide0/0.0':    

KERNELS=="0.0"     SUBSYSTEMS=="ide"     DRIVERS=="ide-disk"       
ATTRS{modalias}=="ide:m-disk"         ATTRS{drivename}=="hda"      
ATTRS{media}=="disk"      
looking at parent device '/devices/pci0000:00/0000:00:1f.1/ide0':    

KERNELS=="ide0"     SUBSYSTEMS==""     DRIVERS==""      
looking at parent device '/devices/pci0000:00/0000:00:1f.1':    
KERNELS=="0000:00:1f.1"     SUBSYSTEMS=="pci"     DRIVERS=="PIIX_IDE"       
ATTRS{broken_parity_status}=="0"         ATTRS{enable}=="1"       
ATTRS{modalias}=="pci:v00008086d000024CAsv0000144Dsd0000C009bc01sc01i8a"
ATTRS{local_cpus}=="1"         ATTRS{irq}=="11"         ATTRS{class}=="0x01018a"
ATTRS{subsystem_device}=="0xc009"         ATTRS{subsystem_vendor}=="0x144d"
ATTRS{device}=="0x24ca"         ATTRS{vendor}=="0x8086"      
looking at parent device '/devices/pci0000:00':    

KERNELS=="pci0000:00"     SUBSYSTEMS==""     DRIVERS=="" 

1.3 mdev

udev是linux2.6内核引入的一种新的设备文件管理机制,用于取代老的devfs.udev最大的有点就是可以动态的管理/dev目录下的设备文件,而不用再系统初始化时就将可能要用到的设备都创建起来,还可以根据设备具体信息命名设备节点,而不是有内核统一分配。但因为udev较mdev复杂,不太适合嵌入式使用,所以在嵌入式邻域一般更多的使用mdev。它是一个简化版的udev,是busybox所带的程序,十分适合嵌入式系统。

udev 和mdev 是两个使用uevent 机制处理热插拔问题的用户空间程序,两者的实现机理不同。udev 是基于netlink 机制的,它在系统启动时运行了一个deamon 程序udevd,通过监听内核发送的uevent 来执行相应的热拔插动作,包括创建/删除设备节点,加载/卸载驱动模块等等。mdev 是基于uevent_helper 机制的,它在系统启动时修改了内核中的uevnet_helper 变量(通过写/proc/sys/kernel/hotplug),值为“/sbin/mdev”。这样内核产生uevent 时会调用uevent_helper 所指的用户级程序,也就是mdev,来执行相应的热拔插动作。

udev 使用的netlink 机制在有大量uevent 的场合效率高,适合用在PC 机上;而mdev 使用的uevent_helper 机制实现简单,适合用在嵌入式系统中。另外要说明的一点是,uevent_helper 的初始值在内核编译时是可配置的,默认值为/sbin/hotplug。如果想修改它的值,写/proc/sys/kernel/hotplug 文件就可以了,例如:

echo “/sbin/mdev” > /proc/sys/kernel/hotplug

而如果使用的是udevd,那么uevent_helper变量应为空,即

echo "" > /proc/sys/kernel/hotplug

接下来我们可以分析下热拔插事件:首先,当在驱动程序中创建设备节点的时候,流程都是”先创建一个类” 然后在”在类下创建设备”。比如创建一个/dev/xxx 代码如下:

static struct class *class;
static struct class_device  *class_dev;
class = class_create(THIS_MODULE, "firstdrv");
class_dev = class_device_create(class , NULL, MKDEV(major, 0), NULL, "xxx");

深入class_device_create函数内部,分析函数调用情况:

class_device_create
    class_device_register
        class_device_add
            kobject_uevent(&class_dev->kobj, KOBJ_ADD);
                kobject_uevent_env
                    /*action_string = "add"*/
                    action_to_string(KOBJ_ADD);  /*add*/
                    /* environment values */
                    /* 分配保存环境变量的内存 */
                    buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);

                    /*设置环境变量*/
                    scratch = buffer;
                    envp [i++] = scratch;
                    scratch += sprintf(scratch, "ACTION=%s", action_string) + 1;
                    envp [i++] = scratch;
                    scratch += sprintf (scratch, "DEVPATH=%s", devpath) + 1;
                    envp [i++] = scratch;
                    scratch += sprintf(scratch, "SUBSYSTEM=%s", subsystem) + 1;

                    call_usermodehelper (argv[0], argv, envp, 0);
                    /*调用用户模式下的辅助程序*/

再分析Busybox的mdev.c

mdev_main
        temp = /sys/class/xxx_drv/xxx
        action = getenv("ACTION");
        env_path = getenv("DEVPATH")
        make_device(temp, 0);
            fd = open("/etc/mdev.conf", O_RDONLY);/*根据这个配置文件*/
            /*如果没有这个配置文件的话  就会创建设备节点*/
            /* 确定设备文件名,类型,主次设备号 */
            device_name = bb_basename(path);  /* = "xxx" */


            /* 'c' == > 字符设备节点;根据"/sys/class/xxx_drv/xxx/dev"的内容确定主次设备号*/
            mknod(device_name, mode | type, makedev(major, minor));

其实热拔插事件就是利用设备的加入(add)或者移除(remove),然后把设备的详细信息输出到sys/下 ,调用/sbin/mdev根据环境变量中的 ACTION 和 DEVPATH,来确定此次热插拔事件的动作以及影响了/sys中的那个目录。接着会看看这个目录中是否有“dev”的属性文件,如果有就利用这些信息为这个设备在/dev 下创建设备节点文件。

  • 实现u盘的自动挂载步骤:

    • ①用busybox制作根文件系统的时候,要选择支持mdev机制

      • Linux System Utilities  --->   
                   [*] mdev      
                   [*] Support /etc/mdev.conf
                   [*] Support command execution at device addition/removal
        
    • ②在/etc/init.d/rsC文件中添加如下内容

      • mount -t tmpfs mdev /dev
        mkdir /dev/pts
        mount -t devpts devpts /dev/pts
            
        #它是mdev运行的基础条件之一    
        mount -t sysfs sysfs /sys 
        
        #设置系统的hotplug程序为mdev,当有热插拔事件产生时,内核就会调用位于 /sbin目录的mdev。这时mdev通过环境变量中的 ACTION 和 DEVPATH,来确定此次热插拔事件的动作以及影响了/sys中的哪个目录。接着会看看这个目录中是否有“dev”属性文件,如果有就利用这些信息为这个设备在/dev 下创建设备节点文件。
        echo "/sbin/mdev" > /proc/sys/kernel/hotplug
        
        #调用/sbin/mdev程序(其实是个链接,作用是传递参数给/bin目录下的busybox程序并调用它),检索/sys/class、/sys/block和/proc中所有类设备目录,如果在目录中含有名为“dev”的文件且文件中包含的是设备号,则mdev就利用这些信息为这个设备在/dev 下创建设备节点文件。
        mdev –s
        
    • 确保编译内核时编译如下选项:

      • CONFIG_PROC_FS=y
        CONFIG_PROC_SYSCTL=y
        CONFIG_HOTPLUG=y
        CONFIG_NET=y
        
  • ③在/etc/mdev.conf文件中添加对热插拔事件的响应,实现U盘和SD卡的自动挂载。

    • sd[a-z][0-9]   0:0 666  @/etc/mdev/udisk_insert.sh                
      sd[a-z]        0:0 666  $/etc/mdev/udisk_remove.sh
      
    • @:表示在插入(创建设备结点)后执行后面的脚本;$:表示在拔出(删除设备结点)前执行后面的脚本。*:表示在创建设备结点后,删除节点设备前执行后面的脚本。

    • /etc/mdev/udisk_insert .sh

      • #!/bin/sh
        
        if [ -d /sys/block/*/$MDEV ] ;then
            mkdir -p /media/$MDEV
            mount /dev/$MDEV /media/$MDEV 
        fi
        
    • etc/mdev/udisk_remove.sh

      • #!/bin/sh
        
        umount -l /media/sd*
        rm -rf /media/sd*
        
    • 以上两个脚本需要可执行权限:

      • chmod +x  /etc/mdev/udisk_insert.sh
        chmod +x  /etc/mdev/udisk_remove.sh
        
  • 最后附上mdev官方文档供大家参考:

-------------
 MDEV Primer
-------------

For those of us who know how to use mdev, a primer might seem lame.  For
everyone else, mdev is a weird black box that they hear is awesome, but can't
seem to get their head around how it works.  Thus, a primer.

-----------
 Basic Use
-----------

Mdev has two primary uses: initial population and dynamic updates.  Both
require sysfs support in the kernel and have it mounted at /sys.  For dynamic
updates, you also need to have hotplugging enabled in your kernel.

Here's a typical code snippet from the init script:
[0] mount -t proc proc /proc
[1] mount -t sysfs sysfs /sys
[2] echo /bin/mdev > /proc/sys/kernel/hotplug
[3] mdev -s

Alternatively, without procfs the above becomes:
[1] mount -t sysfs sysfs /sys
[2] sysctl -w kernel.hotplug=/bin/mdev
[3] mdev -s


Of course, a more "full" setup would entail executing this before the previous
code snippet:
[4] mount -t tmpfs -o size=64k,mode=0755 tmpfs /dev
[5] mkdir /dev/pts
[6] mount -t devpts devpts /dev/pts

The simple explanation here is that [1] you need to have /sys mounted before
executing mdev.  Then you [2] instruct the kernel to execute /bin/mdev whenever
a device is added or removed so that the device node can be created or
destroyed.  Then you [3] seed /dev with all the device nodes that were created
while the system was booting.

For the "full" setup, you want to [4] make sure /dev is a tmpfs filesystem
(assuming you're running out of flash).  Then you want to [5] create the
/dev/pts mount point and finally [6] mount the devpts filesystem on it.

-------------
 MDEV Config   (/etc/mdev.conf)
-------------

Mdev has an optional config file for controlling ownership/permissions of
device nodes if your system needs something more than the default root/root
660 permissions.

The file has the format:
    <device regex>       <uid>:<gid> <octal permissions>
 or @<maj[,min1[-min2]]> <uid>:<gid> <octal permissions>

For example:
    hd[a-z][0-9]* 0:3 660

The config file parsing stops at the first matching line.  If no line is
matched, then the default of 0:0 660 is used.  To set your own default, simply
create your own total match like so:
    .* 1:1 777

You can rename/move device nodes by using the next optional field.
    <device regex> <uid>:<gid> <octal permissions> [=path]
So if you want to place the device node into a subdirectory, make sure the path
has a trailing /.  If you want to rename the device node, just place the name.
    hda 0:3 660 =drives/
This will move "hda" into the drives/ subdirectory.
    hdb 0:3 660 =cdrom
This will rename "hdb" to "cdrom".

Similarly, ">path" renames/moves the device but it also creates
a direct symlink /dev/DEVNAME to the renamed/moved device.

If you also enable support for executing your own commands, then the file has
the format:
    <device regex> <uid>:<gid> <octal permissions> [=path] [@|$|*<command>]
    or
    <device regex> <uid>:<gid> <octal permissions> [>path] [@|$|*<command>]
The special characters have the meaning:
    @ Run after creating the device.
    $ Run before removing the device.
    * Run both after creating and before removing the device.

The command is executed via the system() function (which means you're giving a
command to the shell), so make sure you have a shell installed at /bin/sh.  You
should also keep in mind that the kernel executes hotplug helpers with stdin,
stdout, and stderr connected to /dev/null.

For your convenience, the shell env var $MDEV is set to the device name.  So if
the device "hdc" was matched, MDEV would be set to "hdc".

----------
 FIRMWARE
----------

Some kernel device drivers need to request firmware at runtime in order to
properly initialize a device.  Place all such firmware files into the
/lib/firmware/ directory.  At runtime, the kernel will invoke mdev with the
filename of the firmware which mdev will load out of /lib/firmware/ and into
the kernel via the sysfs interface.  The exact filename is hardcoded in the
kernel, so look there if you need to know how to name the file in userspace.

------------
 SEQUENCING
------------

Kernel does not serialize hotplug events. It increments SEQNUM environmental
variable for each successive hotplug invocation. Normally, mdev doesn't care.
This may reorder hotplug and hot-unplug events, with typical symptoms of
device nodes sometimes not created as expected.

However, if /dev/mdev.seq file is found, mdev will compare its
contents with SEQNUM. It will retry up to two seconds, waiting for them
to match. If they match exactly (not even trailing '\n' is allowed),
or if two seconds pass, mdev runs as usual, then it rewrites /dev/mdev.seq
with SEQNUM+1.

IOW: this will serialize concurrent mdev invocations.

If you want to activate this feature, execute "echo >/dev/mdev.seq" prior to
setting mdev to be the hotplug handler. This writes single '\n' to the file.
NB: mdev recognizes /dev/mdev.seq consisting of single '\n' characher
as a special case. IOW: this will not make your first hotplug event
to stall for two seconds.

推荐阅读更多精彩内容