Linux 异步 I/O 框架 io_uring:基本原理、程序示例与性能压测

2023-11-17

io_uring是 2019 年 Linux 5.1内核首次引入的高性能异步 I/O 框架,能显着加速 I/O 密集型应用的性能。但如果你的应用已经在使用传统 Linux AIO 了,并且使用方式恰当, 那io_uring并不会带来太大的性能提升—— 根据测试,即便打开高级特性,也只有 5%。除非你真的需要这 5% 的额外性能,否则 **切换**成io_uring代价可能也挺大,因为要重写应用来适配io_uring(或者让依赖的平台或框架去适配,总之需要改代码)。

既然性能跟传统 AIO 差不多,那为什么还称 io_uring 为革命性技术呢?

1、它首先和最大的贡献在于:统一了 Linux 异步 I/O 框架

  • Linux AIO 只支持 direct I/O 模式的 存储文件(storage file),而且主要用在 数据库这一细分领域
  • io_uring 支持存储文件和网络文件(network sockets),也支持更多的异步系统调用 (accept/openat/stat/...),而非仅限于 read/write 系统调用。

2、在 设计上是真正的异步 I/O,作为对比,Linux AIO 虽然也 是异步的,但仍然可能会阻塞,某些情况下的行为也无法预测;

3、**灵活性和可扩展性**非常好,甚至能基于 io_uring 重写所有系统调用,而 Linux AIO 设计时就没考虑扩展性。

eBPF 也算是异步框架(事件驱动),但与 io_uring 没有本质联系,二者属于不同子系统, 并且在模型上有一个本质区别:

  1. eBPF 对用户是透明的,只需升级内核(到合适的版本), 应用程序无需任何改造
  2. io_uring 提供了 新的系统调用和用户空间 API,因此 需要应用程序做改造

eBPF 作为动态跟踪工具,能够更方便地排查和观测 io_uring 等模块在执行层面的具体问题。

本文介绍 Linux 异步 I/O 的发展历史,io_uring 的原理和功能, 并给出了一些 **程序示例**和 **性能压测**结果(我们在 5.10 内核做了类似测试)。

以下是译文。

很多人可能还没意识到,Linux 内核在过去几年已经发生了一场革命。这场革命源于 **两个激动人心的新接口**的引入:eBPF 和 io_uring。我们认为,二者将会完全 改变应用与内核交互的方式,以及应用开发者思考和看待内核的方式

本文介绍 io_uring(我们在 ScyllaDB 中有 io_uring 的深入使用经验),并略微提及一下 eBPF。

1 Linux I/O 系统调用演进

1.1 基于 fd 的阻塞式 I/O:read()/write()

作为大家最熟悉的读写方式,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)已经这种最简单的方式已经不适用了。

1.2 非阻塞式 I/O:select()/poll()/epoll()

阻塞式之后,出现了一些新的、非阻塞的系统调用,例如 select()、poll() 以及更新的 epoll()。应用程序在调用这些函数读写时不会阻塞,而是 立即返回,返回的是一个已经 ready 的文件描述符列表

但这种方式存在一个致命缺点:只支持 network sockets 和 pipes ——epoll() 甚至连 storage files 都不支持。

1.3 线程池方式

对于 storage I/O,经典的解决思路是 thread pool[5]:主线程将 I/O 分发给 worker 线程,后者代替主线程进行阻塞式读写,主线程不会阻塞。

​这种方式的问题是 线程上下文切换开销可能非常大,后面性能压测会看到。

1.4 Direct I/O(数据库软件):绕过 page cache

随后出现了更加灵活和强大的方式:数据库软件(database software) 有时 并不想使用操作系统的 page cache[6], 而是希望打开一个文件后, 直接从设备读写这个文件(direct access to the device)。这种方式称为 直接访问(direct access)或 直接 I/O(direct I/O),

  • 需要指定 O_DIRECT flag;
  • 需要 应用自己管理自己的缓存 —— 这正是数据库软件所希望的;
  • 是 zero-copy I/O,因为应用的缓冲数据直接发送到设备,或者直接从设备读取。

1.5 异步 IO(AIO)

前面提到,随着存储设备越来越快,主线程和 worker 线性之间的上下文切换开销占比越来越高。现在市场上的一些设备,例如 Intel Optane[7], 延迟已经低到和上下文切换一个量级(微秒 us)。换个方式描述, 更能让我们感受到这种开销:上下文每切换一次,我们就少一次 dispatch I/O 的机会

因此,Linux 2.6 内核引入了异步 I/O(asynchronous I/O)接口, 方便起见,本文简写为 linux-aio。AIO **原理**是很简单的:

  • 用户通过 io_submit() 提交 I/O 请求,
  • 过一会再调用 io_getevents() 来检查哪些 events 已经 ready 了。
  • 使程序员 能编写完全异步的代码

近期,Linux AIO 甚至支持了[8] epoll():也就是说 不仅能提交 storage I/O 请求,还能提交网络 I/O 请求。照这样发展下去,linux-aio似乎能成为一个王者。但由于它糟糕的演进之路,这个愿望几乎不可能实现了。我们从 Linus 标志性的激烈言辞中就能略窥一斑

Reply to: to support opening files asynchronously[9] So I think this is ridiculously ugly. AIO is a horrible ad-hoc design, with the main excuse being “other, less gifted people, made that design, and we are implementing it for compatibility because database people — who seldom have any shred of taste — actually use it”. — Linus Torvalds (on lwn.net)

首先,作为数据库从业人员,我们想借此机会为我们的没品(lack of taste)向 Linus 道歉。但更重要的是,我们要进一步解释一下 为什么 Linus 是对的:Linux AIO 确实问题缠身,

  1. 只支持 O_DIRECT 文件,因此 对常规的非数据库应用(normal, non-database applications) 几乎是无用的
  2. 接口在 设计时并未考虑扩展性。虽然可以扩展 —— 我们也确实这么做了 —— 但每加一个东西都相当复杂;
  3. 虽然从 技术上说接口是非阻塞的,但实际上有很多可能的原因都会导致它阻塞[10],而且引发的方式难以预料。

1.6 小结

