io_uring 用法分析 I :异步 IO ,Windows IOCP 接口与 Proactor 模式

在具体研究 io_uring 之前,有必要了解之前的 aio,包括 glib 实现的 POSIX aio 和 Linux 后来提供的只支持  O_DIRECT 的 Linux aio (不支持 socket 因为 socket 不能 O_DIRECT)。不然我实在看不懂 io_uring 目前的资料(和 epoll 铺天盖地的资料实在是没法比啊)。

首先是基本的情况,由于 Linux 早期不支持 Posix ais,所以实际 glib 是在用户态模拟了一个的,实际原理是通过多线程后台同步读写,然后自己维护一个 buffer,然而性能垃圾而且有 bug,只能说异步 IO 的实现有点复杂了属于,大牛都写不出个好用的来(M$ IOCP下面研究一下)

首先有必要搞明白 Proactor 和 Reactor 模型的到底区别在哪里?

One thread per client

  • 最古老的复用是用线程/进程复用,由 kernel 来调度公平地为每个用户服务。如果要做成千上万的并发连接的话,尤其是长连接,就涉及这么多的进程线程调度的开销。
  • 当然,one thread per client 也是会阻塞挂起的。

Reactor

  • 通过 master + workers 的 Reactor 方案则是通过数据结构来维护所有的用户的信息,让特定的 workers 去给他服务。
  • 这里就涉及要用定时器来单线程(尽管有固定数量的多个工作进程(或者线程也行),但是他们是单线程为多个用户服务的)调度每个用户了。
  • 这个过程中如果涉及了阻塞的系统调用,基本的方法是再次注册一个等待事件到 epoll wait 的事件集合中,然后换一个回调函数。
  • 回调函数在 Reactor 的概念是这样的,他就是一个阻塞事件就绪之后调用的东西,比如一个 fd,如果能读写了,他就会醒来,此时 Reactor 会根据 hashmap dispatch 这个回调函数给一个工作线程运行。
  • 此时工作线程运行这个 read 或者 write 就不会阻塞了。
  • 当然,还有一种情况,如果读了一半又要阻塞了怎么办?(由于我们必须让工作线程服务多个用户,所以这个必须是非阻塞调用),或者说用户人态定时的 expiration 到了。
  • 所以一般我们绑定这个 Callback 是一个状态机来的,他必须是保留着状态的,这样才能重新注回到 epoll wait 里面(就涉及要打断正在 blocking 的 epoll wait),并且因为保留了状态(比如说用一个 Object)所以回调函数还是同一个。

Proactor 复习

  • 我之前搞不明白 Proactor 这个有什么区别。其实最直观的区别就是 Proactor 的回调触发是说异步读写 Complete 了的,而 Reactor 回调的时候,读写还没有开始。所以这个东西和 DMA 是差不多的机制的。但是问题是,这样整个架构不是没什么区别吗?无非是去掉了要调用读写的部分,而数据已经准备好了而已。
  • 当然一个区别是,我们不需要再自己调度了,不需要自己维护定时器了,因为事情实际已经委托给一个内核线程来进行读写了。
  • 由于实际网上根本找不到具体讲这个怎么做的(只有一大堆讲个大概原理的),所以只能看各大网络库的实际实现依赖的 OS 接口了,就是微软的 IOCP I/O Completion Ports - Win32 apps | Microsoft Docs。当然,只需要学一下 boost  的 asio 就能明白了,以及 Proactor 的论文还有那本书 POSA2。不过既然都要看 IOCP,就从 IOCP 提供的 API 去看怎么实现 Proactor 吧。
  • 先复习一下 asio 是什么样的架构,以及他对应的部件先吧The Proactor Design Pattern: Concurrency Without Threads - 1.77.0 (boost.org) 

这里注意那个 Completion Event Queue 也是由 OS 实现的,而提供一个访问接口。后面的 io_uring 实际也是这种模型。其他的就注释在图上了,而这个删除 fd 只是一种做法,如果是持续提供服务的,当然只是做一些善后处理,之后重复注册(应该)。

