7,阻塞IO和非阻塞IO-多路複用

阻塞IO,非阻塞IO,IO多路複用,異步IO,

 參考連接:html

  https://www.cnblogs.com/Eva-J/articles/8324837.htmlpython

 

注意:

  socket的server與client收發消息時,若是client端已經sk.close()後,server端還在收消息,那麼收到的消息是空.

 

 

拿下樓取快遞舉例子:
    1.阻塞IO:
        在樓下一直等着,等快遞員來了,拿着快遞走人
    2.非阻塞IO:
        下樓拿快遞,快遞員沒有來,上樓,過一會再下樓,直到拿到快遞
    3.IO多路複用:
        物業安排一個保安在樓下等快遞員,誰的快遞到了,保安通知誰下樓取快遞.

 

 

 

 阻塞IO,對應代碼就是socket的server跟client代碼

recv的阻塞:下圖展現的是,一臺機器從另外一臺機器接收數據的過程,以及阻塞的過程.

 

 

 

 下圖顯示的是,程序在本機接收數據的過程,數據尚未從另外一臺機器傳過來時的過程.

 

 以上是阻塞IO的模型

 

非阻塞IO:

        

 

 

非阻塞IO的代碼,在沒有起多進程,多線程的狀況下,實現了併發.

server端代碼:

import socket
sk = socket.socket()
sk.bind(('127.0.0.1',9090))
sk.setblocking(False)  # 注意此處是進行的不阻塞.
sk.listen()
conn_l = []
del_l = []
while True:
    try:
        conn,addr = sk.accept()
        conn_l.append(conn)
    except BlockingIOError as e:
        for conn in conn_l:
            try:
                ret = conn.recv(1024)
                if ret:
                    print(ret)
                    conn.send(b'hello')
                else:
                    conn.close()
                    del_l.append(conn)
            except (BlockingIOError,OSError) :pass  # 報不一樣的異常的時候,能夠把異常寫一塊兒,
        try:
            for conn in del_l:
                conn_l.remove(conn)
            # del_l.clear()  # 是否注掉,存疑.
        except ValueError:
            pass



client端:
import socket

sk=socket.socket()
sk.connect(('127.0.0.1',9090))
while 1:
sk.send(b'hello server')
msg=sk.recv(1024)
print(msg)
sk.close()

 

通常不用非阻塞IO,

非阻塞IO,弊端:linux

  一直在while True,給操做系統帶來很大 壓力,另外,在沒有客戶端鏈接的時候,一直在處理異常,沒有實際的做用.程序員

/web

 

IO多路複用:

  引入select 代理的使用,就比如取快遞,本身不去取(乾等着),讓路人甲給盯着,等快遞到了,路人甲通知本身去取快遞.

  

 

 IO多路複用的代碼體現

 

# IO多路複用 - 操做系統提供的
# 1.程序不能干預過程
# 2.操做系統之間的差別
 server端: import select  #注意select模塊的引入 import socket

sk = socket.socket()
sk.bind(('127.0.0.1',9090))
sk.listen()
sk.setblocking(False)
rlst = [sk]
while True:
    rl,wl,xl = select.select(rlst,[],[])  #[sk,conn1,conn2]
    # 爲何忽然把sk返回回來了?  sk對象有數據能夠被讀了
    # 爲何返回三個列表? 讀事件的列表 寫事件的列表 條件的列表
    # 爲何是列表?  有可能同時有多個被監聽的對象發生讀事件
    for obj in rl:
        if obj is sk:   # is的意思更精準,判斷的是obj就是sk
            conn,addr = obj.accept()
            rlst.append(conn)
        else:
            try:
                ret = obj.recv(1024)
                print(ret)
                obj.send(b'hello')
            except ConnectionResetError:
                obj.close()
                rlst.remove(obj)

# TCP協議來講,若是對方關閉了鏈接
# 另外一方有可能繼續 接收 空消息 或者 報錯

# 背代碼
# 將具體的狀況套到代碼中 將邏輯理順
# 理解以前IO多路複用的那張圖

# 什麼叫IO多路複用:
    # io多路複用是操做系統提供的一種 監聽 網絡IO操做的機制
    # 監聽三個列表
    # 當某一個列表有對應的事件發生的時候
    # 操做系統通知應用程序
    # 操做系統根據返回的內容作具體的操做
