【全栈第14课】字符设备驱动 /dev/hello
Android 全栈工程师进阶教程 第14课。字符设备驱动 /dev/hello。基于公开 AOSP 知识整理,含原理、可编译示例、常见问题定位。
> 本文是《Android 全栈工程师进阶教程》系列第 14 课。完整 26 课见 GitHub 仓库。
【全栈第14课】字符设备驱动 /dev/hello
0. 这节课你将做出什么
写一个真实的 Linux 字符设备驱动:编成 .ko,insmod 后自动创建 /dev/hello,支持 open/read/write/ioctl。
学完你能回答:一个驱动怎么变成 /dev/xxx 让用户空间访问?用户 read(fd) 后内核驱动里发生了什么?触控/传感器/指纹这些驱动的最基础形态长什么样?
1. 背景:Linux"一切皆文件",驱动也是
Linux 把设备抽象成文件:用户空间通过 /dev/xxx 这个"文件"来操作硬件——open 打开、read/write 读写、ioctl 发控制命令。
驱动的工作,就是实现这些文件操作背后的真正逻辑。字符设备(character device)是最基础的一类——按字节流访问(对比块设备按块),触控、传感器、串口、指纹大多是字符设备。
学会字符驱动 = 掌握"用户空间 ↔ 内核驱动 ↔ 硬件"这条路的内核端入口。
2. 全局图:字符驱动六要素
用户空间: open("/dev/hello") / read / write / ioctl
│ VFS(虚拟文件系统)根据设备号路由
▼
内核: file_operations 回调表 ── 你实现的 hello_open/read/write/ioctl
│
├─ alloc_chrdev_region 申请设备号(主+次)
├─ cdev_add 注册字符设备
├─ class_create+device_create 自动建 /dev/hello 节点
└─ copy_to/from_user 内核↔用户空间安全拷贝数据
3. 内核 API 印证(基于 6.17 真编译)
本课在 Linux 6.17 真编译时验证了几个新内核 API(老教程常写错):
class_create(name)—— 6.4+ 是单参数(老教程是class_create(owner, name)两参数,新内核编不过)file_operations.unlocked_ioctl—— 现代用这个,不是老的.ioctl
4. 前置准备
- 依赖 L13(知道 .ko 怎么编、怎么加载)。
- 真编译环境:有 kernel headers 的 Linux(本课用本机 6.17;某真实机型 GKI prebuilt 编不了,见 L13)。
- 文件:
hello_chardev.c、Makefile、test_hello.c(用户态测试)。
5. 动手:写字符驱动(逐段)
5.1 文件操作回调实现
#include <linux/module.h>
#include <linux/fs.h> // file_operations
#include <linux/cdev.h> // cdev
#include <linux/device.h> // class_create/device_create
#include <linux/uaccess.h> // copy_to_user/copy_from_user
#include <linux/slab.h> // kzalloc
#define DEV_NAME "hello"
#define BUF_SIZE 256
#define HELLO_IOC_CLEAR _IO('H', 0) // ioctl 命令:用 _IO 宏定义,'H' 是幻数
static char *kbuf; // 内核缓冲区
static int data_len;
// open:用户 open("/dev/hello") 时调用
static int hello_open(struct inode *i, struct file *f) {
pr_info("hello: open by pid=%d\n", current->pid); // pr_info 打到 dmesg
return 0;
}
// read:用户 read(fd) → copy_to_user 把内核数据拷给用户
static ssize_t hello_read(struct file *f, char __user *ubuf, size_t len, loff_t *off) {
if (*off >= data_len) return 0; // 读到末尾返回 0(EOF)
if (len > data_len - *off) len = data_len - *off;
if (copy_to_user(ubuf, kbuf + *off, len)) return -EFAULT; // 内核→用户,失败返 -EFAULT
*off += len;
return len; // 返回实际读了多少字节
}
// write:用户 write(fd) → copy_from_user 把用户数据拷进内核
static ssize_t hello_write(struct file *f, const char __user *ubuf, size_t len, loff_t *off) {
if (len > BUF_SIZE) len = BUF_SIZE;
if (copy_from_user(kbuf, ubuf, len)) return -EFAULT; // 用户→内核
data_len = len;
return len;
}
// ioctl:控制命令(这里实现清空缓冲)
static long hello_ioctl(struct file *f, unsigned int cmd, unsigned long arg) {
if (cmd == HELLO_IOC_CLEAR) { memset(kbuf, 0, BUF_SIZE); data_len = 0; return 0; }
return -ENOTTY; // 不支持的命令返回 -ENOTTY(约定)
}
> 为什么不能直接用用户传进来的指针? 用户空间的指针在内核里不能直接解引用(地址空间不同、可能非法/恶意)。必须用 copy_to_user/copy_from_user 安全拷贝,它们会检查地址合法性。这是内核安全的基本功。
5.2 file_operations 表 —— 把回调挂上
static const struct file_operations hello_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.read = hello_read,
.write = hello_write,
.unlocked_ioctl = hello_ioctl, // 现代用 unlocked_ioctl
};
这张表就是"用户对 /dev/hello 做什么操作 → 调哪个函数"的映射。VFS 拿到你的操作,查这张表分发。
5.3 模块加载:申请设备号 + 注册 + 建节点
static dev_t dev_num;
static struct cdev hello_cdev;
static struct class *hello_class;
static int __init hello_init(void) {
int ret;
kbuf = kzalloc(BUF_SIZE, GFP_KERNEL); // 分配内核缓冲
if (!kbuf) return -ENOMEM;
ret = alloc_chrdev_region(&dev_num, 0, 1, DEV_NAME); // ① 动态申请设备号(主+次)
if (ret) goto e1;
cdev_init(&hello_cdev, &hello_fops); // ② 初始化 cdev,挂上 fops
ret = cdev_add(&hello_cdev, dev_num, 1); // 注册字符设备
if (ret) goto e2;
hello_class = class_create(DEV_NAME); // ③ 创建 class(6.4+ 单参数!)
if (IS_ERR(hello_class)) { ret = PTR_ERR(hello_class); goto e3; }
device_create(hello_class, NULL, dev_num, NULL, DEV_NAME); // udev 自动建 /dev/hello
pr_info("hello: loaded major=%d\n", MAJOR(dev_num));
return 0;
e3: cdev_del(&hello_cdev); // 错误回滚:逆序释放
e2: unregister_chrdev_region(dev_num, 1);
e1: kfree(kbuf);
return ret;
}
static void __exit hello_exit(void) { // rmmod 时逆序拆
device_destroy(hello_class, dev_num);
class_destroy(hello_class);
cdev_del(&hello_cdev);
unregister_chrdev_region(dev_num, 1);
kfree(kbuf);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Lesson14 hello char device driver"); // ← 不写会 warning
> 错误回滚(goto e1/e2/e3)是内核驱动的标准写法:init 里每一步可能失败,失败时要把前面成功的步骤逆序撤销,否则资源泄漏。这不是可选项,是规范。
5.4 Makefile
obj-m += hello_chardev.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(shell pwd) modules
6. 看底层:read 一次,内核走了什么
用户: read(fd, buf, 64)
│ 系统调用 sys_read
▼ VFS 根据 fd 找到 /dev/hello 对应的 file_operations
▼ 调用 hello_read(file, buf, 64, &offset)
│ copy_to_user(buf, kbuf+offset, n) ← 内核缓冲 → 用户缓冲
▼ 返回实际字节数 n
用户: read 返回 n
open/write/ioctl 同理,VFS 都按 file_operations 表分发到你的函数。
7. 编译 & 运行(✅ 真编译过)
make # 真编译,产出 hello_chardev.ko
sudo insmod hello_chardev.ko
dmesg | tail # 看 "hello: loaded major=xxx"
ls -l /dev/hello # 自动建的节点
echo "test" | sudo tee /dev/hello && sudo cat /dev/hello # 写入再读回
sudo rmmod hello_chardev
真编译结果(本机 6.17):
CC [M] hello_chardev.o
LD [M] hello_chardev.ko ← .ko 真的编出来了
> 验证状态(诚实):本课驱动在本机 Linux 6.17 真 make modules 编出 .ko(class_create 单参数等新 API 经真编译确认)。insmod 跑需 root,你本人执行(命令如上)。
8. 踩坑提醒(真编译踩到的)
class_create参数变了:6.4+ 是单参数class_create(name),老教程class_create(owner, name)在新内核编不过。- 缺
MODULE_DESCRIPTION:新内核会 warning(missing MODULE_DESCRIPTION()),加上消除。 - 直接用用户指针:必须
copy_to/from_user,直接解引用用户指针会崩或被安全检查拦。 - 忘了错误回滚:init 中途失败不逆序释放 → 资源泄漏,rmmod 后再 insmod 可能失败。
.ko内核版本不匹配:给 A 内核编的 .ko 装到 B 内核 → version magic 错(L13)。
9. 常见问题分析与定位(字符驱动实战)
9.1 常见问题清单
insmod成功但/dev/hello没出现open /dev/hello报 Permission denied- read/write 数据不对/截断
- rmmod 报
module is in use卸不掉 - 驱动崩导致 kernel panic
9.2 分析与定位
① /dev/hello 没出现
- 原因:
device_create没调/失败,或 udev/ueventd 没建节点。 - 定位:
dmesg | grep hello # init 走到哪了,有没有报错
cat /proc/devices | grep hello # 设备号注册了吗
ls -l /sys/class/hello_class/ # class 建了吗
- 修复:确认 class_create + device_create 都成功;手动
mknod临时建节点验证。
② Permission denied
- 原因:/dev/hello 节点权限/SELinux 不允许当前进程访问。
- 定位:
ls -lZ /dev/hello(权限 + SELinux 标签);dmesg | grep avc(Android 上 SELinux 拦)。 - 修复:Android 上要配 sepolicy 给节点打标签 + allow(L6/L11)。
③ 数据不对/截断
- 原因:read/write 里 len/offset 处理错,或 copy_to/from_user 返回值没判。
- 定位:在 hello_read/write 里
pr_info打印 len/off/data_len,看哪不对。
④ module is in use 卸不掉
- 原因:还有进程 open 着 /dev/hello(引用计数 >0)。
- 定位:
lsof /dev/hello或fuser /dev/hello看谁占着;关掉那个进程再 rmmod。
⑤ 驱动崩 panic
- 原因:空指针(kbuf 没分配就用)、越界、用户指针直接解引用。
- 定位:
dmesg/ramdump 看 panic 栈顶在你驱动哪一行(L24);常见是 copy_to_user 地址错、kbuf NULL。
9.3 定位决策树
字符驱动出问题
├─ /dev/hello 没出现 → dmesg + cat /proc/devices + ls /sys/class → class/device_create 失败
├─ Permission denied → ls -lZ + dmesg avc → 节点权限/SELinux(L6/L11)
├─ 数据不对 → hello_read/write 里 pr_info 打 len/off → 边界/copy 返回值
├─ 卸不掉 → lsof/fuser /dev/hello → 关占用进程
└─ panic → dmesg/ramdump 栈顶(L24) → 空指针/越界/用户指针
10. 小结
- Linux 一切皆文件:驱动通过 file_operations 实现 /dev/xxx 的 open/read/write/ioctl。
- 六要素:file_operations 表 + 设备号(alloc_chrdev_region)+ cdev_add + class/device_create(建节点)+ copy_to/from_user(安全拷贝)+ 错误回滚。
- 新内核 API:
class_create单参数、.unlocked_ioctl(真编译确认)。 - 绝不直接用用户指针,必须 copy_to/from_user。
- 排查口诀:没节点查 device_create+/proc/devices;权限查 SELinux;卸不掉查 lsof;崩看 dmesg 栈顶。
下节预告
L15:platform driver + Device Tree —— 真实 ARM SoC 驱动不靠手动 insmod,而是 DTS 描述硬件、驱动 compatible 匹配自动 probe。