Linux 里有个特殊目录 /dev/,里面住着各种设备文件:
/dev/null ← 写什么扔什么,读永远 EOF
/dev/zero ← 读出来全是 \0
/dev/random ← 读出来是随机字节
/dev/tty ← 当前终端
对用户程序来说,它们和普通文件没区别:open、read、write、close,完全一样的接口。
这一章实现字符设备(char device)框架,让 VFS 能路由 /dev/ 路径,并内置 null 和 zero 两个最基础的设备。
什么是字符设备
字符设备(character device)的特点:
- 按字节读写,没有块/扇区的概念
- 没有随机寻址(不像磁盘文件可以 seek)
- 读写是即时的:写进去就消失(null)或立刻可读(zero/tty)
与之对应的是块设备(block device),如硬盘,以固定大小的块为单位操作。
设备注册表
核心数据结构 cdev_t:
typedef struct {
char name[16]; // 设备名,如 "null"、"zero"
int (*open) (void);
int (*read) (void *buf, uint32_t len);
int (*write)(const void *buf, uint32_t len);
void (*close)(int fd);
int used;
} cdev_t;
static cdev_t devs[CDEV_MAX]; // 全局设备表
通过函数指针实现多态——不同设备注册不同的 read/write 实现。
chardev_register 向 devs[] 添加一个设备,chardev_open 按名字查找并调用 open()。
/dev/null
static int null_read(void *buf, uint32_t len) {
(void)buf; (void)len;
return 0; // EOF
}
static int null_write(const void *buf, uint32_t len) {
(void)buf;
return (int)len; // 假装全写进去了,其实丢掉
}
Unix 里用 >/dev/null 重定向就是这个效果——写操作不报错,数据直接消失。
/dev/zero
static int zero_read(void *buf, uint32_t len) {
uint8_t *p = (uint8_t *)buf;
for (uint32_t i = 0; i < len; i++) p[i] = 0;
return (int)len;
}
常见用途:dd if=/dev/zero of=file bs=1024 count=1024 快速创建一个 1MB 的全零文件。
VFS 集成
在 VFS 层加入第四种文件类型 VFILE_CDEV = 4,并给 VFile 结构加上 cdev_fd 字段:
typedef struct {
...
int type;
int pipe_idx;
int proc_fd;
int cdev_fd; // 新增
} VFile;
vfs_open 检测路径前缀:
int vfs_open(const char *path) {
if (str_startswith(path, "/proc/"))
return vfs_open_proc(path + 5);
if (str_startswith(path, "/dev/"))
return vfs_open_cdev(path + 5); // 跳过 "/dev/"
...
}
vfs_read/write/close 按 type 分发:
if (f->type == VFILE_CDEV)
return chardev_read(f->cdev_fd, buf, len);
修复 fd=0 的二义性
之前 SYS_READ(fd=0) 总是走 TTY,但现在进程可能会 open("/dev/null") 并拿到 fd=0(进程 fd_table 里第一个空位就是 0)。
修复:只有当 fd_table[0] < 0(进程没有显式打开 fd 0)时才走 TTY:
if (rfd == 0 && current->fd_table[0] < 0) {
rn = tty_read(kbuf, len);
} else {
rn = fd_read(current->fd_table, rfd, kbuf, len);
}
踩坑:boot loader 扇区数不够
加了 chardev 之后,kernel.bin 从 24352 字节增长到 25728 字节(多了 1376 字节 ≈ 2.7 扇区)。
boot.asm 原来只读 50 个扇区(25600 字节),现在少读了最后 128 字节。偏偏这 128 字节里包含了 GDT 表!
表现:内核每次启动打印 “Kernel loaded!” 后立刻 triple fault,无限重启。
根因:gdt_init() 执行 lgdt 指令,加载的 GDT 基地址指向一片未被加载的内存(恰好是零),CPU 读到无效段描述符,触发保护异常,继而 double fault,最终 triple fault,机器重启。
修复:把 boot.asm 里的扇区数从 50 改为 60,给内核留足空间。
mov al, 60 ; 读 60 个扇区(30KB),足够未来一段时间
这类问题很隐蔽——代码逻辑完全正确,二进制文件本身也对,只是没被完整加载进内存。调试时要从"数据是否被正确加载"这个角度排查。
测试
; open /dev/null,写入数据
mov rax, SYS_OPEN
lea rdi, [rel dev_null] ; "/dev/null"
syscall
; 返回 fd=0(进程第一个空位)
; SYS_FWRITE(fd=0, "discard me", 10)
; → vfs_write → chardev_write → null_write → 静默丢弃
; open /dev/zero,读 8 字节
mov rax, SYS_OPEN
lea rdi, [rel dev_zero] ; "/dev/zero"
syscall
; SYS_READ(fd, buf, 8)
; → vfs_read → chardev_read → zero_read → 全零
运行结果:
chardev test start
write /dev/null: ok
read /dev/zero: ok (got zeros)
总结
| 新增内容 | 作用 |
|---|---|
chardev.h/c |
设备注册表,函数指针多态 |
/dev/null |
写丢弃,读 EOF |
/dev/zero |
读全零 |
VFILE_CDEV |
VFS 新文件类型 |
vfs_open_cdev |
/dev/ 前缀路由 |
cdev_fd in VFile |
VFS 层到 chardev 层的索引 |
字符设备框架建立后,添加新设备只需要实现 4 个函数(open/read/write/close)并调用 chardev_register,完全不需要修改 VFS 或系统调用层。