1. 程式人生 > >Python Day10(補充)

Python Day10(補充)

設置 系統內存 無需 should 重新 append 同步io dem 發生

一、阻塞IO、非阻塞IO、IO多路復用、信號驅動IO、異步IO

同步IO和異步IO,阻塞IO和非阻塞IO分別是什麽,到底有什麽區別?不同的人在不同的上下文下給出的答案是不同的。所以先限定一下本文的上下文。 本文討論的背景是Linux環境下的network IO。
1.概念說明 在進行解釋之前,首先要說明幾個概念:
  • 用戶空間和內核空間
  • 進程切換
  • 進程的阻塞
  • 文件描述符
  • 緩存 I/O
用戶空間與內核空間 現在操作系統都是采用虛擬存儲器,那麽對32位操作系統而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方)。操作系統的核心是內核,獨立於普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。為了保證用戶進程不能直接操作內核(kernel),保證內核的安全,操心系統將虛擬空間劃分為兩部分,一部分為內核空間,一部分為用戶空間。針對linux操作系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱為內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱為用戶空間。 即:操作系統有自己的內存空間,用戶有自己的內存空間,用戶是不能直接訪問操作系統的內存空間的。
進程切換 為了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復以前掛起的某個進程的執行。這種行為被稱為進程切換。因此可以說,任何進程都是在操作系統內核的支持下運行的,是與內核緊密相關的。 從一個進程的運行轉到另一個進程上運行,這個過程中經過下面這些變化: 1. 保存處理機上下文,包括程序計數器和其他寄存器。 2. 更新PCB信息。 3. 把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。 4. 選擇另一個進程執行,並更新其PCB。 5. 更新內存管理的數據結構。 6. 恢復處理機上下文。 總而言之就是很耗資源,具體的可以參考這篇文章:<http://guojing.me/linux-kernel-architecture/posts/process-switch/> 註:進程控制塊(Processing Control Block),是操作系統核心中一種數據結構,主要表示進程狀態。其作用是使一個在多道程序環境下不能獨立運行的程序(含數據),成為一個能獨立運行的基本單位或與其它進程並發執行的進程。或者說,OS是根據PCB來對並發執行的進程進行控制和管理的。 PCB通常是系統內存占用區中的一個連續存區,它存放著操作系統用於描述進程情況及控制進程運行所需的全部信息
進程的阻塞 正在執行的進程,由於期待的某些事件未發生,如請求系統資源失敗、等待某種操作的完成、新數據尚未到達或無新工作做等,則由系統自動執行阻塞原語(Block),使自己由運行狀態變為阻塞狀態。可見,進程的阻塞是進程自身的一種主動行為,也因此只有處於運行態的進程(獲得CPU),才可能將其轉為阻塞狀態。當進程進入阻塞狀態,是不占用CPU資源的。 文件描述符fd 文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。 文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核為每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞著文件描述符展開。但是文件描述符這一概念往往只適用於UNIX、Linux這樣的操作系統。 緩存 I/O
緩存 I/O 又被稱作標準 I/O,大多數文件系統的默認 I/O 操作都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操作系統會將 I/O 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操作系統內核的緩沖區中,然後才會從操作系統內核的緩沖區拷貝到應用程序的地址空間。 緩存 I/O 的缺點: 數據在傳輸過程中需要在應用程序地址空間和內核進行多次數據拷貝操作,這些數據拷貝操作所帶來的 CPU 以及內存開銷是非常大的。
2.IO模式 對於一次IO訪問(以read舉例),數據會先被拷貝到操作系統內核的緩沖區中,然後才會從操作系統內核的緩沖區拷貝到應用程序的地址空間。所以說,當一個read操作發生時,它會經歷兩個階段:
  1. 等待數據準備 (Waiting for the data to be ready)
  2. 將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)
正式因為這兩個階段,linux系統產生了下面五種網絡模式的方案。
  • 阻塞 I/O(blocking IO)
  • 非阻塞 I/O(nonblocking IO)
  • I/O 多路復用( IO multiplexing)
  • 信號驅動 I/O( signal driven IO)
  • 異步 I/O(asynchronous IO)
註:由於signal driven IO在實際中並不常用,所以以下只提及剩下的四種IO Model。 阻塞 I/O(blocking IO) 在linux中,默認情況下所有的socket都是blocking,一個典型的讀操作流程大概是這樣: 技術分享

