libuv流程

考虑到系统性能的问题,现在很多流程都使用了异步机制,这样可以更快的返回去做另外的事情,但是异步也存在一个很大的问题,就是不知道何时会完成之前的事情,很多流程就变得不够可控,现在在项目中就碰到过因为此问题而产生的bug,因为不可控,所以多线程之间资源的互斥和保护就变的异常重要,否则一不小心就会因为修改了本部应该修改的线程或进程资源而导致系统crush掉。

现在使用的是libuv,它是一个第三方的异步机制的开源库。接触了有一段时间了,接下来把我从这开源代码的流程和感想写一下。

首先uv库中充满了回调函数,而且很多都回调函数里面再调用回调函数,uv库中核心的结构体是uv_loop_t

 // uv_loop_t
  
  struct uv_loop_s {
    /* User data - use this for whatever. */
    void* data;
    /* Loop reference counting. */
    uint32_t active_handles; 
    void* handles_queue[2];
    void* active_reqs[2];
    /* Internal flag to signal loop stop. */
    uint32_t stop_flag;
    /* platform dependent fields */
    UV_LOOP_PRIVATE_FIELDS
  };

在真正使用该变量时需要调用uv_loop_init去初始化部分的成员变量,再去调用uv_run,然后uv库就会自己去做别人给他注册的回调函数了,从我们的角度来看就是就去调用本地文件接口或者网络流接口就行了。

接下来主要说一下,uv_run里面的最重要的函数uv__io_poll

void uv__io_poll(uv_loop_t* loop, int timeout) {
   struct pollfd pfd;
   struct pollfd* pe;
   QUEUE* q;
   uv__io_t* w;
   uint64_t base;
   uint64_t diff;
   int nevents;
   int count;
   int nfd;
   int i;
     
   if (loop->nfds == 0) {
     assert(QUEUE_EMPTY(&loop->watcher_queue));
     return;
   }
 
   while (!QUEUE_EMPTY(&loop->watcher_queue)) {
     q = QUEUE_HEAD(&loop->watcher_queue);
     QUEUE_REMOVE(q);
     QUEUE_INIT(q);
 
     w = QUEUE_DATA(q, uv__io_t, watcher_queue);
     assert(w->pevents != 0);
     assert(w->fd >= 0);
     assert(w->fd < (int)loop->nwatchers);
 
     pfd.fd = w->fd;
     pfd.events = w->pevents;
     uv__add_pollfd(loop, &pfd);
    w->events = w->pevents;
   }
 
   assert(timeout >= -1);
   base = uv_now(loop);
   count = 5;
 
   for (;;) {
     nfd = poll(loop->pollfds, loop->npollfds, timeout);
 
     SAVE_ERRNO(uv_update_time(loop));
 
     if (nfd == 0) {
       assert(timeout != -1);
       return;
     }
 
     if (nfd == -1) {
       int err = get_errno();
       if (err == EAGAIN ) {
         set_errno(0);
       }
       else if ( err != EINTR) {
         TDLOG("uv__io_poll abort for errno(%d)", err);
         ABORT();
       }
       if (timeout == -1) {
         continue;
       }
       if (timeout == 0) {
        return;
       }
       goto update_timeout;
     }
 
     nevents = 0;
 
     for (i = 0; i < loop->npollfds; ++i) {
       pe = &loop->pollfds[i];
 
       if (pe->fd < 0)
         continue;
       w = loop->watchers[pe->fd];
 
       if (!(pe->revents & (UV__POLLIN | UV__POLLOUT | UV__POLLHUP)))
         continue;
 
       //w = loop->watchers[pe->fd];
       if (w) {
         w->cb(loop, w, pe->revents);
         ++nevents;
       }
       else {
         pe->fd = -1;
       }
     }
 
     uv__cleanup_pollfds(loop);
    if (nevents != 0) {
       if (--count != 0) {
         timeout = 0;
         continue;
       }
       return;
     }
     if (timeout == 0) {
       return;
     }
     if (timeout == -1) {
       continue;
     }
 update_timeout:
     assert(timeout > 0);
 
     diff = uv_now(loop) - base;
     if (diff >= (uint64_t)timeout) {
       return;
     }
     timeout -= diff;
   }
 }

实际上该函数就是使用了poll,只要有本地数据,网络数据可读或者可写,就会直接去执行callback函数。需要注意的是如果调用了uv_async_send函数,也会在poll中成功返回。

w->cb中的回调函数主要是通过void uv__io_init(uv__io_t* w, uv__io_cb cb, int fd)函数去注册,uv库里面与三个注册回调函数:

1. uv__io_init(&wa->io_watcher, uv__async_io, pipefd[0]);看接口也知道,最后一个参数是文件描述符fd,而该文件描述符是管道,如果要在poll中有数据可读写,直接向pipefd[1]操作即可。该回调函数是也是三个回调函数中比较重要的函数。

static void uv__async_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
   struct uv__async* wa;
   char buf[1024];
   unsigned n;
   ssize_t r;
   int err;
 
   n = 0;
   for (;;) {
     r = read(w->fd, buf, sizeof(buf));
 
     if (r > 0)
       n += r;
 
     if (r == sizeof(buf))
       continue;
 
     if (r != -1)
       break;
 
     err = get_errno();
     if (err == EAGAIN || err == EWOULDBLOCK)
       break;
     err = get_errno();
     if (err == EAGAIN || err == EWOULDBLOCK)
       break;
 
     if (err == EINTR)
       continue;
 
     TDLOG("uv__async_io abort for errno(%d)", err);
     ABORT();
   }
 
   wa = container_of(w, struct uv__async, io_watcher);
 
 #if defined(__linux__)
   if (wa->wfd == -1) {
     uint64_t val;
     assert(n == sizeof(val));
     memcpy(&val, buf, sizeof(val));  /* Avoid alignment issues. */
     wa->cb(loop, wa, val);
     return;
   }
 #endif
   //LOGD("uv async", "get send event true");
   wa->cb(loop, wa, n);
 }
最终的核心,该函数还是会去调用注册好的回调函数static void uv__async_event(uv_loop_t* loop, struct uv__async* w, unsigned int nevents);

该函数还是调用回调函数,之后就是各自的回调函数不再统一了。

都是通过int uv_async_init(uv_loop_t* loop, uv_async_t* handle, uv_async_cb async_cb)函数注册的。

NOTE:需要注意的是该函数在uv自己体系里面会去初始化注册一部分自己的回调函数,同时我们自己编写的代码也可能会去注册自己的回调函数,这两者之间的执行先后顺序是不一定的。最终是int uv_async_send(uv_async_t* handle)来触发自己相应的回调函数去执行,所以需要注意的是注册的各个回调函数共用的全局变量被同时使用导致内核crush的问题。我在做项目的过程中就遇到了类似的问题。(主要是在使用uv本地文件读写的接口时有使用到)。

2. uv__io_init(&handle->io_watcher, uv__poll_io, fd);

3.uv__io_init(&stream->io_watcher, uv__stream_io, -1);该注册的回调函数主要是个网络流使用的,一开始最后一个参数为-1,当创建socket成功之后,就会被重新赋值。


因为我这里接触本地文件读的情况比较多,所以写一下uv_fs_read的详细流程

int uv_fs_read(uv_loop_t* loop, uv_fs_t* req,
                uv_file file,
                const uv_buf_t bufs[],
                unsigned int nbufs,
                int64_t off,
                uv_fs_cb cb) {
   INIT(READ);
   req->file = file;
 
   req->nbufs = nbufs;
   req->bufs = req->bufsml;
   if (nbufs > ARRAY_SIZE(req->bufsml))
     req->bufs = (uv_buf_t*)malloc(nbufs * sizeof(*bufs));
 
   if (req->bufs == NULL)
     return -ENOMEM;
 
   memcpy(req->bufs, bufs, nbufs * sizeof(*bufs));
 
   req->off = off;
   POST;
 }
一开始就是初始化一些结构体变量,表明这是UV_FS类型的req,当然文件相关的fd,读取的buffer地址和长度也需要告知。

实际上关键的是POST函数

#define POST                                                                  \
   do {                                                                        \
     if ((cb) != NULL) {                                                       \
       uv__work_submit((loop), &(req)->work_req, uv__fs_work, uv__fs_done);    \
       return 0;                                                               \
     }                                                                         \
     else {                                                                    \
       uv__fs_work(&(req)->work_req);                                          \
       uv__fs_done(&(req)->work_req, 0);                                       \
       return (req)->result;                                                   \
     }                                                                         \
   }                                                                           \
   while (0)
如果是使用同步接口的话,就直接执行else分支,如果使用异步接口的话就使用上面的分支,无论怎么样最终都会执行uv__fs_work和uv__fs_done这两个函数。

我们一般使用异步接口,uv__work_submit函数实际上就是插入队列中,其中之前说过的会有两个线程,线程做的就是不断地从插入的队列中取出来,然后执行相应的回调函数。

static void post(QUEUE* q) {
   uv_mutex_lock(&_mutex);
   QUEUE_INSERT_TAIL(&_wq, q);
   uv_mutex_unlock(&_mutex);
 
   uv_cond_signal(&_cond);
 }
这个是插入队列并且发送线程信号通知worker线程去取队列里面的req。