以上可以清晰地看出 Linux I/O 的演进:

  • 最开始是同步(阻塞式)系统调用;
  • 然后随着 实际需求和具体场景,不断加入新的异步接口,还要保持与老接口的兼容和协同工作。

另外也看到,在非阻塞式读写的问题上 并没有形成统一方案

  1. Network socket 领域:添加一个异步接口,然后去轮询(poll)请求是否完成(readiness);
  2. Storage I/O 领域:只针对某一细分领域(数据库)在某一特定时期的需求,添加了一个定制版的异步接口。

这就是 Linux I/O 的演进历史 —— 只着眼当前,出现一个问题就引入一种设计,而并没有多少前瞻性 —— 直到 io_uring 的出现。

 

2io_uring

io_uring 来自资深内核开发者 Jens Axboe 的想法,他在 Linux I/O stack 领域颇有研究。从最早的 patch aio: support for IO polling[11]可以看出,这项工作始于一个很简单的观察:随着设备越来越快,中断驱动(interrupt-driven)模式效率已经低于轮询模式(polling for completions) —— 这也是高性能领域最常见的主题之一。

  • io_uring 的 基本逻辑与 linux-aio 是类似的:提供两个接口,一个将 I/O 请求提交到内核,一个从内核接收完成事件。
  • 但随着开发深入,它逐渐变成了一个完全不同的接口:设计者开始从源头思考如何支持完全异步的操作

2.1 与 Linux AIO 的不同

io_uring 与 linux-aio 有着本质的不同:

  1. 在设计上是真正异步的(truly asynchronous)。只要 设置了合适的 flag,它在 系统调用上下文中就只是将请求放入队列, 不会做其他任何额外的事情, 保证了应用永远不会阻塞
  2. 支持任何类型的 I/O:cached files、direct-access files 甚至 blocking sockets。 由于设计上就是异步的(async-by-design nature),因此 无需 poll+read/write 来处理 sockets。只需提交一个阻塞式读(blocking read),请求完成之后,就会出现在 completion ring。
  3. 灵活、可扩展:基于 io_uring 甚至能重写(re-implement)Linux 的每个系统调用。

2.2 原理及核心数据结构:SQ/CQ/SQE/CQE

每个 io_uring 实例都有 两个环形队列(ring),在内核和应用程序之间共享:

  • 提交队列:submission queue (SQ)
  • 完成队列:completion queue (CQ)

​这两个队列:

  • 都是 单生产者、单消费者,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.3 带来的好处

io_uring 这种请求方式还有一个好处是:原来需要多次系统调用(读或写),现在变成批处理一次提交。

还记得 Meltdown 漏洞吗?当时我还写了一篇文章[12]解释为什么我们的 Scylla NoSQL 数据库受影响很小:aio 已经将我们的 I/O 系统调用批处理化了。

io_uring 将这种批处理能力带给了 storage I/O 系统调用之外的其他一些系统调用,包括:

  • read
  • write
  • send
  • recv
  • accept
  • openat
  • stat
  • 专用的一些系统调用,例如 fallocate

此外,io_uring 使异步 I/O 的使用场景也不再仅限于数据库应用, 普通的 非数据库应用也能用。这一点值得重复一遍:

虽然 io_uring 与 aio 有一些相似之处,但它的 扩展性和架构是革命性的:它 将异步操作的强大能力带给了所有应用(及其开发者),而不再仅限于是数据库应用这一细分领域

我们的 CTO Avi Kivity 在 the Core C++ 2019 event 上 有一次关于 async 的分享[13]。核心点包括:从延迟上来说

  1. 现代多核、多 CPU 设备,其内部本身就是一个基础网络;
  2. **CPU 之间**是另一个网络;
  3. **CPU 和磁盘 I/O 之间**又是一个网络。

因此网络编程采用异步是明智的,而现在开发自己的应用也应该考虑异步。这 从根本上改变了 Linux 应用的设计方式

  • 之前都是一段顺序代码流,需要系统调用时才执行系统调用,
  • 现在需要思考一个文件是否 ready,因而自然地引入 event-loop,不断通过共享 buffer 提交请求和接收结果。

2.4 三种工作模式

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)支持轮询功能。相比中断驱动方式,这种方式延迟更低(连系统调用都省了[14]), 但可能会消耗更多 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 的。

2.5io_uring系统调用 API

有三个:

  • io_uring_setup(2)
  • io_uring_register(2)
  • io_uring_enter(2)

下面展开介绍。完整文档见 manpage[15]

2.5.1io_uring_setup()

执行异步 I/O 需要先 设置上下文

int io_uring_setup(u32 entries, struct io_uring_params *p);

这个系统调用

  • 创建一个 SQ 和一个 CQ
  • queue size 至少 entries 个元素,
  • 返回一个文件描述符,随后用于在这个 io_uring 实例上执行操作。

SQ 和 CQ 在应用和内核之间共享,避免了在初始化和完成 I/O 时(initiating and completing I/O)拷贝数据。

参数 p:

  • 应用用来配置 io_uring,
  • 内核返回的 SQ/CQ 配置信息也通过它带回来。

io_uring_setup() 成功时返回一个文件描述符(fd)。应用随后可以将这个 fd 传给 mmap(2) 系统调用,来 map the submission and completion queues 或者传给 to the io_uring_register() or io_uring_enter() system calls.

2.5.2io_uring_register()

注册用于异步 I/O 的 文件或用户缓冲区(files or user buffers):

int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args);

注册文件或用户缓冲区,使内核能 长时间持有对该文件在内核内部的数据结构引用(internal kernel data structures associated with the files), 或创建 应用内存的长期映射(long term mappings of application memory associated with the buffers), 这个操作只会在注册时执行一次,而不是每个 I/O 请求都会处理,因此减少了 per-I/O overhead。

注册的缓冲区(buffer)性质

  • Registered buffers 将会 被锁定在内存中(be locked in memory),并 计入用户的 RLIMIT_MEMLOCK 资源限制。
  • 此外,每个 buffer 有 1GB 的大小限制
  • 当前,buffers 必须是 匿名、非文件后端的内存(anonymous, non-file-backed memory),例如 malloc(3) or mmap(2) with the MAP_ANONYMOUS flag set 返回的内存。
  • Huge pages 也是支持的。整个 huge page 都会被 pin 到内核,即使只用到了其中一部分。
  • 已经注册的 buffer 无法调整大小,想调整只能先 unregister,再重新 register 一个新的。

