Go 與異步 IO - io_uring 的思考

本來準(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.OpennewFile谣殊,可以看到文件的 文件描述符 被放到 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_offsetsio_cqring_offsets

配置 io_uring

flags 以位掩碼的方式必怜,結(jié)合相應(yīng) sq_thread_cpu肉拓,sq_thread_idlewq_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_offsetsio_cqring_offsets 便是SQCQ在共享內(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->flagsIOSQE_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->flagsIOSQE_IO_LINK 標(biāo)記后,下一個(gè) sqe 和當(dāng)前 sqe 自動(dòng)組成新鏈或者當(dāng)前 sqe 的鏈中祟偷,鏈中沒有設(shè)置 IOSQWE_IO_LINKsqe 便是鏈尾

如果鏈中的有請(qǐng)求執(zhí)行失敗了察滑,那么鏈中后續(xù)的 sqe 都會(huì)被取消( cqe.res-ECANCELED)
io_uring 還提供了以外一種設(shè)置鏈?zhǔn)秸?qǐng)求的方式,設(shè)置 sqe->flagsIOSQE_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_uringio_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->flagsIORING_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)配置 WithSQPollThreadCpuWithSQPollThreadIdle

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)聽便是使用了 eventfdepoll

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-goWithCQSize 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)的解決方案有兩種

  1. 使用單獨(dú)的提交 goroutine,將需要提交的請(qǐng)求通過內(nèi)部 channel 發(fā)送給提交 goroutine贾节,這樣保證了 SQ 環(huán)的單一生產(chǎn)者
  2. 使用鎖的方式汁汗,對(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末亚脆,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子盲泛,更是在濱河造成了極大的恐慌濒持,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,029評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寺滚,死亡現(xiàn)場(chǎng)離奇詭異柑营,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)玛迄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,395評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門由境,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蓖议,你說我怎么就攤上這事虏杰。” “怎么了勒虾?”我有些...
    開封第一講書人閱讀 157,570評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵纺阔,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我修然,道長(zhǎng)笛钝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,535評(píng)論 1 284
  • 正文 為了忘掉前任愕宋,我火速辦了婚禮玻靡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘中贝。我一直安慰自己囤捻,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,650評(píng)論 6 386
  • 文/花漫 我一把揭開白布邻寿。 她就那樣靜靜地躺著蝎土,像睡著了一般。 火紅的嫁衣襯著肌膚如雪绣否。 梳的紋絲不亂的頭發(fā)上誊涯,一...
    開封第一講書人閱讀 49,850評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音蒜撮,去河邊找鬼暴构。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的丹壕。 我是一名探鬼主播庆械,決...
    沈念sama閱讀 39,006評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼菌赖!你這毒婦竟也來了缭乘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,747評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤琉用,失蹤者是張志新(化名)和其女友劉穎堕绩,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體邑时,經(jīng)...
    沈念sama閱讀 44,207評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奴紧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,536評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了晶丘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片黍氮。...
    茶點(diǎn)故事閱讀 38,683評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖浅浮,靈堂內(nèi)的尸體忽然破棺而出沫浆,到底是詐尸還是另有隱情,我是刑警寧澤滚秩,帶...
    沈念sama閱讀 34,342評(píng)論 4 330
  • 正文 年R本政府宣布专执,位于F島的核電站,受9級(jí)特大地震影響郁油,放射性物質(zhì)發(fā)生泄漏本股。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,964評(píng)論 3 315
  • 文/蒙蒙 一桐腌、第九天 我趴在偏房一處隱蔽的房頂上張望拄显。 院中可真熱鬧,春花似錦案站、人聲如沸凿叠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,772評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蹬碧,卻和暖如春舱禽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背恩沽。 一陣腳步聲響...
    開封第一講書人閱讀 32,004評(píng)論 1 266
  • 我被黑心中介騙來泰國打工誊稚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,401評(píng)論 2 360
  • 正文 我出身青樓里伯,卻偏偏與公主長(zhǎng)得像城瞎,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子疾瓮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,566評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容