libuv源码分析
October 4, 2021
习惯了思维图之类的简单整理之后,有一段时间没有认真写写东西了。前段时间为了研究下node的性能指标,特意复习了一下libuv的源码,但是一直懒得去整理,直到今天看到这段话后才决定整理一番:
学习是一种行动反射,不是为了晓得些『知识』,要切己体察,代入自己要事上琢磨,落实行动,这就是知行合一。否则,读书也是一种玩物尚志。
定期总结也是学习的一部分。
简介
libuv是个异步服务的框架,整体实现及功能先看一下libuv官网的这张分层结构图。
- 底层根据操作系统类型,选择不同的多路复用实现。linux下基本就是epoll了。据说nodejs选择libuv而非libev作为底层实现,是因为libuv等windows的支持更为友好(IOCP)。具体没有对比详细对比过。
- uv__io_t:从底层看,上层大部分的操作都跟io有关,或者可以转换为io操作。如tcp/udp的网络IO,tty的相应,signal的处理、pipe读写等。所以libuv封装了一层IO“基类”,工其他handler使用。
- Handler:TCP、UDP、Pipe等,定位类似libevent中的event,通常实现是基于uv__io_t或者其“子类”(如uv_tcp_t、uv_pipe_t、uv_tty_t继承自uv_stream_t),暴露给业务使用的数据&接口封装。
- 其他:作为框架,也封装了一些常用的能力,如定时器、线程池等,这块没有具体研究。私以为这种操作尽量避免。libevent后面的版本扩展了一堆http的能力,但是后面框架的代码量和复杂度就增加了很多,甚至还有bug,基本没法维护了,感觉一个框架干好一件事就不错了。
TCP-Echo-Server数据结构图
libuv的源码较多,涉及了大量的IO封装,阅读过程中简单整理了一份用libuv实现的tcp echo server的数据结构图,从中大约可以看出libuv的整体数据结构及关联关系。从而大致了解相关的调度机制。
libuv跟libevent、libev等reactor模式框架实现上没有本质上得区别,本质上还是基于select/epoll、IOCP等底层多路复用机制,封装给上层网络(tcp/udp)、文件(read/write、dir)等异步调用场景使用,避免单进程/线程block调用的影响。
从左到右分为以下几个部分,后续分别介绍:
- uv_loop_t
- uv_io_t
- 各个handler: uv_tcp_t、uv_udp_t、uv_pipe_t等
- request对象
- 内部callback
- 用户callback
uv_loop_t
loop核心流程在官方文档上有介绍,uv_loop_t为事件循环所需的核心数据结构。通常一个进程只需要一个loop对象,即使用default loop即可。
beckend_fd的作用类似于epoll fd。用作各类事件的监听。
watchers 是一个指针数组,用于存储fd与handler的关联,通过fd值可以直接索引到对应handler。
water_queue、pending_queue用来串联各种handler。
核心实现在uv_run()中,大致流程如下:
- uv__run_timers:处理超时任务。
如果没有堆顶元素,则没有任何定时器存在,函数将直接返回。
如果当前时间小于定时任务的超时间,那么堆顶 timer 未到到超时时间,非堆顶的 timer 更没有达到超时时间,整个 uv__run_timers 也就会退出。
如果当前时间等于或大于定时任务的超时间,这个 timer 就是一定达到或超过执行时间的。这时,就可以从 timer 堆中将其取出,然后调用其回调函数 handle->timer_cb(handle) 处理定时任务,然后再次重复获取下一个出现在堆顶的 timer,直到情况 1 或 2 成立。
- uv__run_pending:处理上一轮被挂起的事件回调。
// 在某些情况下,IO 观察者绑定的回调函数并不是立即调用的,而是被延迟到下一次事件循环的固定阶段调用的,
// 在 uv_run 中调用的 uv__run_pending 处理这些被延迟的 IO 观察者
// 该函数遍历 loop->pending_queue 队列节点,取得 I/O 观察者后调用 cb,并且指定 events 参数为固定值 POLLOUT(表示可写),因此可以猜测被插入到 loop->pending_queue 队列中的情形都是可写 I/O 事件。该队列上是用来保存被延迟到下次事件循环中处理的 IO 观察者。
tcp-server的实现中,通过client fd回写应答包给client时,write操作不一定是同步的(某些条件会同步)。所以才会有pending这种状态存在。
- uv__run_idle:处理idle事件
为啥一个loop要存在3个实现方式一样的queue【"unix/loop-watcher.c"中实现】: idle、prepare、check ?
prepare、check分别在loop的前后。
parepare、idle的区别在于:存在idle时,循环将执行零超时轮询(timeout=0)。 http://docs.libuv.org/en/v1.x/idle.html
NoteThe notable difference with prepare handles is that when there are active idle handles, the loop will perform a zero timeout poll instead of blocking for i/o.
- uv__run_prepare:处理prepare事件
场景:Prepare handles will run the given callback once per loop iteration, right before polling for i/o.
- uv_backend_timeout:计算超时阻塞等待时间。
注意:存在idle handle时不阻塞。
- uv__io_poll:核心poll操作。处理各种handler
- uv__run_check:处理check事件。
场景:Check handles will run the given callback once per loop iteration, right after polling for i/o.
uv_io_t
各handler通常需要挂在在loop的watchers数组中,uv_io_t最大的作用就是用来串联各handler数组或链表。作为handler的一部分,他也包含了一些通用的数据属性。
cb:指向各handler的内部回调。如stream_io时,默认指向uv__stream_io()。如果handler作用有变化时可能会被重置。比如当fd为一个tcp listen fd时,cb会指向uv__server_io(),用来专门处理listen fd的accept操作。
pending_queue、watcher_queue:用作挂在loop中的双向链表指针。用于遍历各handler时使用。当handler状态变更时会被从loop中剔除。
fd、pevents、events 用于存储fd及fd需要监听的事件。
uv_tcp_t
继承自uv_stream_t。属于handler的一种。
Handles是需要挂载在主循环的监听对象,比如socket fd、文件fd,也包括定时器、idle_t等需要每次loop都需要判断是否处理的对象。具有较长的生命周期。
request对象
Request为短时间的操作对象,如socket需要的回包,通常挂载在handle对象上(回包需要fd)。也可以不依赖handle.
内部callback
典型如uv__server_io、uv__stream_io等,处理accept、read等操作,处理之后再转给业务对应的callback处理。
uv__server__io流程:
1、accept fd,存放在accepted_fd
2、fd转给业务connection_cb
3、如果业务主动关闭,跳出listen。否则一直accept
uv__stream_io() // stream默认cb
1、uv__read:有读标识,读取消息后转给业务回调read-cb
2、uv__write:有写标识,send()
用户callback
这种就是用户/业务层的回调操作了。
uv__io_poll
- 向epoll里面注入所有I/O观察者。
- 开始进入 epoll_pwait 轮询IO事件通常情况下,在 timeout 大于 0 的情况下,循环不断迭代到 timeout 减小到 0 时,循环跳出在没有设置定时器的情况下,如果不出现错误,循环将一直不会跳出循环主要由 timeout 和 count 控制是否跳出,符合整个事件循环
- 处理IO事件:获取IO观察者,调用关联的回调函数。这里通常为内部callback