Node.js 单线程高并发:从内核中断到 JS 回调的完整技术栈

核心命题:一个线程如何同时处理数万并发连接?答案不在 Node.js 本身,而在操作系统内核的 I/O 多路复用机制。Node.js 所做的,是构建一条从内核事件到用户态回调的完整链路。


目录

  1. 问题的本质:阻塞是万恶之源
  2. [内核基石: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")
  3. [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")
  4. 事件循环:单线程的调度中枢
  5. [完整数据流:从网卡中断到 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")
  6. 线程池:无法非阻塞时的兜底方案
  7. [突破单线程: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")
  8. [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/Oopen(), 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 单线程能处理高并发的根本原因:

  1. 内核提供了 I/O 多路复用(epoll/kqueue/IOCP):一个系统调用可以同时监视数万个 fd,当任意 fd 就绪时立即通知。

  2. 非阻塞 I/O + 事件驱动 :所有网络 I/O 都是非阻塞的。read() 在没有数据时立即返回 EAGAIN,不会阻塞线程。epoll 告诉我们"谁准备好了",然后才去读。

  3. 事件循环是调度中枢uv_run() 不断循环"等待事件 → 触发回调",确保单线程能高效处理大量并发事件。

  4. 回调/Promise 是编程模型:用户代码通过回调或 async/await 注册"当数据到达时做什么",事件循环负责在正确的时间调用这些回调。

  5. 线程池兜底:对于无法非阻塞的操作(文件 I/O、DNS),线程池在后台执行,完成后通过 async handle 通知主循环。

  6. 多进程/多线程扩展:Cluster 和 Worker Threads 允许利用多核 CPU,每个核心运行独立的事件循环。

一句话总结:Node.js 的单线程不是"只有一个线程",而是"只有一个线程在执行 JS 代码"。在这个线程的背后,是内核的 epoll 在高效地等待 I/O 事件,是 libuv 在将内核事件路由到 JS 回调,是线程池在后台处理无法非阻塞的操作。

相关推荐
万岳科技程序员小赵18 小时前
同城外卖系统开发:用户端、商家端、骑手端业务协同与源码架构解析
架构·同城外卖系统源码·同城外卖系统开发·同城外卖小程序
光锥智能19 小时前
原生统一架构打通底层技术壁垒 Kairos 3.1 定义具身世界模型新范式
架构
●VON19 小时前
鸿蒙 PC Markdown 编辑器换行兼容:LF、CRLF 与混合换行归一化
华为·架构·编辑器·harmonyos·鸿蒙
熊猫钓鱼>_>20 小时前
ArkTS 方舟编程语言 · 原创快速入门教程
运维·架构·ts·harmonyos·arkts·鸿蒙·js
大龄秃头程序员20 小时前
SwiftUI 实战:从零构建双引擎 LLM 聊天客户端(Ollama + DeepSeek)
架构
●VON21 小时前
鸿蒙 PC Markdown 编辑器错误处理:让失败可恢复而不是只弹提示
华为·架构·编辑器·harmonyos·鸿蒙
heimeiyingwang21 小时前
【架构实战】CI/CD流水线:从手动部署到一键上线
ci/cd·架构
中微极客1 天前
视频Agent:从一次性生成到多轮编排架构
人工智能·架构·音视频
●VON1 天前
鸿蒙 PC Markdown 编辑器质量工程:证据驱动的技术验证
华为·架构·编辑器·harmonyos·鸿蒙