一排监、事件驅(qū)動(dòng)模型介紹
1惊楼、傳統(tǒng)的編程模式
例如:線性模式大致流程
開(kāi)始--->代碼塊A--->代碼塊B--->代碼塊C--->代碼塊D--->......--->結(jié)束
每一個(gè)代碼塊里是完成各種各樣事情的代碼玖瘸,但編程者知道代碼塊A,B,C,D...的執(zhí)行順序,唯一能夠改變這個(gè)流程的是數(shù)據(jù)檀咙。輸入不同的數(shù)據(jù)雅倒,根據(jù)條件語(yǔ)句判斷,流程或許就改為A--->C--->E...--->結(jié)束弧可。每一次程序運(yùn)行順序或許都不同蔑匣,但它的控制流程是由輸入數(shù)據(jù)和你編寫(xiě)的程序決定的。如果你知道這個(gè)程序當(dāng)前的運(yùn)行狀態(tài)(包括輸入數(shù)據(jù)和程序本身)棕诵,那你就知道接下來(lái)甚至一直到結(jié)束它的運(yùn)行流程裁良。
例如:事件驅(qū)動(dòng)型程序模型大致流程
開(kāi)始--->初始化--->等待
與上面?zhèn)鹘y(tǒng)編程模式不同,事件驅(qū)動(dòng)程序在啟動(dòng)之后年鸳,就在那等待趴久,等待什么呢?等待被事件觸發(fā)搔确。傳統(tǒng)編程下也有“等待”的時(shí)候彼棍,比如在代碼塊D中,你定義了一個(gè)input()膳算,需要用戶(hù)輸入數(shù)據(jù)座硕。但這與下面的等待不同,傳統(tǒng)編程的“等待”涕蜂,比如input()华匾,你作為程序編寫(xiě)者是知道或者強(qiáng)制用戶(hù)輸入某個(gè)東西的,或許是數(shù)字机隙,或許是文件名稱(chēng)蜘拉,如果用戶(hù)輸入錯(cuò)誤,你還需要提醒他有鹿,并請(qǐng)他重新輸入旭旭。事件驅(qū)動(dòng)程序的等待則是完全不知道,也不強(qiáng)制用戶(hù)輸入或者干什么葱跋。只要某一事件發(fā)生持寄,那程序就會(huì)做出相應(yīng)的“反應(yīng)”源梭。這些事件包括:輸入信息、鼠標(biāo)稍味、敲擊鍵盤(pán)上某個(gè)鍵還有系統(tǒng)內(nèi)部定時(shí)器觸發(fā)废麻。
2、事件驅(qū)動(dòng)模型
通常模庐,我們寫(xiě)服務(wù)器處理模型的程序時(shí)烛愧,有以下幾種模型:
(1)每收到一個(gè)請(qǐng)求,創(chuàng)建一個(gè)新的進(jìn)程赖欣,來(lái)處理該請(qǐng)求屑彻;
(2)每收到一個(gè)請(qǐng)求,創(chuàng)建一個(gè)新的線程顶吮,來(lái)處理該請(qǐng)求社牲;
(3)每收到一個(gè)請(qǐng)求,放入一個(gè)事件列表悴了,讓主進(jìn)程通過(guò)非阻塞I/O方式來(lái)處理請(qǐng)求
3搏恤、第三種就是協(xié)程、事件驅(qū)動(dòng)的方式湃交,一般普遍認(rèn)為第(3)種方式是大多數(shù)網(wǎng)絡(luò)服務(wù)器采用的方式
示例:
1 #事件驅(qū)動(dòng)之鼠標(biāo)點(diǎn)擊事件注冊(cè)
2
3 <!DOCTYPE html>
4 <html lang="en">
5 <head>
6 <meta charset="UTF-8">
7 <title>Title</title>
8
9 </head>
10 <body>
11
12 <p onclick="fun()">點(diǎn)我呀</p>
13
14
15 <script type="text/javascript">
16 function fun() {
17 alert('約嗎?')
18 }
19 </script>
20 </body>
21
22 </html>
執(zhí)行結(jié)果:
在UI編程中熟空,常常要對(duì)鼠標(biāo)點(diǎn)擊進(jìn)行相應(yīng),首先如何獲得鼠標(biāo)點(diǎn)擊呢搞莺?
兩種方式:
1息罗、創(chuàng)建一個(gè)線程循環(huán)檢測(cè)是否有鼠標(biāo)點(diǎn)擊
那么這個(gè)方式有以下幾個(gè)缺點(diǎn):
CPU資源浪費(fèi),可能鼠標(biāo)點(diǎn)擊的頻率非常小才沧,但是掃描線程還是會(huì)一直循環(huán)檢測(cè)迈喉,這會(huì)造成很多的CPU資源浪費(fèi);如果掃描鼠標(biāo)點(diǎn)擊的接口是阻塞的呢温圆?
如果是堵塞的挨摸,又會(huì)出現(xiàn)下面這樣的問(wèn)題,如果我們不但要掃描鼠標(biāo)點(diǎn)擊岁歉,還要掃描鍵盤(pán)是否按下得运,由于掃描鼠標(biāo)時(shí)被堵塞了,那么可能永遠(yuǎn)不會(huì)去掃描鍵盤(pán)锅移;
如果一個(gè)循環(huán)需要掃描的設(shè)備非常多熔掺,這又會(huì)引來(lái)響應(yīng)時(shí)間的問(wèn)題;
所以非剃,該方式是非常不好的瞬女。
2、事件驅(qū)動(dòng)模型
目前大部分的UI編程都是事件驅(qū)動(dòng)模型努潘,如很多UI平臺(tái)都會(huì)提供onClick()事件,這個(gè)事件就代表鼠標(biāo)按下事件。事件驅(qū)動(dòng)模型大體思路如下:
1.有一個(gè)事件(消息)隊(duì)列疯坤;
2.鼠標(biāo)按下時(shí)报慕,往這個(gè)隊(duì)列中增加一個(gè)點(diǎn)擊事件(消息);
3.有個(gè)循環(huán)压怠,不斷從隊(duì)列取出事件眠冈,根據(jù)不同的事件,調(diào)用不同的函數(shù)菌瘫,如onClick()蜗顽、onKeyDown()等;
4.事件(消息)一般都各自保存各自的處理函數(shù)指針雨让,這樣雇盖,每個(gè)消息都有獨(dú)立的處理函數(shù);
什么是事件驅(qū)動(dòng)模型 栖忠?
目前大部分的UI編程都是事件驅(qū)動(dòng)模型崔挖,如很多UI平臺(tái)都會(huì)提供onClick()事件,這個(gè)事件就代表鼠標(biāo)按下事件庵寞。事件驅(qū)動(dòng)模型大體思路如下:
1.有一個(gè)事件(消息)隊(duì)列狸相;
2.鼠標(biāo)按下時(shí)膜楷,往這個(gè)隊(duì)列中增加一個(gè)點(diǎn)擊事件(消息)啡氢;
3.有個(gè)循環(huán)畅涂,不斷從隊(duì)列取出事件知染,根據(jù)不同的事件目养,調(diào)用不同的函數(shù)秒赤,如onClick()妆档、onKeyDown()等院刁;
4.事件(消息)一般都各自保存各自的處理函數(shù)指針渐白,這樣尊浓,每個(gè)消息都有獨(dú)立的處理函數(shù);
事件驅(qū)動(dòng)編程是一種編程范式纯衍,這里程序的執(zhí)行流由外部事件來(lái)決定栋齿。它的特點(diǎn)是包含一個(gè)事件循環(huán),當(dāng)外部事件發(fā)生時(shí)使用回調(diào)機(jī)制來(lái)觸發(fā)相應(yīng)的處理襟诸。另外兩種常見(jiàn)的編程范式是(單線程)同步以及多線程編程瓦堵。
需知:每個(gè)cpu都有其一套可執(zhí)行的專(zhuān)門(mén)指令集,如SPARC和Pentium歌亲,其實(shí)每個(gè)硬件之上都要有一個(gè)控制程序菇用,cpu的指令集就是cpu的控制程序。
二陷揪、IO模型準(zhǔn)備
在進(jìn)行解釋之前惋鸥,首先要說(shuō)明幾個(gè)概念:
1.用戶(hù)空間和內(nèi)核空間
2.進(jìn)程切換
3.進(jìn)程的阻塞
4.文件描述符
5.緩存 I/O
1杂穷、用戶(hù)空間和內(nèi)核空間
例如:采用虛擬存儲(chǔ)器,對(duì)于32bit操作系統(tǒng)卦绣,它的尋址空間(虛擬存儲(chǔ)空間為4G耐量,即2的32次方)。
操作系統(tǒng)的核心是內(nèi)核滤港,獨(dú)立于普通的應(yīng)用程序廊蜒,可以訪問(wèn)受保護(hù)的內(nèi)存空間,也可以訪問(wèn)底層硬件的所有權(quán)限溅漾。
為了保證用戶(hù)進(jìn)程不能直接操作內(nèi)核(kernel),保證內(nèi)核的安全山叮,操作系統(tǒng)將虛擬空間劃分為兩部分:一部分為內(nèi)核空間,另一部分為用戶(hù)空間添履。
那么操作系統(tǒng)是如何分配空間的屁倔?這里就會(huì)涉及到內(nèi)核態(tài)和用戶(hù)態(tài)的兩種工作狀態(tài)。
1G: 0 --->內(nèi)核態(tài)
3G: 1 --->用戶(hù)態(tài)
CPU的指令集缝龄,是通過(guò)0和1 決定你是用戶(hù)態(tài)汰现,還是內(nèi)核態(tài)
計(jì)算機(jī)的兩種工作狀態(tài): 內(nèi)核態(tài)和用戶(hù)態(tài)
cpu的兩種工作狀態(tài):
現(xiàn)在的操作系統(tǒng)都是分時(shí)操作系統(tǒng),分時(shí)的根源叔壤,來(lái)自于硬件層面操作系統(tǒng)內(nèi)核占用的內(nèi)存與應(yīng)用程序占用的內(nèi)存彼此之間隔離瞎饲。cpu通過(guò)psw(程序狀態(tài)寄存器)中的一個(gè)2進(jìn)制位來(lái)控制cpu本身的工作狀態(tài),即內(nèi)核態(tài)與用戶(hù)態(tài)炼绘。
內(nèi)核態(tài):操作系統(tǒng)內(nèi)核只能運(yùn)作于cpu的內(nèi)核態(tài)嗅战,這種狀態(tài)意味著可以執(zhí)行cpu所有的指令,可以執(zhí)行cpu所有的指令俺亮,這也意味著對(duì)計(jì)算機(jī)硬件資源有著完全的控制權(quán)限驮捍,并且可以控制cpu工作狀態(tài)由內(nèi)核態(tài)轉(zhuǎn)成用戶(hù)態(tài)。
用戶(hù)態(tài):應(yīng)用程序只能運(yùn)作于cpu的用戶(hù)態(tài)脚曾,這種狀態(tài)意味著只能執(zhí)行cpu所有的指令的一小部分(或者稱(chēng)為所有指令的一個(gè)子集)东且,這一小部分指令對(duì)計(jì)算機(jī)的硬件資源沒(méi)有訪問(wèn)權(quán)限(比如I/O),并且不能控制由用戶(hù)態(tài)轉(zhuǎn)成內(nèi)核態(tài)本讥。
2珊泳、進(jìn)程切換
為了控制進(jìn)程的執(zhí)行,內(nèi)核必須有能力掛起正在CPU上執(zhí)行的進(jìn)程拷沸,并恢復(fù)以前掛起的某個(gè)進(jìn)程的執(zhí)行色查,這種行為就被稱(chēng)為進(jìn)程切換。
總結(jié):進(jìn)程切換是很消耗資源的撞芍。
3秧了、進(jìn)程的阻塞
正在執(zhí)行的進(jìn)程,由于期待的某些事件未發(fā)生序无,如請(qǐng)求系統(tǒng)資源失敗验毡、等待某種操作的完成衡创、新數(shù)據(jù)尚未到達(dá)或無(wú)新工作做等,則由系統(tǒng)自動(dòng)執(zhí)行阻塞原語(yǔ)(Block)晶通,使自己由運(yùn)行狀態(tài)變?yōu)樽枞麪顟B(tài)钧汹。可見(jiàn)录择,進(jìn)程的阻塞是進(jìn)程自身的一種主動(dòng)行為,也因此只有處于運(yùn)行態(tài)的進(jìn)程(獲得CPU)碗降,才可能將其轉(zhuǎn)為阻塞狀態(tài)隘竭。當(dāng)進(jìn)程進(jìn)入阻塞狀態(tài),是不占用CPU資源的讼渊。
4动看、文件描述符fd
文件描述符(File descriptor)是計(jì)算機(jī)科學(xué)中的一個(gè)術(shù)語(yǔ),是一個(gè)用于表述指向文件的引用的抽象化概念爪幻。
文件描述符在形式上是一個(gè)非負(fù)整數(shù)菱皆。實(shí)際上,它是一個(gè)索引值挨稿,指向內(nèi)核為每一個(gè)進(jìn)程所維護(hù)的該進(jìn)程打開(kāi)文件的記錄表仇轻。當(dāng)程序打開(kāi)一個(gè)現(xiàn)有文件或者創(chuàng)建一個(gè)新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符奶甘。在程序設(shè)計(jì)中篷店,一些涉及底層的程序編寫(xiě)往往會(huì)圍繞著文件描述符展開(kāi)。但是文件描述符這一概念往往只適用于UNIX臭家、Linux這樣的操作系統(tǒng)疲陕。
5、緩存 I/O
緩存 I/O 又被稱(chēng)作標(biāo)準(zhǔn) I/O钉赁,大多數(shù)文件系統(tǒng)的默認(rèn) I/O 操作都是緩存 I/O蹄殃。在 Linux 的緩存 I/O 機(jī)制中,操作系統(tǒng)會(huì)將 I/O 的數(shù)據(jù)緩存在文件系統(tǒng)的頁(yè)緩存( page cache )中你踩,也就是說(shuō)诅岩,數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間姓蜂。用戶(hù)空間沒(méi)法直接訪問(wèn)內(nèi)核空間的按厘,內(nèi)核態(tài)到用戶(hù)態(tài)的數(shù)據(jù)拷貝。
緩存 I/O 的缺點(diǎn):
數(shù)據(jù)在傳輸過(guò)程中需要在應(yīng)用程序地址空間和內(nèi)核進(jìn)行多次數(shù)據(jù)拷貝操作钱慢,這些數(shù)據(jù)拷貝操作所帶來(lái)的 CPU 以及內(nèi)存開(kāi)銷(xiāo)是非常大的逮京。
本文討論的背景是Linux環(huán)境下的network IO。
IO發(fā)生時(shí)涉及的對(duì)象和步驟:
對(duì)于一個(gè)network IO (這里我們以read舉例)束莫,它會(huì)涉及到兩個(gè)系統(tǒng)對(duì)象懒棉,
1草描、一個(gè)是調(diào)用這個(gè)IO的process (or thread),
2策严、另一個(gè)就是系統(tǒng)內(nèi)核(kernel)穗慕。
當(dāng)一個(gè)read操作發(fā)生時(shí),它會(huì)經(jīng)歷兩個(gè)階段:
1妻导、等待數(shù)據(jù)準(zhǔn)備 (Waiting for the data to be ready)
2逛绵、將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中 (Copying the data from the kernel to the process)
記住這兩點(diǎn)很重要,因?yàn)檫@些IO Model的區(qū)別就是在兩個(gè)階段上各有不同的情況倔韭。
常見(jiàn)的幾種IO 模型:
1.blocking IO (阻塞IO)
2.nonblocking IO (非阻塞IO)
3.IO multiplexing (IO多路復(fù)用)
4.signal driven IO (信號(hào)驅(qū)動(dòng)式IO)
5.asynchronous IO (異步IO)
一术浪、不常用的IO模型
1、信號(hào)驅(qū)動(dòng)IO模型(Signal-driven IO)
使用信號(hào)寿酌,讓內(nèi)核在描述符就緒時(shí)發(fā)送SIGIO信號(hào)通知應(yīng)用程序胰苏,稱(chēng)這種模型為信號(hào)驅(qū)動(dòng)式I/O(signal-driven I/O)。
原理圖:
首先開(kāi)啟套接字的信號(hào)驅(qū)動(dòng)式I/O功能醇疼,并通過(guò)sigaction系統(tǒng)調(diào)用安裝一個(gè)信號(hào)處理函數(shù)硕并。該系統(tǒng)調(diào)用將立即返回,我們的進(jìn)程繼續(xù)工作秧荆,也就是說(shuō)進(jìn)程沒(méi)有被阻塞倔毙。當(dāng)數(shù)據(jù)報(bào)準(zhǔn)備好讀取時(shí),內(nèi)核就為該進(jìn)程產(chǎn)生一個(gè)SIGIO信號(hào)辰如。隨后就可以在信號(hào)處理函數(shù)中調(diào)用recvfrom讀取數(shù)據(jù)報(bào)普监,并通知主循環(huán)數(shù)據(jù)已經(jīng)準(zhǔn)備好待處理,也可以立即通知主循環(huán)琉兜,讓它讀取數(shù)據(jù)報(bào)凯正。
無(wú)論如何處理SIGIO信號(hào),這種模型的優(yōu)勢(shì)在于等待數(shù)據(jù)報(bào)到達(dá)期間進(jìn)程不被阻塞豌蟋。主循環(huán)可以繼續(xù)執(zhí)行 廊散,只要等到來(lái)自信號(hào)處理函數(shù)的通知:既可以是數(shù)據(jù)已準(zhǔn)備好被處理,也可以是數(shù)據(jù)報(bào)已準(zhǔn)備好被讀取梧疲。
二允睹、常用的四種IO模型:
1、 blocking IO(阻塞IO模型)
原理圖:
示例:一收一發(fā)程序會(huì)進(jìn)入死循環(huán)
server.py
1 #!/usr/bin/env python
2 # -*- coding:utf-8 -*-
3 #Author: nulige
4
5 import socket
6
7 sk=socket.socket()
8
9 sk.bind(("127.0.0.1",8080))
10
11 sk.listen(5)
12
13 while 1:
14 conn,addr=sk.accept()
15
16 while 1:
17 conn.send("hello client".encode("utf8"))
18 data=conn.recv(1024)
19 print(data.decode("utf8"))
client.py
1 #!/usr/bin/env python
2 # -*- coding:utf-8 -*-
3 #Author: nulige
4
5 import socket
6
7 sk=socket.socket()
8
9 sk.connect(("127.0.0.1",8080))
10
11 while 1:
12 data=sk.recv(1024)
13 print(data.decode("utf8"))
14 sk.send(b"hello server")
當(dāng)用戶(hù)進(jìn)程調(diào)用了recvfrom這個(gè)系統(tǒng)調(diào)用幌氮,kernel就開(kāi)始了IO的第一個(gè)階段:準(zhǔn)備數(shù)據(jù)缭受。對(duì)于network io來(lái)說(shuō),很多時(shí)候數(shù)據(jù)在一開(kāi)始還沒(méi)有到達(dá)(比如该互,還沒(méi)有收到一個(gè)完整的UDP包)米者,這個(gè)時(shí)候kernel就要等待足夠的數(shù)據(jù)到來(lái)。而在用戶(hù)進(jìn)程這邊,整個(gè)進(jìn)程會(huì)被阻塞蔓搞。當(dāng)kernel一直等到數(shù)據(jù)準(zhǔn)備好了胰丁,它就會(huì)將數(shù)據(jù)從kernel中拷貝到用戶(hù)內(nèi)存,然后kernel返回結(jié)果喂分,用戶(hù)進(jìn)程才解除block的狀態(tài)锦庸,重新運(yùn)行起來(lái)。
所以蒲祈,blocking IO的特點(diǎn)就是在IO執(zhí)行的兩個(gè)階段都被block了甘萧。
2、non-blocking IO(非阻塞IO)
原理圖:
從圖中可以看出梆掸,當(dāng)用戶(hù)進(jìn)程發(fā)出read操作時(shí)幔嗦,如果kernel中的數(shù)據(jù)還沒(méi)有準(zhǔn)備好,那么它并不會(huì)block用戶(hù)進(jìn)程沥潭,而是立刻返回一個(gè)error。從用戶(hù)進(jìn)程角度講 嬉挡,它發(fā)起一個(gè)read操作后钝鸽,并不需要等待,而是馬上就得到了一個(gè)結(jié)果庞钢。用戶(hù)進(jìn)程判斷結(jié)果是一個(gè)error時(shí)拔恰,它就知道數(shù)據(jù)還沒(méi)有準(zhǔn)備好,于是它可以再次發(fā)送read操作基括。一旦kernel中的數(shù)據(jù)準(zhǔn)備好了颜懊,并且又再次收到了用戶(hù)進(jìn)程的system call,那么它馬上就將數(shù)據(jù)拷貝到了用戶(hù)內(nèi)存风皿,然后返回河爹。
所以,用戶(hù)進(jìn)程其實(shí)是需要不斷的主動(dòng)詢(xún)問(wèn)kernel數(shù)據(jù)好了沒(méi)有桐款。
注意:
在網(wǎng)絡(luò)IO時(shí)候咸这,非阻塞IO也會(huì)進(jìn)行recvform系統(tǒng)調(diào)用,檢查數(shù)據(jù)是否準(zhǔn)備好魔眨,與阻塞IO不一樣媳维,”非阻塞將大的整片時(shí)間的阻塞分成N多的小的阻塞, 所以進(jìn)程不斷地有機(jī)會(huì) ‘被’ CPU光顧”。即每次recvform系統(tǒng)調(diào)用之間遏暴,cpu的權(quán)限還在進(jìn)程手中侄刽,這段時(shí)間是可以做其他事情的,
也就是說(shuō)非阻塞的recvform系統(tǒng)調(diào)用調(diào)用之后朋凉,進(jìn)程并沒(méi)有被阻塞州丹,內(nèi)核馬上返回給進(jìn)程,如果數(shù)據(jù)還沒(méi)準(zhǔn)備好侥啤,此時(shí)會(huì)返回一個(gè)error当叭。進(jìn)程在返回之后茬故,可以干點(diǎn)別的事情,然后再發(fā)起recvform系統(tǒng)調(diào)用蚁鳖。重復(fù)上面的過(guò)程磺芭,循環(huán)往復(fù)的進(jìn)行recvform系統(tǒng)調(diào)用。這個(gè)過(guò)程通常被稱(chēng)之為輪詢(xún)醉箕。輪詢(xún)檢查內(nèi)核數(shù)據(jù)钾腺,直到數(shù)據(jù)準(zhǔn)備好,再拷貝數(shù)據(jù)到進(jìn)程讥裤,進(jìn)行數(shù)據(jù)處理放棒。需要注意,拷貝數(shù)據(jù)整個(gè)過(guò)程己英,進(jìn)程仍然是屬于阻塞的狀態(tài)间螟。
示例:
服務(wù)端:
1 import time
2 import socket
3 sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
4 sk.bind(('127.0.0.1',6667))
5 sk.listen(5)
6 sk.setblocking(False) #設(shè)置成非阻塞狀態(tài)
7 while True:
8 try:
9 print ('waiting client connection .......')
10 connection,address = sk.accept() # 進(jìn)程主動(dòng)輪詢(xún)
11 print("+++",address)
12 client_messge = connection.recv(1024)
13 print(str(client_messge,'utf8'))
14 connection.close()
15 except Exception as e: #捕捉錯(cuò)誤
16 print (e)
17 time.sleep(4) #每4秒打印一個(gè)捕捉到的錯(cuò)誤
客戶(hù)端:
1 import time
2 import socket
3 sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
4
5 while True:
6 sk.connect(('127.0.0.1',6667))
7 print("hello")
8 sk.sendall(bytes("hello","utf8"))
9 time.sleep(2)
10 break
缺點(diǎn):
1、發(fā)送了太多系統(tǒng)調(diào)用數(shù)據(jù)
2损肛、數(shù)據(jù)處理不及時(shí)
3厢破、IO multiplexing(IO多路復(fù)用)
IO multiplexing這個(gè)詞可能有點(diǎn)陌生,但是如果我說(shuō)select治拿,epoll摩泪,大概就都能明白了。有些地方也稱(chēng)這種IO方式為event driven IO劫谅。我們都知道见坑,select/epoll的好處就在于單個(gè)process就可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的IO。它的基本原理就是select/epoll這個(gè)function會(huì)不斷的輪詢(xún)所負(fù)責(zé)的所有socket捏检,當(dāng)某個(gè)socket有數(shù)據(jù)到達(dá)了荞驴,就通知用戶(hù)進(jìn)程。
IO多路復(fù)用的三種方式:
1贯城、select--->效率最低戴尸,但有最大描述符限制,在linux為1024冤狡。
2孙蒙、poll ---->和select一樣,但沒(méi)有最大描述符限制悲雳。
3挎峦、epoll --->效率最高,沒(méi)有最大描述符限制合瓢,支持水平觸發(fā)與邊緣觸發(fā)坦胶。
IO多路復(fù)用的優(yōu)勢(shì):同時(shí)可以監(jiān)聽(tīng)多個(gè)連接,用的是單線程,利用空閑時(shí)間實(shí)現(xiàn)并發(fā)顿苇。
注意:
Linux系統(tǒng): select峭咒、poll、epoll
Windows系統(tǒng):select
Mac系統(tǒng):select纪岁、poll
原理圖:
當(dāng)用戶(hù)進(jìn)程調(diào)用了select凑队,那么整個(gè)進(jìn)程會(huì)被block,而同時(shí)幔翰,kernel會(huì)“監(jiān)視”所有select負(fù)責(zé)的socket漩氨,當(dāng)任何一個(gè)socket中的數(shù)據(jù)準(zhǔn)備好了,select就會(huì)返回遗增。這個(gè)時(shí)候用戶(hù)進(jìn)程再調(diào)用read操作叫惊,將數(shù)據(jù)從kernel拷貝到用戶(hù)進(jìn)程。
這個(gè)圖和blocking IO的圖其實(shí)并沒(méi)有太大的不同做修,事實(shí)上霍狰,還更差一些。因?yàn)檫@里需要使用兩個(gè)system call (select 和 recvfrom)饰及,而blocking IO只調(diào)用了一個(gè)system call (recvfrom)蚓耽。但是,用select的優(yōu)勢(shì)在于它可以同時(shí)處理多個(gè)connection旋炒。(多說(shuō)一句。所以签杈,如果處理的連接數(shù)不是很高的話瘫镇,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大答姥。select/epoll的優(yōu)勢(shì)并不是對(duì)于單個(gè)連接能處理得更快铣除,而是在于能處理更多的連接。)
在IO multiplexing Model中鹦付,實(shí)際中尚粘,對(duì)于每一個(gè)socket,一般都設(shè)置成為non-blocking敲长,但是郎嫁,如上圖所示,整個(gè)用戶(hù)的process其實(shí)是一直被block的祈噪。只不過(guò)process是被select這個(gè)函數(shù)block泽铛,而不是被socket IO給block。
注意1:select函數(shù)返回結(jié)果中如果有文件可讀了辑鲤,那么進(jìn)程就可以通過(guò)調(diào)用accept()或recv()來(lái)讓kernel將位于內(nèi)核中準(zhǔn)備到的數(shù)據(jù)copy到用戶(hù)區(qū)盔腔。
注意2: select的優(yōu)勢(shì)在于可以處理多個(gè)連接,不適用于單個(gè)連接
示例:
server.py
1 #server.py
2
3 import socket
4 import select
5 sk=socket.socket()
6 sk.bind(("127.0.0.1",9904))
7 sk.listen(5)
8
9 while True:
10 # sk.accept() #文件描述符
11 r,w,e=select.select([sk,],[],[],5) #輸入列表,輸出列表弛随,錯(cuò)誤列表,5: 是監(jiān)聽(tīng)5秒
12 for i in r: #[sk,]
13 conn,add=i.accept()
14 print(conn)
15 print("hello")
16 print('>>>>>>')
client.py
1 import socket
2
3 sk=socket.socket()
4
5 sk.connect(("127.0.0.1",9904))
6
7 while 1:
8 inp=input(">>").strip()
9 sk.send(inp.encode("utf8"))
10 data=sk.recv(1024)
11 print(data.decode("utf8"))
IO多路復(fù)用中的兩種觸發(fā)方式:
水平觸發(fā):如果文件描述符已經(jīng)就緒可以非阻塞的執(zhí)行IO操作了,此時(shí)會(huì)觸發(fā)通知.允許在任意時(shí)刻重復(fù)檢測(cè)IO的狀態(tài), 沒(méi)有必要每次描述符就緒后盡可能多的執(zhí)行IO.select,poll就屬于水平觸發(fā)瓢喉。
邊緣觸發(fā):如果文件描述符自上次狀態(tài)改變后有新的IO活動(dòng)到來(lái),此時(shí)會(huì)觸發(fā)通知.在收到一個(gè)IO事件通知后要盡可能 多的執(zhí)行IO操作,因?yàn)槿绻谝淮瓮ㄖ袥](méi)有執(zhí)行完IO那么就需要等到下一次新的IO活動(dòng)到來(lái)才能獲取到就緒的描述 符.信號(hào)驅(qū)動(dòng)式IO就屬于邊緣觸發(fā)。
epoll:即可以采用水平觸發(fā),也可以采用邊緣觸發(fā)舀透。
1栓票、水平觸發(fā)
只有高電平或低電平的時(shí)候才觸發(fā)
1-----高電平---觸發(fā)
0-----低電平---不觸發(fā)
示例:
server服務(wù)端
1 #水平觸發(fā)
2 import socket
3 import select
4 sk=socket.socket()
5 sk.bind(("127.0.0.1",9904))
6 sk.listen(5)
7
8 while True:
9 r,w,e=select.select([sk,],[],[],5) #input輸入列表,output輸出列表盐杂,erron錯(cuò)誤列表,5: 是監(jiān)聽(tīng)5秒
10 for i in r: #[sk,]
11 print("hello")
12
13 print('>>>>>>')
client客戶(hù)端
1 import socket
2
3 sk=socket.socket()
4
5 sk.connect(("127.0.0.1",9904))
6
7 while 1:
8 inp=input(">>").strip()
9 sk.send(inp.encode("utf8"))
10 data=sk.recv(1024)
11 print(data.decode("utf8"))
2逗载、邊緣觸發(fā)
1---------高電平--------觸發(fā)
0---------低電平--------觸發(fā)
IO多路復(fù)用優(yōu)勢(shì):同時(shí)可以監(jiān)聽(tīng)多個(gè)連接
示例:select可以監(jiān)控多個(gè)對(duì)象
服務(wù)端
1 #優(yōu)勢(shì)
2 import socket
3 import select
4 sk=socket.socket()
5 sk.bind(("127.0.0.1",9904))
6 sk.listen(5)
7 inp=[sk,]
8
9 while True:
10 r,w,e=select.select(inp,[],[],5) #[sk,conn],5是每隔幾秒監(jiān)聽(tīng)一次
11
12 for i in r: #[sk,]
13 conn,add=i.accept() #發(fā)送系統(tǒng)調(diào)用
14 print(conn)
15 print("hello")
16 inp.append(conn)
17 # conn.recv(1024)
18 print('>>>>>>')
客戶(hù)端:
1 import socket
2
3 sk=socket.socket()
4
5 sk.connect(("127.0.0.1",9904))
6
7 while 1:
8 inp=input(">>").strip()
9 sk.send(inp.encode("utf8"))
10 data=sk.recv(1024)
11 print(data.decode("utf8"))
多了一個(gè)判斷链烈,用select方式實(shí)現(xiàn)的并發(fā)
示例:實(shí)現(xiàn)并發(fā)聊天功能 (select+IO多路復(fù)用厉斟,實(shí)現(xiàn)并發(fā))
服務(wù)端:
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: #監(jiān)聽(tīng)sk和conn
8 r,w,e=select.select(inputs,[],[],5) #conn發(fā)生變化,sk不變化就走else
9 print(len(r))
10 #判斷sk or conn 誰(shuí)發(fā)生了變化
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號(hào)客戶(hù)>>>'%inputs.index(obj))
20 obj.sendall(bytes(inp,'utf8'))
21
22 print('>>',r)
客戶(hù)端:
1 import socket
2 sk=socket.socket()
3 sk.connect(('127.0.0.1',8801))
4
5 while True:
6 inp=input(">>>>")
7 sk.sendall(bytes(inp,"utf8"))
8 data=sk.recv(1024)
9 print(str(data,'utf8'))
執(zhí)行結(jié)果:
先運(yùn)行服務(wù)端,再運(yùn)行多個(gè)客戶(hù)端强衡,就可以聊天啦擦秽。(可以接收多個(gè)客戶(hù)端消息)
1 #server
2 >> [<socket.socket fd=276, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8801)>]
3 1
4 hello
5 回答1號(hào)客戶(hù)>>>word
6 >> [<socket.socket fd=344, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8801), raddr=('127.0.0.1', 54388)>]
7 1
8
9 #clinet
10 >>>>hello
11 word
4、Asynchronous I/O(異步IO)
用戶(hù)進(jìn)程發(fā)起read操作之后漩勤,立刻就可以開(kāi)始去做其它的事感挥。而另一方面,從kernel的角度越败,當(dāng)它受到一個(gè)asynchronous read之后触幼,首先它會(huì)立刻返回,所以不會(huì)對(duì)用戶(hù)進(jìn)程產(chǎn)生任何block究飞。然后置谦,kernel會(huì)等待數(shù)據(jù)準(zhǔn)備完成,然后將數(shù)據(jù)拷貝到用戶(hù)內(nèi)存亿傅,當(dāng)這一切都完成之后媒峡,kernel會(huì)給用戶(hù)進(jìn)程發(fā)送一個(gè)signal,告訴它read操作完成了葵擎。
異步最大特點(diǎn):全程無(wú)阻塞
synchronous IO(同步IO)和asynchronous IO(異步IO)的區(qū)別:
1.A synchronous I/O operation causes the requesting process to be blocked until that I/O operationcompletes;
2.An asynchronous I/O operation does not cause the requesting process to be blocked;
兩者的區(qū)別就在于synchronous IO做”IO operation”的時(shí)候會(huì)將process阻塞谅阿。(有一丁點(diǎn)阻塞,都是同步IO)按照這個(gè)定義酬滤,之前所述的blocking IO签餐,non-blocking IO,IO multiplexing都屬于synchronous IO(同步IO)盯串。
同步IO:包括 blocking IO贱田、non-blocking、select嘴脾、poll男摧、epoll(故:epool只是偽異步而已)(有阻塞)
異步IO:包括:asynchronous (無(wú)阻塞)
五種IO模型比較:
經(jīng)過(guò)上面的介紹蔬墩,會(huì)發(fā)現(xiàn)non-blocking IO和asynchronous IO的區(qū)別還是很明顯的。在non-blocking IO中耗拓,雖然進(jìn)程大部分時(shí)間都不會(huì)被block拇颅,但是它仍然要求進(jìn)程去主動(dòng)的check,并且當(dāng)數(shù)據(jù)準(zhǔn)備完成以后乔询,也需要進(jìn)程主動(dòng)的再次調(diào)用recvfrom來(lái)將數(shù)據(jù)拷貝到用戶(hù)內(nèi)存樟插。而asynchronous IO則完全不同。它就像是用戶(hù)進(jìn)程將整個(gè)IO操作交給了他人(kernel)完成竿刁,然后他人做完后發(fā)信號(hào)通知黄锤。在此期間,用戶(hù)進(jìn)程不需要去檢查IO操作的狀態(tài)食拜,也不需要主動(dòng)的去拷貝數(shù)據(jù)鸵熟。
5、selectors模塊應(yīng)用
python封裝好的模塊:selectors
selectors模塊: 會(huì)選擇一個(gè)最優(yōu)的操作系統(tǒng)實(shí)現(xiàn)方式
示例:
select_module.py
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) #設(shè)置成非阻塞
10 sel.register(conn, selectors.EVENT_READ, read) #conn綁定的是read
11
12 def read(conn, mask):
13 try:
14 data = conn.recv(1000) # Should be ready
15 if not data:
16 raise Exception
17 print('echoing', repr(data), 'to', conn)
18 conn.send(data) # Hope it won't block
19 except Exception as e:
20 print('closing', conn)
21 sel.unregister(conn) #解除注冊(cè)
22 conn.close()
23
24 sock = socket.socket()
25 sock.bind(('localhost', 8090))
26 sock.listen(100)
27 sock.setblocking(False)
28 #注冊(cè)
29 sel.register(sock, selectors.EVENT_READ, accept)
30 print("server....")
31
32 while True:
33 events = sel.select() #監(jiān)聽(tīng)[sock,conn1,conn2]
34 print("events",events)
35 #拿到2個(gè)元素负甸,一個(gè)key,一個(gè)mask
36 for key, mask in events:
37 # print("key",key)
38 # print("mask",mask)
39 callback = key.data #綁定的是read函數(shù)
40 # print("callback",callback)
41 callback(key.fileobj, mask) #key.fileobj=sock,conn1,conn2
client.py
1 import socket
2
3 sk=socket.socket()
4
5 sk.connect(("127.0.0.1",8090))
6 while 1:
7 inp=input(">>>")
8 sk.send(inp.encode("utf8")) #發(fā)送內(nèi)容
9 data=sk.recv(1024) #接收信息
10 print(data.decode("utf8")) #打印出來(lái)
執(zhí)行結(jié)果:
先運(yùn)行select_module.py流强,再運(yùn)行clinet.py
1 #server
2
3 server....
4 events [(SelectorKey(fileobj=<socket.socket fd=312, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090)>, fd=312, events=1, data=<function accept at 0x01512F60>), 1)]
5 accepted <socket.socket fd=376, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57638)> from ('127.0.0.1', 57638)
6 events [(SelectorKey(fileobj=<socket.socket fd=376, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57638)>, fd=376, events=1, data=<function read at 0x015C26A8>), 1)]
7 echoing b'hello' to <socket.socket fd=376, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57638)>
8 events [(SelectorKey(fileobj=<socket.socket fd=312, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090)>, fd=312, events=1, data=<function accept at 0x01512F60>), 1)]
9 accepted <socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57675)> from ('127.0.0.1', 57675)
10 events [(SelectorKey(fileobj=<socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57675)>, fd=324, events=1, data=<function read at 0x015C26A8>), 1)]
11 echoing b'uuuu' to <socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57675)>
12 events [(SelectorKey(fileobj=<socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57675)>, fd=324, events=1, data=<function read at 0x015C26A8>), 1)]
13 closing <socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57675)>
14 events [(SelectorKey(fileobj=<socket.socket fd=312, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090)>, fd=312, events=1, data=<function accept at 0x01512F60>), 1)]
15 accepted <socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57876)> from ('127.0.0.1', 57876)
16 events [(SelectorKey(fileobj=<socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57876)>, fd=324, events=1, data=<function read at 0x015C26A8>), 1)]
17 echoing b'welcome' to <socket.socket fd=324, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8090), raddr=('127.0.0.1', 57876)>
18
19 #clinet (啟動(dòng)兩個(gè)client)
20 >>>hello
21 hello
22
23 >>>welcome
24 welcome
6、I/O多路復(fù)用的應(yīng)用場(chǎng)景
(1)當(dāng)客戶(hù)處理多個(gè)描述字時(shí)(一般是交互式輸入和網(wǎng)絡(luò)套接口)呻待,必須使用I/O復(fù)用打月。
(2)當(dāng)一個(gè)客戶(hù)同時(shí)處理多個(gè)套接口時(shí),而這種情況是可能的蚕捉,但很少出現(xiàn)奏篙。
(3)如果一個(gè)TCP服務(wù)器既要處理監(jiān)聽(tīng)套接口,又要處理已連接套接口迫淹,一般也要用到I/O復(fù)用秘通。
(4)如果一個(gè)服務(wù)器即要處理TCP,又要處理UDP千绪,一般要使用I/O復(fù)用。
(5)如果一個(gè)服務(wù)器要處理多個(gè)服務(wù)或多個(gè)協(xié)議梗脾,一般要使用I/O復(fù)用荸型。
'''與多進(jìn)程和多線程技術(shù)相比,I/O多路復(fù)用技術(shù)的最大優(yōu)勢(shì)是系統(tǒng)開(kāi)銷(xiāo)小炸茧,系統(tǒng)不必創(chuàng)建進(jìn)程/線程瑞妇,也不必維護(hù)這些進(jìn)程/線程,從而大大減小了系統(tǒng)的開(kāi)銷(xiāo)梭冠。'''
最后辕狰,再舉幾個(gè)不是很恰當(dāng)?shù)睦觼?lái)說(shuō)明這四個(gè)IO Model:
有A,B控漠,C蔓倍,D四個(gè)人在釣魚(yú):
A用的是最老式的魚(yú)竿悬钳,所以呢,得一直守著偶翅,等到魚(yú)上鉤了再拉桿默勾;【阻塞】
B的魚(yú)竿有個(gè)功能,能夠顯示是否有魚(yú)上鉤(這個(gè)顯示功能一直去判斷魚(yú)是否上鉤)聚谁,所以呢母剥,B就和旁邊的MM聊天,隔會(huì)再看看有沒(méi)有魚(yú)上鉤形导,有的話就迅速拉桿环疼;【非阻塞】
C用的魚(yú)竿和B差不多,但他想了一個(gè)好辦法朵耕,就是同時(shí)放好幾根魚(yú)竿炫隶,然后守在旁邊,一旦有顯示說(shuō)魚(yú)上鉤了憔披,它就將對(duì)應(yīng)的魚(yú)竿拉起來(lái)等限;【同步】
D是個(gè)有錢(qián)人,干脆雇了一個(gè)人幫他釣魚(yú)芬膝,一旦那個(gè)人把魚(yú)釣上來(lái)了望门,就給D發(fā)個(gè)短信(消息回掉機(jī)制,主動(dòng)告知)锰霜〕镂螅【異步】