linux 热插拔

2019-07-12 19:18发布

热插拔
有 2 个不同角度来看待热插拔:
   从内核角度看,热插拔是在硬件、内核和内核驱动之间的交互。
   从用户角度看,热插拔是内核和用户空间之间,通过调用用户空间程序(如hotplug、udev 和 mdev)的交互。 当需要通知用户内核发生了某种热插拔事件时,内核才调用这个用户空间程序。
现在的计算机系统,要求 Linux 内核能够在硬件从系统中增删时,可靠稳定地运行。这就对设备驱动作者增加了压力,因为在他们必须处理一个毫无征兆地突然出现或消失的设备。
热插拔工具
当用户向系统添加或删除设备时,内核会产生一个热插拔事件,并在 /proc/sys/kernel/hotplug 文件里查找处理设备连接的用户空间程序。这个用户空间程序主要有 hotplug:这个程序是一个典型的 bash 脚本, 只传递执行权给一系列位于 /etc/hot-plug.d/ 目录树的程序。hotplug 脚本搜索所有的有 .hotplug 后缀的可能对这个事件进行处理的程序并调用它们, 并传递给它们许多不同的已经被内核设置的环境变量。(基本已被淘汰,具体内容请参阅《LDD3》) udev :用于linux2.6.13或更高版本的内核上,为用户空间提供使用固定设备名的动态/dev目录的解决方案。它通过在 sysfs 的 /class/ 和/block/ 目录树中查找一个称为 dev 的文件,以确定所创建的设备节点文件的主次设备号。所以要使用udev,驱动必须为设备在sysfs中创建类接口及其dev属性文件,方法和sculld模块中创建dev属性相同。 udev的资料网上十分丰富,我就不在这废话了,给出以下链接有兴趣的自己研究: 《UDEV Primer》(英文),地址:http://webpages.charter.net/decibelshelp/LinuxHelp_UDEVPrimer.html   《udev规则编写》(luofuchong翻译),地址:http://www.cnitblog.com/luofuchong/archive/2007/12/18/37831.html   《什么是udev》地址:http://blog.csdn.net/steganography/archive/2006/04/10/657620.aspx   《udev-FAQ 中文翻译》地址:http://gnawux.bokee.com/3225765.html   《udev轻松上路》地址:http://www.blog.edu.cn/user1/3313/archives/2007/1635169.shtml   《Udev (简体中文)》地址:http://wiki.archlinux.org/index.php/Udev_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)   Udev官方主页:http://www.kernel.org/pub/linux/utils/kernel/hotplug/udev.html 下载地址:http://www.kernel.org/pub/linux/utils/kernel/hotplug/   在《LFS》中也有介绍udev的使用,很值得参考!下载地址:http://lfs.osuosl.org/lfs/downloads/stable/     mdev:一个简化版的udev,是busybox所带的程序,十分适合嵌入式系统。   因为hotplug现在也在被慢慢地淘汰,udev不再依赖hotplug了,所以这里不再介绍; udev较mdev复杂,不太适合嵌入式使用。(本人也有做udev的实验,交叉编译是通过了,但是使用上有问题,没有实现其功能。也许是我的文件系统没做好,以后有时间再研究和写记录。有成功高人的通知一声,交流一下经验。^_^谢谢!); mdev简单易用,比较适合嵌入式系统,实验成功。以下详细介绍mdev的使用。   ================================     设备节点的创建,是通过sysfs接口分析dev文档取得设备节点号,这个很显而易见。那么udevd是通过什么机制来得知内核里模块的变化情况,如何得知设备的插入移除情况呢?当然是通过hotplug机制了,那 hotplug又是怎么实现的?或说内核是如何通知用户空间一个事件的发生的呢?
答案是通过netlink socket通讯,在内核和用户空间之间传递信息。
内核调用kobject_uevent函数发送netlink message给用户空间,这部分工作通常无需驱动去自己处理,在统一设备模型里面,在子系统这一层面,已将这部分代码处理好了,包括在设备对应的特定的 Kobject创建和移除的时候都会发送相应add和remove消息,当然前提是您在内核中配置了hotplug的支持。
Netlink socket作为一种内核和用户空间的通信方式,不但仅用在hotplug机制中,同样还应用在其他很多真正和网络相关的内核子系统中。
Udevd通过标准的socket机制,创建socket连接来获取内核广播的uevent事件 并解析这些uevent事件 Udevtrigger的工作机制
运行udevd以后,使用udevtrigger的时候,会把内核中已存在的设备的节点创建出来,那么他是怎么做到这一点的? 分析udevtrigger的代码能够看出:
udevtrigger通过向/sysfs 文档系统下现有设备的uevent节点写"add"字符串,从而触发uevent事件,使得udevd能够接收到这些事件,并创建buildin的设备驱动的设备节点连同任何已insmod的模块的设备节点。
所以,我们也能够手工用命令行来模拟这一过程:
/ # echo "add" > /sys/block/mtdblock2/uevent
/ # 
/ # UEVENT[178.415520] add      /block/mtdblock2 (block)
但是,进一步看代码,您会发现,实际上,不管您往uevent里面写什么,都会触发add事件,这个从kernel内部对uevent属性的实现函数能够看出来,默认的实现是:
static ssize_t store_uevent(struct device *dev, struct device_attribute *attr,
                         const char *buf, size_t count)
{
       kobject_uevent(&dev->kobj, KOBJ_ADD);
       return count;
}
所以不管写的内容是什么,都是触发add操作,真遗憾,我还想通过这个属性实验remove的操作。 不知道这样限制的原因是什么。
而udevstart的实现方式和udevtrigger就不同了,他基本上是重复实现了udevd里面的机制,通过遍历sysfs,自己完成设备节点的创建,不通过udevd来完成。
      udevd创建每一个节点的时候,都会fork出一个新的进程来单独完成这个节点的创建工作。
      Uevent_seqnum 用来标识当前的uevent事件的序号(已产生了多少uevent事件),您能够通过如下操作来查看:
