1. 程式人生 > 其它 >7層網路以及5種Linux IO模型以及相應IO基礎

7層網路以及5種Linux IO模型以及相應IO基礎

一、七層網路模型

  OSI是Open System Interconnection的縮寫,意為開放式系統互聯。國際標準化組織(ISO)制定了OSI模型,該模型定義了不同計算機互聯的標準,它是一個七層的、抽象的模型體。

      

  1、物理層

  並不是物理媒體本身,它只是開放系統中利用物理媒體實現物理連線的功能描述和執行連線的規程,建立、維護、斷開物理連線,傳輸單位是位元(bit)。

  物理層的媒體包括架空明線、平衡電纜、光纖、無線通道等。通訊用的互連裝置指DTE(Data Terminal Equipment)和DCE(Data Communications Equipment)間的互連裝置。DTE即資料終端裝置,又稱物理裝置,如計算機、終端等都包括在內。而DCE則是資料通訊裝置或電路連線裝置,如調變解調器等。資料傳輸通常是經過DTE-DCE,再經過DCE-DTE的路徑。互連裝置指將DTE、DCE連線起來的裝置,如各種插頭、插座。LAN中的各種粗、細同軸電纜、T型接頭、插頭、接收器、傳送器、中繼器等都屬物理層的媒體和聯結器。

  物理層的主要功能是:

  ①為資料端裝置提供傳送資料的通路,資料通路可以是一個物理媒體,也可以是多個物理媒體連線而成。一次完整的資料傳輸,包括啟用物理連線、傳送資料和終止物理連線。所謂啟用,就是不管有多少物理媒體參與,都要在通訊的兩個資料終端裝置間連線起來,形成一條通路。

  ②傳輸資料。物理層要形成適合資料傳輸需要的實體,為資料傳送服務。一是要保證資料能在其上正確通過,二是要提供足夠的頻寬(頻寬是指每秒鐘內能通過的位元(Bit)數),以減少通道上的擁塞。傳輸資料的方式能滿足點到點,一點到多點,序列或並行,半雙工或全雙工,同步或非同步傳輸的需要。

  2、資料鏈路層

  可以粗略地理解為資料通道,傳輸單位是幀(Frame)。物理層要為終端裝置間的資料通訊提供傳輸介質及其連線。介質是長期的,連線是有生存期的。在連線生存期內,收發兩端可以進行不等的一次或多次資料通訊。每次通訊都要經過建立通訊聯絡和拆除通訊聯絡兩個過程。這種建立起來的資料收發關係就叫做資料鏈路。而在物理媒體上傳輸的資料難免受到各種不可靠因素的影響而產生差錯,為了彌補物理層上的不足,為上層提供無差錯的資料傳輸,就要能對資料進行檢錯和糾錯。鏈路層應具備如下功能:

  ①鏈路連線的建立、拆除和分離;

  ②差錯檢測和恢復。還有鏈路標識,流量控制等等。

  獨立的鏈路產品中最常見的當屬網絡卡、網橋、二路交換機等。

  3、網路層

  在網路層: 有IP (IPV4、IPV6)協議、ICMP協議、ARP協議、RARP協議和BOOTP協議,負責建立“主機”到“主機”的通訊,傳輸單位是分組(資料包Packet)。

  當資料終端增多時。它們之間有中繼裝置相連,此時會出現一臺終端要求不只是與惟一的一臺而是能和多臺終端通訊的情況,這就產生了把任意兩臺資料終端裝置的資料鏈接起來的問題,也就是路由或者叫尋徑。另外,當一條物理通道建立之後,被一對使用者使用,往往有許多空閒時間被浪費掉。人們自然會希望讓多對使用者共用一條鏈路,為解決這一問題就出現了邏輯通道技術和虛擬電路技術。

  4、傳輸層

  在傳輸層: 有TCP協議與UDP協議,負責建立“埠”到“埠”的通訊,傳輸單位是資料段(Segment)。

  有一個既存事實,即世界上各種通訊子網在效能上存在著很大差異。例如電話交換網,分組交換網,公用資料交換網,區域網等通訊子網都可互連,但它們提供的吞吐量,傳輸速率,資料延遲通訊費用各不相同。對於會話層來說,卻要求有一效能恆定的介面。傳輸層就承擔了這一功能。

  5、會話層

  會話單位的控制層,其主要功能是按照在應用程序之間約定的原則,按照正確的順序收、發資料,進行各種形態的對話。會話層規定了會話服務使用者間會話連線的建立和拆除規程以及資料傳送規程。

  會話層提供的服務是應用建立和維持會話,並能使會話獲得同步。會話層使用校驗點可使通訊會話在通訊失效時從校驗點繼續恢復通訊。這種能力對於傳送大的檔案極為重要。

  6、表示層

  其主要功能是把應用層提供的資訊變換為能夠共同理解的形式,提供字元程式碼、資料格式、控制資訊格式、加密等的統一表示。表示層的作用之一是為異種機通訊提供一種公共語言,以便能進行互操作。這種型別的服務之所以需要,是因為不同的計算機體系結構使用的資料表示法不同。例如,IBM主機使用EBCDIC編碼,而大部分PC機使用的是ASCII碼。在這種情況下,便需要表示層來完成這種轉換。

  7、應用層

  嚮應用程式提供服務,這些服務按其嚮應用程式提供的特性分成組,並稱為服務元素。有些可為多種應用程式共同使用,有些則為較少的一類應用程式使用。應用層是開放系統的最高層,是直接為應用程序提供服務的。其作用是在實現多個系統應用程序相互通訊的同時,完成一系列業務處理所需的服務。

  在應用層: 有FTP、HTTP、TELNET、SMTP、DNS等協議。

