1 回答

TA貢獻1818條經驗 獲得超7個贊
我無法完全解析您所寫的內容。
我會嘗試做一個純粹的猜測并想象你可能正在監督這樣一個事實,即進入非阻塞模式的套接字上的write(2)
andread(2)
系統調用(以及它們的同類調用,例如send(2)
and )可以自由使用(并分別返回)比請求的數據少。 換句話說,一個非阻塞套接字上的調用被告知寫入 1 兆字節的數據將消耗當前適合相關內核緩沖區的數據并立即返回,表明它只消耗了同樣多的數據。下一次立即調用可能會返回。recv(2)
write(2)
write(2)
EWOULDBLOCK
調用也是如此read(2)
:如果你傳遞一個足夠大的緩沖區來保存 1 兆字節的數據,并告訴它讀取該字節數,調用只會耗盡內核緩沖區的內容并立即返回,表明有多少它實際復制的數據。下一次立即調用read(2)
可能會返回EWOULDBLOCK
。
因此,任何獲取或放入套接字數據的嘗試幾乎都會立即成功:無論是在數據在內核緩沖區和用戶空間之間被鏟起之后,還是馬上——使用EAGAIN
返回碼。
當然,在執行這樣的系統調用的過程中,操作系統線程有可能被掛起,但這不能算作“系統調用中的阻塞”。
更新原始答案以響應 OP 的以下評論:
<...>
這是我在《UNIX 網絡編程》(第 1 卷,第 3 卷)第 6.2 章中看到的內容:同步 I/O 操作會導致請求進程被阻塞,直到該 I/O 操作完成。使用這些定義,前四個 I/O 模型——阻塞、非阻塞、I/O 多路復用和信號驅動 I/O——都是同步的,因為實際的 I/O 操作 (recvfrom) 會阻塞進程。
它使用“塊”來描述非阻塞 I/O 操作。這讓我很困惑。
如果進程實際上沒有被阻塞,我仍然不明白為什么這本書使用“阻塞進程”。
我只能猜測這本書的作者打算強調該過程確實在進入系統調用之后一直被阻塞,直到從它返回。讀取和寫入非阻塞套接字確實會阻塞以在內核和用戶空間之間傳輸數據(如果可用)。我們通俗地說這不會阻塞,因為我們的意思是“它不會阻塞等待并且在不確定的時間內什么都不做”。
這本書的作者可能會將此與所謂的異步 I/O(在 Windows? 上稱為“重疊”)進行對比——在這種情況下,您基本上為內核提供了一個帶有/用于數據的緩沖區,并要求它與您的代碼——從某種意義上說,相關的系統調用立即返回并且 I/O 在后臺執行(關于您的用戶空間代碼)。
據我所知,Go 在它支持的任何一個平臺上都沒有使用內核的異步 I/O 設施。您可以在那里尋找有關 Linux 及其當代io_uring
子系統的發展。
哦,還有一點。這本書可能(至少在敘述的時候)正在討論一種簡化的“經典”方案,其中沒有進程內線程,唯一的并發單元是進程(具有單個執行線程)。在這個方案中,任何系統調用顯然都會阻塞整個過程。相比之下,Go 只在支持線程的內核上工作,因此在 Go 程序中,系統調用永遠不會阻塞整個進程——只會阻塞它被調用的線程。
讓我再次嘗試解釋這個問題 - 我認為 - OP 聲明了它。
服務多個客戶端請求的問題并不新鮮——其中最明顯的第一個陳述是“C10k 問題”。
快速回顧一下,在它管理的套接字上具有阻塞操作的單線程服務器實際上一次只能處理一個客戶端。
為了解決這個問題,有兩種直接的方法:
派生服務器進程的副本以處理每個傳入的客戶端連接。
在支持線程的操作系統上,在同一進程中創建一個新線程來處理每個傳入的客戶端。
它們各有優缺點,但它們在資源使用方面都很糟糕,而且——更重要的是——它們不能很好地適應大多數客戶端在處理方面的 I/O 速率和帶寬相對較低這一事實典型服務器上可用的資源。
換句話說,當與客戶端進行典型的 TCP/IP 交換時,服務線程大部分時間都處于休眠狀態,write(2)
并read(2)
在客戶端套接字上調用。
這就是大多數人在談論套接字上的“阻塞操作”時的意思:如果一個套接字是阻塞的,并且對它的操作將阻塞,直到它可以真正執行,并且源線程將被置于睡眠狀態不確定數量的時間。
另一個需要注意的重要事情是,當套接字準備就緒時,與兩次喚醒之間的睡眠時間相比,完成的工作量通常是微不足道的。當胎面休眠時,它的資源(例如內存)實際上被浪費了,因為它們不能用于做任何其他工作。
輸入“輪詢”。它通過注意到網絡套接字的就緒點相對較少且介于兩者之間來解決浪費資源的問題,因此讓單個線程服務大量此類套接字是有意義的:它允許保持線程幾乎理論上盡可能繁忙,并且還允許在需要時橫向擴展:如果單個線程無法處理數據流,則添加另一個線程,等等。
這種方法確實很酷,但它有一個缺點:必須重寫讀取和寫入數據的代碼以使用回調樣式而不是原始的普通順序樣式。用回調編寫很難:你通常必須實現復雜的緩沖區管理和狀態機來處理這個問題。
Go 運行時通過為其執行流單元增加另一層調度來解決這個問題——goroutines:對于 goroutines,socket 上的操作總是阻塞的,但是當一個 goroutine 即將阻塞在一個 socket 上時,這是透明的,只通過掛起goroutine 本身——直到請求的操作能夠繼續——并使用 goroutine 運行的線程來做其他工作1。
這允許兩種方法中最好的:程序員可以編寫經典的無腦順序無回調網絡代碼,但用于處理網絡請求的線程已被充分利用2。
至于阻塞的原始問題,當套接字上的數據傳輸發生時,goroutine 和它運行的線程確實都被阻塞了,但是由于發生的是內核和用戶空間緩沖區之間的數據鏟斗,所以延遲是大多數時候都很小,與經典的“投票”案例沒有什么不同。
請注意,在 Go(至少直到,包括 Go 1.14)中執行系統調用(包括在不可輪詢描述符上的 I/O)確實會阻塞調用 goroutine 和它運行的線程,但處理方式與可輪詢的不同描述符:當一個特殊的監控線程注意到一個 goroutine 在系統調用中花費了超過一定時間(20 μs,IIRC)時,運行時從下面拉出所謂的“處理器”(在 OS 線程上運行 goroutines 的運行時事物) gorotuine 并嘗試讓它在另一個 OS 線程上運行另一個 goroutine;如果有一個 goroutine 想要運行但沒有空閑的 OS 線程,Go 運行時會創建另一個。
因此,“正?!钡淖枞?I/O 在 Go 中仍然在兩種意義上都是阻塞的:它阻塞了 goroutine 和 OS 線程,但是 Go 調度程序確保程序作為一個整體仍然能夠取得進展。
這可以說是使用內核提供的真正異步 I/O 的完美案例,但目前還沒有。
- 1 回答
- 0 關注
- 155 瀏覽
添加回答
舉報