$ cat /sys/kernel/uevent_seqnum
2673       udev的工作原理 当系统内核发现安装或者卸载了某一个硬件设备时,内核会执行hotplug,以便让hotplug去安装或卸载该硬件的驱动程序;hotplug在处理完硬件的驱动程序后,就会去呼叫执行udevd,以便让udevd可以产生或者删除硬件的设备文件。 接着udevd会通过libsysfs读取sys文件系统,以便取得该硬件设备的信息;然后再向namedev查询该外部设备的设备文件信息,例如文件的名称、权限等。最后,udevd就依据上述的结果,在/dev/目录中自动建立该外部设备的设备文件,同时在/etc/udev/rules.d下检查有无针对该设备的使用权限     ====================

1.kobject, ktype, kset

kobject代表sysfs中的目录。 ktype代表kobject的类型,主要包含release函数和attr的读写函数。比如,所有的bus都有同一个bus_type;所有的class都有同一个class_type。 kset包含了subsystem概念,kset本身也是一个kobject,所以里面包含了一个kobject对象。另外,kset中包含kset_uevent_ops,里面主要定义了三个函数        int (*filter)(struct kset *kset, struct kobject *kobj);        const char *(*name)(struct kset *kset, struct kobject *kobj);        int (*uevent)(struct kset *kset, struct kobject *kobj, struct kobj_uevent_env *env); 这三个函数都与uevent相关。filter用于判断uevent是否要发出去。name用于得到subsystem的名字。uevent用于填充env变量。

2.uevent内核部分

uevent是sysfs向用户空间发出的消息。比如,device_add函数中,会调用kobject_uevent(&dev->kobj, KOBJ_ADD); 这里kobj是发消息的kobj,KOBJ_ADD是发出的事件。uevent的事件在kobject_action中定义: enum kobject_action {        KOBJ_ADD,        KOBJ_REMOVE,        KOBJ_CHANGE,        KOBJ_MOVE,        KOBJ_ONLINE,        KOBJ_OFFLINE,        KOBJ_MAX };   int kobject_uevent(struct kobject *kobj, enum kobject_action action) {        return kobject_uevent_env(kobj, action, NULL); }   kobject_uevent_env:        由kobject的parent向上查找,直到找到一个kobject包含kset。        如果kset中有filter函数,调用filter函数,看看是否需要过滤uevent消息。        如果kset中有name函数,调用name函数得到subsystem的名字;否则,subsystem的名字是kset中kobject的名字。        分配一个kobj_uevent_env,并开始填充env环境变量:        增加环境变量ACTION=        增加环境变量DEVPATH=        增加环境变量SUBSYSTEM=        增加环境变量kobject_uevent_env中参数envp_ext指定的环境变量。        调用kset的uevent函数,这个函数会继续填充环境变量。        增加环境变量SEQNUM=,这里seq是静态变量,每次累加。        调用netlink发送uevent消息。        调用uevent_helper,最终转换成对用户空间sbin/mdev的调用。

