- 网络
- 通信
- 异步编程
【免费下载链接】libuv
Cross-platform asynchronous I/O
导读
libuv 是一套跨平台(Windows 与 Unix 同一套 API)的高性能事件驱动异步 I/O 库,Node.js 正是建立在其之上。本文以 libuv 仓库内官方指南的开篇章节(docs/src/guide/about.rst 与 docs/src/guide/introduction.rst)为核心,梳理这份指南的来龙去脉、目标读者、libuv 的历史演进,并结合仓库中的示例代码与核心源码(uv_run、uv_loop_init)带你从零跑通第一个事件循环程序,掌握 handle、request、回调与错误处理等最基础也最核心的编程模型。读完本文,你将具备阅读后续文件系统、网络、线程等进阶章节并独立编写 libuv 程序的能力。
这份指南的由来:一本"缺文档"催生的书
about.rst的正文非常简短,但信息量集中。它讲述了这份官方指南的起源:
- 2012 年 6 月 16 日的一个下午,Nikhil Marathe 因为不想写代码而开始写这本书。当时他正在开发 node-taglib,被 libuv 缺乏好文档这件事困扰——参考文档(API reference)是存在的,但没有任何系统性的教程。
- 这本书正是对"没有综合教程"这一痛点的回应,它试图做到准确,但也坦诚地承认"书中可能有错误,欢迎提交 Pull Request"。
- Nikhil 特别感谢了 Marc Lehmann 撰写的 libev 详尽 man page,它描述了两个库的大量共有语义(libuv 早期的 API 正是基于 libev 设计的)。
- 这本书最初用 Sphinx 和 vim 制作。
- 文档末尾的 note 记录了一个关键事实:2017 年 libuv 项目将这份工作正式并入官方文档,此后由项目方持续维护。
这段历史解释了为什么仓库里会有一个完整的 docs/src/guide 目录——它不是临时笔记,而是被官方吸收的正式教程体系。整个指南由 docs/src/guide.rst 通过 Sphinxtoctree组织,包含八章:
introduction(引言,即本文主题的相邻章节)basics(libuv 基础)filesystem(文件系统)networking(网络)threads(线程)processes(进程)eventloops(高级事件循环)utilities(实用工具)about(关于本书)
guide.rst 开头还带有一条官方警告:这套内容刚并入官方文档时未经彻底审查,发现错误可提交 issue 或 Pull Request——这与about.rst中"Pull requests are encouraged"的态度一脉相承。
这本书写给谁:两类读者与学习前提
introduction.rst明确了目标读者,这决定了你该以什么心态阅读这套指南:
- 系统程序员:正在编写守护进程、网络服务/客户端等底层程序,发现事件循环模式很适合自己,决定使用 libuv。
- Node.js 模块作者:想用 C/C++ 封装平台 API,并向外暴露(异步)JavaScript 接口。这类读者在 Node.js 语境下使用 libuv,需要额外参考 V8/Node.js 相关的资源,因为这本书不涉及 Node.js 专属内容。
指南同时强调:假设读者已经熟悉 C 语言,而且这套教程覆盖 libuv 的主要领域,但不是讨论每个函数和数据结构的完整参考手册——完整细节请查阅官方 API 文档(本文以仓库内 docs/src/api.rst 等章节为准)。
历史背景:libev 与 IOCP 之上长出的 libuv
introduction.rst用一小节交代了 libuv 的起源,这是理解它架构的关键背景:
- 2009 年,Node.js 项目启动,它是一个与浏览器解耦的 JavaScript 环境,使用 Google V8 和 Marc Lehmann 的 libev,把"事件驱动"的 I/O 模型与浏览器塑造过的编程语言结合起来。
- 随着 Node.js 流行,支持 Windows 变得重要,但libev 只跑在 Unix 上。Windows 上对应 kqueue/(e)poll 的内核事件通知机制是IOCP。
- libuv 最初是 libev 或 IOCP 之上的抽象层,对外提供基于 libev 的 API——也就是说,同一套 API 在 Unix 上走 libev、在 Windows 上走 IOCP。
- 在 node-v0.9.0 对应的 libuv 版本中,libev 被移除,libuv 成为独立的高质量系统编程库。如今 Node.js 之外的用户还包括 Mozilla 的 Rust 语言及各种语言绑定。
仓库中的 README.md 把这套架构总结为 feature highlights,可作为背景佐证:
- 由 epoll、kqueue、IOCP、event ports 支撑的完整事件循环;
- 异步 TCP/UDP 套接字;
- 异步 DNS 解析;
- 异步文件与文件系统操作;
- 文件系统事件(fs event);
- ANSI 转义序列控制的 TTY;
- 基于 Unix 域套接字或命名管道(Windows)的 IPC 与套接字共享;
- 子进程;
- 线程池;
- 信号处理;
- 高分辨率时钟;
- 线程与同步原语。
仓库 docs/src/static/architecture.txt(及对应架构图 docs/src/static/architecture.png)以 ASCII 图直观呈现了这种分层:上层是 Network I/O(TCP、UDP、TTY、Pipe)与 File/DNS/User Code,中间是事件循环核心uv__io_t,最底层是平台相关后端——Unix 上的 epoll、kqueue、event ports,Windows 上的 IOCP,以及用于把阻塞操作搬离事件循环的线程池(Thread Pool)。
关于版本:about.rst注明"本书和代码基于 libuv v1.42.0"。而当前仓库 include/uv/version.h 中的版本宏为UV_VERSION_MAJOR 1、UV_VERSION_MINOR 53、UV_VERSION_PATCH 1,且UV_VERSION_IS_RELEASE 0、UV_VERSION_SUFFIX "dev",即当前处于1.53.1 的开发版本(1.x 主版本保持 ABI 稳定,API 可在小版本间向后兼容演进,这点在 version.h 头注释中有明确说明)。阅读示例代码时,个别接口细节可能随版本演进有所调整,但核心编程模型没有变化。
构建 libuv 并运行示例代码
introduction.rst提供了基于 autotools 的构建方式,克隆仓库后执行:
sh autogen.sh ./configure make并特别说明:不需要执行make install,构建示例只需在docs/code/目录下执行make。仓库内的 docs/code 目录正是全书示例代码的存放位置,包含helloworld、default-loop、idle-basic、uvstop、tcp-echo-server、uvcat、spawn、thread-create等二十余个示例。
示例的构建体系由 docs/code/CMakeLists.txt 定义:它通过add_subdirectory("../../" build)引入 libuv 本体,然后用add_executable把SIMPLE_SAMPLES列表中的每个示例链接到静态库uv_a;其中signal、progress、queue-cancel、queue-work、tty、tty-gravity等仅在非 Windows 平台构建;uvwget则需要系统存在 CURL 库(FIND_PACKAGE(CURL))才会编译。因此:
cd docs/code cmake -B build .. cmake --build build即可一次性产出全部示例可执行文件(或按原文档方式用make构建)。
如果你更关注 libuv 本身的完整构建与测试,可参考 README.md 的 Build Instructions:Unix 系平台支持 autotools 与 CMake 两种方式,Windows 仅支持 CMake(需要 Visual Studio 2015 Update 3 及以上版本或 VS 2017+ 的 MSBuild/VC++ 工具链);测试驱动为uv_run_tests/uv_run_tests_a,测试清单位于 test/test-list.h,可按build/uv_run_tests_a TEST_NAME单独跑某个测试。
另外,本书所在的文档目录 docs 本身就是用 Sphinx 构建的(about.rst提到的 Sphinx 正是它):docs/requirements.txt 锁定 Sphinx 7.0.1 与 furo 主题等依赖,docs/src/conf.py 从include/uv/version.h自动读取版本号用于文档版本展示,Windows 用户可用 docs/make.bat 代替make执行html、man、epub、linkcheck等目标。
快速上手:第一个事件循环程序
basics.rst是这本书真正的技术起点,它奠定了全文反复使用的核心概念。
事件驱动与事件循环
libuv 强制一种异步、事件驱动的编程风格,核心职责是提供事件循环和基于回调的 I/O 通知。事件驱动编程中,应用程序对某些事件表达兴趣,事件发生时由注册的回调响应;libuv 负责从操作系统收集事件或监视其他事件源。事件循环通常"永远"运行下去,伪代码如下:
while there are still events to process: e = get the next event if there is a callback associated with e: call the callback典型的事件包括:文件可写、socket 有数据可读、定时器超时。这个事件循环被封装在uv_run()中——它是使用 libuv 时的"最终函数"。
为什么需要异步非阻塞?因为传统read、fprintf等 I/O 函数是阻塞的:真正的磁盘写入或网络读取相对 CPU 耗时极不对称,函数直到任务完成才返回,程序只能干等,其他 I/O 全部被堵住。线程方案(每个阻塞操作开一个线程)可行但有额外开销;libuv 走的是异步、非阻塞路线:应用程序请求操作系统监视 socket,把事件通知放入队列,自己随时检查队列、按需取数据。它"异步"是因为表达兴趣和使用数据发生在不同的时间与空间点;"非阻塞"是因为进程在这期间可以自由做其他事,系统事件被当作普通 libuv 事件处理。basics.rst中的注释也提醒:底层的实现机制(worker 线程/轮询)并非使用者需要关心的事。
Hello World:一个立即退出的循环
#include <stdio.h> #include <stdlib.h> #include <uv.h> int main() { uv_loop_t *loop = malloc(sizeof(uv_loop_t)); uv_loop_init(loop); printf("Now quitting.\n"); uv_run(loop, UV_RUN_DEFAULT); uv_loop_close(loop); free(loop); return 0; }(完整源码见 docs/code/helloworld/main.c)
这个程序立即退出,因为它没有任何事件要处理——libuv 事件循环必须通过各类 API 函数明确告诉它去监视什么事件。
basics.rst还特别强调了内存管理约定:从 libuv v1.0 起,用户应先为循环分配内存,再调用uv_loop_init(uv_loop_t *)初始化,这样可以接入自定义内存管理;循环结束记得用uv_loop_close(uv_loop_t *)反初始化并释放存储。示例程序从不关闭循环,因为程序在循环结束后就退出、系统会回收内存;但生产级、尤其是长期运行的系统程序,必须正确释放资源。
从源码看,uv_loop_init在 src/unix/loop.c 中会:保存并恢复loop->data后清零整个结构体,分配internal_fields,初始化互斥锁与定时器堆(heap_init)、各句柄队列(idle/async/check/prepare/handle_queue)、pending_queue与watcher_queue,调用平台相关的uv__platform_loop_init初始化 epoll/kqueue 等后端,并注册一个内部wq_async句柄用于线程池任务完成后的唤醒。这解释了为什么一个"空"循环也能有条不紊地启动和关闭。
默认循环
如果你只需要单个循环,直接使用 libuv 提供的默认循环uv_default_loop()即可:
#include <stdio.h> #include <uv.h> int main() { uv_loop_t *loop = uv_default_loop(); printf("Default loop.\n"); uv_run(loop, UV_RUN_DEFAULT); uv_loop_close(loop); return 0; }(完整源码见 docs/code/default-loop/main.c)
basics.rst特别提示:Node.js 使用默认循环作为主循环,如果你在写绑定(binding),务必意识到这一点。
错误处理
- 初始化或同步函数失败时返回负数;
- 异步函数失败时会把状态参数传给回调;
- 错误消息定义为
UV_E*常量; - 用
uv_strerror(int)和uv_err_name(int)可分别拿到描述错误的const char *与错误名; - I/O 读回调(文件、socket)会收到
nread参数:nread < 0表示出错,其中UV_EOF是文件结尾错误,常需要特殊处理。
Handles 与 Requests:两个最核心的概念
libuv 的编程模型建立在两类对象上:
- Handle(句柄):对 I/O 设备、定时器或进程的表达,是不透明结构体,命名形如
uv_TYPE_t,例如uv_tcp_t、uv_udp_t、uv_timer_t、uv_idle_t、uv_signal_t、uv_process_t、uv_fs_event_t。Handle 代表长期存在的对象,通过配套的uv_TYPE_init(uv_loop_t *, uv_TYPE_t *)函数初始化。 - Request(请求):Handle 上的异步操作标识,短命(通常只跨一个回调),用于在"发起动作"与"回调"之间保存上下文。例如 UDP 套接字用
uv_udp_t表示,而每次写入用一个uv_udp_send_t结构体,写入完成后传给回调。
basics.rst还给出了完整的类型清单(handle 类型、request 类型以及既非 handle 也非 request 的信息结构体如uv_cpu_info_t、uv_interface_address_t等),可参考 include/uv.h 中的实际声明。
回调就是 libuv 在 watcher 感兴趣的事件发生时调用的函数,应用逻辑通常写在回调里:I/O watcher 回调收到读到的数据,定时器回调在超时时被触发。
用 idle 句柄观察生命周期
下面这个例子用 idle 句柄演示 watcher 生命周期:回调在事件循环每一轮都被调用一次,计数达到10e6后uv_idle_stop停止 watcher,因为没有活跃 watcher 了,uv_run()随即退出:
#include <stdio.h> #include <uv.h> int64_t counter = 0; void wait_for_a_while(uv_idle_t* handle) { counter++; if (counter >= 10e6) uv_idle_stop(handle); } int main() { uv_idle_t idler; uv_idle_init(uv_default_loop(), &idler); uv_idle_start(&idler, wait_for_a_while); printf("Idling...\n"); uv_run(uv_default_loop(), UV_RUN_DEFAULT); uv_loop_close(uv_default_loop()); return 0; }(完整源码见 docs/code/idle-basic/main.c)
idle handle 的典型用途(例如保持进程存活、穿插后台任务)会在utilities一章展开讨论。另外,回调式编程常需要传递"上下文":所有 handle 和 request 都带一个void* data成员,可以在调用点设置、在回调里取回;uv_loop_t也有同样的 data 成员。这是整个 C 库生态的常见模式。
进阶:uv_stop 与事件循环的精细控制
eventloops.rst展示了对事件循环的"驾驶技巧"。libuv 给予用户相当大的循环控制权,你可以同时摆弄多个循环,甚至把 libuv 事件循环嵌入另一个事件循环驱动的库(比如 Qt 的 UI 事件循环驱动一个 libuv 后端做密集系统级任务)。
uv_stop()用于停止事件循环,但其语义容易误解,关键点:
- 循环最早也要到下一轮迭代才会停止,可能更晚;
- 本轮迭代中已就绪、待处理的事件仍会被处理,所以
uv_stop()不能当"急停开关"用; - 调用
uv_stop()后,本轮循环不会因 I/O 而阻塞。
其背后的机制在uv_run()的实现里一目了然。参考 src/unix/core.c 中uv_run的主循环:
while (r != 0 && loop->stop_flag == 0) { can_sleep = uv__queue_empty(&loop->pending_queue) && uv__queue_empty(&loop->idle_handles); uv__run_pending(loop); uv__run_idle(loop); uv__run_prepare(loop); timeout = 0; if ((mode == UV_RUN_ONCE && can_sleep) || mode == UV_RUN_DEFAULT) timeout = uv__backend_timeout(loop); uv__metrics_inc_loop_count(loop); uv__io_poll(loop, timeout); /* Process immediate callbacks (e.g. write_cb) a small fixed number of * times to avoid loop starvation.*/ for (r = 0; r < 8 && !uv__queue_empty(&loop->pending_queue); r++) uv__run_pending(loop); uv__metrics_update_idle_time(loop); uv__run_check(loop); uv__run_closing_handles(loop); uv__update_time(loop); uv__run_timers(loop); ... }stop_flag由uv_stop()设置。所有 libuv 回调都在事件循环内部被调用,因此在回调里调用uv_stop()也会让本轮迭代完整走完:循环先更新定时器,运行 pending、idle、prepare 回调,再处理任何挂起的 I/O 回调。如果你在它们当中调用uv_stop(),stop_flag被置位,uv__backend_timeout()返回 0(见 src/unix/core.c),于是uv__io_poll不再阻塞等待 I/O;反之,如果你在 check 回调里调用uv_stop(),I/O 早已完成,不受影响。
uv_stop()的实用价值在于:当结果已算出或出错时,不必逐个停掉所有 handler,直接停掉整个循环即可。示例 docs/code/uvstop/main.c 同时注册了 idle 与 prepare 两个 watcher,idle 回调计数到 5 时调用uv_stop(),可以看到"当前迭代仍会完成"的行为:
#include <stdio.h> #include <uv.h> int64_t counter = 0; void idle_cb(uv_idle_t *handle) { printf("Idle callback\n"); counter++; if (counter >= 5) { uv_stop(uv_default_loop()); printf("uv_stop() called\n"); } } void prep_cb(uv_prepare_t *handle) { printf("Prep callback\n"); } int main() { uv_idle_t idler; uv_prepare_t prep; uv_idle_init(uv_default_loop(), &idler); uv_idle_start(&idler, idle_cb); uv_prepare_init(uv_default_loop(), &prep); uv_prepare_start(&prep, prep_cb); uv_run(uv_default_loop(), UV_RUN_DEFAULT); return 0; }事件循环的更完整设计(I/O 循环各阶段)还可继续阅读仓库内的 docs/src/design.rst。
后续章节导览:一本书的完整地图
about.rst与introduction.rst交代了背景,而指南的"正文"从这里才刚刚开始。作为收尾,把八章的技术内容在地图上标注出来,方便按需深入:
| 章节 | 文档路径 | 核心内容 |
|---|---|---|
| 基础 | docs/src/guide/basics.rst | 事件循环、Hello World、错误处理、Handle/Request 模型 |
| 文件系统 | docs/src/guide/filesystem.rst | uv_fs_*系列、同步/异步双形态、线程池执行的阻塞操作 |
| 网络 | docs/src/guide/networking.rst | TCP/UDP 套接字、非阻塞 I/O、DNS |
| 线程 | docs/src/guide/threads.rst | 线程创建、同步原语 |
| 进程 | docs/src/guide/processes.rst | 子进程、spawn、管道 |
| 事件循环 | docs/src/guide/eventloops.rst | uv_stop、多循环、循环嵌入 |
| 工具 | docs/src/guide/utilities.rst | 定时器、uv_ref/uv_unref引用计数、其他实用 API |
例如文件系统章节会告诉你一个关键区别:socket 操作使用操作系统提供的非阻塞原语,而文件系统操作内部调用阻塞函数,但通过线程池调度、在需要应用交互时通知事件循环上的 watcher(详见 docs/src/guide/filesystem.rst 开头注释);所有uv_fs_*函数都有同步/异步两种形态,回调为 NULL 时自动同步执行并阻塞,返回值为 libuv 错误码。
至此,从这本书为什么存在、为谁而写,到 libuv 的历史架构、构建方式,再到第一个事件循环程序和uv_stop的底层语义,你已经拿到了阅读后续所有章节所需的完整上下文。接下来,请打开 docs/src/guide/basics.rst 与 docs/src/guide/networking.rst,配合 docs/code 目录下的示例逐个编译运行,事件驱动编程的手感会很快建立起来。
- 网络
- 通信
- 异步编程
【免费下载链接】libuv
Cross-platform asynchronous I/O
相关推荐
libuv事件循环:异步编程的核心引擎
libuv事件循环:异步编程的核心引擎 本文深入解析了libuv事件循环的11个执行阶段、UV_RUN运行模式的区别、定时器处理机制以及线程安全性与多事件循环的
网络通信异步编程libuv 用户指南:从事件循环到多进程的跨平台异步 I/O 编程实战
libuv 用户指南:从事件循环到多进程的跨平台异步 I/O 编程实战 libuv 是一个跨平台的高性能事件驱动 I/O 库,为 Windows 与 Unix
网络通信异步编程libuv 基础指南:事件循环、Handles 与 Requests 编程模型详解
libuv 基础指南:事件循环、Handles 与 Requests 编程模型详解 导读 本文是 libuv 官方指南的第二篇(Basics of libuv)
网络通信异步编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考