【libuv高效编程】libuv学习超详细教程2——libuv框架初窥

libuv框架

从官方的文档可以找到一个设计框架的图片:libuv/docs/src/static/architecture.png

libuv004

从这张图片可以看出libuv的设计框架(从上往下看,从左往右分成网络IO与文件IO等操作),从网络I/O看,在linux(unix)平台它可以通过网络的底层epoll作为异步的I/O处理,在OSX上可以使用kqueue等,它的中间有一层抽象层uv__io_t;而在Windows平台可以通过IOCP作为异步的处理;对应文件I/O操作,它不像网络I/O,libuv没有特定平台的异步IO原语(primitives)可以依赖,所以libuv是在线程池中执行阻塞(同步)IO来实现异步的操作。

同步I/O

在操作系统中,程序运行的空间分为内核空间和用户空间,用户空间所有对io操作的代码(如文件的读写、socket的收发等)都会通过系统调用进入内核空间完成实际的操作。

而且我们都知道CPU的速度远远快于硬盘、网络等I/O。在一个线程中,CPU执行代码的速度极快,然而,一旦遇到I/O操作,如读写文件、发送网络数据时,就需要等待 I/O 操作完成,才能继续进行下一步操作,这种情况称为同步 I/O。

在某个应用程序运行时,假设需要读写某个文件,此时就发生了 I/O 操作,在I/O操作的过程中,系统会将当前线程挂起,而其他需要CPU执行的代码就无法被当前线程执行了,这就是同步I/O操作,因为一个IO操作就阻塞了当前线程,导致其他代码无法执行,所以我们可以使用多线程或者多进程来并发执行代码,当某个线程/进程被挂起后,不会影响其他线程或进程。

多线程和多进程虽然解决了这种并发的问题,但是系统不能无上限地增加线程/进程。由于系统切换线程/进程的开销也很大,所以,一旦线程/进程数量过多,CPU的时间就花在线程/进程切换上了,真正运行代码的时间就少了,这样子的结果也导致系统性能严重下降。

多线程和多进程只是解决这一问题的一种方法,另一种解决I/O问题的方法是异步I/O。

异步I/O

当程序需要对I/O进行操作时,它只发出I/O操作的指令,并不等待I/O操作的结果,然后就去执行其他代码了。一段时间后,当I/O返回结果时,再通知CPU进行处理。这样子用户空间中的程序不需要等待内核空间中的 I/O 完成实际操作,就可执行其他任务,提高CPU的利用率。

简单来说就是,用户不需要等待内核完成实际对io的读写操作就直接返回了。

大家可以思考一下,如何去实现异步I/O模型,思考明白了,就表示对libuv框架有一定了解了,或者说看完这篇文章后,你能知道为什么libuv是这样子设计的,那就可以了。

Handles 和 Requests

Handles就是句柄,Requests是请求,句柄代表着一个可用的资源(或者说是一个对象),比如一个TCP连接,在整个TCP连接的生命周期中,句柄都是可用的,当断开连接后,句柄也应该随之释放。而请求则表示一个操作的开始,比如TCP请求建立连接、TCP请求读取数据、发送数据等,这个请求是期望得到应答的,当产生应答的时候,请求就完成了。还有很重要的一点是:这些请求是可以在句柄上操作的,比如在某个TCP连接中请求发送数据、读取数据;当然也有不在句柄上的请求。

事件循环

事件循环是libuv的核心部分,它的主要职责是对 I/O 进行轮询,然后基于不同的事件源调度它们的回调,它是libuv中建立所有I/O操作的内容,所有网络I/O工作在非阻塞套接字上,这些循环依赖平台上的最佳机制进行轮询,比如在linux平台上使用epoll,在OSX上使用kqueue,在Windows上使用IOCP等机制,当这些网络I/O有数据到达的时候,libuv将通过回调函数去执行相应的操作,比如读取数据、写入数据等。

对于文件I/O的操作,由于平台并未对文件I/O提供轮询机制,libuv通过线程池的方式阻塞他们,每个I/O将对应产生一个线程,并在线程中进行阻塞,当有数据可操作的时候解除阻塞,进而进行回调处理,因此libuv将维护一个线程池,线程池中可能创建了多个线程并阻塞它们。

接下来放一张官方的图片吧,它在libuv/docs/src/static/loop_iteration.png路径下:

libuv005

