字符设备基本概念:从分类到 LED 控制链路
课程位置:驱动初级 → 字符设备 → 基本概念 → 设备驱动的分类、设备驱动框架、一切皆文件、字符设备框架的由来。
主要依据:2026-10-05 11:04《字符设备 基本概念》录音转写;对照
I:\Linux内核驱动\学习系统\04_驱动初级\02_字符设备\01_基本概念下四节老师笔记和截图。转写存在大量同音字和重复句。本文按老师的讲课顺序整理;"课程示意"和"实际 Linux 机制"分别说明。LED 完整链路是原理演示,不是已经在 Jetson 上完成的 GPIO 点灯实验。
一、这节课的主线
老师从"设备太多,需要分类"讲起,再画出应用、内核、硬件之间的层次;用普通文件的 open/read/write/close 引出"一切皆文件";最后用 LED 说明:应用为什么能把 /dev/led 当文件操作,以及驱动为何要有设备号、字符设备对象和文件操作回调。(录音 00:03---44:25)
一句话串起来:**应用写设备文件 → 内核按设备号找到字符设备驱动 → 驱动解释写入的数据 → 驱动操作 GPIO → LED 电平改变。**中间每一步都需要相应的注册、映射或硬件准备,单有文件名不能点亮 LED。
二、设备驱动为什么分三类
老师按主要访问方式介绍字符、块、网络三类设备,并选择字符设备作为驱动入门。(录音 00:03---03:36)
| 类型 | 本节要抓住的特点 | 课堂例子 | 学习时的边界 |
|---|---|---|---|
| 字符设备 | 通常以字节流或顺序数据的方式提供读写等接口。 | 串口、键盘、鼠标;LED 可做成字符设备。 | "字符"不等于每次只能传 1 字节,也不代表驱动绝不使用缓冲区。摄像头的 /dev/video0 虽是字符设备节点,传输仍按帧等方式组织。 |
| 块设备 | 按块访问,支持按位置访问数据,适合存储。 | 硬盘、SD 卡、U 盘。 | 常配合缓存和 I/O 调度;不是所有 Flash 都必然以块设备形式暴露。 |
| 网络设备 | 接入内核网络子系统,收发网络数据包。 | 有线网卡、Wi-Fi。 | 应用通常通过 socket 使用网络;不必要求每张网卡都有可 open 的 /dev/xxx 节点。 |
老师还提到 GPIO、I²C、SPI、Platform 等名称。它们描述总线、控制器或设备组织方式,与上面按用户接口划分的三类不是同一把尺子:例如一个挂在 I²C 上的传感器可以通过字符设备节点向应用提供数据。
三、设备驱动在系统的哪一层
老师用手机"按手电筒按钮"的例子:上层界面提出"灯亮"的需求,内核中的驱动负责把请求变成硬件动作。分层让应用通过受控接口使用设备,也让多个开发者遵守共同的访问方式。(录音 05:24---17:35)
#mermaid-svg-eHwzUmsXgM98D8Ld{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eHwzUmsXgM98D8Ld .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eHwzUmsXgM98D8Ld .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eHwzUmsXgM98D8Ld .error-icon{fill:#552222;}#mermaid-svg-eHwzUmsXgM98D8Ld .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eHwzUmsXgM98D8Ld .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eHwzUmsXgM98D8Ld .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eHwzUmsXgM98D8Ld .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eHwzUmsXgM98D8Ld .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eHwzUmsXgM98D8Ld .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eHwzUmsXgM98D8Ld .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eHwzUmsXgM98D8Ld .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eHwzUmsXgM98D8Ld .marker.cross{stroke:#333333;}#mermaid-svg-eHwzUmsXgM98D8Ld svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eHwzUmsXgM98D8Ld p{margin:0;}#mermaid-svg-eHwzUmsXgM98D8Ld .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eHwzUmsXgM98D8Ld .cluster-label text{fill:#333;}#mermaid-svg-eHwzUmsXgM98D8Ld .cluster-label span{color:#333;}#mermaid-svg-eHwzUmsXgM98D8Ld .cluster-label span p{background-color:transparent;}#mermaid-svg-eHwzUmsXgM98D8Ld .label text,#mermaid-svg-eHwzUmsXgM98D8Ld span{fill:#333;color:#333;}#mermaid-svg-eHwzUmsXgM98D8Ld .node rect,#mermaid-svg-eHwzUmsXgM98D8Ld .node circle,#mermaid-svg-eHwzUmsXgM98D8Ld .node ellipse,#mermaid-svg-eHwzUmsXgM98D8Ld .node polygon,#mermaid-svg-eHwzUmsXgM98D8Ld .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eHwzUmsXgM98D8Ld .rough-node .label text,#mermaid-svg-eHwzUmsXgM98D8Ld .node .label text,#mermaid-svg-eHwzUmsXgM98D8Ld .image-shape .label,#mermaid-svg-eHwzUmsXgM98D8Ld .icon-shape .label{text-anchor:middle;}#mermaid-svg-eHwzUmsXgM98D8Ld .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eHwzUmsXgM98D8Ld .rough-node .label,#mermaid-svg-eHwzUmsXgM98D8Ld .node .label,#mermaid-svg-eHwzUmsXgM98D8Ld .image-shape .label,#mermaid-svg-eHwzUmsXgM98D8Ld .icon-shape .label{text-align:center;}#mermaid-svg-eHwzUmsXgM98D8Ld .node.clickable{cursor:pointer;}#mermaid-svg-eHwzUmsXgM98D8Ld .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eHwzUmsXgM98D8Ld .arrowheadPath{fill:#333333;}#mermaid-svg-eHwzUmsXgM98D8Ld .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eHwzUmsXgM98D8Ld .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eHwzUmsXgM98D8Ld .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eHwzUmsXgM98D8Ld .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eHwzUmsXgM98D8Ld .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eHwzUmsXgM98D8Ld .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eHwzUmsXgM98D8Ld .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eHwzUmsXgM98D8Ld .cluster text{fill:#333;}#mermaid-svg-eHwzUmsXgM98D8Ld .cluster span{color:#333;}#mermaid-svg-eHwzUmsXgM98D8Ld div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eHwzUmsXgM98D8Ld .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eHwzUmsXgM98D8Ld rect.text{fill:none;stroke-width:0;}#mermaid-svg-eHwzUmsXgM98D8Ld .icon-shape,#mermaid-svg-eHwzUmsXgM98D8Ld .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eHwzUmsXgM98D8Ld .icon-shape p,#mermaid-svg-eHwzUmsXgM98D8Ld .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eHwzUmsXgM98D8Ld .icon-shape .label rect,#mermaid-svg-eHwzUmsXgM98D8Ld .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eHwzUmsXgM98D8Ld .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eHwzUmsXgM98D8Ld .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eHwzUmsXgM98D8Ld :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} open/write/close
应用层:按钮或测试程序
系统调用
VFS:通用文件接口
字符设备:按设备号找到 file_operations
LED 驱动回调:解释命令并控制 GPIO
硬件:GPIO 电平改变,LED 亮或灭
- 应用层只能使用内核允许的接口,不能把芯片手册上的物理寄存器地址当成普通指针直接写。
- 系统调用 让
open/write/close从用户态进入内核态;VFS提供通用文件操作入口,并把设备文件操作分发到相应驱动。 - 字符设备驱动 属于内核代码。若驱动直接访问内存映射的硬件寄存器,可能需要
ioremap()建立内核可用的 I/O 映射,再使用合适的 I/O 访问函数;若使用 GPIO 子系统,则通过其接口操作。ioremap()不是"应用地址转内核地址"的步骤,也不是每个 LED 驱动都必须自己调用的函数。Linux 内核 I/O 文档 - U-Boot发生在 Linux 启动之前,是引导程序;老师把它放在图的硬件附近是为了说明它能直接初始化硬件,不能把 U-Boot 当成 Linux 的"硬件层"或设备文件操作的一环。
四、"一切皆文件"到底是什么意思
老师先用普通文件 /home/yhai/log 演示:open() 得到文件描述符 fd,write(fd, buf, len) 写入,read(fd, buf, len) 读出,最后 close(fd)。写完后同一个打开文件的当前偏移量 通常已经在末尾,立即读取可能得到 0 字节;课堂选择关闭后重新打开,使偏移量回到开头。对于支持定位的普通文件,也可使用 lseek(fd, 0, SEEK_SET)。(录音 19:01---25:52)
这个例子的目的是建立统一操作形式。设备文件 /dev/led 也可以用 open/write/close,但内核最终调用的是驱动提供的函数,数据不一定像普通文件那样存到磁盘。因此"一切皆文件"是统一接口的设计思想,不是说所有设备都等同于普通磁盘文件。(录音 25:52---35:27)
| 名称 | 例子 | 作用 | 容易混淆的地方 |
|---|---|---|---|
| 文件路径 | /dev/led |
应用用它找到设备节点。 | 名字可改;仅有这个名字不能找到物理 LED。 |
设备号 dev_t |
课堂示意:主号 500、次号 0 |
设备节点记录的编号,用来关联内核注册的字符设备。 | 主号主要标识分配给驱动的一组设备号,次号常区分实例;500 只是课堂例子,不能随意占用。 |
文件描述符 fd |
open() 返回的非负整数 |
当前进程用它引用一次已打开的文件。 | fd 不是主设备号或次设备号,也不是固定的硬件编号。 |
| 驱动回调 | open、write、release 等 |
内核收到文件操作时调用的实现。 | 设备节点必须能对应到已注册驱动及其回调。 |
课堂示意命令 mknod /dev/led c 500 0 中,c 表示字符设备节点 ,后两个数是主、次设备号。实际使用时,要先确认驱动确实注册了这组设备号、该编号没有冲突;许多系统会由 udev/mdev 创建节点。touch /dev/led 创建的是普通文件,不能替代字符设备节点。mknod 手册
五、把老师的 LED 例子走完整
老师在"设备驱动框架"和"一切皆文件"中画了 /dev/led,在"字符设备框架的由来"中概括出注册设备号 → 初始化字符设备 → 实现文件操作三步。(录音 14:27---17:35、31:16---33:49、35:47---44:25)把板上的实际路径补全如下:
0. 先明确硬件和协议
确认 LED 接在哪个 GPIO、引脚复用和电气连接、有效电平是高还是低。应用和驱动还要约定:例如写入一个二进制字节 1 表示亮、0 表示灭。这只是例子;若驱动约定接收 ASCII 字符 '1'/'0' 或使用 ioctl,应用必须按该约定发送。
1. 驱动向内核登记设备
驱动申请或注册设备号,例如课程示意的 500:0;准备 struct file_operations,把 .open、.write、.release 等成员指向自己的回调;初始化并添加 struct cdev,让这组设备号与文件操作关联。课程后续章节会具体讲 alloc_chrdev_region() / register_chrdev_region()、cdev_init() 和 cdev_add()。Linux 字符设备 API 文档
驱动还需取得并初始化对应 GPIO 资源,把引脚配置成适当模式。注册设备号解决"内核怎样找到这个驱动",硬件初始化解决"驱动怎样实际控制这个 LED";两件事都不可省。
2. 板上出现匹配的设备节点
/dev/led 必须是设备号与驱动一致的字符设备节点 。它可以由设备管理机制创建;若教学实验手动执行 mknod /dev/led c 500 0,前提是驱动确实注册了 500:0。节点中的编号让 VFS 在打开它时找到对应字符设备;路径中的 led 字样本身并不负责匹配驱动。
3. 应用打开设备
应用调用 fd = open("/dev/led", O_WRONLY)。系统调用进入内核,VFS 查看设备节点的主、次设备号,找到已注册的 cdev 和 file_operations,调用驱动的 .open(若实现了),成功后向应用返回 fd。这个 fd 只在当前进程的文件描述符表里有意义。
4. 应用发送"亮/灭",驱动改变 GPIO
下面是双方约定接收二进制单字节 1/0 时的应用侧示意 ;当前课堂模块没有提供这个接口,不可直接拿来操作之前只打印日志的 led.ko。
c
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
unsigned char on = 1;
int fd = open("/dev/led", O_WRONLY);
if (fd < 0) {
perror("open");
return 1;
}
ssize_t n = write(fd, &on, sizeof(on));
if (n != (ssize_t)sizeof(on)) {
if (n < 0)
perror("write");
else
fprintf(stderr, "short write\n");
close(fd);
return 1;
}
close(fd);
return 0;
}
老师板书中的 write(fd, 1) 只是表达"写入 1"的简写;真实系统调用是 write(fd, 缓冲区地址, 字节数),成功时返回实际写入的字节数,应检查返回值。write 手册
完整分发过程是:write() 系统调用 → VFS → 驱动的 .write 回调 → 安全地读取用户传入的数据并解释 1/0 → 通过 GPIO 子系统或已映射的寄存器设置输出电平 → LED 根据电路的有效电平亮或灭。把 on 改为 0 再写一次,驱动按约定执行相反动作。是否真的亮灯,要观察实物或测量引脚,不能仅靠模块加载日志判断。
5. 关闭、卸载与清理
close(fd) 关闭这个文件描述符;当对应的打开文件不再被引用时,若驱动实现了 .release,内核会调用它。close() 本身不必然"把设备让给别人",只有驱动专门实现独占访问时才有这种约束。以后卸载模块时,驱动还需移除 cdev、释放设备号与 GPIO 等资源;在有人使用设备时,卸载可能受模块引用计数阻止。
GPIO 与实体 LED LED 字符设备驱动 系统调用与 VFS 应用 GPIO 与实体 LED LED 字符设备驱动 系统调用与 VFS 应用 #mermaid-svg-qUyzgzj7l9No003W{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qUyzgzj7l9No003W .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qUyzgzj7l9No003W .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qUyzgzj7l9No003W .error-icon{fill:#552222;}#mermaid-svg-qUyzgzj7l9No003W .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qUyzgzj7l9No003W .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qUyzgzj7l9No003W .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qUyzgzj7l9No003W .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qUyzgzj7l9No003W .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qUyzgzj7l9No003W .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qUyzgzj7l9No003W .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qUyzgzj7l9No003W .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qUyzgzj7l9No003W .marker.cross{stroke:#333333;}#mermaid-svg-qUyzgzj7l9No003W svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qUyzgzj7l9No003W p{margin:0;}#mermaid-svg-qUyzgzj7l9No003W .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qUyzgzj7l9No003W text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-qUyzgzj7l9No003W .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qUyzgzj7l9No003W .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-qUyzgzj7l9No003W .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-qUyzgzj7l9No003W .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-qUyzgzj7l9No003W #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-qUyzgzj7l9No003W .sequenceNumber{fill:white;}#mermaid-svg-qUyzgzj7l9No003W #sequencenumber{fill:#333;}#mermaid-svg-qUyzgzj7l9No003W #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-qUyzgzj7l9No003W .messageText{fill:#333;stroke:none;}#mermaid-svg-qUyzgzj7l9No003W .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qUyzgzj7l9No003W .labelText,#mermaid-svg-qUyzgzj7l9No003W .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-qUyzgzj7l9No003W .loopText,#mermaid-svg-qUyzgzj7l9No003W .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-qUyzgzj7l9No003W .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qUyzgzj7l9No003W .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-qUyzgzj7l9No003W .noteText,#mermaid-svg-qUyzgzj7l9No003W .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-qUyzgzj7l9No003W .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qUyzgzj7l9No003W .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qUyzgzj7l9No003W .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qUyzgzj7l9No003W .actorPopupMenu{position:absolute;}#mermaid-svg-qUyzgzj7l9No003W .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-qUyzgzj7l9No003W .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qUyzgzj7l9No003W .actor-man circle,#mermaid-svg-qUyzgzj7l9No003W line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-qUyzgzj7l9No003W :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} open("/dev/led") 按设备号找到 cdev,调用 .open 返回 fd write(fd, &on, 1) 调用 .write 解释数据并设置引脚电平 LED 状态改变 close(fd) 调用 .release(若已实现)
六、老师说的"三步曲"与下一节
| 课堂三步 | 它解决的问题 | 下一步学习重点 |
|---|---|---|
| ① 注册设备号 | 让内核为字符设备分配或认可一组主、次设备号。 | 固定编号和动态分配的区别、如何释放编号。 |
| ② 初始化字符设备 | 用 cdev 关联设备号和 file_operations;硬件资源也要单独准备。 |
cdev_init/cdev_add、失败时清理、GPIO 初始化。 |
| ③ 实现文件操作 | 把通用的 open/read/write/release 等转为具体硬件行为。 |
回调参数、用户与内核之间的数据传递、ioctl 的控制命令。 |
ioctl 是老师在结尾提出的特殊控制接口,可用于不能方便地表达为普通读写的数据或配置操作,如串口工作模式;是否需要它应由设备接口设计决定,并非每个 LED 都必须实现。(录音 43:44---44:25)
七、与刚完成的内核模块实验有什么不同
前一节在 Jetson 上实测的 led.ko 已经证明 AArch64 模块可以编译、加载、打印入口日志并卸载 。它没有展示设备号注册、cdev、/dev/led、file_operations 或 GPIO 实际控制。因此那次结果是模块生命周期验收 ,本节是字符设备工作链路的概念学习 ;下一阶段需要逐项实现并验证上述接口,最后才可声称"应用写 /dev/led 控制了实体 LED"。
八、复习时能否独立讲清楚
- 为什么当前虚拟机里的 x86-64
led.ko不能用于 AArch64 Jetson?它与字符设备框架问题有什么区别? /dev/led、主/次设备号、进程内的fd、file_operations各负责哪一段关联?- 为什么
touch /dev/led不够?为什么手动mknod后也不代表 LED 必然能亮? - 从应用
write(fd, &on, 1)到 GPIO 电平变化,要经过哪些内核环节? - 哪些结论已在板上实测,哪些仍停留在本节的设计示意?