1. 程式人生 > 實用技巧 >Linux 五種IO模型

Linux 五種IO模型

1 概念說明

在進行解釋之前,首先要說明幾個概念:

使用者空間和核心空間:

現在作業系統都是採用虛擬儲存器,那麼對32位作業系統而言,它的定址空間(虛擬儲存空間)為4G(2的32次方)。
作業系統的核心是核心,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也有訪問底層硬體裝置的所有許可權。
為了保證使用者程序不能直接操作核心(kernel),保證核心的安全,作業系統將虛擬空間劃分為兩部分,一部分為核心空間,
一部分為使用者空間。針對linux作業系統而言,將最高的1G位元組(從虛擬地址0xC0000000到0xFFFFFFFF),供核心使用,
稱為核心空間,而將較低的3G位元組(從虛擬地址0x00000000到0xBFFFFFFF),供各個程序使用,稱為使用者空間。

程序切換:

為了控制程序的執行,核心必須有能力掛起正在CPU上執行的程序,並恢復以前掛起的某個程序的執行。這種行為被稱為程序切換。因此可以說,任何程序都是在作業系統核心的支援下執行的,是與核心緊密相關的。

從一個程序的執行轉到另一個程序上執行,這個過程中經過下面這些變化:

儲存處理機上下文,包括程式計數器和其他暫存器。

更新PCB資訊。

把程序的PCB移入相應的佇列,如就緒、在某事件阻塞等佇列。

選擇另一個程序執行,並更新其PCB。

更新記憶體管理的資料結構。

恢復處理機上下文。

程序的阻塞:

正在執行的程序,由於期待的某些事件未發生,如請求系統資源失敗、等待某種操作的完成、新資料尚未到達或無新工作做等,

則由系統自動執行阻塞原語(Block),使自己由執行狀態變為阻塞狀態。可見,程序的阻塞是程序自身的一種主動行為,
也因此只有處於執行態的程序(獲得CPU),才可能將其轉為阻塞狀態。當程序進入阻塞狀態,是不佔用CPU資源的。

檔案描述符fd:

檔案描述符(File descriptor)是電腦科學中的一個術語,是一個用於表述指向檔案的引用的抽象化概念。

檔案描述符在形式上是一個非負整數。實際上,它是一個索引值,指向核心為每一個程序所維護的該程序開啟檔案的記錄表。
當程式開啟一個現有檔案或者建立一個新檔案時,核心向程序返回一個檔案描述符。
在程式設計中,一些涉及底層的程式編寫往往會圍繞著檔案描述符展開。

但是檔案描述符這一概念往往只適用於UNIX、Linux這樣的作業系統。

快取 IO:

快取 IO 又被稱作標準 IO,大多數檔案系統的預設 IO 操作都是快取 IO。在 Linux 的快取 IO 機制中,
作業系統會將 IO 的資料快取在檔案系統的頁快取( page cache )中,也就是說,
資料會先被拷貝到作業系統核心的緩衝區中,然後才會從作業系統核心的緩衝區拷貝到應用程式的地址空間。

快取 IO 的缺點:  

資料在傳輸過程中需要在應用程式地址空間和核心進行多次資料拷貝操作,這些資料拷貝操作所帶來的 CPU 以及記憶體開銷是非常大的。

2.IO模型簡介

網路IO的本質是socket的讀取,socket在linux系統被抽象為流,IO可以理解為對流的操作。剛才說了,對於一次IO訪問(以read舉例),資料會先被拷貝到作業系統核心的緩衝區中,然後才會從作業系統核心的緩衝區拷貝到應用程式的地址空間。所以說,當一個read操作發生時,它會經歷兩個階段:
第一階段:等待資料準備 (Waiting for the data to be ready)。

第二階段:將資料從核心拷貝到程序中 (Copying the data from the kernel to the process)。

對於socket流而言:

第一步:通常涉及等待網路上的資料分組到達,然後被複制到核心的某個緩衝區。

第二步:把資料從核心緩衝區複製到應用程序緩衝區。

網路應用需要處理的無非就是兩大類問題,網路IO,資料計算。相對於後者,網路IO的延遲,給應用帶來的效能瓶頸大於後者。網路IO的模型大致有如下幾種:

BIO – 阻塞模式I/O(bloking IO)

NIO – 非阻塞模式I/O(non-blocking IO)

IO Multiplexing - I/O多路複用模型(multiplexing IO)

AIO – 非同步I/O模型(asynchronous IO)

