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 负责"属性导出"。

相关推荐
开压路机1 天前
进程控制
linux·服务器
香蕉鼠片1 天前
跨平台开发到底是什么
linux·windows·macos
bukeyiwanshui1 天前
20260417 DNS实验
linux
代码中介商1 天前
Linux 帮助手册与用户管理完全指南
linux·运维·服务器
weixin_449173651 天前
Linux -- 项目中查找日志的常用Linux命令
linux·运维·服务器
想唱rap1 天前
C++智能指针
linux·jvm·数据结构·c++·mysql·ubuntu·bash
Strugglingler1 天前
基于whiptail开发shell导航工具
linux·shell·ui设计·whiptail
艾醒(AiXing-w)1 天前
Linux系统管理(二十)——Linux root磁盘不足?一站式应急清理方案(亲测可用)
linux·运维·服务器
小义_1 天前
【Kubernetes】(五) pod2
linux·云原生·容器·kubernetes
哇哦9821 天前
渗透安全(渗透防御)②
linux·安全·渗透防御