什么是libuv?
libuv是一个跨平台的异步IO库,设计这个库的最初目的是为Node.js提供事件驱动的异步I/O模型。
libuv提供了对不同I/O的轮询抽象,句柄和流的套接是套接字和其它实体的高级抽象和跨平台文件I/O和线程功能。
libuv提供了核心实用程序,如定时器,非阻塞网络,网络访问,异步文件系统访问,子进程。
libuv事件循环机制是可以让我们注册回调函数来响应不同的事件和任务,这些回调可以用来执行各种异步操作,处理定时器,监听文件系统事件,监听网络事件,通过合理使用这些阶段,我们可以编写高效的异步应用程序,同时确保事件的顺序和调度是可控的。
libuv由多个子系统组成,如下图
不同抽象的执行流程
Handlers 和 request
libuv提供了两个抽象 handles和request
handles代表长生命周期操作,在某些条件达到时触发
- 在激活时,每次事件循环时都会调用一次该回调。
- 每个TCP服务器创建新连接时执行一次他的回调。
request表示短期操作。这些操作可以通过handles执行,write request在handler上写数据,getaddrinfo不需要执行handles而是直接在事件循环中执行。
I/O循环
I/O循环是libuv的核心部分,它为所有的I/O操作提供上下文环境,而且IO函数被指定到单个线程中,只要每个事件循环在不同的线程中运行,就可以运行多个事件循环。
注意:libuv的事件循环或其他涉及到循环或句柄的API都不是线程安全的。
所有的IO相关异步操作都可以通过IO事件循环完成或都可以在事件循环中被处理,而处理的方式就是以回调的方式操作,对应javascript就是回调函数。也正因如此,事件循环可以在一个线程中大量循环处理IO操作,但是如果回调函数偏向CPU密集型操作的运算,则会导致循环阻塞。
常见的单线程异步处理方式:
所有的网络IO都在非阻塞的套接字上执行,这些套接字使用平台上可用的最佳机制进行轮询:如Linux上的epoll。但是事件的访问等待还是阻塞的,当访问激活时会激活事件循环对应监听的事件,事件循环会将调用当前IO事件的回调函数,然后在handles中完成读写执行等操作。
事件循环的图示
Call pending callbacks处理上一个事件循环中错误或等待的挂起任务Run idle handles处理回调(空闲阶段)Run perpare handles处理回调(准备阶段)Poll forIO处理IO相关的handles操作Run check handles处理回调(检查阶段)Call close callback清理被关闭的handlesUpdate loop time更新循环时间Run due times处理定时任务
idle、prepare、check三个阶段区别
idle阶段
idle是一个事件循环的附加阶段,主要用于处理低优先级的任务- 在这个阶段,libuv会执行那些没有高优先级任务需要执行时才会执行回调的函数
- 这个阶段用于执行一些后台任务或者清理工作。
prepare阶段
prepare阶段是事件循环的第一个阶段。- 这个阶段libuv会准备事件循环要执行的任务,同时这个阶段不需要用户干预,libuv会自己处理
check阶段
check阶段是事件循环的最后一个阶段。- 这个阶段libuv会检查是否有需要执行的回调函数
- 如果由等待执行的回调函数,libuv会执行它们
- 这个阶段通常用于执行回调函数,如定时器回调或其它异步任务回调
文件IO
文件IO不依赖于平台的文件IO原语,因此当前的方法是在线程池中运行阻塞文件IO操作。
libuv使用全局线程池,所有的循环都可以在该线程池上排队工作,主要包括以下三种类型操作:
- 文件系统操作
read,write,open,close - DNS:
getaddrinfo,getnameinfo - 用户通过
uv_queue_work提交的任务
源码结构目录
以下是libuv的源码目录
.
├── include
│ ├── uv 针对不同平台的不同类型声明定义
│ │ ├── aix.h
│ │ ├── bsd.h
│ │ ├── darwin.h
│ │ ├── errno.h
│ │ ├── linux.h
│ │ ├── os390.h
│ │ ├── posix.h
│ │ ├── sunos.h
│ │ ├── threadpool.h
│ │ ├── tree.h
│ │ ├── unix.h
│ │ ├── version.h
│ │ └── win.h
│ └── uv.h 平台无关头文件
├── src 源码目录
│ ├── fs-poll.c 文件系统轮询相关实现
│ ├── heap-inl.h 堆相关的内联函数实现
│ ├── idna.c 国际化域名编解码实现
│ ├── idna.h
│ ├── inet.c 网络相关的函数实现,如套接字的创建和连接
│ ├── queue.h 队列的定义和实现
│ ├── random.c 随机数生成器的实现
│ ├── strscpy.c 字符串拷贝函数实现
│ ├── strscpy.h
│ ├── strtok.c 字符串分割函数实现
│ ├── strtok.h
│ ├── thread-common.c 线程相关的通用实现,如互斥锁和条件变量
│ ├── threadpool.c 线程池实现,用于高效地执行多个并行任务
│ ├── timer.c 计时器实现,用于按时间出发某些事件
│ ├── uv-common.c libuv库的公共实现,包括事件循环,IO处理和计时器
│ ├── uv-common.h
│ ├── uv-data-getter-setters.c 数据获取和设备函数的实现,用于操作libuv内部的数据结构
│ ├── version.c 版本信息的定义和实现
│ ├── unix/ unix平台特性实现
│ └── win/ windows平台的特性实现
└── uv_win_longpath.manifest
编译
首先我们从githu上获取源码后使用cmake指令生成makefile编译描述文件,该操作会设置编译所需的环境变量和系统变量。
cmake CmakeList.txt
执行编译
我电脑是32核的,可以在编译的时候指定并行编译所占用的CPU个数
make -j32
编译完后会在当前目录中生成如下文件
这三个文件表示的都是 libuv.so.1.0.0 由于历史原因,不同的软件使用libuv时采取的目标文件名有所不同,我们创建该方式是可以兼容.so,.so.1,.so.1.0.0的编译连接请求。
├── libuv.so -> libuv.so.1
├── libuv.so.1 -> libuv.so.1.0.0
├── libuv.so.1.0.0
除了生成目标文件外,makefile还编译了三个测试可执行文件如下所示
├── uv_run_benchmarks_a 全用例基准测试文件
├── uv_run_tests 测试文件
├── uv_run_tests_a 全用例测试文件