SIO - 訊號驅動I/OM模型(signal-driven IO)

注:由於signal driven IO在實際中並不常用,所以我這隻提及剩下的四種IO Model。  

三、 IO模型

1. BIO – 阻塞模式I/O

使用者程序從發起請求,到最終拿到資料前,一直掛起等待; 資料會由使用者程序完成拷貝  

'''
舉個例子:一個人去 商店買一把菜刀,
他到商店問老闆有沒有菜刀(發起系統呼叫)
如果有(表示在核心緩衝區有需要的資料)
老闆直接把菜刀給買家(從核心緩衝區拷貝到使用者緩衝區)
這個過程買家一直在等待

如果沒有,商店老闆會向工廠下訂單(IO操作,等待資料準備好)
工廠把菜刀運給老闆(進入到核心緩衝區)
老闆把菜刀給買家(從核心緩衝區拷貝到使用者緩衝區)
這個過程買家一直在等待
是同步io
'''

程式碼示例:

import socket

server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)


while True:
    conn, addr = server.accept()
    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0:break
            print(data)
            conn.send(data.upper())
        except ConnectionResetError as e:
            break
    conn.close()
    
# 在服務端開設多程序或者多執行緒 程序池執行緒池 其實還是沒有解決IO問題	
該等的地方還是得等 沒有規避
只不過多個人等待的彼此互不干擾

2. NIO – 非阻塞模式I/O

使用者程序發起請求,如果資料沒有準備好,那麼立刻告知使用者程序未準備好;此時使用者程序可選擇繼續發起請求、或者先去做其他事情,稍後再回來繼續發請求,直到被告知資料準備完畢,可以開始接收為止; 資料會由使用者程序完成拷貝

'''
舉個例子:一個人去 商店買一把菜刀,
他到商店問老闆有沒有菜刀(發起系統呼叫)
老闆說沒有,在向工廠進貨(返回狀態)
買家去別地方玩了會,又回來問,菜刀到了麼(發起系統呼叫)
老闆說還沒有(返回狀態)
買家又去玩了會(不斷輪詢)
最後一次再問,菜刀有了(資料準備好了)
老闆把菜刀遞給買家(從核心緩衝區拷貝到使用者緩衝區)

整個過程輪詢+等待:輪詢時沒有等待,可以做其他事,從核心緩衝區拷貝到使用者緩衝區需要等待
是同步io

同一個執行緒,同一時刻只能監聽一個socket,造成浪費,引入io多路複用,同時監聽讀個socket
'''

程式碼示例:

"""
要自己實現一個非阻塞IO模型
"""
import socket
import time


server = socket.socket()
server.bind(('127.0.0.1', 8081))
server.listen(5)
server.setblocking(False)
# 將所有的網路阻塞變為非阻塞
r_list = []
del_list = []
while True:
    try:
        conn, addr = server.accept()
        r_list.append(conn)
    except BlockingIOError:
        # time.sleep(0.1)
        # print('列表的長度:',len(r_list))
        # print('做其他事')
        for conn in r_list:
            try:
                data = conn.recv(1024)  # 沒有訊息 報錯
                if len(data) == 0:  # 客戶端斷開連結
                    conn.close()  # 關閉conn
                    # 將無用的conn從r_list刪除
                    del_list.append(conn)
                    continue
                conn.send(data.upper())
            except BlockingIOError:
                continue
            except ConnectionResetError:
                conn.close()
                del_list.append(conn)
        # 揮手無用的連結
        for conn in del_list:
            r_list.remove(conn)
        del_list.clear()

# 客戶端
import socket


client = socket.socket()
client.connect(('127.0.0.1',8081))


while True:
    client.send(b'hello world')
    data = client.recv(1024)
    print(data)

3. IO Multiplexing - I/O多路複用模型

類似BIO,只不過找了一個代理,來掛起等待,並能同時監聽多個請求; 資料會由使用者程序完成拷貝