static void worker(void* arg) {
   struct uv__work* w;
   QUEUE* q;
 
   (void) arg;
 
   for (;;) {
     uv_mutex_lock(&_mutex);
 
     while (QUEUE_EMPTY(&_wq) && QUEUE_EMPTY(&_wq_low)) {
       uv_cond_wait(&_cond, &_mutex);
     }
 
     if (!QUEUE_EMPTY(&_wq))
       q = QUEUE_HEAD(&_wq);
     else
       q = QUEUE_HEAD(&_wq_low);
 
     if (q == &_exit_message)
       uv_cond_signal(&_cond);
     else {
       QUEUE_REMOVE(q);
       QUEUE_INIT(q);  /* Signal uv_cancel() that the work req is
                              executing. */
     }
 
     uv_mutex_unlock(&_mutex);
 
     if (q == &_exit_message)
       break;
 
     w = QUEUE_DATA(q, struct uv__work, wq);
     w->work(w);
 
     uv_mutex_lock(&w->loop->wq_mutex);
     w->work = NULL;  /* Signal uv_cancel() that the work req is done
                         executing. */
     QUEUE_INSERT_TAIL(&w->loop->wq, &w->wq);
     uv_async_send(&w->loop->wq_async);
     uv_mutex_unlock(&w->loop->wq_mutex);
   }
 }
Note:需要注意的是uv_fs_read函数,可能会被不同的线程调用,一个是worker线程,一个自己的线程,两个线程对于全局变量内存的使用需要格外的注意!






