Linux系统的设备类(/sys/class)原理与实践

类是C++和面向对象设计中的关键概念,是实现抽象的一种基本方法。Linux内核的大多数代码都是用C语言编写的,但这并不妨碍使用面向对象技术。或者说,使用C语言也可以做“面向对象”编程。事实上,主要使用C语言的Linux内核中有很多精彩的面向对象设计,设备类便是其中之一。

简单说,设备类就是对硬件设备进行抽象和归类,实现代码复用。在接口层面,sysfs中的class子目录便是设备类的大本营。class这个名字也是光明正大地竖起了一杆面向对象的大旗。

进一步说,在 Linux 系统中,/sys/class是 sysfs虚文件系统的核心目录之一。虚者,假也,虚文件系统是Linux内核的一大特色,以文件系统之名行“两大空间接口”之实。sysfs是Linux内核中众多虚文件系统中的一种,sys代表系统,用于暴露系统设备的状态与配置信息。而/sys/class则是sysfs中的关键部分,它的核心作用是按 “功能” 对硬件设备进行分类。分类的目的是让用户和应用程序能根据使用需要直观地找到想要用的设备。因为此,它的组织方式不是按硬件总线或内核驱动的底层逻辑分类,而是按使用者角度的用途对设备分类。

从实现角度来说,/sys/class下的内容是对 /sys/devices中的设备节点按照功能进行聚合,/sys/class的每个目录代表一类设备,目录中的文件其实是/sys/devices下各个设备对象的“符号链接“。

一、核心设计理念:按 “功能” 分类


/sys/class 以 “设备功能” 为标准来对设备进行分类,比如块设备(block)、电源(power_supply)、网卡(net)等。以幽兰代码本为例,它的class目录下有下面这些设备类:

android_usb  block          dma             gpio         i2c-dev    mem        nvme            pps        rkwifi       spi_host       tpm        ubi                 video4linuxata_device   bsg            dma_heap        graphics     ieee80211  misc       nvme-generic    ptp        rtc          spi_master     tpmrm      udc                 vtconsoleata_link     devcoredump    drm             hdmirx       input      mmc_host   nvme-subsystem  pwm        scsi_device  spi_slave      tty        usbmisc             wakeupata_port     devfreq        drm_dp_aux_dev  hidraw       iommu      mpp_class  pci_bus         regulator  scsi_host    spi_transport  typec      usb_power_delivery  watchdogbacklight    devfreq-event  dvb             hwmon        leds       mtd        phy             retimer    sound        tee            typec_mux  usb_rolebdi          devlink        extcon          i2c-adapter  mdio_bus   net        power_supply    rfkill     spidev       thermal        u_audio    vc

设备类(/sys/class)把硬件设备按功能类型组织成目录,让用户和应用程序能避开底层驱动的复杂逻辑,直接通过功能维度快速找到并管理目标设备。比如,我们在Linux中,如果要看系统里有哪些块设备,那么只要lsblk就可以列出所有的块设备。

geduer@ulan:~$ lsblk\NAME         MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTSsda            8:0    1  14.6G  0 disk└─sda1         8:1    1  14.6G  0 part /media/geduer/ULANmtdblock0     31:0    0     8M  0 diskmmcblk0      179:0    0 115.2G  0 disk├─mmcblk0p1  179:1    0     8M  0 part├─mmcblk0p2  179:2    0   512M  0 part└─mmcblk0p3  179:3    0 114.7G  0 partmmcblk0boot0 179:32   0     4M  1 diskmmcblk0boot1 179:64   0     4M  1 disknvme0n1      259:0    0 476.9G  0 disk├─nvme0n1p1  259:1    0   953M  0 part /boot├─nvme0n1p2  259:2    0 452.2G  0 part /├─nvme0n1p3  259:3    0   9.5G  0 part└─nvme0n1p4  259:4    0  14.3G  0 part

而lsblk工作的底层基础,便是内核中的设备类机制。观察/sys/class/block目录,可以看到一张类似的列表:

geduer@ulan:/sys/class/block$ lltotal 0drwxr-xr-x  2 root root 0 Jan  1  2021 ./drwxr-xr-x 80 root root 0 Oct 29 11:32 ../lrwxrwxrwx  1 root root 0 Jan  1  2021 loop0 -> ../../devices/virtual/block/loop0/lrwxrwxrwx  1 root root 0 Jan  1  2021 loop1 -> ../../devices/virtual/block/loop1/lrwxrwxrwx  1 root root 0 Jan  1  2021 loop2 -> ../../devices/virtual/block/loop2/lrwxrwxrwx  1 root root 0 Jan  1  2021 loop3 -> ../../devices/virtual/block/loop3/lrwxrwxrwx  1 root root 0 Jan  1  2021 loop4 -> ../../devices/virtual/block/loop4/lrwxrwxrwx  1 root root 0 Jan  1  2021 loop5 -> ../../devices/virtual/block/loop5/lrwxrwxrwx  1 root root 0 Jan  1  2021 loop6 -> ../../devices/virtual/block/loop6/lrwxrwxrwx  1 root root 0 Jan  1  2021 loop7 -> ../../devices/virtual/block/loop7/lrwxrwxrwx  1 root root 0 Jan  1  2021 mmcblk0 -> ../../devices/platform/fe2e0000.mmc/mmc_host/mmc0/mmc0:0001/block/mmcblk0/lrwxrwxrwx  1 root root 0 Jan  1  2021 mmcblk0boot0 -> ../../devices/platform/fe2e0000.mmc/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0boot0/lrwxrwxrwx  1 root root 0 Jan  1  2021 mmcblk0boot1 -> ../../devices/platform/fe2e0000.mmc/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0boot1/lrwxrwxrwx  1 root root 0 Jan  1  2021 mmcblk0p1 -> ../../devices/platform/fe2e0000.mmc/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p1/lrwxrwxrwx  1 root root 0 Jan  1  2021 mmcblk0p2 -> ../../devices/platform/fe2e0000.mmc/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p2/lrwxrwxrwx  1 root root 0 Jan  1  2021 mmcblk0p3 -> ../../devices/platform/fe2e0000.mmc/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p3/lrwxrwxrwx  1 root root 0 Jan  1  2021 mtdblock0 -> ../../devices/platform/fe2b0000.spi/spi_master/spi5/spi5.0/mtd/mtd0/mtdblock0/lrwxrwxrwx  1 root root 0 Jan  1  2021 nvme0n1 -> ../../devices/platform/fe150000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/nvme/nvme0/nvme0n1/lrwxrwxrwx  1 root root 0 Jan  1  2021 nvme0n1p1 -> ../../devices/platform/fe150000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/nvme/nvme0/nvme0n1/nvme0n1p1/lrwxrwxrwx  1 root root 0 Jan  1  2021 nvme0n1p2 -> ../../devices/platform/fe150000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/nvme/nvme0/nvme0n1/nvme0n1p2/lrwxrwxrwx  1 root root 0 Jan  1  2021 nvme0n1p3 -> ../../devices/platform/fe150000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/nvme/nvme0/nvme0n1/nvme0n1p3/lrwxrwxrwx  1 root root 0 Jan  1  2021 nvme0n1p4 -> ../../devices/platform/fe150000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/nvme/nvme0/nvme0n1/nvme0n1p4/lrwxrwxrwx  1 root root 0 Jan  1  2021 ram0 -> ../../devices/virtual/block/ram0/lrwxrwxrwx  1 root root 0 Oct 29 11:33 sda -> ../../devices/platform/usbdrd3_1/fc400000.usb/xhci-hcd.6.auto/usb2/2-1/2-1:1.0/host0/target0:0:0/0:0:0:0/block/sda/lrwxrwxrwx  1 root root 0 Oct 29 11:33 sda1 -> ../../devices/platform/usbdrd3_1/fc400000.usb/xhci-hcd.6.auto/usb2/2-1/2-1:1.0/host0/target0:0:0/0:0:0:0/block/sda/sda1/

Linux 硬件管理的底层入口是 /sys/devices,但它按设备在硬件总线(如 PCIe、USB、I2C)上的物理连接关系组织,结构比较复杂。以幽兰的电池设备为例,它在/sys/devices下的路径为:

/sys/devices/platform/feaa0000.i2c/i2c-2/2-0063/power_supply/cw2017-battery

藏得很深,不熟悉的人很难找到。但在 /sys/class中,只要进到power_supply目录,就可以看到它了。

geduer@ulan:/sys/class/power_supply$ lsbq25700-charger  cw2017-battery  dc  tcpm-source-psy-2-0022