當用戶進程調用了recvfrom這個系統調用,kernel就開始了IO的第一個階段:準備數據(對於網絡IO來說,很多時候數據在一開始還沒有到達。比如,還沒有收到一個完整的UDP包。這個時候kernel就要等待足夠的數據到來)。這個過程需要等待,也就是說數據被拷貝到操作系統內核的緩沖區中是需要一個過程的。而在用戶進程這邊,整個進程會被阻塞(當然,是進程自己選擇的阻塞)。當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶內存,然後kernel返回結果,用戶進程才解除block的狀態,重新運行起來。 所以,blocking IO的特點就是在IO執行的兩個階段都被block了。 非阻塞 I/O(nonblocking IO) linux下,可以通過設置socket使其變為non-blocking。當對一個non-blocking socket執行讀操作時,流程是這個樣子: 技術分享

當用戶進程發出read操作時,如果kernel中的數據還沒有準備好,那麽它並不會block用戶進程,而是立刻返回一個error。從用戶進程角度講 ,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。用戶進程判斷結果是一個error時,它就知道數據還沒有準備好,於是它可以再次發送read操作。一旦kernel中的數據準備好了,並且又再次收到了用戶進程的system call,那麽它馬上就將數據拷貝到了用戶內存,然後返回。 所以,nonblocking IO的特點是用戶進程需要不斷的主動詢問kernel數據好了沒有。 I/O 多路復用( IO multiplexing) IO multiplexing就是我們說的select,poll,epoll,有些地方也稱這種IO方式為event driven IO。select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。 技術分享

當用戶進程調用了select,那麽整個進程會被block,而同時,kernel會“監視”所有select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。 所以,I/O 多路復用的特點是通過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函數就可以返回。 這個圖和blocking IO的圖其實並沒有太大的不同,事實上,還更差一些。因為這裏需要使用兩個system call (select 和 recvfrom),而blocking IO只調用了一個system call (recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。 所以,如果處理的連接數不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。) 在IO multiplexing Model中,實際中,對於每一個socket,一般都設置成為non-blocking,但是,如上圖所示,整個用戶的process其實是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。 異步 I/O(asynchronous IO) inux下的asynchronous IO其實用得很少。先看一下它的流程: 技術分享

用戶進程發起read操作之後,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對用戶進程產生任何block。然後,kernel會等待數據準備完成,然後將數據拷貝到用戶內存,當這一切都完成之後,kernel會給用戶進程發送一個signal,告訴它read操作完成了。 即:用戶去銀行辦一張銀行卡,只需要和銀行說一聲並告訴銀行用戶的地址,然後用戶就可以回家想幹嘛幹嘛了,銀行幫用戶辦好銀行卡後會直接順豐快遞到用戶家門口,用戶只需開個門簽收下就行。是不是很酷,但是底層實現比較復雜,而且操作系統對異步IO支持也不是很好,所以生產中很少用到。 總結: blocking和non-blocking的區別 調用blocking IO會一直block住對應的進程直到操作完成,而non-blocking IO在kernel還準備數據的情況下會立刻返回。 synchronous IO和asynchronous IO的區別 在說明synchronous IO和asynchronous IO的區別之前,需要先給出兩者的定義。POSIX的定義是這樣子的:
  • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
  • An asynchronous I/O operation does not cause the requesting process to be blocked;
兩者的區別就在於synchronous IO做”IO operation”的時候會將process阻塞。按照這個定義,之前所述的blocking IO,non-blocking IO,IO multiplexing都屬於synchronous IO。 有人會說,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。 各個IO Model的比較如圖所示: 技術分享

通過上面的圖片,可以發現non-blocking IO和asynchronous IO的區別還是很明顯的。在non-blocking IO中,雖然進程大部分時間都不會被block,但是它仍然要求進程去主動的check,並且當數據準備完成以後,也需要進程主動的再次調用recvfrom來將數據拷貝到用戶內存。而asynchronous IO則完全不同。它就像是用戶進程將整個IO操作交給了他人(kernel)完成,然後他人做完後發信號通知。在此期間,用戶進程不需要去檢查IO操作的狀態,也不需要主動的去拷貝數據。

二、I/O 多路復用之select、poll、epoll

