上一章实现了 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)
→ 等到任意一个有数据,才返回
实现架构
整体分四层:
用户程序
└─ SYS_POLL(fds, nfds, timeout)
└─ syscall_handler
├─ fd=0 → tty_poll() 检查 line_ready
└─ fd>0 → vfs_poll(vfd, ev)
├─ PIPE_R → pipe_poll(idx, read=1) len > 0 ?
├─ PIPE_W → pipe_poll(idx, read=0) len < BUF ?
└─ FILE → 总是就绪
每一层只做一件事:判断现在是否可以 read/write 而不会阻塞。
pollfd_t 数据结构
新建 poll.h:
#define POLLIN 0x0001
#define POLLOUT 0x0004
#define POLLERR 0x0008
#define POLLHUP 0x0010
typedef struct {
int32_t fd;
int16_t events;
int16_t revents;
} pollfd_t;
这和 Linux 的 struct pollfd 二进制兼容(注意字段顺序和大小)。
tty_poll
最简单的一层——检查 TTY 缓冲区里有没有完整的行:
int tty_poll(void) {
return line_ready ? 1 : 0;
}
line_ready 由 IRQ4 处理函数在收到 \n 时设置。
pipe_poll
int pipe_poll(int idx, int is_read) {
pipe_t *p = &pipes[idx];
if (is_read)
return p->len > 0 ? 1 : 0; // 有数据可读
else
return p->len < PIPE_BUF_SIZE ? 1 : 0; // 有空间可写
}
vfs_poll
在 VFS 层统一分发:
int vfs_poll(int fd, int16_t events) {
VFile *f = &fd_table[fd];
if (f->type == VFILE_PIPE_R && (events & POLLIN))
return pipe_poll(f->pipe_idx, 1) ? POLLIN : 0;
if (f->type == VFILE_PIPE_W && (events & POLLOUT))
return pipe_poll(f->pipe_idx, 0) ? POLLOUT : 0;
if (f->type == VFILE_FILE) {
if (events & POLLOUT) return POLLOUT; // 文件总是可写
if (events & POLLIN) return POLLIN; // 文件总是可读
}
return 0;
}
SYS_POLL 系统调用
case SYS_POLL: {
pollfd_t *ufds = (pollfd_t *)a1;
int nfds = (int)a2;
int ready = 0;
for (int i = 0; i < nfds; i++) ufds[i].revents = 0;
for (int i = 0; i < nfds; i++) {
int32_t ufd = ufds[i].fd;
int16_t ev = ufds[i].events;
int16_t rev = 0;
if (ufd == 0) { // stdin = TTY
if ((ev & POLLIN) && tty_poll()) rev |= POLLIN;
} else if (ufd > 0 && current->fd_table[ufd] >= 0) {
int vfd = current->fd_table[ufd];
rev = (int16_t)vfs_poll(vfd, ev);
}
ufds[i].revents = rev;
if (rev) ready++;
}
return (uint64_t)ready;
}
逻辑非常直白:遍历每个 fd,调对应的 poll 函数,返回就绪总数。
用户程序测试
用 NASM 写,在 .data 段内联 pollfd_t 结构:
align 4
pfd:
dd 0 ; fd = 0 (stdin)
dw 0x0001 ; POLLIN
dw 0 ; revents(内核填写)
主循环:
.poll_loop:
mov rax, SYS_POLL
lea rdi, [rel pfd]
mov rsi, 1 ; nfds=1
mov rdx, 0 ; timeout=0(非阻塞)
syscall
cmp rax, 0
jle .poll_loop ; 没有就绪,继续轮询
运行结果:
poll test: waiting for stdin...
poll: stdin ready!
input: hello
- 程序进入 poll 自旋循环
- 串口收到 “hello\n”,IRQ4 设置
line_ready=1 - 下一次
poll调用返回 1,退出循环 - 读取并输出 “input: hello”
关于超时
本章实现的 poll 不支持真正的 sleep(timeout 参数被忽略)。原因很简单:让进程睡眠需要调度器配合,下一章才实现 select 的阻塞语义。
目前的自旋 poll 功能完全正确,只是 CPU 利用率不理想——对于 toy OS 的演示来说完全够用。
总结
| 新增内容 | 作用 |
|---|---|
poll.h — pollfd_t 结构 |
POSIX 兼容的 poll fd 描述符 |
tty_poll() |
检查 TTY 行就绪 |
pipe_poll(idx, is_read) |
检查 pipe 读/写就绪 |
vfs_poll(vfd, events) |
VFS 层统一分发 |
SYS_POLL |
系统调用入口,遍历 fds 填写 revents |
poll 的核心价值:把"等哪个 fd"的决策从用户空间移到内核,一次调用搞定多路等待。