1. 程式人生 > 其它 >讀Node入門有感——>【2】構建基礎的HTTP伺服器【續】【番外篇:阻塞非阻塞的理解】

讀Node入門有感——>【2】構建基礎的HTTP伺服器【續】【番外篇:阻塞非阻塞的理解】

阻塞與非阻塞

寫接下來的文章之前讓我們先來理解以下阻塞非阻塞與同步非同步的區別
四個相關概念:

  • 同步(Synchronous)
  • 非同步(Asynchronous)
  • 阻塞(Blocking)
  • 非阻塞(Nonblocking)

    翻譯一下就是:
    程序間的通訊是通過send()和receive()兩種基本操作完成的。具體如何實現這兩種基本操作,存在著不同的設計。訊息的傳遞有可能是阻塞的或非阻塞的-也被稱為同步或非同步的:
  • 阻塞式傳送(blocking send)傳送方程序會被一直阻塞,直到訊息被接受方程序收到。
  • 非阻塞式傳送(nonblocking send)。傳送方程序呼叫send()後,立即就可以其他操作。
  • 阻塞式接收(blocking receive) 接收方呼叫receive()後一直阻塞,知道訊息到達可用。
  • 非阻塞式接受(nonblocking receive)接收方呼叫receive()函式後,要麼得到一個有效的結果,要麼得到一個空值,即不會被阻塞。
    上述不同型別的傳送方式和不同型別的接收方式,可以自由組合。
  • 也就是說,從程序級通訊的維度討論時,阻塞和同步(非阻塞和非同步)就是一對同義詞,且需要針對傳送方接收方做區別對待。

先修知識

使用者空間和核心空間
作業系統為了支援多個應用同時執行,需要保證不同程序之間相對獨立(一個程序的崩潰不會影響其它程序,惡意程序不能直接讀取和修改其它程序執行時的程式碼和資料)。因此作業系統核心需要擁有高於普通程序的許可權

,以此來排程和管理使用者的應用程式。
於是記憶體空間被劃分為兩部分,一部分為核心空間,一部分為使用者空間,核心空間儲存的程式碼和資料具有更高階的許可權。記憶體訪問的相關硬體在程式執行期間會進行訪問控制(Access Control),使得使用者空間的程式不能直接讀寫核心空間的存在。

上圖展示了程序切換中幾個最重要的步驟:
1.當一個程式正在執行的過程中,中斷(interrupt)或系統呼叫(system call)發生可以使得CPU的控制權會從當前程序轉移到作業系統核心。
2.作業系統核心負責儲存程序i在CPU中的上下文(程式計數器,暫存器)到PCBi(作業系統分配給程序的一個記憶體塊)中。
3.從PCBj取出程序j的CPU上下文,將CPU控制權轉移給程序j,開始執行程序j的指令。
幾個底層概念的通俗(不嚴謹)解釋:

中斷(interrupt)
  • CPU微處理器有一箇中斷訊號位,在每個CPU時鐘週期的末尾,CPU會去檢測那個中斷訊號位是否有中斷訊號到達,如果有,則會根據中斷優先順序決定是否要暫停當前執行的指令,轉而去執行處理中斷的指令。(其實就是CPU層級的while輪詢)、
時鐘中斷(Clock Interrupt)
  • 一個硬體時鐘會每隔一段(很短)的時間就產生一箇中斷訊號傳送給CPU,CPU在響應這個中斷時,就回去執行作業系統核心的指令,繼而將CPU的控制權轉移給了作業系統核心,可以由作業系統核心決定下一個要被執行的指令。
系統呼叫(system call)
  • system call是作業系統提供給應用程式的介面。使用者通過呼叫systemcall來完成哪些需要作業系統核心進步的操作,例如硬碟,網路介面裝置的讀寫等。
    從上述描述中,可以看出來,作業系統在進行切換時,需要進行一系列的記憶體讀寫操作,這帶來了一定的開銷:
  • 對於一個執行著UNIX系統的現代PC來說,程序切換通常至少需要花費300us的時間
程序阻塞