概而言之,/sys/class 忽略物理连接,只按设备的 “功能用途” 分组,每个功能目录下的文件以链接形式直接指向 /sys/devices 中对应的真实设备节点,这种方式极大降低了设备访问的复杂度。
在Linux内核启动时,通过kset_create_and_add创建/sys/class根目录,并把结果保存在全局变量class_kset中,而class_kset中的每个kobject对应一个类目录(如/sys/class/net)。
对kset不熟悉的朋友可以看我的另外一篇文章https://mp.weixin.qq.com/s/Q63nHsPuMpcta2WKrdDXWw

二、用户视角的硬件接口

Linux 硬件管理的底层逻辑很复杂,涉及到各种总线、控制器、端口驱动等,而 /sys/class 相当于给用户 / 程序提供了一个 “去底层化” 的操作入口 。

  • • ”用户“无需了解设备在物理总线上的连接关系(如 “这个 USB 设备是插在哪个 PCIe 控制器上的”),只需通过功能分类(如 “网络设备”“存储设备”)即可快速定位目标。

  • • 所有操作通过文件读写完成(cat 查看状态、echo 配置参数),无需编写复杂驱动或调用底层接口,极大降低了硬件管理的技术门槛。

举例来说,可以通过/sys/class文件接口来查看幽兰无线网卡的地址,只要下面几条命令:

cd /sys/class/net/wlan0geduer@ulan:/sys/class/net/wlan0$ cat address20:50:e7:61:53:2c
再举一个例子,也可以通过命令来观察设备的驱动程序。先cd device 进入 device 目录,这其实是进入到了/sys/devices/ 中的实际设备节点,这是其物理拓扑路径的入口。
device 目录下的 driver 子目录是与当前网卡绑定的驱动程序相关信息的入口。通过该目录下的 bind 和 unbind 接口,可以手动解除或重新建立设备与驱动的绑定关系。
geduer@ulan:/sys/class/net/wlan0/device/driver$ lsbind  mmc2:0001:1  mmc2:0001:2  uevent  unbind

若需临时调试或更换驱动,可通过unbind文件临时解除现有设备的绑定。
例如,我可以使用如下命令解除mmc2:0001:1 的设备与当前无线网卡驱动的绑定。

sudo sh -c 'echo "mmc2:0001:1" > /sys/class/net/wlan0/device/driver/unbind'

类似的,可以使用下面的命令重新将mmc2:0001:1设备和无线网卡驱动绑定。
sudo sh -c 'echo "mmc2:0001:1" > /sys/class/net/wlan0/device/driver/bind'

可见,借助 /sys/class,用户仅通过cd、cat、echo等基础命令即可管理硬件设备,大幅简化了硬件管理的流程,提高工作效率。

三、“分层抽象” 的思想

分层抽象的核心是 “分而治之”:通过划分层级明确职责,通过接口隔离屏蔽细节,最终让复杂系统变得有序、可复用、可扩展。Linux内核的设备模型正是这一思想的经典实践,也是Linux内核能够兼容千差万别硬件的关键原因。

可以把Linux 内核的设备模型归纳为下面四个层次:

硬件层→驱动层→类抽象层→用户空间层

分层抽象的四层(块设备场景)

层级

核心角色

与其他层的关系

硬件层

物理设备(如 USB 芯片、SATA 控制器)

驱动层直接操作的对象,提供硬件接口(寄存器、协议)

驱动层

硬件适配 + 内核通用接口实现

向下对接硬件(处理 USB/NVMe 协议),向上提供内核统一接口(block_device_operations)

类抽象层(block_class)

统一抽象 + 屏蔽差异

向下依赖驱动层的通用接口,向上提供用户空间统一接口(/dev、uevent)

用户空间层

设备使用(如 mount、fdisk)

只依赖类层的抽象接口,完全不接触驱动和硬件

驱动层和类层的分工,完美体现了 “分层抽象” 的核心思想,驱动层处理“硬件特有”的逻辑,类层处理“同类通用”的逻辑。

驱动层:做 “差异化适配”,不关心抽象

驱动的核心任务是 “让硬件能被内核识别”,但不关心 “如何向用户暴露设备”:

  • • 例如 usb 存储驱动:只处理 usb 协议(Bulk 传输、URB 管理),将内核的 bio 请求转换为 usb 命令;

  • • 又如 nvme 驱动:只处理 nvme 协议(队列命令、DMA 传输),将 bio 请求转换为 nvme 命令;

类层:做 “通用化抽象”,不关心硬件