<think>我们聚焦于Libuv库与Node.js事件循环的关系和工作原理。根据引用[1][2][4][5],Libuv是Node.js异步I/O的核心,它实现了事件循环和线程池,管理非阻塞I/O操作。###Libuv库的核心作用1.**跨平台异步I/O**:Libuv为Node.js提供了统一的异步I/O接口,封装了不同操作系统底层I/O操作的差异(如Linux的epoll,Windows的IOCP)[^4]。2.**事件循环实现**:Libuv实现了Node.js事件循环的6个阶段(Timers,Pending,Idle,Poll,Check,Close)[^1][^2]。3.**线程池管理**:对于无法异步的系统调用(如文件读写),Libuv使用线程池(默认4线程)模拟异步,避免阻塞主线程[^4][^5]。###Libuv事件循环工作原理```mermaidgraphLRA[事件循环启动]-->B[Timers阶段]B-->C[Pending阶段]C-->D[Idle/Prepare阶段]D-->E[Poll阶段]E-->F[Check阶段]F-->G[Close阶段]G-->B```1.**Timers阶段**:检查定时器(`setTimeout`/`setInterval`),执行到期回调[^1]。2.**Pending阶段**:处理操作系统遗留的待处理回调(如TCP错误)[^2]。3.**Poll阶段(核心)**:-执行I/O回调(文件、网络等)-**阻塞等待新事件**:当队列空时,根据以下条件决定下一步:-若有`setImmediate`回调→跳至Check阶段-若有Timer到期→跳至Timers阶段-否则阻塞等待I/O事件[^1][^2]4.**Check阶段**:执行`setImmediate`回调。5.**Close阶段**:处理关闭事件回调(如`socket.on('close')`)。###Libuv线程池工作机制```mermaidflowchartTDsubgraph主线程A[发起文件读取]-->|提交任务|B[Libuv线程池]B-->|完成通知|C[事件循环Poll阶段]endsubgraph线程池B-->D[线程1]B-->E[线程2]end```-**阻塞操作**:文件I/O、DNS解析等同步系统调用由线程池处理[^4]。-**非阻塞操作**:网络I/O通过操作系统提供的异步接口(如epoll)直接处理,无需线程池[^5]。###性能关键点1.**网络I/O高效性**:直接使用操作系统异步接口,零线程消耗。2.**文件I/O开销**:线程池处理文件操作,默认4线程可能成为瓶颈(可通过`UV_THREADPOOL_SIZE`调整)[^4]。###示例:Libuv如何驱动Node.js```javascript//示例1:网络请求(非阻塞,无需线程池)consthttp=require('http');http.get('http://example.com',(res)=>{//回调由Libuv在Poll阶段执行});//示例2:文件读取(使用线程池)constfs=require('fs');fs.readFile('file.txt',(err,data)=>{//回调由Libuv在Poll阶段执行(线程池处理完成后)});```###总结:Libuv与Node.js事件循环的关系-**核心引擎**:Libuv是Node.js事件循环的底层实现,提供跨平台异步能力[^4]。-**协作模式**:-V8引擎执行JavaScript代码。-Node.jsAPI层调用Libuv的异步接口。-Libuv通过事件循环调度任务,利用线程池或系统异步I/O实现非阻塞[^5]。---###相关问题1.如何调整Libuv线程池的大小?2.Libuv在不同操作系统下使用的I/O多路复用机制有何差异?3.为什么网络I/O不需要线程池而文件I/O需要?4.如何监控Libuv事件循环的延迟?5.Libuv的`uv_run`函数在Node.js启动过程中如何被调用?</think>### Libuv 库与 Node.js 事件循环工作原理详解 #### 一、Libuv 的核心作用 Libuv 是 Node.js 异步 I/O 的底层引擎,提供跨平台的事件循环实现。它的核心功能包括: 1. **事件循环驱动** 实现 Node.js 事件循环的 6 个阶段(Timers → Pending → Idle → **Poll** → Check → Close)[^1][^2] 2. **异步 I/O 抽象层** 统一封装不同操作系统的异步 API: - Linux:`epoll` - macOS:`kqueue` - Windows:`IOCP`(完成端口)[^4] 3. **线程池管理** 处理无法异步的系统调用(如文件 I/O),默认创建 4 个线程(可通过 `UV_THREADPOOL_SIZE` 调整)[^4][^5] #### 二、事件循环与 Libuv 的协作流程 ```mermaid graph LR A[JavaScript 代码] --> B[调用 Node API] B --> C{操作类型} C -->|非阻塞 I/O| D[Libuv 系统 API] C -->|阻塞 I/O| E[Libuv 线程池] D & E --> F[事件队列] F --> G[事件循环阶段调度] G --> H[执行回调] ``` 1. **阶段执行顺序** ```javascript // 验证阶段顺序的代码示例 setTimeout(() => console.log('Timeout'), 0); setImmediate(() => console.log('Immediate')); fs.readFile(__filename, () => { console.log('ReadFile'); setTimeout(() => console.log('Inner Timeout'), 0); setImmediate(() => console.log('Inner Immediate')); }); ``` **典型输出**: ``` ReadFile // Poll 阶段执行 Inner Immediate // Check 阶段优先于 Timers Inner Timeout Timeout Immediate ``` 2. **Poll 阶段的关键逻辑** - 当 Poll 队列**非空**时:同步执行所有 I/O 回调直到队列空或达到上限 - 当 Poll 队列**空**时: ```mermaid flowchart LR A[Poll 队列空] --> B{有 setImmediate?} B -->|是| C[跳至 Check 阶段] B -->|否| D{有 Timer 到期?} D -->|是| E[跳至 Timers 阶段] D -->|否| F[阻塞等待新 I/O 事件] ``` #### 三、线程池工作原理 Libuv 使用线程池处理阻塞型操作: ```mermaid flowchart TD subgraph 主线程 A[fs.readFile] --> B[提交任务到线程池] end subgraph 线程池 B --> C[线程1 读文件] B --> D[线程2 解压缩] end C & D --> E[任务完成通知] E --> F[回调加入 Poll 队列] ``` - **适用场景**:文件操作、DNS 解析、CPU 密集型任务 - **网络 I/O 例外**:直接通过操作系统异步接口处理,**不占用线程池**[^4][^5] #### 四、性能优化机制 1. **事件循环延迟监控** 通过 `monitorEventLoopDelay` 检测阻塞: ```javascript const { monitorEventLoopDelay } = require('perf_hooks'); const h = monitorEventLoopDelay(); h.enable(); // 阻塞操作后检查 console.log(h.percentile(99)); // 99% 分位的延迟 ``` 2. **避免 Poll 阶段阻塞** - 分解长任务:用 `setImmediate` 分片处理 - 优先使用流式 API(如 `fs.createReadStream`) 3. **线程池调优** ```bash UV_THREADPOOL_SIZE=8 node app.js # 增加线程数 ``` #### 五、与浏览器事件循环的差异 | **特性** | Node.js (Libuv) | 浏览器 | |------------------|-------------------------|------------------| | 微任务优先级 | `nextTick` > `Promise` | `Promise` 统一 | | I/O 处理 | 线程池 + 系统异步 | WebAPI 线程 | | 阶段划分 | 6 个明确阶段 | 宏任务/微任务 | > 关键结论:**Libuv 是 Node.js 高性能的基石**,它通过事件循环调度主线程任务,同时用线程池卸载阻塞操作,实现单线程下的高并发[^4][^5]。 --- ### 相关问题 1. 如何验证 Libuv 线程池大小对性能的影响? 2. 在哪些场景下 Libuv 会使用操作系统的原生异步 I/O(非线程池)? 3. `epoll`/`kqueue`/`IOCP` 这些系统级 I/O 机制有何区别? 4. 为什么 `setImmediate` 在 I/O 回调中总是优先于 `setTimeout` 执行? 5. 如何检测事件循环是否被长时间阻塞?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值