Linux驱动中为什么既有 sysfs,又有字符设备?以 DHT11 驱动为例彻底讲透

下面我直接给你一篇CSDN 博文风格的完整内容,你可以直接发,或者稍微改改标题、前言再发。


Linux驱动中为什么既有 sysfs,又有字符设备?以 DHT11 驱动为例彻底讲透

前言

最近在做 Linux 驱动项目时,遇到了一个很典型、也很容易让初学者困惑的问题:

驱动已经支持字符设备 /dev/dht11 访问了,为什么还要再提供一套 sysfs /sys/class/dht11 接口?

刚开始我也会觉得:

  • 字符设备不是已经能访问驱动了吗?

  • 既然 open/read/write 都能用了,为什么还要多此一举搞 sysfs?

  • 这两种方式是不是重复了?

  • 它们的代码是不是写在一个驱动文件里?

把这些问题梳理清楚之后,我发现这其实是 Linux 驱动开发中一个非常重要的知识点。

这篇文章我就结合 DHT11 驱动 ,把 sysfs 与字符设备的区别、联系、适用场景、代码组织方式 一次讲透。


一、问题背景

在 Linux 驱动里,用户态访问驱动常见有两种方式:

1. 字符设备方式

例如:

复制代码
/dev/dht11

用户程序通过标准文件接口来访问设备:

  • open()

  • read()

  • write()

  • ioctl()

  • close()

例如:

复制代码
int fd = open("/dev/dht11", O_RDONLY);
read(fd, buf, sizeof(buf));
close(fd);

这是一种正式的设备访问方式


2. sysfs 方式

例如:

复制代码
/sys/class/dht11/dht11/temp
/sys/class/dht11/dht11/humidity

用户态通过读取或写入属性文件来访问驱动:

复制代码
cat /sys/class/dht11/dht11/temp
cat /sys/class/dht11/dht11/humidity
echo 1 > /sys/class/dht11/dht11/enable

这是一种设备属性导出方式


二、既然字符设备已经能访问驱动,为什么还要有 sysfs?

这是最核心的问题。

答案是:

字符设备和 sysfs 解决的不是同一类问题。

它们虽然都能让用户态"碰到驱动",但定位完全不同:

  • 字符设备偏操作型接口

  • sysfs 偏属性型接口


1. 字符设备:操作型接口

字符设备更强调:

  • 对设备进行读写

  • 执行控制命令

  • 传输数据

  • 提供完整交互能力

适合的场景有:

  • 连续读取传感器数据

  • 返回结构体数据

  • 实现 ioctl 控制命令

  • 阻塞/非阻塞读

  • poll/select/epoll

  • 二进制数据传输

所以字符设备更像是:

"设备功能接口"


2. sysfs:属性型接口

sysfs 更强调:

  • 暴露设备状态

  • 暴露设备参数

  • 暴露简单控制项

  • 方便调试

  • 方便脚本调用

适合的场景有:

  • 查看温度

  • 查看湿度

  • 查看设备状态

  • 查看驱动版本

  • 设置开关量

  • 设置简单阈值参数

所以 sysfs 更像是:

"设备属性接口"


三、一个很形象的类比

可以把驱动想象成一台机器。

1. /dev/dht11 字符设备

像这台机器的正式操作面板

你可以通过它:

  • 启动功能

  • 获取数据

  • 发控制命令

  • 做复杂交互


2. /sys/class/dht11/... sysfs

像这台机器外面的状态铭牌 + 简单按钮

你可以通过它:

  • 看当前温度

  • 看当前湿度

  • 看设备是否启用

  • 改一个简单配置项


所以二者并不是重复,而是:

一个偏"功能访问",一个偏"属性展示"。


四、DHT11 这种设备为什么特别适合同时支持两种方式?

DHT11 本质上是一个非常典型的简单传感器设备,它最核心的信息就两个:

  • 温度

  • 湿度

这种设备非常适合通过 sysfs 把属性直接导出来:

复制代码
/sys/class/dht11/dht11/temp
/sys/class/dht11/dht11/humidity

这样用 cat 一下就能看到,非常直观,调试也方便。

