上一章实现了 TTY 输入,但用户程序只能这样等输入:

.read_loop:
    syscall SYS_READ(fd=0, buf, 64)
    cmp rax, 0
    jle .read_loop

问题很明显:

  1. CPU 全速空转:没有输入时 SYS_READ 每次返回 0,进程白白占用 CPU
  2. 无法同时等多个 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.hpollfd_t 结构 POSIX 兼容的 poll fd 描述符
tty_poll() 检查 TTY 行就绪
pipe_poll(idx, is_read) 检查 pipe 读/写就绪
vfs_poll(vfd, events) VFS 层统一分发
SYS_POLL 系统调用入口,遍历 fds 填写 revents

poll 的核心价值:把"等哪个 fd"的决策从用户空间移到内核,一次调用搞定多路等待。