逛知乎看到的,覺得寫的挺透徹的,轉載一下,原文鏈接: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 這一句。
//file: kernel/wait.c
void
prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{unsigned long flags;wait->flags &= ~WQ_FLAG_EXCLUSIVE;spin_lock_irqsave(&q->lock, flags);if (list_empty(&wait->task_list))__add_wait_queue(q, wait);set_current_state(state);spin_unlock_irqrestore(&q->lock, flags);
}
接下來 Linux 就會選擇下一個就緒狀態的進程來執行。這就是阻塞原理的上半段,就是進程修改自己的狀態,主動交出 CPU 的執行權。當有數據到達的時候,內核首先將數據包放到該 socket 的接收隊列中。然后掃描一下 socket 等待隊列,然后發現:“呦呵,有進程阻塞在這個 socket 上面哎,好喚醒它”。
具體到代碼里就是 __wake_up_common
這個函數會訪問 socket 的等待隊列。
//file: kernel/sched/core.c
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,int nr_exclusive, int wake_flags, void *key)
{wait_queue_t *curr, *next;list_for_each_entry_safe(curr, next, &q->task_list, task_list) {unsigned flags = curr->flags;if (curr->func(curr, mode, wake_flags, key) &&(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)break;}
}
在 __wake_up_common
中找出一個等待隊列項 curr
,然后調用其回調函數 curr->func
,來完成進程的喚醒。不過,要注意的是,這個喚醒只是把相應的進程放到可運行隊列里而已。真正的執行還得等其它進程主動釋放 CPU 或者是時間片到了之后,內核把其它進程拿下以后才能真正獲得 CPU 并開始執行。
參考:圖解 | 深入理解高性能網絡開發路上的絆腳石 - 同步阻塞網絡 IO說到這里,你可能還會問了。內核是如何接收包的,畢竟喚醒用戶進程是它干的。難道它不是一個死循環么?是的,并不是。 網卡上收到數據包的時候,是通過硬中斷喚醒內核進程處理,硬中斷會觸發軟中斷。有了軟中斷請求以后,ksoftirqd 內核線程才開始執行。來從網卡上取包,處理,放到接收隊列,然后喚醒用戶進程。
參見:圖解Linux網絡包接收過程
究其根源,是由網卡的硬中斷來觸發的。如果一段時間內沒有網絡包處理,那么沒有死循環來消耗 CPU 的。對網絡底層還有啥不理解的,來看看我的公眾號「開發內功修煉」 或許可以幫你解開一些困惑。
Github: GitHub - yanfeizhang/coder-kung-fu: 開發內功修煉
哦對了,想理解多路復用,來看看我的這一篇吧,也是從源碼角度深入分析的。圖解 | 深入揭秘 epoll 是如何實現 IO 多路復用的!