1. 程式人生 > >由select引發的思考

由select引發的思考

一、前言

  網路程式設計裡一個經典的問題,select,poll和epoll的區別?這個問題剛學習程式設計時就接觸了,當時看了材料很不明白,許多概念和思想沒有體會,現在在這個階段,再重新回頭看這個問題,有一種豁然開朗的感覺,

把目前我所能理解到的記錄下來。

二、從作業系統開始談起

  有幾個以後一直談到的概念,有必要先了解,以前就是這個步驟沒做,學習的時候一臉懵逼。

  1. 使用者空間和核心空間

    目前作業系統定址模式為虛擬定址,即處理器產生虛擬地址,之後翻譯成實體地址,匯流排到實體地址處理,處理器拿到處理後的資料

    作業系統 的核心為核心,可以訪問受保護的記憶體空間,可以訪問底層硬體設施,可以做任何事情。所以為了系統的穩定,講道理核心應該被保護起來。

    所以虛擬空間被分為核心空間和使用者空間。核心空間在最高的1G中,使用者空間在剩下的記憶體中。

    多數我們所用的程序在使用者空間處理,把請求發給核心,在核心空間中操作硬體。

  2. 程序上下文切換(程序切換)

    掛起當前程序,恢復某個程序,大家都知道這是個開銷大的過程,那具體有哪些步驟呢?

    首先,儲存當前程序一些必要資訊用以日後恢復,如描繪地址空間的頁表,程序表,檔案表等等。

    然後切換頁全域性目錄,安裝一個新的地址空間

    最後回覆目標程序的上下文

  3. 檔案描述符(fd)

    計算機的一個術語,指向檔案物件的一個抽象表示。形式上是一個非負整數的索引值,指向檔案表中的檔案

    當程式開啟或建立一個檔案時,核心向程序返回一個檔案描述符,代表該檔案。

    通過操作檔案描述符,我們實現真實操作檔案的目的

  4. 程序阻塞(process block)

    當某個程序等待一個執行結果時,自身阻塞(不幹事),直到得到結果,再繼續往下,

    重點是:這個是程序自身行為,且阻塞時不佔用cpu資源(cpu也不幹事),

    所以I/O請求最拉低效能,俗話說佔著茅坑不拉屎,在計算機裡也是存在的,所以人們設計了多執行緒,多程序等方案來解決這個問題。

  5. I/O過程

    一般有兩種模式,直接 I/O,快取 I/O(預設),

    快取 I/O:

      程序發起系統呼叫(通知系統我要讀寫了!)

      寫: 程序(資料) ——-》 程序緩衝池 ————》 核心緩衝池 ———-》儲存裝置

      讀: 儲存裝置(資料)———》核心緩衝池 ———-》程序緩衝池 ———》程序

    直接 I/O(程序快取池消失了!):      

      程序發起系統呼叫(通知系統我要讀寫了!)

      寫: 程序(資料) ————》 核心緩衝池 ———-》儲存裝置

      讀: 儲存裝置(資料)———》核心緩衝池 ———》程序

    以上每個步驟之間都是有可能程序會有阻塞(block)發生,根據不同位置的阻塞,就產生了多種網路模式,以適應於不同場景。

