从零写OS(三十六):ext2 写操作 —— 文件创建、引用计数与 tty 字符设备

前几章文件系统都是只读的。这一章实现 ext2 的写路径,让 echo hello > /tmp/a.txt && cat /tmp/a.txt 能正常工作,且重启后文件仍然存在。过程中还修了 fd 引用计数和 tty 字符设备两个问题。 ext2 磁盘结构回顾 块组 0: [超级块][组描述符][block bitmap][inode bitmap][inode table][数据块...] 关键字段: sb->s_free_inodes_count / gd->bg_free_inodes_count:空闲 inode 计数 sb->s_free_blocks_count / gd->bg_free_blocks_count:空闲 block 计数 gd->bg_inode_bitmap:inode bitmap 所在块号 gd->bg_block_bitmap:block bitmap 所在块号 gd->bg_inode_table:inode table 起始块号 inode 号从 1 开始;前 10 个是系统保留的(root=2,lost+found=11)。 alloc_inode / alloc_block alloc_inode uint32_t alloc_inode(void) { // 1. 读 inode bitmap 块 // 2. 找第一个为 0 的位(bit i → inode i+1) // 3. 置位,写回 bitmap // 4. 更新超级块和组描述符的 free_inodes_count // 5. 返回 inode 号(从 1 开始) } alloc_block uint32_t alloc_block(void) { // 1. 读 block bitmap 块 // 2. 找第一个为 0 的位,跳过 block 0(保留) // 3. 置位,写回 bitmap // 4. 更新超级块和组描述符的 free_blocks_count // 5. 清零新分配的块(避免垃圾数据) // 6. 返回块号 } 注意:block bitmap 的 bit 0 对应 block 0,必须跳过(block 0 不存在)。实际第一个可分配的块由文件系统布局决定(本项目中是 610)。 ...

May 22, 2026 · 3 min · 大飞

从零写OS(三十三):阻塞式 TTY —— read 不再忙等

上一章 busybox sh 成功显示了 / # 提示符,但 shell 拿不到任何输入——因为 tty_read 是忙轮询的,没有字符就直接返回 0,shell 以为收到 EOF,立刻退出。这一章实现真正的阻塞式 TTY,让 shell 能等待用户输入。 之前的问题 之前的 tty_read 大概是这样: int tty_read(char *buf, int len) { // 轮询串口状态寄存器 if (!(inb(0x3F8 + 5) & 1)) return 0; // 没数据就返回 0 *buf = inb(0x3F8); return 1; } 这有两个问题: 没有输入时返回 0,上层程序(shell)以为是 EOF 如果上层在循环里调这个,CPU 100% 占用 正确做法:没有输入时挂起进程,等串口中断来了再唤醒。 串口中断(IRQ4) COM1 对应 IRQ4,接在 PIC 主片的 IR4 引脚。要用串口中断,需要两步: 1. 在 PIC 上 unmask IRQ4: // PIC 主片 IMR 寄存器:0 表示开放,1 表示屏蔽 uint8_t mask = inb(0x21); mask &= ~(1 << 4); // 清除 bit4,开放 IRQ4 outb(0x21, mask); 2. 开启串口的接收中断: ...

May 22, 2026 · 2 min · 大飞

从零写OS(二十六):poll —— 同时等待多个 fd

上一章实现了 TTY 输入,但用户程序只能这样等输入: .read_loop: syscall SYS_READ(fd=0, buf, 64) cmp rax, 0 jle .read_loop 问题很明显: CPU 全速空转:没有输入时 SYS_READ 每次返回 0,进程白白占用 CPU 无法同时等多个 fd:如果既要等 stdin,又要等 pipe,必须串行轮询,任一 fd 都可能饿死 poll 解决这两个问题。 poll 是什么 poll(fds[], nfds, timeout) 是一个系统调用: 传入一组 pollfd_t 结构,每个描述"我关心 fd X 的什么事件" 内核检查每个 fd 是否满足条件 返回就绪的 fd 数量,并在 revents 里标记哪些事件发生了 struct pollfd { int fd; // 监听哪个 fd short events; // 关心:POLLIN(可读)/ POLLOUT(可写) short revents; // 内核填写:实际发生了什么 }; int poll(struct pollfd *fds, int nfds, int timeout_ms); 有了 poll,程序可以: poll([{fd=0, POLLIN}, {fd=pipe_r, POLLIN}], 2, -1) → 等到任意一个有数据,才返回 实现架构 整体分四层: ...

May 14, 2026 · 3 min · 大飞

从零写OS(二十五):TTY 终端输入

到目前为止,进程只能输出,没有办法接收用户输入。SYS_READ 形同虚设,键盘按了也没反应。 这一章实现 TTY——Unix 对终端设备的最初抽象,让进程能以行为单位从串口读取输入。 TTY 是什么 TTY 原本是 teletypewriter(电传打字机)的缩写。在 Unix 里,它泛指"终端"——一个字符设备,既能接收键盘输入,又能输出文字。 现代 Linux 里的 /dev/tty、/dev/pts/0 都是 TTY 的后代。我们这里用串口 COM1 模拟一个最简单的 TTY。 整体架构 用户键盘输入 ↓ 串口硬件触发 IRQ4 isr_handler → tty_recv(c) ← 回显 + 写入 ring buffer ↓ 遇到 '\n' line_ready = 1 ↓ 进程 syscall SYS_READ(fd=0) tty_read(buf, len) ← 把一行搬到用户 buf ↓ 用户程序处理输入 三个关键组件: Ring Buffer:固定大小的环形缓冲区,存放还未被读走的字符 行规程(line discipline):积累字符,遇 \n 才通知"行就绪" IRQ4 中断处理:从串口读一个字节,交给 TTY Ring Buffer 环形缓冲区是一个固定数组,加上读指针和写指针: [ _ _ _ _ h e l l o \n _ _ ] ↑ ↑ rx_read rx_write rx_len = 6 写字符:rx_buf[rx_write] = c; rx_write = (rx_write + 1) % TTY_BUF_SIZE; rx_len++ ...

May 14, 2026 · 2 min · 大飞
京ICP备14031575号-3