1、簡要分析html
一個socket對應一個數據流,經過I/O操做中的read從流中讀入數據,write向流中寫入數據。當read時,socket流中沒有數據的話,read阻塞,線程睡眠,CPU開始作其餘的任務,流中有數據可讀時,read返回。linux
在阻塞IO模式下,一個線程只能處理一個IO事件。若是處理多個事件,須要多線程或多進程,可是效率比較低。web
一、若是採用非阻塞方式,須要不斷輪訓全部的流,假設共有N個socket流streams[N], 以下:sql
// busy poll
while True:
for stream in streams[N]: if stream has data read all data
這種模式最大的缺點是,若是沒有socket可讀,也將一直佔用CPU,浪費CPU時間。segmentfault
二、爲了解決這個問題,引入select,當streams[N]中有k(0 < k <= N)個socket流可操做時才返回,不然阻塞,釋放CPU。緩存
// select
while True:
select(streams[N])
for stream in streams[N]: if stream has data read all data
當streams[N]中沒有數據時, 線程阻塞在select處,CPU處理其餘任務。select返回時表示有k個流可操做,但是select並無通知咱們是那些流,所以咱們須要輪詢全部的N個流,安全
時間複雜度爲O(N). 在N比較小時,這樣處理ok,可是當鏈接數達到數萬甚至幾十萬時(C10K問題),select的輪詢機制會致使效率低下。網絡
三、epoll則解決了selec後的輪詢。epoll會返回每一個可操做的socket流以及這些流產生了那些IO事件。數據結構
// epoll
while True:
active_streams[k] = epoll(streams[N])
for stream in active_streams[k] deal all data
這樣epoll將複雜度下降爲O(1), 提升了效率[1], 解決了C10K問題。多線程
2、Linux IO模式分析(轉載: https://segmentfault.com/a/1190000003063859)
注:本文是對衆多博客的學習和總結,可能存在理解錯誤。請帶着懷疑的眼光,同時若是有錯誤但願能指出。
同步IO和異步IO,阻塞IO和非阻塞IO分別是什麼,到底有什麼區別?不一樣的人在不一樣的上下文下給出的答案是不一樣的。因此先限定一下本文的上下文。
本文討論的背景是Linux環境下的network IO。
在進行解釋以前,首先要說明幾個概念:
- 用戶空間和內核空間
- 進程切換
- 進程的阻塞
- 文件描述符
- 緩存 I/O
如今操做系統都是採用虛擬存儲器,那麼對32位操做系統而言,它的尋址空間(虛擬存儲空間)爲4G(2的32次方)。操做系統的核心是內核,獨立於普通的應用程序,能夠訪問受保護的內存空間,也有訪問底層硬件設備的全部權限。爲了保證用戶進程不能直接操做內核(kernel),保證內核的安全,操心繫統將虛擬空間劃分爲兩部分,一部分爲內核空間,一部分爲用戶空間。針對linux操做系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱爲內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱爲用戶空間。
爲了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復之前掛起的某個進程的執行。這種行爲被稱爲進程切換。所以能夠說,任何進程都是在操做系統內核的支持下運行的,是與內核緊密相關的。
從一個進程的運行轉到另外一個進程上運行,這個過程當中通過下面這些變化:
1. 保存處理機上下文,包括程序計數器和其餘寄存器。
2. 更新PCB信息。
3. 把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。
4. 選擇另外一個進程執行,並更新其PCB。
5. 更新內存管理的數據結構。
6. 恢復處理機上下文。
注:總而言之就是很耗資源,具體的能夠參考這篇文章:進程切換
正在執行的進程,因爲期待的某些事件未發生,如請求系統資源失敗、等待某種操做的完成、新數據還沒有到達或無新工做作等,則由系統自動執行阻塞原語(Block),使本身由運行狀態變爲阻塞狀態。可見,進程的阻塞是進程自身的一種主動行爲,也所以只有處於運行態的進程(得到CPU),纔可能將其轉爲阻塞狀態。當進程進入阻塞狀態,是不佔用CPU資源的
。
文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。
文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者建立一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫每每會圍繞着文件描述符展開。可是文件描述符這一律念每每只適用於UNIX、Linux這樣的操做系統。
緩存 I/O 又被稱做標準 I/O,大多數文件系統的默認 I/O 操做都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操做系統會將 I/O 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。
緩存 I/O 的缺點:
數據在傳輸過程當中須要在應用程序地址空間和內核進行屢次數據拷貝操做,這些數據拷貝操做所帶來的 CPU 以及內存開銷是很是大的。
剛纔說了,對於一次IO訪問(以read舉例),數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。因此說,當一個read操做發生時,它會經歷兩個階段:
1. 等待數據準備 (Waiting for the data to be ready)
2. 將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)
正式由於這兩個階段,linux系統產生了下面五種網絡模式的方案。
- 阻塞 I/O(blocking IO)
- 非阻塞 I/O(nonblocking IO)
- I/O 多路複用( IO multiplexing)
- 信號驅動 I/O( signal driven IO)
- 異步 I/O(asynchronous IO)
注:因爲signal driven IO在實際中並不經常使用,因此我這隻說起剩下的四種IO Model。
在linux中,默認狀況下全部的socket都是blocking,一個典型的讀操做流程大概是這樣:
當用戶進程調用了recvfrom這個系統調用,kernel就開始了IO的第一個階段:準備數據(對於網絡IO來講,不少時候數據在一開始尚未到達。好比,尚未收到一個完整的UDP包。這個時候kernel就要等待足夠的數據到來)。這個過程須要等待,也就是說數據被拷貝到操做系統內核的緩衝區中是須要一個過程的。而在用戶進程這邊,整個進程會被阻塞(固然,是進程本身選擇的阻塞)。當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶內存,而後kernel返回結果,用戶進程才解除block的狀態,從新運行起來。
因此,blocking IO的特色就是在IO執行的兩個階段都被block了。
linux下,能夠經過設置socket使其變爲non-blocking。當對一個non-blocking socket執行讀操做時,流程是這個樣子:
當用戶進程發出read操做時,若是kernel中的數據尚未準備好,那麼它並不會block用戶進程,而是馬上返回一個error。從用戶進程角度講 ,它發起一個read操做後,並不須要等待,而是立刻就獲得了一個結果。用戶進程判斷結果是一個error時,它就知道數據尚未準備好,因而它能夠再次發送read操做。一旦kernel中的數據準備好了,而且又再次收到了用戶進程的system call,那麼它立刻就將數據拷貝到了用戶內存,而後返回。
因此,nonblocking IO的特色是用戶進程須要不斷的主動詢問kernel數據好了沒有。
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()函數就能夠返回。
這個圖和blocking IO的圖其實並無太大的不一樣,事實上,還更差一些。由於這裏須要使用兩個system call (select 和 recvfrom),而blocking IO只調用了一個system call (recvfrom)。可是,用select的優點在於它能夠同時處理多個connection。
因此,若是處理的鏈接數不是很高的話,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優點並非對於單個鏈接能處理得更快,而是在於能處理更多的鏈接。)
在IO multiplexing Model中,實際中,對於每個socket,通常都設置成爲non-blocking,可是,如上圖所示,整個用戶的process實際上是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。
inux下的asynchronous IO其實用得不多。先看一下它的流程:
用戶進程發起read操做以後,馬上就能夠開始去作其它的事。而另外一方面,從kernel的角度,當它受到一個asynchronous read以後,首先它會馬上返回,因此不會對用戶進程產生任何block。而後,kernel會等待數據準備完成,而後將數據拷貝到用戶內存,當這一切都完成以後,kernel會給用戶進程發送一個signal,告訴它read操做完成了。
調用blocking IO會一直block住對應的進程直到操做完成,而non-blocking IO在kernel還準備數據的狀況下會馬上返回。
在說明synchronous IO和asynchronous IO的區別以前,須要先給出二者的定義。POSIX的定義是這樣子的:
- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
- An asynchronous I/O operation does not cause the requesting process to be blocked;
二者的區別就在於synchronous IO作」IO operation」的時候會將process阻塞。按照這個定義,以前所述的blocking IO,non-blocking IO,IO multiplexing都屬於synchronous IO。
有人會說,non-blocking IO並無被block啊。這裏有個很是「狡猾」的地方,定義中所指的」IO operation」是指真實的IO操做,就是例子中的recvfrom這個system call。non-blocking IO在執行recvfrom這個system call的時候,若是kernel的數據沒有準備好,這時候不會block進程。可是,當kernel中數據準備好的時候,recvfrom會將數據從kernel拷貝到用戶內存中,這個時候進程是被block了,在這段時間內,進程是被block的。
而asynchronous IO則不同,當進程發起IO 操做以後,就直接返回不再理睬了,直到kernel發送一個信號,告訴進程說IO完成。在這整個過程當中,進程徹底沒有被block。
各個IO Model的比較如圖所示:
經過上面的圖片,能夠發現non-blocking IO和asynchronous IO的區別仍是很明顯的。在non-blocking IO中,雖然進程大部分時間都不會被block,可是它仍然要求進程去主動的check,而且當數據準備完成之後,也須要進程主動的再次調用recvfrom來將數據拷貝到用戶內存。而asynchronous IO則徹底不一樣。它就像是用戶進程將整個IO操做交給了他人(kernel)完成,而後他人作完後發信號通知。在此期間,用戶進程不須要去檢查IO操做的狀態,也不須要主動的去拷貝數據。
select,poll,epoll都是IO多路複用的機制。I/O多路複用就是經過一種機制,一個進程能夠監視多個描述符,一旦某個描述符就緒(通常是讀就緒或者寫就緒),可以通知程序進行相應的讀寫操做。但select,poll,epoll本質上都是同步I/O,由於他們都須要在讀寫事件就緒後本身負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需本身負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。(這裏囉嗦下)
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,來找到就緒的描述符。
select目前幾乎在全部的平臺上支持,其良好跨平臺支持也是它的一個優勢。select的一 個缺點在於單個進程可以監視的文件描述符的數量存在最大限制,在Linux上通常爲1024,能夠經過修改宏定義甚至從新編譯內核的方式提高這一限制,但 是這樣也會形成效率的下降。
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不一樣與select使用三個位圖來表示三個fdset的方式,poll使用一個 pollfd的指針實現。
struct pollfd { int fd; /* file descriptor */ short events; /* requested events to watch */ short revents; /* returned events witnessed */ };
pollfd結構包含了要監視的event和發生的event,再也不使用select「參數-值」傳遞的方式。同時,pollfd並無最大數量限制(可是數量過大後性能也是會降低)。 和select函數同樣,poll返回後,須要輪詢pollfd來獲取就緒的描述符。
從上面看,select和poll都須要在返回後,
經過遍歷文件描述符來獲取已經就緒的socket
。事實上,同時鏈接的大量客戶端在一時刻可能只有不多的處於就緒狀態,所以隨着監視的描述符數量的增加,其效率也會線性降低。
epoll是在2.6內核中提出的,是以前的select和poll的加強版本。相對於select和poll來講,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。
epoll操做過程須要三個接口,分別以下:
int epoll_create(int size);//建立一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大 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);
1. int epoll_create(int size);
建立一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大,這個參數不一樣於select()中的第一個參數,給出最大監聽的fd+1的值,參數size並非限制了epoll所能監聽的描述符最大個數,只是對內核初始分配內部數據結構的一個建議
。
當建立好epoll句柄後,它就會佔用一個fd值,在linux下若是查看/proc/進程id/fd/,是可以看到這個fd的,因此在使用完epoll後,必須調用close()關閉,不然可能致使fd被耗盡。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函數是對指定描述符fd執行op操做。
- epfd:是epoll_create()的返回值。
- op:表示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隊列裏
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待epfd上的io事件,最多返回maxevents個事件。
參數events用來從內核獲得事件的集合,maxevents告以內核這個events有多大,這個maxevents的值不能大於建立epoll_create()時的size,參數timeout是超時時間(毫秒,0會當即返回,-1將不肯定,也有說法說是永久阻塞)。該函數返回須要處理的事件數目,如返回0表示已超時。
epoll對文件描述符的操做有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是默認模式,LT模式與ET模式的區別以下:
LT模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序能夠不當即處理該事件
。下次調用epoll_wait時,會再次響應應用程序並通知此事件。
ET模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序必須當即處理該事件
。若是不處理,下次調用epoll_wait時,不會再次響應應用程序並通知此事件。
LT(level triggered)是缺省的工做方式,而且同時支持block和no-block socket.在這種作法中,內核告訴你一個文件描述符是否就緒了,而後你能夠對這個就緒的fd進行IO操做。若是你不做任何操做,內核仍是會繼續通知你的。
ET(edge-triggered)是高速工做方式,只支持no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核經過epoll告訴你。而後它會假設你知道文件描述符已經就緒,而且不會再爲那個文件描述符發送更多的就緒通知,直到你作了某些操做致使那個文件描述符再也不爲就緒狀態了(好比,你在發送,接收或者接收請求,或者發送接收的數據少於必定量時致使了一個EWOULDBLOCK 錯誤)。可是請注意,若是一直不對這個fd做IO操做(從而致使它再次變成未就緒),內核不會發送更多的通知(only once)
ET模式在很大程度上減小了epoll事件被重複觸發的次數,所以效率要比LT模式高。epoll工做在ET模式的時候,必須使用非阻塞套接口,以免因爲一個文件句柄的阻塞讀/阻塞寫操做把處理多個文件描述符的任務餓死。
假若有這樣一個例子:
1. 咱們已經把一個用來從管道中讀取數據的文件句柄(RFD)添加到epoll描述符
2. 這個時候從管道的另外一端被寫入了2KB的數據
3. 調用epoll_wait(2),而且它會返回RFD,說明它已經準備好讀取操做
4. 而後咱們讀取了1KB的數據
5. 調用epoll_wait(2)......
LT模式:
若是是LT模式,那麼在第5步調用epoll_wait(2)以後,仍然能受到通知。
ET模式:
若是咱們在第1步將RFD添加到epoll描述符的時候使用了EPOLLET標誌,那麼在第5步調用epoll_wait(2)以後將有可能會掛起,由於剩餘的數據還存在於文件的輸入緩衝區內,並且數據發出端還在等待一個針對已經發出數據的反饋信息。只有在監視的文件句柄上發生了某個事件的時候 ET 工做模式纔會彙報事件。所以在第5步的時候,調用者可能會放棄等待仍在存在於文件輸入緩衝區內的剩餘數據。
當使用epoll的ET模型來工做時,當產生了一個EPOLLIN事件後,
讀數據的時候須要考慮的是當recv()返回的大小若是等於請求的大小,那麼頗有多是緩衝區還有數據未讀完,也意味着該次事件尚未處理完,因此還須要再次讀取:
while(rs){ buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0); if(buflen < 0){ // 因爲是非阻塞的模式,因此當errno爲EAGAIN時,表示當前緩衝區已無數據可讀 // 在這裏就看成是該次事件已處理處. if(errno == EAGAIN){ break; } else{ return; } } else if(buflen == 0){ // 這裏表示對端的socket已正常關閉. } if(buflen == sizeof(buf){ rs = 1; // 須要再次讀取 } else{ rs = 0; } }
Linux中的EAGAIN含義
Linux環境下開發常常會碰到不少錯誤(設置errno),其中EAGAIN是其中比較常見的一個錯誤(好比用在非阻塞操做中)。
從字面上來看,是提示再試一次。這個錯誤常常出如今當應用程序進行一些非阻塞(non-blocking)操做(對文件或socket)的時候。
例如,以 O_NONBLOCK的標誌打開文件/socket/FIFO,若是你連續作read操做而沒有數據可讀。此時程序不會阻塞起來等待數據準備就緒返回,read函數會返回一個錯誤EAGAIN,提示你的應用程序如今沒有數據可讀請稍後再試。
又例如,當一個系統調用(好比fork)由於沒有足夠的資源(好比虛擬內存)而執行失敗,返回EAGAIN提示其再調用一次(也許下次就能成功)。
下面是一段不完整的代碼且格式不對,意在表述上面的過程,去掉了一些模板代碼。
1 #define IPADDRESS "127.0.0.1" 2 #define PORT 8787 3 #define MAXSIZE 1024 4 #define LISTENQ 5 5 #define FDSIZE 1000 6 #define EPOLLEVENTS 100 7 8 listenfd = socket_bind(IPADDRESS,PORT); 9 10 struct epoll_event events[EPOLLEVENTS]; 11 12 //建立一個描述符 13 epollfd = epoll_create(FDSIZE); 14 15 //添加監聽描述符事件 16 add_event(epollfd,listenfd,EPOLLIN); 17 18 //循環等待 19 for ( ; ; ){ 20 //該函數返回已經準備好的描述符事件數目 21 ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1); 22 //處理接收到的鏈接 23 handle_events(epollfd,events,ret,listenfd,buf); 24 } 25 26 //事件處理函數 27 static void handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf) 28 { 29 int i; 30 int fd; 31 //進行遍歷;這裏只要遍歷已經準備好的io事件。num並非當初epoll_create時的FDSIZE。 32 for (i = 0;i < num;i++) 33 { 34 fd = events[i].data.fd; 35 //根據描述符的類型和事件類型進行處理 36 if ((fd == listenfd) &&(events[i].events & EPOLLIN)) 37 handle_accpet(epollfd,listenfd); 38 else if (events[i].events & EPOLLIN) 39 do_read(epollfd,fd,buf); 40 else if (events[i].events & EPOLLOUT) 41 do_write(epollfd,fd,buf); 42 } 43 } 44 45 //添加事件 46 static void add_event(int epollfd,int fd,int state){ 47 struct epoll_event ev; 48 ev.events = state; 49 ev.data.fd = fd; 50 epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev); 51 } 52 53 //處理接收到的鏈接 54 static void handle_accpet(int epollfd,int listenfd){ 55 int clifd; 56 struct sockaddr_in cliaddr; 57 socklen_t cliaddrlen; 58 clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen); 59 if (clifd == -1) 60 perror("accpet error:"); 61 else { 62 printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port); //添加一個客戶描述符和事件 63 add_event(epollfd,clifd,EPOLLIN); 64 } 65 } 66 67 //讀處理 68 static void do_read(int epollfd,int fd,char *buf){ 69 int nread; 70 nread = read(fd,buf,MAXSIZE); 71 if (nread == -1) { 72 perror("read error:"); 73 close(fd); //記住close fd 74 delete_event(epollfd,fd,EPOLLIN); //刪除監聽 75 } 76 else if (nread == 0) { 77 fprintf(stderr,"client close.\n"); 78 close(fd); //記住close fd 79 delete_event(epollfd,fd,EPOLLIN); //刪除監聽 80 } 81 else { 82 printf("read message is : %s",buf); 83 //修改描述符對應的事件,由讀改成寫 84 modify_event(epollfd,fd,EPOLLOUT); 85 } 86 } 87 88 //寫處理 89 static void do_write(int epollfd,int fd,char *buf) { 90 int nwrite; 91 nwrite = write(fd,buf,strlen(buf)); 92 if (nwrite == -1){ 93 perror("write error:"); 94 close(fd); //記住close fd 95 delete_event(epollfd,fd,EPOLLOUT); //刪除監聽 96 }else{ 97 modify_event(epollfd,fd,EPOLLIN); 98 } 99 memset(buf,0,MAXSIZE); 100 } 101 102 //刪除事件 103 static void delete_event(int epollfd,int fd,int state) { 104 struct epoll_event ev; 105 ev.events = state; 106 ev.data.fd = fd; 107 epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev); 108 } 109 110 //修改事件 111 static void modify_event(int epollfd,int fd,int state){ 112 struct epoll_event ev; 113 ev.events = state; 114 ev.data.fd = fd; 115 epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev); 116 } 117 118 //注:另一端我就省了
select的調用過程以下所示:
(1)使用copy_from_user從用戶空間拷貝fd_set到內核空間
(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
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察看,通常來講這個數目和系統內存關係很大。
主要來源:
https://segmentfault.com/a/1190000003063859
http://www.cnblogs.com/Anker/p/3265058.html