通过eventfd()订阅 completion 事件

可以用 eventfd(2) 订阅 io_uring 实例的 completion events。将 eventfd 描述符通过这个系统调用注册就行了。

The credentials of the running application can be registered with io_uring which returns an id associated with those credentials. Applications wishing to share a ring between separate users/processes can pass in this credential id in the SQE personality field. If set, that particular SQE will be issued with these credentials.

2.5.3io_uring_enter()

int io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t *sig);

这个系统调用用于初始化和完成(initiate and complete)I/O,使用共享的 SQ 和 CQ。单次调用同时执行:

  1. 提交新的 I/O 请求
  2. 等待 I/O 完成

参数:

  1. fd 是 io_uring_setup() 返回的文件描述符;
  2. to_submit 指定了 SQ 中提交的 I/O 数量;
  3. 依据不同模式:
  • 默认模式,如果指定了 min_complete,会等待这个数量的 I/O 事件完成再返回;
  • 如果 io_uring 是 polling 模式,这个参数表示:
  1. 0:要求内核返回当前以及完成的所有 events,无阻塞;
  2. 非零:如果有事件完成,内核仍然立即返回;如果没有完成事件,内核会 poll,等待指定的次数完成,或者这个进程的时间片用完。

注意:对于 interrupt driven I/O, 应用无需进入内核就能检查 CQ 的 event completions

io_uring_enter() 支持很多操作,包括:

  • Open, close, and stat files
  • Read and write into multiple buffers or pre-mapped buffers
  • Socket I/O operations
  • Synchronize file state
  • Asynchronously monitor a set of file descriptors
  • Create a timeout linked to a specific operation in the ring
  • Attempt to cancel an operation that is currently in flight
  • Create I/O chains
  • Ordered execution within a chain
  • Parallel execution of multiple chains

当这个系统调用返回时,表示一定数量的 SEQ 已经被消费和提交了,此时可以安全的重用队列中的 SEQ。此时 IO 提交有可能还停留在异步上下文中,即实际上 SQE 可能还没有被提交 —— 不过 用户不用关心这些细节 —— 当随后内核需要使用某个特定的 SQE 时,它已经复制了一份。

2.6 高级特性

io_uring 提供了一些用于特殊场景的高级特性:

  1. File registration(文件注册):每次发起一个指定文件描述的操 作,内核都需要 花费一些时钟周期(cycles) 将文件描述符映射到内部表示。对于那些 **针对同一文件进行重复操作**的场景,io_uring 支持 提前注册这些文件,后面直接查找就行了。
  2. Buffer registration(缓冲区注册):与 file registration 类 似,direct I/O 场景中,内核需要 map/unmap memory areas。io_uring 支持提前 注册这些缓冲区(buffers)。
  3. Poll ring(轮询环形缓冲区):对于非常快是设备,处理中断的开 销是比较大的。io_uring 允许用户关闭中断,使用轮询模式。前面“三种工作模式”小节 也介绍到了这一点。
  4. Linked operations(链接操作):允许用户发送串联的请求。这两 个请求同时提交,但后面的会等前面的处理完才开始执行。

2.7 用户空间库liburing

`liburing`[16] 提供了一个简单的高层 API, 可用于一些基本场景,应用程序避免了直接使用更底层的系统调用。此外,这个 API 还避免了一些重复操作的代码,如设置 io_uring 实例。

举个例子,在 io_uring_setup() 的 manpage 描述中,调用这个系统调用获得一个 ring 文 件描述符之后,应用必须调用 mmap() 来这样的逻辑需要一段略长的代码,而用liburing 的话,下面的函数已经将上述流程封装好了:

int io_uring_queue_init(unsigned entries, struct io_uring *ring, unsigned flags);

下一节来看两个例子基于 liburing 的例子。

3 基于 liburing 的示例应用

编译:

$ git clone https://github.com/axboe/liburing.git
$ git co -b liburing-2.0 tags/liburing-2.0

$ cd liburing
$ ls examples/
io_uring-cp  io_uring-cp.c  io_uring-test  io_uring-test.c  link-cp  link-cp.c  Makefile  ucontext-cp  ucontext-cp.c

$ make -j4

$ ./examples/io_uring-test <file>
Submitted=4, completed=4, bytes=16384

$ ./examples/link-cp <in-file> <out-file>

3.1io_uring-test

这个程序使用 4 个 SQE,从输入文件中 读取最多 16KB 数据

源码及注释

为方便看清主要逻辑,忽略了一些错误处理代码,完整代码见io_uring-test.c[17]

/* SPDX-License-Identifier: MIT */
/*
 * Simple app that demonstrates how to setup an io_uring interface,
 * submit and complete IO against it, and then tear it down.
 *
 * gcc -Wall -O2 -D_GNU_SOURCE -o io_uring-test io_uring-test.c -luring
 */
#include "liburing.h"

#define QD    4 // io_uring 队列长度

