操作系統IO模式整理
1.關鍵概念理解
- 同步:發起一個調用,得到結果才返回。
- 非同步:調用發起後,調用直接返回;調用方主動詢問被調用方獲取結果,或被調用方通過回調函數。
- 阻塞:調用是指調用結果返回之前,當前線程會被掛起。調用線程只有在得到結果之後才會返回。
- 非阻塞:調用指在不能立刻得到結果之前,該調用不會阻塞當前線程。
同步才有阻塞和非阻塞之分;
阻塞與非阻塞關乎如何對待事情產生的結果(阻塞:不等到想要的結果我就不走了)2.明確進程狀態
理解進程的狀態轉換
- 就緒狀態 -> 運行狀態:處於就緒狀態的進程被調度後,獲得CPU資源(分派CPU時間片),於是進程由就緒狀態轉換為運行狀態。
- 運行狀態 -> 就緒狀態:處於運行狀態的進程在時間片用完後,不得不讓出CPU,從而進程由運行狀態轉換為就緒狀態。此外,在可剝奪的操作系統中,當有更高優先順序的進程就 、 緒時,調度程度將正執行的進程轉換為就緒狀態,讓更高優先順序的進程執行。
- 運行狀態 -> 阻塞狀態:當進程請求某一資源(如外設)的使用和分配或等待某一事件的發生(如I/O操作的完成)時,它就從運行狀態轉換為阻塞狀態。進程以系統調用的形式請求操作系統提供服務,這是一種特殊的、由運行用戶態程序調用操作系統內核過程的形式。
- 阻塞狀態 -> 就緒狀態:當進程等待的事件到來時,如I/O操作結束或中斷結束時,中斷處理程序必須把相應進程的狀態由阻塞狀態轉換為就緒狀態。
3.從操作系統層面執行應用程序理解 IO 模型
阻塞IO模型:
- 簡介:進程會一直阻塞,直到數據拷貝完成應用程序調用一個IO函數,導致應用程序阻塞,等待數據準備好。 如果數據沒有準備好,一直等待….數據準備好了,從內核拷貝到用戶空間,IO函數返回成功指示。 我們 第一次接觸到的網路編程都是從 listen()、send()、recv()等介面開始的。使用這些介面可以很方便的構建伺服器 /客戶機的模型。
- 阻塞I/O模型圖:在調用recv()/recvfrom()函數時,發生在內核中等待數據和複製數據的過程。
當調用recv()函數時,系統首先查是否有準備好的數據。如果數據沒有準備好,那麼系統就處於等待狀態。當數據準備好後,將數據從系統緩衝區複製到用戶空間,然後該函數返回。在套接應用程序中,當調用recv()函數時,未必用戶空間就已經存在數據,那麼此時recv()函數就會處於等待狀態。
阻塞模式給網路編程帶來了一個很大的問題,如在調用 send()的同時,線程將被阻塞,在此期間,線程將無法執行任何運算或響應任何的網路請求。這給多客戶機、多業務邏輯的網路編程帶來了挑戰。這時,我們可能會選擇多線程的方式來解決這個問題。應對多客戶機的網路應用,最簡單的解決方式是在伺服器端使用多線程(或多進程)。多線程(或多進程)的目的是讓每個連接都擁有獨立的線程(或進程),這樣任何一個連接的阻塞都不會影響其他的連接。
具體使用多進程還是多線程,並沒有一個特定的模式。傳統意義上,進程的開銷要遠遠大於線程,所以,如果需要同時為較多的客戶機提供服務,則不推薦使用多進程;如果單個服務執行體需要消耗較多的 CPU 資源,譬如需要進行大規模或長時間的數據運算或文件訪問,則進程較為安全。非阻塞IO模型
- 簡介:非阻塞IO通過進程反覆調用IO函數(多次系統調用,並馬上返回);在數據拷貝的過程中,進程是阻塞的; 我們把一個SOCKET介面設置為非阻塞就是告訴內核,當所請求的I/O操作無法完成時,不要將進程睡眠,而是返回一個錯誤。這樣我們的I/O操作函數將不斷的測試數據是否已經準備好,如果沒有準備好,繼續測試,直到數據準備好為止。在這個不斷測試的過程中,會大量的佔用CPU的時間。
IO復用模型:
- 簡介:IO multiplexing就是我們說的select,poll,epoll,有些地方也稱這種IO方式為event driven IO。select/epoll的好處就在於單個process就可以同時處理多個網路連接的 IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。
當用戶進程調用了select,那麼整個進程會被block,而同時,kernel會「監視」所有select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。
所以,I/O 多路復用的特點是通過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函數就可以返回。
非同步IO模型
- 簡介:用戶進程發起read操作之後,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對用戶進程產生任何block。然後,kernel會等待數據準備完成,然後將數據拷貝到用戶內存,當這一切都完成之後,kernel會給用戶進程發送一個signal,告訴它read操作完成了。
4.區別IO多路復用中的select poll epoll
select
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select
函數監視的文件描述符分3類,分別是writefds
、readfds
、和exceptfds
。調用後select函數會阻塞,直到有描述符就緒(有數據 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設為null即可),函數返回。當select函數返回後,可以 通過遍歷fdset,來找到就緒的描述符
poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同與select使用三個點陣圖來表示三個fdset的方式,poll使用一個 pollfd
的指針實現。pollfd並沒有最大數量限制(但是數量過大後性能也是會下降)。 和select函數一樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符。
epoll
epoll是通過事件的就緒通知方式,調用epoll_create
創建實例,調用epoll_ctl
添加或刪除監控的文件描述符,調用epoll_wait
阻塞住,直到有就緒的文件描述符,通過epoll_event
參數返回就緒狀態的文件描述符和事件。
epoll操作過程需要三個介面,分別如下: int epoll_create(int size);//創建一個epoll的句柄
,size用來告訴內核這個監聽的數目一共有多大 生成一個 epoll 專用的文件描述符,其實是申請一個內核空間,用來存放想關注的 socket fd 上是否發生以及發生了什麼事件。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待 I/O 事件的發生;返回發生事件數。參數說明:
epfd
: 由 epoll_create() 生成的 Epoll 專用的文件描述符;epoll_event
: 用於回傳代處理事件的數組;maxevents
: 每次能處理的事件數;timeout
: 等待 I/O 事件發生的超時值;
區別總結
(1)select,poll實現需要自己不斷輪詢所有fd集合,直到設備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設備就緒時,調用回調函數,把就緒fd放入就緒鏈表中,並喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,但是select和poll在「醒著」的時候要遍歷整個fd集合,而epoll在「醒著」的時候只要判斷一下就緒鏈表是否為空就行了,這節省了大量的CPU時間。這就是回調機制帶來的性能提升。
(2)select,poll每次調用都要把fd集合從用戶態往內核態拷貝一次,epoll 通過 mmap 把內核空間和用戶空間映射到同一塊內存,省去了拷貝的操作。應用舉例
- Tornado:
- 使用單線程的方式,避免線程切換的性能開銷,同時避免在使用一些函數介面時出現線程不安全的情況
- 支持非同步非阻塞網路IO模型,避免主進程阻塞等待。
tornado 的 IOLoop 模塊 是非同步機制的核心,它包含了一系列已經打開的文件描述符和每個描述符的處理器 (handlers)。這些 handlers 就是對 select, poll , epoll等的封裝。(所以本質上說是 IO 復用)
- Django沒有用非同步,通過使用多進程的WSGI server(比如uWSGI)來實現並發,這也是WSGI普遍的做法。
推薦閱讀:
※【瞎折騰-01】deepin系統菜單快捷鍵魔改(恢復快捷鍵)
※想實現一個linux內核安全功能模塊的技術思路是怎樣的?
※linux掛載機制,訪問父目錄的細節是怎樣的?
※在Linux內核模塊中對空指針解引用,為什麼內核不掛?