LV06-04-linux设备模型-03-sysfs文件系统

sysfs是什么?和内核对象有啥关系?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、sysfs 文件系统

1. sysfs 简介

sysfs 文件系统是 Linux 内核提供的一种基于RAM的虚拟文件系统,用于向用户空间提供内核中设备, 驱动程序和其他内核对象的信息。它以一种层次结构的方式组织数据,并将这些数据表示为文件和目录,使得用户空间可以通过文件系统接口访问和操作内核对象的属性。

sysfs是一种表示内核对象、对象属性,以及对象关系的一种机制,一般内核对象、属性、以及对象关系组织成树状形式。其中内核对象被映射为用户态的目录;对象属性被映射为用户态的文件,文件在目录下;对象关系被映射成用户空间的符号链接。

sysfs 提供了一种统一的接口,用于浏览和管理内核中的设备、总线、驱动程序和其他内核对象。它在 /sys 目录下挂载,用户可以通过查看和修改 /sys 目录下的文件和目录来获取和配置内核对象的信息。

sysfs 文件系统总是被挂载在 /sys 挂载点上。虽然在较早期的 2.6 内核系统上并没有规定 sysfs 的标准挂载位置,可以把 sysfs 挂载在任何位置,但 2.6 内核修正了这一规则,要求 sysfs 总是挂载在 /sys 目录上;针对以前的 sysfs 挂载位置不固定或没有标准被挂载,有些程序从 /proc/mounts 中解析出 sysfs 是否被挂载以及具体的挂载点,这个步骤现在已经不需要了。

2. /sys目录

我们知道在Linux中一切皆“文件”,在根文件系统中有个/sys文件目录,里面记录各个设备之间的关系。 下面学习/sys下几个较为重要目录的作用。

image-20250105105341778

其中和设备模型有关的文件夹为 bus, class, devices。

2.1 /sys/bus

/sys/bus目录下的每个子目录都是注册好了的总线类型:

image-20250105105704308

这里是设备按照总线类型分层放置的目录结构, 每个子目录(总线类型)下包含两个子目录——devices和drivers文件夹。其中devices下是该总线类型下的所有设备, 而这些设备都是符号链接,它们分别指向真正的设备(/sys/devices/下),例如/sys/bus/i2c:

image-20250105110207314

2.2 /sys/devices

/sys/devices目录下是全局设备结构体系,包含所有被发现的注册在各种总线上的各种物理设备。一般来说, 所有的物理设备都按其在总线上的拓扑结构来显示。/sys/devices是内核对系统中所有设备的分层次表达模型, 也是/sys文件系统管理设备的最重要的目录结构。

每个设备子目录代表一个具体的设备, 通过其路径层次结构和符号链接反映设备的关系和拓扑结构。 每个设备子目录中包含了设备的属性、 状态和其他相关信息。 如下图:

image-20250105111237343

2.3 /sys/class

/sys/class目录包含了设备类别的子目录。 每个子目录代表一个设备类别, 例如磁盘、网络接口等。 每个设备类别子目录中包含了属于该类别的设备的信息。我们知道每种设备都具有自己特定的功能,比如:鼠标的功能是作为人机交互的输入,按照设备功能分类无论它 挂载在哪条总线上都是归类到/sys/class/input下。

image-20250105111902569

使用 class 进行归类有哪些好处?

(1)逻辑上的组织: 通过将设备按照类别进行归类, 可以在设备模型中建立逻辑上的组织结构。 这样, 相关类型的设备可以被放置在同一个类别目录下, 使得设备的组织结构更加清晰和可管理。

(2)统一的接口和属性: 每个设备类别目录下可以定义一组统一的接口和属性, 用于描述和配置该类别下所有设备的共同特征和行为。 这样, 对于同一类别的设备, 可以使用相同的方法和属性来操作和配置, 简化了设备驱动程序的编写和维护。

(3)简化设备发现和管理: 通过将设备进行分类, 可以提供一种简化的设备发现和管理机制。 用户和应用程序可以在类别目录中查找和识别特定类型的设备, 而无需遍历整个设备模型。这样, 设备的发现和访问变得更加高效和方便。

(4)扩展性和可移植性: 使用class进行归类可以提供一种扩展性和可移植性的机制。 当引入新的设备类型时, 可以将其归类到现有的类别中, 而无需修改现有的设备管理和驱动程序。这种扩展性和可移植性使得系统更加灵活, 并且对于开发人员和设备供应商来说,更容易集成新设备。

比如应用现在要设置 gpio。我们先要对gpio进行导出,首先要知道引脚编号:

1
引脚编号 = GPIO编号 =(GROUP-1)*32+PIN

比如现在GPIO1_IO03,引脚编号就是3:

1
2
3
4
5
echo 3 > /sys/class/gpio/export             # 导出GPIO,导出成功,会生成 /sys/class/gpio/gpio3 目录
echo out > /sys/class/gpio/gpio3/direction # 设置GPIO方向
echo 1 > /sys/class/gpio/gpio3/value # 设置GPIO引脚的值

echo 3 > /sys/class/gpio/unexport # 取消导出

若是被占用了,在导出时就会报:

image-20250105114155397

这里就不管了,知道这么操作就行了。这里我就没有可用的实例,直接参考讯为的开发板吧:

1
2
3
4
# 如果使用类可以直接使用以下命令:
echo 1 > /sys/class/gpio/gpio157/value
# 如果不使用类, 使用以下命令:
echo 1 > /sys/devices/platform/fe770000.gpio/gpiochip4/gpio/gpio157/value

2.4 其他目录

/sys 下的子目录 所包含的内容
/sys/dev 这个目录下维护一个按字符设备和块设备的主次设备号(major:minor)链接到真实的设备(/sys/devices 下)的符号链接文件,它是在内核 2.6.26 首次引入;
/sys/block 这里是系统中当前所有的块设备所在,按照功能来说放置在 /sys/class 之下会更核识,但只是由于历史遗留因素而一直存在于 /sys/block,但从 2.6.22 开始就已标记为过时,只有在打开了 CONFIG_SYSFS_DEPRECATED 配置下编译才会有这个目录的存在,并且在 2.6.26 内核中已正式移到 /sys/class/block,旧的接口 /sys/block 为了向后兼容保留存在,但其中的内容已经变为指向它们在 /sys/devices/ 中真是设备的符号链接文件;
/sys/firmware 这里是系统加载固件机制的对用户空间的接口,关于固件有专用于固件加载的一套 API。
/sys/fs 这里按照设计是用于描述系统中所有文件系统,包括文件系统本身和按文件系统分类存放的已挂载点,但目前只有 fuse,ext4 等少数文件系统支持 sysfs 接口,一些传统的虚拟文件系统(VFS)层次控制参数仍然在 sysctl(/proc/sys/fs)接口中;
/sys/kernel 这里是内核所有可调整参数的位置,有些内核可调整参数仍然位于 sysctl(/proc/sys/fs)接口中;
/sts/module 这里有系统中所有模块的信息,不论这些模块是以内联(inlined)方式编译到内核映像文件(vmlinuz)中还是编译为外部模块(ko 文件),都可能会出现在 /sys/module 中:编译为外部模块(ko 文件)在加载后会出现对应的 /sys/module//,并且在这个目录下会出现一些属性文件和属性目录来表示次外部模块的一些信息,如版本号、加载状态、所提供的驱动程序等;编译为内联方式的模块则只在当它有非 0 属性的模块参数时会出现对应的 /sys/module/,这些模块的可用参数会出现在 /sys/module//parameters/ 中,如 /sys/module/printk/parameters/time 这个可读可写参数控制着内联模块 printk 在打印内核消息时是否加上时间前缀;所有内联模块的参数也可以由 “.=” 的形式写在内核启动参数上,如启动内核时加上参数 “printk.time = 1” 与向 “/sys/module/printk/parameters/time” 写入 1 的效果相同;没有非 0 属性参数的内联模快不会出现于此;
/sys/power 这里是系统中电源选项,这个目录下有几个属性文件可以用于控制整个机器的电源状态,如可以向其中写入控制命令让机器关机、重启等。

3. sys 目录层次图解

bus、device、class三个目录层次大概得关系如下:

image-20250105120320519

二、 sysfs与kobject

1. sysfs与kobject的关系

当使用 kobject 时,通常不会单独使用它,而是将其嵌入到一个数据结构中。这样做的目的是将高级对象接入到设备模型中。比如 struct cdev 结构体和 struct device 结构体,如下所示

image-20250106161831064

所以我们也可以把总线,设备,驱动看作是 kobject 的派生类。因为他们都是设备模型中的实体,通过继承或扩展 kobject 来实现与设备模型的集成。

在 Linux 内核中,kobject 是一个通用的基础结构,用于构建设备模型。每个 kobject 实例 对应于 sys 目录下的一个目录,这个目录包含了该 kobject 相关的属性,操作和状态信息。如下图所示:

image-20250106163909605

因此,可以说 kobject 是设备模型的基石,通过创建对应的目录结构和属性文件, 它提 供了一个统一的接口和框架,用于管理和操作设备模型中的各个实体。

2. 代码逻辑

上面提到每一个kobject,都会对应sysfs中的一个目录,这个目录怎么被创建的?从哪里开始分析呢?肯定是从 kobject_create_and_add() 这个函数创建 kobject 作为入手点,前面写 kobject create demo 的时候我们知道父节点为 NULL 的时候,对应的目录会在系统根目录/sys 下创建。函数追踪路径如下:

