核心命题:一个线程如何同时处理数万并发连接?答案不在 Node.js 本身,而在操作系统内核的 I/O 多路复用机制。Node.js 所做的,是构建一条从内核事件到用户态回调的完整链路。
目录
- 问题的本质:阻塞是万恶之源
- [内核基石:epoll 的实现原理](#内核基石:epoll 的实现原理 "#2-%E5%86%85%E6%A0%B8%E5%9F%BA%E7%9F%B3epoll-%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86")
- [libuv:跨平台 I/O 多路复用的封装](#libuv:跨平台 I/O 多路复用的封装 "#3-libuv%E8%B7%A8%E5%B9%B3%E5%8F%B0-io-%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8%E7%9A%84%E5%B0%81%E8%A3%85")
- 事件循环:单线程的调度中枢
- [完整数据流:从网卡中断到 JS 回调](#完整数据流:从网卡中断到 JS 回调 "#5-%E5%AE%8C%E6%95%B4%E6%95%B0%E6%8D%AE%E6%B5%81%E4%BB%8E%E7%BD%91%E5%8D%A1%E4%B8%AD%E6%96%AD%E5%88%B0-js-%E5%9B%9E%E8%B0%83")
- 线程池:无法非阻塞时的兜底方案
- [突破单线程:Worker Threads 与 Cluster](#突破单线程:Worker Threads 与 Cluster "#7-%E7%AA%81%E7%A0%B4%E5%8D%95%E7%BA%BF%E7%A8%8Bworker-threads-%E4%B8%8E-cluster")
- [io_uring:下一代异步 I/O](#io_uring:下一代异步 I/O "#8-iouring%E4%B8%8B%E4%B8%80%E4%BB%A3%E5%BC%82%E6%AD%A5-io")
1. 问题的本质:阻塞是万恶之源
1.1 传统多线程模型的困境
一个 TCP 连接的生命周期:
scss
客户端 SYN → 内核三次握手 → accept() 返回 fd → read() 等待数据 → 处理 → write() → 关闭
如果用同步阻塞模型,read() 在数据到达前会挂起线程。要同时服务 10000 个连接,就需要 10000 个线程。每个线程占用 8MB 栈空间(Linux 默认),仅栈内存就需要 ~80GB。
1.2 破局思路:让内核告诉我们"谁准备好了"
核心思想:不要主动轮询每个 fd,而是让内核在 fd 就绪时通知我们。这就是 I/O 多路复用------用一个系统调用同时监视多个 fd 的状态变化。
三种系统调用的演进:
| 系统调用 | 数据结构 | 时间复杂度 | 最大 fd 数 | 触发模式 |
|---|---|---|---|---|
select() |
fd_set 位图 | O(N) | 1024 (FD_SETSIZE) | LT |
poll() |
pollfd 数组 | O(N) | 无上限 | LT |
epoll_wait() |
内核红黑树 | O(1)~O(K) | 无上限 | LT/ET |
其中 K 是就绪 fd 数量。当连接数大但活跃连接少时(典型 Web 场景),epoll 的性能优势是数量级的。
2. 内核基石:epoll 的实现原理
2.1 三个系统调用,三个内核操作
c
// 1. 创建 epoll 实例 ------ 在内核中创建一棵红黑树 + 一个就绪链表
int epfd = epoll_create1(0);
// 2. 注册/修改/删除监视的 fd ------ 将 fd 插入红黑树,同时注册回调
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
// 3. 等待事件 ------ 阻塞直到有 fd 就绪,返回就绪事件数组
int nfds = epoll_wait(epfd, events, maxevents, timeout);
2.2 内核数据结构
epoll 实例在内核中由 struct eventpoll 管理:
c
// Linux 内核 fs/eventpoll.c(简化)
struct eventpoll {
struct rb_root_cached rbr; // 红黑树根,存储所有被监视的 fd(epitem)
struct list_head rdllist; // 就绪链表,存储已触发事件的 epitem
struct rb_root_cached accept_queue; // O(1) 的接受队列(Linux 6.x 新增优化)
wait_queue_head_t wq; // 等待队列,epoll_wait() 在此睡眠
struct file *file; // epoll 实例自身的 file 结构
int user_watches; // 当前监视的 fd 数量
};
// 每个被监视的 fd 对应一个 epitem
struct epitem {
struct rb_node rbn; // 红黑树节点,用于快速查找
struct list_head rdlink; // 就绪链表节点,事件触发时挂入 rdllist
struct epoll_filefd ffd; // 被监视的 fd 及其 file 结构
struct epitem __rcu *next; // 哈希桶链表(用于反向查找)
struct eventpoll *ep; // 所属的 eventpoll 实例
int nwait; // 当前注册的等待者数量
struct epoll_event event; // 用户注册的关注事件(EPOLLIN/EPOLLOUT等)
};
2.3 epoll_ctl 的内核实现:注册回调链
epoll_ctl(EPOLL_CTL_ADD) 的关键操作不是简单地记录 fd,而是向目标设备注册回调:
c
// Linux 内核 fs/eventpoll.c: ep_insert()(简化)
static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
struct file *tfile, int fd, int full_check)
{
// 1. 分配 epitem
epi = kmem_cache_zalloc(epi_cache, GFP_KERNEL);
epi->ep = ep;
epi->ffd = ffd;
epi->event = *event;
// 2. 初始化就绪链表节点
INIT_LIST_HEAD(&epi->rdlink);
// 3. 将 epitem 插入红黑树
ep_rbtree_insert(ep, epi);
// 4. 关键步骤:向目标文件注册回调
// 当设备有数据可读时,会调用 ep_poll_callback
if (tfile->f_op->poll &&
!(epi->event.events & EPOLLET)) {
// 对于水平触发模式,立即检查当前是否已就绪
revents = ep_item_poll(epi, &pt, 1);
// 如果已就绪,直接加入就绪链表
if (revents & event->events)
ep_ready_push(ep, epi);
}
// 5. 注册 wakeup 回调 ------ 这是整个机制的核心
// 当设备(如 socket)的数据就绪时,设备的 wait_queue 会调用
// ep_poll_callback → 将 epitem 挂入 rdllist → 唤醒 epoll_wait
//
// 对于 socket:sk->sk_data_ready → ep_poll_callback
// 对于 pipe: pipe->wait → ep_poll_callback
// 对于 timer: timer->wait → ep_poll_callback
}
2.4 事件触发链路:从网卡到用户态
当一个 TCP 包到达时,完整的中断处理链路:
scss
网卡收到数据包
→ 网卡驱动触发硬件中断 (IRQ)
→ 中断处理程序(硬中断上半部)
→ NAPI 轮询,数据拷贝到内核 socket 接收缓冲区
→ 唤醒 socket 的等待队列:sk->sk_data_ready(sk)
→ ep_poll_callback() 被调用
→ 将对应的 epitem 挂入 eventpoll.rdllist(就绪链表)
→ 唤醒在 epoll_wait() 中睡眠的进程
ep_poll_callback 是连接内核网络栈与 epoll 的桥梁:
c
// Linux 内核 fs/eventpoll.c: ep_poll_callback(简化)
static int ep_poll_callback(wait_queue_entry_t *wait,
unsigned mode, int sync, void *key)
{
struct epitem *epi = ep_item_from_wait(wait);
struct eventpoll *ep = epi->ep;
// 检查事件是否匹配用户关注的事件
if (epi->event.events & key_to_poll(key)) {
// 将 epitem 加入就绪链表
if (list_empty(&epi->rdlink))
list_add_tail(&epi->rdlink, &ep->rdllist);
// 唤醒在 epoll_wait 中等待的进程
wake_up_locked_poll(&ep->wq, EPOLLIN);
}
return 1; // 返回 1 表示已处理,从等待队列中移除
}
2.5 epoll_wait 的内核实现
c
// Linux 内核 fs/eventpoll.c: ep_poll(简化)
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
int maxevents, long timeout)
{
// 1. 先检查就绪链表是否有事件(无锁快速路径)
if (!list_empty(&ep->rdllist))
goto send_events; // 有就绪事件,直接返回
// 2. 没有就绪事件,加入等待队列并睡眠
init_wait(&wait);
wait.func = ep_autoremove_wake_function;
add_wait_queue(&ep->wq, &wait);
for (;;) {
// 设置进程状态为 TASK_INTERRUPTIBLE
set_current_state(TASK_INTERRUPTIBLE);
// 释放锁,让出 CPU
spin_unlock_irq(&ep->lock);
timeout = schedule_hrtimeout_range(timeout, ...); // 睡眠
spin_lock_irq(&ep->lock);
// 被唤醒后检查:是超时、被信号中断、还是有事件?
if (timeout == 0) break; // 超时
if (signal_pending(current)) break; // 被信号中断
if (!list_empty(&ep->rdllist)) break; // 有事件了!
}
remove_wait_queue(&ep->wq, &wait);
send_events:
// 3. 将就绪链表中的事件拷贝到用户空间
eavail = list_empty(&ep->rdllist) ? 0 : 1;
while (eavail && cnt < maxevents) {
head = ep->rdllist.next; // 取就绪链表头
epi = list_entry(head, struct epitem, rdlink);
list_del_init(&epi->rdlink); // 从就绪链表移除
// 拷贝事件到用户空间
if (copy_to_user(&events[cnt], &epi->event, sizeof(struct epoll_event)))
return -EFAULT;
cnt++;
}
return cnt; // 返回就绪事件数量
}
2.6 水平触发 (LT) vs 边缘触发 (ET) 的内核差异
LT(Level Triggered,默认模式) :只要 fd 的接收缓冲区中有数据,每次 epoll_wait() 都会返回该 fd 的就绪事件。内核实现中,ep_poll_callback 在每次数据到达时都会将 epitem 挂入就绪链表。
ET(Edge Triggered) :只有 fd 状态发生变化时才通知一次。内核实现差异:
c
// ep_insert() 中:
if (!(epi->event.events & EPOLLET)) {
// LT 模式:插入红黑树后立即检查当前状态
revents = ep_item_poll(epi, &pt, 1);
if (revents & event->events)
ep_ready_push(ep, epi);
}
// ET 模式不做这个检查,只有后续的数据到达(ep_poll_callback 被调用)
// 才会将 epitem 挂入就绪链表
Node.js 的 libuv 使用 LT 模式(除 macOS 的 kqueue 外),因为 LT 模式更安全------不会因遗漏读取而导致事件丢失。
3. libuv:跨平台 I/O 多路复用的封装
libuv 是 Node.js 的异步 I/O 引擎,它在不同操作系统上选择最优的 I/O 多路复用后端:
| 操作系统 | 后端 | 系统调用 |
|---|---|---|
| Linux | epoll | epoll_create1 + epoll_ctl + epoll_pwait |
| macOS/BSD | kqueue | kqueue + kevent |
| Windows | IOCP | CreateIoCompletionPort + GetQueuedCompletionStatus |
| illumos | event ports | port_create + port_associate + port_get |
3.1 编译期后端选择
libuv 通过预处理器在编译期确定后端,零运行时开销:
c
// deps/uv/src/uv-common.c
int uv__io_fork(uv_loop_t* loop) {
// ...
}
// deps/uv/src/unix/loop.c
int uv_loop_init(uv_loop_t* loop) {
// ...
// 调用平台特定的初始化
uv__platform_loop_init(loop);
// ...
}
3.2 Linux 后端初始化:创建 epoll 实例
c
// deps/uv/src/unix/linux.c
int uv__platform_loop_init(uv_loop_t* loop) {
int fd;
// 后端 fd 就是 epoll 实例的文件描述符
fd = epoll_create1(O_CLOEXEC);
if (fd < 0)
return uv__translate_libuv_errno(errno);
loop->backend_fd = fd;
// io_uring 批量优化初始化(Linux 5.1+)
// 当需要批量注册/注销 fd 到 epoll 时,io_uring 可以
// 将多个 epoll_ctl 系统调用合并为一次 io_uring_enter
loop->iou_ring = NULL;
loop->iou_sqhead = NULL;
// ...
return 0;
}
3.3 核心函数 uv__io_poll:事件循环的 poll 阶段
这是 libuv 在 Linux 上等待 I/O 事件的核心实现,每一个事件循环迭代都会调用它:
c
// deps/uv/src/unix/linux.c(关键逻辑简化)
void uv__io_poll(uv_loop_t* loop, int timeout) {
struct epoll_event events[1024]; // 栈上分配,避免堆分配
struct epoll_event* pe;
struct epoll_event e;
int fd = loop->backend_fd; // epoll 实例 fd
int nevents;
int count;
int no_epoll_pwait = 0;
// 基准测试:探测内核在 1ms 内能处理多少 epoll_ctl
// 用于后续 io_uring 批量优化的阈值计算
if (loop->flags & UV_LOOP_ENABLE_IO_URING_SQE) {
// ...io_uring 批量优化逻辑
}
for (;;) {
// 如果当前 loop 已关闭,直接返回
if (loop->nfds == 0) {
if (timeout == 0) return;
// ...
}
// 将 watcher_queue 中的新 watcher 注册到 epoll
while (!uv__queue_empty(&loop->watcher_queue)) {
uv__queue* q = uv__queue_head(&loop->watcher_queue);
uv__queue_remove(q);
uv__queue_init(q);
w = uv__queue_data(q, uv__io_t, watcher_queue);
op = EPOLL_CTL_MOD; // 默认修改
if (w->events == 0)
op = EPOLL_CTL_ADD; // 新 watcher,添加
w->events = w->pevents;
e.events = w->pevents;
e.data = w; // 将 uv__io_t 指针存入 data
// 关键系统调用:注册/修改 fd 到 epoll
if (epoll_ctl(fd, op, w->fd, &e)) {
if (errno == EBADF) {
// fd 已关闭,清理
// ...
}
}
}
// 核心系统调用:阻塞等待事件
// epoll_pwait 比 epoll_wait 多一个 sigmask 参数
// 用于在等待期间临时恢复信号处理
if (no_epoll_pwait)
nevents = epoll_wait(fd, events, ARRAY_SIZE(events), timeout);
else
nevents = epoll_pwait(fd, events, ARRAY_SIZE(events), timeout, NULL);
if (nevents == 0) {
// 超时,没有事件
assert(timeout != -1);
return;
}
if (nevents == -1) {
// 被信号中断,重试
if (errno == EINTR) continue;
// 其他错误
return;
}
// 遍历所有就绪事件,调用对应的回调
for (int i = 0; i < nevents; i++) {
pe = &events[i];
w = pe->data.ptr; // 取出之前存入的 uv__io_t 指针
// 设置 fd 的就绪事件
w->events = pe->events;
// 调用回调函数 ------ 这是从内核事件到用户态代码的关键跳转
// 对于 stream,这个回调是 uv__stream_io()
// 对于 timer,这个回调是 uv__run_timers() 相关的
uv__io_cb(loop, w, pe->events);
}
// 更新 timeout 并继续循环
if (timeout == 0) return;
if (timeout != -1) {
// 重新计算剩余超时时间
// ...
}
}
}
关键设计 :uv__io_t 结构体中的 cb 字段就是事件回调函数指针。当 epoll_ctl 注册 fd 时,将 uv__io_t 的指针存入 epoll_event.data。当 epoll_wait 返回就绪事件时,从 data.ptr 取回 uv__io_t 指针,调用其 cb 回调。这就是 libuv 将 epoll 事件路由到具体处理函数的机制。
3.4 uv__stream_io:I/O 事件的分发器
当 epoll 报告某个 stream fd 有事件时,uv__stream_io 被调用:
c
// deps/uv/src/unix/stream.c
void uv__stream_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
uv_stream_t* stream = container_of(w, uv_stream_t, io_watcher);
// 如果是连接中的 socket,处理连接完成
if (stream->connect_req) {
uv__stream_connect(stream);
return;
}
// 可读事件或错误事件 → 调用 uv__read()
if (events & (POLLIN | POLLERR))
uv__read(stream);
// 可写事件 → 处理写队列
if (events & (POLLOUT | POLLERR | POLLHUP))
uv__write(stream);
}
3.5 uv__read:从内核读数据到用户态缓冲区
c
// deps/uv/src/unix/stream.c(关键逻辑)
static void uv__read(uv_stream_t* stream) {
uv_buf_t buf;
ssize_t nread;
int count = 32; // 防饥饿:最多连续读 32 次
while (stream->read_cb
&& (stream->flags & UV_HANDLE_READING)
&& (count-- > 0)) {
// 1. 调用 alloc_cb 让用户代码分配缓冲区
// 在 Node.js 中,这会调用到 stream_base.cc 的 OnStreamAlloc
buf = uv_buf_init(NULL, 0);
stream->alloc_cb((uv_handle_t*)stream, 64 * 1024, &buf);
// 2. 系统调用 read() ------ 从内核 socket 接收缓冲区拷贝数据到用户缓冲区
// 如果内核缓冲区为空且 fd 是非阻塞的,返回 EAGAIN
// 这就是为什么需要 epoll 先告诉我们"有数据"再调 read
do {
nread = read(uv__stream_fd(stream), buf.base, buf.len);
} while (nread < 0 && errno == EINTR);
if (nread < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 内核缓冲区已空,重新注册 POLLIN 等待下次事件
uv__io_start(stream->loop, &stream->io_watcher, POLLIN);
// nread=0 通知用户"暂时没有数据"
stream->read_cb(stream, 0, &buf);
} else {
// 真正的错误
stream->read_cb(stream, UV__ERR(errno), &buf);
}
return;
} else if (nread == 0) {
// EOF:对端关闭了连接
uv__stream_eof(stream, &buf);
return;
} else {
// 3. 成功读到数据!调用 read_cb 通知用户
stream->read_cb(stream, nread, &buf);
// 如果没有读满缓冲区,说明内核缓冲区已空,直接返回
// 等下次 epoll 事件再读
if (nread < buf.len)
return;
}
}
}
防饥饿设计 :count = 32 限制了单次 epoll 事件中连续 read 的次数。如果数据到达速度极快(如本地回环),防止一个 stream 独占事件循环。
4. 事件循环:单线程的调度中枢
4.1 事件循环的 C++ 驱动
Node.js 的事件循环由 SpinEventLoopInternal() 驱动:
cpp
// src/api/embed_helpers.cc
Maybe<ExitCode> SpinEventLoopInternal(Environment* env) {
// ...
do {
// 1. 运行 libuv 事件循环(阻塞直到没有活跃 handle 或超时)
uv_run(env->event_loop(), UV_RUN_DEFAULT);
// 2. 排空 V8 平台的后台任务(如 GC 任务)
platform->DrainTasks(isolate);
// 3. 检查是否还有活跃的 handle/request
more = uv_loop_alive(env->event_loop());
if (more && !env->is_stopping()) continue;
// 4. 触发 process.emit('beforeExit')
if (EmitProcessBeforeExit(env).IsNothing())
break;
// 5. beforeExit 回调可能注册了新的 handle,重新检查
more = uv_loop_alive(env->event_loop());
} while (more == true && !env->is_stopping());
// ...
}
4.2 libuv 的 uv_run 六阶段
c
// deps/uv/src/unix/core.c(简化)
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
for (;;) {
// 阶段 0: 更新逻辑时间
uv__update_time(loop);
// 阶段 1: Timers ------ 执行到期的 setTimeout/setInterval 回调
uv__run_timers(loop);
// 阶段 2: Pending callbacks ------ 执行上一轮被推迟的 I/O 回调
// (通常是系统限制导致的,如 EMFILE)
uv__run_pending(loop);
// 阶段 3: Idle/Prepare(内部使用,JS 层不直接暴露)
uv__run_idle(loop);
uv__run_prepare(loop);
// 阶段 4: Poll ------ 核心阶段!调用 uv__io_poll() 等待 I/O 事件
// 如果有 I/O 事件就绪,在这里触发对应的回调
// 如果没有事件且 timeout > 0,阻塞等待
uv__io_poll(loop, loop->backend_timeout);
// 阶段 5: Check ------ 执行 setImmediate 回调
uv__run_check(loop);
// 阶段 6: Close callbacks ------ 执行 close 事件的回调
uv__run_close(loop);
}
}
4.3 InternalCallbackScope:微任务排空机制
每次 C++ 层调用 JS 回调时,都会创建 InternalCallbackScope。其析构函数负责排空微任务队列和 nextTick 队列:
cpp
// src/api/callback.cc
void InternalCallbackScope::Close() {
if (closed_) return;
closed_ = true;
// 1. 触发 async_hooks 的 after 钩子
if (!failed_ && async_context_.async_id != 0 && !skip_hooks_) {
AsyncWrap::EmitAfter(env_, async_context_.async_id);
}
// 2. 弹出 async context 栈
if (pushed_ids_) {
env_->async_hooks()->pop_async_context(async_context_.async_id);
}
if (failed_) return;
// 3. 只在最外层 scope 排空任务队列(嵌套调用时跳过)
if (env_->async_callback_scope_depth() > 1 || skip_task_queues_) {
return;
}
// 4. 排空 V8 微任务队列(Promise.then 回调)
Local<Context> context = env_->context();
if (!tick_info->has_tick_scheduled()) {
context->GetMicrotaskQueue()->PerformCheckpoint(isolate);
}
// 5. 排空 process.nextTick 队列 + 其他内部微任务
if (!tick_info->has_tick_scheduled() && !tick_info->has_rejection_to_warn()) {
return;
}
Local<Function> tick_callback = env_->tick_callback_function();
tick_callback->Call(context, process, 0, nullptr);
}
关键洞察 :Promise 微任务和 nextTick 队列的排空发生在每个回调之后 ,而不是等到事件循环的某个阶段。这意味着在一个回调中 Promise.resolve().then() 注册的回调会在下一个回调之前执行。
5. 完整数据流:从网卡中断到 JS 回调
这是本文的核心章节。我们以一个 TCP 服务端接收数据为例,追踪一个 TCP 数据包从到达网卡到触发 JS data 事件的完整路径。
5.1 第一层:JS 层 --- net.createServer()
javascript
// 用户代码
const server = net.createServer((socket) => {
socket.on('data', (chunk) => {
console.log(chunk.toString());
});
});
server.listen(3000);
server.listen(3000) 的调用链:
javascript
// lib/net.js --- Server.prototype.listen()
// → setupListenHandle() 被调用
function setupListenHandle(address, port, addressType, backlog, fd, flags) {
// 1. 创建 C++ 层的 TCP handle
// new TCP(TCPConstants.SERVER) 调用 tcp_wrap.cc 的 TCPWrap::New()
this._handle = new TCP(TCPConstants.SERVER);
// 2. 绑定地址
this._handle.bind(address, port, flags);
// 3. 注册连接回调 ------ 当有新连接时,C++ 层调用这个函数
this._handle.onconnection = onconnection;
// 4. 开始监听 ------ 调用 libuv 的 uv_listen()
// 内部调用 listen() 系统调用,将 fd 加入 epoll
this._handle.listen(backlog || 511);
}
5.2 第二层:C++ Binding 层 --- TCPWrap
cpp
// src/tcp_wrap.cc
void TCPWrap::New(const FunctionCallbackInfo<Value>& args) {
// 创建 TCPWrap 对象,内部调用 uv_tcp_init()
// uv_tcp_init() 初始化 uv_tcp_t 结构体,设置 type = UV_TCP
new TCPWrap(env, args.This(), type);
}
// TCPWrap 构造函数
TCPWrap::TCPWrap(Environment* env, Local<Object> object, int type)
: ConnectionWrap(env, object, ProviderType::PROVIDER_TCPWRAP) {
// 调用 libuv 初始化 TCP handle
int r = uv_tcp_init(env->event_loop(), handle());
// handle() 返回 uv_tcp_t* 指针
}
void TCPWrap::Listen(const FunctionCallbackInfo<Value>& args) {
TCPWrap* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This());
int backlog = args[0]->Int32Value(context).FromJust();
// 调用 libuv 的 uv_listen
// 第二个参数 OnConnection 是 C++ 函数指针
// 当有新连接时,libuv 会调用这个回调
int err = uv_listen(
reinterpret_cast<uv_stream_t*>(wrap->handle()),
backlog,
OnConnection // ← 连接回调
);
}
5.3 第三层:libuv --- uv_listen 与 uv__server_io
c
// deps/uv/src/unix/stream.c
int uv_listen(uv_stream_t* stream, int backlog, uv_connection_cb cb) {
stream->connection_cb = cb; // 保存回调
// 开始监听
// 1. 调用 listen() 系统调用
// 2. 将 server fd 注册到 epoll,监听 POLLIN(新连接到达)
uv__io_start(stream->loop, &stream->io_watcher, POLLIN);
return uv__stream_listen(stream, backlog);
}
// 当 epoll 报告 server fd 可读(有新连接),uv__stream_io 被调用
// 对于 server socket,它走的是 connection 分支
void uv__server_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
uv_stream_t* stream = container_of(w, uv_stream_t, io_watcher);
// 调用 uv_accept 接受连接
// 注意:uv_accept 本身是非阻塞的,它只是从内核的
// 已完成连接队列(accept queue)中取出一个连接
stream->connection_cb(stream, 0);
}
5.4 第四层:C++ Binding --- OnConnection 处理新连接
cpp
// src/connection_wrap.cc
template <typename WrapType, typename UVType>
void ConnectionWrap<WrapType, UVType>::OnConnection(uv_stream_t* handle,
int status) {
WrapType* wrap_data = static_cast<WrapType*>(handle->data);
Environment* env = wrap_data->env();
HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());
if (status == 0) {
// 1. 创建客户端 TCPWrap 对象
// 内部调用 uv_tcp_init() 创建新的 uv_tcp_t
Local<Object> client_obj;
WrapType::Instantiate(env, wrap_data, WrapType::SOCKET)
.ToLocal(&client_obj);
// 2. 从内核接受连接
// uv_accept() 调用 accept4() 系统调用
// 从内核的已完成连接队列中取出新连接,得到新的 client fd
WrapType* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, client_obj);
uv_stream_t* client = reinterpret_cast<uv_stream_t*>(&wrap->handle_);
if (uv_accept(handle, client))
return; // accept 失败(EAGAIN),等下次
client_handle = client_obj;
}
// 3. 调用 JS 层的 onconnection 回调
Local<Value> argv[] = { Integer::New(env->isolate(), status), client_handle };
wrap_data->MakeCallback(env->onconnection_string(), arraysize(argv), argv);
}
5.5 第五层:JS 层 --- onconnection 创建 Socket
javascript
// lib/net.js
function onconnection(err, clientHandle) {
const handle = this;
const self = handle[owner_symbol]; // Server 实例
// 1. 为新连接创建 Socket 对象
const socket = new Socket({
handle: clientHandle, // C++ 层的 TCPWrap
allowHalfOpen: self.allowHalfOpen,
readable: true,
writable: true,
});
// 2. 在 initSocketHandle 中注册数据读取回调
// self._handle.onread = onStreamRead;
// 这行代码将 onStreamRead 函数存入 C++ handle 的内部字段
// 当有数据可读时,C++ 层会调用这个函数
// 3. 开始读取数据
// 内部调用 handle.readStart()
// → StreamBase::ReadStartJS() → uv_read_start()
// → 将 client fd 注册到 epoll,监听 POLLIN
// 4. 触发 'connection' 事件
self.emit('connection', socket);
}
5.6 第六层:数据到达 --- 从 read() 到 JS 回调
当 client fd 上有数据到达时,完整的调用链:
scss
epoll_wait() 返回 client fd 的 POLLIN 事件
→ uv__io_cb() = uv__stream_io()
→ uv__read(stream)
→ alloc_cb() 分配缓冲区(64KB)
→ read(fd, buf, len) 系统调用,从内核拷贝数据
→ read_cb(stream, nread, &buf)
→ EmitToJSStreamListener::OnStreamRead()
cpp
// src/stream_base.cc --- C++ 层接收数据并转入 JS
void EmitToJSStreamListener::OnStreamRead(ssize_t nread, const uv_buf_t& buf_) {
StreamBase* stream = static_cast<StreamBase*>(stream_);
Environment* env = stream->stream_env();
HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());
// 将 uv_buf_t 转为 V8 ArrayBuffer
std::unique_ptr<BackingStore> bs = env->release_managed_buffer(buf_);
if (nread <= 0) {
if (nread < 0)
stream->CallJSOnreadMethod(nread, Local<ArrayBuffer>());
return;
}
// 创建 ArrayBuffer 并拷贝数据
// 注意:这里有一次内存拷贝(从 uv_buf_t 到 ArrayBuffer)
// 对于 userBuffer 模式可以避免这次拷贝
stream->CallJSOnreadMethod(nread, ArrayBuffer::New(isolate, std::move(bs)));
}
CallJSOnreadMethod 调用 JS 层的 onStreamRead:
cpp
// src/stream_base.cc
MaybeLocal<Value> StreamBase::CallJSOnreadMethod(
ssize_t nread, Local<ArrayBuffer> ab, size_t offset, ...) {
// 将 nread 和 offset 存入 stream_base_state 数组
// 这是 C++ → JS 的高效数据传递方式(避免创建 JS 对象)
env->stream_base_state()[kReadBytesOrError] = static_cast<int32_t>(nread);
env->stream_base_state()[kArrayBufferOffset] = offset;
// 从 handle 的内部字段取出 onread 函数
Local<Value> onread = wrap->object()
->GetInternalField(StreamBase::kOnReadFunctionField)
.As<Value>();
// 调用 JS 函数 onStreamRead(arrayBuffer)
return wrap->MakeCallback(onread.As<Function>(), 1, &ab);
}
5.7 第七层:JS 层 --- onStreamRead 触发 'data' 事件
javascript
// lib/internal/stream_base_commons.js
function onStreamRead(arrayBuffer) {
// 从 C++ 层传递的 stream_base_state 获取读取结果
const nread = streamBaseState[kReadBytesOrError];
const stream = this[owner_symbol]; // Socket 实例
stream[kUpdateTimer](); // 重置超时计时器
if (nread > 0 && !stream.destroyed) {
// 创建 Buffer 视图(零拷贝!ArrayBuffer 的 slice)
const offset = streamBaseState[kArrayBufferOffset];
const buf = new FastBuffer(arrayBuffer, offset, nread);
// push 到 Readable stream 的内部缓冲区
// 如果缓冲区未满,返回 true;否则返回 false
const result = stream.push(buf);
if (!result) {
// 背压(backpressure):消费速度跟不上生产速度
// 暂停读取,从 epoll 中移除 POLLIN 监视
handle.reading = false;
handle.readStop();
}
// push() 内部触发 'readable' 事件或 'data' 事件
// 用户的 socket.on('data', ...) 回调在此被调用
}
if (nread === UV_EOF) {
// 对端关闭连接,推送 null 标记流结束
stream.push(null);
}
}
5.8 完整调用链总结
scss
[内核层]
网卡收到 TCP 数据包
→ 硬中断 → NAPI 轮询
→ 数据拷贝到 socket 接收缓冲区
→ sk_data_ready() 唤醒等待队列
→ ep_poll_callback() 将 epitem 挂入就绪链表
→ 唤醒 epoll_wait()
[libuv 层]
epoll_wait() 返回就绪事件
→ uv__io_cb() = uv__stream_io()
→ uv__read()
→ alloc_cb() 分配 64KB 缓冲区
→ read(fd, buf, 65536) 系统调用
→ read_cb() 回调
[C++ Binding 层]
EmitToJSStreamListener::OnStreamRead()
→ StreamBase::CallJSOnreadMethod()
→ 设置 stream_base_state[kReadBytesOrError]
→ 取出 InternalField 中的 onread 函数
→ MakeCallback() 调用 JS
[JS 层]
onStreamRead(arrayBuffer)
→ new FastBuffer(arrayBuffer, offset, nread) // 零拷贝 Buffer
→ stream.push(buf)
→ 触发 'data' 事件
→ 用户回调 socket.on('data', ...) 执行
[回调返回后]
InternalCallbackScope::~InternalCallbackScope()
→ Close()
→ PerformCheckpoint() 排空 Promise 微任务
→ tick_callback->Call() 排空 nextTick 队列
6. 线程池:无法非阻塞时的兜底方案
6.1 为什么需要线程池
不是所有操作都能通过 epoll 实现异步。以下操作没有对应的非阻塞系统调用:
- 文件 I/O :
open(),read(),write(),close()--- Linux 上无法对普通文件设置 O_NONBLOCK 并真正异步 - DNS 解析 :
getaddrinfo()--- 阻塞的系统调用 fs.stat(),fs.rename()等文件系统操作crypto.pbkdf2()--- CPU 密集型计算
这些操作被委派给 libuv 的线程池。
6.2 线程池的完整实现
c
// deps/uv/src/threadpool.c
// 全局状态
static uv_cond_t cond; // 条件变量
static uv_mutex_t mutex; // 全局互斥锁
static unsigned int idle_threads; // 空闲线程数
static unsigned int nthreads; // 线程总数
static uv_thread_t* threads; // 线程数组
static uv_thread_t default_threads[4]; // 默认 4 个线程的栈上存储
static struct uv__queue wq; // 工作队列
static struct uv__queue slow_io_pending_wq; // 慢 I/O 专用队列
// 线程池初始化
static void init_threads(void) {
nthreads = 4; // 默认 4 个线程
// 读取环境变量 UV_THREADPOOL_SIZE
char buf[16];
err = uv_os_getenv("UV_THREADPOOL_SIZE", buf, &buflen);
if (err == 0)
nthreads = atoi(buf);
if (nthreads == 0) nthreads = 1;
if (nthreads > 1024) nthreads = 1024; // 最大 1024
// 创建线程,每个线程 8MB 栈空间
config.stack_size = 8u << 20; // 8 MB
for (i = 0; i < nthreads; i++)
uv_thread_create_ex(threads + i, &config, worker, &sem);
}
6.3 worker 线程的主循环
c
// deps/uv/src/threadpool.c
static void worker(void* arg) {
uv_thread_setname("libuv-worker");
uv_sem_post((uv_sem_t*)arg); // 通知主线程:我已启动
uv_mutex_lock(&mutex);
for (;;) {
// 等待工作:如果队列为空,睡眠在条件变量上
while (uv__queue_empty(&wq) ||
(uv__queue_head(&wq) == &run_slow_work_message &&
slow_io_work_running >= slow_work_thread_threshold())) {
idle_threads += 1;
uv_cond_wait(&cond, &mutex); // 阻塞等待
idle_threads -= 1;
}
// 取出工作
q = uv__queue_head(&wq);
if (q == &exit_message) break; // 退出信号
uv__queue_remove(q);
uv_mutex_unlock(&mutex);
// 执行工作函数(在线程池中执行,不在主线程)
w = uv__queue_data(q, struct uv__work, wq);
w->work(w); // ← 这里是实际的文件 I/O 或 DNS 查询
// 工作完成,将结果放回主循环的完成队列
uv_mutex_lock(&w->loop->wq_mutex);
w->work = NULL; // 标记为已完成
uv__queue_insert_tail(&w->loop->wq, &w->wq);
// 通过 async handle 通知主循环
uv_async_send(&w->loop->wq_async);
uv_mutex_unlock(&w->loop->wq_mutex);
uv_mutex_lock(&mutex);
}
}
6.4 完成回调如何回到主线程
线程池中的工作完成后,通过 uv_async_send() 通知主循环:
c
// deps/uv/src/threadpool.c
void uv__work_done(uv_async_t* handle) {
uv_loop_t* loop = container_of(handle, uv_loop_t, wq_async);
// 将完成队列中的工作全部取出
uv_mutex_lock(&loop->wq_mutex);
uv__queue_move(&loop->wq, &wq);
uv_mutex_unlock(&loop->wq_mutex);
// 在主线程中调用 done 回调
while (!uv__queue_empty(&wq)) {
q = uv__queue_head(&wq);
uv__queue_remove(q);
w = container_of(q, struct uv__work, wq);
w->done(w, err); // ← 在主线程中执行!
}
}
uv_async_send() 的本质是向 eventfd(Linux)或 self-pipe(其他 Unix)写入一个字节,使 epoll_wait() 立即返回。这样主循环的 poll 阶段就能检测到线程池的完成事件。
6.5 慢 I/O 保护机制
libuv 对文件系统 I/O 有特殊的"慢 I/O"保护:
c
// 慢 I/O 线程阈值 = (nthreads + 1) / 2
// 例如 4 线程时,最多 2 个线程同时执行慢 I/O
static unsigned int slow_work_thread_threshold(void) {
return (nthreads + 1) / 2;
}
这确保了即使有大量慢速文件 I/O,线程池中仍有线程可以处理快速任务(如 DNS 查询),避免整个线程池被文件 I/O 阻塞。
7. 突破单线程:Worker Threads 与 Cluster
7.1 Worker Threads:共享进程的线程
javascript
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
const worker = new Worker('./worker.js');
worker.on('message', (msg) => console.log(msg));
worker.postMessage('hello');
} else {
parentPort.on('message', (msg) => {
parentPort.postMessage(`received: ${msg}`);
});
}
每个 Worker:
- 拥有独立的 V8 Isolate(独立的 JS 堆)
- 拥有独立的 libuv 事件循环
- 拥有独立的 eventpoll 实例
- 通过
MessagePort(基于共享内存 + Atomics)与主线程通信
7.2 Cluster:多进程模型
javascript
const cluster = require('cluster');
const os = require('os');
if (cluster.isPrimary) {
// 创建与 CPU 核心数相同的 Worker 进程
for (let i = 0; i < os.cpus().length; i++) {
cluster.fork();
}
} else {
require('./app').listen(3000);
}
Cluster 的底层是 child_process.fork(),每个 Worker 进程:
- 拥有独立的进程空间(独立的 V8、独立的 libuv、独立的 epoll 实例)
- 通过 IPC 管道(Unix Domain Socket 或 TCP)与 Primary 通信
- Primary 通过
SO_REUSEPORT(Linux 3.9+)或轮询分发将连接分配给 Worker
7.3 三种并发模型对比
| 模型 | 隔离级别 | 内存开销 | 通信方式 | 适用场景 |
|---|---|---|---|---|
| 事件循环 | 无隔离(单线程) | 最低 | 直接函数调用 | I/O 密集型 |
| Worker Threads | 线程级(共享进程) | 中等(每 Worker ~10MB) | MessagePort(共享内存) | CPU 密集型 |
| Cluster | 进程级(完全隔离) | 最高(每 Worker ~50MB+) | IPC 管道 | 高可用 + 充分利用多核 |
8. io_uring:下一代异步 I/O
8.1 io_uring 相比 epoll 的突破
epoll 的根本限制:每次 I/O 操作仍需两次系统调用(epoll_wait + read/write)。
io_uring 的突破:提交和完成都通过共享的环形缓冲区(ring buffer),可以将多个 I/O 操作批量提交,无需每次都进入内核。
scss
epoll 模型: epoll_wait() → read() → epoll_wait() → read() → ...
每次 I/O 需要 2 次系统调用
io_uring 模型:io_uring_enter() 批量提交 N 个 I/O
io_uring_enter() 或 busy-poll 批量获取完成事件
N 个 I/O 只需 2 次系统调用
8.2 libuv 中的 io_uring 集成
libuv 并未用 io_uring 完全替代 epoll,而是在特定场景利用 io_uring 优化:
c
// deps/uv/src/unix/linux.c --- uv__io_poll 中
// 当需要批量注册/注销 fd 到 epoll 时(如大量连接同时建立/关闭)
// 使用 io_uring 批量提交 epoll_ctl 请求
if (loop->flags & UV_LOOP_ENABLE_IO_URING_SQE) {
// 批量 epoll_ctl 优化
// 将多个 EPOLL_CTL_ADD/MOD/DEL 合并为一次 io_uring_enter
}
此外,libuv 正在探索将文件 I/O 通过 io_uring 实现真正的异步(而非线程池),这将消除文件 I/O 对线程池的依赖。
总结:单线程高并发的本质
Node.js 单线程能处理高并发的根本原因:
-
内核提供了 I/O 多路复用(epoll/kqueue/IOCP):一个系统调用可以同时监视数万个 fd,当任意 fd 就绪时立即通知。
-
非阻塞 I/O + 事件驱动 :所有网络 I/O 都是非阻塞的。
read()在没有数据时立即返回 EAGAIN,不会阻塞线程。epoll 告诉我们"谁准备好了",然后才去读。 -
事件循环是调度中枢 :
uv_run()不断循环"等待事件 → 触发回调",确保单线程能高效处理大量并发事件。 -
回调/Promise 是编程模型:用户代码通过回调或 async/await 注册"当数据到达时做什么",事件循环负责在正确的时间调用这些回调。
-
线程池兜底:对于无法非阻塞的操作(文件 I/O、DNS),线程池在后台执行,完成后通过 async handle 通知主循环。
-
多进程/多线程扩展:Cluster 和 Worker Threads 允许利用多核 CPU,每个核心运行独立的事件循环。
一句话总结:Node.js 的单线程不是"只有一个线程",而是"只有一个线程在执行 JS 代码"。在这个线程的背后,是内核的 epoll 在高效地等待 I/O 事件,是 libuv 在将内核事件路由到 JS 回调,是线程池在后台处理无法非阻塞的操作。