但与此同时,如果上层应用程序需要正式访问,比如 LVGL 界面程序、上位机程序或者自定义应用程序,就更适合通过字符设备统一读取:

复制代码
/dev/dht11

因此对于 DHT11 来说:

  • sysfs 很适合调试、演示、脚本

  • 字符设备很适合应用层正式调用

这两者并存,反而是一个很合理的设计。


五、sysfs 和字符设备各自适合什么场景?


1. 更适合用字符设备的场景

(1)一次读取完整结构化数据

比如驱动里定义:

复制代码
struct dht11_data {
    int temp;
    int humidity;
};

那么用户态直接 read() 一次把整个结构体读出来,更自然。


(2)需要复杂控制命令

比如:

  • 设置采样周期

  • 清除错误状态

  • 切换模式

  • 校验设备状态

这种场景一般更适合 ioctl()


(3)需要阻塞/非阻塞机制

如果设备要支持:

  • 数据未就绪时阻塞

  • poll/select/epoll

  • 异步通知

这些都属于字符设备擅长的事情。


(4)高频读取

如果应用程序需要高频访问设备,字符设备会更正规,也更容易扩展。


2. 更适合用 sysfs 的场景

(1)查看简单属性

例如:

复制代码
cat /sys/class/dht11/dht11/temp
cat /sys/class/dht11/dht11/humidity

(2)驱动调试

开发阶段直接:

复制代码
watch -n 1 cat /sys/class/dht11/dht11/temp

就能实时看到数据变化。


(3)Shell 脚本调用

例如:

复制代码
temp=$(cat /sys/class/dht11/dht11/temp)
if [ "$temp" -gt 30 ]; then
    echo "warning: too hot"
fi

(4)导出简单控制参数

例如:

复制代码
echo 1 > /sys/class/dht11/dht11/enable
echo 2000 > /sys/class/dht11/dht11/period_ms

六、为什么 sysfs 不能完全替代字符设备?

很多人会想:

既然 sysfs 这么方便,能不能只用 sysfs,不做字符设备?

答案是:

不能完全替代。

因为 sysfs 有很明确的设计边界。

sysfs 的理念是:

一个文件对应一个属性,内容尽量简单。

所以 sysfs 不适合:

  • 大块数据传输

  • 流式数据

  • 二进制协议

  • 复杂命令交互

  • 高性能频繁读写

因此 sysfs 适合"轻量、简单、属性化"的东西,不适合承担所有设备通信功能。


七、为什么字符设备也不能完全替代 sysfs?

从"能不能做"的角度说,字符设备当然可以做很多事。

比如你完全可以设计 read()ioctl() 来完成:

  • 读温度

  • 读湿度

  • 设置开关

  • 设置参数

但问题在于这样工程上不够优雅

如果所有简单属性都必须通过字符设备访问,就会出现这些问题:

  • 看一个状态还得专门写程序

  • 改一个参数要写 ioctl

  • shell 调试不方便

  • 演示不直观

  • 不符合 Linux 设备模型中"属性导出"的习惯

所以字符设备虽然"能做",但并不适合替代 sysfs 做所有事情。


八、sysfs 与字符设备的本质区别总结

一句话总结:

字符设备主要用于"操作设备",sysfs 主要用于"查看或设置设备属性"。

再展开一下:

对比项 字符设备 /dev/dht11 sysfs /sys/class/dht11
本质 操作型接口 属性型接口
常见调用方式 open/read/write/ioctl cat/echo
数据形式 文本或二进制都可以 一般是文本
适合复杂控制
适合状态查看 可以但不优雅 非常适合
适合脚本调用 一般 很适合
适合高频/流式数据 更适合 不适合
调试友好性 一般 很强

九、sysfs 与字符设备的代码是不是写在同一个驱动文件里?

这个问题也很关键。

答案是:

通常是写在同一个驱动程序文件里,尤其是小项目。

比如一个 DHT11 驱动,完全可以在同一个 dht11_drv.c 里同时实现:

  • 字符设备接口

  • sysfs 属性接口


1. 为什么可以写在一个文件里?

因为它们本质上是:

同一个驱动的两套用户态访问接口

底层访问硬件的逻辑是共用的。

例如驱动里可能有一个统一的数据读取函数:

复制代码
static int dht11_read_data(struct dht11_dev *dev)
{
    // 读取DHT11时序,解析温湿度
}

那么:

  • 字符设备的 read() 可以调用它

  • sysfs 的 show() 也可以调用它

所以完全可以写在同一个 .c 文件里。


2. 一个典型驱动文件结构

一个小型 DHT11 驱动通常可以这么组织:

复制代码
// 1. 头文件、宏定义、结构体定义

// 2. 底层硬件访问函数
static int dht11_read_data(...);

// 3. 字符设备接口
static int dht11_open(...);
static ssize_t dht11_read(...);
static long dht11_ioctl(...);

static const struct file_operations dht11_fops = {
    .open = dht11_open,
    .read = dht11_read,
    .unlocked_ioctl = dht11_ioctl,
};

// 4. sysfs 属性接口
static ssize_t temp_show(...);
static ssize_t humidity_show(...);
static DEVICE_ATTR_RO(temp);
static DEVICE_ATTR_RO(humidity);

// 5. 驱动初始化函数
static int __init dht11_init(void)
{
    // 注册字符设备
    // 创建 class
    // 创建设备节点
    // 创建 sysfs 属性
}

// 6. 驱动退出函数
static void __exit dht11_exit(void)
{
    // 删除 sysfs 属性
    // 删除 device/class
    // 删除 cdev
}

这就是最常见的写法。


十、那什么时候会拆成多个文件?

如果项目比较大,也可以拆成多个文件,例如:

复制代码
dht11_core.c      // 底层硬件逻辑
dht11_chrdev.c    // 字符设备接口
dht11_sysfs.c     // sysfs接口
dht11.h           // 公共头文件

这种拆分方式的优点是:

  • 结构更清晰

  • 易维护

  • 易扩展

  • 适合大型项目

但对于 DHT11 这种小型驱动来说,放在一个文件里通常更直接。

所以更准确地说:

小项目一般写在同一个 .c 文件里,大项目可以拆分;但本质上它们都属于同一个驱动。


十一、它们是不是两个驱动?

不是。

这一点特别容易混淆。

很多初学者会下意识觉得:

  • /dev/dht11 是一套驱动

  • /sys/class/dht11 又是一套驱动

实际上并不是。

正确理解应该是:

同一个驱动,同时向用户态暴露了两种不同的访问入口。

可以理解成:

  • /dev/dht11 是"正式入口"

  • /sys/class/dht11/... 是"属性入口"

但它们底层访问的还是同一个设备、同一套驱动逻辑。


十二、代码层面它们如何关联?

通常驱动里会定义一个设备结构体,例如:

复制代码
struct dht11_dev {
    dev_t devno;
    struct cdev cdev;
    struct class *class;
    struct device *device;

    int temp;
    int humidity;
    int gpio;
};

这里面既有:

  • 字符设备相关成员:devnocdev

  • sysfs 相关成员:classdevice

  • 设备状态成员:temphumiditygpio

这就说明:

sysfs 和字符设备是围绕同一个设备对象组织起来的。


十三、初始化时两者通常是一起创建的

在驱动初始化函数中,常见流程如下:

1. 注册字符设备

复制代码
alloc_chrdev_region(&devno, 0, 1, "dht11");
cdev_init(&dht11.cdev, &dht11_fops);
cdev_add(&dht11.cdev, devno, 1);

2. 创建设备类与设备节点

复制代码
dht11.class = class_create(THIS_MODULE, "dht11");
dht11.device = device_create(dht11.class, NULL, devno, NULL, "dht11");

这一步通常既会在 sysfs 下生成设备目录,也方便 udev/mdev 创建设备节点 /dev/dht11


3. 创建 sysfs 属性文件

复制代码
device_create_file(dht11.device, &dev_attr_temp);
device_create_file(dht11.device, &dev_attr_humidity);

于是用户态就能访问:

复制代码
/sys/class/dht11/dht11/temp
/sys/class/dht11/dht11/humidity