'''
舉個例子:多個人去 一個商店買菜刀,
多個人給老闆打電話,說我要買菜刀(發起系統呼叫)
老闆把每個人都記錄下來(放到select中)
老闆去工廠進貨(IO操作)
有貨了,再挨個通知買到的人,來取刀(通知/返回可讀條件)
買家來到商店等待,老闆把到給買家(從核心緩衝區拷貝到使用者緩衝區)

多路複用:老闆可以同時接受很多請求(select模型最大1024個,epoll模型),
但是老闆把到給買家這個過程,還需要等待,
是同步io


強調:
​ 1. 如果處理的連線數不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server效能更好,可能延遲還更大。select/epoll的優勢並不是對於單個連線能處理得更快,而是在於能處理更多的連線。
​ 2. 在多路複用模型中,對於每一個socket,一般都設定成為non-blocking,但是,如上圖所示,整個使用者的process其實是一直被block的。只不過process是被select這個函式block,而不是被socket IO給block。
​ 結論: select的優勢在於可以處理多個連線,不適用於單個連線
'''

程式碼示例:

"""
當監管的物件只有一個的時候 其實IO多路複用連阻塞IO都比比不上!!!
但是IO多路複用可以一次性監管很多個物件

server = socket.socket()
conn,addr = server.accept()

監管機制是作業系統本身就有的 如果你想要用該監管機制(select)
需要你匯入對應的select模組
"""
import socket
import select

server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)
server.setblocking(False)
read_list = [server]

while True:
    r_list, w_list, x_list = select.select(read_list, [], [])
    """
    幫你監管
    一旦有人來了 立刻給你返回對應的監管物件
    """
    # print(res)  # ([<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080)>], [], [])
    # print(server)
    # print(r_list)
    for i in r_list:  #
        """針對不同的物件做不同的處理"""
        if i is server:
            conn, addr = i.accept()
            # 也應該新增到監管的佇列中
            read_list.append(conn)
        else:
            res = i.recv(1024)
            if len(res) == 0:
                i.close()
                # 將無效的監管物件 移除
                read_list.remove(i)
                continue
            print(res)
            i.send(b'heiheiheiheihei')

 # 客戶端
import socket
client = socket.socket()
client.connect(('127.0.0.1',8080))
while True:

    client.send(b'hello world')
    data = client.recv(1024)
    print(data)

IO複用中的三個API(select、poll和epoll)的區別和聯絡

select,poll,epoll都是IO多路複用的機制,I/O多路複用就是通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),
能夠通知應用程式進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,
也就是說這個讀寫過程是阻塞的,而非同步I/O則無需自己負責進行讀寫,非同步I/O的實現會負責把資料從核心拷貝到使用者空間。三者的原型如下所示: int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); int poll(struct pollfd *fds, nfds_t nfds, int timeout); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

1.select

select的第一個引數nfds為fdset集合中最大描述符值加1,fdset是一個位數組,其大小限制為__FD_SETSIZE(1024),位陣列的每一位代表其對應的描述符是否需要被檢查。第二三四引數表示需要關注讀、寫、錯誤事件的檔案描述符位陣列,這些引數既是輸入引數也是輸出引數,可能會被核心修改用於標示哪些描述符上發生了關注的事件,所以每次呼叫select前都需要重新初始化fdset。timeout引數為超時時間,該結構會被核心修改,其值為超時剩餘的時間select的呼叫步驟如下:

(1)使用copy_from_user從使用者空間拷貝fdset到核心空間

(2)註冊回撥函式__pollwait