# 對於只有一個對象須要監聽的狀況  IO多路複用並沒有法發揮做用
# 對於併發接收網絡請求的應用場景  IO多路複用能夠幫助你在節省CPU利用率和操做系統調用的基礎上完成併發需求

# IO多路複用
    # select 是windows上的機制 輪詢的方式來監聽每個對象是否有對應的事件發生的,數據越多延遲越大
    #         可以處理的對象數是有限的
    # poll  linux系統上的,  和select的機制基本一致,也是輪詢機制,對底層存儲被監聽對象的數據結構作了優化
    #         可以處理的對象個數增長了
    # epoll  linux上的機制, 再也不用輪詢了,採用了回調函數的方式來通知應用被監聽的對象有事件發生了,就是有消息了立馬就返回,不用等所有輪詢完了再返回.                  

 

 

 

/數據庫

/編程

/windows

/數組

/緩存

/

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

 

 

 

blocking IO:阻塞IO.

nonblockingIO:非阻塞IO.

IO multiplexing:IO多路複用.

asychronous IO:異步複用,

 

 

IO模型
阻塞IO
正常的socket服務都是阻塞IO
只要是遇到阻塞,整個進程/線程都會進入阻塞狀態
下降了CPU的使用率,
tcp協議起socket server的時候有一個請求進入阻塞,其餘的對象都不能接收請求
解決方案 :
socketserver/協程 # 正確的
多線程/多進程 有多少個client就要開多少線程/進程 # 很差
池 : 可以同時處理的client是有限的,和池的個數相等 # 很差
非阻塞IO
sk.setblocking(False)
在遇到IO的時候不阻塞,程序可以利用不阻塞的時間作其餘事情
內部是使用while True在作其餘任務的詢問,致使CPU的使用率過大
IO多路複用
操做系統提供的,監聽網絡IO操做的機制,它是一個代理,能夠同時監聽多個鏈接對象
select 操做系統在內部幫助你輪詢全部被監聽的對象,受監聽對象的數量的影響
poll 可以監聽的網絡對象數量多,優化了底層的數據結構,受監聽對象的數量的影響
epoll 內部使用了回調函數的方式,減小操做系統的輪詢消耗資源
異步IO
數據copy階段作了優化 這是上面全部的IO模型都沒有作到的

 

 

IO發生時涉及的對象和步驟。對於一個network IO (這裏咱們以read舉例),它會涉及到兩個系統對象,一個是調用這個IO的process (or thread),另外一個就是系統內核(kernel)。當一個read操做發生時,該操做會經歷兩個階段:

1等待數據準備 (Waiting for the data to be ready)
2將數據從內核拷貝到進程中(Copying the data from the kernel to the process)

阻塞IO(blocking IO)

 

在linux中,默認狀況下全部的socket都是blocking,一個典型的讀操做流程大概是這樣:

 

 

  

 

  當用戶進程調用了recvfrom這個系統調用,kernel就開始了IO的第一個階段:準備數據。對於network io來講,不少時候數據在一開始尚未到達(好比,尚未收到一個完整的UDP包),這個時候kernel就要等待足夠的數據到來。

 

    而在用戶進程這邊,整個進程會被阻塞。當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶內存,而後kernel返回結果,用戶進程才解除block的狀態,從新運行起來。
    因此,blocking IO的特色就是在IO執行的兩個階段(等待數據和拷貝數據兩個階段)都被block了。

 

    幾乎全部的程序員第一次接觸到的網絡編程都是從listen()、send()、recv() 等接口開始的,使用這些接口能夠很方便的構建服務器/客戶機的模型。然而大部分的socket接口都是阻塞型的。以下圖

 

    

ps:所謂阻塞型接口是指系統調用(通常是IO接口)不返回調用結果並讓當前線程一直阻塞,只有當該系統調用得到結果或者超時出錯時才返回。

      

  實際上,除非特別指定,幾乎全部的IO接口 ( 包括socket接口 ) 都是阻塞型的。這給網絡編程帶來了一個很大的問題,如在調用recv(1024)的同時,線程將被阻塞,在此期間,線程將沒法執行任何運算或響應任何的網絡請求。

一個簡單的解決方案:

#在服務器端使用多線程(或多進程)。多線程(或多進程)的目的是讓每一個鏈接都擁有獨立的線程(或進程),這樣任何一個鏈接的阻塞都不會影響其餘的鏈接。

    該方案的問題是:

#開啓多進程或都線程的方式,在遇到要同時響應成百上千路的鏈接請求,則不管多線程仍是多進程都會嚴重佔據系統資源,下降系統對外界響應效率,並且線程與進程自己也更容易進入假死狀態。

    改進方案:    

#不少程序員可能會考慮使用「線程池」或「鏈接池」。
「線程池」旨在減小建立和銷燬線程的頻率,其維持必定合理數量的線程,並讓空閒的線程從新承擔新的執行任務。
「鏈接池」維持鏈接的緩存池,儘可能重用已有的鏈接、減小建立和關閉鏈接的頻率。
這兩種技術均可以很好的下降系統開銷,都被普遍應用不少大型系統,如websphere、tomcat和各類數據庫等。

  

    改進後方案其實也存在着問題:

#「線程池」和「鏈接池」技術也只是在必定程度上緩解了頻繁調用IO接口帶來的資源佔用。
並且,所謂「池」始終有其上限,當請求大大超過上限時,「池」構成的系統對外界的響應並不比沒有池的時候效果好多少。
因此使用「池」必須考慮其面臨的響應規模,並根據響應規模調整「池」的大小。

    對應上例中的所面臨的可能同時出現的上千甚至上萬次的客戶端請求,「線程池」或「鏈接池」或許能夠緩解部分壓力,可是不能解決全部問題。總之,多線程模型能夠方便高效的解決小規模的服務請求,但面對大規模的服務請求,多線程模型也會遇到瓶頸,能夠用非阻塞接口來嘗試解決這個問題。

 

非阻塞IO(non-blocking IO)

 

Linux下,能夠經過設置socket使其變爲non-blocking。當對一個non-blocking socket執行讀操做時,流程是這個樣子:

 

  

 

  從圖中能夠看出,當用戶進程發出read操做時,若是kernel中的數據尚未準備好,那麼它並不會block用戶進程,而是馬上返回一個error。從用戶進程角度講 ,它發起一個read操做後,並不須要等待,而是立刻就獲得了一個結果。用戶進程判斷結果是一個error時,它就知道數據尚未準備好,因而用戶就能夠在本次到下次再發起read詢問的時間間隔內作其餘事情,或者直接再次發送read操做。一旦kernel中的數據準備好了,而且又再次收到了用戶進程的system call,那麼它立刻就將數據拷貝到了用戶內存(這一階段仍然是阻塞的),而後返回。

 

    也就是說非阻塞的recvfrom系統調用調用以後,進程並無被阻塞,內核立刻返回給進程,若是數據還沒準備好,此時會返回一個error。進程在返回以後,能夠乾點別的事情,而後再發起recvfrom系統調用。重複上面的過程,循環往復的進行recvfrom系統調用。這個過程一般被稱之爲輪詢。輪詢檢查內核數據,直到數據準備好,再拷貝數據到進程,進行數據處理。須要注意,拷貝數據整個過程,進程仍然是屬於阻塞的狀態。 

 因此,在非阻塞式IO中,用戶進程實際上是須要不斷的主動詢問kernel數據準備好了沒有。

非阻塞IO的例子,
import socket
sk=socket.socket()
sk.bind(('127.0.0.1',9090))
sk.setblocking(False)
sk.listen()
conn_l=[]
del_l=[]
while True:
    try:
        conn,addr=sk.accept()
        conn_l.append(conn)
    except BlockingIOError as e:
        for conn in conn_l:
            try:
                ret=conn.recv(1024)
                if ret:
                    print(ret)
                    conn.send(b'hello')
                else:
                    conn.close()
                    del_l.append(conn)
            except (BlockingIOError,OSError) :pass
        for conn in del_l:
            conn_l.remove(conn)
        del_l.clear()



#client端

import socket
sk=socket.socket()
sk.connect(('127.0.0.1',9090))
for i in range(1000):
    sk.send(b'byebye')
    print(sk.recv(1024))
sk.close()
View Code
 

可是非阻塞IO模型毫不被推薦,

其優勢:可以在等待任務完成的時間裏幹其餘的活了,(包括提交其餘任務,)

缺點:循環調用recv()將大幅度提升推高cpu佔有率,任務完成的響應延遲增大了,由於每過一段時間纔去輪訓一次read操做,而任務可能在兩次輪訓之間的任意時間完成,這會致使總體數據吞吐量的下降.

 

 

多路複用IO(IO multiplexing)