select,poll,epoll都是IO多路復用的機制。I/O多路復用就是通過一種機制,一個進程可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。
sellect、poll、epoll三者的區別: sellect select最早於1983年出現在4.2BSD中,它通過一個select()系統調用來監視多個文件描述符的數組,當select()返回後,該數組中就緒的文件描述符便會被內核修改標誌位,使得進程可以獲得這些文件描述符從而進行後續的讀寫操作。 select目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優點,事實上從現在看來,這也是它所剩不多的優點之一。 select的一個缺點在於單個進程能夠監視的文件描述符的數量存在最大限制,在Linux上一般為1024,不過可以通過修改宏定義甚至重新編譯內核的方式提升這一限制。 另外,select()所維護的存儲大量文件描述符的數據結構,隨著文件描述符數量的增大,其復制的開銷也線性增長。同時,由於網絡響應時間的延遲使得大量TCP連接處於非活躍狀態,但調用select()會對所有socket進行一次線性掃描,所以這也浪費了一定的開銷。 poll poll在1986年誕生於System V Release 3,它和select在本質上沒有多大差別,但是poll沒有最大文件描述符數量的限制。 poll和select同樣存在一個缺點就是,包含大量文件描述符的數組被整體復制於用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨著文件描述符數量的增加而線性增大。 另外,select()和poll()將就緒的文件描述符告訴進程後,如果進程沒有對其進行IO操作,那麽下次調用select()和poll()的時候將再次報告這些文件描述符,所以它們一般不會丟失就緒的消息,這種方式稱為水平觸發(Level Triggered)。 epoll 直到Linux2.6才出現了由內核直接支持的實現方法,那就是epoll,它幾乎具備了之前所說的一切優點,被公認為Linux2.6下性能最好的多路I/O就緒通知方法。 epoll可以同時支持水平觸發和邊緣觸發(Edge Triggered,只告訴進程哪些文件描述符剛剛變為就緒狀態,它只說一遍,如果我們沒有采取行動,那麽它將不會再次告知,這種方式稱為邊緣觸發),理論上邊緣觸發的性能要更高一些,但是代碼實現相當復雜。 epoll同樣只告知那些就緒的文件描述符,而且當我們調用epoll_wait()獲得就緒文件描述符時,返回的不是實際的描述符,而是一個代表就緒描述符數量的值,你只需要去epoll指定的一個數組中依次取得相應數量的文件描述符即可,這裏也使用了內存映射(mmap)技術,這樣便徹底省掉了這些文件描述符在系統調用時復制的開銷。 另一個本質的改進在於epoll采用基於事件的就緒通知方式。在select/poll中,進程只有在調用一定的方法後,內核才對所有監視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會采用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知。
Python select Python中的select模塊專註於I/O多路復用,提供了select poll epoll三個方法(其中後兩個在Linux中可用,windows僅支持select,官方解釋:On Windows, only sockets are supported; on Unix, all file descriptors),另外也提供了kqueue方法(freeBSD系統) Python的select()方法直接調用操作系統的IO接口,它監控sockets,open files, and pipes(所有帶fileno()方法的文件句柄)何時變成readable 和writeable, 或者通信錯誤(異常),select()使得同時監控多個連接變的簡單,並且這比寫一個長循環來等待和監控多客戶端連接要高效,因為select直接通過操作系統提供的C的網絡接口進行操作,而不是通過Python的解釋器。 select方法 當我們使用select方法時: 進程指定內核監聽哪些文件描述符(最多監聽1024個fd)的哪些事件,當沒有文件描述符事件發生時,進程被阻塞;當一個或者多個文件描述符事件發生時,進程被喚醒。 具體過程大致如下:   1、調用select()方法,上下文切換轉換為內核態   2、將fd從用戶空間復制到內核空間   3、內核遍歷所有fd,查看其對應事件是否發生   4、如果沒發生,將進程阻塞,當設備驅動產生中斷或者timeout時間後,將進程喚醒,再次進行遍歷   5、返回遍歷後的fd   6、將fd從內核空間復制到用戶空間 使用方法: fd_r_list, fd_w_list, fd_e_list = select.select(rlist, wlist, xlist, [timeout]) 參數列表:
  • rlist: wait until ready for reading
  • wlist: wait until ready for writing
  • xlist: wait for an “exceptional condition”
  • timeout: 超時時間