类层的核心任务是 “定义内核空间对上的标准化接口,为用户空间提供标准化的服务”。

  • • 无论驱动是 usb还是 nvme,类层都统一生成 /dev/sdX 或 /dev/nvmeXn1 设备节点;

  • • 统一暴露 sysfs 属性(size、ro),统一生成 uevent 事件;

  • • 类层的 “输入” 是与驱动层之间的标准化接口,“输出” 是与用户空间的标准化接口 —— 这就屏蔽了驱动层的硬件差异。

下面以优盘设备为例,理解分层抽象在设备注册中的具体实现,其关键流程如下:
USB 设备插入
   ↓
USB 总线探测到设备 → 创建 struct usb_interface(设备)
   ↓
usb-storage 驱动匹配成功 → .probe() 被调用
   ↓
usb-storage 初始化 SCSI 主机(scsi_host)
   ↓
SCSI 子系统扫描到 LUN → 创建 struct scsi_device(设备)
   ├───────────────────────────────────────┐
   ↓                                       ↓
sd_mod 驱动匹配 scsi_device           sg 驱动通过 class_interface 感知 scsi_device
   ↓                                       ↓
sd_mod 的 .probe() 被调用             sg_add_device() 被回调
   ↓                                       ↓
sd_mod 创建 struct gendisk            sg 分配 Sg_device + cdev
   ↓                                       ↓
sd_mod 调用 add_disk(gendisk)         sg 调用 device_create(sg_sysfs_class, ...)
        ↓                                       ↓
        内部:分配 struct device                内部:分配 struct device
              dev->class = &block_class               dev->class = sg_sysfs_class  ← 🎯
              device_add(dev)                         device_add(dev)
                     ↓                                       ↓
                     /sys/class/block/sda                   /sys/class/scsi_generic/sg0
                     udev → /dev/sda                        udev → /dev/sg0

在 Linux 内核的 SCSI 子系统中,设备抽象被清晰地划分为总线层与类层两个层次,分别服务于内核内部管理和用户空间访问:

  • • scsi_device是总线层设备, SCSI 总线子系统内部的设备抽象。

  • • sg_add_device() 创建的 struct device是类层设备为面向用户空间的设备接口。

有了这样的分层之后,一个硬件设备就会有多个设备对象,是不同层为其创建的。还是以优盘为例。在drivers\scsi\sg.c源码中定义了类接口(class interface)结构体,用于监听本类设备的添加/移除事件。

在init_sg()中将 sg 驱动注册为 SCSI 设备的观察者。SCSI 子系统在每次创建 struct scsi_device 时,会通知所有注册的 class_interface;因此,每当一个SCSI设备出现,sg_add_device() 就会被调用,相应的每当一个SCSI设备销毁sg_remove_device()会被调用。

在sg_add_device()中,会调用device_create创建一个类层设备对象,并生成用户空间可见的设备文件。

在device_create中,它会调用device_create_groups_vargs创建设备实例,通过参数指定了所属类。 device_create_groups_vargs内部的关键操作有:
  • • 调用device_initialize()初始化设备。

  • • 调用device_add()将已初始化的设备正式注册到 Linux 设备模型中。

    使用挥码枪和NanoCode在device_create设置断点,然后插入一个优盘,断点就会命中。

在设备类层面,divice_type是一个关键结构体,它是一类“设备的行为模板”,其定义如下:
dt lk!device_type   +0x000 name             : Ptr64 Char   +0x008 groups           : Ptr64 Ptr64 attribute_group   +0x010 uevent           : Ptr64       +0x018 devnode          : Ptr64       +0x020 release          : Ptr64       +0x028 pm               : Ptr64 dev_pm_ops
以磁盘类为例,它定义的device_type实例名叫disk_type:
const struct device_type disk_type = {
    .name     = "disk",               // 类型名,用于 uevent 中的 DEVTYPE=disk
    .groups   = disk_attr_groups,     // 所有磁盘共享的 sysfs 属性(size, ro, ...)
    .release  = disk_release,         // 统一的释放逻辑
    .devnode  = block_devnode,        //统一的设备节点命名分发器
};

它将“磁盘”这一设备子类的所有公共行为打包成一个模板;任何使用 disk_type 的设备通过 dev->type = &disk_type自动继承这些行为;无需每个驱动重复实现 size 属性、release 回调、命名逻辑等。

有了这个设计后,驱动只需声明设备的属性,类框架自动执行全套标准化操作一个硬件设备。如果要复用设备类的代码,它只需:device_initialize() + device_add()。

struct device *dev = &my_dev->dev;
device_initialize(dev);          // 初始化通用字段
dev->class = &block_class;       // 声明所属类
dev->type  = &disk_type;         // 声明设备类型
dev_set_name(dev, "mydisk");
device_add(dev);                 // ← 激活所有复用策略!