二、七層網路模型傳輸過程

            

            

  TCP/IP中的資料包傳輸過程如下:

  每個分層中,都會對所傳送的資料附加一個首部,在這個首部中包含了該層必要的資訊,如傳送的目標地址以及協議相關資訊。通常,為協議提供的資訊為包首部,所要傳送的內容為資料。在下一層的角度看,從上一層收到的包全部都被認為是本層的資料。

  網路中傳輸的資料包由兩部分組成:一部分是協議所要用到的首部,另一部分是上一層傳過來的資料。首部的結構由協議的具體規範詳細定義。在資料包的首部,明確標明瞭協議應該如何讀取資料。反過來說,看到首部,也就能夠了解該協議必要的資訊以及所要處理的資料。

  ① 應用程式處理

  首先應用程式會進行編碼處理,這些編碼相當於 OSI 的表示層功能;編碼轉化後,郵件不一定馬上被髮送出去,這種何時建立通訊連線何時傳送資料的管理功能,相當於 OSI 的會話層功能。

  ② TCP 模組的處理

  TCP 根據應用的指示,負責建立連線、傳送資料以及斷開連線。TCP 提供將應用層發來的資料順利傳送至對端的可靠傳輸。為了實現這一功能,需要在應用層資料的前端附加一個 TCP 首部。

  ③ IP 模組的處理

  IP 將 TCP 傳過來的 TCP 首部和 TCP 資料合起來當做自己的資料,並在 TCP 首部的前端加上自己的 IP 首部。IP 包生成後,參考路由控制表決定接受此 IP 包的路由或主機。

  ④ 網路介面(乙太網驅動)的處理

  從 IP 傳過來的 IP 包對於乙太網來說就是資料。給這些資料附加上乙太網首部並進行傳送處理,生成的乙太網資料包將通過物理層傳輸給接收端。

  ⑤ 網路介面(乙太網驅動)的處理

  主機收到乙太網包後,首先從乙太網包首部找到 MAC 地址判斷是否為傳送給自己的包,若不是則丟棄資料。

  如果是傳送給自己的包,則從乙太網包首部中的型別確定資料型別,再傳給相應的模組,如 IP、ARP 等。這裡的例子則是 IP 。

  ⑥ IP 模組的處理

  IP 模組接收到 資料後也做類似的處理。從包首部中判斷此 IP 地址是否與自己的 IP 地址匹配,如果匹配則根據首部的協議型別將資料傳送給對應的模組,如 TCP、UDP。這裡的例子則是 TCP。
  另外,對於有路由器的情況,接收端地址往往不是自己的地址,此時,需要藉助路由控制表,在調查應該送往的主機或路由器之後再進行轉發資料。

  ⑦ TCP 模組的處理

  在 TCP 模組中,首先會計算一下校驗和,判斷資料是否被破壞。然後檢查是否在按照序號接收資料。最後檢查埠號,確定具體的應用程式。資料被完整地接收以後,會傳給由埠號識別的應用程式。

  ⑧ 應用程式的處理

  接收端應用程式會直接接收發送端傳送的資料。通過解析資料,展示相應的內容。

  傳輸過程中協議如下:

    