3.uevent用户空间部分

uevent的用户空间程序有两个,一个是udev,一个是mdev。 udev通过netlink监听uevent消息,它能完成两个功能:        1.自动加载模块        2.根据uevent消息在dev目录下添加、删除设备节点。 另一个是mdev,mdev在busybox的代码包中能找到,它通过上节提到的uevent_helper函数被调用。   下面简要介绍udev的模块自动加载过程: etc目录下有一个uevent规则文件/etc/udev/rules.d/50-udev.rules udev程序收到uevent消息后,在这个规则文件里匹配,如果匹配成功,则执行这个匹配定义的shell命令。例如,规则文件里有这么一行: ACTION=="add", SUBSYSTEM=="?*", ENV{MODALIAS}=="?*", RUN+="/sbin/modprobe $env{MODALIAS}" 所以,当收到uevent的add事件后,shell能自动加载在MODALIAS中定义的模块。   mdev的模块自动加载过程与之类似,它的配置文件在/etc/mdev.conf中。例如: $MODALIAS=.* 0:0 660 @modprobe "$MODALIAS" 这条规则指的是:当收到的环境变量中含有MODALIAS,那么加载MODALIAS代表的模块。 mdev的详细说明在busybox的docs/mdev.txt中。

4.uevent在设备驱动模型中的应用

在sys目录下有一个子目录devices,代表一个kset。 创建设备时,调用的device_initialize函数中,默认会把kset设置成devices_kset,即devices子目录代表的kset。 devices_kset中设置了uevent操作集device_uevent_ops。 static struct kset_uevent_ops device_uevent_ops = {        .filter =    dev_uevent_filter,        .name =   dev_uevent_name,        .uevent = dev_uevent, };   dev_uevent_filter中,主要是规定了要想发送uevent,dev必须有class或者bus。 dev_uevent_name中,返回dev的class或者bus的名字。 dev_uevent函数:        如果dev有设备号,添加环境变量MAJOR与MINOR。        如果dev->type有值,设置DEVTYPE=<dev->type->name>。        如果dev->driver,设置DRIVER=<dev->driver->name>。        如果有bus,调用bus的uevent函数。        如果有class,调用class的uevent函数。 如果有dev->type,调用dev->type->uevent函数。   一般在bus的uevent函数中,都会添加MODALIAS环境变量,设置成dev的名字。这样,uevent传到用户空间后,就可以通过对MODALIAS的匹配自动加载模块。这样的bus例子有platform和I2C等等。 ==========================   热插拔(hotplug,打这个词的时候我常常想到热干面)不一定非要指类似U盘那样的插入拔出,此处的热插拔广义上讲,是指一个设备加入系统,内核如何通知用户空间。举个简单的例子,如果你的电脑中有块PCI网卡,针对该网卡的驱动程序以内核模块的形式被编译(obj-m),那么Linux系统在启动过程中是如何自动加载该网卡的驱动模块呢?大家都知道现在udev负责干这事,其实除了udev,还可以有其他的手法,你自己就可以这样做。

我们先讨论udev,udev最关键的东西是当系统发现一个设备时,它要能够被通知该事件,一旦它知道了这件事,那么余下的事情就都好说了,无非是个如何查找模块并加载的过程。所以我们看到,这里的关键是热插拔事件的通知机制。Linux的设备模型为此提供了非常完美的支持,其原理其实发源于kset这一层,对此在《深入Linux设备驱动程序内核机制》一书中有详细的描述,虽然这部分看起来蛮复杂,貌似挺能吓唬住一些新手,其实说白了,要点就是通过sysfs建立关系,沟通内核与用户空间,然后就是uevent,也就是下面要说的热插拔事件。

