本來準(zhǔn)備寫一篇詳細(xì)關(guān)于
io_uring
的中文文章逸寓,不過在使用上官方的一些文章寫的已經(jīng)非常詳細(xì),簡(jiǎn)單的拿來翻譯感覺又失去了樂趣
于是便借鑒 liburing,配合 Go 提供的并發(fā)機(jī)制實(shí)現(xiàn)了一個(gè) golang 版本的異步 IO 庫 —— iouring-go 來學(xué)習(xí)io_uring
的使用
本文不會(huì)去詳細(xì)介紹
io_uring
的一些細(xì)節(jié)霎肯,如果想對(duì)io_uring
了解更多可以查看文末的推薦閱讀
Golang 中并發(fā) IO 的現(xiàn)狀
對(duì)于 Go 這種本身便是為并發(fā)而生的語言來說蔓纠,使用 io_uring
這種系統(tǒng)級(jí)異步接口也不是那么的迫切
比如對(duì)于普通文件的讀寫以及 socket 的操作都會(huì)通過 netpoll
來進(jìn)行優(yōu)化,當(dāng)文件/套接字可讀可寫時(shí) netpoll 便會(huì)喚醒相應(yīng) goroutine 來對(duì)文件進(jìn)行讀寫
而對(duì)于可能會(huì)阻塞的系統(tǒng)調(diào)用匣砖,在 syscall 底層調(diào)用 syscall.Syscall/Syscall6
時(shí)配合 runtime 判斷是否將P 和 G 綁定的 M 解綁,然后將 P 交給其他 M 來使用昏滴,通過這種機(jī)制可以減少系統(tǒng)調(diào)用從用戶態(tài)切換到內(nèi)核態(tài)對(duì)整個(gè)程序帶來的損耗
Go runtime 實(shí)際上已經(jīng)實(shí)現(xiàn)了用戶態(tài)的并發(fā) IO猴鲫,現(xiàn)在 Linux 內(nèi)核提供了新的異步 IO 接口,那又該如何去利用這種新的技術(shù)呢
我們首先先看一下當(dāng)前 Go 是如何做到異步 IO 的
IO 與 netpoll
文件 IO 與 netpoll
// src/os/file.go
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
f, err := openFileNolog(name, flag, perm)
}
// src/os/file_unix.go
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
// ...
r, e = syscall.Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
// ...
return newFile(uintptr(r), name, kindOpenFile), nil
}
// src/os/file_unix.go
func newFile(fd uintptr, name string, kind newFileKind) *File {
fdi := int(fd)
f := &File{&file{
pfd: poll.FD{
Sysfd: fdi,
IsStream: true,
ZeroReadIsEOF: true,
},
name: name,
stdoutOrErr: fdi == 1 || fdi == 2,
}}
pollable := kind == kindOpenFile || kind == kindPipe || kind == kindNonBlock
// ...
if err := f.pfd.Init("file", pollable); err != nil {
// ...
} else if pollable {
if err := syscall.SetNonblock(fdi, true); err == nil {
f.nonblock = true
}
return f
}
}
從 os.Open
到 newFile
谣殊,可以看到文件的 文件描述符
被放到 poll.FD
進(jìn)行初始化了, poll.FD.Init
便是將文件描述符
注冊(cè)到 netpoll(epoll)
中
需要注意當(dāng)文件被注冊(cè)到
netpoll(epoll)
后拂共,會(huì)將它置為非阻塞模式(SetNonblock
),因?yàn)?netpoll(epoll)
采用的是邊緣觸發(fā)模式
比如說非阻塞文件描述符中有可讀事件時(shí)姻几,epoll
只會(huì)通知一次(除非有新的數(shù)據(jù)被寫入文件會(huì)再次通知)宜狐,也就說需要所有數(shù)據(jù)讀出來直到返回-EAGAIN
,對(duì)于阻塞模式的socket文件蛇捌,當(dāng)從socket中讀取數(shù)據(jù)時(shí)就可能會(huì)阻塞等待抚恒,這樣也就失去了 epoll 的意義
我們可以再看一下 poll.FD
是如何利用 netpoll
進(jìn)行讀取的
// src/internal/poll/fd_unix.go
func (fd *FD) Read(p []byte) (int, error) {
// ...
for {
n, err := ignoringEINTR(syscall.Read, fd.Sysfd, p)
if err != nil {
n = 0
if err == syscall.EAGAIN && fd.pd.pollable() {
continue
}
}
err = fd.eofError(n, err)
return n, err
}
}
可以看到 ignoringEINTR
中調(diào)用 syscall.Read
讀取文件,如果出現(xiàn) syscall.EAGAIN
豁陆,那么就調(diào)用 fd.pd.waitRead
來等待數(shù)據(jù)可讀
// src/internal/poll/fd_unix.go
type FD struct {
// ...
Sysfd int
pd pollDesc
}
pollDesc
Go 對(duì) netpoll
的抽象
// src/internal/poll/fd_poll_runtime.go
func runtime_pollServerInit()
func runtime_pollOpen(fd uintptr)(uintptr, int)
func runtime_pollClose(ctx uintptr)
// ...
type pollDesc struct {
runtimeCtx uintptr
}
func (pd *pollDesc) init(fd *FD) error {
serverInit.Do(runtime_pollServerInit)
ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd))
// ...
pd.runtimeCtx = ctx
return nil
}
runtime_poll*
這些函數(shù)才是真正的 netpoll柑爸,而這些函數(shù)是在src/runtime/netpoll.go 中實(shí)現(xiàn),并通過 go:linkname
來鏈接到 internal/poll 中
// src/runtime/netpoll.go
// go:linkname poll_runtime_pollServerInit internal/poll.runtime_pollServerInit
func poll_runtime_pollServerInit() {
// ...
}
根據(jù)具體的平臺(tái)來實(shí)現(xiàn) poller盒音,對(duì)于 Linux表鳍,便是使用 epoll
// src/runtime/netpoll_epoll.go
// 注冊(cè)文件到 netpoll 中
func netpolllopen(fd uintptr, pd *pollDesc) int32 {
var ev epollevent
ev.events = _EPOLLIN | _EPOLLOUT | _EPOLLRDHUP | _EPOLLET
// ...
return -epollctr(epfd, _EPOLL_CTL_ADD, int32(fd), &ev)
}
添加新的文件描述符時(shí),可以發(fā)現(xiàn)fd
是以 邊緣觸發(fā)
的方式注冊(cè)到 netpoll(epoll)
中
socket IO 與 netpoll
從 netpoll
這個(gè)名字上就可以看出祥诽,netpoll
是 Go 為了高性能的異步網(wǎng)絡(luò)而實(shí)現(xiàn)的
看一下創(chuàng)建 TCPListener socket 的流程
// src/net/tcpsock.go
type TCPListener struct {
fd *netFD
// ...
}
// src/net/fd_posix.go
type netFD struct {
pfd poll.FD
// ...
}
// 1.
// src/net/tcpsock.go
func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error) {
sl := &sysListener{network: network, address: laddr.String()}
ln, err := sl.listenTCP(context.Background(), laddr)
// ...
}
// 2.
// src/net/tcpsock_posix.go
func (sl *sysListener) listenTCP(ctx context.Context, laddr *TCPAddr) (*TCPListener, error) {
fd, err := internetSocket(ctx, sl.network, laddr, nil, syscall.SOCK_STREAM, 0, "listen", sl.ListenConfig.Control)
// ...
return &TCPListener{fd: fd, lc, sl.ListenConfig}, nil
}
// 3.
// src/net/ipsock_posix.go
func internelSocket(ctx context.Context, ...) (fd *netFD, err error) {
// ...
return socket(ctx, net, family, sotype, proto, ipv6only, laddr, radddr, ctrlFn)
}
// 4.
// src/sock_posix.go
func socket(...) (fd *netFD, err error) {
s, err := sysSocket(family, sotype, proto)
// ...
fd, err = newFD(s, family, sotype, net)
}
// 5.
// src/fd_unix.go
func newFD(sysfd, family, sotype int, net string) (*netFD, error) {
ret := &netFD{
pfd: poll.FD{
Sysfd: sysfd,
IsStream: sotype == syscall.SOCK_STREAM,
// ...
},
// ...
}
return ret, nil
}
創(chuàng)建 TCPListener 鏈路還是挺長(zhǎng)的譬圣,不過在第四步 socket
函數(shù)中可以看到調(diào)用 newFD
來返回 netFD
實(shí)例,而 netFD.pfd
便是 poll.FD
, 而對(duì) netFD
的讀寫和文件IO一樣便都會(huì)通過 poll.FD
來利用 netpoll
雄坪。
netpoll 喚醒 goroutine
掛起 goroutine
通過 poll.pollDesc
將文件描述符加入到 netpoll
后厘熟,當(dāng)對(duì)文件描述符進(jìn)行讀寫時(shí),如果 syscall.Read
返回 syscall.EAGAIN
的話就需要調(diào)用 pollDesc.waitRead/waitWrite
來等待可讀可寫
// src/internal/poll/fd_poll_runtime.go
func (pd *pollDesc) waitRead(isFile bool) error{
return pd.wait('r', isFile)
}
func (pd *pollDesc) waitWrite(isFile bool) error{
return pd.wait('w', isFile)
}
func (pd *pollDesc) wait(mode int, isFile bool) error{
// ...
res := runtime_pollWait(pd.runtimeCtx, mode)
return convertErr(res, isFile)
}
// src/runtime/netpoll.go
//go:linkname poll_runtime_pollWait internal/poll.runtime_pollWait
func poll_runtime_pollWait(pd *pollDesc, mode int) int {
// ...
for !netpollblock(pd, int32(mode), false) {
// ...
}
// ...
}
func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {
gpp := &pd.rg
// ...
// 狀態(tài)檢查
if waitio || netpollcheckerr(pd, mode) == 0 {
gopark(netpollblockcommit, unsafe.Pointer(gpp), waitReasonIOWait, traceEvGoBlockNet, 5)
}
// ...
}
func netpollblockcommit(gp *g, gpp unsafe.Pointer) bool {
r := atomic.Casuintptr((*uintptr)(gpp), pdWait, uintptr(unsafe.Pointer(gp)))
// ...
}
等待文件可讀寫维哈,最終會(huì)調(diào)用 netpollblock
函數(shù)绳姨,并不會(huì)直接調(diào)用 epoll wait 的系統(tǒng)調(diào)用,而是掛起當(dāng)前 goroutine, 并等待喚醒
喚醒 goroutine
// src/runtime/netpoll_epoll.go
func netpoll(delay int64) gList {
// ...
var waitms int32
// 計(jì)算 waitms阔挠,大概規(guī)則:
// delay < 0, waitms = -1飘庄,阻塞等待
// delay == 0, waitms = 0, 不阻塞
// delay > 0, delay 以納秒為單位作為 waitms
var events [128]epollevent
retry:
n := epollwait(epfd, &events[0], int32(len(events)), waitms)
if n < 0 {
// ...
}
var toRun gList
for i := int32(0); i < n; i++ {
ev := &events[i]
// ...
var mode int32
if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 {
mode += 'r'
}
if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 {
mode += 'w'
}
if mode != 0 {
pd := *(**pollDesc)(unsafe.Pointer(&ev.data))
pd.everr = false
if ev.events == _EPOLLERR {
pd.everr = true
}
netpollready(&toRun, pd, mode)
}
}
return toRun
}
netpoll
會(huì)調(diào)用 epollWait
來獲取epoll事件,而在 runtime 中很多地方都會(huì)調(diào)用 netpoll
函數(shù)
監(jiān)控函數(shù) sysmon
// src/runtime/proc.go
func sysmon() {
// ....
for {
// ...
list := netpoll(0)
if !list.empty() {
//...
injectglist(&list) // 將 goroutine 放到 runable 隊(duì)列中
}
}
}
查找可運(yùn)行的 goroutine
// src/runtime/proc.go
func findrunable() (gp *g, inheritTIme bool) {
top:
// ...
if list := netpoll(0); !list.empty() {
gp := list.pop()
injectglist(&list)
// ...
return gp, false
}
// ....
stop:
// ...
list := netpoll(delta) // block until new work is available
// ...
}
GC 時(shí)調(diào)用 startTheWorld
// src/runtime/proc.go
func startTheWorld() {
systemstack(func() {startTheWorldWithSema(false)})
// ...
}
func startTheWorldWithSema(emitTraceEvent bool) int64 {
// ...
list := netpoll(0)
injectglist(&list)
// ...
}
通常獲取可用的 goroutine 時(shí)都可能有機(jī)會(huì)去調(diào)用 netpoll
购撼,然后再調(diào)用 injectglist(&list)
將可運(yùn)行 goroutine 加入到runq
隊(duì)列中
系統(tǒng)級(jí)異步接口 —— io_uring
本節(jié)不會(huì)詳細(xì)介紹 io_uring 的具體操作跪削,關(guān)于 io_uring 的使用谴仙,可以查看 Lord of the io_uring
Linux kernel 5.1 新增了異步接口 io_uring
,它是 Jens Axboe 以高效碾盐,可擴(kuò)展晃跺,易用為目的設(shè)計(jì)的一種全新異步接口,為什么是全新呢毫玖,因?yàn)?Linux 已經(jīng)提供了異步 IO 接口 —— AIO掀虎,不過就連 Linus 都對(duì)它一陣吐槽
Re: [PATCH 09/13] aio: add support for async openat()
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".
But AIO was always really really ugly.
io_uring 提供的異步接口,不僅僅可以使用 文件 IO孕豹,套接字 IO涩盾,甚至未來可以擴(kuò)展加入其它系統(tǒng)調(diào)用
而且 io_uring
采用應(yīng)用程序和內(nèi)核共享內(nèi)存的方式十气,來提交請(qǐng)求和獲取完成事件
使用共享內(nèi)存的方式可能是內(nèi)核對(duì)接口優(yōu)化的一種趨勢(shì)
io_uring 名稱的意思便是 io use ring励背,而 ring 便是指和內(nèi)核內(nèi)存共享的 提交隊(duì)列
和完成隊(duì)列
兩個(gè)環(huán)形緩沖區(qū)
SubmissionQueueEntry
我們來看一下 io_uring 用來提交請(qǐng)求的結(jié)構(gòu)
struct io_uring_sqe {
__u8 opcode; /* 請(qǐng)求的操作類型 */
__u8 flags; /* IOSQE_ flags */
__u16 ioprio; /* ioprio for the request */
__s32 fd; /* 用于 IO 的文件描述符 */
union {
__u64 off; /* offset into file */
__u64 addr2;
};
union {
__u64 addr; /* pointer to buffer or iovecs */
__u64 splice_off_in;
};
__u32 len; /* buffer size or number of iovecs */
/*
* 用于特定操作的字段
*/
union {
__kernel_rwf_t rw_flags;
__u32 fsync_flags;
__u16 poll_events; /* compatibility */
__u32 poll32_events; /* word-reversed for BE */
__u32 sync_range_flags;
__u32 msg_flags;
__u32 timeout_flags;
__u32 accept_flags;
__u32 cancel_flags;
__u32 open_flags;
__u32 statx_flags;
__u32 fadvise_advice;
__u32 splice_flags;
};
__u64 user_data; /* 用來關(guān)聯(lián)請(qǐng)求的完成事件 */
union {
struct {
/* pack this to avoid bogus arm OABI complaints */
union {
/* index into fixed buffers, if used */
__u16 buf_index;
/* for grouped buffer selection */
__u16 buf_group;
} __attribute__((packed));
/* personality to use, if used */
__u16 personality;
__s32 splice_fd_in;
};
__u64 __pad2[3];
};
};
io_uring_sqe
一個(gè)非常復(fù)雜的結(jié)構(gòu),最核心的三個(gè)字段 opcode
砸西,fd
叶眉,user_data
-
opcode
指定具體是什么操作,比如IORING_OP_READV
芹枷,IORING_OP_ACCEPT
衅疙,IORING_OP_OPENAT
,現(xiàn)在支持 35 種操作 -
fd
表示用于 IO 操作的文件描述符 -
user_data
當(dāng)請(qǐng)求操作完成后鸳慈,io_uring 會(huì)生成一個(gè)完成事件放到完成隊(duì)列(CompletionQueue)
中饱溢,而user_data
便是用來和完成隊(duì)列
中的事件進(jìn)行綁定的,他會(huì)原封不動(dòng)的復(fù)制到完成隊(duì)列的事件(cqe)
中 -
flags
字段用來實(shí)現(xiàn)鏈?zhǔn)秸?qǐng)求之類的功能
可以發(fā)現(xiàn)
opcode
是uint8走芋,也就是現(xiàn)在來看最多支持 256 個(gè)系統(tǒng)調(diào)用绩郎,但是整個(gè)io_uring_sqe
還為未來預(yù)留了一些空間__pad2
通過 opcode
結(jié)合結(jié)合其他 union
的字段,便實(shí)現(xiàn)了擴(kuò)展性極強(qiáng)的 io_uring
接口
CompletionQueueEvent
完成隊(duì)列事件(CompletionQueueEvent)
的結(jié)構(gòu)就比較簡(jiǎn)單了翁逞,主要是表示提交的異步操作執(zhí)行的結(jié)果
struct io_uring_cqe {
__u64 user_data; /* 直接復(fù)制 sqe->data */
__s32 res; /* 異步操作的結(jié)果 */
__u32 flags;
};
user_data
便是從 sqe->data
直接復(fù)制過來了肋杖,可以通過 user_data
綁定到對(duì)應(yīng)的 sqe
res
便是異步操作執(zhí)行的結(jié)果,如果 res < 0挖函,通常說明操作執(zhí)行錯(cuò)誤
flags
暫時(shí)沒有使用
io_uring_setup
io_uring 使用共享內(nèi)存的方式状植,來提交請(qǐng)求和獲取執(zhí)行結(jié)果,減少內(nèi)存拷貝帶來的損耗
io_uring_setup
接口會(huì)接受一個(gè)指定提交隊(duì)列大小的 uint32
類型參數(shù)和一個(gè) io_uring_params
對(duì)象
#include <linux/io_uring.h>
int io_uring_setup(u32 entries, struct io_uring_params *p);
調(diào)用成功會(huì)返回一個(gè)文件描述符
怨喘,用于后續(xù)的操作
io_uring_params
struct io_uring_params {
__u32 sq_entries;
__u32 cq_entries;
__u32 flags;
__u32 sq_thread_cpu;
__u32 sq_thread_idle;
__u32 features;
__u32 wq_fd;
__u32 resv[3];
struct io_sqring_offsets sq_off;
struct io_cqring_offsets cq_off;
};
io_uring_params
不只用來配置 io_uring
實(shí)例津畸,內(nèi)核也會(huì)填充 io_uring_params
中關(guān)于 io_uring
實(shí)例的信息,比如用來映射共享內(nèi)存的請(qǐng)求隊(duì)列和完成隊(duì)列字段的偏移量 - io_sqring_offsets
和 io_cqring_offsets
配置 io_uring
flags
以位掩碼的方式必怜,結(jié)合相應(yīng) sq_thread_cpu
肉拓,sq_thread_idle
,wq_fd
棚赔,cq_entries
字段來配置 io_uring 實(shí)例
/*
* io_uring_setup() flags
*/
#define IORING_SETUP_IOPOLL (1U << 0) /* io_context is polled */
#define IORING_SETUP_SQPOLL (1U << 1) /* SQ poll thread */
#define IORING_SETUP_SQ_AFF (1U << 2) /* sq_thread_cpu is valid */
#define IORING_SETUP_CQSIZE (1U << 3) /* app defines CQ size */
#define IORING_SETUP_CLAMP (1U << 4) /* clamp SQ/CQ ring sizes */
#define IORING_SETUP_ATTACH_WQ (1U << 5) /* attach to existing wq */
#define IORING_SETUP_R_DISABLED (1U << 6) /* start with ring disabled */
通常 cq_entries 為 sq_entries 的兩倍帝簇,通過 flags
指定 IORING_SETUP_CQSIZE
徘郭,然后設(shè)置 cq_entries
字段為指定大小
cq_entries 不能小于 sq_entries
iouring-go 提供了初始化 io_uring 對(duì)象時(shí)的配置函數(shù),可以看一下這些函數(shù)的具體實(shí)現(xiàn)
type IOURingOption func(*IOURing)
func New(entries uint, opts ...IOURingOption) (iour *IOURing, err error)
func WithParams(params *iouring_syscall.IOURingParams) IOURingOption
func WithAsync() IOURingOption
func WithDisableRing() IOURingOption
func WithCQSize(size uint32) IOURingOption
func WithSQPoll() IOURingOption
func WithSQPollThreadCPU(cpu uint32) IOURingOption
func WithSQPollThreadIdle(idle time.Duration) IOURingOption
內(nèi)核填充信息
內(nèi)核會(huì)向 io_uring_params
填充跟 io_uring 實(shí)例相關(guān)的信息
sq_entries
請(qǐng)求隊(duì)列的大小丧肴,io_uring_setup
會(huì)傳遞請(qǐng)求隊(duì)列的大小 entries
残揉,io_uring 會(huì)根據(jù) entries
設(shè)置 sq_entries
為 2 的次方大小
cq_entries
完成隊(duì)列的大小,通常為 sq_entries
的兩倍芋浮,即使通過 IORING_SETUP_CQSIZE
flag 設(shè)置了 cq_enries
抱环,內(nèi)核依然會(huì)以 2 的次方重新計(jì)算出 cq_entries
的大小
features
記錄了當(dāng)前內(nèi)核版本支持的一些功能
/*
* io_uring_params->features flags
*/
#define IORING_FEAT_SINGLE_MMAP (1U << 0)
#define IORING_FEAT_NODROP (1U << 1)
#define IORING_FEAT_SUBMIT_STABLE (1U << 2)
#define IORING_FEAT_RW_CUR_POS (1U << 3)
#define IORING_FEAT_CUR_PERSONALITY (1U << 4)
#define IORING_FEAT_FAST_POLL (1U << 5)
#define IORING_FEAT_POLL_32BITS (1U << 6)
#define IORING_FEAT_SQPOLL_NONFIXED (1U << 7)
io_sqring_offsets
和 io_cqring_offsets
便是SQ
和CQ
在共享內(nèi)存中的偏移量
struct io_sqring_offsets {
__u32 head;
__u32 tail;
__u32 ring_mask;
__u32 ring_entries;
__u32 flags;
__u32 dropped;
__u32 array;
__u32 resv1;
__u64 resv2;
};
struct io_cqring_offsets {
__u32 head;
__u32 tail;
__u32 ring_mask;
__u32 ring_entries;
__u32 overflow;
__u32 cqes;
__u32 flags;
__u32 resv1;
__u64 resv2;
};
根據(jù)這些偏移量便可以調(diào)用 mmap 來映射 SQ 和 CQ
ptr = mmap(0, sq_off.array + sq_entries * sizeof(__u32),
PROT_READ|PROT_WRITE, MAP_SHARED|MAP_POPULATE,
ring_fd, IORING_OFF_SQ_RING);
可以參考 iouring-go 對(duì) IOURing 對(duì)象的初始化
// iouring-go/iouring.go
func New(entries uint, opts ...IOURingOption) (*IOURing, error) {
iour := &IOURing{
params: &iouring_syscall.IOURingParams{},
userDatas: make(map[uint64]*UserData),
cqeSign: make(chan struct{}, 1),
closer: make(chan struct{}),
closed: make(chan struct{}),
}
for _, opt := range opts {
opt(iour)
}
var err error
iour.fd, err = iouring_syscall.IOURingSetup(entries, iour.params)
if err != nil {
return nil, err
}
if err := mmapIOURing(iour); err != nil {
munmapIOURing(iour)
return nil, err
}
// ...
}
mmapIOURing
中實(shí)現(xiàn)了對(duì)請(qǐng)求隊(duì)列以及完成隊(duì)列的內(nèi)存映射
// iouring-go/mmap.go
func mmapIOURing(iour *IOURing) (err error) {
defer func() {
if err != nil {
munmapIOURing(iour)
}
}()
iour.sq = new(SubmissionQueue)
iour.cq = new(CompletionQueue)
if err = mmapSQ(iour); err != nil {
return err
}
if (iour.params.Features & iouring_syscall.IORING_FEAT_SINGLE_MMAP) != 0 {
iour.cq.ptr = iour.sq.ptr
}
if err = mmapCQ(iour); err != nil {
return err
}
if err = mmapSQEs(iour); err != nil {
return err
}
return nil
}
這里不再詳細(xì)介紹 io_uring
的使用 ,想要了解更多可以查看文末的推薦閱讀
io_uring 的功能
這里簡(jiǎn)單介紹一下 io_uring
提供的一些功能纸巷,以及在 Go 中如何去使用
順序執(zhí)行
設(shè)置 sqe->flags
的 IOSQE_IO_DRAIN
標(biāo)記镇草,這樣只有當(dāng)該 sqe
之前所有的 sqes
都完成后,才會(huì)執(zhí)行該 sqe
瘤旨,而后續(xù)的 sqe
也會(huì)在該 sqe
完成后才會(huì)執(zhí)行
在 iouring-go 中可以在構(gòu)建 IOURing
對(duì)象時(shí)使用 WithDrain
來全局設(shè)置請(qǐng)求順序執(zhí)行
iour := iouring.New(8, WithDrain())
針對(duì)單一請(qǐng)求設(shè)置 WithDrain
梯啤,保證請(qǐng)求會(huì)在之前所有的請(qǐng)求都完成才會(huì)執(zhí)行,而后續(xù)的請(qǐng)求也都會(huì)在該請(qǐng)求完成之后才會(huì)開始執(zhí)行
request, err := iour.SubmitRequest(iouring.Read(fd, buf).WithDrain(), nil)
鏈?zhǔn)秸?qǐng)求
io_uring
提供了一組請(qǐng)求的鏈?zhǔn)?順序執(zhí)行的方法存哲,可以讓鏈中的請(qǐng)求會(huì)在上一個(gè)請(qǐng)求執(zhí)行完成后才會(huì)執(zhí)行因宇,而且不影響鏈外其他請(qǐng)求的并發(fā)執(zhí)行
設(shè)置 sqe->flags
的 IOSQE_IO_LINK
標(biāo)記后,下一個(gè) sqe
和當(dāng)前 sqe
自動(dòng)組成新鏈或者當(dāng)前 sqe
的鏈中祟偷,鏈中沒有設(shè)置 IOSQWE_IO_LINK
的 sqe
便是鏈尾
如果鏈中的有請(qǐng)求執(zhí)行失敗了察滑,那么鏈中后續(xù)的 sqe
都會(huì)被取消( cqe.res
為 -ECANCELED
)
io_uring
還提供了以外一種設(shè)置鏈?zhǔn)秸?qǐng)求的方式,設(shè)置 sqe->flags
為 IOSQE_IO_HARDLINK
flag修肠,這種方式會(huì)讓鏈中的請(qǐng)求忽略之前請(qǐng)求的結(jié)果贺辰,也就是說即使鏈中之前的請(qǐng)求執(zhí)行失敗了,也不會(huì)取消鏈中后邊的請(qǐng)求
iouring-go 中可以使用 SubmitLinkRequests
或者 SubmitHardLinkRequests
方法來設(shè)置鏈?zhǔn)秸?qǐng)求
preps := []iouring.PrepRequest{ iouring.Read(fd1, buf), iouring.Write(fd2, buf) }
requests, err := iour.SubmitLinkRequest(preps, nil)
請(qǐng)求取消
當(dāng)請(qǐng)求提交后嵌施,還可以提交取消請(qǐng)求的請(qǐng)求饲化,這樣如果請(qǐng)求還沒有執(zhí)行或者請(qǐng)求的操作可以被中斷(比如 socket IO),那么就可以被異步的取消艰管,而對(duì)于已經(jīng)啟動(dòng)的磁盤IO請(qǐng)求則無法取消
在 iouring-go 中滓侍,提交請(qǐng)求后會(huì)返回一個(gè) iouring.Request
對(duì)象,通過request.Cancel
方法就可以取消請(qǐng)求
request, err := iour.SubmitRequest(iouring.Timeout(1 * time.Second), nil)
cancelRequest, err := request.Cancel()
Cancel
方法會(huì)返回一個(gè) cancelRequest 對(duì)象牲芋,表示提交的取消請(qǐng)求
可以監(jiān)聽 request
的執(zhí)行是否失敗撩笆,并且失敗原因是否為 iouring.ErrRequestCanceled
<- request.Done()
if err := request.Err(); if err != nil {
if err == iouring.ErrRequestCanceled {
fmt.Println("request is canceled")
}
}
也可去監(jiān)聽 cancelRequest 的執(zhí)行結(jié)果,如果cancelRequest.Err
方法返回 nil
缸浦,便是可能成功取消了夕冲,注意是可能取消了,因?yàn)橐恍┎僮魇菬o法被取消的
<- cancelRequest.Done()
if err := cancelRequest.Err(); if err != nil{
if err == iouring.ErrRequestNotFound(){
fmt.Println("canceled request is not found")
}
// do something
}
定時(shí)和請(qǐng)求完成計(jì)數(shù)
io_uring
提供了 IORING_OP_TIMEOUT
請(qǐng)求裂逐,可以用來提交超時(shí)請(qǐng)求
超時(shí)請(qǐng)求可以分為三種:
- 相對(duì)時(shí)間超時(shí)
- 絕對(duì)時(shí)間超時(shí)
- 對(duì)請(qǐng)求完成計(jì)數(shù)歹鱼,到達(dá)指定的完成事件數(shù)量后,超時(shí)請(qǐng)求就會(huì)完成
iouring-go 對(duì)這三種情況封裝了三個(gè)函數(shù) iouring.Timeout
卜高,iouring.TimeoutWithTime
弥姻,iouring.CountCompletionEvent
來分別代表三種超時(shí)請(qǐng)求
now := time.Now()
request, err := iouring.SubmitRequest(iouring.Timeout(2 * time.Second), nil)
if err != nil {
panic(err)
}
<- request.Done()
fmt.Println(time.Now().Sub(now))
根據(jù) io_uring
提供的超時(shí)請(qǐng)求南片,可以實(shí)現(xiàn)系統(tǒng)級(jí)的異步定時(shí)器
請(qǐng)求超時(shí)
io_uring
通過 IOSQE_IO_LINK
將一個(gè)請(qǐng)求和 IORING_OP_LINK_TIMEOUT
請(qǐng)求鏈接在一起,那么就可以做到請(qǐng)求的超時(shí)控制
iouring-go 同樣提供了簡(jiǎn)便方法 WithTimeout
preps := iouring.Read(fd, buf).WithTimeout()
WithTimeout
方法會(huì)返回兩個(gè) PrepRequest
對(duì)象庭敦,所以需要使用 SubmitRequests
來提交
iouring-go 中請(qǐng)求超時(shí)的一些操作使用起來感覺還不是特別友好疼进,有待優(yōu)化
注冊(cè)文件
io_uring
的一些 IO 操作需要提供文件描述符,而頻繁的將文件和內(nèi)核之間進(jìn)行映射也會(huì)導(dǎo)致一定的性能損耗秧廉,所以可以使用 io_uring
的 io_uring_register
接口來提前注冊(cè)文件描述符
詳細(xì)的概念可以參考 io_uring_register
iouring-go 也提供了文件描述符的注冊(cè)功能伞广,而且對(duì)于已經(jīng)注冊(cè)的文件描述符會(huì)自動(dòng)使用
func (iour *IOURing) RegisterFile(file *os.File) error
func (iour *IOURing) RegisterFiles(files []*os.File) error
func (iour *IOURing) UnregisterFile(file *os.File) error
func (iour *IOURing) UnregisterFiles(files []*os.File) error
當(dāng) io_uring
的文件描述符被關(guān)閉后,這些注冊(cè)的文件會(huì)自動(dòng)注銷
需要注意疼电,調(diào)用 io_uring_register
來注冊(cè)文件描述符時(shí)嚼锄,如果有其他的正在進(jìn)行的請(qǐng)求的話,會(huì)等到這些請(qǐng)求都完成才會(huì)注冊(cè)
注冊(cè)文件描述符在 Go 中帶來的并發(fā)問題
type fileRegister struct {
lock sync.Mutex
iouringFd int
fds []int32
sparseindexs map[int]int
registered bool
indexs sync.Map
}
需要注意由于存在對(duì)索引 fileRegister.indexs
的并發(fā)讀寫蔽豺,所以使用 sync.Map
区丑,也就意味著,使用注冊(cè)文件描述符茫虽,會(huì)帶來一定的并發(fā)問題刊苍,經(jīng)過簡(jiǎn)單的測(cè)試,sync.Map
帶來的性能損耗導(dǎo)致注冊(cè)文件描述符帶來的優(yōu)勢(shì)并沒有那么大的
在 Go 中使用
io_uring
的最大問題便是對(duì)io_uring
實(shí)例的競(jìng)爭(zhēng)問題濒析,而通過 Go 暴露給外部使用的并發(fā)機(jī)制,并不能讓io_uring
帶來的異步 IO 發(fā)揮最大的性能將
io_uring
融入 runtime 中啥纸,才是最終的解決方案
注冊(cè)緩沖區(qū)
和注冊(cè)文件描述符類似号杏, io_uring
為了減少 IO 請(qǐng)求中緩沖區(qū)的映射,同樣可以使用 io_uring_register
來注冊(cè)緩沖區(qū)
如果要在請(qǐng)求中使用緩沖區(qū)的話斯棒,需要使用 IORING_OP_READ_FIXED
或者 IORING_OP_WRITE_FIXED
請(qǐng)求
具體可以參考 io_uring_register
內(nèi)核側(cè)請(qǐng)求隊(duì)列輪詢
將請(qǐng)求放到SQ
的環(huán)形緩沖區(qū)后盾致,需要調(diào)用 io_uring_enter
來通知內(nèi)核有請(qǐng)求需要處理
io_uring 為了進(jìn)一步減少系統(tǒng)調(diào)用,可以在 io_uring_setup
是設(shè)置 io_uring_params->flags
的 IORING_SETUP_SQPOLL
flags荣暮,內(nèi)核就會(huì)創(chuàng)建一個(gè)輪詢請(qǐng)求隊(duì)列的線程
可以通過 ps
命令查看用來輪詢的內(nèi)核線程
ps --ppid 2 | grep io_uring-sq
需要注意在 5.10 之前的版本庭惜,需要使用特權(quán)用戶來執(zhí)行,而 5.10 以后只需 CAP_SYS_NICE
權(quán)限即可
并且 5.10 之前穗酥,SQPoll 需要配合注冊(cè)的文件描述符一起使用护赊,而 5.10 以后則不需要,可以通過查看內(nèi)核填充的 io_uring_params->features
是否設(shè)置了 IORING_FEAT_SQPOLL_NONFIXED
// iouring-go/iouring.go
func (iour *IOURing) doRequest(sqe *iouring_syscall.SubmissionQueueEntry, request PrepRequest, ch chan<- Result) (*UserData, error) {
// ...
if sqe.Fd() >= 0 {
if index, ok := iour.fileRegister.GetFileIndex(int32(sqe.Fd())); ok {
sqe.SetFdIndex(int32(index))
} else if iour.Flags&iouring_syscall.IORING_SETUP_SQPOLL != 0 &&
iour.Features&iouring_syscall.IORING_FEAT_SQPOLL_NONFIXED == 0 {
return nil, ErrUnregisteredFile
}
}
// ...
}
iouring-go 同樣提供了開啟 SQPoll 的 WithSQPoll
以及設(shè)置與 SQPoll 內(nèi)核線程的相關(guān)配置 WithSQPollThreadCpu
和 WithSQPollThreadIdle
iour, err := iouring.New(8, iouring.WithSQPoll())
但是在 Go 簡(jiǎn)單的設(shè)置 io_uring_params
并不能正常的工作砾跃,可能是由于 Go 的 GMP 模型導(dǎo)致的一些問題骏啰。暫時(shí)還在思考解決方案
注冊(cè) eventfd,利用 epoll
通過 io_uring_register
可以將 eventfd
注冊(cè)到 io_uring 實(shí)例中抽高,然后將 eventfd
加入到 epoll
中判耕,如果當(dāng) io_uring 中有完成事件時(shí),便會(huì)通知 eventfd
在 iouring-go 中翘骂,對(duì)于完成事件的監(jiān)聽便是使用了 eventfd
和 epoll
type IOURing struct {
eventfd int
cqeSign chan struct{}
// ...
}
func New() (*IOURing, error) {
// ....
if err := iour.registerEventfd(); err != nil {
return nil, err
}
if err := registerIOURing(iour); err != nil {
return nil, err
}
// ...
}
func (iour *IOURing) registerEventfd() error {
eventfd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.FD_CLOEXEC)
if err != nil {
return os.NewSyscallError("eventfd", err)
}
iour.eventfd = eventfd
return iouring_syscall.IOURingRegister(
iour.fd,
iouring_syscall.IOURING_REGISTER_EVENTFD,
unsafe.Pointer(&iour.eventfd), 1,
)
}
func registerIOURing(iour *IOURing) error {
if err := initpoller(); err != nil {
return err
}
if err := unix.EpollCtl(poller.fd, unix.EPOLL_CTL_ADD, iour.eventfd,
&unix.EpollEvent{Fd: int32(iour.eventfd), Events: unix.EPOLLIN | unix.EPOLLET},
); err != nil {
return os.NewSyscallError("epoll_ctl_add", err)
}
poller.Lock()
poller.iours[iour.eventfd] = iour
poller.Unlock()
return nil
}
poller
會(huì)調(diào)用 EpollWait 等待完成隊(duì)列
中有完成事件壁熄,并通知相應(yīng)的 IOURing 對(duì)象
// iouring-go/iouring.go
func (iour *IOURing) getCQEvent(wait bool) (cqe *iouring_syscall.CompletionQueueEvent, err error) {
var tryPeeks int
for {
if cqe = iour.cq.peek(); cqe != nil {
iour.cq.advance(1)
return
}
if !wait && !iour.sq.cqOverflow() {
err = syscall.EAGAIN
return
}
if iour.sq.cqOverflow() {
_, err = iouring_syscall.IOURingEnter(iour.fd, 0, 0, iouring_syscall.IORING_ENTER_FLAGS_GETEVENTS, nil)
if err != nil {
return
}
continue
}
if tryPeeks++; tryPeeks < 3 {
runtime.Gosched()
continue
}
select {
case <-iour.cqeSign:
case <-iour.closer:
return nil, ErrIOURingClosed
}
}
}
// iouring-go/poller.go
func (poller *iourPoller) run() {
for {
n, err := unix.EpollWait(poller.fd, poller.events, -1)
if err != nil {
continue
}
for i := 0; i < n; i++ {
fd := int(poller.events[i].Fd)
poller.Lock()
iour, ok := poller.iours[fd]
poller.Unlock()
if !ok {
continue
}
select {
case iour.cqeSign <- struct{}{}:
default:
}
}
poller.adjust()
}
}
保證數(shù)據(jù)不丟失
默認(rèn)情況下帚豪, CQ 環(huán)的大小是 SQ 環(huán)的 兩倍,為什么 SQ 環(huán)的大小會(huì)小于 CQ 環(huán)草丧,是因?yàn)?SQ 環(huán)中的 sqe 一旦被內(nèi)核發(fā)現(xiàn)志鞍,便會(huì)被內(nèi)核消耗掉,也就意味著 sqe 的生命周期很短方仿,而請(qǐng)求的完成事件都會(huì)放到 CQ 環(huán)中
我們也可以通過 IORING_SETUP_CQSIZE
或者 iouring-go
的 WithCQSize
Option 里設(shè)置 CQ 環(huán)的大小
但是依然會(huì)存在 CQ 環(huán)溢出的情況固棚,而內(nèi)核會(huì)在內(nèi)部存儲(chǔ)溢出的時(shí)間,直到 CQ 環(huán)有空間容納更多事件仙蚜。
可以通過 io_uring_params->features 是否設(shè)置 IORING_FEAT_NODROP 來判斷當(dāng)前內(nèi)核是否支持該功能
如果 CQ 環(huán)溢出此洲,那么提交請(qǐng)求時(shí)可能會(huì)以 -EBUSY
錯(cuò)誤失敗,需要重新提交
并且當(dāng) CQ 環(huán)中數(shù)據(jù)被消耗后委粉,需要調(diào)用 io_uring_enter
來通知內(nèi)核 CQ 環(huán)中有空余空間
func (iour *IOURing) getCQEvent(wait bool) (cqe *iouring_syscall.CompletionQueueEvent, err error) {
// ...
if iour.sq.cqOverflow() {
_, err := iour.syscall.IOURingEnter(iour.fd, 0, 0, iouring_syscall.IORING_ENTER_FLAGS_GETEVENTS, nil)
if err != nil{
return
}
continue
}
// ...
}
io_uring 與 Go —— iouring-go
競(jìng)爭(zhēng)問題
在實(shí)現(xiàn) iouring-go 中遇到的問題呜师,一個(gè)是并發(fā)導(dǎo)致對(duì) io_uring
的競(jìng)爭(zhēng)問題
對(duì)于 CQ 環(huán)的競(jìng)爭(zhēng)是使用單一的 CQ 環(huán)消費(fèi) goroutine IOURing.run()
來完成 cqe
的消費(fèi)
func New(entries int, opts ...IOURingOption) (iour *IOURing, err error) {
iour := &IOURing{...}
// ...
go iour.run()
return
}
func (iour *IOURing) run() {
for {
cqe, err := iour.getCQEvent(true)
// ...
}
}
SQ 環(huán)的解決方案有兩種
- 使用單獨(dú)的提交 goroutine,將需要提交的請(qǐng)求通過內(nèi)部 channel 發(fā)送給提交 goroutine贾节,這樣保證了 SQ 環(huán)的單一生產(chǎn)者
- 使用鎖的方式汁汗,對(duì)于提交請(qǐng)求的函數(shù)加鎖,保證同一時(shí)間只有一個(gè) goroutine 在提交請(qǐng)求
第一種方式聽起來使用 channel 更優(yōu)雅一些栗涂,但是 channel 內(nèi)部依然使用鎖的方式以及額外的內(nèi)存復(fù)制
另外最大的弊端就是將 IOURIng
的提交函數(shù)
(將請(qǐng)求發(fā)送給提交channel)和真正將請(qǐng)求提交給內(nèi)核(調(diào)用 io_uring_enter
通知內(nèi)核有新的請(qǐng)求)分開
當(dāng)多個(gè)提交函數(shù)
向 channel 發(fā)送的請(qǐng)求的順序無法保證知牌,這樣鏈?zhǔn)秸?qǐng)求就無法實(shí)現(xiàn)(除非對(duì)于鏈?zhǔn)秸?qǐng)求再次加鎖)
第二種方式,采用加鎖的方式斤程,保證了同一時(shí)間只有一個(gè)提交函數(shù)在處理 SQ 環(huán)角寸,并且可以立即是否真正提交成功(調(diào)用 IOURing.submit
方法通知內(nèi)核有新的請(qǐng)求)
iouring-go 采用了第二種方式
真正去解決這個(gè)問題的方式,估計(jì)可能只有 runtime 才能給出答案忿墅,為每一個(gè) P 創(chuàng)建一個(gè) io_uring 實(shí)例在 runtime 內(nèi)部解決競(jìng)爭(zhēng)問題扁藕,內(nèi)部使用 eventfd 注冊(cè)到 netpoll
中來獲取完成隊(duì)列
通知
io_uring 與 channel
對(duì)于 iouring-go 設(shè)計(jì)比較好的地方,我感覺便是對(duì) channel 的利用疚脐,異步 IO 加上 channel亿柑,可以將異步在并發(fā)的程序中發(fā)揮出最大的作用
當(dāng)然,如果只是簡(jiǎn)單的使用 channel 的話又會(huì)引入其他一些問題棍弄,后續(xù)會(huì)進(jìn)行說明
func (iour *IOURing) SubmitRequest(request PrepRequest, ch chan<- Result) (Request, error)
SubmitRequest
方法接收一個(gè) channel望薄,當(dāng)請(qǐng)求完成后,會(huì)將結(jié)果發(fā)送到 channel 中照卦,這樣通過多個(gè)請(qǐng)求復(fù)用同一個(gè) channel式矫,程序便可以監(jiān)聽一組請(qǐng)求的完成情況
func (iour *IOURing) run() {
for {
cqe, err := iour.getCQEvent(true)
// ...
userData := iour.userData[cqe.UserData]
// ...
userData.request.complate(cqe)
if userData.resulter != nil {
userData.resulter <- userData.request
}
}
}
而 SubmitRequest
方法同樣會(huì)返回一個(gè) Request 接口對(duì)象,通過 Request 我們同樣可以去查看請(qǐng)求的是否完成已經(jīng)它的完成結(jié)果
type Request interface {
Result
Cancel() (Request, error)
Done() <-chan struct{}
GetRes() (int, error)
// Can Only be used in ResultResolver
SetResult(r0, r1 interface{}, err error) error
}
type Result interface {
Fd() int
Opcode() uint8
GetRequestBuffer() (b0, b1 []byte)
GetRequestBuffers() [][]byte
GetRequestInfo() interface{}
FreeRequestBuffer()
Err() error
ReturnValue0() interface{}
ReturnValue1() interface{}
ReturnFd() (int, error)
ReturnInt() (int, error)
}
利用 channel 便可以完成對(duì)異步 IO 的異步監(jiān)聽和同步監(jiān)聽
channel 帶來的問題
當(dāng)然使用 channel 又會(huì)帶來其他的問題役耕,比如 channel 滿了以后采转,對(duì) io_uring 完成隊(duì)列的消費(fèi)便會(huì)阻塞在向 channel 發(fā)送數(shù)據(jù),阻塞時(shí)間過長(zhǎng)也會(huì)導(dǎo)致 CQ 環(huán)溢出
比較好的解決方案是,在 channel 上抽象出一層 Resulter
故慈,Resulter
會(huì)對(duì)完成事件進(jìn)行自動(dòng)緩沖板熊,當(dāng)然這也會(huì)帶來一定的代碼復(fù)雜度,所以 iouring-go 便將 channel 阻塞的問題交給使用者察绷,要求 channel 的消費(fèi)端盡快消費(fèi)掉數(shù)據(jù)
思考 io_uring 在 Go 中的發(fā)展
netpoll
在 Linux 平臺(tái)下使用了 epoll
干签,而且 epoll
在使用上并沒有競(jìng)爭(zhēng)問題,當(dāng)然如果要使用 io_uring
來替代 epoll
來實(shí)現(xiàn) netpoll
的話并不是不可能拆撼,只是這樣對(duì)于工作很好的 epoll 來說并沒有什么必要容劳,而且是否能夠帶來可觀的性能收益也都是不確定的
在高并發(fā)的情況下,有限的 SQ 環(huán)和 CQ 環(huán)闸度,對(duì)于請(qǐng)求數(shù)量大于完成事件的消費(fèi)速度的情況竭贩,CQ 環(huán)的大量溢出帶來對(duì)內(nèi)核的壓力以及新的請(qǐng)求提交帶來的錯(cuò)誤處理,都會(huì)提高真正利用 io_uring
的難度
對(duì)于 SQ 環(huán)和 CQ 環(huán)的大小限制莺禁,也許需要通過 Pool 的方式來解決留量,初始化多個(gè) io_uring 實(shí)例,當(dāng)一個(gè)實(shí)例的 SQ 環(huán)滿哟冬,那么就使用另外的實(shí)例來提交請(qǐng)求
而使用 Pool 又會(huì)增加一定的復(fù)雜度
io_uring
的功能實(shí)際可以覆蓋了 epoll
的楼熄,比如提交的阻塞 IO 請(qǐng)求便相當(dāng)于 epoll
+ syscall
,另外 io_uring
還提供了超時(shí)設(shè)置和請(qǐng)求的超時(shí)控制浩峡,相當(dāng)于實(shí)現(xiàn)了系統(tǒng)級(jí)的定時(shí)器以及 netpoll
的 deadline
但是 epoll 自身的優(yōu)勢(shì)可岂,比如沒有競(jìng)爭(zhēng)問題,沒有監(jiān)聽文件描述符的數(shù)量限制红符,都讓 epoll 在實(shí)際的使用中更加好用青柄,而這些問題對(duì)于 io_uring
在本身設(shè)計(jì)上就會(huì)導(dǎo)致的問題
比如競(jìng)爭(zhēng)問題,使用環(huán)形緩沖區(qū)可以協(xié)調(diào)應(yīng)用和內(nèi)核對(duì)請(qǐng)求隊(duì)列的訪問预侯,但是應(yīng)用中多個(gè)線程或者 goroutine 就會(huì)引發(fā)對(duì)環(huán)形緩沖區(qū)的競(jìng)爭(zhēng)問題
而請(qǐng)求數(shù)量的限制,那么就需要考慮到請(qǐng)求完成事件的溢出問題峰锁,內(nèi)核不能無限制的去保存溢出的完成事件萎馅,當(dāng)然這個(gè)問題通過應(yīng)用中在 io_uring
實(shí)例上抽象出 io_uring 池
的方式來解決
使用 io_uring
來實(shí)現(xiàn)異步網(wǎng)絡(luò)框架,對(duì)已有的網(wǎng)絡(luò)模型會(huì)是非常大的沖擊虹蒋,怎么去使用 io_uring
來發(fā)揮最大的能力依然處于探索階段糜芳,畢竟 io_uring
是一個(gè)出現(xiàn)才 1 年的技術(shù)
而對(duì)于普通的磁盤 IO 來說,io_uring
還是有很大的發(fā)揮空間的魄衅,利用 Go 中已有的并發(fā)機(jī)制峭竣,結(jié)合具體的性能評(píng)估,對(duì)于文件服務(wù)器來說晃虫,也許會(huì)帶來極大的提升
另外一個(gè)問題便是皆撩,對(duì)于 5.1 引入,5.6 開始功能變得豐富成熟的 io_uring
來說,現(xiàn)在大量的環(huán)境處于 3.X扛吞,4.X呻惕,甚至 2.X , io_uring
仍然需要等待時(shí)機(jī)才能去發(fā)揮它真正的作用滥比,而這段時(shí)間便是留給我們?nèi)ヌ接懺趺醋? io_uring
更好用
推薦閱讀
徹底學(xué)會(huì)使用 epoll 系列
曹春暉:談一談 Go 和 Syscall
io_uring
【譯】高性能異步 IO —— io_uring (Effecient IO with io_uring)
Efficient IO with io_uring
What’s new with io_uring
io_uring 在 LWN 中的討論
io_uring 在內(nèi)核中的提交記錄
Lord of the io_uring
liburing