0
点赞
收藏
分享

微信扫一扫

Linux设备模型剖析系列之二(uevent、sysfs)


三、uevent

1. uevent的功能

  • uevent是kobject的一部分,用于在kobject状态发生改变时(增加、移除等),通知用户空间程序。用户空间程序收到这样的事件后,会做相应的处理。该机制通常是用来支持热拔插设备的,例如U盘插入后,USB相关的驱动软件会动态创建用于表示该U盘的device结构(相应的也包括其中的kobject),并告知用户空间程序,为该U盘动态的创建/dev/目录下的设备节点,更进一步,可以通知其它的应用程序,将该U盘设备mount到系统中,从而动态的支持该设备。

2. uevent在kernel中的位置

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

Linux设备模型剖析系列之二(uevent、sysfs)_驱动开发

由此可知,uevent的机制是比较简单的,设备模型中任何设备有事件需要上报时,会触发uevent提供的接口。uevent模块准备好上报事件的格式后,可以通过两个途径把事件上报到用户空间:一种是通过kmod模块,直接调用用户空间的可执行文件;另一种是通过netlink通信机制,将事件从内核空间传递给用户空间。有关kmod和netlink,会在其它文章中描述,因此本文就不再详细说明了。

3. uevent的内部逻辑解析

3.1 源代码的位置

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

  • include/linux/kobject.h
  • lib/kobject_uevent.c

3.2 数据结构描述

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

  • kobject_action

/* include/linux/kobject.h, line 50 */
enum kobject_action {
KOBJ_ADD,
KOBJ_REMOVE,
KOBJ_CHANGE,
KOBJ_MOVE,
KOBJ_ONLINE,
KOBJ_OFFLINE,
KOBJ_MAX
};

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

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

ONLINE/OFFLINE:kobject(或上层数据结构)的上线/下线事件,即是否使能。

CHANGE: kobject(或上层数据结构)的状态或者内容发生改变。如果设备驱动需要上报的事件不再上面事件的范围内,或者是自定义的事件,可以使用该event,并携带相应的参数。

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

MAX: 事件种类的最大值

  • kobj_uevent_env

/* include/linux/kobject.h, line 31 */
#define UEVENT_NUM_ENVP 32 /* number of env pointers */
#define UEVENT_BUFFER_SIZE 2048 /* buffer for the variables */

/* include/linux/kobject.h, line 116 */
struct kobj_uevent_env {
char *envp[UEVENT_NUM_ENVP];
int envp_idx;
char buf[UEVENT_BUFFER_SIZE];
int buflen;
};

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

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

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

buflen,访问buf的变量。

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

/* include/linux/kobject.h, line 123 */
struct kset_uevent_ops {
int (* const filter)(struct kset *kset, struct kobject *kobj);
const char *(* const name)(struct kset *kset, struct kobject *kobj);
int (* const uevent)(struct kset *kset, struct kobject *kobj, struct kobj_uevent_env *env);
};

  • kset_uevent_ops是为kset量身订做的一个数据结构,里面包含filteruevent两个回调函数,用处如下:
  • filter:当任何kobject需要上报uevent时,它所属的kset可以通过该接口过滤,阻止(过滤掉)不希望上报的event,从而达到从整体上管理的目的。
  • uevent:当任何kobject需要上报uevent时,它所属的kset可以通过该接口统一为这些event添加环境变量。因为很多时候上报uevent时的环境变量都是相同的,因此可以由kset统一处理,就不需要让每个kobject独自添加了。
  • name:该接口可以返回kset的名称。如果一个kset没有合法的名称,则其下的所有kobject将不允许上报uevent。

3.3 内部接口

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

/* include/linux/kobject.h, line 206 */
int kobject_uevent(struct kobject *kobj, enum kobject_action action);
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action, char *envp[]);
int add_uevent_var(struct kobj_uevent_env *env, const char *format, ...);
int kobject_action_type(const char *buf, size_t count, enum kobject_action *type);

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

int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
return kobject_uevent_env(kobj, action, NULL);
}

3.3.3 add_uevent_var
  • ​add_uevent_var​​以格式化字符的形式(类似printf、printk等),将环境变量copy到env指针中。
3.3.4 kobject_action_type
  • ​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​​中相应的代码,这里不再详细描述。

四、sysfs

