Appearance
Poll阶段深度解析
Poll阶段是事件循环的核心,大部分I/O回调都在这里执行。理解Poll阶段对于掌握Node.js异步机制至关重要。
Poll阶段的职责
Poll阶段主要负责两件事:
- 计算阻塞时间:决定事件循环在这里等待多久
- 处理I/O事件:执行I/O相关的回调函数
┌─────────────────────────────────────────────────────┐
│ Poll阶段 │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 1. 计算超时时间 │ │
│ │ - 如果有setImmediate,超时 = 0 │ │
│ │ - 如果有定时器到期,超时 = 0 │ │
│ │ - 否则,等待I/O事件 │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 2. 等待I/O事件(可能阻塞) │ │
│ │ - epoll_wait (Linux) │ │
│ │ - kqueue (macOS) │ │
│ │ - IOCP (Windows) │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 3. 执行I/O回调 │ │
│ │ - 处理完成的读/写操作 │ │
│ │ - 处理新连接 │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘libuv中的Poll实现
Linux: epoll
c
// deps/uv/src/unix/linux.c
void uv__io_poll(uv_loop_t* loop, int timeout) {
struct epoll_event events[1024];
int nfds;
// 等待I/O事件
nfds = epoll_wait(loop->backend_fd, events, 1024, timeout);
// 处理每个就绪的事件
for (int i = 0; i < nfds; i++) {
uv__handle_t* handle = events[i].data.ptr;
// 调用相应的回调
if (events[i].events & EPOLLIN) {
handle->read_cb(handle);
}
if (events[i].events & EPOLLOUT) {
handle->write_cb(handle);
}
}
}macOS: kqueue
c
// deps/uv/src/unix/kqueue.c
void uv__io_poll(uv_loop_t* loop, int timeout) {
struct kevent events[1024];
struct timespec ts;
int nfds;
ts.tv_sec = timeout / 1000;
ts.tv_nsec = (timeout % 1000) * 1000000;
// 等待I/O事件
nfds = kevent(loop->backend_fd, NULL, 0, events, 1024, &ts);
// 处理事件...
}Windows: IOCP
c
// deps/uv/src/win/core.c
void uv__poll(uv_loop_t* loop, DWORD timeout) {
OVERLAPPED_ENTRY entries[64];
ULONG count;
// 获取完成的I/O操作
GetQueuedCompletionStatusEx(
loop->iocp,
entries,
64,
&count,
timeout,
FALSE
);
// 处理完成的操作...
}Poll阶段的超时计算
超时逻辑
c
int uv__backend_timeout(const uv_loop_t* loop) {
// 1. 如果循环将要停止,不等待
if (loop->stop_flag != 0)
return 0;
// 2. 如果没有活跃的handles和requests,不等待
if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
return 0;
// 3. 如果有idle handles,不等待
if (!QUEUE_EMPTY(&loop->idle_handles))
return 0;
// 4. 如果有pending的I/O回调,不等待
if (!QUEUE_EMPTY(&loop->pending_queue))
return 0;
// 5. 如果有关闭中的handles,不等待
if (loop->closing_handles != NULL)
return 0;
// 6. 计算到最近定时器的时间
return uv__next_timeout(loop);
}最近定时器计算
c
int uv__next_timeout(const uv_loop_t* loop) {
// 如果没有定时器,返回无限等待
if (RB_EMPTY(&loop->timer_handles))
return -1; // 无限等待
// 获取最近的定时器
const uv_timer_t* timer = RB_MIN(uv__timers, &loop->timer_handles);
// 计算差值
uint64_t diff = timer->timeout - loop->time;
if (diff > INT_MAX)
return INT_MAX;
return (int)diff;
}Poll阶段的行为
场景1:有setImmediate待执行
javascript
setImmediate(() => {
console.log('immediate');
});
// Poll阶段不会阻塞,立即进入check阶段timers → ... → poll(timeout=0) → check → ...
│ │
│ └─ 执行setImmediate
└─ 不等待,立即返回场景2:有定时器即将到期
javascript
setTimeout(() => {
console.log('timer');
}, 100);
// Poll阶段最多等待100mstimers → ... → poll(timeout=100ms) → check → close → timers
│ │
│ └─ 执行setTimeout回调
└─ 等待I/O或100ms超时场景3:只有I/O操作
javascript
const net = require('net');
const server = net.createServer((socket) => {
console.log('新连接');
});
server.listen(3000);
// Poll阶段会一直等待直到有连接timers → ... → poll(timeout=-1) → ...
│
└─ 无限等待直到有I/O事件场景4:空闲状态
javascript
// 只有这行代码
console.log('hello');
// 没有异步操作,进程退出执行同步代码 → 检查是否有活跃任务 → 没有 → 退出回调执行限制
Poll阶段会限制一次迭代中执行的回调数量,防止饿死其他阶段:
c
// 简化的逻辑
#define UV__IO_POLL_LIMIT 1000
void uv__io_poll(uv_loop_t* loop, int timeout) {
int count = 0;
while (has_events && count < UV__IO_POLL_LIMIT) {
// 处理一个事件
process_event();
count++;
}
// 如果还有事件没处理完,下次迭代继续
}Poll阶段与网络I/O
TCP连接处理
javascript
const net = require('net');
const server = net.createServer((socket) => {
// 这个回调在Poll阶段执行
console.log('客户端连接');
socket.on('data', (data) => {
// 这个回调也在Poll阶段执行
console.log('收到数据:', data.toString());
});
});
server.listen(3000);执行流程
1. server.listen() 注册监听socket到epoll
2. Poll阶段:
┌─────────────────────────────────────────────┐
│ epoll_wait() 等待事件 │
│ │
│ 事件1: 新连接到达 │
│ → 触发 connection 回调 │
│ │
│ 事件2: socket可读 │
│ → 触发 data 回调 │
│ │
│ 事件3: socket可写 │
│ → 继续写入缓冲的数据 │
└─────────────────────────────────────────────┘
3. 处理完所有就绪事件后,进入下一阶段Poll阶段与文件I/O
文件I/O不使用epoll/kqueue,而是使用线程池:
javascript
const fs = require('fs');
fs.readFile('large-file.txt', (err, data) => {
// 虽然回调在Poll阶段执行
// 但实际读取是在线程池中完成的
console.log('文件读取完成');
});文件I/O流程
主线程 线程池
│ │
│ fs.readFile() │
│────────────────────────────────>│
│ │ 读取文件
│ 继续事件循环 │ (阻塞操作)
│ │
│<────────────────────────────────│
│ 通过pipe/事件通知 │
│ │
│ Poll阶段执行回调 │
│ │深入理解:epoll机制
为什么epoll高效
传统select/poll:
┌─────────────────────────────────────────────────┐
│ 每次调用都要传递所有监听的fd │
│ 内核需要遍历所有fd检查状态 │
│ O(n) 复杂度 │
└─────────────────────────────────────────────────┘
epoll:
┌─────────────────────────────────────────────────┐
│ 内核维护监听列表 │
│ 只返回就绪的fd │
│ O(1) 添加/删除,O(就绪数) 获取事件 │
└─────────────────────────────────────────────────┘epoll三个系统调用
c
// 1. 创建epoll实例
int epfd = epoll_create1(0);
// 2. 添加/修改/删除监听
struct epoll_event event;
event.events = EPOLLIN | EPOLLOUT;
event.data.fd = socket_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &event);
// 3. 等待事件
struct epoll_event events[100];
int n = epoll_wait(epfd, events, 100, timeout);边缘触发vs水平触发
c
// 水平触发(LT) - 默认
// 只要fd就绪就会通知,直到处理完
event.events = EPOLLIN;
// 边缘触发(ET)
// 只在状态变化时通知一次
event.events = EPOLLIN | EPOLLET;libuv默认使用水平触发,更容易正确使用。
Poll阶段调试
追踪I/O事件
javascript
const { createHook } = require('async_hooks');
createHook({
init(asyncId, type, triggerAsyncId) {
if (type.includes('TCP') || type.includes('PIPE')) {
console.log(`I/O操作: ${type}, ID: ${asyncId}`);
}
}
}).enable();监控Poll阶段延迟
javascript
const { monitorEventLoopDelay } = require('perf_hooks');
const h = monitorEventLoopDelay({ resolution: 20 });
h.enable();
setInterval(() => {
// 高延迟可能意味着Poll阶段回调执行时间过长
console.log(`Event loop delay: ${h.percentile(99) / 1e6}ms (p99)`);
}, 1000);常见问题
问题1:Poll阶段阻塞太久
javascript
// 错误:在回调中执行耗时同步操作
server.on('connection', (socket) => {
// 这会阻塞整个事件循环
const result = heavyComputation(); // ❌
socket.write(result);
});
// 正确:使用Worker线程
const { Worker } = require('worker_threads');
server.on('connection', (socket) => {
const worker = new Worker('./compute.js');
worker.on('message', (result) => {
socket.write(result);
});
});问题2:回调执行顺序不确定
javascript
// 同一阶段内的回调顺序取决于I/O完成顺序
fs.readFile('a.txt', () => console.log('a'));
fs.readFile('b.txt', () => console.log('b'));
// 输出顺序不确定,取决于哪个文件先读完问题3:连接积压
javascript
// 大量连接同时到达时
server.on('connection', async (socket) => {
// 每个连接都要数据库查询
const data = await db.query('...'); // 可能很慢
socket.write(data);
});
// 解决:使用连接池和超时
const pool = new ConnectionPool({ max: 100 });
server.on('connection', (socket) => {
socket.setTimeout(5000);
// 使用连接池限制并发
});与其他阶段的关系
┌────────────────────────────────────────────────────┐
│ 事件循环 │
│ │
│ timers ─────────────────────────────────────> │
│ │ │
│ │ 如果有到期定时器,执行回调 │
│ │ │
│ pending ────────────────────────────────────> │
│ │ │
│ │ 执行上次循环推迟的I/O回调 │
│ │ │
│ poll ───────────────────────────────────────> │
│ │ │
│ │ ┌─ 计算超时时间 │
│ │ │ - 有setImmediate? → 0 │
│ │ │ - 有定时器? → min(定时器时间) │
│ │ │ - 否则 → -1 (无限) │
│ │ │ │
│ │ └─ 等待I/O,执行回调 │
│ │ │
│ check ──────────────────────────────────────> │
│ │ │
│ │ 执行setImmediate回调 │
│ │ │
│ close ──────────────────────────────────────> │
│ │
└────────────────────────────────────────────────────┘深入:I/O多路复用机制
I/O轮询是Node.js异步I/O的核心。下面深入分析libuv如何在不同操作系统上实现高效的I/O多路复用。
什么是I/O多路复用
传统模型(每连接一个线程):
┌─────────────────────────────────────────────────────────┐
│ 线程1 ─── socket1 ─── 阻塞等待 ─── 读取 ─── 处理 │
│ 线程2 ─── socket2 ─── 阻塞等待 ─── 读取 ─── 处理 │
│ 线程3 ─── socket3 ─── 阻塞等待 ─── 读取 ─── 处理 │
│ ... │
│ 问题:10000连接需要10000个线程 │
└─────────────────────────────────────────────────────────┘
I/O多路复用:
┌─────────────────────────────────────────────────────────┐
│ 单线程 ─┬─ socket1 ─┐ │
│ ├─ socket2 ─┼─ 系统告诉我们哪些就绪 ─── 处理 │
│ └─ socket3 ─┘ │
│ 优势:一个线程处理10000个连接 │
└─────────────────────────────────────────────────────────┘I/O多路复用发展历史
select (1983)
│
│ 限制1024个fd
│ 每次调用复制所有fd
▼
poll (1997)
│
│ 无fd数量限制
│ 仍需遍历所有fd
▼
epoll (Linux 2002) / kqueue (BSD 2000)
│
│ O(1)事件通知
│ 只返回就绪的fd
▼
io_uring (Linux 2019)
│
│ 异步I/O
│ 减少系统调用水平触发vs边缘触发
c
// 水平触发(LT,默认)
event.events = EPOLLIN;
// 特点:只要fd可读,每次epoll_wait都会返回
// 优点:简单,不会丢事件
// 缺点:可能重复通知
// 边缘触发(ET)
event.events = EPOLLIN | EPOLLET;
// 特点:只有状态变化时通知一次
// 优点:减少重复通知
// 缺点:必须一次读完所有数据,否则丢失
// libuv使用水平触发,更安全libuv的I/O抽象
c
// 统一的watcher结构
struct uv__io_s {
uv__io_cb cb; // 回调函数
void* pending_queue[2]; // pending队列节点
void* watcher_queue[2]; // watcher队列节点
unsigned int pevents; // 待注册的事件
unsigned int events; // 当前注册的事件
int fd; // 文件描述符
};本章小结
- Poll阶段是事件循环的核心,负责处理大部分I/O回调
- Poll阶段使用系统级异步机制:epoll(Linux)、kqueue(macOS)、IOCP(Windows)
- 超时时间由setImmediate和定时器决定
- 文件I/O使用线程池,网络I/O使用系统异步机制
- 一次迭代中执行的回调数有限制,防止饿死其他阶段
- I/O多路复用允许单线程处理大量连接
- libuv抽象了平台差异,提供统一的API
- 长时间同步操作会阻塞整个事件循环
下一章,我们将深入check阶段,理解setImmediate的工作机制。