int main(int argc, char *argv[]) {
    int i, fd, pending, done;
    void *buf;

    // 1. 初始化一个 io_uring 实例
    struct io_uring ring;
    ret = io_uring_queue_init(QD,    // 队列长度
                              &ring, // io_uring 实例
                              0);    // flags,0 表示默认配置,例如使用中断驱动模式

    // 2. 打开输入文件,注意这里指定了 O_DIRECT flag,内核轮询模式需要这个 flag,见前面介绍
    fd = open(argv[1], O_RDONLY | O_DIRECT);
    struct stat sb;
    fstat(fd, &sb); // 获取文件信息,例如文件长度,后面会用到

    // 3. 初始化 4 个读缓冲区
    ssize_t fsize = 0;             // 程序的最大读取长度
    struct iovec *iovecs = calloc(QD, sizeof(struct iovec));
    for (i = 0; i < QD; i++) {
        if (posix_memalign(&buf, 4096, 4096))
            return 1;
        iovecs[i].iov_base = buf;  // 起始地址
        iovecs[i].iov_len = 4096;  // 缓冲区大小
        fsize += 4096;
    }

    // 4. 依次准备 4 个 SQE 读请求,指定将随后读入的数据写入 iovecs 
    struct io_uring_sqe *sqe;
    offset = 0;
    i = 0;
    do {
        sqe = io_uring_get_sqe(&ring);  // 获取可用 SQE
        io_uring_prep_readv(sqe,        // 用这个 SQE 准备一个待提交的 read 操作
                            fd,         // 从 fd 打开的文件中读取数据
                            &iovecs[i], // iovec 地址,读到的数据写入 iovec 缓冲区
                            1,          // iovec 数量
                            offset);    // 读取操作的起始地址偏移量
        offset += iovecs[i].iov_len;    // 更新偏移量,下次使用
        i++;

        if (offset > sb.st_size)        // 如果超出了文件大小,停止准备后面的 SQE
            break;
    } while (1);

    // 5. 提交 SQE 读请求
    ret = io_uring_submit(&ring);       // 4 个 SQE 一次提交,返回提交成功的 SQE 数量
    if (ret < 0) {
        fprintf(stderr, "io_uring_submit: %s\n", strerror(-ret));
        return 1;
    } else if (ret != i) {
        fprintf(stderr, "io_uring_submit submitted less %d\n", ret);
        return 1;
    }

    // 6. 等待读请求完成(CQE)
    struct io_uring_cqe *cqe;
    done = 0;
    pending = ret;
    fsize = 0;
    for (i = 0; i < pending; i++) {
        io_uring_wait_cqe(&ring, &cqe);  // 等待系统返回一个读完成事件
        done++;

        if (cqe->res != 4096 && cqe->res + fsize != sb.st_size) {
            fprintf(stderr, "ret=%d, wanted 4096\n", cqe->res);
        }

        fsize += cqe->res;
        io_uring_cqe_seen(&ring, cqe);   // 更新 io_uring 实例的完成队列
    }

    // 7. 打印统计信息
    printf("Submitted=%d, completed=%d, bytes=%lu\n", pending, done, (unsigned long) fsize);

    // 8. 清理工作
    close(fd);
    io_uring_queue_exit(&ring);
    return 0;
}

其他说明

代码中已经添加了注释,这里再解释几点:

  • 每个 SQE 都执行一个 allocated buffer,后者是用 iovec 结构描述的;
  • 第 3 & 4 步:初始化所有 SQE,用于接下来的 IORING_OP_READV 操作,后者提供了 readv(2)系统调用的异步接口
  • 操作完成之后,这个 SQE iovec buffer 中存放的是相关 readv 操作的结果;
  • 接下来调用 io_uring_wait_cqe() 来 reap CQE,并通过 cqe->res 字段验证读取的字节数;
  • io_uring_cqe_seen() 通知内核这个 CQE 已经被消费了。

3.2link-cp

link-cp 使用 io_uring 高级特性 SQE chaining 特性来复制文件。

I/O chain

io_uring 支持创建 I/O chain。一个 chain 内的 I/O 是顺序执行的,多个 I/O chain 可以并行执行。

io_uring_enter() manpage 中对 IOSQE_IO_LINK 有 详细解释[18]

When this flag is specified, it forms a link with the next SQE in the submission ring. That next SQE will not be started before this one completes. This, in effect, forms a chain of SQEs, which can be arbitrarily long. The tail of the chain is denoted by the first SQE that does not have this flag set. This flag has no effect on previous SQE submissions, nor does it impact SQEs that are outside of the chain tail. This means that multiple chains can be executing in parallel, or chains and individual SQEs. Only members inside the chain are serialized. A chain of SQEs will be broken, if any request in that chain ends in error. io_uring considers any unexpected result an error. This means that, eg, a short read will also terminate the remainder of the chain. If a chain of SQE links is broken, the remaining unstarted part of the chain will be terminated and completed with -ECANCELED as the error code. Available since 5.3.

为实现复制文件功能,link-cp 创建一个长度为 2 的 SQE chain。

  • 第一个 SQE 是一个读请求,将数据从输入文件读到 buffer;
  • 第二个请求,与第一个请求是 linked,是一个写请求,将数据从 buffer 写入输出文件。

源码及注释

/* SPDX-License-Identifier: MIT */
/*
 * Very basic proof-of-concept for doing a copy with linked SQEs. Needs a
 * bit of error handling and short read love.
 */
#include "liburing.h"

#define QD    64         // io_uring 队列长度
#define BS    (32*1024)

struct io_data {
    size_t offset;
    int index;
    struct iovec iov;
};

static int infd, outfd;
static unsigned inflight;

// 创建一个 read->write SQE chain
static void queue_rw_pair(struct io_uring *ring, off_t size, off_t offset) {
    struct io_uring_sqe *sqe;
    struct io_data *data;
    void *ptr;

    ptr = malloc(size + sizeof(*data));
    data = ptr + size;
    data->index = 0;
    data->offset = offset;
    data->iov.iov_base = ptr;
    data->iov.iov_len = size;

    sqe = io_uring_get_sqe(ring);                            // 获取可用 SQE
    io_uring_prep_readv(sqe, infd, &data->iov, 1, offset);   // 准备 read 请求
    sqe->flags |= IOSQE_IO_LINK;                             // 设置为 LINK 模式
    io_uring_sqe_set_data(sqe, data);                        // 设置 data

    sqe = io_uring_get_sqe(ring);                            // 获取另一个可用 SQE
    io_uring_prep_writev(sqe, outfd, &data->iov, 1, offset); // 准备 write 请求
    io_uring_sqe_set_data(sqe, data);                        // 设置 data
}

// 处理完成(completion)事件:释放 SQE 的内存缓冲区,通知内核已经消费了 CQE。
static int handle_cqe(struct io_uring *ring, struct io_uring_cqe *cqe) {
    struct io_data *data = io_uring_cqe_get_data(cqe);       // 获取 CQE
    data->index++;

    if (cqe->res < 0) {
        if (cqe->res == -ECANCELED) {
            queue_rw_pair(ring, BS, data->offset);
            inflight += 2;
        } else {
            printf("cqe error: %s\n", strerror(cqe->res));
            ret = 1;
        }
    }

    if (data->index == 2) {        // read->write chain 完成,释放缓冲区内存
        void *ptr = (void *) data - data->iov.iov_len;
        free(ptr);
    }

    io_uring_cqe_seen(ring, cqe);  // 通知内核已经消费了 CQE 事件
    return ret;
}

