第一百三十二章:字符设备
合道期涉及内核源码:
一
林小源跟随守山老者,踏入了字符设备的领地。
这里的一切都像是流动的。溪水从山涧淌下,每一滴水珠都是一个字节;风穿过树梢,发出有节奏的滴答声,像是串口在发送数据。路边竖着一块石碑,上面刻着 /dev/ttyS0,碑身上标记着两组数字:主 4,次 64。
"这是什么?"林小源指着那两组数字问。
守山老者还没来得及回答,一个年轻的修士从旁边的小路上走了过来。他穿着一身简洁的灰袍,袍子上只绣了一个 字样。
"设备号。"年轻修士开口说道,声音清脆,"主设备号标识驱动——哪个驱动在管这个设备。次设备号标识具体设备——同一类设备可能有好几个,次设备号用来区分它们。"
"你是?"
"我是字符设备的引路人。"年轻修士微微一笑,"你可以叫我 cdev。所有字符设备的注册,都要经过我的手。"
cdev 带着林小源来到一处广场。广场中央立着一块巨大的石板,石板上刻着一张表——。
"这是字符设备的核心。"cdev 拍了拍石板,"一个字符设备驱动,必须填好这张表。open、release、read、write、ioctl——这些就是用户空间跟你的设备对话的全部方式。"
林小源凑近石板,看到表中的每一行都是一根细线,连向山脚下的某个设备。
"注册一个字符设备,总共四步。"cdev 竖起四根手指,"第一,分配设备号——用 ,让内核给你分配一个空闲的主次设备号对。第二,初始化 cdev——用 ,把你的 file_operations 绑上去。第三,注册 cdev——用 ,告诉内核'这个设备号归我管了'。第四,在 /dev/ 下创建设备文件——用 ,让用户空间能找到你的设备。"
/*
* 字符设备驱动的关键结构:
*
* struct file_operations:
* open — 打开设备
* release — 关闭设备
* read — 读取数据
* write — 写入数据
* ioctl — 控制命令
*
* 注册字符设备:
* 1. 分配设备号 (MAJOR, MINOR)
* 2. 注册 cdev
* 3. 创建设备文件 (/dev/xxx)
*
* 设备号:
* 主设备号 — 标识驱动
* 次设备号 — 标识具体设备
* 例: /dev/ttyS0 — 主 4, 次 64
*/
printf("=== 字符设备 — 最简单的驱动 ===\n\n");
printf("字符设备驱动:\n\n");
printf("--- file_operations ---\n");
printf("struct file_operations {\n");
printf(" .open = my_open,\n");
printf(" .release = my_release,\n");
printf(" .read = my_read,\n");
printf(" .write = my_write,\n");
printf(" .ioctl = my_ioctl,\n");
printf("};\n\n");
printf("--- 注册步骤 ---\n");
printf("1. 分配设备号:\n");
printf(" alloc_chrdev_region(&dev, 0, 1, \"mydev\")\n\n");
printf("2. 初始化 cdev:\n");
printf(" cdev_init(&cdev, &fops)\n\n");
printf("3. 注册 cdev:\n");
printf(" cdev_add(&cdev, dev, 1)\n\n");
printf("4. 创建设备文件:\n");
printf(" mknod /dev/mydev c MAJOR MINOR\n\n");
printf("--- 设备号 ---\n");
printf("主设备号: 标识驱动\n");
printf("次设备号: 标识具体设备\n\n");
printf("例:\n");
printf(" /dev/ttyS0 — 主 4, 次 64\n");
printf(" /dev/ttyS1 — 主 4, 次 65\n\n");
printf("--- read/write 实现 ---\n");
printf("ssize_t my_read(struct file *f,\n");
printf(" char __user *buf,\n");
printf(" size_t len, loff_t *off) {\n");
printf(" // 从设备读取数据\n");
printf(" copy_to_user(buf, kernel_buf, len);\n");
printf(" return len;\n");
printf("}\n\n");
printf("--- 用户空间访问 ---\n");
printf("int fd = open(\"/dev/mydev\", O_RDWR);\n");
printf("read(fd, buf, 100);\n");
printf("write(fd, buf, 100);\n");
printf("ioctl(fd, CMD, arg);\n");
printf("close(fd);\n");#include <stdio.h>
/*
* 字符设备驱动的关键结构:
*
* struct file_operations:
* open — 打开设备
* release — 关闭设备
* read — 读取数据
* write — 写入数据
* ioctl — 控制命令
*
* 注册字符设备:
* 1. 分配设备号 (MAJOR, MINOR)
* 2. 注册 cdev
* 3. 创建设备文件 (/dev/xxx)
*
* 设备号:
* 主设备号 — 标识驱动
* 次设备号 — 标识具体设备
* 例: /dev/ttyS0 — 主 4, 次 64
*/
int main() {
printf("=== 字符设备 — 最简单的驱动 ===\n\n");
printf("字符设备驱动:\n\n");
printf("--- file_operations ---\n");
printf("struct file_operations {\n");
printf(" .open = my_open,\n");
printf(" .release = my_release,\n");
printf(" .read = my_read,\n");
printf(" .write = my_write,\n");
printf(" .ioctl = my_ioctl,\n");
printf("};\n\n");
printf("--- 注册步骤 ---\n");
printf("1. 分配设备号:\n");
printf(" alloc_chrdev_region(&dev, 0, 1, \"mydev\")\n\n");
printf("2. 初始化 cdev:\n");
printf(" cdev_init(&cdev, &fops)\n\n");
printf("3. 注册 cdev:\n");
printf(" cdev_add(&cdev, dev, 1)\n\n");
printf("4. 创建设备文件:\n");
printf(" mknod /dev/mydev c MAJOR MINOR\n\n");
printf("--- 设备号 ---\n");
printf("主设备号: 标识驱动\n");
printf("次设备号: 标识具体设备\n\n");
printf("例:\n");
printf(" /dev/ttyS0 — 主 4, 次 64\n");
printf(" /dev/ttyS1 — 主 4, 次 65\n\n");
printf("--- read/write 实现 ---\n");
printf("ssize_t my_read(struct file *f,\n");
printf(" char __user *buf,\n");
printf(" size_t len, loff_t *off) {\n");
printf(" // 从设备读取数据\n");
printf(" copy_to_user(buf, kernel_buf, len);\n");
printf(" return len;\n");
printf("}\n\n");
printf("--- 用户空间访问 ---\n");
printf("int fd = open(\"/dev/mydev\", O_RDWR);\n");
printf("read(fd, buf, 100);\n");
printf("write(fd, buf, 100);\n");
printf("ioctl(fd, CMD, arg);\n");
printf("close(fd);\n");
return 0;
}二
"等等。"林小源盯着石板上的 read 实现,皱起了眉头,"这里为什么要用 ?直接把内核缓冲区的地址返回给用户空间不就行了?"
cdev 的表情突然变得严肃。他伸手在石板上重重一拍,一道半透明的光幕从石板上升起,将广场一分为二——左边是内核空间,右边是用户空间。光幕上流动着密密麻麻的权限标记。
"看到这道光幕没有?"cdev 说,"内核空间和用户空间是隔离的。内核不能直接访问用户空间的内存——因为你不知道那个地址是不是合法的、那个页面是不是还在内存里。如果用户空间给你一个野指针,你直接解引用,整个内核就崩了。"
林小源倒吸一口凉气。
"所以, 和 是专用的函数。"cdev 继续说,"它们会做完整的权限检查——验证地址范围、检查页面权限、处理缺页异常。如果出错,返回错误码,不会崩内核。"
"代价呢?"
"多了一次数据拷贝。"cdev 坦然说,"但安全和可移植性,比那点性能更重要。你要记住,在内核里,安全永远排在性能前面。"
三
"那 又是什么?"林小源指着 file_operations 表上的最后一行,"read 和 write 能读写数据,为什么还需要 ioctl?"
cdev 没有立刻回答。他带着林小源走到广场边缘的一间小屋前。屋内摆满了各式各样的控制面板——有的上面旋钮密布,有的只有一排开关,有的是一块触摸屏。
"read 和 write 能做的事很有限——就是读数据、写数据。"cdev 说,"但设备有很多操作,不是简单的读写能表达的。比如设置串口的波特率——115200 还是 9600?比如查询磁盘的状态——是忙碌还是空闲?比如控制摄像头的曝光参数。"
他拍了拍一块控制面板:"ioctl 就是这个——设备特有的控制面板。你想对设备做什么特殊的操作,都可以通过 ioctl 下发命令。它是 read/write 之外的扩展接口。"
林小源看着那些面板,忽然明白了:字符设备就像一扇门,read/write 是门上的窗口,而 ioctl 是门边的一整面控制墙。
"但别滥用。"cdev 补充道,语气变得警告,"能用 read/write 解决的事,不要用 ioctl。ioctl 的命令号是设备自定义的,不同设备之间不通用,调试起来也麻烦。"
道藏笔记
内核启示
字符设备以字节流方式访问。
字符设备的关键结构:
- — 操作函数表
- — 字符设备对象
注册步骤:
- 分配设备号
- 初始化 cdev
- 注册 cdev
- 创建设备文件
设备号:
- 主设备号 — 标识驱动
- 次设备号 — 标识具体设备
字符设备是"最简单"的驱动——像文件一样读写。
字符设备之试
字符设备把 read、write、ioctl 等操作交给驱动实现时,依靠的函数表叫什么?