这张图很明确的表示了libuv中所有I/O的事件循环处理的过程,其实就是uv_run()函数执行的过程,它内部是一个while循环。

  1. 首先判断循环是否是处于活动状态,它主要是通过当前是否存在处于alive活动状态的句柄,如果句柄都没有了,那循环也就没有意义了,如果不存在则直接退出。

  2. 开始倒计时,主要是维护所有句柄中的定时器,当某个句柄超时了,就会告知应用层已经超时了,就退出去或者重新加入循环中。

  3. 调用待处理的回调函数,如果有待处理的回调函数,则去处理它,如果没有就接着往下运行。

  4. 运行空闲句柄,反正它这个线程都默认会有空闲句柄的,这个空闲句柄会在每次循环中被执行。

  5. 运行准备句柄回调处理,这个名字有点奇怪,我是跟着官方手册来学习的,也不知道怎么去翻译它,简单来说就是在某个I/O要阻塞前,有需要的话就去运行一下他的回调函数,举个例子吧,比如我要从某个文件读取数据,如果我在读取数据进入阻塞状态之前想打印一个信息,那么就可以通过这个准备句柄的回调函数去处理这个打印信息。

  6. 计算轮询超时,在阻塞I/O之前,循环会计算阻塞的时间,并将这个I/O进入阻塞状态(如果可以的话,阻塞超时为0则表示不阻塞),这些是计算超时的规则:

  • 如果使用该UV_RUN_NOWAIT模式运行循环,则超时为0。

  • 如果要停止循环(uv_stop()被调用),则超时为0。

  • 如果没有活动的句柄或请求,则超时为0。

  • 如果有任何空闲的句柄处于活动状态,则超时为0。

  • 如果有任何要关闭的句柄,则超时为0。

  • 如果以上情况均不匹配,则采用最接近的定时器超时,或者如果没有活动的定时器,则为无穷大。

重要知识点说明: I/O 循环作为事件循环迭代的一部分,事件循环将会被阻塞在 I/O 循环上(例如:linux 上的 epoll_pwait() 调用),直到该套接字有 I/O 事件发生时唤醒这个线程,调用关联的回调函数,然后便可以在 handles 上进行读、写或其他想要进行的操作 requests。这也直接避免了时间循环一直工作导致占用 CPU 的问题。

  1. 检查句柄的回调,其实当程序能执行到这一步,就表明I/O已经退出阻塞状态了,那么有可能是可读/写数据,也有可能超时了,此时libuv将在这里检查句柄的回调,如果有可读可写的操作就调用他们对应的回调,当超时了就调用超时的处理。

  2. 如果通过调用uv_close()函数关闭了句柄,则会调用close将这个I/O关闭。

  3. 在超时后更新下一次的循环时间,前提是通过UV_RUN_DEFAULT模式去运行这个循环,关于运行的模式在后续会讲解到,目前有3中模式,UV_RUN_NOWAIT、UV_RUN_ONCE、UV_RUN_DEFAULT

参考

libuv官方文档

  • 16
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
libhv和libuv都是开源的跨平台网络库,用于处理网络编程中的底层操作。它们的主要功能是封装对操作系统提供的异步IO接口,使开发者能更加方便地编写高性能的网络应用程序。 libhv是基于C语言的网络库,提供了一套简洁易用的API接口。它开发的衷是为了满足高性能Web服务器的需求,因此在性能和效率方面有很好的表现。它支持多线程、异步IO、定时器、事件派发等功能,可以在处理大量并发连接的场景中保持低延迟和高吞吐量。libhv还能处理HTTP、WebSocket等应用层协议的解析和处理,使开发者能更加专注于业务逻辑的实现。 libuv也是跨平台的网络库,但是与libhv不同的是,libuv更加注重事件驱动的编程模型。它采用了事件循环机制,可以处理大量同时发生的事件,并将事件分发给相应的事件处理器进行处理。libuv的优点在于它的跨平台性和高性能,它的事件循环机制可以充分利用操作系统提供的异步IO接口,使得网络应用程序能在不同的操作系统上实现高度一致的性能。 在使用上,libhv和libuv都可以在不同的操作系统上运行,包括Windows、Linux、macOS等。它们都提供了丰富的API接口,使得开发者能够灵活地处理网络编程中的各种需求。同时,它们都有活跃的开发社区,可以获取到及时的技术支持和更新。 总体来说,libhv和libuv都是优秀的网络库,具有很高的性能和可靠性。它们的出现不仅使网络编程更加容易,也为开发高性能的网络应用提供了很好的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值