返回三個值: select方法用來監視文件描述符(當文件描述符條件不滿足時,select會阻塞),當某個文件描述符狀態改變後,會返回三個列表 1、當參數1 序列中的fd滿足“可讀”條件時,則獲取發生變化的fd並添加到fd_r_list中 2、當參數2 序列中含有fd時,則將該序列中所有的fd添加到 fd_w_list中 3、當參數3 序列中的fd發生錯誤時,則將該發生錯誤的fd添加到 fd_e_list中 4、當超時時間為空,則select會一直阻塞,直到監聽的句柄發生變化 當超時時間 = n(正整數)時,那麽如果監聽的句柄均無任何變化,則select會阻塞n秒,之後返回三個空列表,如果監聽的句柄有變化,則直接執行。 demo:利用select方法實現一個高並發的socket服務端 技術分享
 1 import select
 2 import socket
 3 import queue
 4 
 5 server = socket.socket()
 6 server.bind(("localhost", 9000))
 7 server.listen(1000)
 8 
 9 msg_dict = {}  # 存放要發送的消息隊列
10 
11 inputs = [server, ]  # 要監測的實例
12 outputs = []
13 
14 while True:
15     readable, writeable, exceptional = select.select(inputs, outputs, inputs)
16 
17     for e in exceptional:  # 如果返回錯誤信息
18         if e in outputs:  # 如果連接在outputs裏則移除
19             outputs.remove(e)
20         inputs.remove(e)  # inputs裏的連接直接移除
21         del msg_dict[e]  # 刪除消息隊列
22 
23     for r in readable:
24         if r is server:  # 代表來了一個新連接
25             conn, addr = server.accept()
26             inputs.append(conn)  # 讓select監測新連接
27             msg_dict[conn] = queue.Queue()  # 生成一個新連接要發送數據的消息隊列
28         else:  # 代表連接接收到數據了
29             try:
30                 data = r.recv(1024)  # 收數據
31             except (ConnectionResetError, ConnectionAbortedError) as e:  # 出現異常,說明用戶斷開連接或者程序中止了一個連接
32                 print(e)
33                 if r in outputs:
34                     outputs.remove(r)
35                 inputs.remove(r)
36                 del msg_dict[r]  # 刪除消息隊列
37                 break
38             msg_dict[r].put(data)  # 將要發送的數據放入消息隊列中
39             outputs.append(r)  # 將連接放入outputs裏,下次select循環必然會返回此次的連接
40 
41     for w in writeable:  # 代表需要給客戶端返回數據
42         data = msg_dict[w].get()  # 獲取連接對應的消息隊列裏的消息
43         w.send(data)  # 給客戶端發送消息
44         outputs.remove(w)  # 從outputs裏移除已經發好消息的連接,這樣下次就不會重復發送消息了
View Code

復雜版:

技術分享
 1 import select
 2 import socket
 3 import queue
 4 
 5 server = socket.socket()
 6 server.setblocking(0)
 7 
 8 server_addr = (localhost, 9000)
 9 
