转载|网络编程中阻塞式函数的底层逻辑
逛知乎看到的,覺得寫的挺透徹的,轉載一下,原文鏈接:Unix網絡編程里的阻塞是在操作系統的內核態創建一個線程來死循環嗎?
原文以阻塞式的recv函數作為講解,但是所有阻塞式的api底層邏輯基本相通。
下面是正文:
作者:張彥飛
鏈接:https://www.zhihu.com/question/492983429/answer/2236327954
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
大家天天都在說阻塞,實際上95%的程序員并沒有真正理解阻塞是啥。這里并沒有循環的事情,我們來從內核視角詳細剖析一下阻塞到底是啥,它是如何工作的。把問題再具體一下,recv 接收數據阻塞的原理是啥? 理解了這個就能真正理解所有的阻塞了。用一段大家都熟悉的代碼來舉例!
int main() {int sk = socket(AF_INET, SOCK_STREAM, 0);connect(sk, ...)recv(sk, ...) }在上面的 demo 中雖然只是簡單的兩三行代碼,但實際上用戶進程和內核配合做了非常多的工作。大致的工作流程如下:
看到這里,你可能還沒看著阻塞的原理。別著急,往下看。我們來看 recv 函數依賴的底層實現。首先通過 strace 命令跟蹤,可以看到 clib 庫函數 recv 會執行到 recvfrom 系統調用。進入系統調用后,用戶進程就進入到了內核態,通過執行一系列的內核協議層函數,然后到 socket 對象的接收隊列中查看是否有數據,沒有的話就把自己添加到 socket 對應的等待隊列里。最后讓出CPU,操作系統會選擇下一個就緒狀態的進程來執行。
整個流程圖如下:
以上這個流程圖是我根據 Linux 內核源碼的執行過程總結后畫出來的。注意上面的第四步和第五步。第四步中是在訪問 sock 對象下面的接收隊列,如果接收隊列中還沒有數據到達,那么就會進入第五步,把當前進程阻塞掉。但是在把自己阻塞掉之前,進程干了一件事, 給 socket 上留了個標記。告訴內核,如果這個 socket 上數據好了,記得叫我起來哈!就是源碼 prepare_to_wait 函數中的 __add_wait_queue 這一句。
接下來 Linux 就會選擇下一個就緒狀態的進程來執行。這就是阻塞原理的上半段,就是進程修改自己的狀態,主動交出 CPU 的執行權。當有數據到達的時候,內核首先將數據包放到該 socket 的接收隊列中。然后掃描一下 socket 等待隊列,然后發現:“呦呵,有進程阻塞在這個 socket 上面哎,好喚醒它”。
具體到代碼里就是 __wake_up_common 這個函數會訪問 socket 的等待隊列。
在 __wake_up_common 中找出一個等待隊列項 curr,然后調用其回調函數 curr->func,來完成進程的喚醒。不過,要注意的是,這個喚醒只是把相應的進程放到可運行隊列里而已。真正的執行還得等其它進程主動釋放 CPU 或者是時間片到了之后,內核把其它進程拿下以后才能真正獲得 CPU 并開始執行。
參考:圖解 | 深入理解高性能網絡開發路上的絆腳石 - 同步阻塞網絡 IO說到這里,你可能還會問了。內核是如何接收包的,畢竟喚醒用戶進程是它干的。難道它不是一個死循環么?是的,并不是。 網卡上收到數據包的時候,是通過硬中斷喚醒內核進程處理,硬中斷會觸發軟中斷。有了軟中斷請求以后,ksoftirqd 內核線程才開始執行。來從網卡上取包,處理,放到接收隊列,然后喚醒用戶進程。
參見:圖解Linux網絡包接收過程
究其根源,是由網卡的硬中斷來觸發的。如果一段時間內沒有網絡包處理,那么沒有死循環來消耗 CPU 的。對網絡底層還有啥不理解的,來看看我的公眾號「開發內功修煉」 或許可以幫你解開一些困惑。
Github: GitHub - yanfeizhang/coder-kung-fu: 開發內功修煉
哦對了,想理解多路復用,來看看我的這一篇吧,也是從源碼角度深入分析的。圖解 | 深入揭秘 epoll 是如何實現 IO 多路復用的!
總結
以上是生活随笔為你收集整理的转载|网络编程中阻塞式函数的底层逻辑的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 颐和园关门时间清客时间
- 下一篇: DNF现在更新的那个活动武器怎么得的?