關於IO,同步/異步/阻塞/非阻塞,這幾個關鍵詞是經常聽到的,譬如:
“Java oio是阻塞的,nio是非阻塞的”
“NodeJS的IO是異步的”
但是這些東西聽多了就容易迷糊,比方說同步是否就是阻塞,異步是否就是非阻塞呢?
先給出結論:
1. 異步/同步與阻塞/非阻塞之間沒有必然的聯系
2. 同步IO可以是阻塞,也可以是非阻塞的
3. 異步IO就是異步IO,它一定是非阻塞的,不存在異步阻塞IO這個說法
POSIX對同步/異步的定義如下,這兩句話非常關鍵
- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
- An asynchronous I/O operation does not cause the requesting process to be blocked;
再給出權威文檔:《UNIX網絡編程:卷一》的第六章
書中列出了如下五種IO模型:
-
阻塞式I/O;
-
非阻塞式I/O;
-
I/O復用(select,poll,epoll...);
-
信號驅動式I/O(SIGIO);
-
異步I/O(POSIX的aio_系列函數);
1. 阻塞式IO
我們手上有一個socket,現在希望能從這個socket里讀點數據出來,我們會對這個socket調用recvfrom方法
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
在默認情況下,recvfrom方法會被阻塞,直到從指定的socket上收到數據才會返回,返回時,buf中已經填充好了數據
阻塞的過程實際上可以分割成兩段:等待kernel准備好從網絡上接收到的數據報 + 等待收到的報文被從kernel復制到buf中
只有在這兩個過程全部完成后,recvfrom方法才會返回。
這就是阻塞式IO模型

2. 非阻塞式IO
還是上面的recvfrom方法,如果將其設置為非阻塞模式(flag與MSG_DONTWAIT異或),情況就會有所不同了:
在內核沒有准備好數據報時,調用recvfrom方法會立即返回異常碼(EWOULDBLOCK或者EAGAIN)(這一段是非阻塞的!)
如果內核已經准備好數據,調用recvfrom方法則會在數據報被從kernel拷貝到buf中后返回(這一段是同步的!)
也就是說,阻塞與非阻塞式IO的主要區別在於等待數據報准備好的第一階段,至於將數據從kernel拷貝到buf中的過程,兩者都是同步的。

但是個人覺得非阻塞式IO可能並不好用,因為在輪詢一個socket是否可讀的過程會直接占滿一個core
如果想要減少cpu資源占用的話,又會增加編程的復雜度。
3. I/O多路復用
IO多路復用有select/poll/epoll這樣的幾種方式
先介紹一下最有代表性的select方法
int select(int nfds, fd_set *restrict readfds,
fd_set *restrict writefds, fd_set *restrict errorfds,
struct timeval *restrict timeout);
select方法的返回值代表當前可以操作的fd數量,如果返回值大於0,說明已經有fd准備就緒,下一步我們就可以調用recvfrom方法從就緒的fd中讀取數據了(先只考慮可讀的情況)
select方法是否阻塞,與timeout參數有關
如果timeout被設置為0,那么select是非阻塞的,對select方法的調用會立即返回。
如果timeout被設置為非0,則select會阻塞,直到有fd可讀,或者timeout到期為止。
總的來說,I/O多路復用是同步阻塞的,但主要是阻塞在對select/poll/epoll方法的調用上,后續的recvfrom則是同步的。

多說一句,I/O多路復用,實際上跟第一條介紹的阻塞IO差不多
只是I/O多路復用可以同時監聽多個fd罷了
這樣就減少了為每個需要監聽的fd開啟一個線程的開銷。
4. 信號驅動式I/O
沒用過也沒見過,直接上截圖:

5. 異步I/O
同步IO中,在調用recvfrom方法時,即使kernel已經將數據准備好,recvfrom方法也不會立即返回
必須要在耗費一定的時間,將數據從kernel完全拷貝到用戶buf中后,recvfrom方法才會返回
也就是說,在recvfrom方法無異常返回的時候,數據已經在buf中准備好了
異步IO則有相當大的不同:
1. 用戶調用一次請求數據的方法,該方法會無阻塞的立即返回。
2. OS接到這個請求后,會將用戶所請求的數據從kernel拷貝到指定的位置。
3. 數據拷貝完成后,第一步中注冊的回調方法會被調用(或者觸發一個信號,總之就是要讓用戶感知到數據已經拷貝完成)
4. 用戶感知到這一事件,此時數據已經准備好,可以直接處理數據了
如下圖所示

但是目前Linux的aio還不成熟,而且epoll提供的IO多路復用模型在性能上已經夠用了,所以在此就不舉例了
ps. NodeJS在Linux上的異步實現是基於libeio,這是用阻塞IO和線程池模擬出來的異步IO
最后上一張圖作為總結

最后再把文章開頭的兩句話再重復一遍,理解想必會更加深刻
- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
- An asynchronous I/O operation does not cause the requesting process to be blocked;
參考文獻