咱們都知道,select/epoll的好處就在於單個process就能夠同時處理多個網絡鏈接的IO。它的基本原理就是select/epoll這個function會不斷的輪詢所負責的全部socket,當某個socket有數據到達了,就通知用戶進程。它的流程如圖:

  當用戶進程調用了select,那麼整個進程會被block,而同時,kernel會「監視」全部select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操做,將數據從kernel拷貝到用戶進程。
    這個圖和blocking IO的圖其實並無太大的不一樣,事實上還更差一些。由於這裏須要使用兩個系統調用(select和recvfrom),而blocking IO只調用了一個系統調用(recvfrom)。可是,用select的優點在於它能夠同時處理多個connection。

    強調:

 1. 若是處理的鏈接數不是很高的話,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優點並非對於單個鏈接能處理得更快,而是在於能處理更多的鏈接。

    2. 在多路複用模型中,對於每個socket,通常都設置成爲non-blocking,可是,如上圖所示,整個用戶的process實際上是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。

    結論: select的優點在於能夠處理多個鏈接,不適用於單個鏈接 

 
#服務端
from socket import *
import select

s=socket(AF_INET,SOCK_STREAM)
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind(('127.0.0.1',8081))
s.listen(5)
s.setblocking(False) #設置socket的接口爲非阻塞
read_l=[s,]
while True:
    r_l,w_l,x_l=select.select(read_l,[],[])
    print(r_l)
    for ready_obj in r_l:
        if ready_obj == s:
            conn,addr=ready_obj.accept() #此時的ready_obj等於s
            read_l.append(conn)
        else:
            try:
                data=ready_obj.recv(1024) #此時的ready_obj等於conn
                if not data:
                    ready_obj.close()
                    read_l.remove(ready_obj)
                    continue
                ready_obj.send(data.upper())
            except ConnectionResetError:
                ready_obj.close()
                read_l.remove(ready_obj)

#客戶端
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',8081))

while True:
    msg=input('>>: ')
    if not msg:continue
    c.send(msg.encode('utf-8'))
    data=c.recv(1024)
    print(data.decode('utf-8'))
 
什麼叫IO多路複用
io多路複用是操做系統提供的一種 監聽 網絡IO操做的機制
監聽三個列表
當某一個列表有對應的事件發生的時候
操做系統通知應用程序
操做系統根據返回的內容作具體的操做
對於只有一個對象須要監聽的狀況 IO多路複用並沒有法發揮做用
對於併發接收網絡請求的應用場景 IO多路複用能夠幫助你在節省CPU利用率和操做系統調用的基礎上完成併發需求



IO多路複用
select 是windows上的機制 輪詢的方式來監聽每個對象是否有對應的事件發生的,數據越多延遲越大
可以處理的對象數是有限的
poll linux 和select的機制基本一致,對底層存儲被監聽對象的數據結構作了優化
可以處理的對象個數增長了
epoll linux 採用了回調函數的方式來通知應用被監聽的對象有事件發生了
 

異步IO(Asynchronous I/O)

 

Linux下的asynchronous IO其實用得很少,從內核2.6版本纔開始引入。先看一下它的流程:

 

 

  用戶進程發起read操做以後,馬上就能夠開始去作其它的事。而另外一方面,從kernel的角度,當它受到一個asynchronous read以後,首先它會馬上返回,因此不會對用戶進程產生任何block。而後,kernel會等待數據準備完成,而後將數據拷貝到用戶內存,當這一切都完成以後,kernel會給用戶進程發送一個signal,告訴它read操做完成了。

 

IO模型比較分析

 

blocking vs non-blocking。調用blocking IO會一直block住對應的進程直到操做完成,而non-blocking IO在kernel還準備數據的狀況下會馬上返回。

 

    再說明synchronous IO和asynchronous IO的區別以前,須要先給出二者的定義。Stevens給出的定義(實際上是POSIX的定義)是這樣子的:
    A synchronous I/O operation causes the requesting process to be blocked until that I/O operationcompletes;
    An asynchronous I/O operation does not cause the requesting process to be blocked; 
    二者的區別就在於synchronous IO作」IO operation」的時候會將process阻塞。按照這個定義,四個IO模型能夠分爲兩大類,以前所述的blocking IO,non-blocking IO,IO multiplexing都屬於synchronous IO這一類,而 asynchronous I/O後一類 。

 

    有人可能會說,non-blocking IO並無被block啊。這裏有個很是「狡猾」的地方,定義中所指的」IO operation」是指真實的IO操做,就是例子中的recvfrom這個system call。non-blocking IO在執行recvfrom這個system call的時候,若是kernel的數據沒有準備好,這時候不會block進程。可是,當kernel中數據準備好的時候,recvfrom會將數據從kernel拷貝到用戶內存中,這個時候進程是被block了,在這段時間內,進程是被block的。而asynchronous IO則不同,當進程發起IO 操做以後,就直接返回不再理睬了,直到kernel發送一個信號,告訴進程說IO完成。在這整個過程當中,進程徹底沒有被block。