10 print(starting up on %s port %s % server_addr)
11 server.bind(server_addr)
12 
13 server.listen(5)
14 
15 inputs = [server, ]  # 自己也要監測呀,因為server本身也是個fd
16 outputs = []
17 
18 message_queues = {}
19 
20 while True:
21     print("waiting for next event...")
22 
23     readable, writeable, exeptional = select.select(inputs, outputs, inputs)  # 如果沒有任何fd就緒,那程序就會一直阻塞在這裏
24 
25     for s in readable:  # 每個s就是一個socket
26 
27         if s is server:  # 別忘記,上面我們server自己也當做一個fd放在了inputs列表裏,傳給了select,如果這個s是server,代表server這個fd就緒了,
28             # 就是有活動了, 什麽情況下它才有活動? 當然 是有新連接進來的時候 呀
29             # 新連接進來了,接受這個連接
30             conn, client_addr = s.accept()
31             print("new connection from",client_addr)
32             conn.setblocking(0)
33             inputs.append(conn)  # 為了不阻塞整個程序,我們不會立刻在這裏開始接收客戶端發來的數據, 把它放到inputs裏, 下一次loop時,這個新連接
34             # 就會被交給select去監聽,如果這個連接的客戶端發來了數據 ,那這個連接的fd在server端就會變成就續的,select就會把這個連接返回,返回到
35             # readable 列表裏,然後你就可以loop readable列表,取出這個連接,開始接收數據了, 下面就是這麽幹 的
36 
37             message_queues[conn] = queue.Queue()  # 接收到客戶端的數據後,不立刻返回 ,暫存在隊列裏,以後發送
38 
39         else:  # s不是server的話,那就只能是一個 與客戶端建立的連接的fd了
40             # 客戶端的數據過來了,在這接收
41             try:
42                 data = s.recv(1024)
43             except (ConnectionResetError, ConnectionAbortedError) as e:  # 出現異常,說明用戶斷開連接或者程序中止了一個連接
44                 print("客戶端斷開了", s)
45                 if s in outputs:
46                     outputs.remove(s)  # 清理已斷開的連接
47                 inputs.remove(s)  # 清理已斷開的連接
48                 del message_queues[s]  # 清理已斷開的連接
49             else:  # 一切正常
50                 print("收到來自[%s]的數據:" % s.getpeername()[0], data)
51                 message_queues[s].put(data)  # 收到的數據先放到queue裏,一會返回給客戶端
52                 if s not in outputs:
53                     outputs.append(s)  # 為了不影響處理與其它客戶端的連接 , 這裏不立刻返回數據給客戶端
54 
55     for s in writeable:
56         try:
57             next_msg = message_queues[s].get_nowait()
58             s.send(next_msg.upper())
59         except queue.Empty:
60             print("client [%s]" % s.getpeername()[0], "queue is empty..")
61             outputs.remove(s)
62         except (ConnectionResetError, ConnectionAbortedError) as e:  # 出現異常,說明用戶斷開連接或者程序中止了一個連接
63             outputs.remove(s)
64         except KeyError:
65             if s in outputs:
66                 outputs.remove(s)
67         else:
68             print("sending msg to [%s]" % s.getpeername()[0], next_msg)
69             outputs.remove(s)
70 
71     for s in exeptional:
72         print("handling exception for ", s.getpeername())
73         inputs.remove(s)
74         if s in outputs:
75             outputs.remove(s)
76         s.close()
77         del message_queues[s]
View Code 在服務端我們可以看到,我們需要不停的調用select, 這就意味著:
  • 當文件描述符過多時,文件描述符在用戶空間與內核空間進行copy會很費時
  • 當文件描述符過多時,內核對文件描述符的遍歷也很浪費時間
  • select默認最大僅僅支持1024個文件描述符(在liunx上可以通過修改打開文件數來修改這個值)
poll方法 poll與select相差不大,相比於select而言,內核監測的文件描述不受限制。 epoll方法 epoll很好的改進了select:
  • epoll的解決方案在epoll_ctl函數中。每次註冊新的事件到epoll句柄中時,會把所有的fd拷貝進內核,而不是在epoll_wait的時候重復拷貝。epoll保證了每個fd在整個過程中只會拷貝一次。
  • epoll會在epoll_ctl時把指定的fd遍歷一遍(這一遍必不可少)並為每個fd指定一個回調函數,當設備就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的fd加入一個就緒鏈表。epoll_wait的工作實際上就是在這個就緒鏈表中查看有沒有就緒的fd
  • epoll對文件描述符沒有額外限制
具體方法:
  • select.epoll(sizehint=-1, flags=0) :創建epoll對象
  • epoll.close():關閉epoll對象的文件描述符
  • epoll.closed():檢測epoll對象是否關閉
  • epoll.fileno():返回epoll對象的文件描述符
  • epoll.fromfd(fd):根據指定的fd創建epoll對象
  • epoll.register(fd[, eventmask]):向epoll對象中註冊fd和對應的事件
  • epoll.modify(fd, eventmask):修改fd的事件
  • epoll.unregister(fd):Remove a registered file descriptor from the epoll object.取消註冊
  • epoll.poll(timeout=-1, maxevents=-1):Wait for events. timeout in seconds (float)阻塞,直到註冊的fd事件發生,會返回一個dict,格式為:{(fd1,event1),(fd2,event2),……(fdn,eventn)}
