對於塊裝置而言,linux可以使用同步IO、POSIX IO、linux AIO、io-uring,前倆者是linux的同步IO介面,後者是linux核心提供的非同步io介面,linux AIO只支援直接IO,未來趨勢是io-uring。網路IO多用select/epoll,將其封裝使用起來像非同步IO,同步與非同步區別在於是否堵塞執行緒,磁碟效能,同步非同步IO都可以壓榨完,對於同步非同步的選擇應該看每種IO方式的IO鏈路,以及對資料的拷貝次數,並結合自己場景和需求去分析,可接受應該在應用層改變。
Lunix AIO是否不成熟
glibc 的AIO,採用的是POSIX介面,無論有無bug、除錯難度,API中無connect、accept、send、recv,存在的都是檔案IO的使用者,資料庫開發者,MySQL 5.6 innodb在lunix下已經使用native AIO實現了,innodb-use-native-aio變數預設開啟。
以nginx 為例,說明nginx僅支援在讀取檔案時使用AIO,寫入檔案時往往是寫入記憶體就立刻返回,AIO不支援快取操作,即使需要操作的檔案在linux檔案快取中存在,也不會通過操作快取中的檔案塊來代替實際對磁碟的操作,會降低實際處理能力的效能。
- 僅支援direct IO 只能使用O-DIRECT,不能借助檔案快取來快取當前的IO請求,還存在size對齊
- 仍然可能被阻塞 在系統層上events條件不成立,會進入睡眠
- 拷貝開銷大 大量小io場景下 拷貝影響比較大
- API 不友好 submit wait-for-completion
- 系統呼叫開銷大
io-uring
- 易用,對其中常用的功能進行了一次封裝,提供了簡單易用的介面liburing
- 可擴充套件,支援網路I/O等非塊裝置
- 高效,減少了每次排程要傳的引數大小,減少系統呼叫次數,通過一次系統呼叫提交多個IO請求的方式
- 可伸縮,使用者態和核心態都支援輪詢,可以在不呼叫syscall的情況下直接處理IO請求
原理與結構
- 原理是讓使用者態程式與核心通過一個共享記憶體的無鎖環形佇列進行高效互動
- 共享記憶體,減少系統呼叫過程中的引數記憶體拷貝,將核心態地址空間對映到使用者態的方式,通過使用者態對io-uring fd進行mmap,可以獲得io-uring相關的兩個核心佇列(IO請求,IO完成時間)的使用者態地址,使用者態程式可以直接操作倆個佇列向核心傳送IO請求,接收完核心完成IO事件通知,
- 無鎖環形佇列,單生產者與單消費者的無鎖佇列,來實現使用者態程式與核心對共享記憶體的高效併發訪問,生產者只修改隊尾指標,消費者只修改隊頭指標,不會相互阻塞。
- 記憶體屏障與保序,保證記憶體操作順序和一致性,1 修改佇列狀態時,保證對佇列元素的寫入已完成,編譯器可以實現,防止編譯器將修改佇列的指令放到佇列元素寫入完成之前,2 讀取佇列狀態時,需要獲取最新寫入和修改的值,保證快取一致性重新整理
輪詢模式
- io-uring 提供io-uring-enter系統呼叫介面,用於通知核心IO請求的產生以及等待核心完成的請求,仍然需要反覆呼叫系統呼叫,進行上下文切換。ioring-setup- iopoll 和 ioring-setup-sqpoll 同時設定,核心執行緒會同時對io-uring的佇列和裝置驅動佇列做輪詢,對請求佇列、完成事件佇列、裝置驅動佇列全部使用輪詢模式,達到最優的IO效能,會產生更多的CPU開銷。
呼叫介面
- io-uring-setup 建立介面
- io-uring-enter 通知核心有IO請求待處理,並根據引數等待請求完成
- io-uring-register 註冊fd和buffer為常用物件,避免核心反覆拷貝
具體實現
- 讓使用者態程式與核心共享記憶體,併發修改同一資料結構,是一種危險行為,使用者態異常操作核心處理邏輯,可能讓使用者態程式破壞核心機制
- uring 的head/tail 指標錯誤,會導致核心處理沒有設定過的sqe,sqe是核心預分配過的記憶體,不會造成核心訪問非法記憶體地址
- uring entries被錯誤修改,可能會造成核心異常,在建立io-uring時就已經確定,可以為每個io-uring單獨儲存一份用於實際邏輯處理邏輯,而不使用共享記憶體中的部分。
- io-uring提供了複雜而強大的非同步IO介面,又實現了liburing來遮蔽高階特性帶來的複雜度,通過共享記憶體與無鎖佇列與核心進行高效能互動,而避免大量的syscall嗲來的效能開銷和限制,可用於加速實時性要求不高的系統呼叫。