kernfs_node)。但上期创建kobject后是空目录,真正让用户空间cat/echo就能读写内核对象状态的,是目录里的属性文件(attribute)属性文件的批量创建、分组、权限控制、可见性管理,全部由struct attribute_group和它背后的一整套kernfs / sysfs / VFS回调链来承担。本文目标是:从内核源码层面理解,执行cat属性文件到最终调用用户定义的show()这条完整链路
理解属性组,先要从三个基础结构体入手:
attribute(基础的描述)
kobj_attribute(增加读写函数)
attribute_group(一组属性文件)
attribute_group结构体定义了属性组的名称、可见性,以及具体的属性数组指针。其中attribute数组中,每个属性元素都保存用户定义的show和store函数,内核最后会通过一系列回调找到该函数调用。
// 定义在 /include/linux/sysfs.h 文件中struct attribute_group {const char *name; // 子目录名,若为NULL则直接创建在kobj目录下umode_t (*is_visible)(struct kobject *, struct attribute *, int);umode_t (*is_bin_visible)(struct kobject *, struct bin_attribute *, int);struct attribute **attrs; // 普通属性 指针数组(NULL结尾)struct bin_attribute **bin_attrs; // 二进制属性 指针数组(NULL结尾)};struct attribute {const char *name;umode_t mode;};struct kobj_attribute {struct attribute attr; // 嵌入一个标准属性,包含 char* name 和 short modessize_t (*show)(struct kobject *kobj, struct kobj_attribute *attr,char *buf);ssize_t (*store)(struct kobject *kobj, struct kobj_attribute *attr,const char *buf, size_t count);};
structdevice_attribute {structattributeattr; // 嵌入一个标准属性,包含 char* name 和 short modessize_t (*show)(structdevice *dev, structkobj_attribute *attr,char *buf);ssize_t (*store)(structdevice *dev, structkobj_attribute *attr,constchar *buf, size_tcount);};
grp->name 决定是否建立子目录:
NULL 时属性文件直接落在 kobj 目录下
!NULL 时在该名字下再建一层子目录
attrs 与 bin_attrs 都是指针数组且必须以 NULL 结尾,这是内核遍历的终止条件
上一小节给出了这两个结构体具体的定义,本小节补充为什么需要对attribute包装?
struct attribute:最基础的「文件名 + 权限」描述。
struct kobj_attribute:把attribute嵌进去,并追加show/store两个函数指针,通过container_of实现用户函数回调
struct device_attribute:面向struct device的同构封装(show第一个参数是struct device*),由于kobject太通用了,开发者通常操作device,使用这个结构体可以少一步转换工作
内核采用「基类 + 子类回调转发」的面向对象手法:sysfs只认struct attribute,具体子系统(kobject / device / driver / bus / class)各自定义带函数的子类,运行时用 container_of 从基类指针反推出子类,再调用用户函数。这是基于kobj_sysfs_ops转发机制实现的
内核源码提供了一组宏,自动完成结构体初始化。宏展开后等价于一份标准的结构体定义:
// include/linux/sysfs.h —— 属性定义辅助宏#define __ATTR(_name, _mode, _show, _store) { \ .attr = { .name = __stringify(_name), \ .mode = VERIFY_OCTAL_PERMISSIONS(_mode) }, \ .show = _show, \ .store = _store, \}// 常用便捷宏(无需手写 struct 初始化)#define __ATTR_RO(_name) { .attr = { .name = __stringify(_name), .mode = 0444 }, .show = _name##_show }#define __ATTR_WO(_name) { .attr = { .name = __stringify(_name), .mode = 0200 }, .store = _name##_store }#define __ATTR_RW(_name) { .attr = { .name = __stringify(_name), .mode = 0644 }, .show = _name##_show, .store = _name##_store }// include/linux/device.h —— 面向 struct device 的封装#define DEVICE_ATTR(_name, _mode, _show, _store) \struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)#define DEVICE_ATTR_RO(_name) struct device_attribute dev_attr_##_name = __ATTR_RO(_name)#define DEVICE_ATTR_WO(_name) struct device_attribute dev_attr_##_name = __ATTR_WO(_name)#define DEVICE_ATTR_RW(_name) struct device_attribute dev_attr_##_name = __ATTR_RW(_name)
1.4 kernfs_node 内核文件系统节点
kernfs_node代表文件系统树中的一个节点(目录、普通文件或符号链接)。它封装了与文件系统交互所需的所有信息,关联到上层的内核对象(如kobject、cgroup等)。对于属性文件而言,其重要成员变量有:
name:属性节点的文件名
attr:属性节点特有信息,含kernfs_ops,判断属性文件是否支持顺序读取、回调kobject的show函数)
priv:指向attribute*,可以通过它反推出用户定义的show和store函数
// 定义在 include/linux/kernfs.h struct kernfs_node {const char *name; /* 节点名称(目录名/文件名) */struct rb_noderb; /* 红黑树节点,用于在父目录中按名称查找 */struct kernfs_node *parent; /* 指向父节点的指针 */unsigned inthash; /* 名称哈希值,加速查找 */const void *ns; /* 命名空间标签,用于支持命名空间隔离 */union {struct kernfs_elem_dirdir; /* 目录特有信息 */struct kernfs_elem_symlink symlink; /* 符号链接特有信息 */struct kernfs_elem_attrattr; /* 文件(属性)特有信息 */ };void *priv; /* 私有数据,常用于存储上层结构指针 */unsigned int flags; /* 节点标志(类型、状态等) */umode_t mode; union kernfs_node_idid;struct kernfs_iattrs *iattr; /* inode 属性(权限、时间戳等),延迟分配 */atomic_tcount; /* 引用计数,管理生命周期 */atomic_tactive; /* 活动引用计数,用于保护正在进行的操作 */};
在kernfs_node的union中,属性节点使用kernfs_elem_attr来保存操作函数与打开状态:
structkernfs_elem_attr {const struct kernfs_ops *ops;struct kernfs_open_node *open;loff_t size;struct kernfs_node *notify_next;};struct kernfs_ops {int (*open)(struct kernfs_open_file *of);int (*release)(struct kernfs_open_file *of);// 省略了一系列函数// seq_show, seq_start, seq_next, seq_stop, read, write, poll, mmap};
sysfs_create_group函数在kobj对应的sysfs目录下,根据attribute_group的描述创建一组属性文件。其具体工作内容为:
创建目录:若属性组目录名grp->name不为空,则在kobj目录下创建一个该名字的子目录,属性文件创建在该目录下
创建属性文件:遍历grp->attrs数组,对每个非空属性调用sysfs_add_file创建对应sysfs文件;若is_visible不为空,则调用它确定该属性是否被创建以及对应权限
创建二进制属性文件:遍历grp->bin_attrs数组,创建对应的二进制文件
更新文件判断:若update参数为1,则上述逻辑会先移除已有文件,再创建更新
// 定义在 /fs/sysfs/group.c 文件中int sysfs_create_group(struct kobject *kobj, const struct attribute_group *grp) {return internal_create_group(kobj, 0, grp);}static int internal_create_group(struct kobject *kobj, int update, const struct attribute_group *grp) {struct kernfs_node *kn;kuid_t uid;kgid_t gid;int error;// ...错误检查省略// 用于配置用户id和组id,为了确定文件权限,默认情况下两个都是0// 若kobj->ktype->get_ownership不为空,则内部调用该函数并传出参数uid和gidkobject_get_ownership(kobj, &uid, &gid); // 1.如果属性组名称存在,则新建子目录,否则获取kobj的目录if (grp->name) {if (update) { // 省略了更新的情况 } else {kn = kernfs_create_dir_ns(kobj->sd, grp->name, S_IRWXU | S_IRUGO | S_IXUGO, uid, gid, kobj, NULL); } } else {kn = kobj->sd; }// 2.在 kn 目录下,创建属性文件error = create_files(kn, kobj, uid, gid, grp, update); }
sysfs_create_group() => internal_create_group() => create_files() => sysfs_add_file_mode_ns()
该函数完成对属性组结构体attribute_group*的遍历,为每个属性创建文件:
staticint create_files(struct kernfs_node *parent, struct kobject *kobj,kuid_t uid, kgid_t gid,const struct attirbute_group *grp, int update){struct attribute *const *attr;struct bin_attribute *const *bin_attr;int error = 0, i;if (grp->attrs) {for (i = 0; attr = grp->attrs; *attr && !error; i++, attr++) {umode_t mode = (*attr)->mode;// ...省略部分逻辑error = sysfs_add_file_mode_ns(parent, *attr, false, mode, uid, gid, NULL); } }if (grp->bin_attrs) {for (i = 0; bin_attr = grp->bin_attrs; *bin_attr; i++, bin_attr++) {umode_t mode = (*bin_attr)->attr.mode;// ...省略部分逻辑error = sysfs_add_file_mode_ns(parent, &(*bin_attr)->attr, true, mode, uid, gid, NULL); } }return error;}
sysfs依赖于内核kernfs内核文件系统。该函数传入kernfs的父目录节点parent、属性attr等参数,在内部调用__kernfs_create_file完成内核节点的创建:
// 定义在 /fs/sysfs/file.c 文件中int sysfs_add_file_mode_ns(struct kernfs_node *parent,const struct attribute *attr,bool is_bin,umode_t mode,kuid_t uid,kgid_t gid,const void *ns){struct lock_class_key *key = NULL;const struct kernfs_ops *ops;struct kernfs_node *kn;loff_t size;/* 根据属性类型设置 kernfs 节点标志 */if (!is_bin) {// 创建kernfs_node时,priv指针指向了目录对应的kobjstruct kobject *kobj = parent->priv;// 创建kobj时,ktype中的sysfs_ops传入了默认参数 kobj_sysfs_ops// 该函数通过传入的attr来调用属性自己的show和store函数const struct sysfs_ops *sysfs_ops = kobj->ktype->sysfs_ops;// 判断指针是否存在(默认参数 kobj_sysfs_ops 中两个指针都存在)if(sysfs_ops->show && sysfs_ops->store) {if (mode & SYSFS_PREALLOC)ops = &sysfs_file_kfops_rw;elseops = &sysfs_file_kfops_rw; // ops被设置为该对象 } elseif (sys_ops->show) {// 逻辑相同,判断sys_ops的两个操作函数 }size = PAGE_SIZE; // ((1UL) << 12) }
else {// ...二进制处理structbin_ttribute *battr = (void *)attr;
size = battr->size; }/* 创建 kernfs 节点 */kn = __kernfs_create_file (parent, attr->name,mode & ~S_IFMT, /* 去除文件类型位 */uid, gid,size, &ops, /* 操作函数 */ (void *)attr, /* 私有数据指向 attr */ns, /* 命名空间 */key);if (IS_ERR(kn))return PTR_ERR(kn);return 0;}
PAGE_SIZE 缓冲:普通属性文件的大小被固定为PAGE_SIZE(通常 4096 字节,即 (1UL) << 12)。这意味着一次show最多返回一页数据——这是sysfs在内存上的约束,后面读路径还会再遇到它
该结构体是__kernfs_create_file中传入的ops操作函数,在系统中已预定义,最终会赋值给kernfs_node->attr->ops:
staticconststructkernfs_opssysfs_file_kfops_rw = { .seq_show = sysfs_kf_seq_show, .write = sysfs_kf_write,};
该函数的作用是调用kobject的show函数,并把该文件对应的属性attr传输给kobject的show函数:
staticint sysfs_kf_seq_show(struct seq_file *sf, void *v){struct kernfs_open_file *of = sf->private;// kernfs_open_file "of" -> kernfs_node "kn" 属性的文件节点 ->// kernfs_node "parent" kobject的文件节点 -> kobject "priv" 属性顶层的kobject// 1. 获取该 kernfs_open_file 对应的kobject struct kobject *kobj = of->kn->parent->priv;// 获取 kernfs_node->patern->priv->ktype->sysfs_ops 中的show函数// 2. 即 kobject 中的 show 函数const struct sysfs_ops *ops = sysfs_file_ops(of->kn);ssize_t count;char *buf;// 确保seq_file指针中分配的内存剩余空间至少有1页count = seq_get_buf(sf, &buf);if (count < PAGE_SIZE) {seq_commit(sf, -1);return 0; }memset(buf, 0, PAGE_SIZE);// 3. 调用 kobject 的 show 函数if (ops->show) {count = ops->show(kobj, of->kn->priv, buf);if (count < 0)return count; }// 每次写入的大小不能超过一页空间if (count >= (ssize_t)PAGE_SIZE) {printk("fill_read_buffer: %pS returned bad count\n",ops->show);count = PAGE_SIZE - 1; }seq_commit(sf, count);return 0;}
该函数是kernfs的函数,用于在指定父目录下创建新节点并支持命名空间隔离。sysfs基于kernfs实现,创建子目录/文件时都会调用它:
parent:父目录的 kernfs 节点
name:目录/文件名称
mode:访问权限(如 0755)
uid / gid:所有者用户/组 ID
size:文件大小(属性文件为 PAGE_SIZE)
priv:私有数据指针,通常关联 kobject / attr,可通过 kernfs_priv() 获取
ns:命名空间隔离,非 NULL 时仅该命名空间进程可见
// 定义在 /fs/kernfs/dir.cstruct kernfs_node *__kernfs_create_file(struct kernfs_node *parent,const char *name, umode_t mode,kuid_t uid, kgid_t gid, loff_t size,void *priv, const void *ns,struct lock_class_key *key) {struct kernfs_node *kn;int rc;// ...省略错误检查/* 1.分配 kernfs_node 结构体 */kn = kernfs_new_node(parent, name, (mode & S_IALLUGO) | S_IFDIR, uid, gid, KERNFS_DIR);/* 2.操作接口赋值 */kn->attr.ops = ops; // kernfs_opskn->attr.size = size;kn->ns = ns;/* 3. 文件属性赋值*/kn->priv = priv; // 私有数据指向attr属性/* 4.将节点插入到父目录的树中 */rc = kernfs_add_one(kn);if (ops->seq_show)kn->flags |= KERNFS_HAS_SEQ_SHOW;//.../* 返回新节点 */return kn;}EXPORT_SYMBOL_GPL(kernfs_create_dir_ns);
💡is_visible / is_bin_visible 的价值:is_visible 用于确定属性是否被创建及对应权限。内核里它极其重要:同一份驱动要适配不同硬件能力时,可以用它动态隐藏不支持的属性文件。返回0表示不创建该文件,返回非0(通常是 attr->mode)表示以该权限创建。下面给出 drivers/base/topology.c 的真实范例:
/* 真实案例:drivers/base/topology.c 中的 is_visible 回调 */static umode_t topology_is_visible(struct kobject *kobj,struct attribute *attr, int unused){/* PPIN 属性:硬件不支持则该文件不可见 */if (attr == &dev_attr_ppin.attr && !topology_ppin(kobj_to_dev(kobj)->id))return 0; // 返回 0 = 隐藏该属性文件return attr->mode; // 否则返回默认权限}static const struct attribute_group topology_attr_group = { .attrs = default_attrs, .bin_attrs = bin_attrs, .is_visible = topology_is_visible, // 创建前动态决定可见性 .name = "topology", // 在设备目录下生成 topology/ 子目录};/* CPU 上线时为该 CPU 设备挂接 topology 接口 */static int topology_add_dev(unsigned int cpu){struct device *dev = get_cpu_device(cpu);return sysfs_create_group(&dev->kobj, &topology_attr_group);}
属性文件创建后,用户空间通过标准open / read / write系统调用访问它。这中间需要把kernfs节点转化成VFS能识别的inode与file。
structinode {/**** 权限和所有权 ****/umode_ti_mode; /* 文件类型及权限(如 S_IFREG|0644) */kuid_ti_uid; /* 所有者用户 ID */kgid_ti_gid; /* 所有者组 ID */unsigned inti_flags; /* 文件标志(如 FS_IMMUTABLE_FL) *//**** 标识和定位 ****/ino_ti_ino; /* inode 号,在文件系统中唯一 */union {const unsigned int i_nlink; /* 硬链接计数 */unsigned int __i_nlink; };dev_ti_rdev; /* 设备号(对设备文件) */loff_ti_size; /* 文件大小(字节) */struct timespec64 i_atime; /* 最后访问时间 */struct timespec64 i_mtime; /* 最后修改时间 */struct timespec64 i_ctime; /* 最后状态改变时间 *//*** 操作函数表 ***/const struct inode_operations *i_op; /* inode 操作 */const struct file_operations *i_fop; /* 文件操作(默认) */struct address_space *i_mapping; /* 地址空间映射 */struct address_spacei_data; /* 内嵌地址空间 */struct Super_block *i_sb; /* 指向所属超级块 *//**** 私有数据和关联对象 ****/void *i_private; /* 文件系统私有数据 */union {struct hlist_head i_dentry; /* 指向该 inode 的 dentry 链表 */struct rcu_headi_rcu; /* 用于 RCU 释放 */ };// ... 省略部分字段};
kernfs_init_inode是kernfs中用于初始化一个新分配VFS inode的关键函数。它将struct inode与对应的struct kernfs_node*关联,并根据节点类型设置合适的操作函数表:
// 定义在 /fs/kernfs/inode.c 文件中static void kernfs_init_inode(struct kernfs_node *kn, struct inode *inode) {kernfs_get(kn);inode->i_private = kn; // inode与内核文件节点关联inode->i_mapping->a_ops = &kernfs_aops; // 定义文件系统处理页缓存inode->i_op = &kernfs_iops; // 临时设置通用的inode操作表inode->i_generation = kn->id.generation; // 唯一标识,表示已经创建set_default_inode_attr(inodekn->mode); // 设置默认inode属性kernfs_refresh_inode(kn, inode); // 刷新属性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:// 将kernfs_file_fops赋值给i_fop// 包含open、read、write等函数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); // 解锁该inode,正式可以访问}
在kernfs_init_inode中,会给属性文件对应的inode->i_fop赋值为默认的文件操作结构体:
conststructfile_operationskernfs_file_fops = { .read = kernfs_fop_read, .write = kernfs_fop_write, .llseek = generic_file_llseek, .mmap = kernfs_fop_mmap, .open = kernfs_fop_open, .release = kernfs_fop_release, .poll = kernfs_fop_poll, .fsync = noop_fsync,};
staticint kernfs_fop_open(struct inode *inode, struct file *file) {// 1. 从inode私有数据获取对应的kernfs节点struct kernfs_node *kn = inode->i_private;// 2. 获取节点所属的kernfs根struct kernfs_root *root = kernfs_root(kn);const struct kernfs_ops *ops;struct kernfs_open_file *of;bool has_read, has_write, has_mmap;int error = -EACCES;// ...省略错误检查// 3.从kernfs内核文件中获取kn->attr.ops文件具体的操作函数ops = kernfs_ops(kn);has_read = ops->seq_show || ops->read || ops->mmap;has_write = ops->write || ops->mmap;has_mmap = ops->mmap;// 4.初始化 kernfs_open_file 结构of = kzalloc(sizeof(structkernfs_open_file), GFP_KERNEL);mutex_init(&of->mutex);of->kn = kn; // 保存kernfs节点指针of->file = file; // 保存VFS文件对象指针of->atomic_write_len = ops->atoic_write_len; // 单次写入字节限制// 5.如果操作集要求预分配缓冲区,则分配单次写入字节限制或PAGE_SIZEif (ops->prealloc) {intlen = of->atomic_write_len ?: PAGE_SIZE;of->prealloc_buf = kmalloc(len+1, GFP_KERNEL);mutex_init(&of->prealloc_mutex); }// 6.初始化seq_file接口(若文件支持顺序读取)if (ops->seq_show)error = seq_open(file, &kernfs_seq_ops);elseerror = seq_open(file, NULL);// 7.关联seq_file和ofof->seq_file = file->private_data;of->seq_file->private = of;// 8.设置文件写标志,表示允许带带偏移的鞋操作if (file->f_mode & FMODE_WRITE)file->f_mode |= FMODE_PWRITE;// 9.将of添加到节点打开文件链表error = kernfs_get_open_node(kn, of);// 10.调用文件特定的open回调if (ops->open) {error = ops->open(of); }// 11.释放活动kernfs_put_active(kn);return 0;}
seq_open是内核seq_file接口的核心函数,用于初始化一个顺序文件的私有数据,并关联到已打开的VFS file对象:
/* *@file: 已打开的VFS文件对象 *@op: 包含一组回调函数 *返回值: 成功返回0*/int seq_open(struct file *file, const struct seq_operations *op) {struct seq_file *p;p = kmem_cache_zalloc(seq_file_cache, GFP_KERNEL);// 将分配好的seq_file指针存入私有对象,后续read、write可以通过private操作file->private_data = p; mutex_init(&p->lock);p->op = op; // 保存操作集p->file = file; // 保存file指针file->f_version = 0;file->f_mode &= ~FMODE_PWRITE;return 0;}
seq_file是内核提供的简化顺序文件读写接口,主要用于输出大量文本数据。开发者只需实现一套(start / next / stop / show)回调,seq_file自动处理缓冲区管理、分页与用户态读取。kernfs为其预置了如下操作集:
staticconststructseq_operationskernfs_seq_ops = { .start = kernfs_seq_start, .next = kernfs_seq_next, .stop = kernfs_seq_stop, .show = kernfs_seq_show,};
staticssize_t kernfs_fop_read(struct file *file, char __user *user_buf,size_t count, loff_t *ppos){struct kernfs_open_file *of = kernfs_of(file);// return ((struct seq_file *)file->private_data)->private;// 在__kernfs_create_file函数中,若seq->show则赋值该标志if(of->kn->flags & KERNFS_HAS_SEQ_SHOW) return seq_read(file, user_buf, count, ppos)elsereturn kernfs_file_direct_read(of, user_buf, count, ppos);}
ssize_tseq_read(structfile *file, char__user *buf, size_tsize, loff_t *ppos){structseq_file *m = file->private_data;void *p;// 省略大部分代码err = m->op->show(m, p);err = copy_to_user(buf, m->buf, n);}
staticintkernfs_seq_show(structseq_file *sf, void *v) {structkernfs_open_file *of = sf->private;// ...returnof->kn->attr.ops->seq_show(sf, v); }
kernfs_seq_show只是转发器,它从of->kn->attr.ops取出真正由sysfs注册的seq_show(sysfs_kf_seq_show)
staticint sysfs_kf_seq_show(structseq_file *sf, void *v) {struct kernfs_open_file *of = sf->private;struct kobject *kobj = of->kn->parent->priv;const structsysfs_ops *ops = sysfs_file_ops(of->kn);ssize_t count;char *buf;count = seq_get_buf(sf, &buf);if (count < PAGE_SIZE) {seq_commit(sf, -1);return0; }memset(buf, 0, PAGE_SIZE);if (ops->show) {count = ops->show(kobj, of->kn->priv, buf);if (count < 0)returncount; }if (count >= (ssize_t) PAGE_SIZE) {count = PAGE_SIZE -1; }seq_commit(sf, count);return 0;}
staticinlinesize_tseq_get_buf(structseq_file *m, char **bufp) {BUG_ON(m->count > m->size);if (m->count < m->size) *bufp = m->buf + m->count;else *bufp = NULL;returnm->size - m->count;}
staticconststructsysfs_ops *sysfs_file_ops(structkernfs_node *kn) {structkobject *kobj = kn->parent->priv;if (kn->flags & KERNFS_LOCKEDP)lockdep_assert_held(kn);returnkobj->ktype ? Kobj->ktype->sysfs_ops : NULL;}
staticssize_t kobj_attr_show(struct kobject *kobj, structattribute *attr, char *buf){struct kobj_attribute *kattr;ssize_t ret = -EIO;kattr = container_of(attr, struct kobj_attribute, attr);if (kattr->show)ret = kattr->show(kobj, kattr, buf);return ret;}
读有完整链路,写完全对称
echo val > filekernfs_fop_writesysfs_kf_writekobj_sysfs_ops->storekattr->store:
/* 写路径:echo -> kernfs_fop_write -> sysfs_kf_write -> kobj_attr_store */static ssize_t sysfs_kf_write(struct kernfs_open_file *of, char *buf, size_t count){const struct sysfs_ops *ops = sysfs_file_ops(of->kn);/* kernfs 已保证 buf 以 '\0' 结尾(预分配或 PAGE_SIZE 缓冲) */return ops->store(of->kn->parent->priv, of->kn->priv, buf, count);}/* kobj_sysfs_ops 中的 store 转发:从 attribute 反推 kobj_attribute */static ssize_t kobj_attr_store(struct kobject *kobj, struct attribute *attr,const char *buf, size_t count){struct kobj_attribute *kattr;ssize_t ret = -EIO;kattr = container_of(attr, struct kobj_attribute, attr);if (kattr->store)ret = kattr->store(kobj, kattr, buf, count);return ret;}
🔑 读写签名差异:
底层sysfs_ops的show/store第一个参数是struct kobject*;kobj_attribute的show/store第一个参数是struct kobject*,第二个参数是struct kobj_attribute*——kobj_attr_show用container_of把基类attribute还原成子类,从而把「属性自身」也传给了用户定义的回调,这正是用户能在回调里访问kattr->private或判断是哪个属性的原因
把全文串起来,内核完成了一系列复杂的回调,最终实现了用户定义函数的调用:
file 中保存了 seq_file 结构体,首先调用 seq_file->op->show;
从而调用了 kernfs_node 中保存的函数 kernfs_node->attr.ops->seq_show;
从而调用了 kobj->ktype->sysfs_ops->show 函数;
最后调用了 kattr->show 函数。
💡 一句话概括整条链:整条路径从 file 回到 kernfs_node,将 attr 作为参数传递,再回到 kobject,然后通过 kobject 中的函数调用 attr 中用户定义的函数。可以压缩为一行:cat /sys/.../foo → vfs_read → kernfs_fop_read → sysfs_kf_seq_show → kobj_attr_show → foo_show()
理论讲完,下面给出一个最小内核模块,演示定义kobj_attribute → 组织 attribute_group → 调用 sysfs_create_group → 用户态读写的流程。插入后会在 /sys/kernel/mydrv/ 下生成 level(可读写)与 enabled(只读)两个文件:
#include<linux/module.h>#include <linux/kernel.h>#include <linux/kobject.h>#include <linux/sysfs.h>#include <linux/slab.h>/* 驱动私有数据结构,作为属性文件的背后状态 */struct mydrv_ctx {int level; // 当前档位 0~3bool enabled; // 开关};/* ---- show / store 回调 ---- */static ssize_t level_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf){struct mydrv_ctx *ctx = dev_get_drvdata(kobj_to_dev(kobj)); // 仅示例return sprintf(buf, "%d\n", ctx->level);}static ssize_t level_store(struct kobject *kobj, struct kobj_attribute *attr,const char *buf, size_t count){struct mydrv_ctx *ctx = dev_get_drvdata(kobj_to_dev(kobj));int val;if (kstrtoint(buf, 10, &val))return -EINVAL;if (val < 0 || val > 3)return -EINVAL;ctx->level = val;return count;}static struct kobj_attribute level_attr = __ATTR(level, 0644, level_show, level_store);static ssize_t enabled_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf){structmydrv_ctx *ctx = dev_get_drvdata(kobj_to_dev(kobj));returnsprintf(buf, "%d\n", ctx->enabled);}static struct kobj_attribute enabled_attr = __ATTR(enabled, 0444, enabled_show, NULL);/* ---- 属性组 ---- */static struct attribute *mydrv_attrs[] = { &level_attr.attr, &enabled_attr.attr,NULL, // 必须以 NULL 结尾};static const struct attribute_group mydrv_group = { .attrs = mydrv_attrs,// .name = "mydrv", // 若指定 name,会在 kobj 目录下再建子目录 mydrv/};static struct kobject *mydrv_kobj;static struct mydrv_ctxctx = { .level = 1, .enabled = true };static int __init mydrv_init(void){/* 在 /sys/kernel/ 下创建目录 mydrv */mydrv_kobj = kobject_create_and_add("mydrv", kernel_kobj);if (!mydrv_kobj)return -ENOMEM;/* 创建属性文件 level / enabled */return sysfs_create_group(mydrv_kobj, &mydrv_group);}static void __exit mydrv_exit(void){sysfs_remove_group(mydrv_kobj, &mydrv_group);kobject_put(mydrv_kobj);}module_init(mydrv_init);module_exit(mydrv_exit);MODULE_LICENSE("GPL");
📌 自动建文件的更优写法:若属性属于某个 kobj_type 的默认组,可在 kobj_type.default_groups(或驱动的 dev_groups)里挂上组数组,kobject 注册时由 create_dir() 自动调用 sysfs_create_groups 完成创建,连 sysfs_create_group 都不用显式调用:
/* 让 kobject 注册时自动创建默认属性组(无需手动调用 sysfs_create_group) */static struct attribute *foo_attrs[] = { &dev_attr_value.attr, &dev_attr_mode.attr,NULL,};ATTRIBUTE_GROUPS(foo); /* 生成 foo_groups[] = { &foo_group, NULL } */static const struct kobj_type foo_ktype = { .release = foo_release, .sysfs_ops = &kobj_sysfs_ops, .default_groups = foo_groups, /* create_dir() 时自动 populate_dir + create_groups */};/* 在驱动中更常见:platform_driver 直接挂 dev_groups */static struct platform_drivermydrv = { .driver = { .name = "mydev", .dev_groups = my_groups, /* 设备 probe 时自动建属性文件 */ },};
普通attribute的show受PAGE_SIZE单页缓冲限制;bin_attribute 的read/write回调带loff_t off与size_t count参数,支持分块、随机偏移读取任意大小数据(典型如硬件寄存器 dump、固件导出)。定义用BIN_ATTR宏,并挂到attribute_group.bin_attrs:
/* 硬件寄存器 dump:突破单页 4KB 限制,支持随机偏移读取 */static ssize_t regs_read(struct file *filp, struct kobject *kobj,struct bin_attribute *bin_attr,char *buf, loff_t off, size_t count){struct mydev *dev = container_of(kobj, structmydev, kobj);size_t regs_size = 0x1000;if (off >= regs_size) // 越界return 0;if (off + count > regs_size) // 截断到合法范围count = regs_size - off;memcpy_fromio(buf, dev->regs_base + off, count); // 从 MMIO 空间拷贝return count;}/* BIN_ATTR 宏定义二进制属性(名称, 权限, read, write, size) */static BIN_ATTR(regs, 0400, regs_read, NULL, 0x1000);static struct bin_attribute *my_bin_attrs[] = { &bin_attr_regs,NULL,};static const struct attribute_group my_group = { .attrs = my_attrs, .bin_attrs = my_bin_attrs,};
属性组的移除必须和创建严格对称,否则会在 sysfs 留下僵尸文件节点:
/* 删除属性组(与创建严格对称,防止 sysfs 残留文件) */void sysfs_remove_group(struct kobject *kobj, const struct attribute_group *grp){struct kernfs_node *parent;if (grp->name) {/* 有 name 时,文件在 name 子目录下 */parent = kernfs_find_and_get(kobj->sd, grp->name);if (!parent)return; } else {parent = kobj->sd;kernfs_get(parent); }kernfs_remove_by_name(parent, grp->name ?: ""); // 移除子目录或文件remove_files(parent, kobj, grp); // 逐一移除属性文件kernfs_put(parent);}

【Linux驱动】字符设备驱动内核源码深度解析