image-20250106171744580

2.1 kobject_create_and_add()

kobject_create_and_add() 函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct kobject *kobject_create_and_add(const char *name, struct kobject *parent)
{
struct kobject *kobj;
int retval;

kobj = kobject_create();
if (!kobj)
return NULL;

retval = kobject_add(kobj, parent, "%s", name);
if (retval) {
pr_warn("%s: kobject_add error: %d\n", __func__, retval);
kobject_put(kobj);
kobj = NULL;
}
return kobj;
}
EXPORT_SYMBOL_GPL(kobject_create_and_add);

首先调用了 kobject_create()函数创建看一个新的 kobject。该函数分配了一 个新的 kobject 结构体,并对其进行初始化,包括将 kobject 的 name 字段设置为传入的 name 参数,将 kobject 的 parent 字段设置为传入的 parent 参数。

接下来,函数调用 kobject_add() 将新创建的 kobject 添加到设备模型中.

2.2  kobject_add()

kobject_add() 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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) {
pr_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;
}
EXPORT_SYMBOL(kobject_add);

函数调用 kobject_add_varg() ,该函数会根据传入的参数将 kobj 添加到设备 模型中。 kobject_add_varg() 函数的实现可能会根据具体情况进行一些额外的处理,例如创建对应的目录并设置父节点等。

2.3 kobject_add_varg()

kobject_add_varg() 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static __printf(3, 0) int kobject_add_varg(struct kobject *kobj,
struct kobject *parent,
const char *fmt, va_list vargs)
{
int retval;

retval = kobject_set_name_vargs(kobj, fmt, vargs);
if (retval) {
pr_err("kobject: can not set name properly!\n");
return retval;
}
kobj->parent = parent;
return kobject_add_internal(kobj);
}

kobject_add_varg() 函数用于将指定的 kobject 添加到设备模型中。它通过调 用 kobject_set_name_vargs() 设置 kobject 的名称,并将父节点赋值给 kobject 的 parent 字段。然 后,它调用 kobject_add_internal() 函数执行实际的添加操作。

2.4 kobject_add_internal()

kobject_add_internal() 函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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) {
if (!parent)
parent = kobject_get(&kobj->kset->kobj);
kobj_kset_join(kobj);
kobj->parent = parent;
}

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)
pr_err("%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
pr_err("%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() 函数用于在设备模型中添加指定的 kobject。它会检查 kobject 的有效 性和名称是否为空,并处理 kobject 所属的 kset 相关的操作。然后,它会创建 kobject 在 sysfs 中的目录,并处理创建失败的情况。最后,它会设置 kobject 的相关状态,并返回相应的错误。

2.5 create_dir()

create_dir() 函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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;
}

create_dir() 函数用于创建与给定 kobject 相关联的目录,并填充该目录。它还处理了引用计数、命名空间操作等相关的逻辑。如果创建目录或填充目录时发生错误,函数会相应地处理并 返回错误码。函数调用sysfs_create_dir_ns() 函数来创建与kobj相关联的目录。

2.6 sysfs_create_dir_ns()

sysfs_create_dir_ns() 函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int sysfs_create_dir_ns(struct kobject *kobj, const void *ns)
{
struct kernfs_node *parent, *kn;
kuid_t uid;
kgid_t gid;

BUG_ON(!kobj);

if (kobj->parent)
parent = kobj->parent->sd;
else
parent = sysfs_root_kn;

if (!parent)
return -ENOENT;

kobject_get_ownership(kobj, &uid, &gid);

kn = kernfs_create_dir_ns(parent, kobject_name(kobj),
S_IRWXU | S_IRUGO | S_IXUGO, uid, gid,
kobj, ns);
if (IS_ERR(kn)) {
if (PTR_ERR(kn) == -EEXIST)
sysfs_warn_dup(parent, kobject_name(kobj));
return PTR_ERR(kn);
}

kobj->sd = kn;
return 0;
}

在上面的函数中,当没有父节点的时候,父节点被赋值成了 sysfs_root_kn,即/sys 目录根 目录的节点。如果有 parent,则它的父节点为 kobj->parent->sd,然后调用 kernfs_create_dir_ns() 函数创建目录。

2.7 __init sysfs_init()

那么 sysfs_root_kn 是在什么时候创建的呢?我们找一下这个函数:__init sysfs_init()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}

2.8 总结

通过上述对 API 函数的分析,我们知道创建目录的规律:

(1)无父目录、无 kset,则将在 sysfs 的根目录(即/sys/)下创建目录。

(2)无父目录、有 kset,则将在 kset 下创建目录,并将 kobj 加入 kset.list。

(3)有父目录、无 kset,则将在 parent 下创建目录。

(4)有父目录、有 kset,则将在 parent 下创建目录,并将 kobj 加入 kset.list。