static int copy_file(struct io_uring *ring, off_t insize) {
    struct io_uring_cqe *cqe;
    size_t this_size;
    off_t offset;

    offset = 0;
    while (insize) {                      // 数据还没处理完
        int has_inflight = inflight;      // 当前正在进行中的 SQE 数量
        int depth;  // SQE 阈值,当前进行中的 SQE 数量(inflight)超过这个值之后,需要阻塞等待 CQE 完成

        while (insize && inflight < QD) { // 数据还没处理完,io_uring 队列也还没用完
            this_size = BS;
            if (this_size > insize)       // 最后一段数据不足 BS 大小
                this_size = insize;

            queue_rw_pair(ring, this_size, offset); // 创建一个 read->write chain,占用两个 SQE
            offset += this_size;
            insize -= this_size;
            inflight += 2;                // 正在进行中的 SQE 数量 +2
        }

        if (has_inflight != inflight)     // 如果有新创建的 SQE,
            io_uring_submit(ring);        // 就提交给内核

        if (insize)                       // 如果还有 data 等待处理,
            depth = QD;                   // 阈值设置 SQ 的队列长度,即 SQ 队列用完才开始阻塞等待 CQE;
        else                              // data 处理已经全部提交,
            depth = 1;                    // 阈值设置为 1,即只要还有 SQE 未完成,就阻塞等待 CQE

        // 下面这个 while 只有 SQ 队列用完或 data 全部提交之后才会执行到
        while (inflight >= depth) {       // 如果所有 SQE 都已经用完,或者所有 data read->write 请求都已经提交
            io_uring_wait_cqe(ring, &cqe);// 等待内核 completion 事件
            handle_cqe(ring, cqe);        // 处理 completion 事件:释放 SQE 内存缓冲区,通知内核 CQE 已消费
            inflight--;                   // 正在进行中的 SQE 数量 -1
        }
    }

    return 0;
}

static int setup_context(unsigned entries, struct io_uring *ring) {
    io_uring_queue_init(entries, ring, 0);
    return 0;
}

static int get_file_size(int fd, off_t *size) {
    struct stat st;

    if (fstat(fd, &st) < 0)
        return -1;
    if (S_ISREG(st.st_mode)) {
        *size = st.st_size;
        return 0;
    } else if (S_ISBLK(st.st_mode)) {
        unsigned long long bytes;

        if (ioctl(fd, BLKGETSIZE64, &bytes) != 0)
            return -1;

        *size = bytes;
        return 0;
    }

    return -1;
}

int main(int argc, char *argv[]) {
    struct io_uring ring;
    off_t insize;
    int ret;

    infd = open(argv[1], O_RDONLY);
    outfd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);

    if (setup_context(QD, &ring))
        return 1;
    if (get_file_size(infd, &insize))
        return 1;

    ret = copy_file(&ring, insize);

    close(infd);
    close(outfd);
    io_uring_queue_exit(&ring);
    return ret;
}

其他说明

代码中实现了三个函数:

  1. copy_file():高层复制循环逻辑;它会调用 queue_rw_pair(ring, this_size, offset) 来构造 SQE pair;并通过一次 io_uring_submit() 调用将所有构建的 SQE pair 提交。 这个函数维护了一个最大 DQ 数量的 inflight SQE,只要数据 copy 还在进行中;否则,即数据已经全部读取完成,就开始等待和收割所有的 CQE。
  2. queue_rw_pair() 构造一个 read-write SQE pair. read SQE 的 IOSQE_IO_LINK flag 表示开始一个 chain,write SQE 不用设置这个 flag,标志着这个 chain 的结束。用户 data 字段设置为同一个 data 描述符,并且在随后的 completion 处理中会用到。
  3. handle_cqe() 从 CQE 中提取之前由 queue_rw_pair() 保存的 data 描述符,并在描述符中记录处理进展(index)。 如果之前请求被取消,它还会重新提交 read-write pair。 一个 CQE pair 的两个 member 都处理完成之后(index==2),释放共享的 data descriptor。最后通知内核这个 CQE 已经被消费。

4io_uring性能压测(基于fio)

对于已经在使用 linux-aio 的应用,例如 ScyllaDB,不要期望换成 io_uring 之后能获得大幅的性能提升,这是因为:io_uring 性能相关的底层机制与 linux-aio 并无本质不同(都是异步提交,轮询结果)。

在此,本文也希望使读者明白:io_uring **首先和最重要的贡献**在于:将 linux-aio 的所有优良特性带给了普罗大众(而非局限于数据库这样的细分领域)。

4.1 测试环境

本节使用 fio 测试 4 种模式:

  1. synchronous reads
  2. posix-aio (implemented as a thread pool)
  3. linux-aio
  4. io_uring

硬件:

  • NVMe 存储设备,物理极限能打到 3.5M IOPS
  • 8 核处理器

4.2 场景一:direct I/O1KB随机读(绕过 page cache)

第一组测试中,我们希望所有的读请求都能 命中存储设备(all reads to hit the storage), 完全绕开操作系统的页缓存(page cache)。

测试配置:

  • 8 个 CPU 执行 72 fio job,
  • 每个 job 随机读取 4 个文件,
  • iodepth=8(number of I/O units to keep in flight against the file.)。

这种配置 保证了 CPU 处于饱和状态,便于观察 I/O 性能。如果 CPU 数量足够多,那每组测试都可能会打满设备带宽,结果对 I/O 压测就没意义了。

表 1. Direct I/O(绕过系统页缓存):1KB 随机读,CPU 100% 下的 I/O 性能

backend

IOPS

context switches

IOPS ±% vs io_uring

sync

814,000

27,625,004

-42.6%

posix-aio (thread pool)

433,000

64,112,335

-69.4%

linux-aio

1,322,000

10,114,149

-6.7%

io_uring (basic)

1,417,000

11,309,574

