1. 程式人生 > >16.python-I/O模型

16.python-I/O模型

一.事件驅動模型
1.什麼是事件驅動模型:本身是一種程式設計正規化,這裡程式的執行是由外部事件來決定的。它的特點是包含一個事件迴圈,當外部事件發生時使用回撥機制來觸發相應的處理。常見的程式設計正規化(單執行緒)同步以及多執行緒程式設計
2.事件驅動模型流程:開始-->初始化-->等待
3.事件驅動模型的原理:目前大部分的UI程式設計都是事件驅動模型,如很多UI平臺都會提供onCick()事件,事件驅動模型大體思路如下:
(1)有一個事件(訊息)佇列
(2)滑鼠按下時,往這個佇列中增加一個點選事件(訊息)
(3)有個迴圈,不斷從佇列中取出事件,根據不同的事件,呼叫不同的函式,如onClick(),onKeyDown()等


(4)事件(訊息)一般都是各自儲存各自的處理函式指標,這樣,每個訊息都有獨立的處理函式
二.IO模型前瞭解的概念
1.使用者空間與核心空間:
(1)現在作業系統都採用虛擬儲存器,那麼對32位作業系統而言,它的定址空間(虛擬儲存空間)為4G(2的32次方)。作業系統的核心是核心,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也有訪問底層硬體裝置的所有許可權。為了保證使用者程序不能直接操作核心,保證核心的安全,作業系統將虛擬空間劃分為倆部分,一部分為核心空間,一部分使用者空間,通過CPU的指令集(CPU執行的程式碼)上的狀態位來決定什麼時候是使用者態,什麼時候是核心態
(2)核心空間:針對linux作業系統而言,將最高的1G位元組,供核心使用,稱為核心空間

(3)使用者空間:針對linux作業系統而言,將較低的3G位元組,供各個程序使用,稱為使用者空間
2.程序切換(非常耗資源)
(1)為了控制程序的執行,核心必須有能力掛起正在CPU上執行的程序,並恢復以前掛起的某個程序的執行。這種行為被稱為程序切換,這種切換操作是由作業系統來完成的。因此可以說,任何程序都是在作業系統核心的支援下執行的,是與核心緊密相關的
(2)從一個程序的執行轉到另一個程序上執行,這個過程中經過以下變化:
1)儲存處理機上下文,包括程式計數器和其他暫存器。
2)更新PCB資訊
3)把程序的PCB移入相應的佇列,如果就緒,在某事件阻塞等待佇列
4)選擇另一個程序執行,並更新其PCB
5)更新記憶體管理的資料結構。

6)恢復處理機上下文。
3.程序的阻塞(當程序進入阻塞狀態,是不佔用CPU資源的)
正在執行的程序,由於期待的某些事件未發生,如請求系統資源失敗,等待某種操作的完成,新資料尚未到達或無新工作做等,則由系統自動執行阻塞原語,使自己由運動狀態變為阻塞狀態。可見,程序的阻塞是程序自身的一種主動行為,也因此只有處於運動態的程序(獲得CPU),才能將其轉為阻塞狀態。
4.檔案描述符:socket就是檔案描述符
檔案描述符是計算機中的一個術語,是一個用於表述指向檔案的引用的抽象化概念。檔案描述符在形式上是一個非負整數。實際上它是一個索引值,指向核心為每一個程序所維護的該程序開啟檔案的記錄表。當程式開啟一個現有檔案或者建立一個新檔案時,核心向程序返回一個檔案描述符。在程式設計中,一些涉及底層的程式縮寫往往會圍繞著檔案描述符展開。但檔案描述符這一概念只適用於UNIX,linux這樣的作業系統
5.快取I/O
(1)快取I/O大多數檔案系統的預設I/O操作都是快取I/O。在linux的快取I/O機制中,作業系統會將I/O的資料快取在檔案系統的頁快取中,也就是說,資料會先被拷貝到作業系統核心的緩衝區中,然後才會從作業系統核心的緩衝區拷貝到應用程式的地址空間。使用者空間沒法直接訪問核心的空間的,核心態到使用者態的資料拷貝。
(2)快取I/O的缺點:資料在傳輸過程中需要在應用程式地址空間和核心進行多次資料拷貝操作,這些資料拷貝操作所帶來的CPU以及記憶體開銷是非常大的。
三.四種I/O模型
1.blocking IO(阻塞I/O):從開始到結束全程堵塞:
(1)在linux中,預設情況下所有的socket都是blocking
(2)當用戶程序呼叫了recvfrom這個系統呼叫,kerne就開始了IO的第一個階段:準備資料。對於network io來說,很多時候資料在一開始還沒有到達,這個時候kernel(核心)就要等待足夠的資料到來。而在使用者程序這邊,整個程序會被阻塞。當kernel一直等到資料準備好了,它就會將資料從kernel中拷貝到使用者記憶體,然後kernel返回結果,使用者程序才解除block的狀態,重新執行起來。所以,blocking IO的特點就是在IO執行的倆個階段都被block了(阻塞IO只發了一次系統呼叫)
(3)結合程式碼:
客戶端程式碼:

import socket

sk=socket.socket()
sk.connect(("127.0.0.1",8080))   #第二步:客戶端connect先發訊息到服務端
while 1:
    data=sk.recv(1024)           #第五步:客戶端接收服務端發來的訊息(如果服務端不發過來訊息,客戶端會阻塞住)
    print(data.decode("utf8"))
    sk.send(b"hello server")

服務端程式碼:

import socket

sk=socket.socket()
sk.bind(("127.0.0.1",8080))
sk.listen(5)                   
while 1:
    #第一步:服務端程式啟動accept()跟作業系統打交道,向作業系統要資料,發一條命令recvfrom是系統呼叫,作業系統核心在等待資料這個過程是阻塞狀態
    conn,addr=sk.accept()       #第三步:當客戶端啟動執行connect方法連結上服務端,服務端核心區有資料了,它就會將資料從核心中拷貝到使用者記憶體接收訊息拿到一個值,程式繼續進行

    while 1:
        conn.send("hello client".encode("utf8"))   #第四步:給客戶端發訊息
        data=conn.recv(1024)
        print(data.decode("utf8"))

2.non-blocking IO(非阻塞I/O)
(1)在linux中,設定socket使其變為non-blocking
(2)當用戶程序發出read操作時,如果kernel中資料還沒有準備好,那麼它並不會block使用者程序,而是立刻返回error。從使用者程序角度講,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。使用者程序判斷結果是一個error時,它就知道資料還沒有準備好,於是它可以再次傳送read操作。一旦kernel中的資料準備好了,並且又再次收到了使用者程序的system call,那麼它馬上就將資料拷貝到了使用者記憶體,然後返回。
(3)缺點:
1)使用者程序需要不斷的主動詢問kerrel資料好了沒有
(4)結合程式碼
客戶端:

import time
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

while True:
    sk.connect(('127.0.0.1',8080))           #第三步:連結服務端
    print("hello")
    sk.sendall(bytes("hello","utf8"))        #第五步:給服務端傳送一條資料
    time.sleep(2)
    break

服務端:

import time
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.bind(('127.0.0.1',8080))
sk.listen(5)
sk.setblocking(False)                     #setblocking設定成非阻塞IO
print ('等待客戶端連結 .......')
while True:
    try:
        #程序主動輪詢
        connection,address = sk.accept()        #第一步:設定成非阻塞IO等待連結的時候不會卡住(會報錯)
        print("+++",address)                   #第四步:列印連結到服務端的客戶端IP和埠
        client_messge = connection.recv(1024)   #第六步:接收客戶端資料
        print(str(client_messge,'utf8'))       #第七步:列印接收資料
        connection.close()                      #第八步:關閉繼續迴圈
    except Exception as e:                     #第二步:捕捉到錯誤繼續往下執行程式碼
        print (e)
        time.sleep(4)

輸出列印:
等待客戶端連結 .......
[WinError 10035] 無法立即完成一個非阻止性套接字操作。
[WinError 10035] 無法立即完成一個非阻止性套接字操作。
+++ ('127.0.0.1', 64251)
hello
[WinError 10035] 無法立即完成一個非阻止性套接字操作。
....
....
....
3.I/O多路複用(同步I/O)
(1)什麼是I/O多路複用:
單個process可以同時處理多個網路連線的IO,它的基本原理就是select/peoll這個function會不斷的輪詢鎖負責的所有socket,當某個socket有資料到達了,就通知使用者程序。當用戶程序呼叫了select,那麼整個程序會被block,而同時,kernel會監聽所有select負責的socket,當任何一個socket中的資料準備好了,select就會返回。這個時候使用者程序在盜用read操作,將資料從kernel拷貝到使用者程序。
(2)I/O多路複用的觸發方式:在linux的IO多路複用中有水平觸發,邊緣觸發倆種模式
1)水平觸發:如果檔案描述符已經就緒可以非阻塞的執行IO操作了,此時會觸發通知,允許在任意時刻重複檢測IO的狀態,沒有必要每次描述符就緒後儘可能多的執行IO.select,poll就屬於水平觸發
2)邊緣觸發:如果檔案描述符自上次狀態後有新的IO活動到來,此時會觸發通知,在收到一個IO事件通知後要儘可能多的執行IO操作,因為如果在一次通知中沒有執行完IO那麼就需要等到下一次新的IO活動的到來才能獲取就緒的描述符,訊號驅動式IO就是屬於邊緣觸發
(3)從電子的角度來解釋水平觸發和:
1)水平觸發:也就是隻有高電平(1)或低電平(0)時才觸發通知,只要在這倆種狀態就能得到通知,只要有資料可讀(描述符就緒)那麼水平觸發的epoll就立即返回。
2)邊緣觸發:只有電平發生變化(高電平到低電平,或者低電平到高電平)的時候才觸發通知,即使有資料可讀,但沒有新的IO活動到來,epoll也不會立即返回
(4)IO多路複用的優點:可同時監聽多個連結
(5)IO多路複用的的實現方法:select、poll、epoll
1)select實現方法:
缺點:每次呼叫都要將所有的檔案描述符(fd)拷貝的核心空間,導致效率下降,遍歷所有的檔案描述符(fd)檢視是否有資料訪問,最大連結數限額1024
現實生活舉例:班裡三十個同學在考試,誰先做完交卷都要通過按鈕來活動,一旦誰按了按鈕老師桌子上的燈就會變紅,一旦燈變紅,老師(select)就知道有人交卷, 但並不知道是誰交的,所以老師必須輪詢的一個一個同學問,就可以以這種效率極低的方式找到要交卷的學生,把卷子收上來
通過select單執行緒實現併發
服務端:

import socket
import select
sk=socket.socket()
sk.bind(("127.0.0.1",8080))
sk.listen(5)
inputs=[sk,]   #sk是socket物件(本身是檔案描述符,對應一張表存在的),可以監聽多個socket物件
while True:
    #第一步:啟動程式監聽會有阻塞的狀態,sk處於沒有變化的狀態
    r,w,e=select.select(inputs,[],[],5)      #第三步:當客戶端連結後,select一旦監聽到sk,把sk賦值給r,r就是sk(除非有新的使用者來否則sk不會發生變化)當前inputs是[sk,]   第十步:當前r就等於列表中就有sk和conn:當前inputs是[sk,conn]

    for obj in r:                           #第四步:迴圈r,當前r就等於[sk,]     第十一步:迴圈r,當前r就等於[sk,conn]
        #對進來的r進行判斷(遍歷進來的是否是sk還是conn)
        if obj==sk:                         #第五步:判斷如果是sk就是接收新的使用者
            conn,add=obj.accept()            #第六步:obj.accept接收此時連結服務端的socket物件conn
            print(conn)
            inputs.append(conn)              #第八步:把對方的socket物件放到inputs=[]裡的列表裡
        else:                               #第十二步:判斷如果是conn就可以收發訊息
            data_byte=obj.recv(1024)
            print(str(data_byte,'utf8'))
            inp=input('回答%s號客戶>>>'%inputs.index(obj))
            obj.sendall(bytes(inp,'utf8'))

    print('>>',r)                          #第九步:列印

客戶端1:

import socket

sk = socket.socket()
sk.connect(('127.0.0.1', 8080))     #第二步:客戶端連結服務端

while True:
    inp = input(">>>>")
    sk.sendall(bytes(inp, "utf8"))   #第七步:客戶端發信息給服務端
    data = sk.recv(1024)              #接收服務端返回的資訊
    print(str(data, 'utf8'))         #列印服務端返回來的資訊

客戶端2:

import socket

sk = socket.socket()
sk.connect(('127.0.0.1', 8080))     #第二步:客戶端連結服務端

while True:
    inp = input(">>>>")
    sk.sendall(bytes(inp, "utf8"))   #第七步:客戶端發信息給服務端
    data = sk.recv(1024)              #接收服務端返回的資訊
    print(str(data, 'utf8'))         #列印服務端返回來的資訊

服務端列印資訊:每5秒列印[],當有客戶端連結列印客戶端連結的資訊
>> []
>> [<socket.socket fd=348, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080)>]
>> [<socket.socket fd=348, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080)>]
>> []
我是客戶端1
回答1號客戶
>>>給客戶端1返回
>> [<socket.socket fd=368, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 59695)>]
我是客戶端2
回答2號客戶
>>>給客戶端2返回
>> [<socket.socket fd=376, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 59698)>]
>> []
>> []
客戶端1列印資訊:
>>>>我是客戶端1
給客戶端1返回
>>>>
客戶端2列印資訊:
>>>>我是客戶端2
給客戶端2返回
>>>>
2)poll實現方法:它就是select和epoll的過渡階段,它沒有最大連結數的限額
3)epoll實現方法:
第一個函式是建立一個epoll控制代碼,將所有的描述符(fd)拷貝到核心空間,但只拷貝一次。回撥函式,某一個函式或某一個動作成功完成之後會觸發的函式為所有的描述符(fd)繫結一個回撥函式,一旦有資料訪問就是觸發該回調函式,回撥函式將(fd)放到連結串列中,函式判斷連結串列是否為空,且最大啟動項沒有限額
現實生活舉例:班裡三十個同學在考試,誰先做完交卷都要通過按鈕來活動,一旦誰按了按鈕老師桌子上的燈就會變紅,並顯示要交卷子的學生的名字,這樣就可以對應學生是誰收卷子,也可以支援同時有很多人交卷子
(5)selectors模組:是可以實現IO多路複用機制,它具有根據平臺選出最佳的IO多路機制,比如在win的系統上他預設的是select模式而在linux上它預設的epoll。
服務端程式碼:

import selectors                     #封裝了一些相應的操作
import socket

sel = selectors.DefaultSelector()    #通過selectors模組下的DefaultSelector這個類拿到根據作業系統做判斷取一個最好的I/O多路方法sel這個物件

#第六步:執行accept函式
def accept(sock, mask):                             #接收了sock和mask
    conn, addr = sock.accept()                       #sock.accept接收此時連結服務端的socket物件拿到conn和addr
    print('accepted', conn, 'from', addr)
    conn.setblocking(False)                         #設定成非阻塞
    sel.register(conn, selectors.EVENT_READ, read)   #把conn跟read做繫結後程序跳到while迴圈

#第十二步:執行read函式
def read(conn, mask):              #接收了conn和mask
    try:                           #加異常防止客戶端突然斷開
        data = conn.recv(1000)
        if not data:              #如果接收到了資料
            raise Exception
        print('客戶端發來的內容是:', repr(data), '客戶端資訊是:', conn)
        conn.send(data)           #給客戶端返回一條資料
    except Exception as e:
        print('斷開的客戶端資訊是:', conn)
        sel.unregister(conn)      #如果沒有接收到資料做一個關閉解除
        conn.close()

sock = socket.socket()              #建立sock物件
sock.bind(('localhost', 8080))    #繫結
sock.listen(100)                    #監聽
sock.setblocking(False)            #設定非阻塞

##register註冊完成繫結功能
sel.register(sock, selectors.EVENT_READ, accept)   #把sock跟accept做繫結
print("服務端啟動.....")

while True:                         #第一步:程式啟動後走while迴圈                         #第七步:執行完accept函式後到這裡
    #所有操作圍繞一個物件sel核心物件展開的
    events = sel.select()            #第二步:呼叫sel,監聽的內容sock封裝到events物件裡     #第八步:如果客戶端發過來此刻監聽的內容就有變化有倆個物件sock和conn封裝到events物件裡
    for key, mask in events:        #第三步:for迴圈events(可迭代物件)拿到key和mask        #第九步:for迴圈events(可迭代物件)拿到key和mask
        callback = key.data          #第四步:當前key.data是accept函式賦值給callback        #第十步:當前key.data是read函式賦值給callback
        #key.fileobj是拿到的監聽的物件。
        callback(key.fileobj, mask)  #第五步:執行callback執行accpt函式                     #第十一步:執行callback執行read函式裡面放到的之前連結相應的檔案描述符conn

客戶端程式碼:

import socket

sk=socket.socket()

sk.connect(("127.0.0.1",8080))
while 1:
    inp=input(">>>")
    sk.send(inp.encode("utf8"))     #客戶端給服務端發訊息
    data=sk.recv(1024)               #客戶端接收服務端返回的訊息
    print(data.decode("utf8"))

客戶端列印結果:
>>>xixi
xixi
服務端列印結果:
服務端啟動.....
accepted <socket.socket fd=408, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 52674)> from ('127.0.0.1', 52674)
客戶端發來的內容是: b'xixi' 客戶端資訊是: <socket.socket fd=408, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 52674)>
斷開的客戶端資訊是: <socket.socket fd=408, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 52674)>
4.非同步IO:從開始到訊號通知整個過程中不能有一點阻塞是非同步存在
(1)使用者程序發起read操作之後,立刻就可以開始去做氣他的事。而另一方面,從kernel的角度,當它受到一個osynchronous read之後,首先它會立刻返回,所以不會對使用者程序產生任何block。然後,kernel會等待資料準備完成,然後將資料拷貝到使用者記憶體,當這一切完成之後,kernel會給使用者程序傳送一個signal,告訴它read操作完成了
四.四種I/O模型的區別
1.阻塞I/O和非阻塞I/O的區別
(1)阻塞I/O是全程阻塞的
(2)非阻塞I/O是在監聽的時候是非阻塞的
2.同步IO操作和非同步IO操作的區別
(1)同步I/O操作是在IO操作之前有阻塞發生
(2)非同步I/O操作是沒有引起任何的阻塞發生