首先,需要明确的一点是io_uring是异步IO模型,这在编程中和同步IO模型是很不一样的。前者中,操作系统(或者说内核)向程序通知的是存在IO事件待处理,而具体的IO操作仍需要用户通过read()、write()等系统调用或库函数完成,后者中,操作系统向程序通知的是IO事件已完成,此时目标数据已经读入到/写出自提前指定好的缓冲区,程序接下来只需要执行后续业务逻辑处理即可。
class EventLoop:实现事件驱动的基础,负责收割事件
class TcpServer:用于编写服务器程序,管理Acceptor、TcpConnection
class Acceptor:用于建立新连接,管理监听套接字对应的Channel
class TcpConnection:管理已有连接,管理连接套接字对应的Channel
class Channel:管理fd(包括监听套接字、连接套接字)
struct ConnInfo:在io_uring事件提交和接收过程期间传递信息
从io_uring_wait()获得返回的struct io_uring_cqe * -> 判断已完成事件的类型 -> 调用对应事件处理函数进行处理
- 事件处理函数已经提前设置好,如读回调、写回调、自定义回调等
创建(socket())并命名(bind())监听套接字 -> 开始监听(listen()) -> 通过io_uring_prep_accept()使内核接管监听套接字 -> 等待io_uring_wait()返回,即存在新连接等待处理
从io_uring_wait()返回的struct io_uring_cqe *中获取新的连接套接字fd -> 通过io_uring_prep_send()使内核接管连接套接字 -> 等待io_uring_wait()返回,即已经有数据被读入到缓冲区中等待处理
设置struct __kernel_timespec格式的超时事件 -> 通过io_uring_prep_timeout()使内核接管超时事件 -> 等待io_uring_wait()返回,即已经有数据被读入到缓冲区中等待处理
echo.cpp核心代码见附录部分或./echo.cpp
timer.cpp核心代码见附录部分或./timer.cpp
- 说明:本实验失败,程序并未在指定超时时间结束后执行回调函数,推测应该是我对io_uring_prep_timeout()中使用的struct __kernel_timespec和需要设置的flags理解有误
测试在虚拟机下进行
CPU:Intel Core i5 8300H
内存:8GB 2667Mhz
硬盘:浦科特M9PeG 512GB
操作系统:Ubuntu 20.04.4
Linux内核版本:5.13.0
liburing版本:2022年5月25日clone自Github上axboe的liburing代码仓库
性能测试程序:GitHub上haraldh的rust_echo_bench代码仓库
单位:request/sec
详细测试log位于:./benchmark
./echo.cpp
- 程序中的void onConnection()是注册到每个class TcpConnection实例上的连接事件回调函数,在每次连接建立、销毁时被调用(一般是输出相关信息),为了防止额外IO操作对测试结果产生影响,测试时此函数函数体为空
- 测试过程中服务器偶尔会出现性能大幅度下降,原因不明
bytes\clients | 1 | 50 | 150 | 300 | 500 | 1000 |
---|---|---|---|---|---|---|
128 bytes | 13117 | 43832 | 40941 | 37453 | 33749 | 31232 |
512 bytes | 12856 | 42195 | 40683 | 36248 | 33586 | 30099 |
1000 bytes | 12821 | 42650 | 39539 | 34777 | 33476 | 30470 |
对比项1,使用参考资料3
bytes\clients | 1 | 50 | 150 | 300 | 500 | 1000 |
---|---|---|---|---|---|---|
128 bytes | 13413 | 46096 | 45784 | 44423 | 42771 | 40945 |
512 bytes | 12502 | 42602 | 46613 | 44911 | 44378 | 44076 |
1000 bytes | 13645 | 47479 | 48653 | 43669 | 41855 | 43587 |
bytes\clients | 1 | 50 | 150 | 300 | 500 | 1000 |
---|---|---|---|---|---|---|
128 bytes | 12862 | 46930 | 45802 | 43954 | 42714 | 42432 |
512 bytes | 12856 | 46659 | 44349 | 42123 | 41532 | 41829 |
1000 bytes | 13122 | 46911 | 44302 | 42601 | 41809 | 42214 |
- 从结果1和结果2对比可以看出,我自己实现的使用io_uring且遵循Reactor架构实现的echo服务器和只使用了io_uring的裸echo服务器性能相比更弱
- 这可能跟为了遵循Reactor架构而衍生出的很多不必要调用有关
- 从结果2和结果3对比可以看出,使用io_uring的裸echo服务器和使用epoll的裸echo服务器性能相比几乎相同
- 理论上来讲,io_uring性能应该更强,现在这个结果可能跟当前对io_uring的使用并没有开启feat fast poll模式有关
- GitHub上chenshuo的muduo代码仓库
- GitHub上chenshuo的recipes代码仓库
- GitHub上frevib的io_uring-echo-server代码仓库
- 长文梳理Muduo库核心代码及优秀编程细节剖析
- AIO 的新归宿:io_uring
- 浅析开源项目之io_uring
- Lord of the io_uring
- Efficient IO with io_uring
- 加入多线程支持,借鉴muduo实现one loop per thread
- 当前是全局缓存方案,要改成类似muduo中的缓存方案
- 当前不支持同时存在多个定时器,加入对TimerQueue的支持
- 断开连接处理逻辑不完整
- 添加对io_uring的feast fast poll支持
-
echo.cpp核心代码
void onConnection(const TcpConnectionPtr& conn) { // 什么也不做 } void onMessage(const TcpConnectionPtr& conn, char *buf, int size) { conn->send(buf, size); } int main() { uint16_t port = 65421; EventLoop loop; TcpServer server(&loop, port); server.setConnectionCallback(onConnection); server.setMessageCallback(onMessage); server.start(); loop.loop(); }
-
timer.cpp核心代码
void printCurrentTime() { time_t nowTime; struct tm *nowLocalTime; nowTime = time(nullptr); nowLocalTime = localtime(&nowTime); printf("%d-%d-%d %d:%d:%d\n", nowLocalTime->tm_year + 1900, nowLocalTime->tm_mon + 1, nowLocalTime->tm_mday, nowLocalTime->tm_hour, nowLocalTime->tm_min, nowLocalTime->tm_sec); } int main() { EventLoop loop; Timer timer(&loop); printCurrentTime(); timer.runAfter(3.5, printCurrentTime); loop.loop(); }