IOCP

  • IOCP 提供的几个接口下面依次介绍(只是 MSDN 浓缩翻译而已,最近发现读英文资料水平大不如从前,明明之前还在跟 15-445,额,好像看 445 视频听力也已经显示颓态了)
  • OVERLAPPED 结构体,这个结构体里面第一部分(规定好)是用来存 IO 事件的状态的,他还可以注册钩子,比如需要启动一个 ReadFile 的 IOCP 行为,就需要放这个结构进去,里面有一个 hevent 的成员,这个东西是 windows 里面的事件,如果 IO completion 了,这个事件就会被触发(所以 windows 的确是直接支持回调的操作的,不过是用事件机制)。

你需要操作系统传递一批数据,于是填了一个overlapped的订单,订单上写着从哪里(数据的偏移位置)开始传输,于是你去干别的事去了。系统完成传输就打个电话(激活hEvent)叫你验收,你看到overlapped订单上系统写着的传输的数据数量验收接收的数据。

From <OVERLAPPED结构是什么意思?怎么用?-CSDN论坛>

  • 首先是 Completion Port 的概念,一个 Completion Port 会管理一系列的 handler (我还是习惯说 fd,不过没关系了)。通过 CreateIoCompletionPort 可以创建一个这样的端口。之后的工作都围绕这个端口进行。(可以认为他是标识那个队列的一个 identifier 吧)。这个 port 的管理是通过 RAII reference count 实现的,所以如果没有文件注册在这个 port 上面的时候,port 的资源会被清理(这些资源比如说 queue 的内存,系统的异步 IO 线程等等)。
  • 然后介绍第一个批处理函数:GetQueuedCompletionStatus 是用来查询这个队列的一个函数(一次只返回一个,批处理的话要用后缀多个 Ex 的函数)。这个函数是一个非阻塞/或者阻塞的函数。至于都已经有了 hevent 了为什么还要用这个呢。是因为这个提供了一种 polling 的机制(poll 叫同步事件分离器,这个 GetQueued 叫异步事件分离器)能让你批量管理一系列的异步回调!如果让自己来写的话,就要自己做一个队列,然后让上面说的 hevent (注意 window 下的 event 是要同步 wait 的这一点不难理解)每次触发了之后就把 overlapped 结构体的信息放到队列里面,然后唤醒一个工作线程。有了这个函数调用之后,就可以一开始就让工作线程阻塞在这个上面,队列的事情让操作系统去负责做了。
  • 他等于是 poll 一个 completion (而 poll 是在等一个 ready,区别在于 completion 可以直接指定完整的读写,而你的 ready 只能通过 low water mark 那些来设置一次 ready 的最多数量,不然自己还要一半一半来搞)。

BOOL GetQueuedCompletionStatus(
  [in]  HANDLE       CompletionPort,
  文件描述符
   out   LPDWORD      lpNumberOfBytesTransferred, 这个是讲他的 IO 一共完成了多少 bytes
  [out] PULONG_PTR   lpCompletionKey, 这个标识了是哪个 IO 请求完成了
  [out] LPOVERLAPPED *lpOverlapped,  结构体里存了异步IO 的完成状态,以及提供一个事件钩子

  [in]  DWORD        dwMilliseconds ,非阻塞超时
);

  • 这个 GetQueued 函数的唤醒机制是这样的,优先唤醒最新阻塞在他上面的(LIFO),然后唤醒的 IO Completion 是最旧的(FIFO)。一旦唤醒成功了,之后整个 thread 都会绑定到这个 port 上(即这个请求组),直到解除绑定(就是他不能再差其他的队列)。
  • 一个高效率的机制是 OS 保证唤醒的时候如果已经 associated (即被唤醒到处理这个任务)的线程超出一个设定的数字之后,他就不会唤醒新的线程,而是只唤醒一个线程,这样就能让一个线程循环除以所有的挤压事件,而不用继续唤醒新的线程增加调度复杂性。

