news 2026/10/11 14:19:48

libuv 官方编程指南:从诞生背景到第一个事件循环程序

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
libuv 官方编程指南:从诞生背景到第一个事件循环程序
  • 网络
  • 通信
  • 异步编程

【免费下载链接】libuv

Cross-platform asynchronous I/O

项目地址:https://gitcode.com/gh_mirrors/li/libuv
点击查看免费下载

导读

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组织,包含八章:

  1. introduction(引言,即本文主题的相邻章节)
  2. basics(libuv 基础)
  3. filesystem(文件系统)
  4. networking(网络)
  5. threads(线程)
  6. processes(进程)
  7. eventloops(高级事件循环)
  8. utilities(实用工具)
  9. about(关于本书)

guide.rst 开头还带有一条官方警告:这套内容刚并入官方文档时未经彻底审查,发现错误可提交 issue 或 Pull Request——这与about.rst中"Pull requests are encouraged"的态度一脉相承。

这本书写给谁:两类读者与学习前提

introduction.rst明确了目标读者,这决定了你该以什么心态阅读这套指南:

  1. 系统程序员:正在编写守护进程、网络服务/客户端等底层程序,发现事件循环模式很适合自己,决定使用 libuv。
  2. 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.rstuv_fs_*系列、同步/异步双形态、线程池执行的阻塞操作
网络docs/src/guide/networking.rstTCP/UDP 套接字、非阻塞 I/O、DNS
线程docs/src/guide/threads.rst线程创建、同步原语
进程docs/src/guide/processes.rst子进程、spawn、管道
事件循环docs/src/guide/eventloops.rstuv_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

项目地址:https://gitcode.com/gh_mirrors/li/libuv
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 14:15:17

C++超级玛丽源码解析:SDL2选型、碰撞检测与手感调校

简介&#xff1a;C版《超级玛丽》完整游戏源码&#xff0c;适合游戏开发初学者及对2D平台跳跃游戏实现感兴趣的读者。资源基于经典任天堂玩法重构&#xff0c;包含游戏主循环、马里奥角色与敌人对象、关卡地图数据、物理碰撞检测及图像音频加载等核心模块&#xff0c;可帮助学习…

作者头像 李华
网站建设 2026/10/11 14:12:22

C++ Win32 捕鱼达人课程设计:消息循环、GDI双缓冲与碰撞检测全解析

简介&#xff1a;这是一份基于 Windows 平台、使用 C 完成的“捕鱼达人”小游戏课程设计资源&#xff0c;面向正在学习 Windows 程序设计与面向对象开发的在校学生。项目以捕鱼游戏为场景&#xff0c;完整覆盖窗口创建、GDI 图形绘制、鼠标键盘事件响应、多线程与定时器、碰撞检…

作者头像 李华