三、什麼是SOCKET 

  Socket是應用層與TCP/IP協議族通訊的中間軟體抽象層,它是一組介面。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket介面後面,對使用者來說,一組簡單的介面就是全部,讓Socket去組織資料,以符合指定的協議。

  Socket 介面是TCP/IP網路的API,Socket介面定義了許多函式或例程,用以開發TCP/IP網路上的應用程式。

  Socket為了實現以上的通訊過程而建立成來的通訊管道,其真實的代表是客戶端和伺服器端的一個通訊程序,雙方程序通過socket進行通訊,而通訊的規則採用指定的協議。socket只是一種連線模式,不是協議,tcp,udp,簡單的說(雖然不準確)是兩個最基本的協議,很多其它協議都是基於這兩個協議如,http就是基於tcp的,用socket可以建立tcp連線,也可以建立udp連線,這意味著,用socket可以建立任何協議的連線,因為其它協議都是基於此的。

  綜上所述:需要IP協議來連線網路;TCP是一種允許我們安全傳輸資料的機制,使用TCP協議來傳輸資料的HTTP是Web伺服器和客戶端使用的特殊協議。HTTP基於TCP協議,但是卻可以使用socket去建立一個TCP連線。

  如圖:

          

四、長短連線

  短連線:連線->傳輸資料->關閉連線

  也可以這樣說:短連線是指SOCKET連線後傳送後接收完資料後馬上斷開連線。

  長連線:連線->傳輸資料->保持連線 -> 傳輸資料-> 。。。 ->關閉連線。

  長連線指建立SOCKET連線後不管是否使用都保持連線,但安全性較差。

  什麼時候用長連線,短連線?

  長連線多用於操作頻繁,點對點的通訊,而且連線數不能太多情況。每個TCP連線都需要三步握手,這需要時間,如果每個操作都是先連線,再操作的話那麼處理速度會降低很多,所以每個操作完後都不斷開,下次處理時直接傳送資料包就OK了,不用建立TCP連線。例如:資料庫的連線用長連線, 如果用短連線頻繁的通訊會造成socket錯誤,而且頻繁的socket 建立也是對資源的浪費。

  而像WEB網站的http服務一般都用短連結,因為長連線對於服務端來說會耗費一定的資源,而像WEB網站這麼頻繁的成千上萬甚至上億客戶端的連線用短連線會更省一些資源,如果用長連線,而且同時有成千上萬的使用者,如果每個使用者都佔用一個連線的話,那可想而知吧。所以併發量大,但每個使用者無需頻繁操作情況下需用短連好。
總之,長連線和短連線的選擇要視情況而定。

五、三次握手四次分手

      

  SYN,ACK,FIN存放在TCP的標誌位,一共有6個字元,這裡就介紹這三個:

SYN:代表請求建立連線,所以在三次握手中前兩次要SYN=1,表示這兩次用於建立連線,至於第三次什麼用,在疑問三裡解答。

FIN:表示請求關閉連線,在四次分手時,我們發現FIN發了兩遍。這是因為TCP的連線是雙向的,所以一次FIN只能關閉一個方向。

ACK:代表確認接受,從上面可以發現,不管是三次握手還是四次分手,在迴應的時候都會加上ACK=1,表示訊息接收到了,並且在建立連線以後的傳送資料時,都需加上ACK=1,來表示資料接收成功。