当然设备驱动程序一般不会和这些太底层的kobject/kset家伙打交道,因为更高层次的device,bus和driver把kobject/kset那一层的细节实现都给封装了起来。所以设备热插拔的uevent事件最终的源头来自于device_add,本帖这里肯定不会讨论device与driver如何绑定那一摊子事情。下面看看device_add的源码,是如何实现uevent机制的:
  1.    
  2.     int device_add(struct device *dev)
  3.     {
  4.           ...
  5.           kobject_uevent(&dev->kobj, KOBJ_ADD);
  6.           ...
  7.     }
复制代码 热插拔的核心实现就那一个函数调用,这里device_add对应的是KOBJ_ADD,那么移除设备自然对应KOBJ_REMOVE了。kobject_uevent函数最终调用的是kobject_uevent_env,后者才是真正干事的伙计。
下面给出kobject_uevent_env函数的核心框架:
  1.     int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
  2.                            char *envp_ext[])
  3.     {
  4.             ...
  5.     #if defined(CONFIG_NET)
  6.             /* send netlink message */
  7.             ...
  8.     #endif
  9.  
  10.             /* call uevent_helper, usually only enabled during early boot */
  11.             if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {
  12.                     char *argv [3];
  13.  
  14.                     argv [0] = uevent_helper;
  15.                     argv [1] = (char *)subsystem;
  16.                     argv [2] = NULL;
  17.                     retval = add_uevent_var(env, "HOME=/");
  18.                     if (retval)
  19.                             goto exit;
  20.                     retval = add_uevent_var(env,
  21.                                             "PATH=/sbin:/bin:/usr/sbin:/usr/bin");
  22.                     if (retval)
  23.                             goto exit;
  24.  
  25.                     retval = call_usermodehelper(argv[0], argv,
  26.                                                  env->envp, UMH_WAIT_EXEC);
  27.             }
  28.  
  29.             ...
  30.     }
复制代码 怎么样,够简洁吧,其实看实际的代码比这要郁闷地多,不过骨架清晰就行了。代码中的netlink message就不用多说了吧,给udev发通知用(有时间的话可以分析分析udev的代码)。本帖重点讨论后半段的if (uevent_helper[0] && !kobj_usermode_filter(kobj))代码,这里的核心调用是call_usermodehelper,这个函数最有意思的地方就在于在内核空间调用用户空间的程序,它的详细实现机制在书中已经讲得很多,这里就不再赘述了。call_usermodehelper在kobject_uevent_env函数中要调用的用户空间程序由uevent_helper[0]来指定,所以如果我们能控制这个uevent_helper[0],就能接收到设备加入系统移出系统等事件。那个if中的kobj_usermode_filter条件一般都会满足(除非这是个特别注意个人隐私的设备,那就不好说了,人家偷偷加入系统就是不想让你知道你也没有办法,但是udev还是能知道的)。

下面看看uevent_helper[0]来自何处:
  1.    
  2.     char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH;
复制代码 貌似要通过内核配置来指定,我看了一下我系统中Linux目录下的.config文件,找到了下面这行:
  1. #
  2. # Generic Driver Options
  3. #
  4. CONFIG_UEVENT_HELPER_PATH=""
复制代码 丫的,居然没指定,那么uevent_helper[0]="",这样的话我们在kobject_uevent_env函数中的那个if语句就没法满足了,看来要重新配置再编译内核了。不过想想sysfs这么强大,内核开发的那帮人好歹给留个用户空间的接口出来吧,一查看还真有:

  1.     static ssize_t uevent_helper_store(struct kobject *kobj,
  2.                                        struct kobj_attribute *attr,
  3.                                        const char *buf, size_t count)
  4.     {
  5.             if (count+1 > UEVENT_HELPER_PATH_LEN)
  6.                     return -ENOENT;
  7.             memcpy(uevent_helper, buf, count);
  8.             uevent_helper[count] = '