事件驅動屬於一種編程的範式,一種編程的風格,它擅長於處理一些未知的事件,經過綁定一個事件,外界觸發後激活這個事情,達到執行某些操做的目的。好比瀏覽器的onclick()事件 html
在進行解釋以前,首先要說明幾個概念:linux
用戶空間和內核空間 nginx
進程切換 編程
進程的阻塞 windows
文件描述符 數組
緩存 I/O瀏覽器
用戶空間與內核空間 緩存
如今操做系統都是採用虛擬存儲器,那麼對32位操做系統而言,它的尋址空間(虛擬存儲空間)爲4G(2的32次方)。
操做系統的核心是內核,獨立於普通的應用程序,能夠訪問受保護的內存空間,也有訪問底層硬件設備的全部權限。
爲了保證用戶進程不能直接操做內核(kernel),保證內核的安全,操心繫統將虛擬空間劃分爲兩部分,一部分爲內核空間,一部分爲用戶空間。
針對linux操做系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱爲內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱爲用戶空間。 安全
進程切換網絡
爲了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復之前掛起的某個進程的執行。這種行爲被稱爲進程切換,這種切換是由操做系統來完成的。所以能夠說,任何進程都是在操做系統內核的支持下運行的,是與內核緊密相關的。
從一個進程的運行轉到另外一個進程上運行,這個過程當中通過下面這些變化:保存處理機上下文,包括程序計數器和其餘寄存器。
更新PCB信息。
把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。
選擇另外一個進程執行,並更新其PCB。
更新內存管理的數據結構。
恢復處理機上下文。
注:總而言之進程切換就是很耗資源的
進程的阻塞
正在執行的進程,因爲期待的某些事件未發生,如請求系統資源失敗、等待某種操做的完成、新數據還沒有到達或無新工做作等,則由系統自動執行阻塞原語(Block),使本身由運行狀態變爲阻塞狀態。可見,進程的阻塞是進程自身的一種主動行爲,也所以只有處於運行態的進程(得到CPU),纔可能將其轉爲阻塞狀態。當進程進入阻塞狀態,是不佔用CPU資源的,好比socket.accept()。
文件描述符fd
文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。
文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者建立一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫每每會圍繞着文件描述符展開。可是文件描述符這一律念每每只適用於UNIX、Linux這樣的操做系統。
緩存 I/O
緩存 I/O 又被稱做標準 I/O,大多數文件系統的默認 I/O 操做都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操做系統會將 I/O 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。用戶空間無法直接訪問內核空間的,內核態到用戶態的數據拷貝
IO模型
阻塞IO
非阻塞IO
多路複用IO
信號驅動模型
異步IO
阻塞IO【blocking IO】
在linux中,默認狀況下全部的socket都是blocking
例如,socket.accept():涉及2個阻塞過程: 1.等待數據準備[等待客戶端鏈接] 2.將數據從內核拷貝到進程中
blocking IO的特色:
就是在IO執行的兩個階段都被block了
非阻塞IO【non-blocking IO】
能夠設定爲非阻塞IO,非阻塞IO無論有沒有接收到數據,都會繼續執行下面的語句,不回阻塞。若是有數據,則取回數據,若是沒有數據,則報錯
問: non-blocking IO會一直單線往下走麼?
答:確定不會,循環發送recefrom系統調用,有則接收,沒有繼續往下走,重複調用, 用戶進程實際上是須要不斷的主動詢問kernel數據好了沒有,期間CPU釋放,能夠執行其餘代碼,數據Copy的階段是阻塞狀態,操做系統須要把內容從內核區copy到用戶區
弊端: 消息不能及時的獲取,非阻塞IO會將大片的阻塞時間分割成N個片斷,期間若是有接收到消息,non-blocking仍是會按照特定的5秒來接收消息,這樣就形成了數據的延時接收。
多路複用IO[IO事件驅動]
多路複用IO也叫IO事件驅動IO[event drivern IO].其中select[Apache]/epoll[Nginx]也是多路複用IO。
select/epoll的好處就在於單個process就能夠同時處理多個網絡鏈接的IO。它的基本原理就是select/epoll這個function會不斷的輪詢所負責的全部socket,當某個socket有數據到達了,就通知用戶進程。
當用戶進程調用了select,那麼整個進程會被block,而同時,kernel會「監視」全部select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操做,將數據從kernel拷貝到用戶進程。只不過process是被select這個函數block,而不是被socket IO給block。
select/epoll的優點並非對於單個鏈接能處理得更快,而是在於能處理更多的鏈接。
注意1:select函數返回結果中若是有文件可讀了,那麼進程就能夠經過調用accept()或recv()來讓kernel將位於內核中準備到的數據copy到用戶區。
注意2: select的優點在於能夠處理多個鏈接,不適用於單個鏈接
異步IO【asynchronized IO】
用戶進程發起read操做以後,馬上就能夠開始去作其它的事。而另外一方面,從kernel的角度,當它受到一個asynchronous read以後,首先它會馬上返回,因此不會對用戶進程產生任何block。而後,kernel會等待數據準備完成,而後將數據拷貝到用戶內存,當這一切都完成以後,kernel會給用戶進程發送一個signal,告訴它read操做完成了。
問:blocking和non-blocking的區別在哪?
答:調用blocking IO會一直block住對應的進程直到操做完成,而non-blocking IO在kernel還準備數據的狀況下會馬上返回。
問: synchronous IO和asynchronous IO的區別在哪?
答: 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;
注意:blocking IO,non-blocking IO,IO multiplexing都屬於synchronous IO
select,poll,epoll都屬於IO多路複用,而IO多路複用又屬於同步的範疇,故,epoll只是一個僞異步而已。
IO區別:
sellect、poll、epoll三者的區別
select
select最先於1983年出如今4.2BSD中,它經過一個select()系統調用來監視多個文件描述符的數組,當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,好比nginx、Tornado、等,咱們叫它異步IO,其實是IO多路複用。
實例1(non-blocking IO):
server:
import time import socket sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # sk.setsockopt sk.set sk.bind(('127.0.0.1',6667)) sk.listen(5) sk.setblocking(False) while True: try: print ('waiting client connection .......') connection,address = sk.accept() # 進程主動輪詢 print("+++",address) client_messge = connection.recv(1024) print(str(client_messge,'utf8')) connection.close() except Exception as e: print (e) time.sleep(4)
client:
import time import socket sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) while True: sk.connect(('127.0.0.1',6667)) print("hello") sk.sendall(bytes("hello","utf8")) time.sleep(2) break
實例2(IO multiplexing):
監聽的重任經過調用select等函數交給了內核去作。IO多路複用有兩個特別的系統調用select、poll、epoll函數。select調用是內核級別的,select輪詢相對非阻塞的輪詢的區別在於—前者能夠等待多個socket,能實現同時對多個IO端口進行監聽,當其中任何一個socket的數據準好了,就能返回進行可讀,而後進程再進行recvfrom系統調用,將數據由內核拷貝到用戶進程,固然這個過程是阻塞的
server:
import socket import select sk = socket.socket() sk.bind(("127.0.0.1", 9904)) sk.listen(5) while True: r, w, e = select.select([sk, ], [], [], 5) for i in r: conn,add=i.accept() print(conn) print("hello") print('>>>>>>')
client:
import socket sk=socket.socket() sk.connect(("127.0.0.1",9904)) while 1: inp=input(">>").strip() sk.send(inp.encode("utf8")) data=sk.recv(1024) print(data.decode("utf8"))
多人併發聊天
server:
import socket import select sk=socket.socket() sk.bind(("127.0.0.1",8801)) sk.listen(5) inputs=[sk,] while True: r,w,e=select.select(inputs,[],[],5) print(len(r)) for obj in r: if obj==sk: conn,add=obj.accept() print(conn) inputs.append(conn) else: data_byte=obj.recv(1024) print(str(data_byte,'utf8')) inp=input('回答%s號客戶>>>'%inputs.index(obj)) obj.sendall(bytes(inp,'utf8')) print('>>',r)
Client:
import socket sk=socket.socket() sk.connect(('127.0.0.1',8801)) while True: inp=input(">>>>") sk.sendall(bytes(inp,"utf8")) data=sk.recv(1024) print(str(data,'utf8'))
【更多參考】http://www.cnblogs.com/yuanchenqi/articles/5722574.html