Linux设备模型(3)_Uevent

作者:wowo 发布于:2014-3-10 20:39 分类:统一设备模型

1. Uevent的功能

Uevent是Kobject的一部分,用于在Kobject状态发生改变时,例如增加、移除等,通知用户空间程序。用户空间程序收到这样的事件后,会做相应的处理。

该机制通常是用来支持热拔插设备的,例如U盘插入后,USB相关的驱动软件会动态创建用于表示该U盘的device结构(相应的也包括其中的kobject),并告知用户空间程序,为该U盘动态的创建/dev/目录下的设备节点,更进一步,可以通知其它的应用程序,将该U盘设备mount到系统中,从而动态的支持该设备。

2. Uevent在kernel中的位置

下面图片描述了Uevent模块在内核中的位置:

uevent

由此可知,Uevent的机制是比较简单的,设备模型中任何设备有事件需要上报时,会触发Uevent提供的接口。Uevent模块准备好上报事件的格式后,可以通过两个途径把事件上报到用户空间:一种是通过kmod模块,直接调用用户空间的可执行文件;另一种是通过netlink通信机制,将事件从内核空间传递给用户空间。

注1:有关kmod和netlink,会在其它文章中描述,因此本文就不再详细说明了。

3. Uevent的内部逻辑解析

3.1 Source Code位置

Uevent的代码比较简单,主要涉及kobject.h和kobject_uevent.c两个文件,如下:

  • include/linux/kobject.h
  • lib/kobject_uevent.c
3.2 数据结构描述

kobject.h定义了uevent相关的常量和数据结构,如下:

  • kobject_action
 1: /* include/linux/kobject.h, line 50 */
 2: enum kobject_action {   
 3:     KOBJ_ADD,
 4:     KOBJ_REMOVE,    
 5:     KOBJ_CHANGE, 
 6:     KOBJ_MOVE,
 7:     KOBJ_ONLINE, 
 8:     KOBJ_OFFLINE,
 9:     KOBJ_MAX 
 10: };

kobject_action定义了event的类型,包括:

ADD/REMOVE,Kobject(或上层数据结构)的添加/移除事件。

ONLINE/OFFLINE,Kobject(或上层数据结构)的上线/下线事件,其实是是否使能。

CHANGE,Kobject(或上层数据结构)的状态或者内容发生改变。

MOVE,Kobject(或上层数据结构)更改名称或者更改Parent(意味着在sysfs中更改了目录结构)。

CHANGE,如果设备驱动需要上报的事件不再上面事件的范围内,或者是自定义的事件,可以使用该event,并携带相应的参数。

  • kobj_uevent_env
 1: /* include/linux/kobject.h, line 31 */
 2: #define UEVENT_NUM_ENVP         32 /* number of env pointers */
 3: #define UEVENT_BUFFER_SIZE      2048 /* buffer for the variables */
 4:  
 5: /* include/linux/kobject.h, line 116 */
 6: struct kobj_uevent_env {
 7:     char *envp[UEVENT_NUM_ENVP];
 8:     int envp_idx;
 9:     char buf[UEVENT_BUFFER_SIZE];
 10:    int buflen;
 11: };

前面有提到过,在利用Kmod向用户空间上报event事件时,会直接执行用户空间的可执行文件。而在Linux系统,可执行文件的执行,依赖于环境变量,因此kobj_uevent_env用于组织此次事件上报时的环境变量。

envp,指针数组,用于保存每个环境变量的地址,最多可支持的环境变量数量为UEVENT_NUM_ENVP。

envp_idx,用于访问环境变量指针数组的index。

buf,保存环境变量的buffer,最大为UEVENT_BUFFER_SIZE。

buflen,访问buf的变量。

  • kset_uevent_ops
 1: /* include/linux/kobject.h, line 123 */
 2: struct kset_uevent_ops {
 3:     int (* const filter)(struct kset *kset, struct kobject *kobj);
 4:     const char *(* const name)(struct kset *kset, struct kobject *kobj);
 5:     int (* const uevent)(struct kset *kset, struct kobject *kobj,
 6:                         struct kobj_uevent_env *env);
 7: };

kset_uevent_ops是为kset量身订做的一个数据结构,里面包含filter和uevent两个回调函数,用处如下:

filter,当任何Kobject需要上报uevent时,它所属的kset可以通过该接口过滤,阻止不希望上报的event,从而达到从整体上管理的目的。