seq: 序列號,什麼意思呢?當傳送一個數據時,資料是被拆成多個數據包來發送,序列號就是對每個資料包進行編號,這樣接受方才能對資料包進行再次拼接。初始序列號是隨機生成的,這樣不一樣的資料拆包解包就不會連線錯了。(例如:兩個資料都被拆成1,2,3和一個數據是1,2,3一個是101,102,103,很明顯後者不會連線錯誤)

ack: 這個代表下一個資料包的編號,這也就是為什麼第二請求時,ack是seq+1

  TCP是雙向的,所以需要在兩個方向分別關閉,每個方向的關閉又需要請求和確認,所以一共就4次分手。

六、檔案描述符

  在UNIX、Linux的系統呼叫中,核心系統把應用程式可以操作的資源都抽象成了檔案概念,比如說硬體裝置,socket,流,磁碟,程序,執行緒;檔案描述符就是索引(指標)。

  檔案描述符就是核心為了高效管理已被開啟的檔案所建立的索引,用於指向被開啟的檔案,所有執行I/O操作的系統呼叫都通過檔案描述符;檔案描述符是一個簡單的非負整數,用以表明每個被程序開啟的檔案。程式剛剛啟動時,第一個開啟的檔案是0,第二個是1,以此類推。也可以理解為檔案的身份ID。如:

        

  標準輸入輸出說明

  stdin,標準輸入,預設裝置是鍵盤,檔案編號為0

  stdout,標準輸出,預設裝置是顯示器,檔案編號為1,也可以重定向到檔案

  stderr,標準錯誤,預設裝置是顯示器,檔案編號為2,也可以重定向到檔案

  /proc/[程序ID]/fd 這個目錄專門用於存放檔案描述符,可以到目錄下檢視檔案描述符使用情況,同時也可以通過ulimit檢視檔案描述符限制,如:

192:~ XXX$ ulimit -n  //-n開啟檔案描述符的最大個數
256
192:~ XXX$ ulimit -Sn  //-S是軟性限額
256
192:~ XXX$ ulimit -Hn  //-H是硬性限額
unlimited

  Linux中最大檔案描述符的限制有兩個方面,一個是使用者級限制,一個是系統級限制,檔案描述符限制均可進行修改,但是也有一個限制,規則如下:

  a. 所有程序開啟的檔案描述符數不能超過/proc/sys/fs/file-max

  b. 單個程序開啟的檔案描述符數不能超過user limit中nofile的soft limit

  c. nofile的soft limit不能超過其hard limit

  d. nofile的hard limit不能超過/proc/sys/fs/nr_open

七、零拷貝

  應用程式獲取資料的兩個階段:

  資料準備:應用程式無法直接操作我們的硬體資源,需要作業系統資源時,先通知我們的核心,核心檢查是否有就緒的資源,如果有則先把對應資料載入到核心空間。

  資料拷貝:把資料資源從核心空間複製到應用程式的使用者空間。

  補充知識 -> 零拷貝

  現代作業系統都使用虛擬記憶體,使用虛擬的地址取代實體地址,這樣做的好處是:

  1.一個以上的虛擬地址可以指向同一個實體記憶體地址,

  2.虛擬記憶體空間可大於實際可用的實體地址;

  利用第一條特性可以把核心空間地址和使用者空間的虛擬地址對映到同一個實體地址,這樣DMA就可以填充對核心和使用者空間程序同時可見的緩衝區了,大致如下圖所示:

            

  關於mmap以及sendfile零拷貝,可以參考:如何實現高效能的IO及其原理?

