時間驅動編程是一種編程範式,這裏程序的執行流由外部事件來決定.它的特色是包含一個事件循環,當外部事件發生時使用回調機制來觸發相應的處理.另外兩種常見的編程範式是(單線程)同步以及多線程編程linux
注意,事件驅動的監聽事件是由操做系統調用的cpu來完成web
單線程下實現併發效果就是IO多路複用編程
如今操做系統都是採用虛擬存儲器,那麼對32位操做系統而言,它的尋址空間(虛擬存儲空間)爲4G(2的32次方)。
操做系統的核心是內核,獨立於普通的應用程序,能夠訪問受保護的內存空間,也有訪問底層硬件設備的全部權限。
爲了保證用戶進程不能直接操做內核(kernel),保證內核的安全,操心繫統將虛擬空間劃分爲兩部分,一部分爲內核空間,一部分爲用戶空間。
針對linux操做系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱爲內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱爲用戶空間。windows
爲了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復之前掛起的某個進程的執行。這種行爲被稱爲進程切換,這種切換是由操做系統來完成的。所以能夠說,任何進程都是在操做系統內核的支持下運行的,是與內核緊密相關的。
從一個進程的運行轉到另外一個進程上運行,這個過程當中通過下面這些變化:緩存
保存處理機上下文,包括程序計數器和其餘寄存器。安全
更新PCB信息。網絡
把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。數據結構
選擇另外一個進程執行,並更新其PCB。多線程
更新內存管理的數據結構。併發
恢復處理機上下文。
注:總而言之就是很耗資源的
正在執行的進程,因爲期待的某些事件未發生,如請求系統資源失敗、等待某種操做的完成、新數據還沒有到達或無新工做作等,則由系統自動執行阻塞原語(Block),使本身由運行狀態變爲阻塞狀態。可見,進程的阻塞是進程自身的一種主動行爲,也所以只有處於運行態的進程(得到CPU),纔可能將其轉爲阻塞狀態。當進程進入阻塞狀態,是不佔用CPU資源的。
文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。
文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者建立一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫每每會圍繞着文件描述符展開。可是文件描述符這一律念每每只適用於UNIX、Linux這樣的操做系統。
緩存 I/O 又被稱做標準 I/O,大多數文件系統的默認 I/O 操做都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操做系統會將 I/O 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。用戶空間無法直接訪問內核空間的,內核態到用戶態的數據拷貝
在linux中,默認狀況下全部的socket都是blocking,一個典型的讀操做流程大概是這樣:
當用戶進程調用了recvfrom這個系統調用,kernel就開始了IO的第一個階段:準備數據。對於network io來講,不少時候數據在一開始尚未到達(好比,尚未收到一個完整的UDP包),這個時候kernel就要等待足夠的數據到來。而在用戶進程這邊,整個進程會被阻塞。當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶內存,而後kernel返回結果,用戶進程才解除block的狀態,從新運行起來。
因此,blocking IO的特色就是在IO執行的兩個階段都被block了。
linux下,能夠經過設置socket使其變爲non-blocking。當對一個non-blocking socket執行讀操做時,流程是這個樣子:
從圖中能夠看出,當用戶進程發出read操做時,若是kernel中的數據尚未準備好,那麼它並不會block用戶進程,而是馬上返回一個error。從用戶進程角度講 ,它發起一個read操做後,並不須要等待,而是立刻就獲得了一個結果。用戶進程判斷結果是一個error時,它就知道數據尚未準備好,因而它能夠再次發送read操做。一旦kernel中的數據準備好了,而且又再次收到了用戶進程的system call,那麼它立刻就將數據拷貝到了用戶內存,而後返回。
因此,用戶進程實際上是須要不斷的主動詢問kernel數據好了沒有。
注意:
在網絡IO時候,非阻塞IO也會進行recvform系統調用,檢查數據是否準備好,與阻塞IO不同,」非阻塞將大的整片時間的阻塞分紅N多的小的阻塞, 因此進程不斷地有機會 ‘被’ CPU光顧」。即每次recvform系統調用之間,cpu的權限還在進程手中,這段時間是能夠作其餘事情的,
也就是說非阻塞的recvform系統調用調用以後,進程並無被阻塞,內核立刻返回給進程,若是數據還沒準備好,此時會返回一個error。進程在返回以後,能夠乾點別的事情,而後再發起recvform系統調用。重複上面的過程,循環往復的進行recvform系統調用。這個過程一般被稱之爲輪詢。輪詢檢查內核數據,直到數據準備好,再拷貝數據到進程,進行數據處理。須要注意,拷貝數據整個過程,進程仍然是屬於阻塞的狀態.
IO multiplexing這個詞可能有點陌生,可是若是我說select,epoll,大概就都能明白了。有些地方也稱這種IO方式爲event driven IO。咱們都知道,select/epoll的好處就在於單個process就能夠同時處理多個網絡鏈接的IO。它的基本原理就是select/epoll這個function會不斷的輪詢所負責的全部socket,當某個socket有數據到達了,就通知用戶進程。它的流程如圖:
當用戶進程調用了select,那麼整個進程會被block,而同時,kernel會「監視」全部select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操做,將數據從kernel拷貝到用戶進程。
這個圖和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。
注意1:select函數返回結果中若是有文件可讀了,那麼進程就能夠經過調用accept()或recv()來讓kernel將位於內核中準備到的數據copy到用戶區。
注意2: select的優點在於能夠處理多個鏈接,不適用於單個鏈接
linux下的asynchronous IO其實用得不多。先看一下它的流程:
用戶進程發起read操做以後,馬上就能夠開始去作其它的事。而另外一方面,從kernel的角度,當它受到一個asynchronous read以後,首先它會馬上返回,因此不會對用戶進程產生任何block。而後,kernel會等待數據準備完成,而後將數據拷貝到用戶內存,當這一切都完成以後,kernel會給用戶進程發送一個signal,告訴它read操做完成了。
到目前爲止,已經將四個IO Model都介紹完了。如今回過頭來回答最初的那幾個問題:blocking和non-blocking的區別在哪,synchronous IO和asynchronous 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阻塞。按照這個定義,以前所述的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。
注意:因爲我們接下來要講的select,poll,epoll都屬於IO多路複用,而IO多路複用又屬於同步的範疇,故,epoll只是一個僞異步而已。
各個IO Model的比較如圖所示:
通過上面的介紹,會發現non-blocking IO和asynchronous IO的區別仍是很明顯的。在non-blocking IO中,雖然進程大部分時間都不會被block,可是它仍然要求進程去主動的check,而且當數據準備完成之後,也須要進程主動的再次調用recvfrom來將數據拷貝到用戶內存。而asynchronous IO則徹底不一樣。它就像是用戶進程將整個IO操做交給了他人(kernel)完成,而後他人作完後發信號通知。在此期間,用戶進程不須要去檢查IO操做的狀態,也不須要主動的去拷貝數據。
五種IO模型比較:
select:select目前幾乎在全部的平臺上支持
select的一個缺點在於單個進程可以監視的文件描述符的數量存在最大限制,在Linux上通常爲1024,不過能夠經過修改宏定義甚至從新編譯內核的方式提高這一限制。
另外,select()所維護的存儲大量文件描述符的數據結構,隨着文件描述符數量的增大,其複製的開銷也線性增加。同時,因爲網絡響應時間的延遲使得大量TCP鏈接處於非活躍狀態,但調用select()會對全部socket進行一次線性掃描,因此這也浪費了必定的開銷。
poll:它和select在本質上沒有多大差異,可是poll沒有最大文件描述符數量的限制。
通常也不用它,至關於過渡階段
epoll:直到Linux2.6纔出現了由內核直接支持的實現方法,那就是epoll。被公認爲Linux2.6下性能最好的多路I/O就緒通知方法。windows不支持
沒有最大文件描述符數量的限制。
好比100個鏈接,有兩個活躍了,epoll會告訴用戶這兩個兩個活躍了,直接取就ok了,而select是循環一遍。
(瞭解)epoll能夠同時支持水平觸發和邊緣觸發(Edge Triggered,只告訴進程哪些文件描述符剛剛變爲就緒狀態,它只說一遍,若是咱們沒有采起行動,那麼它將不會再次告知,這種方式稱爲邊緣觸發),理論上邊緣觸發的性能要更高一些,可是代碼實現至關複雜。
另外一個本質的改進在於epoll採用基於事件的就緒通知方式。在select/poll中,進程只有在調用必定的方法後,內核纔對全部監視的文件描述符進行掃描,而epoll事先經過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會採用相似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便獲得通知。
水平觸發:水平觸發:若是文件描述符已經就緒能夠非阻塞的執行IO操做了,此時會觸發通知.容許在任意時刻重複檢測IO的狀態,沒有必要每次描述符就緒後儘量多的執行 IO.select,poll就屬於水平觸發.
邊緣觸發:邊緣觸發:若是文件描述符自上次狀態改變後有新的IO活動到來,此時會觸發通知.在收到一個IO事件通知後要儘量多的執行IO操做,由於若是在一次通知中沒有執行 完IO那麼就須要等到下一次新的IO活動到來才能獲取到就緒的描述符.信號驅動式IO就屬於邊緣觸發.
epoll既能夠採用水平觸發,也能夠採用邊緣觸發.
水平觸發:也就是隻有高電平(1)或低電平(0)時才觸發通知,只要在這兩種狀態就能獲得通知.上面提到的只要 有數據可讀(描述符就緒)那麼水平觸發的epoll就當即返回.
邊緣觸發:只有電平發生變化(高電平到低電平,或者低電平到高電平)的時候才觸發通知.上面提到即便有數據 可讀,可是沒有新的IO活動到來,epoll也不會當即返回.
1 import time 2 import socket 3 sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 4 sk.setsockopt 5 sk.bind(('127.0.0.1',6667)) 6 sk.listen(5) 7 sk.setblocking(False) 8 while True: 9 try: 10 print ('waiting client connection .......') 11 connection,address = sk.accept() # 進程主動輪詢 12 print("+++",address) 13 client_messge = connection.recv(1024) 14 print(str(client_messge,'utf8')) 15 connection.close() 16 except Exception as e: 17 print (e) 18 time.sleep(4) 19 20 #############################client 21 22 import time 23 import socket 24 sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 25 26 while True: 27 sk.connect(('127.0.0.1',6667)) 28 print("hello") 29 sk.sendall(bytes("hello","utf8")) 30 time.sleep(2) 31 break 32 33 實例1(non-blocking IO):
優勢:可以在等待任務完成的時間裏幹其餘活了(包括提交其餘任務,也就是 「後臺」 能夠有多個任務在同時執行)。
缺點:任務完成的響應延遲增大了,由於每過一段時間纔去輪詢一次read操做,而任務可能在兩次輪詢之間的任意時間完成。這會致使總體數據吞吐量的下降
實例2(IO multiplexing):
在非阻塞實例中,輪詢的主語是進程,而「後臺」 可能有多個任務在同時進行,人們就想到了循環查詢多個任務的完成狀態,只要有任何一個任務完成,就去處理它。不過,這個監聽的重任經過調用select等函數交給了內核去作。IO多路複用有兩個特別的系統調用select、poll、epoll函數。select調用是內核級別的,select輪詢相對非阻塞的輪詢的區別在於—前者能夠等待多個socket,能實現同時對多個IO端口進行監聽,當其中任何一個socket的數據準好了,就能返回進行可讀,而後進程再進行recvfrom系統調用,將數據由內核拷貝到用戶進程,固然這個過程是阻塞的。
1 import socket 2 import select 3 sk=socket.socket() 4 sk.bind(("127.0.0.1",8801)) 5 sk.listen(5) 6 inputs=[sk,] 7 while True: 8 r,w,e=select.select(inputs,[],[],5) 9 print(len(r)) 10 11 for obj in r: 12 if obj==sk: 13 conn,add=obj.accept() 14 print(conn) 15 inputs.append(conn) 16 else: 17 data_byte=obj.recv(1024) 18 print(str(data_byte,'utf8')) 19 inp=input('回答%s號客戶>>>'%inputs.index(obj)) 20 obj.sendall(bytes(inp,'utf8')) 21 22 print('>>',r) 23 24 #***********************client.py 25 26 import socket 27 sk=socket.socket() 28 sk.connect(('127.0.0.1',8801)) 29 30 while True: 31 inp=input(">>>>") 32 sk.sendall(bytes(inp,"utf8")) 33 data=sk.recv(1024) 34 print(str(data,'utf8')) 35 36 select 實現併發聊天
select屬於水平觸發
(1) 若是內核緩衝區沒有數據--->等待--->數據到了內核緩衝區,轉到用戶進程緩衝區;
(2) 若是先用select監聽到某個文件描述符對應的內核緩衝區有了數據,當咱們再調用accept或recv時,直接將數據轉到用戶緩衝區。
1 import selectors 2 import socket 3 sel = selectors.DefaultSelector() #獲得一個對象,能夠自主根據不一樣的平臺選擇不一樣的監聽方式 4 5 def accept(sock,mask): 6 conn,addr = sock.accept() 7 conn.setblocking(False) 8 sel.register(conn,selectors.EVENT_READ,read) #將conn與read綁定 9 10 def read(conn,mask): 11 try: 12 date = conn.recv(1024) 13 if not date: 14 raise Exception 15 date = date.decode("utf8") 16 print("收到客戶端發來的消息",date) 17 date = date.upper() 18 conn.send(date.encode("utf-8")) 19 except Exception as e: 20 print("closing",conn) 21 sel.unregister(conn) 22 conn.close() 23 24 25 sock = socket.socket() 26 sock.bind(("127.0.0.1",8090)) 27 sock.listen(5) #監聽100個接入用戶 28 sock.setblocking(False) #設置非阻塞io 29 sel.register(sock,selectors.EVENT_READ,accept) #綁定操做 30 print("sever...") 31 32 while True: 33 events = sel.select() 34 for key,mask in events: 35 callback = key.data 36 callback(key.fileobj,mask) 37 *********************************************** 38 from socket import * 39 ip_port = ("127.0.0.1",8090) 40 back_log = 5 41 buffer_size = 1024 42 43 phone = socket(AF_INET,SOCK_STREAM) #建立客戶端套接字 44 phone.connect(ip_port) #嘗試連接服務端 45 46 while True: #通信循環 47 use_choose = input("請輸入信息》》》:").strip() 48 if not use_choose:continue 49 phone.send(use_choose.encode("utf-8")) #發送消息至服務端 50 print("客戶端已發送消息!") 51 date = phone.recv(buffer_size) #接收服務端返回的消息 52 print("收到服務端發來的消息:",date.decode("utf-8")) 53 54 selectors實例
selectors屬於水平觸發