可以看到,字符设备和 sysfs 在代码层面本来就是紧密关联的。


十四、从驱动分层角度理解更清楚

一个完整驱动其实可以拆成三层来理解。


1. 底层硬件层

负责:

  • GPIO 初始化

  • 拉高拉低引脚

  • 精确时序采样

  • 校验 DHT11 数据

这是最底层。


2. 驱动核心层

负责:

  • 保存温湿度数据

  • 管理设备状态

  • 提供统一读取函数

例如:

复制代码
static int dht11_read_data(struct dht11_dev *dev);

3. 用户接口层

向用户态提供访问入口:

接口1:字符设备

复制代码
/dev/dht11

接口2:sysfs

复制代码
/sys/class/dht11/dht11/temp
/sys/class/dht11/dht11/humidity

所以本质上是:

同一个核心驱动逻辑,对外提供两套不同风格的接口。


十五、面试中怎么回答这个问题?

如果面试官问:

既然有字符设备,为什么还需要 sysfs?

可以这样回答:

Linux 驱动里字符设备和 sysfs 面向的场景不同。字符设备主要提供 read/write/ioctl 这类操作型接口,适合正式应用程序访问和复杂控制;而 sysfs 主要提供属性型接口,适合导出设备状态、参数和简单控制项,方便调试和脚本操作。对于 DHT11 这种简单传感器,字符设备适合程序化读取完整数据,sysfs 适合直接查看温湿度等属性,因此两者并存是很常见也很合理的设计。

如果面试官继续问:

sysfs 和字符设备的代码是不是写在一起?

可以答:

通常属于同一个驱动,小项目里经常写在同一个 .c 文件中,共用同一个设备结构体和底层硬件访问逻辑;大型项目中也可以拆成多个源文件,但本质上仍然是同一个驱动的两套用户态接口。


十六、我对这个问题的最终理解

在真正理解这块之前,我会觉得:

能访问驱动就行,为什么还要分 sysfs 和字符设备?

现在我更倾向于这样理解:

  • 字符设备解决"怎么操作设备"

  • sysfs 解决"怎么查看/设置设备属性"

二者不是重复,而是分工不同。

对一个驱动来说:

  • 用字符设备可以提供正式、完整、可扩展的访问能力

  • 用 sysfs 可以提供轻量、直观、便于调试的属性访问能力

尤其像 DHT11 这种简单传感器设备,两种方式一起提供,反而是更合理的工程设计。


十七、总结

最后做一个简洁总结。

1. 为什么字符设备之外还要 sysfs?

因为:

  • 字符设备偏操作型接口

  • sysfs 偏属性型接口

二者功能定位不同。


2. 为什么 DHT11 适合两者并存?

因为 DHT11 同时具备:

  • 程序化读取数据的需求

  • 直接查看温湿度属性的需求


3. sysfs 与字符设备是不是写在同一个驱动里?

是的,通常属于同一个驱动。

  • 小项目:常写在同一个 .c 文件

  • 大项目:可拆成多个 .c 文件


4. 最值得记住的一句话

sysfs 和字符设备不是两个驱动,而是同一个驱动向用户态提供的两种不同访问方式。


5. 再用一句话彻底记住

字符设备负责"设备操作",sysfs 负责"属性导出"。

相关推荐
xlp666hub2 小时前
深度剖析Linux Input子系统(2):驱动开发流程与现代 Multi-touch 协议
linux
zzzsde3 小时前
【Linux】Ext文件系统(1)
linux·运维·服务器
xlq223223 小时前
34 信号
linux
木下~learning4 小时前
对于Linux中等待队列和工作队列的讲解和使用|RK3399
linux·c语言·网络·模块化编程·工作队列·等待队列
齐齐大魔王4 小时前
linux-核心工具
linux·运维·服务器
醇氧4 小时前
Linux 系统的启动过程
linux·运维·服务器
IMPYLH4 小时前
Linux 的 dircolors 命令
linux·运维·服务器·数据库
齐齐大魔王4 小时前
linux-基础操作
linux·运维·服务器
bwz999@88.com4 小时前
ubuntu24.04更换国内源
linux·运维·服务器