device_add() 自动完成:

  1. 1. 创建 sysfs 设备目录(/sys/devices/.../mydisk)

  2. 2. 挂载 device_type 定义的属性组(如 size, ro)

  3. 3. 挂载 class 定义的属性组(如有)
    4 .建立 /sys/class/ 功能视图符号链接

  4. 5. 发送标准化 uevent(含 SUBSYSTEM、DEVTYPE 等)

  5. 6. 触发 devtmpfs 创建 /dev/mydisk 节点(通过 device_type->devnode)

以上就是 Linux 设备模型“声明式设计 + 策略复用”的实现过程。

四、符号链接原理

前面提到/sys/class每个子目录下的条目指向 /sys/devices/...中真实设备节点的符号链接。

那么这个符号链接具体是怎么链接到/sys/devices/中的真实设备节点的?带着问题,我们一探究竟。
在device_add()函数中,它调用device_add_class_symlinks()来创建/sys/class的软链接。

具体是在dev->class->p->subsys.kobj下创建和dev->kobj联系的节点。

创建符号链接其实就是在/sys/class/类目录下创建一个子目录,然后将这个目录链接到/sys/device/...(设备目录)。
下面明确这函数中这两个参数kobject对应的目录:
  • • dev->kobject对应/sys/device/...(设备目录)

  • • dev->class->p->subsys.kobj对应/sys/class/类目录。
    我用一张图表示它们的关系:


    kernfs_node是文件的具体节点,简单来说,就是创建一个kernfs_node,在这个kernfs_node存放一个指针,指向需要链接文件的kernfs_node。后续对这个符号链接执行相关操作的时候,通过这个指针能找到对应文件的kernfs_node然后获取device真正执行。

众里寻他千百度

在做扩展命令sysclass的时候遇到一个问题,如图我需要通过kernfs_node得到 kobject ,但是我在kernfs_node找不到和kobject有关的字段。

下面是kernfs_node的定义,没有发现和kobject相关的字段,这个问题困扰我许久。

后来,我将注意力转向这个看起来很奇怪的指针,它会指向哪里?

我突然想到我之前在做sysdevice时似乎看见过相关的代码,然后我又去看曾经做的总结,寻找蛛丝马迹。 果然,总结里提到了 kernfs目录节点和kobject的绑定。

这里传入的参数为kobject,指定kernfs_node->priv 为kobject。

众里寻他千百度。蓦然回首,那人却在,灯火阑珊处。 我一直找不到的答案,原来就藏在那个看似 “奇怪” 却早有伏笔的 priv 指针里。就像诗句中在人潮中执着寻找 “那人”,满脑子都是 “他该在灯火最亮的地方”,却没料到答案偏偏藏在看似清冷的 “阑珊处”。

五、NanoCode实践

理解了sys/class原理后,我尝试在 NanoCode调试器中编写代码,动态遍历 /sys/class/下的所有类目录和设备目录,链接到对应的device并打印地址。完整信息较长,摘录部分如下:

该命令树形显示了/sys/class 目录下所有已注册设备类(class)以及对应的设备、device地址。
  • • 根节点为类名(如 input)

  • • 子节点为设备名(如 _input0)

  • • 子节点后的地址为device的内存地址
    完整的执行结果已整理并上传至网盘,欢迎下载查阅:
    百度网盘链接:

https://pan.baidu.com/s/1IOR7D-Ztf8-6SJXk1RAKYA?pwd=ch5n

NanoCode的新版本,会包含我开发的这个新命令,大家可以通过挥码枪连接幽兰代码本或者GDK8,在NanoCode上只要输入! ndx.sysclass便可以一键遍历并可视化整个 /sys/class 的设备拓扑结构。

六、小结

/sys/class 的设计,是 Linux 内核在“复杂”与“简洁”之间架起的一座桥梁。以“功能”为灯,照亮用户通往设备的路径。在 /sys/devices 那片由总线、控制器、地址交织而成的密林中,/sys/class 如同一张精心绘制的地图——不展示每一条小径的曲折,只标注“此处有网卡”“彼处是电池”。这种以用户视角重构内核世界的智慧,正是分层抽象思想的生动体现。最终,让系统得以将复杂交给内核,把简单留给用户。

(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)

*************************************************

正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生

扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物

Image

也欢迎关注格友公众号

Image

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值