上圖展示了一個程序的不同狀態:

  • New程序正在被建立
  • Running程序的指令正在被執行
  • Waiting程序正在等待一些事件的發生(例如I/O的完成或者收到某個訊號)
  • Ready程序在等待被作業系統排程
  • Terminated程序執行完畢(可能是被強行終止的)
    我們所說的“阻塞”是指程序在發起了一個系統呼叫(System Call)後,由於該系統呼叫的操作不能立即完成,需要等待一段時間,於是核心將程序掛起等待(waiting)狀態,以確保它不會被排程執行,佔用CPU資源。
    友情提示:在任意時刻,一個CPU核心上(processor)只可能執行一個程序。
    作者:蕭蕭
    連結:https://www.zhihu.com/question/19732473/answer/241673170
    來源:知乎
    著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

I/O System Call 的阻塞/非阻塞, 同步/非同步這裡再重新審視 阻塞/非阻塞 IO 這個概念, 其實阻塞和非阻塞描述的是程序的一個操作是否會使得程序轉變為“等待”的狀態, 但是為什麼我們總是把它和 IO 連在一起討論呢?原因是, 阻塞這個詞是與系統呼叫 System Call 緊緊聯絡在一起的, 因為要讓一個程序進入 等待(waiting) 的狀態, 要麼是它主動呼叫 wait() 或 sleep() 等掛起自己的操作, 另一種就是它呼叫 System Call, 而 System Call 因為涉及到了 I/O 操作, 不能立即完成, 於是核心就會先將該程序置為等待狀態, 排程其他程序的執行, 等到 它所請求的 I/O 操作完成了以後, 再將其狀態更改回 ready 。作業系統核心在執行 System Call 時, CPU 需要與 IO 裝置完成一系列物理通訊上的互動, 其實再一次會涉及到阻塞和非阻塞的問題, 例如, 作業系統發起了一個讀硬碟的請求後, 其實是向硬碟裝置通過匯流排發出了一個請求,它即可以阻塞式地等待IO 裝置的返回結果,也可以非阻塞式的繼續其他的操作。 在現代計算機中,這些物理通訊操作基本都是非同步完成的, 即發出請求後, 等待 I/O 裝置的中斷訊號後, 再來讀取相應的裝置緩衝區。 但是,大部分作業系統預設為使用者級應用程式提供的都是阻塞式的系統呼叫 (blocking systemcall)介面, 因為阻塞式的呼叫,使得應用級程式碼的編寫更容易(程式碼的執行順序和編寫順序是一致的)。但同樣, 現在的大部分作業系統也會提供非阻塞I/O 系統呼叫介面(Nonblocking I/O system call)。 一個非阻塞呼叫不會掛起呼叫程式, 而是會立即返回一個值, 表示有多少bytes 的資料被成功讀取(或寫入)。非阻塞I/O 系統呼叫( nonblocking system call )的另一個替代品是 非同步I/O系統呼叫 (asychronous system call)。 與非阻塞 I/O 系統呼叫類似,asychronous system call 也是會立即返回, 不會等待 I/O 操作的完成, 應用程式可以繼續執行其他的操作, 等到 I/O 操作完成了以後,作業系統會通知呼叫程序(設定一個使用者空間特殊的變數值 或者 觸發一個 signal 或者 產生一個軟中斷 或者 呼叫應用程式的回撥函式)。此處, 非阻塞I/O 系統呼叫( nonblocking system call ) 和 非同步I/O系統呼叫 (asychronous system call)的區別是:一個非阻塞I/O 系統呼叫 read() 操作立即返回的是任何可以立即拿到的資料, 可以是完整的結果, 也可以是不完整的結果, 還可以是一個空值。而非同步I/O系統呼叫 read()結果必須是完整的, 但是這個操作完成的通知可以延遲到將來的一個時間點。下圖展示了同步I/O 與 非同步 I/O 的區別 (非阻塞 IO 在下圖中沒有繪出). 注意, 上面提到的 非阻塞I/O 系統呼叫( nonblocking system call ) 和 非同步I/O系統呼叫 都是非阻塞式的行為(non-blocking behavior)。 他們的差異僅僅是返回結果的方式和內容不同。非阻塞 I/O 如何幫助伺服器提高吞吐量考慮一個單程序伺服器程式, 收到一個 Socket 連線請求後, 讀取請求中的檔名,然後讀請求的檔名內容,將檔案內容返回給客戶端。 那麼一個請求的處理流程會如下圖所示。R 表示讀操作W 表示寫操作C 表示關閉操作在這個過程中, 我們可以看到, CPU 和 硬碟IO 的資源大部分時間都是閒置的。 此時, 我們會希望在等待 I/O 的過程中繼續處理新的請求。方案一: 多程序每到達一個請求, 我們為這個請求新建立一個程序來處理。 這樣, 一個程序在等待 IO 時, 其他的程序可以被排程執行, 更加充分地利用 CPU 等資源。問題: 每新建立一個程序都會消耗一定的記憶體空間, 且程序切換也會有時間消耗, 高併發時, 大量程序來回切換的時間開銷會變得明顯起來。方案二:多執行緒和多程序方案類似,為每一個請求新建一個執行緒進行處理,這樣做的重要區別是, 所有的執行緒都共享同一個程序空間問題: 需要考慮是否需要為特定的邏輯使用鎖。引申問題: 一個程序中的某一個執行緒發起了 system call 後, 是否造成整個程序的阻塞? 如果會, 那麼多執行緒方案與單程序方案相比就沒有明顯的改善。解決辦法1:核心支援的執行緒(kenerl supported threads) 作業系統核心能夠感知到執行緒, 每一個執行緒都會有一個核心呼叫棧(kenerl stack) 和 儲存CPU 暫存器下文的 table 。在這種方案中, 如果 CPU 是多核的, 不同的執行緒還可以執行在不同的 CPU processor 上。 既實現了IO 併發, 也實現了 CPU 併發。問題: 核心支援執行緒可移植性差, 其實現對於不同的作業系統而言有所差別。解決辦法2: 使用者支援的執行緒(user supported threads) 核心感知不到使用者執行緒, 每一個使用者的程序擁有一個排程器, 該排程器可以感知到執行緒發起的系統呼叫, 當一個執行緒產生系統呼叫時, 不阻塞整個程序, 切換到其他執行緒繼續執行。 當 I/O 呼叫完成以後, 能夠重新喚醒被阻塞的執行緒。實現細節: 應用程式基於執行緒庫 thread libray 編寫執行緒庫中包含 “虛假的” read(), write(), accept()等系統呼叫。執行緒庫中的 read(), write(), accept() 的底層實現為非阻塞系統呼叫(Non-blocking system call), 呼叫後,由於可以立即返回, 則將特定的執行緒狀態標記為 waiting, 排程其他的可執行執行緒。 核心完成了 IO 操作後, 呼叫執行緒庫的回撥函式, 將原來處於 waiting 狀態的執行緒標記為 runnable.從上面的過程可以看出,使用者級支援執行緒(User-Supported Threads)的解決方案基於非阻塞IO系統呼叫( non-blocking system call) , 且是一種基於作業系統核心事件通知(event-driven)的解決方案, 該方案可以降低系統處理併發請求時的程序切換開銷。 基於這個方案, 可以引申到更為寬泛的 event-driven progreamming 話題上。 但是這裡就不作贅述了。總結:阻塞/非阻塞, 同步/非同步的概念要注意討論的上下文:在程序通訊層面, 阻塞/非阻塞, 同步/非同步基本是同義詞, 但是需要注意區分討論的物件是傳送方還是接收方。傳送方阻塞/非阻塞(同步/非同步)和接收方的阻塞/非阻塞(同步/非同步) 是互不影響的。在 IO 系統呼叫層面( IO system call )層面, 非阻塞 IO 系統呼叫 和 非同步 IO 系統呼叫存在著一定的差別, 它們都不會阻塞程序, 但是返回結果的方式和內容有所差別, 但是都屬於非阻塞系統呼叫( non-blocing system call ) 2. 非阻塞系統呼叫(non-blocking I/O system call 與 asynchronous I/O system call) 的存在可以用來實現執行緒級別的 I/O 併發, 與通過多程序實現的 I/O 併發相比可以減少記憶體消耗以及程序切換的開銷。

注:文章來源於某知乎大佬