实践[C语言] libuv[1]初探

178 阅读5分钟

什么是libuv?

libuv是一个跨平台的异步IO库,设计这个库的最初目的是为Node.js提供事件驱动的异步I/O模型。

libuv提供了对不同I/O的轮询抽象,句柄和流的套接是套接字和其它实体的高级抽象和跨平台文件I/O和线程功能。

libuv提供了核心实用程序,如定时器,非阻塞网络,网络访问,异步文件系统访问,子进程。

libuv事件循环机制是可以让我们注册回调函数来响应不同的事件和任务,这些回调可以用来执行各种异步操作,处理定时器,监听文件系统事件,监听网络事件,通过合理使用这些阶段,我们可以编写高效的异步应用程序,同时确保事件的顺序和调度是可控的。

libuv由多个子系统组成,如下图

image.png

不同抽象的执行流程

Handlers 和 request

libuv提供了两个抽象 handlesrequest

handles代表长生命周期操作,在某些条件达到时触发

  1. 在激活时,每次事件循环时都会调用一次该回调。
  2. 每个TCP服务器创建新连接时执行一次他的回调。

request表示短期操作。这些操作可以通过handles执行,write requesthandler上写数据,getaddrinfo不需要执行handles而是直接在事件循环中执行。

I/O循环

I/O循环是libuv的核心部分,它为所有的I/O操作提供上下文环境,而且IO函数被指定到单个线程中,只要每个事件循环在不同的线程中运行,就可以运行多个事件循环。

注意:libuv的事件循环或其他涉及到循环或句柄的API都不是线程安全的。

所有的IO相关异步操作都可以通过IO事件循环完成或都可以在事件循环中被处理,而处理的方式就是以回调的方式操作,对应javascript就是回调函数。也正因如此,事件循环可以在一个线程中大量循环处理IO操作,但是如果回调函数偏向CPU密集型操作的运算,则会导致循环阻塞。

常见的单线程异步处理方式:

所有的网络IO都在非阻塞的套接字上执行,这些套接字使用平台上可用的最佳机制进行轮询:如Linux上的epoll。但是事件的访问等待还是阻塞的,当访问激活时会激活事件循环对应监听的事件,事件循环会将调用当前IO事件的回调函数,然后在handles中完成读写执行等操作。

事件循环的图示

image.png

  1. Call pending callbacks 处理上一个事件循环中错误或等待的挂起任务
  2. Run idle handles处理回调(空闲阶段)
  3. Run perpare handles处理回调(准备阶段)
  4. Poll forIO处理IO相关的handles操作
  5. Run check handles处理回调(检查阶段)
  6. Call close callback 清理被关闭的handles
  7. Update loop time 更新循环时间
  8. 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       全用例测试文件