io_uring 是 Linux 内核提供的高性能异步 I/O 框架,它在 Linux 5.1 版本中被引入,由 Jens Axboe 开发 。在 io_uring 出现之前,传统的异步 I/O 模型,如 epoll 或者 POSIX AIO,在大规模 I/O 操作中效率较低,存在系统调用开销大、数据拷贝次数多、异步处理能力有限等问题。为了解决这些问题,io_uring 应运而生,它的出现为 Linux 异步 I/O 领域带来了新的解决方案,旨在提供更高效、更强大的 I/O 处理能力。
支持异步、轮询、无锁、零拷贝:io_uring 支持异步操作,应用程序在发起 I/O 请求后不必等待操作完成,可以继续执行其他任务,提高了系统的并发处理能力;它还支持轮询模式,不依赖硬件的中断,通过调用 IORING_ENTER_GETEVENTS 不断轮询收割完成事件,减少了中断开销;同时,io_uring 采用了无锁设计,避免了锁竞争带来的性能损耗;在数据传输过程中,io_uring 支持零拷贝技术,减少了数据在用户空间和内核空间之间的拷贝次数,提高了数据传输的效率。例如,在一个文件传输的场景中,使用 io_uring 可以大大减少数据拷贝的时间,提高文件传输的速度。
解决“拷贝开销大”的问题?
之所以在提交和完成事件中存在大量的内存拷贝,是因为应用程序和内核之间的通信需要拷贝数据,所以为了避免这个问题,需要重新考量应用与内核间的通信方式。我们发现,两者通信,不是必须要拷贝,通过现有技术,可以让应用与内核共享内存。
要实现核外与内核的零拷贝,最佳方式就是实现一块内存映射区域,两者共享一段内存,核外往这段内存写数据,然后通知内核使用这段内存数据,或者内核填写这段数据,核外使用这部分数据。因此,需要一对共享的ring buffer用于应用程序和内核之间的通信。
- 一块用于核外传递数据给内核,一块是内核传递数据给核外,一方只读,一方只写。
- 提交队列SQ(submission queue)中,应用是IO提交的生产者,内核是消费者。
- 完成队列CQ(completion queue)中,内核是IO完成的生产者,应用是消费者。
- 内核控制SQ ring的head和CQ ring的tail,应用程序控制SQ ring的tail和CQ ring的head
io_uring 的核心是两个环形缓冲区:提交队列(Submission Queue,SQ)和完成队列(Completion Queue,CQ)。这两个队列在内核态和用户态之间共享,通过内存映射(mmap)的方式实现。
提交队列(SQ)用于存放用户程序提交的 I/O 请求。当用户程序需要进行 I/O 操作时,它会创建一个提交队列条目(Submission Queue Entry,SQE),并将其放入 SQ 中。每个 SQE 包含了 I/O 操作的详细信息,如操作类型(读、写等)、文件描述符、缓冲区地址、数据长度等。
完成队列(CQ)用于存放内核完成 I/O 操作后的结果。当内核完成一个 I/O 操作后,会将对应的完成队列条目(Completion Queue Entry,CQE)放入 CQ 中。CQE 包含了 I/O 操作的返回值(如读取或写入的字节数、错误码等)以及用户在 SQE 中设置的用户数据。
环形缓冲区的工作方式基于生产者 - 消费者模型。用户程序是 SQ 的生产者,内核是 SQ 的消费者;内核是 CQ 的生产者,用户程序是 CQ 的消费者。通过这种方式,io_uring 实现了用户态和内核态之间高效的通信,减少了系统调用的次数和数据拷贝的开销。例如,在传统的 I/O 模型中,每次 I/O 操作都需要进行系统调用,从用户态切换到内核态,而 io_uring 通过共享的环形缓冲区,用户程序可以直接将 I/O 请求放入 SQ,内核从 SQ 中获取请求并处理,处理完成后将结果放入 CQ,用户程序再从 CQ 中获取结果,避免了频繁的系统调用和上下文切换。
io_uring 的异步 I/O 操作机制是其高性能的关键之一。在传统的 I/O 模型中,当应用程序发起 I/O 请求后,通常需要等待 I/O 操作完成才能继续执行其他任务,这期间应用程序会被阻塞。而在 io_uring 中,用户程序提交 I/O 请求后,无需等待操作完成,就可以继续执行其他任务。
当用户程序将 I/O 请求写入提交队列(SQ)后,内核会异步地处理这些请求。内核会根据请求的类型和参数,执行相应的 I/O 操作,如从磁盘读取数据或向网络发送数据。在 I/O 操作执行过程中,用户程序可以继续执行其他代码,不会被阻塞。当 I/O 操作完成后,内核会将操作结果写入完成队列(CQ),并通过事件通知机制(如 epoll)通知用户程序。用户程序可以通过轮询 CQ 或等待事件通知的方式,获取 I/O 操作的结果,并进行后续处理。这种异步操作方式使得应用程序能够充分利用 CPU 资源,提高了系统的并发处理能力。
例如,在一个文件服务器中,当有多个客户端同时请求读取文件时,使用 io_uring 可以让服务器在处理一个客户端的 I/O 请求时,同时处理其他客户端的请求,而不需要等待每个 I/O 请求都完成后再处理下一个,大大提高了服务器的响应速度和吞吐量。
io_uring 支持批量提交和处理 I/O 请求,这进一步提升了其性能。用户程序可以一次性将多个 I/O 请求写入提交队列(SQ),然后通过一次系统调用(如 io_uring_enter)通知内核处理这些请求。内核会批量处理这些请求,并将结果批量写入完成队列(CQ)。这种批量操作方式减少了系统调用的次数和上下文切换的开销,提高了 I/O 操作的效率。例如,在处理大量文件读写操作时,使用批量操作可以显著减少系统调用的开销,提高文件读写的速度。
此外,io_uring 支持的操作类型非常丰富,不仅包括传统的文件 I/O 操作(如 read、write、open、close 等),还支持网络相关的系统调用,如 send、recv、accept、connect 等。这使得开发者可以使用 io_uring 来构建高性能的网络服务器和应用程序。在开发一个高并发的 Web 服务器时,可以使用 io_uring 来处理客户端的连接请求、数据接收和发送等操作,充分发挥其高性能和异步处理的优势。io_uring 还支持一些其他的系统调用,如文件系统的操作(如 fsync、fdatasync 等),为开发者提供了更强大的功能和更灵活的编程方式。