事件: EPOLLIN Available for read 可讀 狀態符為1 EPOLLOUT Available for write 可寫 狀態符為4 EPOLLPRI Urgent data for read EPOLLERR Error condition happened on the assoc. fd 發生錯誤 狀態符為8 EPOLLHUP Hang up happened on the assoc. fd 掛起狀態 EPOLLET Set Edge Trigger behavior, the default is Level Trigger behavior 默認為水平觸發,設置該事件後則邊緣觸發 EPOLLONESHOT Set one-shot behavior. After one event is pulled out, the fd is internally disabled EPOLLRDNORM Equivalent to EPOLLIN EPOLLRDBAND Priority data band can be read. EPOLLWRNORM Equivalent to EPOLLOUT EPOLLWRBAND Priority data may be written. EPOLLMSG Ignored. 關於水平觸發和邊緣觸發: Level_triggered(水平觸發,有時也稱條件觸發):當被監控的文件描述符上有可讀寫事件發生時,epoll.poll()會通知處理程序去讀寫。如果這次沒有把數據一次性全部讀寫完(如讀寫緩沖區太小),那麽下次調用 epoll.poll()時,它還會通知你在上沒讀寫完的文件描述符上繼續讀寫,當然如果你一直不去讀寫,它會一直通知你,如果系統中有大量你不需要讀寫的就緒文件描述符,而它們每次都會返回,這樣會大大降低處理程序檢索自己關心的就緒文件描述符的效率!!! 優點很明顯:穩定可靠 Edge_triggered(邊緣觸發,有時也稱狀態觸發):當被監控的文件描述符上有可讀寫事件發生時,epoll.poll()會通知處理程序去讀寫。如果這次沒有把數據全部讀寫完(如讀寫緩沖區太小),那麽下次調用epoll.poll()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現第二次可讀寫事件才會通知你,這種模式比水平觸發效率高,系統不會充斥大量你不關心的就緒文件描述符。缺點:某些條件下不可靠 epoll實例: server: 技術分享
 1 import socket
 2 import select
 3 
 4 s = socket.socket()
 5 s.bind((127.0.0.1,8888))
 6 s.listen(5)
 7 epoll_obj = select.epoll()
 8 epoll_obj.register(s,select.EPOLLIN)
 9 connections = {}
10 while True:
11     events = epoll_obj.poll()
12     for fd, event in events:
13         print(fd,event)
14         if fd == s.fileno():
15             conn, addr = s.accept()
16             connections[conn.fileno()] = conn
17             epoll_obj.register(conn,select.EPOLLIN)
18             msg = conn.recv(200)
19             conn.sendall(ok.encode())
20         else:
21             try:
22                 fd_obj = connections[fd]
23                 msg = fd_obj.recv(200)
24                 fd_obj.sendall(ok.encode())
25             except BrokenPipeError:
26                 epoll_obj.unregister(fd)
27                 connections[fd].close()
28                 del connections[fd]
29 
30 s.close()
31 epoll_obj.close()
View Code

client:

技術分享
 1 import socket
 2 
 3 s = socket.socket()
 4 s.connect((127.0.0.1, 9999))
 5 while True:
 6     input_msg = input(>>:)
 7     if len(input_msg) == 0:
 8         continue
 9     s.sendall(input_msg.encode())
10     msg = s.recv(1024)
11     print(msg.decode())
View Code selectors python3.4新增selectors模塊,封裝了select,高層次、高效率的I/O多路復用,它具有根據操作系統平臺選出最佳的IO多路機制,比如在win的系統上他默認的是select模式而在linux上它默認的epoll。 demo:
 1 import selectors
 2 import socket
 3 
 4 sel = selectors.DefaultSelector()
 5 
 6 def accept(sock, mask):
 7     conn, addr = sock.accept()  # Should be ready
 8     print(accepted, conn, from, addr)
 9     conn.setblocking(False)
10     sel.register(conn, selectors.EVENT_READ, read)
11 
12 def read(conn, mask):
13     try:
14         data = conn.recv(1000)  # Should be ready
15     except (ConnectionAbortedError, ConnectionResetError) as e:
16         print(e)
17         print(closing, conn)
18         sel.unregister(conn)
19         conn.close()
20     else:
21         print(echoing, repr(data), to, conn)
22         conn.send(data)  # Hope it won‘t block
23 
24 sock = socket.socket()
25 sock.bind((localhost, 9000))
26 sock.listen(100)
27 sock.setblocking(False)
28 sel.register(sock, selectors.EVENT_READ, accept)
29 
30 while True:
31     events = sel.select()
32     for key, mask in events:
33         callback = key.data
34         callback(key.fileobj, mask)

Python Day10(補充)