io_uring (enhanced)

1,486,000

11,483,468

4.9%

​几点分析:

  1. io_uring 相比 linux-aio 确实有一定提升,但并非革命性的。
  2. 开启高级特性,例如 buffer & file registration 之后性能有进一步提升 —— 但也还 没有到为了这些性能而重写整个应用的地步,除非你是搞数据库研发,想榨取硬件的最后一分性能。
  3. io_uring and linux-aio 都比同步 read 接口快 2 倍,而后者又比 posix-aio 快 2 倍 —— 初看有点差异。但看看 上下文切换次数,就不难理解为什么 posix-aio 这么慢了。
  • 同步 read 性能差是因为:在这种没有 page cache 的情况下,每次 read 系统调用都会阻塞,因此就会涉及一次上下文切换
  • posix-aio 性能更差是因为:不仅内核和应用程序之间要频繁上下文切换,线程池的 多个线程之间也在频繁切换

4.2 场景二:buffered I/O1KB随机读(数据提前加载到内存,100% hot cache)

第二组测试 buffered I/O:

  1. 将文件数据提前加载到内存,然后再测随机读。
  • 由于 数据全部在 page cache,因此 同步 read 永远不会阻塞
  • 这种场景下,我们预期 同步读和 io_uring 的性能差距不大(都是最好的)
  • 其他测试条件不变。

表 2. Buffered I/O(数据全部来自 page cache,100% hot cache):1KB 随机读,100% CPU 下的 I/O 性能

Backend

IOPS

context switches

IOPS ±% vs io_uring

sync

4,906,000

105,797

-2.3%

posix-aio (thread pool)

1,070,000

114,791,187

-78.7%

linux-aio

4,127,000

105,052

-17.9%

io_uring

5,024,000

106,683

​结果分析:

  1. 同步读和 io_uring 性能差距确实很小,二者都是最好的。 但注意, **实际的应用**不可能一直 100% 时间执行 IO 操作,因此 基于同步读的真实应用性能 还是要比基于 io_uring 要差的,因为 io_uring 会将多个系统调用批处理化。
  2. posix-aio 性能最差,直接原因是 上下文切换次数太多,这也和场景相关:在这种 CPU 饱和的情况下,它的线程池反而是累赘,会完全拖慢性能。
  3. linux-aio 并 不是针对 buffered I/O 设计的,在这种 page cache 直接返回的场景, 它的 异步接口反而会造成性能损失 —— 将操作分 为 dispatch 和 consume 两步不但没有性能收益,反而有额外开销。

4.3 性能测试小结

最后再次提醒,本节是极端应用/场景( 100% CPU + 100% cache miss/hit)测试, 真实应用的行为通常处于同步读和异步读之间:时而一些阻塞操作,时而一些非阻塞操作。但不管怎么说,用了 io_uring 之后,用户就无需担心同步和异步各占多少比例了,因为它 在任何场景下都表现良好

  1. 如果操作是非阻塞的,io_uring 不会有额外开销;
  2. 如果操作是阻塞式的,也没关系,io_uring 是完全异步的,并且不依赖线程池或昂贵的上下文切换来实现这种异步能力;

本文测试的都是随机读,但对 其他类型的操作,io_uring 表现也是非常良好的。例如:

  1. 打开/关闭文件
  2. 设置定时器
  3. 通过 network sockets 传输数据

而且 使用的是同一套 io_uring 接口

4.4 ScyllaDB 与 io_uring

Scylla 重度依赖 direct I/O,从一开始就使用 linux-aio。在我们转向 io_uring 的过程中,最开始测试显示对某些 workloads,能取得 50% 以上的性能提升。但 深入研究之后发现,这是因为我们 之前的 linux-aio 用的不够好。这也揭示了一个 经常被忽视的事实:获得高性能没有那么难(前提是你得弄对了)。在对比 io_uring 和 linux-aio 应用之后,我们 很快改进了一版,二者的性能差距就消失了。但坦率地说,解决这个问题 需要一些工作量,因为要改动一个已经使用 了很多年的基于 linux-aio 的接口。而对 io_uring 应用来说,做类似的改动是轻而 易举的。

以上只是一个场景,io_uring 相比 linux-aio 的 **优势**是能应用于 file I/O 之外的场景。此外,它还自带了特殊的 高性能[19] 接口,例如 buffer registration、file registration、轮询模式等等。

启用 io_uring 高级特性之后,我们看到性能确实有提升:Intel Optane 设备,单个 CPU 读取 512 字节,观察到 5% 的性能提升。与 表 1 & 2 对得上。虽然 5% 的提升 看上去不是太大,但对于希望压榨出硬件所有性能的数据库来说,还是非常宝贵的。

5 eBPF

eBPF 也是一个 事件驱动框架(因此也是异步的),允许用户空间程序动态向内核注入字节码,主要有两个使用场景:

  1. Networking
  2. Tracing & Observability:例如 bcc[21] 等工具

eBPF 在内核 4.9 首次引入,4.19 以后功能已经很强大。

谈到与 io_uring 的结合,就是用 bcc 之类的工具跟踪一些 I/O 相关的内核函数,例如:

  1. Trace how much time an application spends sleeping, and what led to those sleeps. (wakeuptime)
  2. Find all programs in the system that reached a particular place in the code (trace)
  3. Analyze network TCP throughput aggregated by subnet (tcpsubnet)
  4. Measure how much time the kernel spent processing softirqs (softirqs)
  5. Capture information about all short-lived files, where they come from, and for how long they were opened (filelife)

6 结束语