八、Linux 網路IO模型

  什麼是同步和非同步,阻塞和非阻塞?

  同步和非同步關注的是結果訊息的通訊機制

  同步:同步的意思就是呼叫方需要主動等待結果的返回

  非同步:非同步的意思就是不需要主動等待結果的返回,而是通過其他手段比如,狀態通知,回撥函式等。

  阻塞和非阻塞主要關注的是等待結果返回時呼叫方的狀態

  阻塞:是指結果返回之前,當前執行緒被掛起,不做任何事

  非阻塞:是指結果在返回之前,執行緒可以做一些其他事,不會被掛起。

  Linux有5種IO模型,如下圖所示:

        

  1、阻塞I/O模型

  應用程式呼叫一個IO函式,導致應用程式阻塞,等待資料準備好。 如果資料沒有準備好,一直等待….資料準備好了,從核心拷貝到使用者空間,IO函式返回成功指示。

  當呼叫recv()函式時,系統首先查是否有準備好的資料。如果資料沒有準備好,那麼系統就處於等待狀態。當資料準備好後,將資料從系統緩衝區複製到使用者空間,然後該函式返回。在套接應用程式中,當呼叫recv()函式時,未必使用者空間就已經存在資料,那麼此時recv()函式就會處於等待狀態。

            

  2、非阻塞IO模型

  我們把一個SOCKET介面設定為非阻塞就是告訴核心,當所請求的I/O操作無法完成時,不要將程序睡眠,而是返回一個錯誤。這樣我們的I/O操作函式將不斷的測試資料是否已經準備好,如果沒有準備好,繼續測試,直到資料準備好為止。在這個不斷測試的過程中,會大量的佔用CPU的時間。上述模型絕不被推薦。

  把SOCKET設定為非阻塞模式,即通知系統核心:在呼叫Windows Sockets API時,不要讓執行緒睡眠,而應該讓函式立即返回。在返回時,該函式返回一個錯誤程式碼。如圖所示,一個非阻塞模式套接字多次呼叫recv()函式的過程。前三次呼叫recv()函式時,核心資料還沒有準備好。因此,該函式立即返回WSAEWOULDBLOCK錯誤程式碼。第四次呼叫recv()函式時,資料已經準備好,被複制到應用程式的緩衝區中,recv()函式返回成功指示,應用程式開始處理資料。

            

  3、IO複用模型

  簡介:主要是select和epoll;對一個IO埠,兩次呼叫,兩次返回,比阻塞IO並沒有什麼優越性;關鍵是能實現同時對多個IO埠進行監聽;

  I/O複用模型會用到select、poll、epoll函式,這幾個函式也會使程序阻塞,但是和阻塞I/O所不同的的,這兩個函式可以同時阻塞多個I/O操作。而且可以同時對多個讀操作,多個寫操作的I/O函式進行檢測,直到有資料可讀或可寫時,才真正呼叫I/O操作函式。

  當用戶程序呼叫了select,那麼整個程序會被block;而同時,kernel會“監視”所有select負責的socket;當任何一個socket中的資料準備好了,select就會返回。這個時候,使用者程序再呼叫read操作,將資料從kernel拷貝到使用者程序。

  這個圖和blocking IO的圖其實並沒有太大的不同,事實上還更差一些。因為這裡需要使用兩個系統呼叫(select和recvfrom),而blocking IO只調用了一個系統呼叫(recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。(select/epoll的優勢並不是對於單個連線能處理得更快,而是在於能處理更多的連線。)

           

  在這種模型中,這時候並不是程序直接發起資源請求的系統呼叫去請求資源,程序不會被“全程阻塞”,程序是呼叫select或poll函式。程序不是被阻塞在真正IO上了,而是阻塞在select或者poll上了。Select或者poll幫助使用者程序去輪詢那些IO操作是否完成。

  不過你可以看到之前都只使用一個系統呼叫,在IO複用中反而是用了兩個系統呼叫,但是使用IO複用你就可以等待多個描述符也就是通過單程序單執行緒實現併發處理,同時還可以兼顧處理套接字描述符和其他描述符。

  4、訊號驅動IO

  簡介:兩次呼叫,兩次返回;

  首先我們允許套介面進行訊號驅動I/O,並安裝一個訊號處理函式,程序繼續執行並不阻塞。當資料準備好時,程序會收到一個SIGIO訊號,可以在訊號處理函式中呼叫I/O操作函式處理資料。

           

  5、非同步IO模型

  當一個非同步過程呼叫發出後,呼叫者不能立刻得到結果。實際處理這個呼叫的部件在完成後,通過狀態、通知和回撥來通知呼叫者的輸入輸出操作。

  在linux的非同步IO模型中,並沒有真正實現非同步通道,最終的實現還是等同於呼叫Epoll。

           

  LInux IO模型總結如圖所示:

          

九、多路複用IO原理詳解

  在linux 沒有實現epoll事件驅動機制之前,我們一般選擇用select或者poll等IO多路複用的方法來實現併發服務程式。但在大資料、高併發、叢集出現後,select和poll的效能瓶頸無法在支撐,於是epoll出現了。

  1、select

  首先來說說select,select 函式監視的檔案描述符分3類,分別是writefds、readfds、和exceptfds。呼叫後select函式會阻塞,直到有描述符就緒(有資料 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設為null即可),函式返回。當select函式返回後,可以通過遍歷fd_set,來找到就緒的描述符。

int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);

  具體select步驟如圖所示:

            

  1. 使用copy_from_user從使用者空間拷貝fd_set到核心空間。
  2. 註冊回撥函式__pollwait。
  3. 遍歷所有FD,呼叫其對應的poll方法(對於socket,這個poll方法是sock_poll,sock_poll根據情況會呼叫到tcp_poll, udp_poll或者datagram_poll)
  4. 以tcp_poll為例,其核心實現就是__pollwait,也就是上面註冊的回撥函式。
  5. __pollwait的主要工作就是把當前程序掛到裝置的等待佇列中,不同的裝置有不同的等待佇列,對於tcp_poll來說,其等待佇列是sk->sk_sleep(注意把程序掛到等待佇列中並不代表程序已經睡眠了)。在裝置收到一條訊息(網路IO)或填寫完檔案資料(磁碟IO)後,會喚醒裝置等待佇列上睡眠的程序,這時當前程序便被喚醒了。
  6. poll方法返回時會返回一個描述讀寫操作是否就緒的mask掩碼,根據這個mask掩碼給fd_set賦值。
  7. 如果遍歷完所有的FD,還沒有返回一個可讀寫的mask掩碼,則會呼叫schedule_timeout讓呼叫select的當前程序進入睡眠。當裝置驅動發生自身資源可讀寫後,會喚醒其等待佇列上睡眠的程序。如果超過設定的超時時間,還是沒人喚醒,則呼叫select的程序會重新被喚醒獲得CPU,進而重新遍歷FD,判斷有沒有就緒的FD。
  8. 把fd_set從核心空間拷貝到使用者空間。
  9. select的觸發方式是水平觸發,應用程式如果沒有完成對一個已經就緒的檔案描述符進行IO操作,那麼之後每次select呼叫還是會將這些檔案描述符通知程序。

  注意:select的實現依賴於檔案的驅動函式poll,在unix中無論是呼叫 select、poll 還是epoll,最終都會呼叫該函式。

  2、poll

