sysfs文件系统
Linux设备模型如同一栋规模宏大的建筑,为了构建它,除了基本的建筑材料外(这就是接下来会谈到的kobject、kset等基础类数据结构),尚需要一种机制,来向建筑外面的世界(用户空间的程序)展示内部的构造,并且通过文件接口的方式实现与外界的沟通与互动。sysfs文件系统就充当了这种角色,它不但在各种基础的建筑材料之间建立彼此的互联层次关系,而且向外界提供了与建筑内设施进行互动的文件接口。这种形象的比喻反映到Linux系统,我们可以看到文件除了在内核空间所展现的合纵连横作用外,而且以文件目录层次结构的形式向用户空间提供了系统硬件设备间的一个拓扑图,这种文件形式的接口也让用户空间与内核空间的数据对象的交互成为可能。读者如果熟悉proc文件系统的话,应当知道sysfs文件系统实际上取代了proc文件系统的功能,当然取代proc文件系统只是sysfs文件系统一小部分的功能而己。这种数据对象间的交互的一个具体的例子就是,透过sysfs文件系统可以取代ioctl的功能:ioctl的实现,如果向一个设备文件发送ioctl命令的话,需要首先打开该设备文件,然后再通过ioctl函数向设备发出命令,很显然需要一个完整(虽然代码可能很简单)的应用程序来做这件事,现在有了sysfs文件系统,一个很简单的shell命令也许就可以完成前面所说的工作。
sysfs文件系统的初始化发生在Linux系统的启动阶段:
int __init sysfs_init(void) { int err; sysfs_root = kernfs_create_root(NULL, KERNFS_ROOT_EXTRA_OPEN_PERM_CHECK, NULL); if (IS_ERR(sysfs_root)) return PTR_ERR(sysfs_root); sysfs_root_kn = sysfs_root->kn; err = register_filesystem(&sysfs_fs_type); if (err) { kernfs_destroy_root(sysfs_root); return err; } return 0; }
函数将向系统注册一个类型为sysfs_fs_type 的文件系统。
static struct file_system_type sysfs_fs_type = { .name = "sysfs", .mount = sysfs_mount, .kill_sb = sysfs_kill_sb, .fs_flags = FS_USERNS_VISIBLE | FS_USERNS_MOUNT, };
关于这个结构没有多少需要解释的地方,唯一可能要注意的地方是sysfs_fs_type中的sysfs_mount成员,它指向函数kernfs_mount_ns,感兴趣的读者可以自己看看这个函数的源码实现,它实际上在内核空间创造了一棵独立的VFS树内核创建这棵VFS树,主要用来沟通系统中总线、设各与驱动,同时向用户空间提供接口及展示系统中各种设备的拓展视图等,事实上它并不用来作为其他实际文件系统的挂载点。
kernfs_mount_ns函数用来产生sysfs文件系统的超级块,其内部调用的最主要的函数是kernfs_fill_super,后者再经过一系列的函数调用链进入到kernfs_init_inode函数,这里之所以重点强调这个函数,是因为在接下来谈到内核对象的属性问题时会看到用户空间和内核对象的沟通问题,这种文件接口形式的交互发生在内核空间和用户空间,所以我们需要知道这条沟通的通道是如何建立起来的。在kernfs_init_inode中,函数将为sysfs文件系统中每个文件或目录所对应的inode赋予一个新的操作对象:
sysfs_fs_type->sysfs_mount->kernfs_mount_ns->kernfs_fill_super->kernfs_get_inode->kernfs_init_inode
static void kernfs_init_inode(struct kernfs_node *kn, struct inode *inode) { kernfs_get(kn); inode->i_private = kn; inode->i_mapping->a_ops = &kernfs_aops; inode->i_op = &kernfs_iops; set_default_inode_attr(inode, kn->mode); kernfs_refresh_inode(kn, inode); /* initialize inode according to type */ switch (kernfs_type(kn)) { case KERNFS_DIR: inode->i_op = &kernfs_dir_iops; inode->i_fop = &kernfs_dir_fops; if (kn->flags & KERNFS_EMPTY_DIR) make_empty_dir_inode(inode); break; case KERNFS_FILE: inode->i_size = kn->attr.size; inode->i_fop = &kernfs_file_fops; break; case KERNFS_LINK: inode->i_op = &kernfs_symlink_iops; break; default: BUG(); } unlock_new_inode(inode); }
sysfs文件系统是个基于RAM实现的文件系统,如果编译内核时指定了CONFIGSYSFS选项,那么这个文件系统就会包含到内核中。对于用户进程中的文件系统来说,sysfs的标准挂载点是“sys”目录。将sysfs文件系统挂载到用户进程的“/sys”目录的命令为:
mount -t sysfs sysfs /sys
如此,所有内核层面的对sysfs文件树的操作,都将一成不变地显示在用户空间的"/sys"目录下。
kobject
如果将Linux设备模型比喻成一座大厦,那么kobject和kset就是构成这座大厦内部的钢筋及由若干钢筋构建的钢架结构,再由若干的它们构成了整座大厦内部的表现形式,设备驱动模型中的bus、device和driver己经是整座大厦向外界展示的那部分了,所以程序员们主要是和后三者打交道。
Linux内核用kobject来表示一个内核对象,它在源码中的定义为:
struct kobject { const char *name; /*用来表示该内核对象的名称。如果该内核对象加入系统,那么它的name将会出现 在sysfs文件系统中(表现形式是一个新的目录名)*/ struct list_head entry; /*用来将一系列的内核对象构成链表。*/ struct kobject *parent; /*指向该内核对象的上层节点。通过引入该成员构建内核对象之间的层次化关系。*/ struct kset *kset; /*当前内核对象所属的kset对象的指针。kset对象代表一个subsystem,其中容纳了 一系列同类型的kobject对象。*/ struct kobj_type *ktype; /*定义了该内核对象的一组sysfs文件系统相关的操作函数和属性。显然不同类型的 内核对象会有不同的ktype,用以体现kobject所代表的内核对象的特质。通过该成员, C中的struct数据类型具备了C++中class类型的某些特点,这里体现了基于c的面向对 象设计思想。同时,内核通过kty成员将对象的sysfs文件操作与其属性文件关联起来。*/ struct kernfs_node *sd; /*用来表示该内核对象在文件系统中对应的目录项的实例。*/ struct kref kref; /*其核心数据是一原子型变量,用来表示内核对象的引用计数。内核通过该成员追踪 内核对象的生命周期。*/ #ifdef CONFIG_DEBUG_KOBJECT_RELEASE struct delayed_work release; #endif unsigned int state_initialized:1; /*表示该kobject所代表的内核对象初始化的状态,1表示对象已被初始化,0表示尚未初始 化。*/ unsigned int state_in_sysfs:1; /*表示该kobject所代表的内核对象有没有在sysfs文件中建立一个入口点。*/ unsigned int state_add_uevent_sent:1; unsigned int state_remove_uevent_sent:1; unsigned int uevent_suppress:1; /*如果该kobject对象隶属于某一k姒,那么它的状态变化可以导致其所在的kset对象 向用户空间发送event消息。成员uevent-suppress用来表示当该kobject状态发生 变化时,是否让其所在的kset向用户空间发送event消息。值1表示不让kset发送这种event 消息。*/ };
kobject数据结构最通用的用法是嵌在表示某一对象的数据结构中,比如内核中定义的字符型设备对象cdev中就嵌入了kobject结构:
struct cdev {//字符设备驱动数据结构 struct kobject kobj;//内嵌内核对象 struct module *owner;//该字符设备所在内核模块的对象指针 const struct file_operations *ops;//描述字符设备所能够实现的操作 struct list_head list;//用来将已向内核注册的所有字符设备形成链表 dev_t dev;//字符设备的设备号,由主设备和次设备号组成 unsigned int count;//隶属同意主设备号的次设备号个数 };
下面介绍内核中定义的对kobject对象上的一些常用的操作函数。需要提醒读者的是,设备驱动程序一般不会与这些底层的函数直接打交道,这里简单介绍这些函数是希望读者能大致了解其功能,因为在讨论到设备驱动模型的高层框架时,它们将经常被提及。当然,如果的确有需要,也可以直接在设备驱动程序模块中调用这些底层的函数。
- kobject_set_name
该函数用来设定kobJect中的name成员,函数原型为:
int kobject_set_name(struct kobject *kobj, const char *fmt, ...)
- kobject_init
该函数用来初始化一个内核对象的kobject结构,其核心功能代码为
void kobject_init(struct kobject *kobj, struct kobj_type *ktype) { char *err_str; if (!kobj) { err_str = "invalid kobject pointer!"; goto error; } if (!ktype) { err_str = "must have a ktype to be initialized properly!\n"; goto error; } if (kobj->state_initialized) { /* do not error out as sometimes we can recover */ printk(KERN_ERR "kobject (%p): tried to init an initialized " "object, something is seriously wrong.\n", kobj); dump_stack(); } kobject_init_internal(kobj); kobj->ktype = ktype; return; error: printk(KERN_ERR "kobject (%p): %s\n", kobj, err_str); dump_stack(); }
除了为kobj指定ktype成员外,真正的初始化工作发生在kobject_init_intemal中:
static void kobject_init_internal(struct kobject *kobj) { if (!kobj) return; kref_init(&kobj->kref); INIT_LIST_HEAD(&kobj->entry); kobj->state_in_sysfs = 0; kobj->state_add_uevent_sent = 0; kobj->state_remove_uevent_sent = 0; kobj->state_initialized = 1; }
函数中的kref_init(&kobj->kref)用于将kobject的引用计数refcount初始化为1,state_initialized置为1表示该内核对象己被初始化,state_in_sysfs置为0表示该内核对象尚未出现在文件树中。
- kobject_add
int kobject_add(struct kobject *kobj, struct kobject *parent, const char *fmt, ...) { va_list args; int retval; if (!kobj) return -EINVAL; if (!kobj->state_initialized) { printk(KERN_ERR "kobject '%s' (%p): tried to add an " "uninitialized object, something is seriously wrong.\n", kobject_name(kobj), kobj); dump_stack(); return -EINVAL; } va_start(args, fmt); retval = kobject_add_varg(kobj, parent, fmt, args); va_end(args); return retval; }
对于kobJect来讲,这是个非常重要的函数。内核源码中这个函数调用链比较烦琐。
- kobject_add的主要功能有两个,一是建立kobject对象间的层次关系,二是在sysfs文件系统中建立一个目录。在将一个kobject对象通过kobject_add函数调用加入系统前,kobject对象必须已被初始化。
- 关于这两个功能的实现细节,kobject_add首先将参数parent赋值给kobj的parent成员kobJ->parent=parent,然后调用kobject_add_intemal(kobj)函数。在kobJect_add_intemal函数内部,如果调用kobJect_add时parent是—NULL指针,那么要看该ko是否在一个kset对象中:如果是就把该kset中的kobject成员作为kobj的parent;否则该kobj对象在sysfs文件树中就将处于根目录的位置。
static int kobject_add_internal(struct kobject *kobj) { int error = 0; struct kobject *parent; if (!kobj) return -ENOENT; if (!kobj->name || !kobj->name[0]) { WARN(1, "kobject: (%p): attempted to be registered with empty " "name!\n", kobj); return -EINVAL; } parent = kobject_get(kobj->parent); /* join kset if set, use it as parent if we do not already have one */ if (kobj->kset) {//在kobj有所属的kset的情况下 if (!parent)//如果调用kobject_add时,传入的parent参数是一NULL指针 parent = kobject_get(&kobj->kset->kobj);//就把kobj所在的kset中的kobj作为它的parent kobj_kset_join(kobj);//将kobj加入到所属kset链表的末尾 kobj->parent = parent; } //在kobj没有所属的kset的情况下,如果调用kobject_add时parent为NULL //那么kobJ->parent也将为NULL pr_debug("kobject: '%s' (%p): %s: parent: '%s', set: '%s'\n", kobject_name(kobj), kobj, __func__, parent ? kobject_name(parent) : "<NULL>", kobj->kset ? kobject_name(&kobj->kset->kobj) : "<NULL>"); error = create_dir(kobj); if (error) { kobj_kset_leave(kobj); kobject_put(parent); kobj->parent = NULL; /* be noisy on error issues */ if (error == -EEXIST) WARN(1, "%s failed for %s with " "-EEXIST, don't try to register things with " "the same name in the same directory.\n", __func__, kobject_name(kobj)); else WARN(1, "%s failed for %s (error: %d parent: %s)\n", __func__, kobject_name(kobj), error, parent ? kobject_name(parent) : "'none'"); } else kobj->state_in_sysfs = 1; return error; }
- kobject_add_internal接下来会调用create_dir在sysfs文件树中创建目录:
static int create_dir(struct kobject *kobj) { const struct kobj_ns_type_operations *ops; int error; error = sysfs_create_dir_ns(kobj, kobject_namespace(kobj)); if (error) return error; error = populate_dir(kobj); if (error) { sysfs_remove_dir(kobj); return error; } /* * @kobj->sd may be deleted by an ancestor going away. Hold an * extra reference so that it stays until @kobj is gone. */ sysfs_get(kobj->sd); /* * If @kobj has ns_ops, its children need to be filtered based on * their namespace tags. Enable namespace support on @kobj->sd. */ ops = kobj_child_ns_ops(kobj); if (ops) { BUG_ON(ops->type <= KOBJ_NS_TYPE_NONE); BUG_ON(ops->type >= KOBJ_NS_TYPES); BUG_ON(!kobj_ns_type_registered(ops->type)); sysfs_enable_ns(kobj->sd); } return 0; }
可以看到,如果kobJ->parent为NULL(刚刚在kobject_add_intemal函数中讨论过这种情况),调用create_dir在sysfs文件树中为当前ko创建目录时,parent_sd=&sysfs_root,否则parent_sd=kobj->parent->sd•parent-sd=&sysfs_root意味着在sysfs文件树的根目录下为kobj创建一个新的目录,否则就是在parent_sd对应的目录底下创建新目录。如果kobj在sysfs中成功创建了一个新目录,自然应该将kobJ->state_insysfs置为1。
在sysfs文件系统中,目录对应的数据结构为struct sysfs_dirent,函数中用sd表示该类型的一个实例,在将kobj对象加入sysfs文件树之后,kobj->sd=sd。
- kobject_init_and_add
该函数实际的工作是将kobject_init和kobject_add两个函数的功能合并到了一起。
int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype, struct kobject *parent, const char *fmt, ...)
- kobject_create
该函数用来分配并初始化一个kobject对象:
struct kobject *kobject_create(void) { struct kobject *kobj; kobj = kzalloc(sizeof(*kobj), GFP_KERNEL); if (!kobj) return NULL; kobject_init(kobj, &dynamic_kobj_ktype); return kobj; }
如果调用kobject_create来产生一个kobject对象,那么调用者将无法为该kobject对象另行指定kobj_type。kobJect_create为产生的kobJect对象指定了一个默认的kobJ对象dynamic_kobjktype,这个行为将影响kobject对象上的sysfs文件操作。如果调用者需要明确指定一个自己的kobj_type对象给该kobject对象,那么还应该使用其他函数,比如调用kobject_init_and_add函数。
- kset_create_and_add
函数内部首先调用kobJect_create来分配并初始化一个kobject对象,然后再调用kobJect_add函数在sysfs文件系统中为新生成的kobject对象建立一个新的目录:
struct kset *kset_create_and_add(const char *name, const struct kset_uevent_ops *uevent_ops, struct kobject *parent_kobj) { struct kset *kset; int error; kset = kset_create(name, uevent_ops, parent_kobj); if (!kset) return NULL; error = kset_register(kset); if (error) { kfree(kset); return NULL; } return kset; }
- kobject_del
void kobject_del(struct kobject *kobj) { struct kernfs_node *sd; if (!kobj) return; sd = kobj->sd; sysfs_remove_dir(kobj); sysfs_put(sd); kobj->state_in_sysfs = 0; kobj_kset_leave(kobj); kobject_put(kobj->parent); kobj->parent = NULL; }
函数将在sysfs文件树中把kobj对应的目录删除,另外如果kobJ隶属于某一kset的话,将其从kset的链表中删除。
kobject的类型属性
kobject数据结构中内嵌有一个struct kobj类型的成员*ktype,在内核中struct kobj_type的定义为:
struct kobj_type { void (*release)(struct kobject *kobj); 释放kobject和其占用资源的函数 const struct sysfs_ops *sysfs_ops; 操作下一个属性数组的方法 struct attribute **default_attrs; 属性数组 const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj); const void *(*namespace)(struct kobject *kobj); };
release显然是一个函数指针,成员sysfs_ops是一struct sysfs_ops类型
的指针,struct sysfs_ops的定义为:
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 *); };
所以sysfs_ops实际上定义了一组针对struct attribute对象的操作函数的集合,struct attribute数据结构则是为kobject内核对象定义的属性成员,它在源码中的定义是:
struct attribute { const char *name; 属性的名称,对应目录下一个文件的名字 umode_t mode; 属性的读写权限 #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lock_class_key *key; struct lock_class_key skey; #endif };
记得前面讨论kobject_init函数初始化一个内核对象kobj对象的时候,会同时赋予它一个具体的struct kobj对象成员,那么现在的问题是,内核会如何使用kobject的这个这个成员呢?
对这个问题的探讨其实关系到内核把一个kobject对象加入到sysfs文件树中的使用意图。为一个kobJect对象创建一个属性文件使用的函数为sysfs_create_files:
int sysfs_create_files(struct kobject *kobj, const struct attribute **ptr) { int err = 0; int i; for (i = 0; ptr[i] && !err; i++) err = sysfs_create_file(kobj, ptr[i]); if (err) while (--i >= 0) sysfs_remove_file(kobj, ptr[i]); return err; } 2
sysfs_create_files->sysfs_add_file->sysfs_add_file_mode_ns
int sysfs_create_file_ns(struct kobject *kobj, const struct attribute *attr, const void *ns) { BUG_ON(!kobj || !kobj->sd || !attr); return sysfs_add_file_mode_ns(kobj->sd, attr, false, attr->mode, ns); }
在使用这个函数时,必须确保要添加属性文件的kobj对象之前己经加入了sysfs〈也即kobj->state_in_sysfs=1),sysfs_add_file函数将在kobj->sd对应的目录下生成一个属性文件。如果以先前的"cld_obj”内核对象为基础,在其下添加一个属性文件"cldatt",可以使用下面的代码:
static struct sttribute cld_stt={ .name="cldatt", .mode=S_IRUGO|S_IWUSR, }; sysfs_creat_file(child,&cld_att);
运行上面的代码后,可在/sys/pa_obj/cld_obj目录下看到一名为"cldatt"的属性文件。用户空间的程序在使用一个内核对象kobJect的属性文件时,会首先open这个属性文件,比如open(“cldatt”,ORDONLY | O_LARGEFILE),然后通过系统调用等一系列潜在的调用链。