三、 I/O模式(以讀為例)

   1. block I/O

    過程 :

      程序發起系統呼叫(通知系統我要讀寫了!)

      讀: 儲存裝置(資料)———》核心緩衝池 ———》程序————》核心通知程序ok,程序解除阻塞

                 程序阻塞 程序阻塞

    解釋:

      程序一直等檔案準備好,再繼續下一步。

    應用場景:

      以上原理是一個使用者連線的情況,很容易理解。

      當一個伺服器對接多個客戶端的時候:

      初級方案:開多程序(大資料或長時間任務、開銷大,更安全)或多執行緒(很多連線,開銷低,資料放一起不太安全)為每一個客戶端建立一個連線來處理。

      不足:高併發的情況就體現出開銷大,效能低。一個是多執行緒切換,上下文切換的效能開銷,另一個是多執行緒數量大,會佔據大量系統資源

      優化方案:採用執行緒池(減少建立和銷燬執行緒的頻率)或連線池(維持連線的快取池,儘量重用已有的連線),降低系統開銷

      不足:降低開銷還是有限度的,在這個時代,高併發大,很容易到達瓶頸。

   2. non-block I/O   

    過程 :

      發起系統呼叫(通知系統我要讀寫了!)

      讀: 儲存裝置(資料)————-》核心緩衝池 ———–》程序————-》核心通知程序ok,程序解除阻塞

                 非阻塞 阻塞

    解釋:

      在核心準備資料階段,立即返回一個error給程序,

      因此程序知道核心還沒準備好,

      所以程序再問核心,核心再回error,直到核心準備好,被詢問時返回準備好的訊號,程序再接觸阻塞,

    應用場景:

      連線量小,沒差別,

      當連線數大的時候,這個模式理論上可以用一個執行緒實現多個連線:

      由於非阻塞,這個執行緒可以迴圈去詢問所有連線目標有沒有準備好,核心都是立馬回覆,error往下,準備好就交給程序,所以不會浪費時間,

      但是,(凡事都有但是),這個簡單的實現方案,效率還是很低的,畢竟從核心空間到使用者空間還是block的,而且會極大推高cpu佔用率。

      特別是當響應事件(讀取或者其他)龐大的時候,執行速度就會很緩慢。

      下面的select等就是基於此想法的發展。

   3. I/O multiplexing

    目標:低開銷,高效率得處理高併發請求

    方案:select 、poll、epoll 三種實現方案

    本質:用 select、poll、epoll 去監聽所有 socket物件,當socket物件發生變化時,通知使用者程序處理。

    特徵:

       select(最早出現):多平臺支援

                通過輪詢,效率較低

                處理連線數量有限制,預設1024個,

                大量使用者態和核心態fd的拷貝,效能低,

                返回的是所有控制代碼列表,沒有告訴是哪一個發生變化,使用者程序還得再次遍歷。

       poll(略微改進):改進了數量連線限制,做到了數量無限制

       epoll(改進所有缺點):當socket變化時,通知程序哪一個完成了,

                  數量無限制(為系統最大開啟檔案數量)

                  fd控制代碼只拷貝一次,效能高

    效能對比:

      這裡寫圖片描述

      橫座標是連線數,Dead Connections 是軟體命名的,縱座標是此時處理連線數

      可以看出 epoll 效能比較穩定,而且效能較優。

    仔細過程討論:

        這裡只討論大致原理,具體實現不同語言有不同的差別。(不是因為我沒做過,不是的)

        首先。在多路複用模型中,對於每一個 socket,一般都設定成為 non-blocking,但是,整個使用者的 process 其實是一直被 block 的

        即使用者程序被select、poll阻塞,但是select、poll是非阻塞的,他們不斷輪詢、掛起來完成工作。

        select:

          1. 從使用者態拷貝 fd_set 至核心空間(告訴核心要監聽的socket)

          2..註冊回撥函式pollwait(將程序掛到等待佇列中,當socket準備好後(執行mask狀態碼判斷),再喚醒程序)

          3. 核心遍歷fd,呼叫每一個的poll方法(本質上是pollwait回撥函式,返回值socket的mask狀態掩碼,即現在準備好了沒,給fd_set賦值)

          4. 當無可讀寫mask碼(沒有任何準備好的),select睡眠,等睡眠時間到,再次醒來輪詢fd-set

          5. 有值時返回fd_set(已經賦值完,例如可以讀的value為1)、將其拷貝至使用者空間

          6. 使用者程序迴圈fd_set,

        分析:

          每次迴圈都要執行上面流程,

          一次迴圈兩次拷貝fi_set,即每次監聽都重新告訴核心要監聽的事件,在使用者量很大的時候是一個很大的開銷

          返回所有的fd_set,卻沒有告訴程序哪一個是完成的,程序還得迴圈判斷,使用者量很大(十萬,百萬)的時候,效能太低

          因此,select只支援1024個連線。

          這也解釋了上圖中為什麼連線量越大,效能越低的現象,許多時間用來處理無活躍的連線、迴圈判斷中,在高併發低活躍的場景中尤為如此。

        poll:

          將fd_set結構改為pollfd結構,可以不限數量,但是其他問題咩有解決。

        epoll:

          改進:

            fd只拷貝一次(開始就告訴核心所有註冊事件,監聽物件)。

            只返回包含所有變化的fd的連結串列。

            連線無限制

          epoll提供三個函式

          epoll_create(控制代碼),開始是有size引數,說明fd數量,現在核心動態分配,

          epoll_ctl(註冊事件型別),註冊監聽事件

          epoll_wait(等待事件發生),捕捉fd訊號,

    三者區別小結:

      select、poll 孿生兄弟,有許多缺點,優點不多,應用場景也不多。是時代的產物

      epoll 是進階版,但是隻有Linux有,

      具體情況具體分析。

  4. 非同步io

    解釋:程序完全不阻塞,請求發完就去做其他事,等資料全部準備好,核心發訊息給程序,程序接著處理,

    實現:聽說很複雜,沒研究。

四、總結和挖坑

  研究了一些作業系統的概念,研究了I/O模式,著重研究了select、poll、epoll 的區別,

  有時間 具體實現和操作,實踐出真知,許多細節可能還有謬誤,待以後水平上升,再來修改。