int poll(struct pollfd fds[], nfds_t nfds, int timeout);

  不同與select使用三個點陣圖來表示三個fdset的方式,poll使用一個 pollfd的指標實現。

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

  和select函式一樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符。select和poll都需要在返回後,通過遍歷檔案描述符來獲取已經就緒的socket。事實上,同時連線的大量客戶端在一時刻可能只有很少的處於就緒狀態,因此隨著監視的描述符數量的增長,其效率也會線性下降。

  3、epoll

  在select/poll時代,伺服器程序每次都把這100萬個連線告訴作業系統(從使用者態複製控制代碼資料結構到核心態),讓作業系統核心去查詢這些套接字上是否有事件發生,輪詢完後,再將控制代碼資料複製到使用者態,讓伺服器應用程式輪詢處理已發生的網路事件,這一過程資源消耗較大,因此,select/poll一般只能處理幾千的併發連線。

  epoll的設計和實現與select完全不同。epoll通過在Linux核心中申請一個簡易的檔案系統(檔案系統一般用什麼資料結構實現?紅黑樹)。epoll提供了三個函式,epoll_create, epoll_ctl和epoll_wait,epoll_create是建立一個epoll控制代碼;epoll_ctl是註冊要監聽的事件型別;epoll_wait則是等待事件的產生。