通過上面的介紹,會發現non-blocking IO和asynchronous IO的區別仍是很明顯的。
在non-blocking IO中,雖然進程大部分時間都不會被block,
可是它仍然要求進程去主動的check,而且當數據準備完成之後,也須要進程主動的再次調用recvfrom來將數據拷貝到用戶內存。

而asynchronous IO則徹底不一樣。
它就像是用戶進程將整個IO操做交給了他人(kernel)完成,而後他人作完後發信號通知。
在此期間,用戶進程不須要去檢查IO操做的狀態,也不須要主動的去拷貝數據。



IO複用:爲了解釋這個名詞,首先來理解下複用這個概念,複用也就是共用的意思,這樣理解仍是有些抽象,爲此,我們來理解下複用在通訊領域的使用,在通訊領域中爲了充分利用網絡鏈接的物理介質,每每在同一條網絡鏈路上採用時分複用或頻分複用的技術使其在同一鏈路上傳輸多路信號,到這裏咱們就基本上理解了複用的含義,即公用某個「介質」來儘量多的作同一類(性質)的事,那IO複用的「介質」是什麼呢?爲此咱們首先來看看服務器編程的模型,客戶端發來的請求服務端會產生一個進程來對其進行服務,每當來一個客戶請求就產生一個進程來服務,然而進程不可能無限制的產生,所以爲了解決大量客戶端訪問的問題,引入了IO複用技術,即:一個進程能夠同時對多個客戶請求進行服務。也就是說IO複用的「介質」是進程(準確的說複用的是select和poll,由於進程也是靠調用select和poll來實現的),複用一個進程(select和poll)來對多個IO進行服務,雖然客戶端發來的IO是併發的可是IO所需的讀寫數據多數狀況下是沒有準備好的,所以就能夠利用一個函數(select和poll)來監聽IO所需的這些數據的狀態,一旦IO有數據能夠進行讀寫了,進程就來對這樣的IO進行服務。

  

理解完IO複用後,咱們在來看下實現IO複用中的三個API(select、poll和epoll)的區別和聯繫