1. 前言

sysfs是一个基于RAM的文件系统,它和kobject一起,可以将kernel的数据结构导出到用户空间,以文件目录结构的形式,提供对这些数据结构(以及数据结构的属性)的访问支持。sysfs是一种表示内核对象、对象属性,以及对象关系的一种机制,一般内核对象、属性、以及对象关系组织成树状形式。其中内核对象被映射为用户态的目录;对象属性被映射为用户态的文件,文件在目录下;对象关系被映射成用户空间的符号链接。

sysfs具备文件系统的所有属性,而本文主要侧重其设备模型的特性,因此不会涉及过多的文件系统实现细节,而只介绍sysfs在Linux设备模型中的作用和使用方法。具体包括:

  • sysfs和kobject的关系
  • attribute的概念
  • sysfs的文件系统操作接口

2. sysfs和kobject的关系

在前面有提到过,每一个kobject,都会对应sysfs中的一个目录。因此在将kobject添加到kernel时,​​create_dir​​函数会调用sysfs文件系统的创建目录接口,创建和kobject对应的目录,相关的代码如下:

/* lib/kobject.c, line 47 */
static int create_dir(struct kobject *kobj)
{
int error = 0;
if (kobject_name(kobj)) {
error = sysfs_create_dir(kobj);
if (!error) {
error = populate_dir(kobj);
if (error)
sysfs_remove_dir(kobj);
}
}
return error;
}

  • 这个函数接受唯一的一个参数,也就是要为其创建目录的kobject的地址。它完成如下操作:
  1. 检查传递进来kobject对象的name的有效性,若无效,则返回0。
  2. 若有效,则调用sysfs_create_dir(kobj)来在sysfs中创建目录。

/* fs/sysfs/dir.c, line 736 */
/*
* sysfs_create_dir - create a directory for an object.
*/
int sysfs_create_dir(struct kobject * kobj)
{
enum kobj_ns_type type;
struct sysfs_dirent *parent_sd, *sd;
const void *ns = NULL;
int error = 0;
BUG_ON(!kobj);
if (kobj->parent)
parent_sd = kobj->parent->sd;
else
parent_sd = &sysfs_root;
error = create_dir(kobj, parent_sd, kobject_name(kobj), &sd);
if (!error)
kobj->sd = sd;
return error;
}

  • 这个函数首先会找到要为其创建目录的kobject的sysfs_dirent对象的父sysfs_dirent,通常是由父kobject的sd字段所指向。若父kobject为NULL,则设为sysfs根目录的sysfs_dirent。
  • 然后调用create_dir(kobj, parent_sd, kobject_name(kobj) , &sd)来创建目录。
  • 最后,若成功,设置kobject的sd字段指向为其新创建的sysfs_dirent,并返回0。若失败则返回错误码。

3. attribute

3.1 attribute的功能概述

在sysfs中,为什么会有attribute的概念呢?其实它是对应kobject而言的,指的是kobject的属性。我们知道,sysfs中的目录描述了kobject,而kobject是特定数据类型变量(如struct device)的体现。因此kobject的属性,就是这些变量的属性。它可以是任何东西,名称、一个内部变量、一个字符串等等。而attribute,在sysfs文件系统中是以文件的形式提供的,即:kobject的所有属性,都在它对应的sysfs目录下以文件的形式呈现。这些文件一般是可读写的,而kernel中定义了这些属性的模块,会根据用户空间的读写操作,记录和返回这些attribute的值。

总结一下:所谓的attibute,就是内核空间和用户空间进行信息交互的一种方法。例如某个驱动定义了一个变量,却希望用户空间程序可以修改该变量,以控制driver的运行行为,那么就可以将该变量以sysfs attribute的形式开放出来。

Linux内核中,attribute分为普通的attribute和二进制attribute,如下:

/* include/linux/sysfs.h, line 26 */
struct attribute
{
const char *name;
umode_t mode;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
bool ignore_lockdep:1;
struct lock_class_key *key;
struct lock_class_key skey;
#endif
};

/* include/linux/sysfs.h, line 100 */
struct bin_attribute {
struct attribute attr;
size_t size;
void *private;
ssize_t (*read)(struct file *, struct kobject *, struct bin_attribute *, char *, loff_t, size_t);
ssize_t (*write)(struct file *,struct kobject *, struct bin_attribute *, char *, loff_t, size_t);
int (*mmap)(struct file *, struct kobject *, struct bin_attribute *attr, struct vm_area_struct *vma);
};