int epoll_create(int size);//建立一個epoll的控制代碼,size用來告訴核心這個監聽的數目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};
// events可以是以下幾個巨集的集合:
// EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);
// EPOLLOUT:表示對應的檔案描述符可以寫;
// EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);
// EPOLLERR:表示對應的檔案描述符發生錯誤;
// EPOLLHUP:表示對應的檔案描述符被結束通話;
// EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
// EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL佇列裡

  一棵紅黑樹,一張準備就緒控制代碼連結串列,少量的核心cache,就幫我們解決了大併發下的socket處理問題。

  ①執行 epoll_create
    核心在epoll檔案系統中建了個file結點,(使用完,必須呼叫close()關閉,否則導致fd被耗盡)
       在核心cache裡建了紅黑樹儲存epoll_ctl傳來的socket,
       在核心cache裡建了rdllist雙向連結串列儲存準備就緒的事件。
  ② 執行 epoll_ctl
    如果增加socket控制代碼,檢查紅黑樹中是否存在,存在立即返回,不存在則新增到樹幹上,然後向核心註冊回撥函式,告訴核心如果這個控制代碼的中斷到了,就把它放到準備就緒list連結串列裡。所有新增到epoll中的事件都會與裝置(如網絡卡)驅動程式建立回撥關係,相應的事件發生時,會呼叫回撥方法。

  ③ 執行epoll_wait

    立刻返回準備就緒表裡的資料即可(將核心cache裡雙向列表中儲存的準備就緒的事件 複製到使用者態記憶體),當呼叫epoll_wait檢查是否有事件發生時,只需要檢查eventpoll物件中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發生的事件複製到使用者態,同時將事件數量返回給使用者。 

        

  對於select的三個缺點以及epoll的解決方案:

  (1)每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,這個開銷在fd很多時會很大。

  (2)同時每次呼叫select都需要在核心遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大。

  (3)select支援的檔案描述符數量太小了,預設是1024。

  對於第一個缺點,epoll的解決方案在epoll_ctl函式中。每次註冊新的事件到epoll控制代碼中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進核心,而不是在epoll_wait的時候重複拷貝。epoll保證了每個fd在整個過程中只會拷貝一次。

  對於第二個缺點,epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的裝置等待佇列中,而只在epoll_ctl時把current掛一遍(這一遍必不可少)併為每個fd指定一個回撥函式,當裝置就緒,喚醒等待佇列上的等待者時,就會呼叫這個回撥函式,而這個回撥函式會把就緒的fd加入一個就緒連結串列)。epoll_wait的工作實際上就是在這個就緒連結串列中檢視有沒有就緒的fd(利用schedule_timeout()實現睡一會,判斷一會的效果,和select實現中的第7步是類似的)。

  對於第三個缺點,epoll沒有這個限制,它所支援的FD上限是最大可以開啟檔案的數目,這個數字一般遠大於2048,舉個例子,在1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關係很大。

  4、select、poll、epoll優缺點對比

        

          

  5、epoll 的水平觸發與邊緣觸發

  Level_triggered(水平觸發):

  當被監控的檔案描述符上有可讀寫事件發生時,epoll_wait()會通知處理程式去讀寫。如果這次沒有把資料一次性全部讀寫完(如讀寫緩衝區太小),那麼下次呼叫 epoll_wait()時,它還會通知你在上沒讀寫完的檔案描述符上繼續讀寫,當然如果你一直不去讀寫,它會一直通知你!!!如果系統中有大量你不需要讀寫的就緒檔案描述符,而它們每次都會返回,這樣會大大降低處理程式檢索自己關心的就緒檔案描述符的效率!!!

  Edge_triggered(邊緣觸發):

  當被監控的檔案描述符上有可讀寫事件發生時,epoll_wait()會通知處理程式去讀寫。如果這次沒有把資料全部讀寫完(如讀寫緩衝區太小),那麼下次呼叫epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該檔案描述符上出現第二次可讀寫事件才會通知你!!!這種模式比水平觸發效率高,系統不會充斥大量你不關心的就緒檔案描述符!!

  select(),poll()模型都是水平觸發模式,訊號驅動IO是邊緣觸發模式,epoll()模型即支援水平觸發,也支援邊緣觸發,預設是水平觸發。