前言这篇文章是为了解答我在开始学习nodejs事件循环时,对自己没有理解清楚的问题,结合官方文档等资料1.poll阶段不阻塞(阻塞时间timeout为0),无限阻塞(阻塞时间timout为-1),poll回调队列的回调函数会被执行吗?1.1什么是I/O,什么是文件描述符?I/O是输入和输出的缩写。在Linux系统中,一切都被视为一个文件。文件(常规文件、套接字、FIFO、管道、终端...)是一系列二进制流。当交换信息时,我们在这些流上发送和接收数据,这就是I/O操作。当进程打开现有文件或创建新文件时,内核会向进程返回一个文件操作符。文件操作符是一个索引,是一个整数,指向系统级的文件描述表,里面包含文件操作、文件类型、访问权限等信息。所有执行I/O操作的系统调用都通过文件描述符。1.2什么是epoll?epoll是Linux内核的一种可扩展的I/O事件通知机制。在浏览器环境中,当我们要监听鼠标事件时,我们会element.addEventListener('click',cbFn),这是浏览器的事件通知机制。同样,libuv调用epoll相关的API来实现I/O事件通知。观察者(Observer)注册到观察者(Subject)。当观察者(Subject)发生变化时,会通知注册的观察者(Observer)执行回调。1.3epoll工作流程epoll分为三步:epoll_create,在epoll文件系统中建立一个文件节点,并开辟epoll自己的内核高速缓存区,构建红黑树,分配所需大小的内存对象,创建一个列表linkedlist,它存储就绪事件。epoll_ctl,把要监听的文件放到对应的红黑树上,为内核中断处理程序注册一个回调函数,通知内核,如果这个句柄的数据到达,就放入就绪链表。epoll_wait,观察就绪链表中是否有数据,提取并清除就绪链表。1.4libuv对poll阶段的实现voiduv__io_poll(uv_loop_t*loop,inttimeout){//...//如果没有观察者,直接返回if(loop->nfds==0){assert(QUEUE_EMPTY(&loop->watcher_queue));返回;}memset(&e,0,sizeof(e));//向epoll系统注册所有I/O观察器while(!QUEUE_EMPTY(&loop->watcher_queue)){//获取队列的头部,并从loop->watcher_queue中移除队列q=QUEUE_HEAD(&loop->watcher_queue);QUEUE_REMOVE(q);QUEUE_INIT(q);//获取I/O观察者结构w=QUEUE_DATA(q,uv__io_t,watcher_queue);断言(w->pevents!=0);断言(w->fd>=0);assert(w->fd<(int)loop->nwatchers);e.events=w->pevents;e.data.fd=w->fd;如果(w->events==0)op=EPOLL_CTL_ADD;否则op=EPOLL_CTL_MOD;//epoll_ctl操作,注册文件描述符和epoll监听的I/O事件断言(op==EPOLL_CTL_ADD);//loop->backend_fd通过epoll_create创建if(epoll_ctl(loop->backend_fd,EPOLL_CTL_MOD,w->fd,&e))abort();}w->events=w->pevents;}//...//记录当前时间,用于计算到达时间后,跳出下面的循环base=loop->time;//count减为0,跳出下面的循环count=48;/*基准表明这提供了最佳的吞吐量。*/real_timeout=超时;/*进入epoll_pwait轮查询I/O事件下面的循环主要是通过超时控制和计数是否跳出,符合整个事件循环*/for(;;){//...//nfds表示产生I/O事件的文件描述符个数,0因为没有事件,可能是超时时间到了,或者timeout=0//events保存从内核获取的事件集合nfds=epoll_pwait(loop->backend_fd,事件,ARRAY_SIZE(事件),超时,psigset);//。..//没有I/O事件if(nfds==0){//...//如果超时为-1,继续循环if(timeout==-1)continue;//如果超时为0,函数直接返回if(timeout==0)return;//更新下一次epoll_pwait的超时时间gotoupdate_timeout;}//epoll_pwait返回错误if(nfds==-1){如果(errno!=EINTR)abort();//如果超时为-1,继续循环if(timeout==-1)continue;//如果timeout为0,函数直接返回if(timeout==0)return;//更新下一次epoll_pwait的超时时间gotoupdate_timeout;}//...//获取I/O观察器并调用关联的回调函数for(i=0;idata.fd;//...//如果有有效事件if(pe->events!=0){if(w==&loop->signal_IOWatcher)have_signals=1;else//执行回调w->cb(loop,w,pe->events);nevents++;}}//...if(nevents!=0){//如果所有文件描述符上都有事件,且count不为0,再次循环if(nfds==ARRAY_SIZE(events)&&--count!=0){/*轮询更多事件,但这次不要阻塞。*/超时=0;继续;}返回;}//如果timeout为0,函数直接返回if(timeout==0)return;//如果timeout为-1,继续循环if(timeout==-1)continue;//重新计算timeoutupdate_timeout:assert(timeout>0);real_timeout-=(loop->time-base);如果(real_timeout<=0)返回;//剩余超时时间timeout=real_timeout;}}epoll注册I/OObserver调用epoll_ctl,注册文件描述符和需要监听的I/O事件,进入循环,调用epoll_pwait轮询I/O事件3.1如果没有I/O事件,则超时0,则直接退出轮询,超时为-1,则继续轮询3.2如果epoll_pwait返回错误,超时为0,则直接退出轮询,超时为-1,则继续轮询3.3如果有I/O事件,调用关联的回调函数3.4如果timeout为0,则直接退出轮询3.5如果timeout为-1,继续轮询所以,当阻塞时间timeout为0时,如果有I/O事件,执行回调,然后进入nextstage,如果没有I/O事件,则直接进入nextstage;当阻塞时间timeout为-1时,会一直轮询I/O事件,有回调就执行2.pending回调什么时候注册回调队列staticintuv__run_pending(uv_loop_t*loop){QUEUE*q;队列pq;uv__io_t*w;如果(QUEUE_EMPTY(&loop->pending_queue))返回0;QUEUE_MOVE(&loop->pending_queue,&pq);while(!QUEUE_EMPTY(&pq)){q=QUEUE_HEAD(&pq);QUEUE_REMOVE(q);QUEUE_INIT(q);w=QUEUE_DATA(q,uv__io_t,pending_queue);w->cb(loop,w,POLLOUT);}return1;}该函数遍历loop->pending_queue队列节点,获取I/O观察者后调用cb。搜索索,只有uv__io_feed中存在向loop->pending_queue队列插入节点的代码,如下voiduv__io_feed(uv_loop_t*loop,uv__io_t*w){if(QUEUE_EMPTY(&w->pending_queue))QUEUE_INSERT_tending_TAL->,&loop->AIL->>pending_queue);}继续搜索uv__io_feed,调用的地方如下//src/unix/pipe.cvoiduv_pipe_connect(uv_connect_t*req,uv_pipe_t*handle,constchar*name,uv_connect_cbcb){//...if(err)uv__io_feed(handle->loop,&handle->io_watcher);}//src/unix/stream.cstaticvoiduv__write_req_finish(uv_write_t*req){//...uv__io_feed(stream->loop,&stream->io_watcher);}//src/unix/tpc.cintuv__tcp_connect(uv_connect_t*req,uv_tcp_t*handle,conststructsockaddr*addr,unsignedintaddrlen,uv_connect_cbcb){//...if(handle->delayed_error)uv__io_feed(handle->循环,&handle->io_watcher);//...}src/unix/udp.c中还有另外三个调用所以,在pendingcallbacks阶段,在以下场景注册回调:管道连接错误时,stream流写入请求完成,当tcp连接有延迟错误,udp时有几种情况3.达到定时器阈值后尽快执行,这可能会延迟他们的操作系统调度或其他正在运行的回调。这是什么意思?timer阶段的源码解读可以看这里:传送门TL;博士;过程如下:setTimeout/setInterval是通过内置类Timeout来实现的,它的时间阈值是1~231-1ms,而且是一个整数,所以setTimeout(callback,0)会转化为setTimeout(callback,1)并且进入tick之后,会获取到这个tick的开始时间,通过uv__hrtime函数调用系统时间,这个过程中可能会受到其他应用的影响。libuv中的所有定时器都是由执行时间节点组成的二进制最小堆结构来存储的。二叉最小堆的特点是父节点总是小于子节点,所以根节点是最小的定时器回调执行时间节点=注册回调时的tick开始时间+根节点时的定时器阈值超时时间二叉最小堆的结点被定时定时器回调的执行时间结点<=当前时间循环tick的开始时间,说明至少有一个过期的定时器,循环遍历二叉最小堆的根结点,并调用定时器对应的回调函数。当二叉最小堆根节点定时器回调的执行时间节点>当前时间周期tick的开始时间时,表示执行时间还没有到。根据二叉最小堆的特点,如果根节点的时间不能满足执行时间,那么后面的节点还没有过期。此时退出timer阶段的回调函数执行,进入下一阶段执行pendingcallbacks的回调函数,idel,准备计算poll阻塞当前tick的时间p。如果pendingcallbacks、idel、closecallbacks的回调队列不为空,则为0,尽快进入下一个tick执行相应的回调;如果有超时定时器,则为0,尽快进入下一个tick,执行超时定时器的回调;如果有非超时定时器,阻塞时间=二进制最小堆根节点定时器回调节点执行时间-当前时间循环tick开始时间;如果没有timer则为-1,无限阻塞执行check和closecallbacks的回调函数综上所述,操作系统调度或其他运行中的Callback是指:系统时间调用,过程中可能会受到其他应用的影响.当轮询被阻塞时,线程就会挂起,CPU就会调度去做其他的事情。CPU接收返回和处理的时间无法控制。每个阶段的回调执行时间无法控制。所以会出现超出阈值尽快执行的效果,而不是到时候就立即执行。Referenceepoll的功能原理介绍从libuv看nodejs事件循环libuv源码分析(五)IO观察器(io_watcher)文件描述符(FileDescriptor)介绍I/O内核原理及5种I/O模型