struct attribute为普通的attribute,使用该attribute生成的sysfs文件,只能用字符串的形式读写(后面会说为什么)。而struct bin_attribute在struct attribute的基础上,增加了read、write等函数,因此它所生成的sysfs文件可以用任何方式读写

说完基本概念,我们要问两个问题:

kernel怎么把attribute变成sysfs中的文件呢?

用户空间对sysfs的文件进行的读写操作,怎么传递给kernel呢?

下面来看看这个过程。

3.2 attibute文件的创建

在linux内核中,attibute文件的创建是由fs/sysfs/file.c中​​sysfs_create_file​​接口完成的,该接口的实现没有什么特殊之处,大多是文件系统相关的操作,和设备模型没有太多的关系,这里先略过不提。

3.3 attibute文件的read和write

看到3.1章节struct attribute的原型时,也许我们会犯嘀咕,该结构很简单啊,name表示文件名称,mode表示文件模式,其它的字段都是内核用于debug kernel Lock的,那文件操作的接口在哪里呢?

不着急,我们去fs/sysfs目录下看看sysfs相关的代码逻辑。

所有的文件系统,都会定义一个​​struct file_operations​​变量,用于描述本文件系统的操作接口,sysfs也不例外:

/* fs/sysfs/file.c, line 472 */
const struct file_operations sysfs_file_operations = {
.read = sysfs_read_file,
.write = sysfs_write_file,
.llseek = generic_file_llseek,
.open = sysfs_open_file,
.poll = sysfs_poll,
};

attribute文件的read操作,会由VFS转到sysfs_file_operations的read(也就是sysfs_read_file)接口上,让我们大概看一下该接口的处理逻辑。

/* fs/sysfs/file.c, line 127 */
static ssize_t sysfs_read_file(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
struct sysfs_buffer *buffer = file->private_data;
ssize_t retval = 0;

mutex_lock(&buffer->mutex);
if (buffer->needs_read_fill || *ppos == 0) {
retval = fill_read_buffer(file->f_path.dentry, buffer);
if (retval)
goto out;
}
...
}

/* fs/sysfs/file.c, line 67 */
static int fill_read_buffer(struct dentry * dentry, struct sysfs_buffer * buffer)
{
struct sysfs_dirent *attr_sd = dentry->d_fsdata;
struct kobject *kobj = attr_sd->s_parent->s_dir.kobj;
const struct sysfs_ops * ops = buffer->ops;
...
count = ops->show(kobj, attr_sd->s_attr.attr, buffer->page);
...
}

read处理看着很简单,sysfs_read_file从file指针中取一个私有指针,转换为一个struct sysfs_buffer类型的指针,以此为参数(buffer),转身就调用fill_read_buffer接口。而fill_read_buffer接口,直接从buffer指针中取出一个struct sysfs_ops指针,调用该指针的show函数,即完成了文件的read操作。

那么后续呢?当然是由ops->show接口接着处理咯。而具体怎么处理,就是其它模块(例如某个driver)的事了,sysfs不再关心(其实,Linux大多的核心代码,都是只提供架构和机制,具体的实现,也就是苦力,留给那些码农吧!这就是设计的魅力)。

不过还没完,这个struct sysfs_ops指针哪来的?好吧,我们再看看open(sysfs_open_file)接口吧。

/* fs/sysfs/file.c, line 326 */
static int sysfs_open_file(struct inode *inode, struct file *file)
{
struct sysfs_dirent *attr_sd = file->f_path.dentry->d_fsdata;
struct kobject *kobj = attr_sd->s_parent->s_dir.kobj;
struct sysfs_buffer *buffer;
const struct sysfs_ops *ops;
int error = -EACCES;

/* need attr_sd for attr and ops, its parent for kobj */
if (!sysfs_get_active(attr_sd))
return -ENODEV;

/* every kobject with an attribute needs a ktype assigned */
if (kobj->ktype && kobj->ktype->sysfs_ops)
ops = kobj->ktype->sysfs_ops;
else {
WARN(1, KERN_ERR "missing sysfs attribute operations for kobject: %s\n", kobject_name(kobj));
goto err_out;
}

...

buffer = kzalloc(sizeof(struct sysfs_buffer), GFP_KERNEL);
if (!buffer)
goto err_out;

mutex_init(&buffer->mutex);
buffer->needs_read_fill = 1;
buffer->ops = ops;
file->private_data = buffer;
...
}