io_uring 和 eBPF 这两大特性 将给 Linux 编程带来革命性的变化。有了这两个特性的加持,开发者就能更充分地利用 Amazon i3en meganode systems[22]之类的多核/多处理器系统,以及 Intel Optane 持久存储[23]之类的 us 级延迟存储设备。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux 异步 I/O 框架 io_uring:基本原理、程序示例与性能压测 的相关文章

  • Capistrano 3 部署无法连接到 GitHub - 权限被拒绝(公钥)

    我使用 Capistrano v3 和 capistrano symfony gem 设置了以下部署脚本 我正在使用 Ubuntu 14 4 部署到 AWS EC2 实例 我正在连接从 AWS 下载的 pem 文件 我的deploy rb中
  • 使用 plistBuddy 获取值数组

    var keychain access groups declare a val usr libexec PlistBuddy c Print var sample plist echo val echo val 0 Ouput Array
  • 限制 Imagemagick 使用的空间和内存

    我在 Rails 应用程序上使用 Imagemagick 使用 rmagick 但我的服务器 Ubuntu 不是很大 当我启动转换进程时 Imagemagick 占据了我的服务器 30GB HDD 的所有位置 内存 我想限制内存和 tmp
  • Gearman,php 扩展问题:使用终端在 .. 中找不到类“GearmanWorker”,但可以在浏览器上使用

    我最近在 ubuntu 10 04 上安装了 gearman 并安装了它的 pecl 扩展 现在 当我在浏览器中运行一个 php 文件时 其中包含 client new GearmanWorker die var Dump client I
  • Linux 上的 RTLD_LOCAL 和dynamic_cast

    我们有一个由应用程序中的一些共享库构成的插件 我们需要在应用程序运行时更新它 出于性能原因 我们在卸载旧插件之前加载并开始使用新插件 并且只有当所有线程都使用旧插件完成后 我们才卸载它 由于新插件和旧插件的库具有相同的符号 我们dlopen
  • linux命令中括号的用途是什么[重复]

    这个问题在这里已经有答案了 我在 Linux 终端中运行以下命令 谁能告诉我 Linux 终端中括号和以下命令的用途是什么 echo GET HTTP 1 0 echo 主机 www google com echo 数控 www googl
  • 为什么我的 Dockerfile CMD 不起作用?

    所以在我的 Dockerfile 的末尾我有这样的内容 WORKDIR home CMD django admin startproject whattt CMD bin bash 当我创建映像然后运行容器时 一切都按预期运行 没有错误 D
  • Web 应用程序的带宽和流量模拟器?

    您能否建议如何创建一个测试环境来模拟 Web 应用程序中的各种类型的带宽和流量 或者也许是一个针对本地主机执行此操作的开源程序 我认为在编写网络应用程序时这是一个非常重要的主题 但这不是一个常见的主题 我能想象创建这种环境的唯一方法是在本地
  • 使用 inotify 的正确方法是什么?

    我想使用inotifyLinux 上的机制 我希望我的应用程序知道文件何时aaa被改变了 您能给我提供一个如何做到这一点的示例吗 文档 来自监视文件系统活动 inotify https developer ibm com tutorials
  • linux下写入后崩溃

    如果我使用 write 将一些数据写入磁盘上的文件会发生什么 但我的应用程序在刷新之前崩溃了 如果没有系统故障 是否可以保证我的数据最终会刷新到磁盘 如果您正在使用write 并不是fwrite or std ostream write 那
  • 在 Linux 上访问 main 之外的主要参数

    是否可以访问参数main在外面main 即在共享库构造函数中 在 Linux 上除了通过解析之外 proc self cmdline 您可以通过将构造函数放入 init array部分 功能在 init array 不像 init 使用相同
  • pprof 和 ps 之间的内存使用差异

    我一直在尝试分析用 cobra 构建的 cli 工具的堆使用情况 这pprof工具显示如下 Flat Flat Sum Cum Cum Name Inlined 1 58GB 49 98 49 98 1 58GB 49 98 os Read
  • Linux“屏幕”的 Windows 等效项还是其他替代方案?

    我正在寻找一种在 Windows 环境中控制程序的方法 我希望它与 Linux 软件有点相似 screen 我搜索的原因是我需要使用标识符启动一个程序 在 Windows 上 这样我以后就能够关闭该特定程序 而无需关闭其他任何程序 即使实际
  • 如何重命名 .tar.gz 文件而不提取内容并在 UBUNTU 中创建新的 .tar.gz 文件?

    我有一个命令将创建一个新的 tar gz现有文件中的文件 sudo tar zcvf Existing tar gz New tar gz 该命令将创建一个新的New tar gz从现有的文件Existing tar gz file 谁能告
  • 如何从远程 ssh 连接上运行的 tmux(复制模式)复制到本地剪贴板

    我通过 OS X 上的 VirtualBox 运行 Linux 我通过在无头状态下运行虚拟机 然后使用端口转发 sshing 到 Linux 机器来实现这一点 现在 无论复制到我的虚拟机上的剪贴板 我都可以粘贴到我的远程 ssh 会话上 但
  • 将node.js +expressjs应用程序的NODE_ENV设置为ubuntu下的守护进程

    我按照这些说明让守护进程正常工作 http kevin vanzonneveld net techblog article run nodejs as a service on ubuntu karmic http kevin vanzon
  • 如何在 Linux/OS X 上温和地终止 Firefox 进程

    我正在使用 Firefox 进行一些自动化操作 尽管我可以从 shell 打开 Firefox 窗口 但我无法正确终止它 如果我kill火狐进程与kill 3 or kill 2当我下次打开新的 Firefox 窗口时 命令会询问我是否要在
  • Python将文件从Linux复制到WIndows

    我正在构建一个网站 该网站有一个表单 可以捕获用户数据并在用户数据上运行一些cgi cgi 的第一步是需要将文件从 Linux Web 服务器复制到 Windows 计算机 服务器将使用 Active Directory 角色帐户作为复制凭
  • 在 Linux 控制台中返回一行?

    我知道我可以返回该行并用以下内容覆盖其内容 r 现在我怎样才能进入上一行来改变它呢 或者有没有办法打印到控制台窗口中的特定光标位置 我的目标是使用 PHP 创建一些自刷新的多行控制台应用程序 Use ANSI 转义码 http en wik
  • 可以作为命令行参数传递多少数据?

    在 Linux 下生成进程时可以发送多少字节作为命令行参数 gahooa 推荐了一篇好文章http www in ulm de mascheck various argmax http www in ulm de mascheck vari