IOCP 和 Proactor

  • 上面说得其实有点云里雾里,最后的感觉还是各种阻塞,这个异步的好处和性能优势到底在哪里呢?

  • 有了上面的基础再回来看这两个图片,其实唯一区别就是工作线程不用进行读写的阻塞/非阻塞调用,仅此而已。但是这个对于性能来说,实际是减少了 Reactor 的很多麻烦的工作的,包括之前讲的我们不需要再自己调度了,不需要自己维护定时器了,因为事情实际已经委托给一个内核线程来进行读写了。
  • 最后补充一个 PostQueuedCompletionStatus 函数,用来投递自定义的 Completion 事件,这个东西的好处是说,如果 master 收到了什么说要关掉所有的 worker 线程,就不用用什么信号机制的,直接用这个投递一个假的完成事件来唤醒所有的 worker。(这个就等同于用 eventfd 来给 epoll wait 统一处理的感觉一样,不过那个是 worker 通知 master,这个是 master 或者某个 worker 通知 worker 而已)。当然,鉴于一个 comletion 只会通知一个 worker,可能要发送好多个,这个经常用于退出时发送一个模拟的IO完成事件来唤醒在等待中的线程。然而一般的实践都是一个线程对应一个 port,所以退出的时候只需要为线程池中的每个线程都调用一次PostQueuedCompletionStatus 就行了。
  • 其实事情就是这样简单而已。利用上面的 API 做到 boost 的 ASIO 里面就是这样的:

  • 这里还有一个问题是 master 和 worker 基本都在干些什么?对于 reactor 来说,master 就是简单的负责监听,然后做的事情是 one clent per。
  • Proactor  的 Proactor 又是谁?worker 又是谁?业务逻辑在哪里写?我不知道。有好多种做法,一种可以在回调里面直接写。比如 accept 的异步回调,如果 accept 回调了,处理完(比如添加到已连接用户中)了又要启动一个新的异步 accept 任务?(没毛病,的确是这样的,listen 维护两个队列,一个是未完成三次握手的,一个是已完成三次握手的,accept 是从已完成三次握手的队列中取出一个而已)。
  • 现在看完 POSAv2 之后我终于搞懂了,不想写了。

  • Proactor 这个东西必须运行 event loop 是为了能保证程序不会退出。操作系统最多把队列和异步调度的工作给做了,至少要他来做同步等待和路由器 dispatch 。至于内核是怎么实现的以及怎么保证异步 IO 能公平调度的话(比如长连接的长时间 IO 怎么实现?epoll 模拟的一个思路是开多个线程内核调度,或者定时器 interrupt 比如每读写特定长度就打断他(比如 10Gbps 的情况下分配 100ms 之类的),还有结合设置低水位来让读写线程休眠或者 epoll)
  • 协程天然适合 Proactor  +  异步 IO。昨晚睡觉一直想到协程有点魔怔了,因为 aio 需要异步执行嘛,有些逻辑又要等 io 完了再执行(即操作完成处理程序做的部分,即 aio 请求的回调函数了)这种情况他就必须睡觉了。或者做 mux 先去处理别的。但是 mux 这个实现本身就是可以用异常控制流来做的(之前写过一篇 call/cc 的笔记,不过内容一般),把 mux 的各个复用抽象成一个个函数,如果这个 io 正在等待,自然就可以 co_await 挂起一个协程(或者 yield 一个临时结果回去),然后运行别的协程。这样整个业务逻辑就可以清晰放在协程内部了。CarterLi/liburing4cpp: Modern C++ binding for liburing (io_uring) that features C++ coroutines support (github.com)
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Mac Rust io-uring是一种在Mac操作系统上使用Rust语言进行开发的io-uring库。 io-uring是Linux内核中的一个新特性,它为应用程序提供了一种高性能、高效率的异步I/O操作方式。它通过使用事件驱动和无锁技术,实现了在高并发环境下进行文件操作的优化。io-uring提供了更低的系统开销和更高的吞吐量,特别适用于需要大量I/O操作的应用程序。 虽然io-uring最初是为Linux内核设计的,但由于其高性能的特性,一些开发者试图将其移植到其他操作系统上。其中,Mac Rust io-uring就是一个在Mac操作系统上使用Rust语言实现io-uring的库。 使用Mac Rust io-uring,开发者可以在Mac环境下利用io-uring的特性来提高文件操作的性能。这对于需要进行大量I/O操作的应用程序来说,是一个很有价值的工具。例如,对于数据库、Web服务器或文件传输等应用,通过使用Mac Rust io-uring,可以显著提高其性能和吞吐量。 Mac Rust io-uring不仅提供了对io-uring的封装,还提供了一些更高级别的功能和接口,以方便开发者使用。开发者可以使用Mac Rust io-uring来实现一些高级的文件操作,例如批量读取或写入文件,提高数据处理的效率。 总之,Mac Rust io-uring是一个在Mac操作系统上使用Rust语言开发的io-uring库,它能够为开发者提供高性能的异步I/O操作方式,从而提高应用程序的性能和吞吐量。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值