name,该接口可以返回kset的名称。如果一个kset没有合法的名称,则其下的所有Kobject将不允许上报uvent

uevent,当任何Kobject需要上报uevent时,它所属的kset可以通过该接口统一为这些event添加环境变量。因为很多时候上报uevent时的环境变量都是相同的,因此可以由kset统一处理,就不需要让每个Kobject独自添加了。

3.3 内部动作

通过kobject.h,uevent模块提供了如下的API(这些API的实现是在"lib/kobject_uevent.c”文件中):

 1: /* include/linux/kobject.h, line 206 */
 2: int kobject_uevent(struct kobject *kobj, enum kobject_action action);
 3: int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
 4:                         char *envp[]);
 5:  
 6: __printf(2, 3)
 7: int add_uevent_var(struct kobj_uevent_env *env, const char *format, ...);
 8:  
 9: int kobject_action_type(const char *buf, size_t count,
 10:                         enum kobject_action *type);

kobject_uevent_env,以envp为环境变量,上报一个指定action的uevent。环境变量的作用是为执行用户空间程序指定运行环境。具体动作如下:

  • 查找kobj本身或者其parent是否从属于某个kset,如果不是,则报错返回(注2:由此可以说明,如果一个kobject没有加入kset,是不允许上报uevent的
  • 查看kobj->uevent_suppress是否设置,如果设置,则忽略所有的uevent上报并返回(注3:由此可知,可以通过Kobject的uevent_suppress标志,管控Kobject的uevent的上报
  • 如果所属的kset有uevent_ops->filter函数,则调用该函数,过滤此次上报(注4:这佐证了3.2小节有关filter接口的说明,kset可以通过filter接口过滤不希望上报的event,从而达到整体的管理效果
  • 判断所属的kset是否有合法的名称(称作subsystem,和前期的内核版本有区别),否则不允许上报uevent
  • 分配一个用于此次上报的、存储环境变量的buffer(结果保存在env指针中),并获得该Kobject在sysfs中路径信息(用户空间软件需要依据该路径信息在sysfs中访问它)
  • 调用add_uevent_var接口(下面会介绍),将Action、路径信息、subsystem等信息,添加到env指针中
  • 如果传入的envp不空,则解析传入的环境变量中,同样调用add_uevent_var接口,添加到env指针中
  • 如果所属的kset存在uevent_ops->uevent接口,调用该接口,添加kset统一的环境变量到env指针
  • 根据ACTION的类型,设置kobj->state_add_uevent_sent和kobj->state_remove_uevent_sent变量,以记录正确的状态
  • 调用add_uevent_var接口,添加格式为"SEQNUM=%llu”的序列号
  • 如果定义了"CONFIG_NET”,则使用netlink发送该uevent
  • 以uevent_helper、subsystem以及添加了标准环境变量(HOME=/,PATH=/sbin:/bin:/usr/sbin:/usr/bin)的env指针为参数,调用kmod模块提供的call_usermodehelper函数,上报uevent。
    其中uevent_helper的内容是由内核配置项CONFIG_UEVENT_HELPER_PATH(位于./drivers/base/Kconfig)决定的(可参考lib/kobject_uevent.c, line 32),该配置项指定了一个用户空间程序(或者脚本),用于解析上报的uevent,例如"/sbin/hotplug”。
    call_usermodehelper的作用,就是fork一个进程,以uevent为参数,执行uevent_helper。

kobject_uevent,和kobject_uevent_env功能一样,只是没有指定任何的环境变量。

add_uevent_var,以格式化字符的形式(类似printf、printk等),将环境变量copy到env指针中。

kobject_action_type,将enum kobject_action类型的Action,转换为字符串。

 

说明:怎么指定处理uevent的用户空间程序(简称uevent helper)?

上面介绍kobject_uevent_env的内部动作时,有提到,Uevent模块通过Kmod上报Uevent时,会通过call_usermodehelper函数,调用用户空间的可执行文件(或者脚本,简称uevent helper )处理该event。而该uevent helper的路径保存在uevent_helper数组中。

可以在编译内核时,通过CONFIG_UEVENT_HELPER_PATH配置项,静态指定uevent helper。但这种方式会为每个event fork一个进程,随着内核支持的设备数量的增多,这种方式在系统启动时将会是致命的(可以导致内存溢出等)。因此只有在早期的内核版本中会使用这种方式,现在内核不再推荐使用该方式。因此内核编译时,需要把该配置项留空。

在系统启动后,大部分的设备已经ready,可以根据需要,重新指定一个uevent helper,以便检测系统运行过程中的热拔插事件。这可以通过把helper的路径写入到"/sys/kernel/uevent_helper”文件中实现。实际上,内核通过sysfs文件系统的形式,将uevent_helper数组开放到用户空间,供用户空间程序修改访问,具体可参考"./kernel/ksysfs.c”中相应的代码,这里不再详细描述。

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

标签: Linux Kernel 内核 设备模型 Uevent

评论:

const
2018-03-13 12:53
wowo 你好,有个问题请教一下, kset_uevent_ops 里面的函数指针都进行了 const 修饰,这样除了确保第一次初始化后不会被再次修改,还有其他作用嘛?
wowo
2018-03-13 16:13
@const:你可以去kernel里面看看,基本上所有的函数指针都是const的。
Jackie
2017-12-07 11:07
@wowo @linuxer:
请教一个问题,kernel发送uevent给用户空间,
可这个时候用户空间处于freeze状态(suspend后还未resume回来),
请问这时候发送的uevent是会丢掉,还是会存在某个buffer中?
当用户空间resume回来的时候,还能收到这个uevent吗?
谢谢!
wowo
2017-12-08 10:06
@Jackie:现在的uevent基本上都使用netlink传递msg,kernel上报的msg会保存在一个socket queue中,等待user醒来并读取。所以resume回来的时候,还是能收到的。
当然,如果保存的数据太多了(超过netlink的限制),也是会失败的,不过这种情况可以忽略(因为netlink的数目很大)。
慢慢想
2016-09-12 13:36
wowo您好,想请教一个问题。
在android启动的时候init进程会进行coldboot操作,往所有uevent文件里写入add,如下:

fd = openat(dfd, "uevent", O_WRONLY);
write(fd, "add\n", 4);

但是在对某个uevent写入add的时候花费了很久的时间,导致系统起不来。(每次启动都是同一个uevent耗费时间)
想请教一下write(fd, "add\n", 4);操作之后相应的driver都做了啥?下面该如何加log分析,我在write前后加了log,可以确定时间全部耗费在write这个函数上了。

非常感谢!
wowo
2016-09-12 20:02
@慢慢想:对Android的uvent机制不是很熟悉,我猜测这里的write,是init进程通过uevent向另外一个进程传递了一个信息(add),另一个进程收到该信息后,做出相应的处理。
你要查为什么这么耗时,就要查这个uevent最终是谁处理的。
write uvent的过程大致如下:
init进程
write uvent(ADD)
    drivers/base/core.c
        static struct device_attribute uevent_attr
            store_uevent
                kobject_action_type
                    KOBJ_ADD
                kobject_uevent
                    kobject_uevent_env
                    ...

                        其它进程,收到ADD event,进行处理(find out it)
慢慢想
2016-09-22 10:20
@wowo:@wowo
怎么回复会显示IP被限制?
wowo
2016-09-22 10:29
@慢慢想:这一段时间反垃圾评论好像有点抽风,不知道怎么回事,昨天我已经把屏蔽的IP列表全部清空了,应该没有问题了,另外你这不是可以评论吗?
慢慢想
2016-09-22 10:30
@wowo:现在又能回复了。。。

您好,“其他进程”应该还是本进程,处理函数为handle_device_fd();不过这个函数没有耗费时间,耗费时间的只是write(fd, "add\n", 4);系统调用。我猜测问题还是出现在kernel space在上报netlink之前,请问这样理解对么?是不是注册这个uevent的driver有问题?

940    fd = openat(dfd, "uevent", O_WRONLY);
941    if(fd >= 0) {
942        write(fd, "add\n", 4);
943        close(fd);
944        handle_device_fd();
945    }
wowo
2016-09-22 10:56
@慢慢想:先确认是不是用的netlink,如果是的话,就看看时间到底耗在哪里了(write耗时?我持怀疑态度)
慢慢想
2016-09-22 14:34
@wowo:您好,多谢指点,我再查查看。
大头元
2016-07-07 14:48
博主您好!之前在板子上做usb驱动实验。1、如果usb驱动编译进内核,插入u盘可以正常识别。2、把之前的usb驱动编译成模块,然后安装到/lib/modules下,插入u盘不能识别。有以下疑问:
1、把驱动编译成模块并安装后,系统启动后会主动加载驱动模块吗?还是必须自己主动的去modprobe或写一些脚本来加载驱动模块?
2、系统热插拔只是起到帮助创建设备文件、挂载的作用。驱动模块的加载跟热插拔有关系吗?
wowo
2016-07-07 15:07
@大头元:可以lsmod,看一下模块是否加载了。如果加载了,和编译到kernel的流程应该一样。
maze
2016-07-07 11:30
wowo您好,我有个疑问啊,Uevent上报用户空间的方法是netlink与kmod?
为什么说是kmod?这里不太理解,kmod是指通过call_usermodehelper这个函数实现吗?
谢谢您提供这个学习平台
wowo
2016-07-07 15:12
@maze:这里的kmod,是指kernel去加载module的意识(对比用户空间module的概念)。
maze
2016-07-07 15:58
@wowo:那么kernel是通过什么方式加载的module?小白,很多都不懂。。。真是让您费心了。
我的理解是不是通过match以及probe?
wowo
2016-07-07 16:07
@maze:kernel加载module,和用户空间加载ELF文件的机制类似。不过已经不是一两句话能说清楚的,你懂的:-)
gzz
2015-08-04 12:40
遇到一个疑惑就是linux下建立framebuffer帧设备节点时是/dev/fb0,而Android下的linux内核启动后建立的是/dev/graphics/fb0.
目前我看了devtmpfs里面假设使用了:貌似创建节点的nodename是根据dev的name来建立的,所以很奇怪这个为何不也是建立一个/dev/fb0.
但有看到uevent是建立一个/dev/graphics/fb0的。
所以疑问就是为何这两个设备节点最终只存在/dev/graphics/fb0啦。
此外要说明的graphics_class类是不带其他命名方式的,不像input_class可以进行class->input_devnode后将设备节点相对路径变为input/xxx路径。
很是疑惑,求解答。
wowo
2015-08-04 13:29
@gzz:一般情况下,Android的kernel是没有使能devtmpfs的,因此,谁帮忙创建“/dev/fb0”呢?所以没有创建。
至于"/dev/graphics/fb0",是Android的init进程,根据uevent自行创建的,”graphics“取自framebuffer所在的class(subsystem) name。具体可参考代码:
system/core/init/devices.c
    ...
     } else if (!strncmp(uevent->subsystem, "graphics", 8)) {
         base = "/dev/graphics/";
         make_dir(base, 0755);
    ...
gzz
2015-08-05 08:55
@wowo:恩,如果解释的话大概也只能按这个来接受了,我打算调试下看看。要是都用起来,按理是真得有2个节点了。tks
gzz
2015-08-05 13:32
@wowo:我用板子调试了一下,发现devtmpfs还是执行了mount的,这很奇怪了。然后也有执行devtmpfs_create_node fb0;还有内核起来时也mount了,不知道为何就没有/dev/fb0?
这就奇怪了,不知道Android里面是怎么在捣鼓的.还请再指教下
wowo
2015-08-05 15:44
@gzz:Android会重新mount dev目录:
system/core/init/init.c:    mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
所以kernel自己mount的应该被覆盖了。
gzz
2015-07-29 16:44
请教几个问题:
1.device_add里面有devtmpfs_create_node与kobject_uevent两种接口。
向问下有什么区别两者
2.在处理的device的dev_t没有指定时,udev是根据什么不去创建设备节点的。?因为看起来kobject_uevent都是执行了的。
linuxer
2015-07-30 09:08
@gzz:针对第一个问题的回答:
我们都知道,在引入linux device model之后,设备节点的创建是通过udev这个userspace的daemon进程捕获内核发送的各种uevent来进行的,这种方法虽然灵活,但是比较耗时,我们曾经进行过嵌入式系统启动过程速度的优化,纵观整个启动过程udev花费的时间不是一个小的数目,因此我们果断干掉udev,使用固定在rootfs中预先建立设备节点的方法

devtmpfs是为了提高启动速度而引入到kernel中,在内核中,各个设备的初始化过程中就已经建立了一个文件系统,在userspace只要进行mount的动作,所有的设备节点就ready了,不需要udev参与了

针对第二个问题的回答:
device在add的时候会发送userspace event,包括其在sys目录下的位置,叫做dev path,udev获得这样的信息可以通过那个dev path,那个目录下有关于主次设备节点号的信息(在dev属性文件中),uevent可以通过这样的信息创建,当然,如果支持devtmpfs,那么udev不需要执行这一步,mount devtmpfs之后,所有的设备节点就在那了
gzz
2015-07-31 13:04
@linuxer:恩,我看到是Android的内核的uevent处理机制,他内部实现是会去看sysfs下面class/devices等是否含有uevent属性文件,暂时是不借助dev文件的。然后如果有找到就去触发一次底层block的uevent事件的上报,基于netlink这种网络机制。因为一般在init内核时,uevent是没起来的。
不过,我暂时不知道普通linux的udev处理是什么机制。应该是你说的那种机制吧。
至于你说的devtmpfs,我是不是可以让系统不支持呢,比如在Android下的linux内核,是不是有什么CONFIG之类可配置。
gzz
2015-07-31 15:21
@linuxer:我理解你的意思了,应该是内核启动时,有的设备节点是可以创建了的,因为是编译在内核中的,这些设备节点就需要提前创建,也可以加快处理速度。而一边在启动脚本中add的设备节点,理论应该是udev来创建的吧。
linuxer
2015-07-31 18:43
@gzz:如果内核已经创建了设备节点,虽然udev通过netlink收到uevent,我相信它也不会重复创建设备节点,当然,如果需要udev可以修改
如果对启动速度没有那么在意的话,可以不配置devtmpfs。
gzz
2015-08-03 14:56
@linuxer:恩,赞同你的说话,vfs_mknod内核里都是会先look_up有没有创建该文件的。所以这两种方式,在内核启动完成后,如果再出现要创建设备节点时,我认为只是谁先来建立节点而已。
而devtmpfs也许就是加快启动速度的吧。
thanks。
twenty
2015-05-22 17:11
上次看懂了1成,这次又看懂了3成,感觉进步挺大哒,哈哈!   感谢上次楼主对在下的回答,特别是“驱动学习螺旋上升的”!  以后还会再回来看,谢谢楼主分享~
wowo
2015-05-22 21:49
@twenty:呵呵,加油~~~
PS:任何的学习,都是螺旋上升的~~~
puppypyb
2014-12-02 12:01
之前一直以为uevent_ops->uevent是完成具体的事件上报函数回调,误解为就是wowo开始所说的uevent机制。看了这篇文章和代码才知道这个callback的作用只是添加一个环境变量,名字比较迷惑人。
另外整个kernel中鲜有利用uevent的场景,所以uevent其实用处不大,更有用的是uevent_ops->filter回调。

说起环境变量上报,搞不清楚上报这些个环境变量的用处是什么,如果是上报Kobject在sysfs中路径信息倒是好理解,至于环境变量,用户空间处理程序(hotplug什么的)的执行需要上报的这些环境变量吗?
wowo
2014-12-02 13:27
@puppypyb:其实环境变量是使用usermodehelper的方式处理uevent时才会需要,如果使用的netlink的话,确实不需要。
环境变量存在的本质,是因为exec系列的“文件执行”函数需要(这也是Linux下文件执行的基础),这可以从do_execve(fs/exec.c)接口从看出:
int do_execve(const char *filename,
        const char __user *const __user *__argv,
        const char __user *const __user *__envp)
至于kernel中uevent用处,还真不好说。如果你需要一些例子的话,可以去看看Android的switch class(drivers/switch/)。
我的观点是,如果软件设计的足够好,一定用不到uevent,poll机制足够了。如果poll解决不了,则说明设计者根本不知道event要上报给谁,才使用uevent的方式随便一扔,谁想收就收吧。
另外,无论是usermodehelper还是netlink的方式,uevent对系统性能是有影响的。特别是netlink的方式,上报的event会通知到所有的用户进程,所以还是少用为妙。
puppypyb
2014-12-02 14:27
@wowo:understood, thanks~
xxx
2014-10-28 23:10
注1:有关kmod和netlink,会在其它文章中描述,因此本文就不再详细说明了。
===>这两个在哪讲啊 啊?????
wowo
2014-10-28 23:17
@xxx:不好意思,还没有来得及写…
hony
2017-01-10 22:07
@wowo:这个...

发表评论:

Copyright @ 2013-2015 蜗窝科技 All rights reserved. Powered by emlog