From 5fba5d21bf2f00ad14776cc0a1b59e8ae9081c66 Mon Sep 17 00:00:00 2001 From: Magic Date: Wed, 2 Feb 2022 00:04:42 +0800 Subject: [PATCH 1/2] =?UTF-8?q?update=20io=5Furing=20=E9=AB=98=E6=95=88=20?= =?UTF-8?q?IO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- io_uring/文章/What is io_uring? | 52 -- io_uring/文章/io_uring 高效 IO.md | 476 +++++++++++++++--- .../文章/智汇华云 | 新时代IO利器-io_uring.md | 343 ------------- io_uring/示例程序:提交队列轮询.c | 145 ------ llvm/文章/LLVM :Clang入门.md | 416 --------------- 5 files changed, 414 insertions(+), 1018 deletions(-) delete mode 100644 io_uring/文章/What is io_uring? delete mode 100644 io_uring/文章/智汇华云 | 新时代IO利器-io_uring.md delete mode 100644 io_uring/示例程序:提交队列轮询.c delete mode 100644 llvm/文章/LLVM :Clang入门.md diff --git a/io_uring/文章/What is io_uring? b/io_uring/文章/What is io_uring? deleted file mode 100644 index 6ff0959..0000000 --- a/io_uring/文章/What is io_uring? +++ /dev/null @@ -1,52 +0,0 @@ -`io_uring` is a new asynchronous I/O API for Linux created by Jens Axboe from Facebook. It aims at providing an API without the limitations of the current [select(2)](http://man7.org/linux/man-pages/man2/select.2.html), [poll(2)](http://man7.org/linux/man-pages/man2/poll.2.html), [epoll(7)](http://man7.org/linux/man-pages/man7/epoll.7.html) or [aio(7)](http://man7.org/linux/man-pages/man7/aio.7.html) family of system calls, which we discussed in the previous section. Given that users of asynchronous programming models choose it in the first place for performance reasons, it makes sense to have an API that has very low performance overheads. We shall see how `io_uring` achieves this in subsequent sections. - -## The io_uring interface - -The very name io_uring comes from the fact that the interfaces uses ring buffers as the main interface for kernel-user space communication. While there are system calls involved, they are kept to a minimum and there is a polling mode you can use to reduce the need to make system calls as much as possible. - -See also - -- [Submission queue polling tutorial](https://unixism.net/loti/tutorial/sq_poll.html#sq-poll) with example program. - -### The mental model - -The mental model you need to construct in order to use `io_uring` to build programs that process I/O asynchronously is fairly simple. - -- There are 2 ring buffers, one for submission of requests (submission queue or SQ) and the other that informs you about completion of those requests (completion queue or CQ). -- These ring buffers are shared between kernel and user space. You set these up with [`io_uring_setup()`](https://unixism.net/loti/ref-iouring/io_uring_setup.html#c.io_uring_setup) and then mapping them into user space with 2 [mmap(2)](http://man7.org/linux/man-pages/man2/mmap.2.html) calls. -- You tell io_uring what you need to get done (read or write a file, accept client connections, etc), which you describe as part of a submission queue entry (SQE) and add it to the tail of the submission ring buffer. -- You then tell the kernel via the [`io_uring_enter()`](https://unixism.net/loti/ref-iouring/io_uring_enter.html#c.io_uring_enter) system call that you’ve added an SQE to the submission queue ring buffer. You can add multiple SQEs before making the system call as well. -- Optionally, [`io_uring_enter()`](https://unixism.net/loti/ref-iouring/io_uring_enter.html#c.io_uring_enter) can also wait for a number of requests to be processed by the kernel before it returns so you know you’re ready to read off the completion queue for results. -- The kernel processes requests submitted and adds completion queue events (CQEs) to the tail of the completion queue ring buffer. -- You read CQEs off the head of the completion queue ring buffer. There is one CQE corresponding to each SQE and it contains the status of that particular request. -- You continue adding SQEs and reaping CQEs as you need. -- There is a [polling mode available](https://unixism.net/loti/tutorial/sq_poll.html#sq-poll), in which the kernel polls for new entries in the submission queue. This avoids the system call overhead of calling [`io_uring_enter()`](https://unixism.net/loti/ref-iouring/io_uring_enter.html#c.io_uring_enter) every time you submit entries for processing. - -See also - -- [The Low-level io_uring Interface](https://unixism.net/loti/low_level.html#low-level) - -## io_uring performance - -Because of the shared ring buffers between the kernel and user space, io_uring can be a zero-copy system. Copying bytes around becomes necessary when system calls that transfer data between kernel and user space are involved. But since the bulk of the communication in `io_uring` is via buffers shared between the kernel and user space, this huge performance overhead is completely avoided. While system calls (and we’re used to making them a lot) may not seem like a significant overhead, in high performance applications, making a lot of them will begin to matter. Also, system calls are not as cheap as they used to be. Throw in workarounds the operating system has in place to deal with [Specter and Meltdown](https://meltdownattack.com/), we are talking non-trivial overheads. So, avoiding system calls as much as possible is a fantastic idea in high-performance applications indeed. - -While using synchronous programming interfaces or even when using asynchronous programming interfaces under Linux, there is at least one system call involved in the submission of each request. In `io_uring`, you can add several requests, simply by adding multiple SQEs each describing the I/O operation you want and make a single call to io_uring_enter. For starers, that’s a win right there. But it gets better. - -You can have the kernel poll and pick up your SQEs for processing as you add them to the submission queue. This avoids the [`io_uring_enter()`](https://unixism.net/loti/ref-iouring/io_uring_enter.html#c.io_uring_enter) call you need to make to tell the kernel to pick up SQEs. For high-performance applications, this means even lesser system call overheads. See [the submission queue polling tutorial](https://unixism.net/loti/tutorial/sq_poll.html#sq-poll) for more details. - -With some clever use of shared ring buffers, `io_uring` performance is really memory-bound, since in polling mode, we can do away with system calls altogether. It is important to remember that performance benchmarking is a relative process with some kind of a common point of reference. According to the [io_uring paper](https://kernel.dk/io_uring.pdf), on a reference machine, in polling mode, `io_uring` managed to clock 1.7M 4k IOPS, while [aio(7)](http://man7.org/linux/man-pages/man7/aio.7.html) manages 608k. Although much more than double, this isn’t a fair comparison since [aio(7)](http://man7.org/linux/man-pages/man7/aio.7.html) doesn’t feature a polled mode. But even when polled mode is disabled, `io_uring` hits 1.2M IOPS, close to double that of [aio(7)](http://man7.org/linux/man-pages/man7/aio.7.html). - -To check the raw throughput of the `io_uring` interface, there is a no-op request type. With this, on the reference machine, `io_uring` achieves 20M messages per second. See [`io_uring_prep_nop()`](https://unixism.net/loti/ref-liburing/submission.html#c.io_uring_prep_nop) for more details. - -## An example using the low-level API - -Writing a small program that reads files and prints them on to the console, like how the Unix `cat` utility does might be a good starting point to get your hands wet with the `io_uring` API. Please see the next chapter for one such example. - -## Just use liburing - -While being acquainted with the low-level `io_uring` API is most certainly a good thing, in real, serious programs you probably want to use the higher-level interface provided by liburing. Programs like [QEMU](https://qemu.org/) already use it. If liburing never existed, you’d have built some abstraction layer over the low-lever `io_uring` interface. liburing does that for you and it is a well thought-out interface as well. In short, you should probably put in some effort to understand how the low-level `io_uring` interface works, but by default you should really be using `liburing` in your programs. - -While there is a reference section here for it, there are some examples based on `liburing` we’ll see in the subsequent chapters. - -> 原文链接:https://unixism.net/loti/what_is_io_uring.html - diff --git a/io_uring/文章/io_uring 高效 IO.md b/io_uring/文章/io_uring 高效 IO.md index bf5a8a3..687bdf6 100644 --- a/io_uring/文章/io_uring 高效 IO.md +++ b/io_uring/文章/io_uring 高效 IO.md @@ -4,80 +4,80 @@ 也就是说,这篇文章和 manpage 之间会有一些重叠。要是什么细节也不提,那也基本不可能讲明白 `io_uring`。 -### 1.0 介绍 +## 1.0 介绍 Linux 上有很多基于文件 IO 的方法。最古老的和最基本的是 `read(2)` 和 `write(2)` 系统调用。后来 `pread(2)` 和 `pwrite(2)` 通过允许传入偏移量的方式增强了它们。之后引入的 `preadv(2)` 和 `pwritev2(2)` 是前者的矢量版本。因为这些系统调用依然不够丰富,Linux 还拥有 `pread2(2)` 和 `pwrite2(2)` 的系统调用,它们进一步扩展了 API 以允许使用修饰符标志。 除去这些系统调用的各种差异,它们的共同特征是同步的接口。这意味着当数据准备就绪(或写入完毕)时,系统调用才结束。 -对于某些次优的情况,需要一个异步的接口。POSIX 拥有 `aio_read(3)` 和 `aio_write(3)` 来满足这个需求,但是实现这个需求过程很枯燥并且性能很差。 +对于某些次优的情况,需要一个异步的接口。POSIX 拥有 `aio_read(3)` 和 `aio_write(3)` 来满足这个需求,但是他们的实现乏善可陈,并且性能很差。 -Linux确实有一个原生的异步 IO 接口,简称为 aio。不幸的是,这个东西(aio)有以下的局限性: +Linux有一个原生的异步 IO 接口,简称为 aio。不幸的是,这个东西(aio)有以下的局限性: -1. **最大的限制条件显然是这东西只能支持 `O_DIRECT`(或者无缓冲的)的异步 IO 访存。** 由于 `O_DIRECT` 本身的限制(缓存绕过,以及大小/对齐约束条件),原生的 aio 接口对大多数场景都不适用。对普通的(有缓冲的)IO,接口的行为是同步的。 +1. **最大的限制条件显然是这东西只能支持 `O_DIRECT`(或称无缓冲)的异步 IO 访存。** 由于 `O_DIRECT` 本身的限制(缓存绕过,以及大小/对齐约束条件),原生的 aio 接口对大多数场景都不适用。对普通的(有缓冲的)IO,接口的行为是同步的。 -2. **即使你满足了 IO 能变成异步的所有条件,有些时候它其实也不是异步的。** 有大把的方法能让 IO 提交最终变成阻塞的 —— 如果进行 IO 需要元数据,提交最后就会阻塞等待那东西。 +2. **即使你满足了 IO 能变成异步的所有条件,有些时候它其实也不是异步的。** 有很多情况能让 IO 提交最终变成阻塞的 —— 如果需要元数据来执行 IO,提交最后就会被阻塞。 - 对于存储设备,同一时间只有固定数目的请求槽位可用。要是这些槽位都被占满了,提交就会阻塞等待,直到有一个能用的槽位。这些不确定的因素意味着依赖提交始终是异步的应用程序仍然被迫卸载这部分。 + 对于存储设备,同一时间只有固定数目的请求槽位可用。要是这些槽位都被占满了,提交就会阻塞等待,直到有一个能用的槽位。依赖于异步提交的程序依然被迫阻塞于这些不确定的因素。 -3. **垃圾 API。** 每个 IO 提交最终都需要拷贝 64+8 字节,每次完成都要拷贝 32 字节。这就得拷贝 104 字节的内存,然而 IO 本应是零拷贝的。取决于你的 IO 大小,这一点肯定很明显。 +3. **垃圾 API。** 每个 IO 提交最终都需要拷贝 64+8 字节,每次 IO 完成都要拷贝 32 字节。这就得拷贝 104 字节的内存,然而 IO 本应是零额外拷贝的。取决于你的 IO 大小,这一点肯定很明显。 - 暴露出的完成事件(completion event)环缓冲区(ring buffer)大多数情况下添麻烦让完成更慢,并且非常非常难(几乎不可能)从应用层正确地使用。 + 公开的完成事件(completion event)环缓冲区(ring buffer)大多数情况下导致 IO 完成更慢,并且非常非常难(几乎不可能)从应用层正确地使用。 - IO 总是需要至少两次系统调用(提交 + 等待完成),而在这些 spectre/meltdown 漏洞影响的日子里会严重拖慢速度。 + IO 总是需要至少两次系统调用(提交 + 等待完成),而在 Intel 修复 spectre/meltdown 漏洞后会严重拖慢速度。 多年以来,人们一直在为解决上述的第一条限制(译注:不支持缓冲的 IO)而做着各种努力(作者本人在 2010 年的时候也试过),但没人成功过。 -从效率的方面来说,低于10微秒时延、超高 IOPS 的硬件设备的到来,使得这接口真正地开始显老了。对于这些类型的设备来说,缓慢和非确定性的提交延迟是很致命的问题,就像单核榨不出多少性能一样。 +从效率的方面来说,支持低于 10 微秒时延、超高 IOPS 的硬件设备的到来,使得这接口开始显老了。对于这些类型的设备来说,缓慢和非确定性的提交延迟是很致命的问题,就像单核榨不出多少性能一样。 -除此之外,因为上述的限制,可以说原生 Linux aio 的用例并不多。它已沦为小众应用的一个角落,以及所有随之而来的问题(长期未发现的 bug等 )。 +除此之外,因为上述的限制,可以说原生 Linux aio 的用例并不多。它已经被归入小众应用的一个角落,带来了随之而来的所有问题(长期未发现的 bug 等)。 -此外,“正常”的应用程序用不着 aio,这说明 Linux 仍然缺乏一个能够提供他们所希望的功能的接口。应用程序或库完全没有理由继续需要创建私有的 IO 卸载线程池来获得像样的异步 IO,尤其是当内核可以更高效地完成这些工作的时候。 +此外,“普通”的应用用不着 aio,这说明 Linux 仍然缺乏一个能够提供他们所希望的功能的接口。应用或库完全没有理由继续需要创建私有的 IO 线程池来获得像样的异步 IO,尤其是当内核可以更高效地完成这些工作的时候。 -### 2.0 改善现状 +## 2.0 改善现状 -最初的工作集中在改善 aio 接口上。并且在此之前,工作进展相当缓慢. +最初的工作集中在改善 aio 接口上。并且在放弃之前,这项工作进展的相当缓慢. 选择此初始方向有多种原因: 1. **如果你扩展和改进现有接口,比提供新接口要好的多。** 采用新的接口需要花费时间,并且要审核和批准新接口可能是一项漫长而艰巨的任务。 -2. **一般而言,这项工作更加轻松。** 作为一个开发者,你总是希望以最少的投入完成最大的产出。扩展现有接口在现有测试基础结构方面为你带来许多优势。 +2. **一般而言,这项工作更加轻松。** 作为一个开发者,你总是希望以最少的投入完成最大的产出。扩展现有接口在已有的测试基础架构方面会带来许多优势。 -现有的 aio 接口由三个主要的系统调用组成:用于设置 aio 上下文的系统调用(`io_setup(2)`)、一个提交IO (`io_submit(2)`)和一个收割和等待IO完成的系统调用(`io_getevents(2)`)。由于需要对多个这些系统调用进行行为更改,因此我们需要添加新的系统调用以传递此信息。 +现有的 aio 接口由三个主要的系统调用组成:用于设置 aio 上下文的系统调用(`io_setup(2)`)、用于提交 IO 的系统调用(`io_submit(2)`)和用于获取和等待 IO 完成的系统调用(`io_getevents(2)`)。由于需要对多个这些系统调用行为进行更改,因此我们需要添加新的系统调用以传递此信息。 -这样就创建了指向同一代码的多个入口点,以及在其他位置的快捷方式。这样的结果在代码复杂性和可维护性方面不是很好,并且最终只能解决其中一个问题。最重要的是,这实际上使其中之一变得更糟,因为现在API的理解和使用更加复杂 +这样就创建了指向同一代码的多个入口点,以及在其他位置的快捷方式。这样的结果在代码复杂性和可维护性方面不是很好,并且最终只能解决 AIO 一个比较突出的问题而已。最重要的是,这实际上使另外的问题变得更糟了,因为修改后的API更难被理解和使用。 -尽管很难放弃一开始的工作而另起炉灶,但是很显然,我们需要一些全新的东西。一个可以让我们实现所有要点的东西。我们需要它具有良好的性能和可扩展性,同时还要使它易于使用,并具有现有接口所缺乏的功能。 +尽管放弃一系列的工作,然后另起炉灶很艰难,但是很显然,我们需要一些全新的东西。一个可以满足我们所有需求的东西。我们需要它具有良好的性能和可扩展性,同时还要使它易于使用,并具有现有接口所缺乏的功能。 -### 3.0 新接口的设计目标 +## 3.0 新接口的设计目标 尽管从头开始不是个很容易做出的决定,这确实让我们有了充分发挥艺术自由、创造新东西的空间。 按重要性由高到低的顺序,主要设计目标是: -1. **用着简单,难以误用。** 一切用户层可见的接口都应以此为主要目标。接口应该直观易用。 -2. **可扩展。** 虽然我的背景主要是与存储相关的,但我希望这套接口不仅仅是面向块设备能用。也就是说,也有可能会出现网络和非块存储设备接口。造新轮子当然要(或者至少尝试)要面向未来嘛。 -3. **功能丰富。** Linux aio 只满足了一部分应用程序的需要。我不想再造一套只覆盖部分功能,或者需要应用重复造轮子(比如 IO 线程池)的接口。 -4. **高效。** 尽管存储 IO 大多都是基于块的,因而大小多在 512 或 4096 字节,但这些大小时候的效率还是对某些应用至关重要的。此外,一些请求甚至可能不携带数据有效载荷。新接口得在每次请求的开销上精打细算。 -5. **可伸缩。** 尽管效率和低时延很重要,但在峰值端提供最佳性能也很关键。特别是在存储方面,我们一直在努力提供一个可扩展的基础设施。新接口应该允许我们将这种可扩展性一直暴露在应用程序中。 +1. **易于使用,难以误用。** 一切用户层可见的接口都应以此为主要目标。接口应该直观易用。 +2. **可扩展。** 虽然我的背景主要是与存储相关的,但我希望这套接口不仅仅是用于面向块的 IO。也就是说,很快会添加网络和非块存储接口。造新轮子当然要(或者至少尝试)面向未来嘛。 +3. **功能丰富。** Linux aio 只满足了一部分应用的需要。我不想再造一套只覆盖部分功能,或者需要应用重复造轮子(比如 IO 线程池)的接口。 +4. **高效。** 尽管存储 IO 大多都是基于块的,因而大小多在 512 或 4096 比特,但在这些大小上的效率还是对某些应用至关重要的。此外,一些请求甚至可能不携带数据有效载荷。新接口得在每次请求的开销上精打细算。 +5. **可伸缩。** 尽管效率和低时延很重要,但在峰值端提供最佳性能也很关键。特别是在存储方面,我们一直在努力提供一个可扩展的基础架构。新接口应该允许我们将这种可扩展性公开给应用。 上面的有些目标可能看起来互相矛盾。高效和可扩展的接口往往很难使用,更重要的是,很难正确使用。既要功能丰富,又要效率高,也很难做到。不过,这些都是我们的目标。 -### 4.0 进入 `io_uring` 新时代 +## 4.0 进入 `io_uring` 新时代 不论设计目标先后如何,最初的设计是以效率为中心。效率不是可以事后考虑的事情,必须从一开始进行设计,一旦接口固定就无法修改。 -我知道我既不需要提交或完成事件的任何内存副本,也不需要间接访存。在以前的基于 aio 的设计结束时,效率和可扩展性都明显受到了 aio 必须处理多个独立副本来处理两方面的 IO 的损害。 +无论是提交或完成事件,我都不想有任何内存副本或间接内存访问。之前基于 aio 的设计时,aio 必须使用多个独立副本来处理两边的 IO,效率和可扩展性都明显受到了损害。 -由于拷贝是不可取的,所以很明显,内核以及程序必须优雅地共享 IO 自身定义的结构并完成事件。如果你把共享的思路发展的足够远,那么把共享数据协调同样驻留在应用与内核共享的内存中是一个自然延伸。一旦你实现了这一飞跃,那么两者之间的同步必须以某种方式进行管理也就很清楚了。 +由于拷贝是不可取的,所以很明显,内核以及程序必须优雅地共享 IO 自身定义的结构并完成事件。如果你把共享的思路发散的足够远,自然会延伸到把共享数据的协调同样驻留在应用与内核共享的内存中。一旦你实现了这一飞跃,那么两者之间的同步必须以某种方式进行管理也就很清楚了。 -一个应用程序无法在不执行系统调用的情况下与内核共享锁,并且系统调用肯定会减少与内核通信的速率。这与我们的效率目标是相悖的。 +一个应用无法在不执行系统调用的情况下与内核共享锁,并且系统调用肯定会减少与内核通信的速率。这与我们的效率目标是相悖的。 -一种满足我们需求的数据结构应该是单生产者单消费者环形缓冲区。有了共享的环形缓冲区,我们可以通过巧妙运用内存顺序(memory ordering)和内存屏障(memory barrier)消除在应用程序和内核之间的共享锁. +一种满足我们需求的数据结构应该是单生产者单消费者环形缓冲区。有了共享的环形缓冲区,我们可以通过巧妙运用内存顺序(memory ordering)和内存屏障(memory barriers)取代在应用和内核之间的共享锁。 -异步接口有两个基本操作:提交请求的操作,以及与所述请求的完成相关联的事件。 +异步接口有两个基本操作:提交请求的操作,以及请求完成后的完成事件。 -对于提交 IO,应用程序是生产者,内核是消费者。而对于完成 IO 则恰好相反,内核会生产完成事件,应用程序则负责消耗它们。因此,我们需要一对环形队列(ring)来为内核和应用程序之间提供一个高效通信通道。 +对于提交 IO,应用是生产者,内核是消费者。而对于完成 IO 则恰好相反,内核会生产完成事件,应用则负责消费它们。因此,我们需要一对环形队列(ring)来为内核和应用之间提供一个高效通信通道。 这对环形队列就是新接口 `io_uring` 的核心。它们被合理命名为提交队列 (submission queue,SQ)以及完成队列(completion queue,CQ),并组成了新接口的基础部分。 @@ -85,7 +85,7 @@ Linux确实有一个原生的异步 IO 接口,简称为 aio。不幸的是, 有了通信基础后,是时候看看如何定义用于描述请求和完成事件的数据结构了。 -完成(completion)方面清晰明了。它需要携带与操作结果有关的信息,以及某种方式将完成情况链接到它所产生的请求上。对于 `io_uring`,选定的布局如下: +完成(completion)方面清晰明了。它需要携带与操作结果有关的信息,以及以某种方式将完成情况链接到来源的请求上。对于 `io_uring`,选定的布局如下: ```c struct io_uring_cqe { @@ -97,17 +97,17 @@ struct io_uring_cqe { 现在我们应该认识了 `io_uring`,`_cqe` 后缀指的是一个完成队列事件(completion queue event),后面就略称为一个 cqe。 -cqe 包含一个 `user_data` 字段。该字段是从请求提交开始就携带的,并且可以包含应用程序识别所述请求所需的任何信息。一种常见的用例是使其成为原始请求的指针。内核不管这个字段,就只把这个字段从提交到完成一直带着走。 +cqe 包含一个 `user_data` 字段。该字段是从请求提交开始就携带的,并且可以包含应用识别所述请求所需的任何信息。一种常见的用例是使其成为指向原始请求的指针。内核不会改动这个字段,只是简单地把这个字段从提交(submission)传递给完成事件(completion event)。 -`res` 字段是请求的结果。把它想象成系统调用的返回值。对于普通的读/写操作,这就像 `read(2)` 或 `write(2)` 的返回值一样。成功就返回传输了多少字节。失败就返回错误代码的相反数。比如发生了 IO 错误,`res` 的值就是 `-EIO`。 +`res` 字段是请求的结果。可以把它类比成系统调用的返回值。对于普通的读/写操作,这就像 `read(2)` 或 `write(2)` 的返回值一样。成功就返回传输了多少字节。失败就返回一个负的错误代码。比如发生了 IO 错误,`res` 的值就是 `-EIO`。 最后,结构体的 `flags` 字段携带与本次操作有关的元信息数据。目前这个字段还没用上。 ------ -定义请求类型更复杂。它不仅需要描述比完成事件更多的信息,另外这也是 `io_uring` 的一个设计目标,即可以扩展到未来的请求类型。 +请求(request)类型的定义会更加复杂。它不仅需要描述比完成事件更多的信息,还要考虑到 `io_uring` 的一个设计目标,即对请求类型未来的扩展。 -目前想到的长这样: +目前的定义如下: ```c struct io_uring_sqe { @@ -133,31 +133,29 @@ struct io_uring_sqe { }; ``` +> 译注:该结构已经更新,详细信息可以查看 [Submission Queue Entry](https://unixism.net/loti/ref-liburing/sqe.html) + 和完成事件类似,提交端的数据结构叫做提交队列项(Submission Queue Entry),或者简称 sqe。 -里面存着一个 `opcode` 字段,描述本次请求的操作码,也就是究竟干点啥。比如,有一个操作码叫 `IORING_OP_READV`,也就是向量读(vectored read)。 +里面存着一个 `opcode` 字段,描述本次请求的操作码,表示当前的请求的操作。比如,有一个操作码叫 `IORING_OP_READV`,也就是向量读取(vectored read)。 -`flags` 字段包含各命令类型通用的修饰选项。我们将在之后的进阶使用场景部分提及。 +`flags` 字段包含跨命令类型通用的修饰符标志。我们将在之后的进阶使用场景部分提及。 -`ioprio` 是本次请求的优先级别。对一般的读写操作,这就和 `ioprio_set(2)` 系统调用的定义里的一样。 +`ioprio` 是本次请求的优先级别。对一般的读写操作,这个字段遵循了 `ioprio_set(2)` 系统调用的定义。 -`fd` 字段是与本次请求相关联的文件描述符(file descriptor),`off` 字段是这个操作在 `fd` 的多少偏移量发生。 +`fd` 字段是与本次请求相关联的文件描述符(file descriptor),`off` 字段是这个操作执行时基于 `fd` 的偏移量。 -如果操作码描述了一个传输数据的操作,那么 `addr` 字段就包含在应该在哪个地址进行这个操作。例如,如果操作是一个向量读/向量写之类的操作,那么这个 `addr` 字段里就是一个指向 `iovec` 结构体数组的指针,就和 `preadv(2)` 里用的一样。 +如果操作码描述了一个传输数据的操作,那么 `addr` 字段就包含在应该在哪个地址进行这个操作。例如,如果操作是一个向量读/向量写之类的操作,那么这个 `addr` 字段里就是一个指向 `iovec` 结构体数组的指针,就和 `preadv(2)` 里用的一样。对于非向量化的 IO 传输,`addr` 就必须直接包含目标地址。 -对于非向量化的 IO 传输,`addr` 就必须直接是目标地址。 +这就引入了另一个字段 `len`,对非向量传输来说这是要传输的字节量,而对向量传输来说就是 addr 指向的向量个数( `iovec` 结构体数组的长度)。 -这就引入了另一个字段 `len`,对非向量传输来说这是要传输的字节量,而对向量传输来说就是 `iovec` 结构体数组元素的数目。 +接下来(`union`)是用于特定操作码的 flag 的一个集合。例如,对于上面提到的向量读(`IORING_OP_READV`),这些 flag 就和 `preadv2(2)` 系统调用中要求的保持一致。 -接下来是一组 flag,指定了与操作码有关的东西。例如,对于上面提到的向量读(`IORING_OP_READV`),这些 flag 就和 `preadv2(2)` 系统调用中要求的保持一致。 - -`user_data` 在操作码中是通用的,并且内核不会去碰这个东西。当这个请求的完成事件被提交之后,它仅仅被拷贝到完成队列事件结构体(cqe)中。 +`user_data` 在操作码中是通用的,并且内核不会去碰这个东西。当这个请求的完成事件被发布时,它被拷贝到完成队列事件结构体(cqe)中。 `buf_index` 之后在进阶用法中会提到的。 -最后,在这个结构的末尾,有一些填充(padding)。这样做是为了确保 sqe 在内存中以 64 字节的大小良好排列,同时也是为了将来可能需要包含更多数据以描述请求的情况。 - -这里有几个能想到的用例 —— 一个是键/值存储命令集,另一个是端到端数据保护,其中程序为其想要写入的数据传递一个预先计算的校验和(checksum)。 +最后,在这个结构的末尾,有一些填充(padding)。这样做是为了确保 sqe 在内存中以 64 字节的大小对齐,同时也是为了将来可能需要包含更多数据以描述请求的情况。这里有几个能想到的用例 —— 一个是键/值存储命令集,另一个是端到端数据保护,其中程序为其想要写入的数据传递一个预先计算的校验和(checksum)。 ### 4.2 通讯信道 @@ -167,13 +165,13 @@ struct io_uring_sqe { 就像上一节一样,让我们从简单的开始,先来讲讲完成环(completion ring)。 -完成请求项(cqe)被组织成了一个数组,支撑其的内存对应用程序和内核来说都是可见、可改的。然而,由于完成请求项(cqe)是内核产生的,只有内核在事实上修改 cqe 项目。 +完成请求项(cqe)被组织成了一个数组,其对应的内存对应用和内核来说都是可见、可改的。然而,由于完成请求项(cqe)是内核生成的,实际上只有内核在修改 cqe 条目。 -通讯是通过一个环缓冲区管理的。当一个新事件被内核提交到完成请求环(CQ 环)中时,它会更新与其相连的尾节点。当程序从中消耗一项时,它会更新头节点。因此,只要头尾节点不同,应用就知道还有一个或更多事件可以用来消耗。 +通讯是通过一个环缓冲区管理的。当一个新事件被内核提交到完成请求环(CQ 环)中时,它会更新与其相连的尾节点。当程序从中消费一项时,它会更新头节点。因此,只要头尾节点不同,应用就知道还有一个或更多事件可以用来消费。 -环计数器(ring counter)本身是自由流动的 32 位整数,当环中事件的数目超过环的容量时,依靠自然进位处理。这种方法的一个优点是,我们可以利用整个环的大小,而不必在一旁管理一个“环满了”的 flag(这将让环的管理变得复杂)。因此,环的尺寸必须是 2 的整数次方。 +环计数器(ring counter)本身是自由流动的 32 位整数,当环中事件的数目超过环的容量时,依靠自然进位(译注:二进制进位)处理。这种方法的一个优点是,我们可以利用环的完整容量,而不必额外管理一个“环满了”的 flag(这将让环的管理变得复杂)。因此,环的尺寸必须是 2 的整数次方。 -要查找事件的索引,应用程序必须用环的大小掩码(size mark)对当前尾索引(current tail index)进行掩码(masking)操作。大概看起来像是这样: +要查找事件的索引,应用必须用环的大小掩码(size mark)对当前尾索引(current tail index)进行掩码(masking)操作。大概看起来像是这样: ```c unsigned head; @@ -198,9 +196,9 @@ write_barrier(); `ring->cqes[]` 是 `io_uring_cqe` 结构的共用数组。下一小节中我们将深入讨论这个共享内存(以及 `io_uring` 实例本身)如何设置和管理,以及神奇的读写屏障操作究竟有什么用。 -对提交方来说,角色是正好相反的。应用程序是更新尾部的那一个,而内核负责消耗项目(并更新头部)。一个很重要的区别是,尽管完成队列环(CQ ring)直接索引 cqe 的共享数组,提交方与这些元素之间还有一个中间数组(indirection array)。因此,提交端的环形缓冲区是这个数组的索引,而数组又包含到 sqes 中的索引。 +对提交请求来说,角色是正好相反的。应用负责更新环位,而内核负责消费条目(并更新环头)。一个很重要的区别是,尽管完成队列环(CQ ring)直接索引 cqe 的共享数组,提交方与这些元素之间还有一个中间数组(indirection array,译注:中间数组包含多个 sqe)。因此,提交端的环形缓冲区是这个数组的索引,而该数组又包含 sqes 的索引。 -这在一开始可能看起来很奇怪和令人困惑,但这是有原因的。有些应用程序可能会在内部数据结构中嵌入请求单元,这允许它们灵活地这样做,同时保留在一个操作中提交多个 sqe 的能力。这反过来使得上述应用程序更容易迁移到 `io_uring` 的接口。 +这在一开始可能看起来很奇怪和令人困惑,但这是有原因的。有些应用可能会在内部数据结构中嵌入请求单元,这允许它们灵活地在一个操作中提交多个 sqe,使得上述应用更容易迁移到 `io_uring` 的接口。 添加一个 sqe 供内核使用基本上是从内核获取 cqe 的相反操作。一个典型的例子是这样的: @@ -224,23 +222,377 @@ sqring->tail = tail; write_barrier(); ``` -和 CQ 环一样,读写屏障将在后面解释。上面是一个简化的例子,它假设 SQ 环当前是空的,或者至少它还有多个项目的空间。 +和 CQ 环一样,读写屏障将在后面解释。上面是一个简化的例子,它假设 SQ 环当前是空的,或者至少它还有一个及以上条目的空间。 -一旦内核使用了 sqe,应用程序就可以自由地重用这个 sqe 条目。即使在内核还没有完全使用给定的 sqe 的情况下也是如此。如果内核在条目被使用之后确实需要访问它,那么它将获得一个稳定的副本。为什么会发生这种情况并不一定重要,但是它对应用程序有一个重要的副作用。 +一旦内核使用了一个 sqe,应用就可以自由地重用这个 sqe 条目。即使在内核尚未完全完成该 sqe 的情况下也是如此。如果内核在条目被消费之后确实需要再次访问它,那么它将获得一个稳定的副本。为什么会发生这种情况并不一定重要,但是它对应用有一个重要的副作用。 -通常情况下,应用程序会请求一个给定大小的环,并且假设这个大小可能直接对应于应用程序在内核中可以有多少个挂起的请求。但是,由于 sqe 生存期(lifetime)仅仅是实际提交的 sqe 生存期,因此应用程序可能会开出比 SQ 环大小指示的更高的挂起请求计数。应用程序必须注意不要这样做,否则可能会有 CQ 环溢出的风险。 +通常情况下,应用会请求一个给定大小的环,并且假设这个大小可能直接对应于应用在内核中可以有多少个挂起的请求。但是,由于 sqe 生存期(lifetime)仅仅是实际提交的 sqe 生存期,因此应用可能会开出比 SQ 环大小指示的更高的挂起请求计数。应用必须注意不要这样做,否则可能会有 CQ 环溢出的风险。 -默认情况下,CQ 环的大小是 SQ 环的两倍。这使得应用程序在管理这方面具有一定的灵活性,但并不能完全消除这样做的必要性。如果应用程序确实违反了这个限制,它将被追踪为 CQ 环中的一个溢出条件。稍后会有更多细节。 +默认情况下,CQ 环的大小是 SQ 环的两倍。这使得应用在管理这方面具有一定的灵活性,但并不能完全消除这样做的必要性。如果应用确实违反了这个限制,它将被追踪为 CQ 环中的一个溢出条件。稍后会有更多细节。 -完成事件可以以任何顺序到达,请求提交和关联完成之间没有顺序。SQ 环和 CQ 环相互独立运行。然而,完成事件将始终对应于给定的提交请求。因此,完成事件总是与特定的提交请求相关联。 +> 译注:现在内核提供了CQ环溢出后保证事件不丢失的能力(io_uring CQ ring backpressure),详见:https://lore.kernel.org/io-uring/20191106235307.32196-1-axboe@kernel.dk/ -### 5.0 `io_uring` 接口 +完成事件可以以任何顺序到达,请求提交和相应的完成之间没有顺序。SQ 环和 CQ 环相互独立运行。然而,完成事件将始终对应于给定的提交请求。因此,完成事件总是关联于特定的提交请求(之后)。 -就像 aio 一样,`io_uring` 也有许多与之相关的系统调用来定义它的操作。第一个是一个系统调用,用来设置一个 `io_uring` 实例: +## 5.0 `io_uring` 接口 + +就像 aio 一样,`io_uring` 也有许多与之相关的系统调用来定义它的操作。第一个是用来设置 `io_uring` 实例的系统调用: ```c int io_uring_setup(unsigned entries, struct io_uring_params *params); ``` +应用必须为这个 io_uring 实例提供所需的条目数量,以及与之相关的一组参数。`entries`表示将与这个 io_uring 实例相关的 seq 的数量,它必须是2的幂数,范围是 [1, 4096]。`params`结构会被内核读取和写入,它被定义为: + +```c +struct io_uring_params { + __u32 sq_entries; + __u32 cq_entries; + __u32 flags; + __u32 sq_thread_cpu; + __u32 sq_thread_idle; + __u32 resv[5]; + struct io_sqring_offsets sq_off; + struct io_cqring_offsets cq_off; +}; +``` + +`sq_entries`会被内核填写,让应用知道这个环支持 sqe 条目的数量。同样地,对于 cqe 条目,`cq_entries`告诉应用 CQ 环有多大。除了通过 io_uring 设置基本通信所必要的`sq_off`和`cq_off`字段,这个结构的其余部分将被推迟到高级用例部分再讨论。 + +在成功调用`io_uring_setup(2)`后,内核将返回一个指向 io_uring 实例的文件描述符,这时`sq_off`和`cq_off`便会派上用场。鉴于 sqe 和 cqe 结构由内核和应用所共享,应用需要一种对该内存访问的方法。这里使用`mmap(2)`将其映射到应用的内存空间,应用使用`sq_off`来计算各个环成员的偏移量。 + +`io_sqring_offsets`结构的定义如下: + +```c +struct io_sqring_offsets { + __u32 head; /* offset of ring head */ + __u32 tail; /* offset of ring tail */ + __u32 ring_mask; /* ring mask value */ + __u32 ring_entries; /* entries in ring */ + __u32 flags; /* ring flags */ + __u32 dropped; /* number of sqes not submitted */ + __u32 array; /* sqe index array / + __u32 resv1; + __u64 resv2; +}; +``` + +为了访问 sqe 共享内存,应用必须使用 io_uring 文件描述符和 SQ 环内存偏移量来调用`mmap(2)`。io_uring 的 API 定义了以下 mmap 偏移量,供应用使用: + +```c +#define IORING_OFF_SQ_RING 0ULL +#define IORING_OFF_CQ_RING 0x8000000ULL +#define IORING_OFF_SQES 0x10000000ULL +``` + +`IORING_OFF_SQ_RING`用于将 SQ 环映射到应用内存空间,`IORING_OFF_CQ_RING`同理用于 CQ 环, `IORING_OFF_SQES`用于映射 sqe (中间)数组。cqe “数组”是 CQ 环自身的一部分,而 SQ 环记录的是 sqe 数组的索引值,所以 sqe 数组必须被独立映射。 + +应用将定义自己的结构用于保存这些偏移量。一个可能的例子如下: + +```c +struct app_sq_ring { + unsigned *head; + unsigned *tail; + unsigned *ring_mask; + unsigned *ring_entries; + unsigned *flags; + unsigned *dropped; + unsigned *array; +}; +``` + +一个典型的设置案例: + +```c +struct app_sq_ring app_setup_sq_ring(int ring_fd, struct io_uring_param *p){ + struct app_sq_ring sqing; + void *ptr; + + ptr = mmap(NULL, p->sq_off.array + p->sq_entries * sizeof(__u32), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, ring_fd, IORING_OFF_SQ_RING); + + sqring->head = ptr + p->sq_off.head; + sqring->tail = ptr + p->sq_off.tail; + sqring->ring_mask = ptr + p->sq_off.ring_mask; + sqring->ring_entries = ptr + p->sq_off.ring_entries; + sqring->flags = ptr + p->sq_off.flags; + sqring->dropped = ptr + p->sq_off.dropped; + sqring->array = ptr + p->sq_off.array; + + return sqring +} +``` + +CQ 环的映射与之类似,`cq_off`和`IORING_OFF_CQ_RING`用于映射 CQ 环。最后,使用`IORING_OFF_SQES`映射 sqe 数组。 + +由于这些是可以在应用间复用的样板代码, liburing 库接口提供了一组函数,用于帮助简单完成设置和内存映射。关于这方面的细节,请参见io_uring库部分。完成以上操作后,应用就可以通过 io_uring 实例进行通信了。 + +应用生产了新的请求需要处理时,还需要一种方式用于通知内核。这通过另一个系统调用完成: + +```c +int io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t sig); +``` + +`fd`指`io_uring_setup(2)`返回的环文件描述符。`to_submit`告诉内核准备被消费和提交的 sqe 的数量。`min_complete`要求内核等待该数量的请求的完成。 + +这种调用兼具提交和等待请求完成,意味着应用可以通过一次该系统调用同时完成这两种需求。 + +`flags`包含修改调用行为的标识符,最重要的一个是: + +```c +#define IORING_ENTER_GETEVENTS (1U << 0) +``` + +如果`flags`中设置了`IORING_ENTER_GETEVENTS`,那么内核将会主动等待`min_complete`个完成事件。看起来`min_complete`和`IORING_ENTER_GETEVENTS`在功能上有重复,但是在有些情况下,这种区别是很重要的,这将在后面介绍。目前而言,如果希望等待完成,必须同时设置`IORING_ENTER_GETEVENTS`。 + +以上基本涵盖了 io_uring 的基本 API。`io_uring_setup(2)`将根据给定大小创建 io_uring 实例。实例设置完成后,应用可以填充 sqe 并且使用`io_uring_enter(2)`来提交它们。等待完成可以在同一个(`io_uring_enter`)调用中设置,或者稍后单独调用。 + +除非应用想要等待请求完成,它可以只检查 CQ 环尾是否有可用事件。内核将直接修改 CQ 环尾,因此应用可以直接消费(环中的)完成事件,而不必一定使用带`IORING_ENTER_GETEVENTS`的集合的`io_uring_enter(2)`。 + +可以通过`io_uring_enter` man page 来查看命令类型和使用方法。 + +### 5.1 sqe 排序 + +通常 sqe 被独立使用,也就是说一个 sqe 的执行不会影响环中后续 sqe 的执行和顺序。这使操作具有充分的灵活性,并且使它们能够以最大效率和性能并行地执行和完成。 + +可能需要排序的用例是保证数据完整性的写入。一个常见例子是一系列写操作,然后执行 fsync/fdatasync。只要我们可以允许以任何顺序完成写入,我们只需要关心在所有写入完成后执行数据同步。应用通常把它转换为一个写-等待操作,然后在所有的写被底层存储确认后发起同步。 + +io_uring 支持在所有先前的请求完成后再开始消费(drain)提交方队列。这允许应用排列上述的同步操作,并且知道在所有之前的命令完成前不会开始(新的消费)。需要在 sqe 的`flags`字段设置`IOSQE_IO_DRAIN`以完成上述操作。 + +需要注意,这将使整个提交队列停滞。取决于 io_uring 在具体应用中的使用,这可能导致比预期更大的流水线停顿(pipeline bubbles)。如果这类操作经常发生,应用可以使用一个独立的 io_uring 实例来负责完整性写入,以便更好地同时执行其他不相关命令。 + +### 5.2 链式 sqe + +虽然`IOSQE_IO_DRAIN`包括一个完整的流水线屏障,但 io_uring 也支持更精细的 sqe 序列控制。链式 sqe 描述了更大的提交环内的一系列 sqe 间的依赖性,其中每个 sqe 的执行都依赖于前一个 sqe 的成功完成。链式 sqe 可以实现一系列有序的写操作,或者类似复制的操作,比如共享两个 sqe 的缓冲区,读取一个文件后写入到另一个文件。 + +应用通过在 sqe 的`flags`字段中设置`IOSQE_IO_LINK`来使用链式 sqe。设置后,sqe 不会在上一个 sqe 未成功完成前启动。如果前一个 sqe 没有完全完成,那么链将会断开,链式 sqe 将会被取消,并返回错误码`-ECANCELED`。在这里,完全完成(fully complete)是指请求完成完全成功(the fully successful completion of the request)。任何错误或者潜在的读写问题都会中断链,请求必须完全完成(the request must complete to its full extent)。 + +只要在`flags`字段中设置了`IOSQE_IO_LINK`,sqe 链就会一直存在。因此该链定义为从第一个设置了`IOSQE_IO_LINK`的 sqe 开始,到后续的第一个未设置该标识符的 sqe 结束。该链支持任意长度。 + +链的执行独立于提交环中的其他 sqe。链是一个独立的执行单位,多条链可以并行执行和完成。(执行单位)包括不属于任何链的 sqe。 + +### 5.3 超时命令 + +尽管大部分 io_uring 支持的命令都致力于数据,无论是 read/write 这类直接操作,还是 fsync 这类间接命令,但是超时命令稍有出入。`IORING_OP_TIMEOUT`帮助实现在完成环上的等待,而非数据相关的工作。超时命令支持两种不同的触发方式,它们可以同时在一个命令中使用。 + +一种触发方式是经典超时,调用者传递一个具有非零秒/纳秒值的`timespec`结构。为了保证32位与64位应用和内核空间之间的兼容性,必须使用以下格式: + +```c +struct __kernel_timespec { + int64_t tv_sec; + long long tv_nsec; +}; +``` + +在某些时候,用户空间也应该有一个`timespec64`结构来匹配这个描述。在此之前,必须使用上述(timespec)结构。如果需要定时超时,sqe 的 addr 字段必须指向这种结构类型,指定的时间过后,将会执行超时命令。 + +第二种触发方式是完成计数。如果采用这种方式,需要在 sqe 的 `offset` 字段填写完成计数值(completion count value)。经过指定数量的完成后,(超时)队列中的超时命令将会被执行。 + +一条超时命令可以同时指定两种超时方式。如果超时命令包含两种触发条件,只要有一个满足触发的条件就会生成超时完成事件。当一个超时完成事件被发布时,所有完成事件的等待者都会被唤醒,无论它们需要的完成量是否满足。 + +## 6.0 内存排序 + +通过 io_uring 实例进行兼顾安全和高效的通信的一个重要方面就是正确使用内存排序原语(memory ordering primitives)。本文的范围不包含对各种内存排序架构的详细介绍。如果你乐于使用 liburing 库提供的简化的 io_uring API,那么你可以略过此章节,并且跳转到 liburing 库章节阅读。如果你对使用原始接口有兴趣,那么理解本章节就很重要。 + +简单起见,我们将其简化为两个简单的内存排序操作。为了保证文章简短,解释会被简化。 + +`read_barrier()`:确保执行后续内存读前,之前的写操作是可见的。 +`write_barrier()`:确保写操作有序。 + +根据讨论的架构不同,两者中的一个或两个可能是无操作的(no-ops)。使用 io_uring 时,这一点无关紧要。重要的是在某些架构中我们需要它们,因此应用开发者应当了解如何实现它们。 + +write_barrier() 需要确保写操作的顺序。比方说,一个应用想要填写一个 sqe,并通知内核有可供消费的 sqe。这可以分为两个操作阶段——首先,填写不同的 sqe 成员,并将 sqe 索引存放在 SQ 环数组中;然后,更新 SQ 环尾,并且通知内核可以消费新的条目。 + +在没有任何顺序要求的情况下,处理器以它认为最理想的任何顺序重新排列这些写操作是合法行为。让我们来看看下面的例子,每个数字表示一个内存操作: + +```c + 1: sqe→opcode = IORING_OP_READV; + 2: sqe→fd = fd; + 3: sqe→off = 0; + 4: sqe→addr = &iovec; + 5: sqe→len = 1; + 6: sqe→user_data = some_value; + 7: sqring→tail = sqring→tail + 1; +``` + +(上述例子)无法确保使 sqe 向内核可见的写操作7,会在写入顺序的最后被执行。至关重要的是,所有在写操作7之前的写入操作都要在写操作7(完成前)可见,不然内核可能会看到一个写了一半的 sqe。从应用的角度来看,在通知内核有新的 sqe 之前,需要写屏障来确保写操作的正确顺序。由于实际上 sqe 能以任意顺序写入,只要它们能够在环尾写入(完成)前可见就行,我们可以通过在写操作6之后,写操作7之前使用一个排序原语来解决。因此写入顺序如下所示: + +```c + 1: sqe→opcode = IORING_OP_READV; + 2: sqe→fd = fd; + 3: sqe→off = 0; + 4: sqe→addr = &iovec; + 5: sqe→len = 1; + 6: sqe→user_data = some_value; + write_barrier(); /* ensure previous writes are seen before tail write */ + 7: sqring→tail = sqring→tail + 1; + write_barrier(); /* ensure tail write is seen */ +``` + +内核在读取 SQ 环尾前,会使用 read_barrier() 来确保应用的环尾写操作可见。在 CQ 环这边,因为消费者/生产者角色的转换,应用只需要在读取 CQ 环尾前使用 read_barrier() 来确保内核的任意写操作是可见的。 + +尽管内存排序被简化为两种具体类型,架构实现还是会根据代码运行的机器不同而不同。即使应用直接使用 io_uring 接口(而非 liburing 帮助函数),它仍然需要架构特定的屏障类型。liburing 库提供这些屏障(函数),建议在应用中使用。 + +有了内存屏障的基本解释和 liburing 库提供管理它们的帮助函数,不妨回过头看看先前使用了 read_barrier() 和 write_barrier() 的例子,希望能为你解惑。 + +## 7.0 liburing 库 + +排除了 io_uring 内部细节,你现在可以放心地学习一种更简单的方式来完成上述大部分操作。liburing 库有两个目标: + + - 免于使用模板化代码设置 io_uring 实例 + - 简化基础使用场景需要的 API + +后者确保应用不必担心内存屏障(的设置),也不必自己管理任何环缓冲区。这使 API 更加易于理解和使用,并且不需要去了解内部工作细节。如果我们只是关注基于 liburing 提供的示例,那么本文将会短得多,但是多了解一些内部工作原理,将对提高应用性能多有助益。 + +此外,liburing 目前专注于减少(使用)模板化代码并且为标准使用场景提供基础帮助函数。liburing 暂时还不能提供一些更高级的特性。然而,这并不意味着你不能混用这两者(译注:应指在标准使用场景下使用 liburing helper,在使用高级特性场景下手动设置)。它们在底层使用相同的结构。即使应用使用了原始接口,仍然推荐(更换)使用 liburing 提供的设置帮助。 + +### 7.1 liburing io_uring 设置 + +让我们从一个例子开始。liburing 提供了以下基本的帮助函数,它的功能和手动调用`io_uring_setup(2)`并随后对三个必要的区域(CQ 环、SQ 环和 sqe )进行`mmap(2)`操作相同: + +```c + struct io_uring ring; + io_uring_queue_init(ENTRIES, &ring, 0); +``` + +io_uring 结构保存了 SQ 环和 CQ 环的信息,并且`io_uring_queue_init(3)`的调用处理了所有的设置逻辑。在这里例子中,我们给`flags`参数传递了 0 值。 + +一旦一个应用使用完一个 io_uring 实例,它可以简单地调用: + +```c + io_uring_queue_exit(&ring); +``` + +来拆卸这个实例。和应用分配的其他资源相似,一旦应用退出,它们就会被内核自动回收。对于应用坑已经创建的任何 io_uring 实例都是这样。 + +### 7.2 liburing 提交和完成 + +一个非常基本的使用场景是,提交一个请求,然后等待它完成。如下是使用 liburing 帮助函数: + +```c + struct io_uring_sqe sqe; + struct io_uring_cqe cqe; + + /* get an sqe and fill in a READV operation */ + sqe = io_uring_get_sqe(&ring); + io_uring_prep_readv(sqe, fd, &iovec, 1, offset); + + /* tell the kernel we have an sqe ready for consumption */ + io_uring_submit(&ring); + + /* wait for the sqe to complete */ + io_uring_wait_cqe(&ring, &cqe); + + /* read and process cqe event */ + app_handle_cqe(cqe); + io_uring_cqe_seen(&ring, cqe); +``` + +这些不言自明。最后的`io_uring_wait_cqe(3)`调用将会返回我们提交的相应的 sqe 的完成事件,前提是没有其他 sqe 正在运行。如果有,那么完成事件可能是对应其他 sqe 的。 + +如果应用只想查看完成(结果)而非等待完成事件,可以调用`io_uring_peek_cqe(3)`。在两种情况下(译注:指 io_uring_wait_cqe(3) 和 io_uring_peek_cqe(3)),应用都需要调用`io_uring_cqe_seen(3)`来处理完成事件,否则即使重复调用`io_uring_wait_cqe(3)`或`io_uring_peek_cqe(3)`,都只会返回(与之前)相同的完成事件。这种分隔(处理)是必要的,使内核能够避免在应用处理完之前覆盖掉现有的完成事件。`io_uring_cqe_seen(3)`会增加 CQ 环头,使内核可以在(被消费的完成事件)同一位置填写新的事件。 + +有很多帮助函数能够填写 sqe,例如`io_uring_prep_readv(3)`。我建议应用尽可能地利用 liburing 提供的帮助函数。 + +liburing 库仍处于起步阶段,并且正在不断开发,以扩展支持的功能和可用的帮助函数。 + +## 8.0 高级用例和特性 + +上述例子和使用场景适用于各种类型的 IO,有基于`O_DIRECT`文件的 IO、缓冲式 IO、套接字 IO等。(使用 io_uring 时)无需特别关注,即可确保它们的正确操作和异步性。不过,io_uring 的确提供了一些可供应用选择的(额外)功能。以下小节将描述其中的大多数功能。 + +### 8.1 固定文件和固定缓冲区 + +每当一个文件描述符被填入 sqe 并提交给内核时,内核必须要检索到一个对该文件的引用。一旦 IO 完成,该文件引用就会被删除。由于文件引用的原子性,在高 IOPS 的工作负载下,这可能会导致明显的减速。为了缓解这个问题,io_uring 为 io_uring 实例提供了一种预注册文件集的方法。这由一个新的系统调用完成: + +```c +int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args); +``` + +`fd`是 io_uring 实例环的文件描述符。`opcode`代表将要注册的类型,对于注册文件集而言,需要使用`IORING_REGISTER_FILES`。`arg`必须指向应用准备打开的文件描述符的数组,并且`nr_args`必须包含该数组的长度。 + +一旦`io_uring_register(2)`成功注册了文件集,应用就可以将文件集数组的索引填入 sqe->fd 字段(从而取代实际的文件描述符),并且将 sqe->flags 字段设为`IOSQE_FIXED_FILE`来标记 sqe->fd 指向的是文件集索引,从而能够使用相关文件。即使已经注册了文件集,应用仍可自由地继续使用未注册的文件,这通过将 sqe->fd 改为未注册的 fd,并且不在 sqe->flags 中设置`IOSQE_FIXED_FILE`来实现。 + +当 io_uring 实例被移除后,注册的文件集会被自动释放,或者在`io_uring_register(2)`调用中的`opcode`字段设置`IORING_UNREGISTER_FILES`来实现(释放注册文件集)。 + +还可以注册一个固定 IO 缓冲区(fixed IO buffer)集合。使用`O_DIRECT`时,内核必须将应用内存页映射到内核中,用以向其执行 IO,并且在 IO 结束后取消对这些页的映射。这些操作的开销很大。如果应用复用 IO 缓冲区,就总共只需要进行一次映射和取消映射,不用每次 IO 操作都进行。 + +为了注册一个固定 IO 缓冲区集,需要在`io_uring_register(2)`调用中的`opcode`字段设置`IORING_REGISTER_BUFFERS`。`args`必须包含填写好每个 iovec 地址和长度的 iovec 结构体数组。`nr_args`表示 iovec 数组的长度。 + +在成功注册了固定 IO 缓冲区集的基础上,应用可以使用`IORING_OP_READ_FIXED`和`IORING_OP_WRITE_FIXED`来从这些缓冲区执行 IO。当使用这些固定操作码(fixed op-codes)时,sqe->addr 必须包含这些缓冲区中至少一个的地址,并且 sqe->len 必须为请求的长度(以字节为单位)。应用可能会注册比给定的 IO 操作大的缓冲区,一个固定的读/写只是一个固定缓冲区的一部分是完全合法的。 + +### 8.2 轮询 IO + +对追求低延迟的应用,io_uring 提供了对文件的轮询 IO的支持。本文所提到的轮询,是指执行 IO 时不依赖于硬件中断来发出完成事件信号。使用轮询 IO 后,应用将反复向硬件驱动询问已提交的 IO 请求的状态。这和使用非轮询 IO 时,应用一般会休眠等待硬件中断来唤醒是不同的。对极低延迟的设备而言,轮询能够显著提高性能。对极高 IOPS 的应用而言也一样,高中断率会使非轮询的负载开销大得多。在延迟或总体 IOPS 率方面,(采用)轮询是否有意义的界限,取决于应用、IO 设备和机器的性能。 + +要使用 IO 轮询,需要在`io_uring_setup(2)`调用中的 io_uring_params->flags 字段设置`IORING_SETUP_IOPOLL`,或者使用 liburing 库的`io_uring_queue_init(3)`帮助函数。使用轮询后,因为不再有自动触发的异步的硬件端的完成事件,应用不再能够通过 CQ 环尾来检查可用的完成。应用必须通过调用`io_uring_enter(2)`,并在该调用中设置`IORING_ENTER_GETEVENTS`和`min_complete`,来主动查询并获取这些事件。`min_complete`代表希望获取的完成事件数量,设置`IORING_ENTER_GETEVENTS`和`min_complete` = 0 是合法的,在轮询 IO 中,这要求内核只检查(一下)驱动端(是否有)完成事件,而不必循环进行。 + +在使用`IORING_SETUP_IOPOLL`注册的(轮询的) io_uring 实例上,只有在轮询时有意义的操作码才能够被使用。这些操作码包括任意读/写命令:`IORING_OP_READV`、`IORING_OP_WRITEV`、`IORING_OP_READ_FIXED`和`IORING_OP_WRITE_FIXED`。在已注册为轮询的 io_uring 实例上,使用非轮询的操作码时不合法的。这么做会使`io_uring_enter(2)`返回`-EINVAL`错误码。背后的原因是,内核不知道`io_uring_enter(2)`调用使用`IORING_ENTER_GETEVENTS`设置时能否安全地休眠以等待事件(唤醒),或者它需要主动轮询事件。 + +### 8.3 内核侧轮询 + +虽然 io_uring 已经高效地通过更少的系统调用来发布和完成更多的请求,某些场景下我们可以通过进一步减少系统调用的数量来提升 IO 执行的效率。内核侧轮询就是这样的一种功能。启用该功能后,应用不再需要通过`io_uring_enter(2)`来提交 IO。当应用填写了新的 sqe 并更新了 SQ 环,内核侧将会自动发现新的(一个或多个)sqe 并且提交他们。这一切通过一个针对该 io_uring 实例的内核线程完成。 + +为了使用这个功能,需要在`io_uring_params->flags`字段设置`IORING_SETUP_SQPOLL`,通过调用`io_uring_setup(2)`或者传递给`io_uring_queue_init(3)`来注册 io_uring 实例。此外,如果应用希望特定 CPU 运行该线程,可以通过在`io_uring_params->flags`字段设置`IORING_SETUP_SQ_AFF`,并且在`io_uring_params->sq_thread_cpu`设置想使用的 CPU。注意,使用`IORING_SETUP_SQPOLL`设置 io_uring 实例是一种特权操作,如果用户没有足够权限,`io_uring_setup(2)`或`io_uring_queue_init(3)`会返回`-EPERM`错误码。 + +当 io_uring 实例不活跃时,为了避免浪费太多 CPU(性能),内核侧的线程会在空闲一段时间后自动休眠。发生这种情况时,线程将会在 SQ 环的`flags`字段设置`IORING_SQ_NEED_WAKEUP`。设置该值后,应用将无法以来内核自动查找新 sqe,它必须设置`IORING_ENTER_SQ_WAKEUP`并调用`io_uring_enter(2)`(用于重新唤醒线程)。引用侧的逻辑如下所示: + +```c + /* fills in new sqe entries */ + add_more_io(); + /* + * need to call io_uring_enter() to make the kernel notice the new IO + * if polled and the thread is now sleeping. + */ + if ((*sqring→flags) & IORING_SQ_NEED_WAKEUP) + io_uring_enter(ring_fd, to_submit, to_wait, IORING_ENTER_SQ_WAKEUP); +``` + +只要应用一直保持执行 IO,就不会用到`IORING_SQ_NEED_WAKEUP`,我们不需要执行任意系统调用就能够实现高效执行 IO。然而,重要的是在线程休眠时,应用程序能够保持与上述类似的逻辑(唤醒线程)。可以通过`io_uring_params->sq_thread_idle`字段来设置线程的具体闲置时间,这个值以毫秒为单位。如果没有设置该值,内核默认在将线程置为休眠状态前等待 1 秒的闲置时间。 + +对于“一般的”中断驱动的 IO,应用可以通过直接查看 CQ 环来找到完成事件。如果使用`IORING_SETUP_IOPOLL`注册 io_uring 实例,那么内核将会负责获取完成事件。对于两种情况(中断和轮询的选择),除非应用希望等待 IO 发生,否则它可以简单地查看 CQ 环来查找事件。 + +## 9.0 性能 + +最后,io_uring 达到了既定的设计目标。我们通过两个不同的环,获得了一个内核和应用间十分高效的传递机制。虽然在应用中正确使用原始接口需要谨慎一些,但主要的复杂之处在于需要使用显式的内存排序原语。这些可以被归结为发布和处理事件时在提交和完成方面的一些具体细节,通常在不同的应用中遵循着相同的模式。随着 liburing 的不断成熟,我希望它提供的 API 能够满足大多数应用的需求。 + +虽然本文的目的不在于全面细节地介绍 io_uring 的性能和可扩展性,但本节将会简要谈谈在这一领域的优势。更多的细节可以看看[[1]](https://lore.kernel.org/linux-block/20190116175003.17880-1-axboe@kernel.dk/)。注意,由于在阻塞方面的进一步优化,这些结果可能会有些过时。例如,在我的测试环境中,使用 io_uring 的每核心峰值大约为 1700K 4k IOPS,而非 1620K。注意,这些数值是不绝对的,它们大多用来衡量相应的优化。我们将不断使用 io_uring 挖掘更低的延迟和更高的峰值性能,现在应用和内核间的通信机制不再是(制约延迟和峰值性能的)瓶颈。 + +### 9.1 原始性能 + +有很多方式能够查看接口的原始性能,大多数测试会涉及内核的其他部分。上面的数字就是一个例子,我们通过随机从块设备或文件中读取来测量性能。峰值性能方面,io_uring 使用轮询获得了 1.7M 4k IOPS。aio 获得了低得多的 608K。这里的比较有点不公平,因为 aio 不支持轮询 IO。如果我们禁止使用轮询,io_uring 能够在(另一个)相同的测试用例下达到 1.2M IOPS。这方面 aio 的缺陷很明显,在相同工作负载下,io_uring 的 IOPS 是 aio 的两倍。 + +io_uring 还支持 no-op 命令,该命令主要用于检查接口的原始吞吐量。根据所使用的系统,观察到的消息数从 12M 每秒(我的笔记本)到 20M 每秒(用于其他引用结果的测试环境)不等。实际结果根据具体的测试案例有很大的不同,主要受必须执行的系统调用数量的约束。原始接口(性能)和内存相关,由于提交和完成的信息很小,并且在内存中线性排列,因此实现的消息速率可以非常高。 + +### 9.2 缓冲异步性能 + +我之前提到过,内核空间缓存的 aio 的实现会比用户空间中的更加高效。一个主要原因是数据缓存与否。当进行有缓冲区的 IO 时,应用通常严重依赖于内核的页缓存(kernels page cache)以获得更好的性能。使用用户空间的应用无法知道它下一个需要的数据是否已经被缓存。应用可以查询这个信息,但是这需要更多的系统调用,并且这个信息未必十分可靠——现在被缓存的数据未必在几毫秒后依然被缓存。因此,一个使用 IO 线程池的应用通常必须用异步的上下文处理请求,这导致至少两次的上下文切换。如果请求的数据已经在页缓存中,将导致性能的急剧下降。 + +io_uring 处理这种情况就像处理其他可能阻塞应用的资源一样。更重要的是,对于不会造成阻塞的操作,数据将以内联方式提供。这使得 io_uring 对于已经在页缓存中的(数据的) IO 来说,和常规的同步接口一样高效。一旦 IO 提交的调用返回,同时CQ 环中就会出现一个等待被应用消费的完成事件,同时数据就会已经被复制。 + +## 10.0 延伸阅读 + +鉴于(io_uring)是一个全新的接口,我们现在还没有大规模使用。截至本文写作时,带有该接口的内核正处于 -rc 阶段。即使有一个相当完整的接口描述,学习研究 io_uring 对于充分理解如何有效使用它也是很有帮助的。 + +一个例子是 fio [[2]](git://git.kernel.dk/fio)附带的 io_uring 引擎。除了注册文件集,它能够使用上述提到的所有高级功能。 + +另一个例子是也由 fio 附带的 t/io_uring.c 样本基准应用。它对文件或设备做简单的随机读取,通过可配置的设置来探索高级用例的完整功能集。 + +liburing 库[[3]](git://git.kernel.dk/liburing)有一套完整的系统调用接口的手册,值得一读。它还附带了一些测试程序,既有针对开发中发现的问题的单元测试,也有技术演示。 + +LWN也写了一篇关于 io_uring 早期阶段的精彩文章[[4]](https://lwn.net/Articles/776703/)。请注意,io_uring 在该文章写完后有了一些新变化,因此我建议在两篇文章有出入之处以本文为准。 + +## 11.0 参考文献 + +[1] https://lore.kernel.org/linux-block/20190116175003.17880-1-axboe@kernel.dk/ + +[2] git://git.kernel.dk/fio + +[3] git://git.kernel.dk/liburing + +[4] https://lwn.net/Articles/776703/ + +版本:0.4, 2019-10-15 + + + > 原文链接:https://kernel.dk/io_uring.pdf diff --git a/io_uring/文章/智汇华云 | 新时代IO利器-io_uring.md b/io_uring/文章/智汇华云 | 新时代IO利器-io_uring.md deleted file mode 100644 index e21cf37..0000000 --- a/io_uring/文章/智汇华云 | 新时代IO利器-io_uring.md +++ /dev/null @@ -1,343 +0,0 @@ -> io_uring是kernel 5.1中引入的一套新的syscall接口,用于支持异步IO。随着客户在高性能计算中的求解问题规模的越来越大,对计算能力和存储IO的需求不断增长,并成为计算和存储技术发展最直接的动力。本文将对io_uring的原理和功能进行分析,让大家了解io_uring的性能以及其应用场景、发展趋势。 - -io_uring是kernel 5.1中引入的一套新的syscall接口,用于支持异步IO。随着客户在高性能计算中的求解问题规模的越来越大,对计算能力和存储IO的需求不断增长,并成为计算和存储技术发展最直接的动力。本文将对io_uring的原理和功能进行分析,让大家了解io_uring的性能以及其应用场景、发展趋势。 - -**概述** - -高性能计算是继理论科学和实验科学之后科学探索的第三范式,被广泛应用在高能物理研究、核武器设计、航天航空飞行器设计、国民经济的预测和决策等领域,对国民经济发展和国防建设具有重要的价值。它作为世界高技术领域的战略制高点,已经成为科技进步的重要标志之一,同时也是一个国家科技综合实力的集中体现。 - -作为信创云计算专家,华云数据已经在政府金融、国防军工、教育医疗、能源电力、交通运输等十几个行业打造了行业标杆案例,客户总量超过30万。在高性能计算方面也部下重局。在华云数据的研发和客户需求调研中发现,随着客户在高性能计算中的求解问题规模的越来越大,对计算能力和存储IO的需求不断增长,并成为计算和存储技术发展最直接的动力。 - -io_uring是Linux Kernel在v5.1版本引入的一套新异步编程框架,同时支持Buffer IO和Direct IO。io_uring 在设计之初就充分考虑框架自身的高性能和通用性,不仅仅面向传统基于块设备的FS/Block IO领域,对网络异步编程也提供支持,以通用的系统调用提供比肩spdk/dpdk 的性能。 - - **IO演变过程** - -**1、基于fd的阻塞式 I/O:read()/write()** - -![image](https://user-images.githubusercontent.com/87457873/149942207-3af8a5c4-0c46-43be-8247-5176d414dabc.png) - -作为大家最熟悉的读写方式,Linux 内核提供了基于文件描述符的系统调用,这些描述符指向的可能是存储文件(storage file),也可能是network sockets: - -ssize_t read(int fd, void *buf, size_t count); - -ssize_t write(int fd, const void *buf, size_t count); - -二者称为阻塞式系统调用(blocking system calls),因为程序调用这些函数时会进入sleep状态,然后被调度出去(让出处理器),直到 I/O 操作完成: - -- 如果数据在文件中,并且文件内容已经缓存在page cache中,调用会立即返回; -- 如果数据在另一台机器上,就需要通过网络(例如 TCP)获取,会阻塞一段时间; -- 如果数据在硬盘上,也会阻塞一段时间。 - -缺点: - -随着存储设备越来越快,程序越来越复杂, 阻塞式(blocking)已经这种最简单的方式已经不适用了。 - -**2、非阻塞式 I/O:select()/poll()/epoll()** - -阻塞式之后,出现了一些新的、非阻塞的系统调用,例如select()、poll()以及更新的epoll()。应用程序在调用这些函数读写时不会阻塞,而是立即返回,返回的是一个已经ready的文件描述符列表。 - -![image](https://user-images.githubusercontent.com/87457873/149942253-f714b31b-f8c8-46b3-8c73-cf307fbb8d60.png) - -缺点:只支持network sockets和pipes——epoll()甚至连storage files都不支持。 - -**3、线程池方式** - -对于storage I/O,经典的解决思路是thread pool:主线程将 I/O分发给worker线程,后者代替主线程进行阻塞式读写,主线程不会阻塞。 - -![image](https://user-images.githubusercontent.com/87457873/149942269-bec5c706-707e-4667-a5d7-239784765786.png) - -这种方式的问题是线程上下文切换开销可能非常大,通过性能压测会看到。 - -**4、Direct** **I/O(数据库软件):绕过 page cache** - -随后出现了更加灵活和强大的方式:数据库软件(database software)有时并不想使用操作系统的page cache,而是希望打开一个文件后,直接从设备读写这个文件(direct access to the device)。这种方式称为直接访问(direct access)或直接 I/O(direct I/O)。 - -需要指定O_DIRECT flag; - -- 需要应用自己管理自己的缓存 —— 这正是数据库软件所希望的; -- 是zero-copy I/O,因为应用的缓冲数据直接发送到设备,或者直接从设备读取。 - -**5、异步IO** - -随着存储设备越来越快,主线程和worker线性之间的上下文切换开销占比越来越高。现在市场上的一些设备。换个方式描述,更能让我们感受到这种开销:上下文每切换一次,我们就少一次dispatch I/O的机会。因此Linux 2.6 内核引入了异步I/O(asynchronous I/O)接口。 - - - -![image](https://user-images.githubusercontent.com/87457873/149942297-b3711c0b-39ce-4512-9b8e-fc5221d64ab7.png) - -Linux 原生AIO 处理流程: - -- 当应用程序调用io_submit系统调用发起一个异步IO 操作后,会向内核的 IO 任务队列中添加一个IO 任务,并且返回成功。 -- 内核会在后台处理 IO 任务队列中的IO 任务,然后把处理结果存储在 IO 任务中。 -- 应用程序可以调用io_getevents系统调用来获取异步IO 的处理结果,如果 IO 操作还没完成,那么返回失败信息,否则会返回IO 处理结果。 - -从上面的流程可以看出,Linux 的异步 IO 操作主要由两个步骤组成: - -- 调用 io_submit 函数发起一个异步 IO 操作。 -- 调用 io_getevents 函数获取异步 IO 的结果。 - -AIO的缺陷 - -- 仅支持direct IO。在采用AIO的时候,只能使用O_DIRECT,不能借助文件系统缓存来缓存当前的IO请求,还存在size对齐(直接操作磁盘,所有写入内存块数量必须是文件系统块大小的倍数,而且要与内存页大小对齐。)等限制,直接影响了aio在很多场景的使用。 -- 仍然可能被阻塞。语义不完备。即使应用层主观上,希望系统层采用异步IO,但是客观上,有时候还是可能会被阻塞。io_getevents(2)调用read_events读取AIO的完成events,read_events中的wait_event_interruptible_hrtimeout等待aio_read_events,如果条件不成立(events未完成)则调用__wait_event_hrtimeout进入睡眠(当然,支持用户态设置最大等待时间)。 -- 拷贝开销大。每个IO提交需要拷贝64+8字节,每个IO完成需要拷贝32字节,总共104字节的拷贝。这个拷贝开销是否可以承受,和单次IO大小有关:如果需要发送的IO本身就很大,相较之下,这点消耗可以忽略,而在大量小IO的场景下,这样的拷贝影响比较大。 -- API不友好。每一个IO至少需要两次系统调用才能完成(submit和wait-for-completion),需要非常小心地使用完成事件以避免丢事件。 -- 系统调用开销大。也正是因为上一条,io_submit/io_getevents造成了较大的系统调用开销,在存在spectre/meltdown(CPU熔断幽灵漏洞,CVE-2017-5754)的机器上,若如果要避免漏洞问题,系统调用性能则会大幅下降。在存储场景下,高频系统调用的性能影响较大。 - -软件工程中,在现有接口基础上改进,相比新开发一套接口,往往有着更多的优势,然而在过去的数年间,针对上述限制的很多改进努力都未尽如人意。终于,全新的异步IO引擎io_uring就在这样的环境下诞生了。 - - - -**io_uring原理** - -io_uring 来自资深内核开发者 Jens Axboe 的想法,他在 Linux I/O stack 领域颇有研究。从最早的patch aio: support for IO polling 可以看出,这项工作始于一个很简单的观察:随着设备越来越快,中断驱动(interrupt-driven)模式效率已经低于轮询模式(polling for completions)。 - -- io_uring 的基本逻辑与 linux-aio 是类似的:提供两个接口,一个将 I/O 请求提交到内核,一个从内核接收完成事件。 -- 但随着开发深入,它逐渐变成了一个完全不同的接口:设计者开始从源头思考 如何支持完全异步的操作。 - -**1、原理及核心数据结构:** - -每个 io_uring 实例都有两个环形队列(ring),在内核和应用程序之间共享: - -- 提交队列:submission queue (SQ) -- 完成队列:completion queue (CQ) - -![image](https://user-images.githubusercontent.com/87457873/149942323-e05da76e-264d-486d-b824-0a29f7d992b3.png) - -这两个队列: - -- 都是单生产者、单消费者,size 是 2 的幂次; -- 提供无锁接口(lock-less access interface),内部使用 内存屏障做同步(coordinated with memory barriers)。 - -使用方式: - -- 请求 - -- 应用创建 SQ entries (SQE),更新 SQ tail; -- 内核消费 SQE,更新 SQ head。 - -- 完成 - -- 内核为完成的一个或多个请求创建 CQ entries (CQE),更新 CQ tail; -- 应用消费 CQE,更新 CQ head。 -- 完成事件(completion events)可能以任意顺序到达,到总是与特定的 SQE 相关联的。 -- 消费 CQE 过程无需切换到内核态。 - -**2、 三种工作模式** - -io_uring 实例可工作在三种模式: - -**(1)中断驱动模式(interrupt driven)** - -默认模式。可通过 io_uring_enter() 提交 I/O 请求,然后直接检查 CQ 状态判断是否完成。 - -**(2)轮询模式(polled)** - -Busy-waiting for an I/O completion,而不是通过异步 IRQ(Interrupt Request)接收通知。 - -这种模式需要文件系统(如果有)和块设备(block device)支持轮询功能。相比中断驱动方式,这种方式延迟更低,但可能会消耗更多 CPU 资源。 - -目前,只有指定了 O_DIRECT flag 打开的文件描述符,才能使用这种模式。当一个读 或写请求提交给轮询上下文(polled context)之后,应用(application)必须调用 io_uring_enter() 来轮询 CQ 队列,判断请求是否已经完成。 - -对一个io_uring 实例来说,不支持混合使用轮询和非轮询模式。 - -**(3)内核轮询模式(kernel polled)** - -这种模式中,会创建一个内核线程(kernel thread)来执行SQ 的轮询工作。 - -使用这种模式的 io_uring 实例,应用无需切到到内核态就能触发(issue)I/O 操作。通过SQ来提交SQE,以及监控CQ的完成状态,应用无需任何系统调用,就能提交和收割 I/O(submit and reap I/Os)。 - -如果内核线程的空闲时间超过了用户的配置值,它会通知应用,然后进入idle状态。这种情况下,应用必须调用io_uring_enter()来唤醒内核线程。如果 I/O 一直很繁忙,内核线性是不会 sleep 的。 - -**3、重要特性** - -- IORING_SETUP_SQPOLL:创建一个内核线程进行sqe的处理(IO 提交),几乎完全消除用户态内核态上下文切换,消除spectre/ meltdown 缓解场景下对系统调用的性能影响,此特性需要额外消耗一个cpu核。 -- ORING_SETUP_IOPOLL:配合blk-mq多队列映射机制,内核IO 协议栈开始真正完整支持IOpolling。 -- IORING_REGISTER_FILES/ IORING_REGISTER_FILES_UPDATE/ IORING_UNREGISTER_FILES:减少fget/ fput原子操作带来的开销 -- IORING_REGISTER_BUFFERS/ IORING_UNREGISTER_BUFFERS:通过提 前向内核 注册 buffer 减少 get_user_pages / unpin_user_pages 开销,应用适配存在一定难度。 -- IORING_FEAT_FAST_POLL:网络编程新利器,向 epoll 等传统基于事件驱动的网络编程模型发起挑战,为用户态提供真正的异步编程 API 。 -- ASYNC BUFFERED READS:更好的支持异步 buffered reads ,但当前对于异步 buffered write 的支持还不够完。 - - - -**功能对比** - -**1、Synchronous I/O、 Libaio和IO_uring对比** - -![image](https://user-images.githubusercontent.com/87457873/149942361-1c19bf0f-818f-4cec-9556-d42a0f3a92c3.png) - -**2、io_uring和spdk的对比** - -![image](https://user-images.githubusercontent.com/87457873/149942381-3a9f6d36-9991-476a-941e-8750b1e3666a.png) - - - -**性能表现** - - - -![image](https://user-images.githubusercontent.com/87457873/149942400-b45feae5-dfb8-4b7f-94f0-ee9ce3604b3d.png) - -非polling模式,io_uring相比libaio提升不是很明显;在polling模式下,io_uring能与spdk接近,甚至在queue depth较高时性能更好,完爆libaio。 - -![image](https://user-images.githubusercontent.com/87457873/149942424-17bb8dea-5bef-4b06-9d31-5a8d2f559779.png) - -在queue depth较低时有约7%的差距,但在queue depth较高时基本接近。 - -![image](https://user-images.githubusercontent.com/87457873/149942440-4924df96-279e-4858-861f-be93aabc00f3.png) - -以上数据来自来自Jens Axboe的测试:https://lore.kernel.org/linux-block/20190116175003.17880-1-axboe@kernel.dk/ - -结论: - -io_uring在非polling模式下,相比libaio,性能提升不是非常显著。 - -io_uring在polling模式下,性能提升显著,与spdk接近,在队列深度较高时性能更好。 - -**io_uring在kvm-qemu下的性能表现** - -**1、直接启用io_uring的场景** - -下面测试结果在一台华云超融合一体机测试,qemu 5.0 已经支持了Asynchronous I/O的io_uring,只需要将虚拟机将驱动改成io_uring。 - -原理如下: - - - -![image](https://user-images.githubusercontent.com/87457873/149942467-a7b1e4f6-e6e6-430a-b2f3-5ea67e18005c.png) - - - -- guestOS的kernel和qemu通信通过 virtqueue -- io_uring通过SQ和CQ进行IO的管理 - -宿主机环境信息: - -![image](https://user-images.githubusercontent.com/87457873/149942487-2a429103-f3b6-4f94-b273-0c50319b8399.png) - -虚拟机环境信息 - -![image](https://user-images.githubusercontent.com/87457873/149942499-6d91b523-daf2-4abd-852c-e1cb5017a7e2.png) - -测试参数 - -``` -[global] -ioengine=libaio -time_based -direct=1 -thread -group_reporting -randrepeat=0 -norandommap -numjobs=4 -ramp_time=10 -runtime=600 -filename=/dev/sdb - -[randwrite-4k-io32] -bs=4k -iodepth=32 -rw=randwrite -stonewall - -[randread-4k-io32] -bs=4k -iodepth=32 -rw=randread -stonewall -``` - -测试数据如下: - -![image](https://user-images.githubusercontent.com/87457873/149942516-0ce842ca-85e8-4ee1-8b4f-6a0fe9adb8f8.png) - -总结: - -1)、libaio和io_uring性能比较,io_uring性能提高不明显,没有跨越式的提升。 - -2)、目前io_uring 的virtio block开发还在持续进行,poll模式等高级功能没有release。很值得期待。具体参见:https://wiki.qemu.org/Features/IOUring - -**2、io passthrough场景** - -原理: - -![image](https://user-images.githubusercontent.com/87457873/149942524-a776e41b-6545-4107-b18a-fcec4b53f9de.png) - -直接透传QEMU Block层 - -- io_uring的SQ和CQ都在GuestOS的kernel中 -- qemu的virtio-blk修改了“fast path” -- 启用了polling模式 - -测试环境版本信息: - -![image](https://user-images.githubusercontent.com/87457873/149942562-a724ac3f-f0b6-4484-b99e-921524186101.png) - -创建虚拟机 - -aio=io_uring -device virtio-blk-pci,drive=hd1,bootindex=2,io-uring-pt=on - -测试结果: - -![image](https://user-images.githubusercontent.com/87457873/149942598-6875fb40-8bd6-4143-a71a-d8f04f99b43e.png) - -测试数据来自: - -https://static.sched.com/hosted_files/kvmforum2020/9c/KVMForum_2020_io_uring_passthrough_Stefano_Garzarella.pdf - -**总结:** - -1)、virtio-blk的性能只有裸设备性能的一半左右。 - -2)、io_uring passthrough的性能和裸设备持平。 - -3)、缺点需要在guestOS的kernel中打patch。 - - - -**社区支持情况** - -**1、内核支持情况** - -![image](https://user-images.githubusercontent.com/87457873/149942648-62ae7e45-f25a-484c-96e2-aef574cdd63b.png) - -**2、io_uring 社区开发现状** - -当前 io_uring 社区开发主要聚焦在以下几个方面。 - -- **io_uring 框架自身的迭代优化** - 前面提到,io_uring 作为一种新型高性能异步编程框架,当前仍处于快速发展中。因此随着特性越来越丰富,以及各种稳定性问题的修复等等,代码也变得越来越臃肿。因此 io_uring 框架也需要同步”进化“,不断进行代码重构优化。 - 通常异步编程一般是将工作交由线程池来做,但对于 io_uring 来说,这只是最坏的 slow path,例如异步读优化,就是想尝试尽量在当前上下文中处理。另外,在 sqpoll 模式下,io_uring 接管用户提交的系统调用,一个系统调用的执行与特定的进程上下文相关,因此 io_uring 需要维护系统调用进程的内存上下文,文件系统上下文等大量信息。同时 io_uring 作为一种框架,框架本身的开销应该尽可能的小,才能与用户态高性能框架 SPDK 对标,因此需要持续优化。 -- **特性增强** - -- io_uring buffer registration 特性增强 - io_uring 的 buffer registration 特性可以用来减少读写操作时频繁的 get_user_pages()/unpin_user_pages() 调用,get_user_pages() 主要用来实现用户态虚拟地址到内核页的转换,会消耗一定的 cpu 资源。来自 Oracle 的同学这组patchset 让 buffer registration 特性支持更新和共享操作,更加方便用户态编程,目前已发到第三版。我们遇到一些业务也明确提出过需要 buffer registration 支持更新操作。 -- fs/userfaultfd: support iouring and polling -- 来自 VMware 的同学这组 patchset 使得 userfaultfd 支持 io_uring,且支持 polling 模式。在RDMA,持久内存等高速场景,系统调用用户态内核态上下文切换的开销在 userfualtfd 整体开销的占比会非常突出,支持 io_uring 后,可以显著减少用户态内核态上下文切换,带来明显的性能提升。 -- add io_uring with IOPOLL support in scsi layer -- io_uring 出现后,Linux 内核才真正完整地支持 iopoll,iopoll 相比于中断模式能带来明显的性能提升。此前只有nvme driver 对 iopoll 提供比较好的支持,现在 scsi 层也开始准备支持 iopoll。 -- no-copy bvec -- 在用 io_uring 进行 IO 栈性能分析时,来自 Facebook 的 Pavel Begunkov 发现在 direct IO 场景下是不需要拷贝bvec 结构的,他提交的这组 patchset 可以进一步提高内核 direct IO 的性能。 -- io_uring and Optane2 -- block 社区已经开始重视提高 IO 栈性能,一种思路是利用高性能设备,借助 io_uring 压测 IO 栈,从而发现软件性能瓶颈。比如 Intel提供最新的 Gen2 Optane SSD 给 block / io_uring 的维护者 Jens Axboe 来做内核 IO 栈性能分析。 -- fs: Support for LOOKUP_NONBLOCK / RESOLVE_NONBLOCK -- Jens Axboe 优化 vfs 的文件查询逻辑,从而可以使 io_uring 的 IORING_OP_OPENAT 操作效率更高。 - -- **IO 栈协同优化** - 借助 io_uirng 这一高性能异步编程框架,能比较容易地发现其他内核子系统的软件性能瓶颈,从而对其进行优化,以提高内核 IO 栈的整体性能。我们之前也基于这个思路发现了 block 层的几处优化,同时提出了 device mapper polling 打通的 RFC。 - - - -**总结** - -1、华云数据产品技术中心始终贴近用户,解决用户的实际问题,同时时刻关注业界的先进技术。得益于io_uring的优异的表现,华云数据在此方面投入大量资源。同时我们在RDMA、SPDK、vdpa、DPU、io_uring、eBPF等热门技术也有相关的研究及产品化的丰富经验,同时对社区不断的共享。 - -2、io_uring 是完全为性能而生的新一代 native async IO 模型,比 libaio 高级不少。通过全新的设计,共享内存,IO 过程不需要系统调用,由内核完成 IO的提交, 以及Polled IO机制,实现了高IOPS。相比kernel bypass,这种原生内核的方式显得友好一些。一定程度上,io_uring代表了Linux kernel block层的未来,至少从中我们可以看出一些block层进化的方向,而且我们看到io_uring也在快速发展,相信未来io_uring会更加的高效、易用,并拥有更加丰富的功能和更强的扩展能力。这让我们充满期待,NVMe的存储时代需要一个属于自己的高速IO引擎。 - -3、在kvm方面,qemu支持io_uring,在操作和运维方便比spdk的vhost简单很多。虽然支持的还不是很完善,但社区仍然在积极推进。希望这个早日release。 - -4、国内阿里在io_uring走的比较靠前,阿里有个专门的内核组在做相关的开发。OpenAnolis已经将io_uring纳入,并且在io_uring方面做了很多探索。通过社区的一些文章可以看到,阿里在数据库、web、echo_sever等相关的领域都已经应用了io_uring。 - -> 原文链接:https://www.huayun.com/news/1521.html - diff --git a/io_uring/示例程序:提交队列轮询.c b/io_uring/示例程序:提交队列轮询.c deleted file mode 100644 index 750bdf4..0000000 --- a/io_uring/示例程序:提交队列轮询.c +++ /dev/null @@ -1,145 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#define BUF_SIZE 512 -#define FILE_NAME1 "/tmp/io_uring_sq_test.txt" -#define STR1 "What is this life if, full of care,\n" -#define STR2 "We have no time to stand and stare." - -void print_sq_poll_kernel_thread_status() { - - if (system("ps --ppid 2 | grep io_uring-sq" ) == 0) - printf("Kernel thread io_uring-sq found running...\n"); - else - printf("Kernel thread io_uring-sq is not running.\n"); -} - -int start_sq_polling_ops(struct io_uring *ring) { - int fds[2]; - char buff1[BUF_SIZE]; - char buff2[BUF_SIZE]; - char buff3[BUF_SIZE]; - char buff4[BUF_SIZE]; - struct io_uring_sqe *sqe; - struct io_uring_cqe *cqe; - int str1_sz = strlen(STR1); - int str2_sz = strlen(STR2); - - fds[0] = open(FILE_NAME1, O_RDWR | O_TRUNC | O_CREAT, 0644); - if (fds[0] < 0 ) { - perror("open"); - return 1; - } - - memset(buff1, 0, BUF_SIZE); - memset(buff2, 0, BUF_SIZE); - memset(buff3, 0, BUF_SIZE); - memset(buff4, 0, BUF_SIZE); - strncpy(buff1, STR1, str1_sz); - strncpy(buff2, STR2, str2_sz); - - int ret = io_uring_register_files(ring, fds, 1); - if(ret) { - fprintf(stderr, "Error registering buffers: %s", strerror(-ret)); - return 1; - } - - sqe = io_uring_get_sqe(ring); - if (!sqe) { - fprintf(stderr, "Could not get SQE.\n"); - return 1; - } - io_uring_prep_write(sqe, 0, buff1, str1_sz, 0); - sqe->flags |= IOSQE_FIXED_FILE; - - sqe = io_uring_get_sqe(ring); - if (!sqe) { - fprintf(stderr, "Could not get SQE.\n"); - return 1; - } - io_uring_prep_write(sqe, 0, buff2, str2_sz, str1_sz); - sqe->flags |= IOSQE_FIXED_FILE; - - io_uring_submit(ring); - - for(int i = 0; i < 2; i ++) { - int ret = io_uring_wait_cqe(ring, &cqe); - if (ret < 0) { - fprintf(stderr, "Error waiting for completion: %s\n", - strerror(-ret)); - return 1; - } - /* Now that we have the CQE, let's process the data */ - if (cqe->res < 0) { - fprintf(stderr, "Error in async operation: %s\n", strerror(-cqe->res)); - } - printf("Result of the operation: %d\n", cqe->res); - io_uring_cqe_seen(ring, cqe); - } - - print_sq_poll_kernel_thread_status(); - - sqe = io_uring_get_sqe(ring); - if (!sqe) { - fprintf(stderr, "Could not get SQE.\n"); - return 1; - } - io_uring_prep_read(sqe, 0, buff3, str1_sz, 0); - sqe->flags |= IOSQE_FIXED_FILE; - - sqe = io_uring_get_sqe(ring); - if (!sqe) { - fprintf(stderr, "Could not get SQE.\n"); - return 1; - } - io_uring_prep_read(sqe, 0, buff4, str2_sz, str1_sz); - sqe->flags |= IOSQE_FIXED_FILE; - - io_uring_submit(ring); - - for(int i = 0; i < 2; i ++) { - int ret = io_uring_wait_cqe(ring, &cqe); - if (ret < 0) { - fprintf(stderr, "Error waiting for completion: %s\n", - strerror(-ret)); - return 1; - } - /* Now that we have the CQE, let's process the data */ - if (cqe->res < 0) { - fprintf(stderr, "Error in async operation: %s\n", strerror(-cqe->res)); - } - printf("Result of the operation: %d\n", cqe->res); - io_uring_cqe_seen(ring, cqe); - } - printf("Contents read from file:\n"); - printf("%s%s", buff3, buff4); -} - -int main() { - struct io_uring ring; - struct io_uring_params params; - - if (geteuid()) { - fprintf(stderr, "You need root privileges to run this program.\n"); - return 1; - } - - print_sq_poll_kernel_thread_status(); - - memset(¶ms, 0, sizeof(params)); - params.flags |= IORING_SETUP_SQPOLL; - params.sq_thread_idle = 2000; - - int ret = io_uring_queue_init_params(8, &ring, ¶ms); - if (ret) { - fprintf(stderr, "Unable to setup io_uring: %s\n", strerror(-ret)); - return 1; - } - start_sq_polling_ops(&ring); - io_uring_queue_exit(&ring); - return 0; -} diff --git a/llvm/文章/LLVM :Clang入门.md b/llvm/文章/LLVM :Clang入门.md deleted file mode 100644 index 78a931d..0000000 --- a/llvm/文章/LLVM :Clang入门.md +++ /dev/null @@ -1,416 +0,0 @@ -# LLVM是什么 - -LLVM项目是可重用(reusable)、模块化(modular)的编译器以及工具链技术(toolchain technologies)的集合。有人将其理解为“底层虚拟机(Low Level Virtual Machine)”的简称,但是[官方](https://llvm.org/)原话为: - -> “The name “LLVM” itself is not an acronym; it is the full name of the project.” - -意思是:LLVM不是首字母缩写,而是这整个项目的全名。 - -LLVM项目的发展起源于2000年伊利诺伊大学厄巴纳-香槟分校维克拉姆·艾夫(Vikram Adve)与克里斯·拉特纳(Chris Lattner)的研究,他们想要为所有静态及动态语言创造出动态的编译技术。2005年,苹果计算机雇用了克里斯·拉特纳及他的团队为苹果计算机开发应用程序系统,LLVM为现今Mac OS X及iOS开发工具的一部分。 - -# 1.LLVM&&clang安装 - -官网安装教程在[这里](http://clang.llvm.org/get_started.html)。这里简单介绍一下。 - -## 1.1Linux环境 - -### 1.1.1 安装前注意事项 - -由于本人使用的是虚拟机,所以在创建虚拟机的时候需要分配较大的内存和磁盘空间。这里踩了很多坑。 - -### 1.1.2 下载有关库 - -``` -$ sudo apt-get install cmake -$ sudo apt-get install git -$ sudo apt-get install gcc -$ sudo apt-get install g++ -``` - -### 1.1.3 下载项目源码 - -可以选择直接git整个工程,也可以去[官网](http://releases.llvm.org/download.html)下载源码然后自行安装。这里就按官网的git方法。 - -``` -$ git clone https://github.com/llvm/llvm-project.git -``` - -下载好后会在当前目录下看到llvm-project文件夹。 - -[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/1.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/1.jpg) - -### 1.1.4 构建项目 - -``` -$ cd llvm-project -``` - -创建构建目录 - -``` -$ mkdir build -$ cd build -``` - -利用cmake构建 - -``` -$ cmake -G [options] ../llvm -``` - -常用的generator有: - -- Ninja ——生成Ninja 文件 -- Unix Makefiles ——生成兼容make的makefile文件 -- Visual Studio ——生成Visual Studio项目与解决方案 -- Xcode ——用于生成Xcode项目 - -个人使用Unix Makefiles - -常用的options有: - -- DCMAKE_INSTALL_PREFIX=directory ——为目录指定要在其中安装LLVM工具和库的完整路径名(默认/usr/local)。 -- DCMAKE_BUILD_TYPE=type ——type选项有Debug,Release,RelWithDebInfo和MinSizeRel。默认值为Debug。 -- DLLVM_ENABLE_ASSERTIONS=On ——启用断言检查进行编译。 -- DLLVM_ENABLE_PROJECTS=”…” ——要另外构建的LLVM子项目的列表,以’;’分隔。例如要构建LLVM,Clang,libcxx和libcxxabi,使用:`DLLVM_INSTALL_PROJECTS="clang;libcxx;libcxxabi"` -- DLLVM_TARGETS_TO_BUILD=”…” ——构建针对的平台的部分项目,以’;’分隔。默认面向所有平台编译(all),指定只编译自己需要的CPU架构可以节省时间。 - -官方文档在[这](http://llvm.org/docs/CMake.html#options-and-variables)。 - -由于全部构建真的很耗费资源和时间,我使用的构建clang命令(可供参考): - -``` -$ cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_TARGETS_TO_BUILD="X86" -DLLVM_ENABLE_PROJECTS=clang -DLLVM_USE_LINKER=gold -G "Unix Makefiles" ../llvm -``` - -当然你如果愿意(~~而且设备跑得动~~)也可以: - -``` -$ cmake -G "Unix Makefiles" ../llvm -``` - -### 1.1.5 编译 - -``` -$ make [-j ] -$ sudo make install -``` - -直接make也可以,但LLVM也支持并行编译,其中core取决于核心数。如: - -``` -$ make -j 4 -``` - -这两步一般会很久…… - -### 1.1.6 测试 - -在编译结束后尝试在命令行中使用clang: - -``` -$ clang -v -``` - -本人得到结果如下: - -``` -clang version 10.0.0 -Target: x86_64-unknown-linux-gnu -Thread model: posix -InstalledDir: /usr/local/bin -Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/7 -Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/7.4.0 -Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/8 -Selected GCC installation: /usr/lib/gcc/x86_64-linux-gnu/7.4.0 -Candidate multilib: .;@m64 -Selected multilib: .;@m64 -``` - -[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/2.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/2.jpg) - -编写一段C语言代码试试看(C++也可以): - -``` -//helloworld.c -#include -int main() { - printf("hello world\n"); - return 0; -} -``` - -用clang编译: - -``` -$ clang helloworld.c -o hello.out -$ ./hello.out -``` - -如果是C++代码则: - -``` -//helloworld.cpp -#include -using namespace std; -int main() { - cout << "hello world" << endl; - return 0; -} -``` - -用clang编译(注意命令是clang++,本人刚开始只写clang提示编译错误…): - -``` -$ clang++ helloworld.cpp -o hello.out -$ ./hello.out -``` - -[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/3.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/3.jpg) - -大功告成! - -## 1.2.Windows环境 - -llvm+clang在windows下有两种类型,一种利用mingw编译,使用gcc的库和头文件;另一种利用vc编译,使用vc的库和头文件。推荐使用vc编译的版本,因为其程序更原生且不依赖于mingw,而只依赖于vc的dll库。 - -具体步骤[官方的教程](http://clang.llvm.org/get_started.html)已经说得很明白啦,下面简单整理介绍一下。 - -### 1.2.1 安装工具 - -- **Git.** 下载源代码。下载地址:https://git-scm.com/download -- **Cmake.** 用于生成Visual Studio的解决方案和项目文件。下载地址:https://cmake.org/download/ -- **Visual Studio(推荐2017或之后版本).** 生成项目的容器。 -- **Python.** 用于运行clang测试套件。下载地址:https://www.python.org/download/ -- **GnuWin32 tools.** clang和LLVM的测试套件需要GNU工具。下载地址:http://getgnuwin32.sourceforge.net/ - -### 1.2.2 clone项目源代码 - -你也可以选择去github上直接download。 - -``` -$ git clone https://github.com/llvm/llvm-progect.git -``` - -### 1.2.3 Cmake编译 - -在项目总目录下新建build目录(避免污染项目源代码) - -``` -$ mkdir build -$ cd build -``` - -运行Cmake。 - -``` -$ cmake -DLLVM_ENABLE_PROJECTS=clang -G "Visual Studio 15 2017" -A x64 -Thost=x64 ..\llvm -``` - -- 如果使用Visual Studio 2019则改为 `-G "Visual Studio 16 2019"`。 -- 如果需要适配x86则改为 `-A Win32`。 - -### 1.2.4 构建build - -在Visual Studio中打开LLVM.sin,右击ALL_BUILD项目,选择生成,等待数小时。 - -### 1.2.5 设置环境变量 - -将llvm/debug/bin 添加到自己的环境变量中。 - -其余具体测试步骤请看[Hacking on clang - Testing using Visual Studio on Windows](http://clang.llvm.org/hacking.html#testingWindows)。 - -有待补充… - -# 2.LLVM简介 - -用户文档:[llvm.org/docs/LangRef.html](https://llvm.org/docs/LangRef.html) - -LLVM是基于静态单一分配的表示形式,可提供类型安全性、底层操作、灵活性,并且适配几乎所有高级语言,具有通用的代码表示。现在LLVM已经成为多个编译器和代码生成相关子项目的母项目。 - -> The LLVM code representation is designed to be used in three different forms: as an in-memory compiler IR, as an on-disk bitcode representation (suitable for fast loading by a Just-In-Time compiler), and as a human readable assembly language representation. - -其中,LLVM提供了完整编译系统的中间层,并将中间语言(Intermediate Repressentation, IR)从编译器取出并进行最优化,最优化后的IR接着被转换及链接到目标平台的汇编语言。 - -我们知道,传统的编译器主要结构为: - -[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/4.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/4.jpg) - -[传统编译器结构,图片摘自网络](https://clheveningflow.github.io/2019/09/28/LLVM1/4.jpg) - - - -Frontend:前端,词法分析、语法分析、语义分析、生成中间代码 - -Optimizer:优化器,进行中间代码优化 - -Backend:后端,生成机器码 - -LLVM主要结构为: - -[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/5.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/5.jpg) - -[LLVM结构,图片摘自网络](https://clheveningflow.github.io/2019/09/28/LLVM1/5.jpg) - - - -也就是说,对于LLVM来说,不同的前后端使用统一的中间代码LLVM IR。如果需要支持一种新的编程语言/硬件设备,那么只需要实现一个新的前端/后端就可以了,而优化截断是一个通用的阶段,针对统一的LLVM IR,都不需要对于优化阶段修改。对比GCC,其前端和后端基本耦合在一起,所以GCC支持一门新的语言或者目标平台会变得很困难。 - -以下内容摘自维基百科: - -> LLVM也可以在编译时期、链接时期,甚至是运行时期产生可重新定位的代码(Relocatable Code)。 - -> LLVM支持与语言无关的指令集架构及类型系统。每个在静态单赋值形式(SSA)的指令集代表着,每个变量(被称为具有类型的寄存器)仅被赋值一次,这简化了变量间相依性的分析。LLVM允许代码被静态的编译,包含在传统的GCC系统底下,或是类似JAVA等后期编译才将IF编译成机器代码所使用的即时编译(JIT)技术。它的类型系统包含基本类型(整数或是浮点数)及五个复合类型(指针、数组、向量、结构及函数),在LLVM具体语言的类型建制可以以结合基本类型来表示,举例来说,C++所使用的class可以被表示为结构、函数及函数指针的数组所组成。 - -> LLVM JIT编译器可以最优化在运行时期时程序所不需要的静态分支,这在一些部分求值(Partial Evaluation)的案例中相当有效,即当程序有许多选项,而在特定环境下其中多数可被判断为是不需要。这个特色被使用在Mac OS X Leopard(v10.5)底下OpenGL的管线,当硬件不支持某个功能时依然可以被成功地运作。OpenGL堆栈下的绘图程序被编译为IR,接着在机器上运行时被编译,当系统拥有高端GPU时,这段程序会进行极少的修改并将传递指令给GPU,当系统拥有低级的GPU时,LLVM将会编译更多的程序,使这段GPU无法运行的指令在本地端的中央处理器运行。LLVM增进了使用Intel GMA芯片等低端机器的性能。一个类似的系统发展于Gallium3D LLVMpipe,它已被合并到GNOME,使其可运行在没有GPU的环境。 - -> 根据2011年的一项测试,GCC在运行时期的性能平均比LLVM高10%。而2013年测试显示,LLVM可以编译出接近GCC相同性能的运行码。 - -# 3.Clang简介 - -Clang是LLVM针对C语言及其家族语言的前端(a C language family frontend for LLVM)。它的主要目标是提供一个GNU编译器套装(GCC)的替代品,支持GNU编译器大多数便已设置以及非官方语言拓展。项目包括Clang前端和Clang静态分析器。 - -> The Clang project provides a language front-end and tooling infrastructure for languages in the C language family (C, C++, Objective C/C++, OpenCL, CUDA, and RenderScript) for the LLVM project. Both a GCC-compatible compiler driver (clang) and an MSVC-compatible compiler driver (clang-cl.exe) are provided. You can get and build the source today. - -Clang项目为LLVM项目中的C语言家族提供了一个语言前端和工具基础设施。其提供了兼容GCC和MSVC的编译器驱动程序(clang和clang-cl.exe)。 -官方手册:http://clang.llvm.org/docs/UsersManual.html#basicusage - -针对于GCC,Clang的优点有: - -- 占用内存小 -- 设计清晰简单,容易理解 -- 编译速度快 -- 设计偏向模块化,易于集成 -- 诊断信息可读性强 - -## Clang(Clang++)使用 - -我们先随便写一段以下代码: - -``` -//test.cpp -#include -#include - -using namespace std; - -int a[10] = {4,2,7,5,6,1,8,9,3,0}; - -int main() { - for(int i = 0; i < 10; ++i) - cout << a[i] << (i == 9?"\n":" "); - sort(a,a+10); - for(int i = 0; i < 10; ++i) - cout << a[i] << (i == 9?"\n":" "); - return 0; -} -``` - -[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/6.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/6.jpg) - -### 3.1.生成预处理文件: - -``` -$ clang++ -E test.cpp -o test.i -``` - -生成文件test.i(部分)如下: - -[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/7.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/7.jpg) - -### 3.2.生成汇编程序: - -``` -$ clang++ -S test.i -``` - -[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/8.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/8.jpg) - -### 3.3.生成目标文件: - -``` -$ clang++ -c test.s -``` - -[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/9.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/9.jpg) - -### 3.4.生成可执行文件: - -``` -$ clang++ -o test.out test.o -``` - -[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/10.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/10.jpg) - -emm…~~和GCC大致还是一样的嘛~~ 非常的好用。 - -### 3.5.查看Clang编译的过程 - -``` -$ clang -ccc-print-phases A.c -``` - -[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/11.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/11.jpg) - -- 0.获取输入:A.c文件,C语言 -- 1.预处理器:处理define、include等 -- 2.编译:生成中间代码(IR) -- 3.后端:生成汇编代码 -- 4.汇编:生成目标代码 -- 5.链接器:链接其他动态库 - -### 3.6.词法分析 - -``` -$ clang -fmodules -E -Xclang -dump-tokens A.c -``` - -如图,写一个小函数对其进行词法分析。 - -[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/12.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/12.jpg) - -### 3.7.语法分析 - -``` -$ clang -fmodules -fsyntax-only -Xclang -ast-dump A.c -``` - -生成语法树如下: - -[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/13.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/13.jpg) - -有颜色区分还是比较美观的。 - -### 3.8.语义分析 - -生成LLVM IR。LLVM IR有3种表示形式(本质是等价的) - -- (1).text:便于阅读的文本格式,类似于汇编语言,拓展名.ll -- (2).memory:内存格式 -- (3).bitcode:二进制格式,拓展名.bc - -生成text格式: - -``` -$ clang -S -emit-llvm A.c -``` - -[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/14.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/14.jpg) - -# 4.学习体会 - -安装LLVM绝对是一件痛苦的事情,我在Linux上安装LLVM+Clang起码花费有40小时(Windows上还没有成功55555)读了很久文档才搞清楚cmake的各种选项的用处,然后就是漫长的等待… - -对编译原理的学习才刚刚起步,读了一下Clang的用户文档,很多选项都~~搞不明白,~~没有机会使用,但是经过使用后感觉LLVM真的是一个很强大的模块化编译器工具集合,而Clang的各种编译选项确实可以帮助理解编译的各个流程,光听编译原理课程是看不见一个实际的编译器是如何生成词法分析、语法分析的结果或者中间代码的。 - -LLVM不仅仅是编译器那么简单,我们可以利用其做各种NB的操作,比如开发新的编译器、新的编程语言、开发编译器插件、进行代码规范检查。它也绝对不是IOS领域特有的,因为它是一个高度模块化、可重用的组件,可适用于多门编程语言和多个硬件设备平台。可以说,大部分从事计算机工作的人都该懂点LLVM,而绝不仅仅只是开发者。 - -友情链接: - -LLVM官网:[http://llvm.org](http://llvm.org/) - -Clang官网:[http://clang.llvm.org](http://clang.llvm.org/) - -LLVM用户文档:[llvm.org/docs/LangRef.html](https://llvm.org/docs/LangRef.html) - -Clang用户文档:http://clang.llvm.org/docs/UsersManual.html From 6a70b1847ff94e147d9420f7ee71788004f3e19b Mon Sep 17 00:00:00 2001 From: Magic Date: Wed, 2 Feb 2022 00:26:19 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E5=85=BC=E5=AE=B9Windows=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=90=8D=E5=91=BD=E5=90=8D=E6=96=B9=E5=BC=8F=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E5=9C=A8Windows=E4=B8=8Bgit=20clone=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- io_uring/文章/What is io_uring.md | 51 +++ .../文章/智汇华云-新时代IO利器-io_uring.md | 342 ++++++++++++++ io_uring/示例程序-提交队列轮询.c | 145 ++++++ llvm/文章/LLVM-Clang入门.md | 416 ++++++++++++++++++ 5 files changed, 957 insertions(+), 3 deletions(-) create mode 100644 io_uring/文章/What is io_uring.md create mode 100644 io_uring/文章/智汇华云-新时代IO利器-io_uring.md create mode 100644 io_uring/示例程序-提交队列轮询.c create mode 100644 llvm/文章/LLVM-Clang入门.md diff --git a/README.md b/README.md index fb2ac05..fef1896 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,9 @@ - [io_uring(2)- 从创建必要的文件描述符 fd 开始](https://github.com/0voice/kernel_new_features/blob/main/io_uring/%E6%96%87%E7%AB%A0/io_uring%EF%BC%882%EF%BC%89-%20%E4%BB%8E%E5%88%9B%E5%BB%BA%E5%BF%85%E8%A6%81%E7%9A%84%E6%96%87%E4%BB%B6%E6%8F%8F%E8%BF%B0%E7%AC%A6%20fd%20%E5%BC%80%E5%A7%8B.md) - [下一代异步 IO io_uring 技术解密](https://github.com/0voice/kernel_new_features/blob/main/io_uring/%E6%96%87%E7%AB%A0/%E4%B8%8B%E4%B8%80%E4%BB%A3%E5%BC%82%E6%AD%A5%20IO%20io_uring%20%E6%8A%80%E6%9C%AF%E8%A7%A3%E5%AF%86.md) - [小谈io_uring](https://github.com/0voice/kernel_new_features/blob/main/io_uring/%E6%96%87%E7%AB%A0/%E5%B0%8F%E8%B0%88io_uring.md) -- [智汇华云 | 新时代IO利器-io_uring](https://github.com/0voice/kernel_new_features/blob/main/io_uring/%E6%96%87%E7%AB%A0/%E6%99%BA%E6%B1%87%E5%8D%8E%E4%BA%91%20%7C%20%E6%96%B0%E6%97%B6%E4%BB%A3IO%E5%88%A9%E5%99%A8-io_uring.md) +- [智汇华云-新时代IO利器-io_uring](https://github.com/0voice/kernel_new_features/blob/main/io_uring/%E6%96%87%E7%AB%A0/%E6%99%BA%E6%B1%87%E5%8D%8E%E4%BA%91-%E6%96%B0%E6%97%B6%E4%BB%A3IO%E5%88%A9%E5%99%A8-io_uring.md) - [Linux 5.1 的 io_uring](https://github.com/0voice/kernel_new_features/blob/main/io_uring/%E6%96%87%E7%AB%A0/Linux%205.1%20%E7%9A%84%20io_uring.md) -- [What is io_uring?](https://github.com/0voice/kernel_new_features/blob/main/io_uring/%E6%96%87%E7%AB%A0/What%20is%20io_uring%3F) +- [What is io_uring](https://github.com/0voice/kernel_new_features/blob/main/io_uring/%E6%96%87%E7%AB%A0/What%20is%20io_uring) - [io_uring_setup](https://github.com/0voice/kernel_new_features/blob/main/io_uring/%E6%96%87%E7%AB%A0/io_uring_setup.md) - [io_uring_enter](https://github.com/0voice/kernel_new_features/blob/main/io_uring/%E6%96%87%E7%AB%A0/io_uring_enter.md) - [io_uring_register](https://github.com/0voice/kernel_new_features/blob/main/io_uring/%E6%96%87%E7%AB%A0/io_uring_register.md) @@ -434,7 +434,7 @@ ### 文章 - [LLVM 入门篇](https://github.com/0voice/kernel_new_features/blob/main/llvm/%E6%96%87%E7%AB%A0/LLVM%20%E5%85%A5%E9%97%A8%E7%AF%87.md) -- [LLVM :Clang入门](https://github.com/0voice/kernel_new_features/blob/main/llvm/%E6%96%87%E7%AB%A0/LLVM%20:Clang%E5%85%A5%E9%97%A8.md) +- [LLVM-Clang入门](https://github.com/0voice/kernel_new_features/blob/main/llvm/%E6%96%87%E7%AB%A0/LLVM-Clang%E5%85%A5%E9%97%A8.md) - [LLVM编译器框架介绍](https://github.com/0voice/kernel_new_features/blob/main/llvm/%E6%96%87%E7%AB%A0/LLVM%E7%BC%96%E8%AF%91%E5%99%A8%E6%A1%86%E6%9E%B6%E4%BB%8B%E7%BB%8D.md) - [Llvm编译的基本概念和流程](https://github.com/0voice/kernel_new_features/blob/main/llvm/%E6%96%87%E7%AB%A0/llvm%E7%BC%96%E8%AF%91%E7%9A%84%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E5%92%8C%E6%B5%81%E7%A8%8B.md) - [后端技术的重用:LLVM不仅仅让你高效](https://github.com/0voice/kernel_new_features/blob/main/llvm/%E6%96%87%E7%AB%A0/%E5%90%8E%E7%AB%AF%E6%8A%80%E6%9C%AF%E7%9A%84%E9%87%8D%E7%94%A8%EF%BC%9ALLVM%E4%B8%8D%E4%BB%85%E4%BB%85%E8%AE%A9%E4%BD%A0%E9%AB%98%E6%95%88.md) diff --git a/io_uring/文章/What is io_uring.md b/io_uring/文章/What is io_uring.md new file mode 100644 index 0000000..0a5198f --- /dev/null +++ b/io_uring/文章/What is io_uring.md @@ -0,0 +1,51 @@ +`io_uring` is a new asynchronous I/O API for Linux created by Jens Axboe from Facebook. It aims at providing an API without the limitations of the current [select(2)](http://man7.org/linux/man-pages/man2/select.2.html), [poll(2)](http://man7.org/linux/man-pages/man2/poll.2.html), [epoll(7)](http://man7.org/linux/man-pages/man7/epoll.7.html) or [aio(7)](http://man7.org/linux/man-pages/man7/aio.7.html) family of system calls, which we discussed in the previous section. Given that users of asynchronous programming models choose it in the first place for performance reasons, it makes sense to have an API that has very low performance overheads. We shall see how `io_uring` achieves this in subsequent sections. + +## The io_uring interface + +The very name io_uring comes from the fact that the interfaces uses ring buffers as the main interface for kernel-user space communication. While there are system calls involved, they are kept to a minimum and there is a polling mode you can use to reduce the need to make system calls as much as possible. + +See also + +- [Submission queue polling tutorial](https://unixism.net/loti/tutorial/sq_poll.html#sq-poll) with example program. + +### The mental model + +The mental model you need to construct in order to use `io_uring` to build programs that process I/O asynchronously is fairly simple. + +- There are 2 ring buffers, one for submission of requests (submission queue or SQ) and the other that informs you about completion of those requests (completion queue or CQ). +- These ring buffers are shared between kernel and user space. You set these up with [`io_uring_setup()`](https://unixism.net/loti/ref-iouring/io_uring_setup.html#c.io_uring_setup) and then mapping them into user space with 2 [mmap(2)](http://man7.org/linux/man-pages/man2/mmap.2.html) calls. +- You tell io_uring what you need to get done (read or write a file, accept client connections, etc), which you describe as part of a submission queue entry (SQE) and add it to the tail of the submission ring buffer. +- You then tell the kernel via the [`io_uring_enter()`](https://unixism.net/loti/ref-iouring/io_uring_enter.html#c.io_uring_enter) system call that you’ve added an SQE to the submission queue ring buffer. You can add multiple SQEs before making the system call as well. +- Optionally, [`io_uring_enter()`](https://unixism.net/loti/ref-iouring/io_uring_enter.html#c.io_uring_enter) can also wait for a number of requests to be processed by the kernel before it returns so you know you’re ready to read off the completion queue for results. +- The kernel processes requests submitted and adds completion queue events (CQEs) to the tail of the completion queue ring buffer. +- You read CQEs off the head of the completion queue ring buffer. There is one CQE corresponding to each SQE and it contains the status of that particular request. +- You continue adding SQEs and reaping CQEs as you need. +- There is a [polling mode available](https://unixism.net/loti/tutorial/sq_poll.html#sq-poll), in which the kernel polls for new entries in the submission queue. This avoids the system call overhead of calling [`io_uring_enter()`](https://unixism.net/loti/ref-iouring/io_uring_enter.html#c.io_uring_enter) every time you submit entries for processing. + +See also + +- [The Low-level io_uring Interface](https://unixism.net/loti/low_level.html#low-level) + +## io_uring performance + +Because of the shared ring buffers between the kernel and user space, io_uring can be a zero-copy system. Copying bytes around becomes necessary when system calls that transfer data between kernel and user space are involved. But since the bulk of the communication in `io_uring` is via buffers shared between the kernel and user space, this huge performance overhead is completely avoided. While system calls (and we’re used to making them a lot) may not seem like a significant overhead, in high performance applications, making a lot of them will begin to matter. Also, system calls are not as cheap as they used to be. Throw in workarounds the operating system has in place to deal with [Specter and Meltdown](https://meltdownattack.com/), we are talking non-trivial overheads. So, avoiding system calls as much as possible is a fantastic idea in high-performance applications indeed. + +While using synchronous programming interfaces or even when using asynchronous programming interfaces under Linux, there is at least one system call involved in the submission of each request. In `io_uring`, you can add several requests, simply by adding multiple SQEs each describing the I/O operation you want and make a single call to io_uring_enter. For starers, that’s a win right there. But it gets better. + +You can have the kernel poll and pick up your SQEs for processing as you add them to the submission queue. This avoids the [`io_uring_enter()`](https://unixism.net/loti/ref-iouring/io_uring_enter.html#c.io_uring_enter) call you need to make to tell the kernel to pick up SQEs. For high-performance applications, this means even lesser system call overheads. See [the submission queue polling tutorial](https://unixism.net/loti/tutorial/sq_poll.html#sq-poll) for more details. + +With some clever use of shared ring buffers, `io_uring` performance is really memory-bound, since in polling mode, we can do away with system calls altogether. It is important to remember that performance benchmarking is a relative process with some kind of a common point of reference. According to the [io_uring paper](https://kernel.dk/io_uring.pdf), on a reference machine, in polling mode, `io_uring` managed to clock 1.7M 4k IOPS, while [aio(7)](http://man7.org/linux/man-pages/man7/aio.7.html) manages 608k. Although much more than double, this isn’t a fair comparison since [aio(7)](http://man7.org/linux/man-pages/man7/aio.7.html) doesn’t feature a polled mode. But even when polled mode is disabled, `io_uring` hits 1.2M IOPS, close to double that of [aio(7)](http://man7.org/linux/man-pages/man7/aio.7.html). + +To check the raw throughput of the `io_uring` interface, there is a no-op request type. With this, on the reference machine, `io_uring` achieves 20M messages per second. See [`io_uring_prep_nop()`](https://unixism.net/loti/ref-liburing/submission.html#c.io_uring_prep_nop) for more details. + +## An example using the low-level API + +Writing a small program that reads files and prints them on to the console, like how the Unix `cat` utility does might be a good starting point to get your hands wet with the `io_uring` API. Please see the next chapter for one such example. + +## Just use liburing + +While being acquainted with the low-level `io_uring` API is most certainly a good thing, in real, serious programs you probably want to use the higher-level interface provided by liburing. Programs like [QEMU](https://qemu.org/) already use it. If liburing never existed, you’d have built some abstraction layer over the low-lever `io_uring` interface. liburing does that for you and it is a well thought-out interface as well. In short, you should probably put in some effort to understand how the low-level `io_uring` interface works, but by default you should really be using `liburing` in your programs. + +While there is a reference section here for it, there are some examples based on `liburing` we’ll see in the subsequent chapters. + +> 原文链接:https://unixism.net/loti/what_is_io_uring.html diff --git a/io_uring/文章/智汇华云-新时代IO利器-io_uring.md b/io_uring/文章/智汇华云-新时代IO利器-io_uring.md new file mode 100644 index 0000000..48081e2 --- /dev/null +++ b/io_uring/文章/智汇华云-新时代IO利器-io_uring.md @@ -0,0 +1,342 @@ +> io_uring是kernel 5.1中引入的一套新的syscall接口,用于支持异步IO。随着客户在高性能计算中的求解问题规模的越来越大,对计算能力和存储IO的需求不断增长,并成为计算和存储技术发展最直接的动力。本文将对io_uring的原理和功能进行分析,让大家了解io_uring的性能以及其应用场景、发展趋势。 + +io_uring是kernel 5.1中引入的一套新的syscall接口,用于支持异步IO。随着客户在高性能计算中的求解问题规模的越来越大,对计算能力和存储IO的需求不断增长,并成为计算和存储技术发展最直接的动力。本文将对io_uring的原理和功能进行分析,让大家了解io_uring的性能以及其应用场景、发展趋势。 + +**概述** + +高性能计算是继理论科学和实验科学之后科学探索的第三范式,被广泛应用在高能物理研究、核武器设计、航天航空飞行器设计、国民经济的预测和决策等领域,对国民经济发展和国防建设具有重要的价值。它作为世界高技术领域的战略制高点,已经成为科技进步的重要标志之一,同时也是一个国家科技综合实力的集中体现。 + +作为信创云计算专家,华云数据已经在政府金融、国防军工、教育医疗、能源电力、交通运输等十几个行业打造了行业标杆案例,客户总量超过30万。在高性能计算方面也部下重局。在华云数据的研发和客户需求调研中发现,随着客户在高性能计算中的求解问题规模的越来越大,对计算能力和存储IO的需求不断增长,并成为计算和存储技术发展最直接的动力。 + +io_uring是Linux Kernel在v5.1版本引入的一套新异步编程框架,同时支持Buffer IO和Direct IO。io_uring 在设计之初就充分考虑框架自身的高性能和通用性,不仅仅面向传统基于块设备的FS/Block IO领域,对网络异步编程也提供支持,以通用的系统调用提供比肩spdk/dpdk 的性能。 + + **IO演变过程** + +**1、基于fd的阻塞式 I/O:read()/write()** + +![image](https://user-images.githubusercontent.com/87457873/149942207-3af8a5c4-0c46-43be-8247-5176d414dabc.png) + +作为大家最熟悉的读写方式,Linux 内核提供了基于文件描述符的系统调用,这些描述符指向的可能是存储文件(storage file),也可能是network sockets: + +ssize_t read(int fd, void *buf, size_t count); + +ssize_t write(int fd, const void *buf, size_t count); + +二者称为阻塞式系统调用(blocking system calls),因为程序调用这些函数时会进入sleep状态,然后被调度出去(让出处理器),直到 I/O 操作完成: + +- 如果数据在文件中,并且文件内容已经缓存在page cache中,调用会立即返回; +- 如果数据在另一台机器上,就需要通过网络(例如 TCP)获取,会阻塞一段时间; +- 如果数据在硬盘上,也会阻塞一段时间。 + +缺点: + +随着存储设备越来越快,程序越来越复杂, 阻塞式(blocking)已经这种最简单的方式已经不适用了。 + +**2、非阻塞式 I/O:select()/poll()/epoll()** + +阻塞式之后,出现了一些新的、非阻塞的系统调用,例如select()、poll()以及更新的epoll()。应用程序在调用这些函数读写时不会阻塞,而是立即返回,返回的是一个已经ready的文件描述符列表。 + +![image](https://user-images.githubusercontent.com/87457873/149942253-f714b31b-f8c8-46b3-8c73-cf307fbb8d60.png) + +缺点:只支持network sockets和pipes——epoll()甚至连storage files都不支持。 + +**3、线程池方式** + +对于storage I/O,经典的解决思路是thread pool:主线程将 I/O分发给worker线程,后者代替主线程进行阻塞式读写,主线程不会阻塞。 + +![image](https://user-images.githubusercontent.com/87457873/149942269-bec5c706-707e-4667-a5d7-239784765786.png) + +这种方式的问题是线程上下文切换开销可能非常大,通过性能压测会看到。 + +**4、Direct** **I/O(数据库软件):绕过 page cache** + +随后出现了更加灵活和强大的方式:数据库软件(database software)有时并不想使用操作系统的page cache,而是希望打开一个文件后,直接从设备读写这个文件(direct access to the device)。这种方式称为直接访问(direct access)或直接 I/O(direct I/O)。 + +需要指定O_DIRECT flag; + +- 需要应用自己管理自己的缓存 —— 这正是数据库软件所希望的; +- 是zero-copy I/O,因为应用的缓冲数据直接发送到设备,或者直接从设备读取。 + +**5、异步IO** + +随着存储设备越来越快,主线程和worker线性之间的上下文切换开销占比越来越高。现在市场上的一些设备。换个方式描述,更能让我们感受到这种开销:上下文每切换一次,我们就少一次dispatch I/O的机会。因此Linux 2.6 内核引入了异步I/O(asynchronous I/O)接口。 + + + +![image](https://user-images.githubusercontent.com/87457873/149942297-b3711c0b-39ce-4512-9b8e-fc5221d64ab7.png) + +Linux 原生AIO 处理流程: + +- 当应用程序调用io_submit系统调用发起一个异步IO 操作后,会向内核的 IO 任务队列中添加一个IO 任务,并且返回成功。 +- 内核会在后台处理 IO 任务队列中的IO 任务,然后把处理结果存储在 IO 任务中。 +- 应用程序可以调用io_getevents系统调用来获取异步IO 的处理结果,如果 IO 操作还没完成,那么返回失败信息,否则会返回IO 处理结果。 + +从上面的流程可以看出,Linux 的异步 IO 操作主要由两个步骤组成: + +- 调用 io_submit 函数发起一个异步 IO 操作。 +- 调用 io_getevents 函数获取异步 IO 的结果。 + +AIO的缺陷 + +- 仅支持direct IO。在采用AIO的时候,只能使用O_DIRECT,不能借助文件系统缓存来缓存当前的IO请求,还存在size对齐(直接操作磁盘,所有写入内存块数量必须是文件系统块大小的倍数,而且要与内存页大小对齐。)等限制,直接影响了aio在很多场景的使用。 +- 仍然可能被阻塞。语义不完备。即使应用层主观上,希望系统层采用异步IO,但是客观上,有时候还是可能会被阻塞。io_getevents(2)调用read_events读取AIO的完成events,read_events中的wait_event_interruptible_hrtimeout等待aio_read_events,如果条件不成立(events未完成)则调用__wait_event_hrtimeout进入睡眠(当然,支持用户态设置最大等待时间)。 +- 拷贝开销大。每个IO提交需要拷贝64+8字节,每个IO完成需要拷贝32字节,总共104字节的拷贝。这个拷贝开销是否可以承受,和单次IO大小有关:如果需要发送的IO本身就很大,相较之下,这点消耗可以忽略,而在大量小IO的场景下,这样的拷贝影响比较大。 +- API不友好。每一个IO至少需要两次系统调用才能完成(submit和wait-for-completion),需要非常小心地使用完成事件以避免丢事件。 +- 系统调用开销大。也正是因为上一条,io_submit/io_getevents造成了较大的系统调用开销,在存在spectre/meltdown(CPU熔断幽灵漏洞,CVE-2017-5754)的机器上,若如果要避免漏洞问题,系统调用性能则会大幅下降。在存储场景下,高频系统调用的性能影响较大。 + +软件工程中,在现有接口基础上改进,相比新开发一套接口,往往有着更多的优势,然而在过去的数年间,针对上述限制的很多改进努力都未尽如人意。终于,全新的异步IO引擎io_uring就在这样的环境下诞生了。 + + + +**io_uring原理** + +io_uring 来自资深内核开发者 Jens Axboe 的想法,他在 Linux I/O stack 领域颇有研究。从最早的patch aio: support for IO polling 可以看出,这项工作始于一个很简单的观察:随着设备越来越快,中断驱动(interrupt-driven)模式效率已经低于轮询模式(polling for completions)。 + +- io_uring 的基本逻辑与 linux-aio 是类似的:提供两个接口,一个将 I/O 请求提交到内核,一个从内核接收完成事件。 +- 但随着开发深入,它逐渐变成了一个完全不同的接口:设计者开始从源头思考 如何支持完全异步的操作。 + +**1、原理及核心数据结构:** + +每个 io_uring 实例都有两个环形队列(ring),在内核和应用程序之间共享: + +- 提交队列:submission queue (SQ) +- 完成队列:completion queue (CQ) + +![image](https://user-images.githubusercontent.com/87457873/149942323-e05da76e-264d-486d-b824-0a29f7d992b3.png) + +这两个队列: + +- 都是单生产者、单消费者,size 是 2 的幂次; +- 提供无锁接口(lock-less access interface),内部使用 内存屏障做同步(coordinated with memory barriers)。 + +使用方式: + +- 请求 + +- 应用创建 SQ entries (SQE),更新 SQ tail; +- 内核消费 SQE,更新 SQ head。 + +- 完成 + +- 内核为完成的一个或多个请求创建 CQ entries (CQE),更新 CQ tail; +- 应用消费 CQE,更新 CQ head。 +- 完成事件(completion events)可能以任意顺序到达,到总是与特定的 SQE 相关联的。 +- 消费 CQE 过程无需切换到内核态。 + +**2、 三种工作模式** + +io_uring 实例可工作在三种模式: + +**(1)中断驱动模式(interrupt driven)** + +默认模式。可通过 io_uring_enter() 提交 I/O 请求,然后直接检查 CQ 状态判断是否完成。 + +**(2)轮询模式(polled)** + +Busy-waiting for an I/O completion,而不是通过异步 IRQ(Interrupt Request)接收通知。 + +这种模式需要文件系统(如果有)和块设备(block device)支持轮询功能。相比中断驱动方式,这种方式延迟更低,但可能会消耗更多 CPU 资源。 + +目前,只有指定了 O_DIRECT flag 打开的文件描述符,才能使用这种模式。当一个读 或写请求提交给轮询上下文(polled context)之后,应用(application)必须调用 io_uring_enter() 来轮询 CQ 队列,判断请求是否已经完成。 + +对一个io_uring 实例来说,不支持混合使用轮询和非轮询模式。 + +**(3)内核轮询模式(kernel polled)** + +这种模式中,会创建一个内核线程(kernel thread)来执行SQ 的轮询工作。 + +使用这种模式的 io_uring 实例,应用无需切到到内核态就能触发(issue)I/O 操作。通过SQ来提交SQE,以及监控CQ的完成状态,应用无需任何系统调用,就能提交和收割 I/O(submit and reap I/Os)。 + +如果内核线程的空闲时间超过了用户的配置值,它会通知应用,然后进入idle状态。这种情况下,应用必须调用io_uring_enter()来唤醒内核线程。如果 I/O 一直很繁忙,内核线性是不会 sleep 的。 + +**3、重要特性** + +- IORING_SETUP_SQPOLL:创建一个内核线程进行sqe的处理(IO 提交),几乎完全消除用户态内核态上下文切换,消除spectre/ meltdown 缓解场景下对系统调用的性能影响,此特性需要额外消耗一个cpu核。 +- ORING_SETUP_IOPOLL:配合blk-mq多队列映射机制,内核IO 协议栈开始真正完整支持IOpolling。 +- IORING_REGISTER_FILES/ IORING_REGISTER_FILES_UPDATE/ IORING_UNREGISTER_FILES:减少fget/ fput原子操作带来的开销 +- IORING_REGISTER_BUFFERS/ IORING_UNREGISTER_BUFFERS:通过提 前向内核 注册 buffer 减少 get_user_pages / unpin_user_pages 开销,应用适配存在一定难度。 +- IORING_FEAT_FAST_POLL:网络编程新利器,向 epoll 等传统基于事件驱动的网络编程模型发起挑战,为用户态提供真正的异步编程 API 。 +- ASYNC BUFFERED READS:更好的支持异步 buffered reads ,但当前对于异步 buffered write 的支持还不够完。 + + + +**功能对比** + +**1、Synchronous I/O、 Libaio和IO_uring对比** + +![image](https://user-images.githubusercontent.com/87457873/149942361-1c19bf0f-818f-4cec-9556-d42a0f3a92c3.png) + +**2、io_uring和spdk的对比** + +![image](https://user-images.githubusercontent.com/87457873/149942381-3a9f6d36-9991-476a-941e-8750b1e3666a.png) + + + +**性能表现** + + + +![image](https://user-images.githubusercontent.com/87457873/149942400-b45feae5-dfb8-4b7f-94f0-ee9ce3604b3d.png) + +非polling模式,io_uring相比libaio提升不是很明显;在polling模式下,io_uring能与spdk接近,甚至在queue depth较高时性能更好,完爆libaio。 + +![image](https://user-images.githubusercontent.com/87457873/149942424-17bb8dea-5bef-4b06-9d31-5a8d2f559779.png) + +在queue depth较低时有约7%的差距,但在queue depth较高时基本接近。 + +![image](https://user-images.githubusercontent.com/87457873/149942440-4924df96-279e-4858-861f-be93aabc00f3.png) + +以上数据来自来自Jens Axboe的测试:https://lore.kernel.org/linux-block/20190116175003.17880-1-axboe@kernel.dk/ + +结论: + +io_uring在非polling模式下,相比libaio,性能提升不是非常显著。 + +io_uring在polling模式下,性能提升显著,与spdk接近,在队列深度较高时性能更好。 + +**io_uring在kvm-qemu下的性能表现** + +**1、直接启用io_uring的场景** + +下面测试结果在一台华云超融合一体机测试,qemu 5.0 已经支持了Asynchronous I/O的io_uring,只需要将虚拟机将驱动改成io_uring。 + +原理如下: + + + +![image](https://user-images.githubusercontent.com/87457873/149942467-a7b1e4f6-e6e6-430a-b2f3-5ea67e18005c.png) + + + +- guestOS的kernel和qemu通信通过 virtqueue +- io_uring通过SQ和CQ进行IO的管理 + +宿主机环境信息: + +![image](https://user-images.githubusercontent.com/87457873/149942487-2a429103-f3b6-4f94-b273-0c50319b8399.png) + +虚拟机环境信息 + +![image](https://user-images.githubusercontent.com/87457873/149942499-6d91b523-daf2-4abd-852c-e1cb5017a7e2.png) + +测试参数 + +``` +[global] +ioengine=libaio +time_based +direct=1 +thread +group_reporting +randrepeat=0 +norandommap +numjobs=4 +ramp_time=10 +runtime=600 +filename=/dev/sdb + +[randwrite-4k-io32] +bs=4k +iodepth=32 +rw=randwrite +stonewall + +[randread-4k-io32] +bs=4k +iodepth=32 +rw=randread +stonewall +``` + +测试数据如下: + +![image](https://user-images.githubusercontent.com/87457873/149942516-0ce842ca-85e8-4ee1-8b4f-6a0fe9adb8f8.png) + +总结: + +1)、libaio和io_uring性能比较,io_uring性能提高不明显,没有跨越式的提升。 + +2)、目前io_uring 的virtio block开发还在持续进行,poll模式等高级功能没有release。很值得期待。具体参见:https://wiki.qemu.org/Features/IOUring + +**2、io passthrough场景** + +原理: + +![image](https://user-images.githubusercontent.com/87457873/149942524-a776e41b-6545-4107-b18a-fcec4b53f9de.png) + +直接透传QEMU Block层 + +- io_uring的SQ和CQ都在GuestOS的kernel中 +- qemu的virtio-blk修改了“fast path” +- 启用了polling模式 + +测试环境版本信息: + +![image](https://user-images.githubusercontent.com/87457873/149942562-a724ac3f-f0b6-4484-b99e-921524186101.png) + +创建虚拟机 + +aio=io_uring -device virtio-blk-pci,drive=hd1,bootindex=2,io-uring-pt=on + +测试结果: + +![image](https://user-images.githubusercontent.com/87457873/149942598-6875fb40-8bd6-4143-a71a-d8f04f99b43e.png) + +测试数据来自: + +https://static.sched.com/hosted_files/kvmforum2020/9c/KVMForum_2020_io_uring_passthrough_Stefano_Garzarella.pdf + +**总结:** + +1)、virtio-blk的性能只有裸设备性能的一半左右。 + +2)、io_uring passthrough的性能和裸设备持平。 + +3)、缺点需要在guestOS的kernel中打patch。 + + + +**社区支持情况** + +**1、内核支持情况** + +![image](https://user-images.githubusercontent.com/87457873/149942648-62ae7e45-f25a-484c-96e2-aef574cdd63b.png) + +**2、io_uring 社区开发现状** + +当前 io_uring 社区开发主要聚焦在以下几个方面。 + +- **io_uring 框架自身的迭代优化** + 前面提到,io_uring 作为一种新型高性能异步编程框架,当前仍处于快速发展中。因此随着特性越来越丰富,以及各种稳定性问题的修复等等,代码也变得越来越臃肿。因此 io_uring 框架也需要同步”进化“,不断进行代码重构优化。 + 通常异步编程一般是将工作交由线程池来做,但对于 io_uring 来说,这只是最坏的 slow path,例如异步读优化,就是想尝试尽量在当前上下文中处理。另外,在 sqpoll 模式下,io_uring 接管用户提交的系统调用,一个系统调用的执行与特定的进程上下文相关,因此 io_uring 需要维护系统调用进程的内存上下文,文件系统上下文等大量信息。同时 io_uring 作为一种框架,框架本身的开销应该尽可能的小,才能与用户态高性能框架 SPDK 对标,因此需要持续优化。 +- **特性增强** + +- io_uring buffer registration 特性增强 + io_uring 的 buffer registration 特性可以用来减少读写操作时频繁的 get_user_pages()/unpin_user_pages() 调用,get_user_pages() 主要用来实现用户态虚拟地址到内核页的转换,会消耗一定的 cpu 资源。来自 Oracle 的同学这组patchset 让 buffer registration 特性支持更新和共享操作,更加方便用户态编程,目前已发到第三版。我们遇到一些业务也明确提出过需要 buffer registration 支持更新操作。 +- fs/userfaultfd: support iouring and polling +- 来自 VMware 的同学这组 patchset 使得 userfaultfd 支持 io_uring,且支持 polling 模式。在RDMA,持久内存等高速场景,系统调用用户态内核态上下文切换的开销在 userfualtfd 整体开销的占比会非常突出,支持 io_uring 后,可以显著减少用户态内核态上下文切换,带来明显的性能提升。 +- add io_uring with IOPOLL support in scsi layer +- io_uring 出现后,Linux 内核才真正完整地支持 iopoll,iopoll 相比于中断模式能带来明显的性能提升。此前只有nvme driver 对 iopoll 提供比较好的支持,现在 scsi 层也开始准备支持 iopoll。 +- no-copy bvec +- 在用 io_uring 进行 IO 栈性能分析时,来自 Facebook 的 Pavel Begunkov 发现在 direct IO 场景下是不需要拷贝bvec 结构的,他提交的这组 patchset 可以进一步提高内核 direct IO 的性能。 +- io_uring and Optane2 +- block 社区已经开始重视提高 IO 栈性能,一种思路是利用高性能设备,借助 io_uring 压测 IO 栈,从而发现软件性能瓶颈。比如 Intel提供最新的 Gen2 Optane SSD 给 block / io_uring 的维护者 Jens Axboe 来做内核 IO 栈性能分析。 +- fs: Support for LOOKUP_NONBLOCK / RESOLVE_NONBLOCK +- Jens Axboe 优化 vfs 的文件查询逻辑,从而可以使 io_uring 的 IORING_OP_OPENAT 操作效率更高。 + +- **IO 栈协同优化** + 借助 io_uirng 这一高性能异步编程框架,能比较容易地发现其他内核子系统的软件性能瓶颈,从而对其进行优化,以提高内核 IO 栈的整体性能。我们之前也基于这个思路发现了 block 层的几处优化,同时提出了 device mapper polling 打通的 RFC。 + + + +**总结** + +1、华云数据产品技术中心始终贴近用户,解决用户的实际问题,同时时刻关注业界的先进技术。得益于io_uring的优异的表现,华云数据在此方面投入大量资源。同时我们在RDMA、SPDK、vdpa、DPU、io_uring、eBPF等热门技术也有相关的研究及产品化的丰富经验,同时对社区不断的共享。 + +2、io_uring 是完全为性能而生的新一代 native async IO 模型,比 libaio 高级不少。通过全新的设计,共享内存,IO 过程不需要系统调用,由内核完成 IO的提交, 以及Polled IO机制,实现了高IOPS。相比kernel bypass,这种原生内核的方式显得友好一些。一定程度上,io_uring代表了Linux kernel block层的未来,至少从中我们可以看出一些block层进化的方向,而且我们看到io_uring也在快速发展,相信未来io_uring会更加的高效、易用,并拥有更加丰富的功能和更强的扩展能力。这让我们充满期待,NVMe的存储时代需要一个属于自己的高速IO引擎。 + +3、在kvm方面,qemu支持io_uring,在操作和运维方便比spdk的vhost简单很多。虽然支持的还不是很完善,但社区仍然在积极推进。希望这个早日release。 + +4、国内阿里在io_uring走的比较靠前,阿里有个专门的内核组在做相关的开发。OpenAnolis已经将io_uring纳入,并且在io_uring方面做了很多探索。通过社区的一些文章可以看到,阿里在数据库、web、echo_sever等相关的领域都已经应用了io_uring。 + +> 原文链接:https://www.huayun.com/news/1521.html diff --git a/io_uring/示例程序-提交队列轮询.c b/io_uring/示例程序-提交队列轮询.c new file mode 100644 index 0000000..869c350 --- /dev/null +++ b/io_uring/示例程序-提交队列轮询.c @@ -0,0 +1,145 @@ +#include +#include +#include +#include +#include +#include + +#define BUF_SIZE 512 +#define FILE_NAME1 "/tmp/io_uring_sq_test.txt" +#define STR1 "What is this life if, full of care,\n" +#define STR2 "We have no time to stand and stare." + +void print_sq_poll_kernel_thread_status() { + + if (system("ps --ppid 2 | grep io_uring-sq" ) == 0) + printf("Kernel thread io_uring-sq found running...\n"); + else + printf("Kernel thread io_uring-sq is not running.\n"); +} + +int start_sq_polling_ops(struct io_uring *ring) { + int fds[2]; + char buff1[BUF_SIZE]; + char buff2[BUF_SIZE]; + char buff3[BUF_SIZE]; + char buff4[BUF_SIZE]; + struct io_uring_sqe *sqe; + struct io_uring_cqe *cqe; + int str1_sz = strlen(STR1); + int str2_sz = strlen(STR2); + + fds[0] = open(FILE_NAME1, O_RDWR | O_TRUNC | O_CREAT, 0644); + if (fds[0] < 0 ) { + perror("open"); + return 1; + } + + memset(buff1, 0, BUF_SIZE); + memset(buff2, 0, BUF_SIZE); + memset(buff3, 0, BUF_SIZE); + memset(buff4, 0, BUF_SIZE); + strncpy(buff1, STR1, str1_sz); + strncpy(buff2, STR2, str2_sz); + + int ret = io_uring_register_files(ring, fds, 1); + if(ret) { + fprintf(stderr, "Error registering buffers: %s", strerror(-ret)); + return 1; + } + + sqe = io_uring_get_sqe(ring); + if (!sqe) { + fprintf(stderr, "Could not get SQE.\n"); + return 1; + } + io_uring_prep_write(sqe, 0, buff1, str1_sz, 0); + sqe->flags |= IOSQE_FIXED_FILE; + + sqe = io_uring_get_sqe(ring); + if (!sqe) { + fprintf(stderr, "Could not get SQE.\n"); + return 1; + } + io_uring_prep_write(sqe, 0, buff2, str2_sz, str1_sz); + sqe->flags |= IOSQE_FIXED_FILE; + + io_uring_submit(ring); + + for(int i = 0; i < 2; i ++) { + int ret = io_uring_wait_cqe(ring, &cqe); + if (ret < 0) { + fprintf(stderr, "Error waiting for completion: %s\n", + strerror(-ret)); + return 1; + } + /* Now that we have the CQE, let's process the data */ + if (cqe->res < 0) { + fprintf(stderr, "Error in async operation: %s\n", strerror(-cqe->res)); + } + printf("Result of the operation: %d\n", cqe->res); + io_uring_cqe_seen(ring, cqe); + } + + print_sq_poll_kernel_thread_status(); + + sqe = io_uring_get_sqe(ring); + if (!sqe) { + fprintf(stderr, "Could not get SQE.\n"); + return 1; + } + io_uring_prep_read(sqe, 0, buff3, str1_sz, 0); + sqe->flags |= IOSQE_FIXED_FILE; + + sqe = io_uring_get_sqe(ring); + if (!sqe) { + fprintf(stderr, "Could not get SQE.\n"); + return 1; + } + io_uring_prep_read(sqe, 0, buff4, str2_sz, str1_sz); + sqe->flags |= IOSQE_FIXED_FILE; + + io_uring_submit(ring); + + for(int i = 0; i < 2; i ++) { + int ret = io_uring_wait_cqe(ring, &cqe); + if (ret < 0) { + fprintf(stderr, "Error waiting for completion: %s\n", + strerror(-ret)); + return 1; + } + /* Now that we have the CQE, let's process the data */ + if (cqe->res < 0) { + fprintf(stderr, "Error in async operation: %s\n", strerror(-cqe->res)); + } + printf("Result of the operation: %d\n", cqe->res); + io_uring_cqe_seen(ring, cqe); + } + printf("Contents read from file:\n"); + printf("%s%s", buff3, buff4); +} + +int main() { + struct io_uring ring; + struct io_uring_params params; + + if (geteuid()) { + fprintf(stderr, "You need root privileges to run this program.\n"); + return 1; + } + + print_sq_poll_kernel_thread_status(); + + memset(¶ms, 0, sizeof(params)); + params.flags |= IORING_SETUP_SQPOLL; + params.sq_thread_idle = 2000; + + int ret = io_uring_queue_init_params(8, &ring, ¶ms); + if (ret) { + fprintf(stderr, "Unable to setup io_uring: %s\n", strerror(-ret)); + return 1; + } + start_sq_polling_ops(&ring); + io_uring_queue_exit(&ring); + return 0; +} \ No newline at end of file diff --git a/llvm/文章/LLVM-Clang入门.md b/llvm/文章/LLVM-Clang入门.md new file mode 100644 index 0000000..2062546 --- /dev/null +++ b/llvm/文章/LLVM-Clang入门.md @@ -0,0 +1,416 @@ +# LLVM是什么 + +LLVM项目是可重用(reusable)、模块化(modular)的编译器以及工具链技术(toolchain technologies)的集合。有人将其理解为“底层虚拟机(Low Level Virtual Machine)”的简称,但是[官方](https://llvm.org/)原话为: + +> “The name “LLVM” itself is not an acronym; it is the full name of the project.” + +意思是:LLVM不是首字母缩写,而是这整个项目的全名。 + +LLVM项目的发展起源于2000年伊利诺伊大学厄巴纳-香槟分校维克拉姆·艾夫(Vikram Adve)与克里斯·拉特纳(Chris Lattner)的研究,他们想要为所有静态及动态语言创造出动态的编译技术。2005年,苹果计算机雇用了克里斯·拉特纳及他的团队为苹果计算机开发应用程序系统,LLVM为现今Mac OS X及iOS开发工具的一部分。 + +# 1.LLVM&&clang安装 + +官网安装教程在[这里](http://clang.llvm.org/get_started.html)。这里简单介绍一下。 + +## 1.1Linux环境 + +### 1.1.1 安装前注意事项 + +由于本人使用的是虚拟机,所以在创建虚拟机的时候需要分配较大的内存和磁盘空间。这里踩了很多坑。 + +### 1.1.2 下载有关库 + +``` +$ sudo apt-get install cmake +$ sudo apt-get install git +$ sudo apt-get install gcc +$ sudo apt-get install g++ +``` + +### 1.1.3 下载项目源码 + +可以选择直接git整个工程,也可以去[官网](http://releases.llvm.org/download.html)下载源码然后自行安装。这里就按官网的git方法。 + +``` +$ git clone https://github.com/llvm/llvm-project.git +``` + +下载好后会在当前目录下看到llvm-project文件夹。 + +[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/1.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/1.jpg) + +### 1.1.4 构建项目 + +``` +$ cd llvm-project +``` + +创建构建目录 + +``` +$ mkdir build +$ cd build +``` + +利用cmake构建 + +``` +$ cmake -G [options] ../llvm +``` + +常用的generator有: + +- Ninja ——生成Ninja 文件 +- Unix Makefiles ——生成兼容make的makefile文件 +- Visual Studio ——生成Visual Studio项目与解决方案 +- Xcode ——用于生成Xcode项目 + +个人使用Unix Makefiles + +常用的options有: + +- DCMAKE_INSTALL_PREFIX=directory ——为目录指定要在其中安装LLVM工具和库的完整路径名(默认/usr/local)。 +- DCMAKE_BUILD_TYPE=type ——type选项有Debug,Release,RelWithDebInfo和MinSizeRel。默认值为Debug。 +- DLLVM_ENABLE_ASSERTIONS=On ——启用断言检查进行编译。 +- DLLVM_ENABLE_PROJECTS=”…” ——要另外构建的LLVM子项目的列表,以’;’分隔。例如要构建LLVM,Clang,libcxx和libcxxabi,使用:`DLLVM_INSTALL_PROJECTS="clang;libcxx;libcxxabi"` +- DLLVM_TARGETS_TO_BUILD=”…” ——构建针对的平台的部分项目,以’;’分隔。默认面向所有平台编译(all),指定只编译自己需要的CPU架构可以节省时间。 + +官方文档在[这](http://llvm.org/docs/CMake.html#options-and-variables)。 + +由于全部构建真的很耗费资源和时间,我使用的构建clang命令(可供参考): + +``` +$ cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_TARGETS_TO_BUILD="X86" -DLLVM_ENABLE_PROJECTS=clang -DLLVM_USE_LINKER=gold -G "Unix Makefiles" ../llvm +``` + +当然你如果愿意(~~而且设备跑得动~~)也可以: + +``` +$ cmake -G "Unix Makefiles" ../llvm +``` + +### 1.1.5 编译 + +``` +$ make [-j ] +$ sudo make install +``` + +直接make也可以,但LLVM也支持并行编译,其中core取决于核心数。如: + +``` +$ make -j 4 +``` + +这两步一般会很久…… + +### 1.1.6 测试 + +在编译结束后尝试在命令行中使用clang: + +``` +$ clang -v +``` + +本人得到结果如下: + +``` +clang version 10.0.0 +Target: x86_64-unknown-linux-gnu +Thread model: posix +InstalledDir: /usr/local/bin +Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/7 +Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/7.4.0 +Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/8 +Selected GCC installation: /usr/lib/gcc/x86_64-linux-gnu/7.4.0 +Candidate multilib: .;@m64 +Selected multilib: .;@m64 +``` + +[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/2.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/2.jpg) + +编写一段C语言代码试试看(C++也可以): + +``` +//helloworld.c +#include +int main() { + printf("hello world\n"); + return 0; +} +``` + +用clang编译: + +``` +$ clang helloworld.c -o hello.out +$ ./hello.out +``` + +如果是C++代码则: + +``` +//helloworld.cpp +#include +using namespace std; +int main() { + cout << "hello world" << endl; + return 0; +} +``` + +用clang编译(注意命令是clang++,本人刚开始只写clang提示编译错误…): + +``` +$ clang++ helloworld.cpp -o hello.out +$ ./hello.out +``` + +[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/3.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/3.jpg) + +大功告成! + +## 1.2.Windows环境 + +llvm+clang在windows下有两种类型,一种利用mingw编译,使用gcc的库和头文件;另一种利用vc编译,使用vc的库和头文件。推荐使用vc编译的版本,因为其程序更原生且不依赖于mingw,而只依赖于vc的dll库。 + +具体步骤[官方的教程](http://clang.llvm.org/get_started.html)已经说得很明白啦,下面简单整理介绍一下。 + +### 1.2.1 安装工具 + +- **Git.** 下载源代码。下载地址:https://git-scm.com/download +- **Cmake.** 用于生成Visual Studio的解决方案和项目文件。下载地址:https://cmake.org/download/ +- **Visual Studio(推荐2017或之后版本).** 生成项目的容器。 +- **Python.** 用于运行clang测试套件。下载地址:https://www.python.org/download/ +- **GnuWin32 tools.** clang和LLVM的测试套件需要GNU工具。下载地址:http://getgnuwin32.sourceforge.net/ + +### 1.2.2 clone项目源代码 + +你也可以选择去github上直接download。 + +``` +$ git clone https://github.com/llvm/llvm-progect.git +``` + +### 1.2.3 Cmake编译 + +在项目总目录下新建build目录(避免污染项目源代码) + +``` +$ mkdir build +$ cd build +``` + +运行Cmake。 + +``` +$ cmake -DLLVM_ENABLE_PROJECTS=clang -G "Visual Studio 15 2017" -A x64 -Thost=x64 ..\llvm +``` + +- 如果使用Visual Studio 2019则改为 `-G "Visual Studio 16 2019"`。 +- 如果需要适配x86则改为 `-A Win32`。 + +### 1.2.4 构建build + +在Visual Studio中打开LLVM.sin,右击ALL_BUILD项目,选择生成,等待数小时。 + +### 1.2.5 设置环境变量 + +将llvm/debug/bin 添加到自己的环境变量中。 + +其余具体测试步骤请看[Hacking on clang - Testing using Visual Studio on Windows](http://clang.llvm.org/hacking.html#testingWindows)。 + +有待补充… + +# 2.LLVM简介 + +用户文档:[llvm.org/docs/LangRef.html](https://llvm.org/docs/LangRef.html) + +LLVM是基于静态单一分配的表示形式,可提供类型安全性、底层操作、灵活性,并且适配几乎所有高级语言,具有通用的代码表示。现在LLVM已经成为多个编译器和代码生成相关子项目的母项目。 + +> The LLVM code representation is designed to be used in three different forms: as an in-memory compiler IR, as an on-disk bitcode representation (suitable for fast loading by a Just-In-Time compiler), and as a human readable assembly language representation. + +其中,LLVM提供了完整编译系统的中间层,并将中间语言(Intermediate Repressentation, IR)从编译器取出并进行最优化,最优化后的IR接着被转换及链接到目标平台的汇编语言。 + +我们知道,传统的编译器主要结构为: + +[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/4.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/4.jpg) + +[传统编译器结构,图片摘自网络](https://clheveningflow.github.io/2019/09/28/LLVM1/4.jpg) + + + +Frontend:前端,词法分析、语法分析、语义分析、生成中间代码 + +Optimizer:优化器,进行中间代码优化 + +Backend:后端,生成机器码 + +LLVM主要结构为: + +[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/5.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/5.jpg) + +[LLVM结构,图片摘自网络](https://clheveningflow.github.io/2019/09/28/LLVM1/5.jpg) + + + +也就是说,对于LLVM来说,不同的前后端使用统一的中间代码LLVM IR。如果需要支持一种新的编程语言/硬件设备,那么只需要实现一个新的前端/后端就可以了,而优化截断是一个通用的阶段,针对统一的LLVM IR,都不需要对于优化阶段修改。对比GCC,其前端和后端基本耦合在一起,所以GCC支持一门新的语言或者目标平台会变得很困难。 + +以下内容摘自维基百科: + +> LLVM也可以在编译时期、链接时期,甚至是运行时期产生可重新定位的代码(Relocatable Code)。 + +> LLVM支持与语言无关的指令集架构及类型系统。每个在静态单赋值形式(SSA)的指令集代表着,每个变量(被称为具有类型的寄存器)仅被赋值一次,这简化了变量间相依性的分析。LLVM允许代码被静态的编译,包含在传统的GCC系统底下,或是类似JAVA等后期编译才将IF编译成机器代码所使用的即时编译(JIT)技术。它的类型系统包含基本类型(整数或是浮点数)及五个复合类型(指针、数组、向量、结构及函数),在LLVM具体语言的类型建制可以以结合基本类型来表示,举例来说,C++所使用的class可以被表示为结构、函数及函数指针的数组所组成。 + +> LLVM JIT编译器可以最优化在运行时期时程序所不需要的静态分支,这在一些部分求值(Partial Evaluation)的案例中相当有效,即当程序有许多选项,而在特定环境下其中多数可被判断为是不需要。这个特色被使用在Mac OS X Leopard(v10.5)底下OpenGL的管线,当硬件不支持某个功能时依然可以被成功地运作。OpenGL堆栈下的绘图程序被编译为IR,接着在机器上运行时被编译,当系统拥有高端GPU时,这段程序会进行极少的修改并将传递指令给GPU,当系统拥有低级的GPU时,LLVM将会编译更多的程序,使这段GPU无法运行的指令在本地端的中央处理器运行。LLVM增进了使用Intel GMA芯片等低端机器的性能。一个类似的系统发展于Gallium3D LLVMpipe,它已被合并到GNOME,使其可运行在没有GPU的环境。 + +> 根据2011年的一项测试,GCC在运行时期的性能平均比LLVM高10%。而2013年测试显示,LLVM可以编译出接近GCC相同性能的运行码。 + +# 3.Clang简介 + +Clang是LLVM针对C语言及其家族语言的前端(a C language family frontend for LLVM)。它的主要目标是提供一个GNU编译器套装(GCC)的替代品,支持GNU编译器大多数便已设置以及非官方语言拓展。项目包括Clang前端和Clang静态分析器。 + +> The Clang project provides a language front-end and tooling infrastructure for languages in the C language family (C, C++, Objective C/C++, OpenCL, CUDA, and RenderScript) for the LLVM project. Both a GCC-compatible compiler driver (clang) and an MSVC-compatible compiler driver (clang-cl.exe) are provided. You can get and build the source today. + +Clang项目为LLVM项目中的C语言家族提供了一个语言前端和工具基础设施。其提供了兼容GCC和MSVC的编译器驱动程序(clang和clang-cl.exe)。 +官方手册:http://clang.llvm.org/docs/UsersManual.html#basicusage + +针对于GCC,Clang的优点有: + +- 占用内存小 +- 设计清晰简单,容易理解 +- 编译速度快 +- 设计偏向模块化,易于集成 +- 诊断信息可读性强 + +## Clang(Clang++)使用 + +我们先随便写一段以下代码: + +``` +//test.cpp +#include +#include + +using namespace std; + +int a[10] = {4,2,7,5,6,1,8,9,3,0}; + +int main() { + for(int i = 0; i < 10; ++i) + cout << a[i] << (i == 9?"\n":" "); + sort(a,a+10); + for(int i = 0; i < 10; ++i) + cout << a[i] << (i == 9?"\n":" "); + return 0; +} +``` + +[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/6.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/6.jpg) + +### 3.1.生成预处理文件: + +``` +$ clang++ -E test.cpp -o test.i +``` + +生成文件test.i(部分)如下: + +[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/7.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/7.jpg) + +### 3.2.生成汇编程序: + +``` +$ clang++ -S test.i +``` + +[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/8.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/8.jpg) + +### 3.3.生成目标文件: + +``` +$ clang++ -c test.s +``` + +[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/9.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/9.jpg) + +### 3.4.生成可执行文件: + +``` +$ clang++ -o test.out test.o +``` + +[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/10.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/10.jpg) + +emm…~~和GCC大致还是一样的嘛~~ 非常的好用。 + +### 3.5.查看Clang编译的过程 + +``` +$ clang -ccc-print-phases A.c +``` + +[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/11.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/11.jpg) + +- 0.获取输入:A.c文件,C语言 +- 1.预处理器:处理define、include等 +- 2.编译:生成中间代码(IR) +- 3.后端:生成汇编代码 +- 4.汇编:生成目标代码 +- 5.链接器:链接其他动态库 + +### 3.6.词法分析 + +``` +$ clang -fmodules -E -Xclang -dump-tokens A.c +``` + +如图,写一个小函数对其进行词法分析。 + +[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/12.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/12.jpg) + +### 3.7.语法分析 + +``` +$ clang -fmodules -fsyntax-only -Xclang -ast-dump A.c +``` + +生成语法树如下: + +[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/13.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/13.jpg) + +有颜色区分还是比较美观的。 + +### 3.8.语义分析 + +生成LLVM IR。LLVM IR有3种表示形式(本质是等价的) + +- (1).text:便于阅读的文本格式,类似于汇编语言,拓展名.ll +- (2).memory:内存格式 +- (3).bitcode:二进制格式,拓展名.bc + +生成text格式: + +``` +$ clang -S -emit-llvm A.c +``` + +[![LLVM](https://clheveningflow.github.io/2019/09/28/LLVM1/14.jpg)](https://clheveningflow.github.io/2019/09/28/LLVM1/14.jpg) + +# 4.学习体会 + +安装LLVM绝对是一件痛苦的事情,我在Linux上安装LLVM+Clang起码花费有40小时(Windows上还没有成功55555)读了很久文档才搞清楚cmake的各种选项的用处,然后就是漫长的等待… + +对编译原理的学习才刚刚起步,读了一下Clang的用户文档,很多选项都~~搞不明白,~~没有机会使用,但是经过使用后感觉LLVM真的是一个很强大的模块化编译器工具集合,而Clang的各种编译选项确实可以帮助理解编译的各个流程,光听编译原理课程是看不见一个实际的编译器是如何生成词法分析、语法分析的结果或者中间代码的。 + +LLVM不仅仅是编译器那么简单,我们可以利用其做各种NB的操作,比如开发新的编译器、新的编程语言、开发编译器插件、进行代码规范检查。它也绝对不是IOS领域特有的,因为它是一个高度模块化、可重用的组件,可适用于多门编程语言和多个硬件设备平台。可以说,大部分从事计算机工作的人都该懂点LLVM,而绝不仅仅只是开发者。 + +友情链接: + +LLVM官网:[http://llvm.org](http://llvm.org/) + +Clang官网:[http://clang.llvm.org](http://clang.llvm.org/) + +LLVM用户文档:[llvm.org/docs/LangRef.html](https://llvm.org/docs/LangRef.html) + +Clang用户文档:http://clang.llvm.org/docs/UsersManual.html \ No newline at end of file