(3)遍歷所有fd,呼叫其對應的poll方法(對於socket,這個poll方法是sock_poll,sock_poll根據情況會呼叫到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll為例,其核心實現就是__pollwait,也就是上面註冊的回撥函式。

(5)__pollwait的主要工作就是把current(當前程序)掛到裝置的等待佇列中,不同的裝置有不同的等待佇列,對於tcp_poll 來說,其等待佇列是sk->sk_sleep(注意把程序掛到等待佇列中並不代表程序已經睡眠了)。在裝置收到一條訊息(網路裝置)或填寫完檔案數 據(磁碟裝置)後,會喚醒裝置等待佇列上睡眠的程序,這時current便被喚醒了。

(6)poll方法返回時會返回一個描述讀寫操作是否就緒的mask掩碼,根據這個mask掩碼給fd_set賦值。

(7)如果遍歷完所有的fd,還沒有返回一個可讀寫的mask掩碼,則會呼叫schedule_timeout是呼叫select的程序(也就是 current)進入睡眠。當裝置驅動發生自身資源可讀寫後,會喚醒其等待佇列上睡眠的程序。如果超過一定的超時時間(schedule_timeout 指定),還是沒人喚醒,則呼叫select的程序會重新被喚醒獲得CPU,進而重新遍歷fd,判斷有沒有就緒的fd。

(8)把fd_set從核心空間拷貝到使用者空間。

總結下select的幾大缺點:

(1)每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,這個開銷在fd很多時會很大

(2)同時每次呼叫select都需要在核心遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大

(3)select支援的檔案描述符數量太小了,預設是1024

2.poll

 poll與select不同,通過一個pollfd陣列向核心傳遞需要關注的事件,故沒有描述符個數的限制,pollfd中的events欄位和revents分別用於標示關注的事件和發生的事件,故pollfd陣列只需要被初始化一次。

 poll的實現機制與select類似,其對應核心中的sys_poll,只不過poll向核心傳遞pollfd陣列,然後對pollfd中的每個描述符進行poll,相比處理fdset來說,poll效率更高。poll返回後,需要對pollfd中的每個元素檢查其revents值,來得指事件是否發生。

3.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()時便得到通知。 epoll既然是對select和poll的改進,就應該能避免上述的三個缺點。那epoll都是怎麼解決的呢?在此之前,我們先看一下epoll 和select和poll的呼叫介面上的不同,
select和poll都只提供了一個函式——select或者poll函式。而epoll提供了三個函 數,epoll_create,epoll_ctl和epoll_wait,epoll_create是建立一個epoll控制代碼;
epoll_ctl是注 冊要監聽的事件型別;epoll_wait則是等待事件的產生。   對於第一個缺點,epoll的解決方案在epoll_ctl函式中。每次註冊新的事件到epoll控制代碼中時(在epoll_ctl中指定 EPOLL_CTL_ADD),
會把所有的fd拷貝進核心,而不是在epoll_wait的時候重複拷貝。epoll保證了每個fd在整個過程中只會拷貝 一次。   對於第二個缺點,epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的裝置等待佇列中,
而只在 epoll_ctl時把current掛一遍(這一遍必不可少)併為每個fd指定一個回撥函式,當裝置就緒,喚醒等待佇列上的等待者時,
就會呼叫這個回撥 函式,而這個回撥函式會把就緒的fd加入一個就緒連結串列)。
epoll_wait的工作實際上就是在這個就緒連結串列中檢視有沒有就緒的fd(利用 schedule_timeout()實現睡一會,判斷一會的效果,和select實現中的第7步是類似的)。   對於第三個缺點,epoll沒有這個限制,它所支援的FD上限是最大可以開啟檔案的數目,這個數字一般遠大於2048,舉個例子,
在1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關係很大。  

總結:  

(1)select,poll實現需要自己不斷輪詢所有fd集合,直到裝置就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要呼叫 epoll_wait不斷輪詢就緒連結串列,
期間也可能多次睡眠和喚醒交替,但是它是裝置就緒時,呼叫回撥函式,把就緒fd放入就緒連結串列中,並喚醒在 epoll_wait中進入睡眠的程序。
雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的 時候只要判斷一下就緒連結串列是否為空就行了,
這節省了大量的CPU時間,這就是回撥機制帶來的效能提升。 (2)select,poll每次呼叫都要把fd集合從使用者態往核心態拷貝一次,並且要把current往裝置等待佇列中掛一次,
而epoll只要 一次拷貝,而且把current往等待佇列上掛也只掛一次(在epoll_wait的開始,
注意這裡的等待佇列並不是裝置等待佇列,只是一個epoll內 部定義的等待佇列),這也能節省不少的開銷。 注:這三種IO多路複用模型在不同的平臺有著不同的支援,而epoll在windows下就不支援,好在我們有selectors模組,幫我們預設選擇當前平臺下最合適的

4. AIO – 非同步I/O模型

發起請求立刻得到回覆,不用掛起等待; 資料會由核心程序主動完成拷貝  

'''
舉個例子:還是買菜刀
現在是網上下單到商店(系統呼叫)
商店確認(返回)
商店去進貨(io操作)
商店收到貨把貨發個賣家(從核心緩衝區拷貝到使用者緩衝區)
買家收到貨(指定訊號)

整個過程無等待
非同步io

AIO框架在windows下使用windows IOCP技術,在Linux下使用epoll多路複用IO技術模擬非同步IO

市面上多數的高併發框架,都沒有使用非同步io而是用的io多路複用,因為io多路複用技術很成熟且穩定,並且在實際的使用過程中,非同步io並沒有比io多路複用效能提升很多,沒有達到很明顯的程度
並且,真正的AIO編碼難度比io多路複用高很多
'''

抄自於:http://liuqingzheng.top/python/Python%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/25-IO%E6%A8%A1%E5%9E%8B/