原来和ktype有关系。这个指针是从该attribute所从属的kobject中拿的。再去看一下ktype的定义,里面有一个struct sysfs_ops的指针。

注意:通过注释“every kobject with an attribute needs a ktype assigned”以及其后代码逻辑可知,如果从属的kobject(就是attribute文件所在的目录)没有ktype,或者没有ktype->sysfs_ops指针,是不允许它注册任何attribute的!

经过确认后,sysfs_open_file从ktype中取出struct sysfs_ops指针,并在随后的代码逻辑中,分配一个struct sysfs_buffer类型的指针(buffer),并把struct sysfs_ops指针保存在其中,随后把buffer指针交给file的private_data,随后read/write等接口便可以取出使用。这是内核中的常用方法!

顺便看一下struct sysfs_ops吧,我想你已经能够猜到了。

/* include/linux/sysfs.h, line 124 */
struct sysfs_ops {
ssize_t (*show)(struct kobject *, struct attribute *,char *);
ssize_t (*store)(struct kobject *,struct attribute *,const char *, size_t);
const void *(*namespace)(struct kobject *, const struct attribute *);
};

attribute文件的write过程和read类似,这里就不再多说。另外,上面只分析了普通attribute的逻辑,而二进制类型的呢?也类似,去看看fs/sysfs/bin.c吧,这里也不说了。

讲到这里,应该已经结束了,事实却不是如此。上面read/write的数据流,只到kobject(也就是目录)级别哦,而真正需要操作的是attribute(文件)啊!这中间一定还有一层转换!确实,不过又交给其它模块了。 下面我们通过一个例子,来说明如何转换的。

4. sysfs在设备模型中的应用总结

让我们通过设备模型class.c中有关sysfs的实现,来总结一下sysfs的应用方式。

首先,在class.c中,定义了class所需的ktype以及sysfs_ops类型的变量,如下:

/* drivers/base/class.c, line 86 */
static const struct sysfs_ops class_sysfs_ops = {
.show = class_attr_show,
.store = class_attr_store,
.namespace = class_attr_namespace,
};

static struct kobj_type class_ktype = {
.sysfs_ops = &class_sysfs_ops,
.release = class_release,
.child_ns_type = class_child_ns_type,
};

由前面章节的描述可知,所有class_type的kobject下面的attribute文件的读写操作,都会交给class_attr_show和class_attr_store两个接口处理。以class_attr_show为例:

/* drivers/base/class.c, line 24 */
#define to_class_attr(_attr) container_of(_attr, struct class_attribute, attr)

static ssize_t class_attr_show(struct kobject *kobj, struct attribute *attr, char *buf)
{
struct class_attribute *class_attr = to_class_attr(attr);
struct subsys_private *cp = to_subsys_private(kobj);
ssize_t ret = -EIO;

if (class_attr->show)
ret = class_attr->show(cp->class, class_attr, buf);
return ret;
}

该接口使用container_of从struct attribute类型的指针中取得一个class模块的自定义指针:struct class_attribute,该指针中包含了class模块自身的show和store接口。下面是struct class_attribute的声明:

/* include/linux/device.h, line 399 */
struct class_attribute {
struct attribute attr;
ssize_t (*show)(struct class *class, struct class_attribute *attr,char *buf);
ssize_t (*store)(struct class *class, struct class_attribute *attr, const char *buf, size_t count);
const void *(*namespace)(struct class *class, const struct class_attribute *attr);
};

因此,所有需要使用attribute的模块,都不会直接定义struct attribute变量,而是通过一个自定义的数据结构,该数据结构的一个成员是struct attribute类型的变量,并提供show和store回调函数。然后在该模块ktype所对应的struct sysfs_ops变量中,实现该本模块整体的show和store函数,并在被调用时,转接到自定义数据结构(struct class_attribute)中的show和store函数中。这样,每个atrribute文件,实际上对应到一个自定义数据结构变量中了。

    举报

    相关推荐

    Linux的设备模型

    0 条评论