随机推荐

  • 测试人员必备:常用自动化测试工具

    Appium 官网 http appium io AppUI自动化测试 Appium 是一个移动端自动化测试开源工具 支持iOS 和Android 平台 支持Python Java 等语言 即同一套Java 或Python 脚本可以同时运行
  • 并发编程系列之CountDownLatch对战Cyclicbarrier

    前言 前面我们介绍了并发容器和队列 今天我们来介绍几个非常有用的并发工具类 今天主要讲CountDownLatch和Cyclicbarrier这两个工具类 通过讲解并对比两个类的区别 OK 让我们开始今天的并发之旅吧 什么是CountDow
  • Python爬虫学习-第四篇 Scrapy框架抓取唯品会数据

    上篇博文讲述了scrapy的框架和组件 对于scrapy有了基本的了解 那么我们进入今天的正题 使用Scrapy框架爬取数据 1 创建Scrapy项目 创建Scrapy工程文件的命令 scrapy startproject scrapyte
  • java实现飞机大战(简单版)

    import javafx animation AnimationTimer import javafx application Application import javafx scene Group import javafx sce
  • java压缩字符串并生成二维码

    针对特殊需求需要使用二维码传输数据 为了降低二维码的复杂度和提高数据传输量 需要先对数据进行压缩 然后生成二维码 压缩后的数据是byte 如果再转回字符串会严重影响压缩效果 因此考虑直接使用byte 生成和解析二维码 为了实现使用byte
  • 解决 “/lib64/libc.so.6: version `GLIBC_2.18‘ not found (required by /lib64/libstdc++.so.6)“

    https blog csdn net wiborgite article details 87707938
  • 基于RedHat 8.2源码编译升级Kernel 5.8.1

    1 基于RedHat 8 2源码编译升级Kernel 5 8 1 1 1 背景 只是单纯为了验证工作中遇到的一个bug 才诞生了此文 1 2 先从如下网址下载内核源码包 我这里下载的是 linux 5 8 1 tar gz 大家可根据自己需
  • 无监督深度估计、运动估计的深度学习方法(二)——SSIM损失函数

    在自监督深度估计中 一般输入2张图像 若为视频 则输入邻近的两帧图像 frame1和frame2 模型先估计相机拍摄这2张图像是的姿态变化pose 然后根据pose将frame1变换到frame2的视角下 得到合成图像synthetic f
  • 数据类型:C++中的基本数据类型

    数据类型 C 中的基本数据类型 在C 中 数据类型是用于存储不同种类数据的变量类型 C 提供了多种基本数据类型 包括字符型和数值类型 本篇博客将详细介绍C 中的基本数据类型 并提供相应的示例代码 目录 引言 字符型数据类型 2 1 char
  • Mysql数据备份-定时自动备份dump备份命令

    登录数据库服务器并打开命令行工具 如Windows系统中的cmd exe 或图形化管理工具 如phpMyAdmin Navicat等 根据数据库类型 执行相应的备份命令 例如 MySQL数据库可以使用以下命令导出备份 mysqldump u
  • 数仓分层理论

    数据仓库 在实际工作中 数仓分层 元数据管理 数据质量管理一直是一个持续优化的过程 我们公司业务也是在持续的做数仓的优化工作 在数据治理这方面还是欠缺很多的经验的 下面先简单整理了一下第一个理论部分的相关笔记 数据仓库理论 数据仓库四大特征
  • 锐捷ap设置为路由模式_路由器AP、路由、桥接模式有什么区别【详细介绍】

    现在的路由大多数已经开始支持多种网络连接模式 那么我们就挑一款模式最全的路由来了解各种模式的区别吧 下文将以TP Link迷你无线路由器为例 在TP Link迷你无线路由器上一般有AP 接入点 模式 Router 无线路由 模式 Repea
  • stm32——EXTI

    EXTI 外部中断 是stm32的众多外设之一 属于中断的一种 它最重要的就是通过检测外部引脚口的电平变化 比如说上升沿 下降沿 以及双边沿 来触发中断 让主程序放下当前的事情 去执行发生中断时应该执行的事情 设置好的函数 大概是如上所属
  • SpringBoot集成OAuth2.0的四种授权方式

    背景 OAuth 开放授权 是一个开放标准 允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息 而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容 OAuth2 0 是OAuth协议的延续版本 但不向后兼容 OAuth
  • Numpy掩码数组masked arrays,这一篇就够了

    Numpy掩码数组masked arrays 这一篇就够了 1 什么是掩码数组 2 示例 2 1 一个例子走进掩码数组 2 2 创建一个数组第二个值是无效的 2 3 创建一个数组所有靠近1 e20的值是无效的 2 4 只获取有效项 2 5
  • 8647服务器装系统,机柜系统资料的.doc

    机柜系统资料的 机房冷通道 机柜系统分为节能系统 机柜 走线系统和配电系统 随着数据中心数据量的飞块速增长 数据中心的能耗量越来越高 主要由运行高功率的IT设备所致 且这些IT设备的密度还在不断提高 IT设备运行所产生大量的热 是数据中心所
  • MySql登录方法

    登录mysql u 用户名 p 用户对应密码 h 主机 socket IP地址 端口号 eg 192 168 10 113 3306 1 方法 mysql u root p密码 mysql u root p mysql hlocalhost
  • shineblink MPU6050六轴传感器测量物体姿态角

    MPU6050六轴传感器测量物体姿态角 一 姿态角 欧拉角 基本概念 二 代码实现功能描述 三 接线图 五 完整代码 六 代码运行结果 七 需要注意的地方 一 姿态角 欧拉角 基本概念 物体在空间中的姿态可以用欧拉角来定义 欧拉角包含pit
  • 数据分析——最小二乘法建立线性回归方程(最简单的一元线性模型为例)

    概述 别看公式多 其实很简单 最小二乘法其实又叫最小平方法 是一种数据拟合的优化技术 实质上是利用最小误差的平方寻求数据的最佳匹配函数 利用最小二乘法可以便捷的求得未知的数据 起到预测的作用 并且是的这些预测的数据与实际数据之间的误差平方和
  • Linux 异步 I/O 框架 io_uring:基本原理、程序示例与性能压测

    io uring是 2019 年 Linux 5 1内核首次引入的高性能异步 I O 框架 能显着加速 I O 密集型应用的性能 但如果你的应用已经在使用传统 Linux AIO 了 并且使用方式恰当 那io uring并不会带来太大的性能