<input id="0qass"><u id="0qass"></u></input>
  • <input id="0qass"><u id="0qass"></u></input>
  • <menu id="0qass"><u id="0qass"></u></menu>

    Linux 操作系統原理 — IO 模型

    目錄

    基本概念

    同步與異步

    • 同步是指一個任務的完成需要依賴另外一個任務時,只有等待被依賴的任務完成后,依賴的任務才能算完成。

    • 異步是指不需要等待被依賴的任務完成,只是通知被依賴的任務要完成什么工作,依賴的任務也立即執行,只要自己完成了整個任務就算完成了,異步一般使用狀態、通知和回調。

    阻塞與非阻塞

    • 阻塞是指調用結果返回之前,當前線程會被掛起,一直處于等待消息通知,不能夠執行其他業務。
    • 非阻塞是指在不能立刻得到結果之前,該函數不會阻塞當前線程,而會立刻返回。

    Linux 的五種 IO 模型

    對于一次 IO 訪問,數據會先被拷貝到內核的緩沖區中,然后才會從內核的緩沖區拷貝到應用程序的地址空間。需要經歷兩個階段:

    1. 準備數據。
    2. 將數據從內核緩沖區拷貝到用戶態進程的地址空間。

    對于一個對 Socket 的輸入操作,第一步一般來說是等待數據從網絡傳到本地。當數據包到達的時候,數據將會從網絡層拷貝到內核的緩存中;第二步是從內核中把數據拷貝到應用程序的數據區中。
    在這里插入圖片描述
    由于存在這兩個階段,Linux 具有下面五種 I/O 模型。

    • 阻塞 I/O
    • 非阻塞 I/O
    • 信號驅動 I/O(SIGIO)
    • 異步 I/O
    • I/O 多路復用

    阻塞 IO

    是最普遍的 I/O 模型,大部分程序都是用的是阻塞式 I/O。Linux 中,默認情況下所有的 Socket 都是阻塞 IO 的。

    對于 UDP 套接字來說,數據就緒的標志比較簡單:

    • 已經收到了一整個數據報
    • 沒有收到

    而 TCP 則比較復雜,需要附加一些其他變量。如下圖所示,當一個用戶進程調用了 recvfrom(),內核就進入 IO 的第一個階段:準備數據(內核需要等待足夠多的數據再拷貝),這個過程需要等待,用戶進程會被阻塞,等內核將數據準備好,然后拷貝到用戶地址空間,內核返回結果,用戶進程才從阻塞態進入就緒態。(如果系統調用收到一個中斷信號,則它的調用會被中斷)。

    所以,我們稱這個用戶進程在調用 recvfrom() 開始,一直到從 recvfrom() 返回的這段時間內都是阻塞的。當 recvfrom() 正常返回后。用戶進程才可以繼續它的操作。

    在這里插入圖片描述

    非阻塞 IO

    Linux 下可以通過設置 Socket 為 non-blocking 模式。。當我們將一個套接字設置為非阻塞模式時,相當于告訴了系統內核:“當我請求的 I/O 操作不能馬上完成,不要讓進程休眠等待,,請馬上返回一個錯誤給我”。

    當用戶進程發出 read() 調用時,如果 Kernel 中的數據還沒有準備好,那么它并不會阻塞用戶進程,而是立刻返回一個 EWOULDBLOCK 的 Error。用戶進程判斷結果是一個 Error 時,它就知道數據還沒有準備好,于是它可以再次發送 read() 調用。一旦 Kernel 中的數據準備好了,并且又再次收到了用戶進程的系統調用,那么它馬上就將數據拷貝到了用戶內存,然后返回。

    非阻塞 IO 模式下用戶進程需要不斷地詢問內核的數據準備好了沒有,如果沒有準備好,那么在某些場景中,用戶進程可以去做別的事情而不需要一直等待。
    在這里插入圖片描述
    當一個應用程序使用了非阻塞模式的套接字,它就會使用一個循環來不停的測試是否一個文件描述符合有數據可讀(稱作 Polling,輪詢)。應用程序不停地 Plling 內核來檢查是否 I/O 操作已經就緒。這是對 CPU 資源的極大浪費,所以這種模式平時使用中不是很普遍。

    同步 IO(信號驅動)

    當我們將一個套接字設置為信號驅動 I/O 模式,讓內核文件描述符就緒后,通過 Signal(信號)通知用戶進程,用戶進程再通過系統調用讀取數據,我們將這種模式稱之為信號驅動 I/O 模式。

    對于信號驅動 I/O 模式,好處在于等待數據的時候不會阻塞,程序可以做自己的事情。當有數據到達的時候,系統內核會向應用程序主動發送一個 SIGIO 信號進行通知,所以應用程序就可以獲得更大的靈活性,而不必為阻塞等待數據進行額外的編碼。

    此方式的本質屬于同步 IO,因為實際讀取數據到用戶進程緩存的工作仍然是由用戶進程自己負責的。
    在這里插入圖片描述

    為了在一個套接字上使用信號驅動 I/O 操作,必須有下面三個步驟:

    1. 必須設定一個處理 SIGIO 信號的函數。
    2. 必須設定套接字的擁有者,一般使用 fcntl 函數的 F_SETOWN 參數來設定擁有著。
    3. 套接字必須被允許使用異步 I/O(接受 SIGIO)。一般使用 fcntl 函數的 F_SETFL 命令,O_ASYNC 為參數來實現。

    異步 IO

    用戶進程發起 read() 調用之后,立刻就可以開始去做其它的事。內核收到一個異步 IO read 之后,會立刻返回,不會阻塞用戶進程。內核會等待數據準備完成,然后將數據拷貝到用戶內存,當這一切都完成之后,內核會給用戶進程發送一個 Signal(信號),告訴它 read() 完成了。用戶進程再從用戶內存讀取數據。

    異步 I/O 與信號驅 動I/O 區別:

    • 信號驅動 I/O 模式下,內核在操作可以操作的時候通知給程序發送 SIGIO 消息。
    • 異步 I/O 模式下,內核在所有操作都被內核操作結束后才會通知給程序。

    當進程進行 I/O 操作時,進程傳遞給內核它的文件描述符、緩存區指針、緩存區的大小以及一個偏移量 offset,以及在內核結束所有操作后和給進程發送通知,這種調用也是立即返回的,程序不需要阻塞來等待程序的就緒。
    在這里插入圖片描述

    IO 多路復用

    通過一種機制,一個進程可以監視多個文件描述符(套接字描述符),一旦某個文件描述符就緒(一般是讀就緒,或者寫就緒),能夠通知程序進行相應的讀寫操作。這樣就不需要每個用戶進程不斷的詢問內核數據準備好了沒有,也不需要內核給用戶進程發送信號了。
    在這里插入圖片描述
    在使用多路復用 I/O 技術時,會調用 select() 函數和 poll() 函數,在調用他們的時候阻塞,而不是在調用 recvfrom() 的時候阻塞。

    當調用 select() 函數阻塞的時候,select() 等待數據報套接字進入讀就緒狀態。當 select() 返回的時候,就是套接字可以讀取數據的時候。這是很好就可以調用 recvfrom() 函數來將數據拷貝到緩存區中。

    和阻塞模式比較,select() 和 poll() 并沒有特別的地方。而且,在阻塞模式下只需要調用一個函數:讀取或發送,在使用了多路復用技術后,需要調用兩個函數:select() 或 poll(),然后才能調用 recvfrom() 進行真正的讀寫。

    但多路復用的好處在于,能同時等待多個文件描述符,而這些文件描述符其中的任意一個進入讀取狀態,select() 函數就可以返回。

    應用場景:

    • 當一個客戶端需要同時處理多個文件描述符的輸入輸出操作時。
    • 當程序需要同時進行多個套接字的操作時。
    • 如果一個 TCP 服務器程序同時處理正在偵聽網絡連接的套接字和已經連接好的套接字。
    • 如果一個服務器程序同時使用 TCP 和 UDP 協議。
    • 如果一個服務器同時使用多種服務并且每種服務使用協議不同。

    select

    在這里插入圖片描述
    Kernel 會監視所有 select() 負責的若干個 Socket,當任意 Socket 中的數據準備好了,select 就會返回。這個時候用戶進程再調用 read(),將數據從 Kernel 拷貝到用戶進程。

    int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    

    形參列表:

    • 讀、寫、異常、集合中的文件描述符的最大值+1
    • 讀集合
    • 寫集合
    • 異常集合
    • 超時結構體

    select() 的返回值為檢測到的事件個數并且返回哪些 I/O 發生了事件,遍歷這些事件,進而處理事件。

    select() 監視的文件描述符分 3 類:

    1. writefds
    2. readfds
    3. exceptfds

    調用后 select() 會阻塞住,直到有描述符就緒(有數據可讀、可寫、或 Except),或者超時(timeout 形參指定等待時間,如果希望立即返回,則設為 null)函數返回。當 select() 返回后,可以通過遍歷 fdset,來找到就緒的描述符。

    select() 的一個缺點在于單個進程能夠監視的文件描述符數量存在限制,在 Linux 上一般為 1024 個,可以通過調整內核的參數進行修改。另外,select() 中的 fd_set 集合容量同樣具有限制(FD_SETSIZE=1024)這需要重新編譯內核。

    poll

    poll() 使用了一個 pollfd 的指針實現。

    int poll(struct pollfd *fds, unsigned int nfds, int timeout);
    

    第一參數是指向結構體數組,每個數組元素都是一個 pollfd 結構。結構體類型參數 pollfd 包含了要監視的 Event 和發生的 Event。

    struct pollfd {
    	int fd; 		/* file descriptor */
    	short events; 	/* requested events to watch */
    	short revents;	/* returned events witnessed */
    };
    

    和 select() 一樣,poll() 返回后,內核要遍歷所有文件描述符,直到找到所有發生事件的 pollfd 文件描述符來獲取其中就緒的描述符。區別在于 poll 沒有監聽的最大數量限制。

    epoll

    在這里插入圖片描述

    epoll 使用一個文件描述符來管理多個描述符,將用戶關心的文件描述符的事件存放到內核的一個事件表中,采用監聽回調的機制,這樣在用戶空間和內核空間的數據拷貝只需要進行一次,避免再次遍歷就緒的文件描述符列表,從而提升了性能。

    相比于 select() 與 poll(),epoll() 最大的好處在于它不會隨著監聽 fd 數目的增長而降低效率,內核中 select() 與 poll() 的實現時采用輪詢來處理的,輪詢的 fd 數目越多,自然耗時越多。而 epoll() 實現是基于回調的,如果 fd 有期望的事件發生就通過回調函數將其加入 epoll 就緒隊列中,也就說說它只關心 “活躍” 的 fd,與 fd 的數目無關。

    另外,內核空間和用戶空間拷貝問題,在這個問題上 select/poll 采取的是內存拷貝的方式,而 epoll 采用的共享內存(緩沖區共享)的方式,避免了一次拷貝。

    epoll 不僅會告訴應用程序有 I/O 事件到來,還會告訴應用程序相關的信息,這些信息是應用填充的,因此根據這些消息應用程序就能直接定位到事件,而不必遍歷整個 fd 集合。

    epoll 的操作過程需要三個接口:

    1. 創建一個 epoll 的句柄,形參 size 用來告訴內核這個監聽的數目一共有多大。
    int epoll_create(int size)
    1. 對指定描述符 fd 執行 op 操作。
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
    • epfd:是 epoll_create() 的返回值。
    • op:表示操作,用三個宏來表示:EPOLL_CTL_ADD,EPOLL_CTL_DEL,EPOLL_CTL_MOD。分別添加、刪除和修改對 fd 的監聽事件。
    • fd:是需要監聽的 fd(文件描述符)。
    • epoll_event:是告訴內核需要監聽什么事件,struct epoll_event 結構如下:
    struct epoll_event {
    	__uint32_t events; /* Epoll events */ 
    	epoll_data_t data; /* User data variable */
    };
    

    events 可以是以下幾個宏的集合:

    • EPOLLIN :表示對應的文件描述符可以讀(包括對端 Socket 正常關閉);
    • EPOLLOUT:表示對應的文件描述符可以寫;
    • EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來);
    • EPOLLERR:表示對應的文件描述符發生錯誤;
    • EPOLLHUP:表示對應的文件描述符被掛斷;
    • EPOLLET: 將 epoll 設為邊緣觸發(Edge Triggered)模式,這是相對于水平觸發(Level Triggered)來說的;
    • EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個 Socket 的話,需要再次把這個 Socket 加入到 epoll 隊列里。
    1. 等待 epfd 上的 IO 事件,最多返回 maxevents 個事件。
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    
    • events:用來從內核得到事件的集合
    • maxevents:告之內核這個 events 有多大,這個 maxevents 的值不能大于創建 epoll_create() 時指定的 size
    • timeout:超時時間,單位毫秒,0 表示立即返回,-1 表示不確定,也有說法說是永久阻塞

    該函數返回需要處理的事件數目,如返回 0 表示已超時。

    epoll 的兩種工作模式:

    1. LT(Level Trigger,水平觸發)模式:當 epoll_wait 檢測到描述符就緒,將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用 epoll_wait 時,會再次響應應用程序并通知此事件。LT 模式是默認的工作模式,同時支持阻塞和非阻塞 Socket。
    2. ET(Edge Trigger,邊緣觸發)模式:當 epoll_wait 檢測到描述符就緒,將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用 epoll_wait 時,不會再次響應應用程序并通知此事件。ET 是高速工作方式,只支持非阻塞 Socket。ET 模式減少了 epoll 事件被重復觸發的次數,因此效率要比 LT 模式高。

    三者的比較

    用通俗的話來講,假如有三個老師分別是 select,poll,epoll,每次當老師要去收全班同學的作業時,也就是當老師被調用時:select 和 poll 老師就會一個一個的檢查在這個班的所有的同學的作業并拿走作業,而 epoll 老師設置了一個講臺,說誰寫完了就放在講臺上,那當 epoll 老師工作的時候,只需要在講臺上拿走完成的作業,而不用全部遍歷。

    幾種 I/O 模式比較

    在這里插入圖片描述

    可以看出,阻塞程度:阻塞 IO > 非阻塞 IO > 多路復用 IO > 信號驅動 IO > 異步 IO,效率是由低到高的。

    ??2020 CSDN 皮膚主題: 編程工作室 設計師:CSDN官方博客 返回首頁
    實付 49.00元
    使用余額支付
    點擊重新獲取
    掃碼支付
    錢包余額 0

    抵扣說明:

    1.余額是錢包充值的虛擬貨幣,按照1:1的比例進行支付金額的抵扣。
    2.余額無法直接購買下載,可以購買VIP、C幣套餐、付費專欄及課程。

    余額充值
    多乐彩