select,poll,epoll都是IO多路複用的機制,I/O多路複用就是經過一種機制,能夠監視多個描述符,一旦某個描述符就緒(通常是讀就緒或者寫就緒),可以通知應用程序進行相應的讀寫操做。但select,poll,epoll本質上都是同步I/O,由於他們都須要在讀寫事件就緒後本身負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需本身負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。三者的原型以下所示: int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); int poll(struct pollfd *fds, nfds_t nfds, int timeout); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 1.select的第一個參數nfds爲fdset集合中最大描述符值加1,fdset是一個位數組,其大小限制爲__FD_SETSIZE(1024),位數組的每一位表明其對應的描述符是否須要被檢查。第二三四參數表示須要關注讀、寫、錯誤事件的文件描述符位數組,這些參數既是輸入參數也是輸出參數,可能會被內核修改用於標示哪些描述符上發生了關注的事件,因此每次調用select前都須要從新初始化fdset。timeout參數爲超時時間,該結構會被內核修改,其值爲超時剩餘的時間。 select的調用步驟以下: (1)使用copy_from_user從用戶空間拷貝fdset到內核空間 (2)註冊回調函數__pollwait (3)遍歷全部fd,調用其對應的poll方法(對於socket,這個poll方法是sock_poll,sock_poll根據狀況會調用到tcp_poll,udp_poll或者datagram_poll) (4)以tcp_poll爲例,其核心實現就是__pollwait,也就是上面註冊的回調函數。 (5)__pollwait的主要工做就是把current(當前進程)掛到設備的等待隊列中,不一樣的設備有不一樣的等待隊列,對於tcp_poll 來講,其等待隊列是sk->sk_sleep(注意把進程掛到等待隊列中並不表明進程已經睡眠了)。在設備收到一條消息(網絡設備)或填寫完文件數 據(磁盤設備)後,會喚醒設備等待隊列上睡眠的進程,這時current便被喚醒了。 (6)poll方法返回時會返回一個描述讀寫操做是否就緒的mask掩碼,根據這個mask掩碼給fd_set賦值。 (7)若是遍歷完全部的fd,尚未返回一個可讀寫的mask掩碼,則會調用schedule_timeout是調用select的進程(也就是 current)進入睡眠。當設備驅動發生自身資源可讀寫後,會喚醒其等待隊列上睡眠的進程。若是超過必定的超時時間(schedule_timeout 指定),仍是沒人喚醒,則調用select的進程會從新被喚醒得到CPU,進而從新遍歷fd,判斷有沒有就緒的fd。 (8)把fd_set從內核空間拷貝到用戶空間。 總結下select的幾大缺點: (1)每次調用select,都須要把fd集合從用戶態拷貝到內核態,這個開銷在fd不少時會很大 (2)同時每次調用select都須要在內核遍歷傳遞進來的全部fd,這個開銷在fd不少時也很大 (3)select支持的文件描述符數量過小了,默認是1024 2. poll與select不一樣,經過一個pollfd數組向內核傳遞須要關注的事件,故沒有描述符個數的限制,pollfd中的events字段和revents分別用於標示關注的事件和發生的事件,故pollfd數組只須要被初始化一次。 poll的實現機制與select相似,其對應內核中的sys_poll,只不過poll向內核傳遞pollfd數組,而後對pollfd中的每一個描述符進行poll,相比處理fdset來講,poll效率更高。poll返回後,須要對pollfd中的每一個元素檢查其revents值,來得指事件是否發生。 3.直到Linux2.6纔出現了由內核直接支持的實現方法,那就是epoll,被公認爲Linux2.6下性能最好的多路I/O就緒通知方法。epoll能夠同時支持水平觸發和邊緣觸發(Edge Triggered,只告訴進程哪些文件描述符剛剛變爲就緒狀態,它只說一遍,若是咱們沒有采起行動,那麼它將不會再次告知,這種方式稱爲邊緣觸發),理論上邊緣觸發的性能要更高一些,可是代碼實現至關複雜。epoll一樣只告知那些就緒的文件描述符,並且當咱們調用epoll_wait()得到就緒文件描述符時,返回的不是實際的描述符,而是一個表明就緒描述符數量的值,你只須要去epoll指定的一個數組中依次取得相應數量的文件描述符便可,這裏也使用了內存映射(mmap)技術,這樣便完全省掉了這些文件描述符在系統調用時複製的開銷。另外一個本質的改進在於epoll採用基於事件的就緒通知方式。在select/poll中,進程只有在調用必定的方法後,內核纔對全部監視的文件描述符進行掃描,而epoll事先經過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會採用相似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便獲得通知。 epoll既然是對select和poll的改進,就應該能避免上述的三個缺點。那epoll都是怎麼解決的呢?在此以前,咱們先看一下epoll 和select和poll的調用接口上的不一樣,select和poll都只提供了一個函數——select或者poll函數。而epoll提供了三個函 數,epoll_create,epoll_ctl和epoll_wait,epoll_create是建立一個epoll句柄;epoll_ctl是注 冊要監聽的事件類型;epoll_wait則是等待事件的產生。   對於第一個缺點,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察看,通常來講這個數目和系統內存關係很大。 總結: (1)select,poll實現須要本身不斷輪詢全部fd集合,直到設備就緒,期間可能要睡眠和喚醒屢次交替。而epoll其實也須要調用 epoll_wait不斷輪詢就緒鏈表,期間也可能屢次睡眠和喚醒交替,可是它是設備就緒時,調用回調函數,把就緒fd放入就緒鏈表中,並喚醒在 epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,可是select和poll在「醒着」的時候要遍歷整個fd集合,而epoll在「醒着」的 時候只要判斷一下就緒鏈表是否爲空就好了,這節省了大量的CPU時間,這就是回調機制帶來的性能提高。 (2)select,poll每次調用都要把fd集合從用戶態往內核態拷貝一次,而且要把current往設備等待隊列中掛一次,而epoll只要 一次拷貝,並且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這裏的等待隊列並非設備等待隊列,只是一個epoll內 部定義的等待隊列),這也能節省很多的開銷。
相關文章
相關標籤/搜索