衆所周知,如今的服務器能夠處理多個socket鏈接,背後併發的實現主要有兩種途徑。php
聊到socket,就不得不提到socket的創建的流程。祭出經典的老圖:
node
服務器依次使用socket,bind,listen以後就會監聽對應的地址,此時accept會一直阻塞直到有鏈接創建,若是客戶端和服務器創建了鏈接,那麼accept就會返回一個鏈接句柄,能夠對鏈接進行讀數據或者寫數據。nginx
那麼問題來了,服務器若是不作特殊處理的話,一次只能處理一個鏈接,新的鏈接來是須要等待上一個鏈接結束才能鏈接成功,這就是最開始的服務器同步阻塞方法。redis
同步阻塞:進程發起IO系統調用後,進程被阻塞,轉到內核空間處理,整個IO處理完畢後返回進程。操做成功則進程獲取到數據。segmentfault
可能之前的擁有的電腦人很少,這種方式一次只能鏈接一個倒也沒有問題,以後訪問的人開始多起來,設計者以爲這樣下去不行,就設計了多線程同步阻塞的方法。每次accept得到一個句柄,就建立一個線程去處理鏈接,這下子就能同時處理多個鏈接呢。這就是經典的多線程同步阻塞的方法。服務器
典型的多線程(進程)併發模型就是cgi。網絡
服務器和客戶端之間的併發,有如下特色:多線程
根據以上特色,咱們能夠得知服務器有如下問題:
若是採用多線程同步阻塞,1個tcp鏈接須要創建1個線程,10k個鏈接須要創建10k個鏈接,然而大部分鏈接是不活躍,即使是須要處理業務邏輯,也能夠快速返回結果,大部分時間也是處於I/O阻塞或網絡等待。這就使得多個線程的建立很耗費資源,且線程的切換也是極其耗費CPU,這就極可能致使了CPU處理業務的消耗的資源很少,可是卻花了不少資源在進程切換上面。併發
多線程併發的問題是大部分socket都是閒置的狀態或者是處於IO阻塞的狀態,那能不能把阻塞的socket先扔到一邊去處理其餘事情,來避免等待所帶來的資源耗費,也就是非阻塞IO的概念。異步
當用戶進程發出read操做時,若是kernel中的數據尚未準備好,那麼它並不會block用戶進程,而是馬上返回一個error。從用戶進程角度講 ,它發起一個read操做後,並不須要等待,而是立刻就獲得了一個結果。用戶進程判斷結果是一個error時,它就知道數據尚未準備好,因而它能夠再次發送read操做。一旦kernel中的數據準備好了,而且又再次收到了用戶進程的system call,那麼它立刻就將數據拷貝到了用戶內存,而後返回。
所以:使用非阻塞IO是須要不斷輪詢IO數據是否好了。
IO多路複用原理就是不斷輪詢多個socket,當其中的某個socket準備好了數據就返回,不然整個進程繼續阻塞,就可讓一個進程在不太耗費資源的狀況下處理多個鏈接,可是這個輪詢的操做是交給內核態去完成,也就避免了內核態和用戶態的切換的問題。
而目前的實現方法有select, poll, epoll,其中epoll的性能最好,用的也是最普遍。
而epoll的實現能夠作到性能幾乎不受鏈接數(單單是鏈接而沒有其餘的操做)的影響。
固然多路複用IO也有本身的問題,也就是自己不支持多核的使用,須要另外解決多核的利用。
其中使用enroll的成熟程序有nginx,redis,nodej等。
根據知乎大佬的介紹,服務器通過發展能夠分爲兩階段:
把傳輸層的tcp併發的鏈接放到IO多路複用去處理,應用層繼續使用多線程併發模型去作。這樣就能夠大幅度減小線程的建立切換的資源耗費。
如:nginx + php-fpm(實際上是php-fpm是多進程)
第二代服務器模型是把應用層也使用IO多路複用去處理,減小應用層的等待外部接口調用阻塞等待,通常是大廠大流量併發須要用到。
如:
參考資料:
[1]:許懷遠的知乎回答
[2]:Linux IO模式及 select、poll、epoll詳解