學習網絡編程時,本身動手實現一個Web Server
是一個頗有意思的經歷。大多數Web Server
都有一個特色:在單位時間內須要處理大量的請求,而且處理這些請求的時間每每還很短。《深刻理解計算機系統》 (CSAPP
) 在講解網絡編程時實現了一個經典的Web Server
,這個Web Server
不只知足了靜態請求,同時還知足了動態請求 (CGI
)。雖然這個Web Server
可以正常使用,可是仍存在一個明顯的缺陷:它是一個迭代式的Web Server
,這意味着在一個請求處理完畢前,不能同時處理另外一個請求,而咱們以前提到Web Server
的一個重要特色就是在單位時間內可能會有大量的請求,因此若是投入工業界,這種狀況天然是沒法容忍的。git
解決上面提到的Web Server
只能一個接着一個處理請求的第一個方案是:當accept
到一個請求時,fork
一個子進程去處理這個請求,而主進程仍然在監聽是否有新的鏈接請求。多進程模型在表面上看彷佛解決了問題,可是咱們都知道fork
一個進程的開銷是很是大的,基於如下幾個事實。github
從概念上說,能夠將fork
認做對父進程程序段、數據段、堆段以及棧段建立拷貝。可是若是真的只是簡單的將父進程虛擬內存頁拷貝到子進程,那就太浪費了。現代UNIX
(Linux
) 在實現fork
時每每會採用兩種技術來避免這種浪費。一是內核將每一進程的代碼段標記爲只讀,從而使得父進程和子進程都沒法修改代碼段。這樣,父進程和子進程能夠共共享同一代碼段。二是對於父進程數據段、堆段和棧段中的各頁,內核採用寫時複製(copy-on-write
) 的方式,這麼作的緣由之一是:fork
以後經常伴隨着exec
,這會用新程序替換進程的代碼段,並從新初始化其數據段、堆段和棧段。可是不管如何,仍存在複製頁表的操做,這也是爲何在UNIX
(Linux
) 下建立進程要比建立線程開銷大的緣由。編程
併發量一大,此時系統內便會有存在大量的進程,這會致使CPU
花費大量的時間在進程調度上,而且進程上下文的切換開銷也很大。網絡
所以,相比於多進程模型,多線程是一個更優的模型:建立線程要快於建立進程,線程間的上下文切換消耗的時間通常也比進程要短。多線程
換用多線程Web Server
模型:每accept
一個請求,建立一個線程,將請求交由該線程處理。換用多線程模型能夠解決由fork
帶來的開銷問題,可是調度問題依然仍是存在的。所以,一個顯而易見的解決辦法是使用線程池,將線程的數量固定下來。基本的實現思路以下。併發
將每一個請求封裝爲一個Job
,每一個Job
包含線程要執行的方法、傳遞給線程的參數以及用於描述該Job
處於Job
隊列的位置的參數。學習
線程池維護着一個Job
隊列,每一個線程從Job
隊列中取下一個Job
執行。由於該Job
隊列是一個共享資源,所以須要控制線程的同步。優化
初始化線程池時,立刻建立必定數量的線程。此時,這些線程都是阻塞狀態的,由於Job
隊列爲空。線程
tinyhttpd是我爲了更有效的學習網絡編程而實現的一個輕量級的Web Server
,目前仍有部分問題須要解決以及優化。按照上面的思路,我實現了一個簡單的線程池,並將其引入到tinyhttpd中。具體的代碼實現請參考threadpool.h和threadpool.c。code
當固定了線程池的線程數量後,仍然存在一個嚴重的問題:實際狀況下,不少鏈接都是長鏈接,這意味着一個線程在處理一個請求時,read
到的數據將會是是不連續的。當線程處理完一批數據後,若是繼續read
,而下一批數據還未到來時,因爲默認狀況下file descriptor
是blocking
的,所以該線程就會進入阻塞狀態。因此,若是線程池中全部的線程都處於阻塞狀態,此時若是有新的請求到來,那麼是沒法處理的。
解決方案是將file descriptor
設置爲non-blocking
,利用事件驅動(Event-driven
)來處理鏈接。