IO:select、poll、epoll
一、什么是IO?
我們都知道unix世界里,一切皆文件,而文件是什么呢?文件就是一串二進制流,不管socket,還是FIFO、管道、終端,對我們來說,一切都是文件,一切都是流。在信息交換的過程中,我們都是對這些流進行數(shù)據(jù)的收發(fā)操作,簡稱為I/O操作(input and output),往流中讀出數(shù)據(jù),系統(tǒng)調(diào)用read,寫入數(shù)據(jù),系統(tǒng)調(diào)用write。
二、文件描述符
那么計算機中這么多流,我們是如何知道要操作哪個流呢?
這時候我們需要有一個能夠?qū)ξ募M行定位的標識符,那么文件描述符就應運而生。文件描述符是內(nèi)核為了高效管理已經(jīng)被打開的文件所創(chuàng)建的索引,他是一個從0開始的整數(shù),程序所有執(zhí)行的I/O操作都是通過文件描述符進行的。其中,在程序剛剛啟動時,0,1,2三個文件描述符已經(jīng)被占用了,0代表標準輸入設備stdin(比如鍵盤),1代表標準輸出設備stdout(顯示器),2代表標準錯誤stderr.POSIX標準要求每次打開文件時(含socket)必須使用當前進程中最小可用的文件描述符號碼,因此再打開一個文件,它的文件描述符會是3。
既然文件描述符是文件的索引,那么它有沒有最大限制呢?對的,文件描述符是系統(tǒng)的一個重要的資源,實際中最大打開的文件數(shù)是系統(tǒng)內(nèi)存的10%,這個是系統(tǒng)級限制,有系統(tǒng)就會有用戶,用戶級限制是單個進程最大打開的文件數(shù),一般是1024,可以使用ulimit -n命令查看。
系統(tǒng)是如何通過文件描述符定位到文件的呢?
系統(tǒng)為每一個進程維護了一個文件描述符表,這是一個進程級的文件描述符表,它通過文件描述符所對應的文件指針指向系統(tǒng)級的打開文件描述符表中的一個打開文件句柄,句柄中存儲了打開文件相應的全部信息,包括文件偏移量、狀態(tài)標示、訪問模式文件類型以及文件屬性等等,其中有一個inode指針,它指向了i-node表?中該文件的表項。
三、阻塞與非阻塞
為了讓大家能順利掌握IO復用,請允許我再嘮叨一下阻塞與非阻塞。
什么是程序的阻塞呢?想象這種情形,比如你等快遞,但快遞一直沒來,你會怎么做?有兩種方式:
快遞沒來,我可以先去睡覺,然后快遞來了給我打電話叫我去取就行了。(非阻塞忙輪詢)
快遞沒來,我就不停的給快遞打電話說:擦,怎么還沒來,給老子快點,直到快遞來。(阻塞)
很顯然,你無法忍受第二種方式,不僅耽擱自己的時間,也會讓快遞很想打你。
而在計算機世界,這兩種情形就對應阻塞和非阻塞忙輪詢。
非阻塞忙輪詢:數(shù)據(jù)沒來,進程就不停的去檢測數(shù)據(jù),直到數(shù)據(jù)來。
阻塞:數(shù)據(jù)沒來,啥都不做,直到數(shù)據(jù)來了,才進行下一步的處理。
????先說說阻塞,為了了解阻塞是如何進行的,我們來討論緩沖區(qū),以及內(nèi)核緩沖區(qū),最終把I/O事件解釋清楚。
緩沖區(qū)的引入是為了減少頻繁I/O操作而引起頻繁的系統(tǒng)調(diào)用(你知道它很慢的),當你操作一個流時,更多的是以緩沖區(qū)為單位進行操作,這是相對于用戶空間而言。對于內(nèi)核來說,也需要緩沖區(qū)。
因為一個線程只能處理一個套接字的I/O事件,如果想同時處理多個,可以利用非阻塞忙輪詢的方式,偽代碼如下:
while true { for i in stream[]
{ if i has data read until unavailable } }
我們只要把所有流從頭到尾查詢一遍,就可以處理多個流了,但這樣做很不好,因為如果所有的流都沒有I/O事件,白白浪費CPU時間片。正如有一位科學家所說,計算機所有的問題都可以增加一個中間層來解決,同樣,為了避免這里cpu的空轉(zhuǎn),我們不讓這個線程親自去檢查流中是否有事件,而是引進了一個代理(一開始是select,后來是poll),這個代理很牛,它可以同時觀察許多流的I/O事件,如果沒有事件,代理就阻塞,線程就不會挨個挨個去輪詢了,偽代碼如下:
while true { select(streams[]) //這一步死在這里,知道有一個流有I/O事件時,才往下執(zhí)行
?for i in streams[] { if i has data read until unavailable } }
四、 IO多路復用
I/O多路復用 (單個線程,通過記錄跟蹤每個I/O流(sock)的狀態(tài),來同時管理多個I/O流 。)
重要的事情再說一遍: I/O multiplexing 這里面的multiplexing 指的其實是在單個線程通過記錄跟蹤每一個Sock(I/O流)的狀態(tài)(對應空管塔里面的Fight progress strip槽)來同時管理多個I/O流.
發(fā)明它的原因,是盡量多的提高服務器的吞吐能力。
是不是聽起來好拗口,看個圖就懂了
在同一個線程里面, 通過撥開關(guān)的方式,來同時傳輸多個I/O流, (學過EE的人現(xiàn)在可以站出來義正嚴辭說這個叫“時分復用”了)。
什么,你還沒有搞懂“一個請求到來了,nginx使用epoll接收請求的過程是怎樣的”,
多看看這個圖就了解了。提醒下,ngnix會有很多鏈接進來, epoll會把他們都監(jiān)視起來,然后像撥開關(guān)一樣,誰有數(shù)據(jù)就撥向誰,然后調(diào)用相應的代碼處理。
select, poll, epoll 都是I/O多路復用的具體的實現(xiàn),之所以有這三個鬼存在,其實是他們出現(xiàn)是有先后順序的。
I/O多路復用這個概念被提出來以后, select是第一個實現(xiàn) (1983 左右在BSD里面實現(xiàn)的)。
select 被實現(xiàn)以后,很快就暴露出了很多問題。
select 會修改傳入的參數(shù)數(shù)組(事件到來相應的位置1,這個操作由內(nèi)核完成),這個對于一個需要調(diào)用很多次的函數(shù),是非常不友好的。
select 如果任何一個sock(I/O stream)出現(xiàn)了數(shù)據(jù),select 會返回所有的sock,但是并不會告訴你是那個sock上有數(shù)據(jù),于是你只能自己一個一個的找,10幾個sock可能還好,要是幾萬的sock每次都找一遍,這個無謂的開銷就頗有海天盛筵的豪氣了。(每次都要去遍歷查找到底是哪個描述符有事件到來)
select 只能監(jiān)視1024個鏈接, 這個跟草榴沒啥關(guān)系哦,linux 定義在頭文件中的,參見FD_SETSIZE。
select 不是線程安全的,如果你把一個sock加入到select, 然后突然另外一個線程發(fā)現(xiàn),尼瑪,這個sock不用,要收回。對不起,這個select 不支持的,如果你喪心病狂的竟然關(guān)掉這個sock, select的標準行為是。。呃。。不可預測的, 這個可是寫在文檔中的哦.
?
于是14年以后(1997年)一幫人又實現(xiàn)了poll, poll 修復了select的很多問題,比如
poll 去掉了1024個鏈接的限制,于是要多少鏈接呢, 主人你開心就好。
poll 從設計上來說,不再修改傳入數(shù)組,不過這個要看你的平臺了,所以行走江湖,還是小心為妙。
其實拖14年那么久也不是效率問題, 而是那個時代的硬件實在太弱,一臺服務器處理1千多個鏈接簡直就是神一樣的存在了,select很長段時間已經(jīng)滿足需求。
但是poll仍然不是線程安全的, 這就意味著,不管服務器有多強悍,你也只能在一個線程里面處理一組I/O流。你當然可以那多進程來配合了,不過然后你就有了多進程的各種問題。
于是5年以后, 在2002, 大神 Davide Libenzi 實現(xiàn)了epoll.
epoll 可以說是I/O 多路復用最新的一個實現(xiàn),epoll 修復了poll 和select絕大部分問題, 比如:
epoll 現(xiàn)在是線程安全的。
epoll 現(xiàn)在不僅告訴你sock組里面數(shù)據(jù),還會告訴你具體哪個sock有數(shù)據(jù),你不用自己去找了。
?
?
select
函數(shù)介紹:
該函數(shù)準許進程指示內(nèi)核等待多個事件中的任何一個發(fā)送,并只在有一個或多個事件發(fā)生或經(jīng)歷一段指定的時間后才喚醒。函數(shù)原型如下:
?
include <sys select.h="">
include <sys time.h="">
?
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)</sys></sys>
返回值:就緒描述符的數(shù)目,超時返回0,出錯返回-1
函數(shù)參數(shù)介紹如下:
(1)第一個參數(shù)maxfdp1指定待測試的描述字個數(shù),它的值是待測試的最大描述字加1(因此把該參數(shù)命名為maxfdp1),描述字0、1、2…maxfdp1-1均將被測試。
因為文件描述符是從0開始的。
(2)中間的三個參數(shù)readset、writeset和exceptset指定我們要讓內(nèi)核測試讀、寫和異常條件的描述字。如果對某一個的條件不感興趣,就可以把它設為空指針。struct fd_set可以理解為一個集合,這個集合中存放的是文件描述符(可以看為是socket的事件),可通過以下四個宏進行設置:
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd, fd_set *fdset); //將一個給定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset); //將一個給定的文件描述符從集合中刪除
int FD_ISSET(int fd, fd_set *fdset); // 檢查集合中指定的文件描述符是否可以讀寫
(3)timeout告知內(nèi)核等待所指定描述字中的任何一個就緒可花多少時間。其timeval結(jié)構(gòu)用于指定這段時間的秒數(shù)和微秒數(shù)。
struct timeval{
??????long tv_sec;???//seconds
??????long tv_usec;??//microseconds
};
這個參數(shù)有三種可能:
(1)永遠等待下去:僅在有一個描述字準備好I/O時才返回。為此,把該參數(shù)設置為空指針NULL。
(2)等待一段固定時間:在有一個描述字準備好I/O時返回,但是不超過由該參數(shù)所指向的timeval結(jié)構(gòu)中指定的秒數(shù)和微秒數(shù)。
(3)根本不等待:檢查描述字后立即返回,這稱為輪詢。為此,該參數(shù)必須指向一個timeval結(jié)構(gòu),而且其中的定時器值必須為0。
?
函數(shù)說明:
I/O多路復用這個概念被提出來以后, select是第一個實現(xiàn) (1983 左右在BSD里面實現(xiàn)的)。
select 被實現(xiàn)以后,很快就暴露出了很多問題。
1) 對socket進行掃描時是線性掃描,即采用輪詢的方法,效率較低。
當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調(diào)度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。
2) select 如果任何一個sock(I/O stream)出現(xiàn)了數(shù)據(jù),select 僅僅會返回,但是并不會告訴你是那個sock上有數(shù)據(jù),于是你只能遍歷所有的socket自己一個一個的找
3) select 只能監(jiān)視1024個鏈接, linux 定義在頭文件中的,參見FD_SETSIZE。
4) 需要維護一個用來存放大量fd的數(shù)據(jù)結(jié)構(gòu),這樣會使得用戶空間和內(nèi)核空間在傳遞該結(jié)構(gòu)時復制開銷大。
5) select 不是線程安全的,如果你把一個sock加入到select, 然后突然另外一個線程發(fā)現(xiàn),這個sock不用,要收回。對不起,這個select 不支持的,如果你喪心病狂的竟然關(guān)掉這個sock, select的標準行為是。。呃。。不可預測的。
1)這3個bitmap有大小限制(FD_SETSIZE,常為1024);
2)由于這3個集合在返回時會被內(nèi)核修改,因此我們每次調(diào)用時都需要重新設置;
3)我們在調(diào)用完成后需要掃描這3個集合(即掃描所有的socket)才能知道哪些fd的讀/寫事件發(fā)生了,一般情況下全量集合比較大而實際發(fā)生讀/寫事件的fd比較少,效率比較低下;
4)(將用戶傳入的數(shù)組拷貝到內(nèi)核空間)內(nèi)核在每次調(diào)用時都需要掃描這3個fd集合(套接字集合),然后查看哪些fd的事件實際發(fā)生,并將它傳到設備等待隊列。在讀/寫比較稀疏的情況下同樣存在效率問題
poll
于是14年以后(1997年)一幫人又實現(xiàn)了poll, poll 修復了select的很多問題,比如
1) poll 去掉了1024個鏈接的限制,于是要多少鏈接呢,主人你開心就好。
2) poll 從設計上來說,不再修改傳入所有的數(shù)組(所有的socket),需要傳遞的是一個pollfd結(jié)構(gòu)的數(shù)組,不過這個要看你的平臺了。
但是poll仍然不是線程安全的,這就意味著,不管服務器有多強悍,你也只能在一個線程里面處理一組I/O流。
你當然可以那多進程來配合了,不過然后你就有了多進程的各種問題。
poll本質(zhì)上和select沒有區(qū)別,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個fd對應的設備狀態(tài),如果設備就緒則在設備等待隊列中加入一項并繼續(xù)遍歷,如果遍歷完所有fd后沒有發(fā)現(xiàn)就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒后它又要再次遍歷fd。這個過程經(jīng)歷了多次無謂的遍歷。
?????它沒有最大連接數(shù)的限制,原因是它是基于鏈表來存儲的,但是同樣有一個缺點:
1) 大量的fd的數(shù)組被整體復制于用戶態(tài)和內(nèi)核地址空間之間,而不管這樣的復制是不是有意義。
2) poll還有一個特點是“水平觸發(fā)”,如果報告了fd后,沒有被處理,那么下次poll時會再次報告該fd。
?
select() 和 poll() 系統(tǒng)調(diào)用的本質(zhì)一樣,poll() 的機制與 select() 類似,與 select() 在本質(zhì)上沒有多大差別,管理多個描述符也是進行輪詢,根據(jù)描述符的狀態(tài)進行處理,但是 poll()?沒有最大文件描述符數(shù)量的限制(但是數(shù)量過大后性能也是會下降)。poll() 和 select() 同樣存在一個缺點就是,包含大量文件描述符的數(shù)組被整體復制于用戶態(tài)和內(nèi)核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨著文件描述符數(shù)量的增加而線性增大。
epoll
于是5年以后, 在2002, 大神 Davide Libenzi 實現(xiàn)了epoll.
epoll 可以說是I/O 多路復用最新的一個實現(xiàn),epoll 修復了poll 和select絕大部分問題, 比如:
1. 沒有最大并發(fā)連接的限制,能打開的FD的上限遠大于1024(1G的內(nèi)存上能監(jiān)聽約10萬個端口)。
2. 效率提升,不是輪詢的方式,不會隨著FD數(shù)目的增加效率下降。
只有活躍可用的FD才會調(diào)用callback函數(shù);即Epoll最大的優(yōu)點就在于它只管你“活躍”的連接,而跟連接總數(shù)無關(guān),因此在實際的網(wǎng)絡環(huán)境中,Epoll的效率就會遠遠高于select和poll。
3. 內(nèi)存拷貝,利用mmap()文件映射內(nèi)存加速與內(nèi)核空間的消息傳遞;即epoll使用mmap減少復制開銷。(共享內(nèi)存的方式實現(xiàn))
epoll使用一個文件描述符管理多個描述符,將用戶關(guān)系的文件描述符的事件存放到內(nèi)核的一個事件表中,這樣在用戶空間和內(nèi)核空間的copy只需一次。
原理:epoll支持水平觸發(fā)和邊緣觸發(fā),最大的特點在于邊緣觸發(fā),它只告訴進程哪些fd剛剛變?yōu)榫途w態(tài),并且只會通知一次。還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd,一旦某個文件描述符就緒,內(nèi)核就會采用類似callback的回調(diào)機制來激活該fd,epoll_wait便可以收到通知。(此處去掉了遍歷文件描述符,而是通過監(jiān)聽回調(diào)的的機制。這正是epoll的魅力所在。)
三種方式總結(jié):
從select那里僅僅知道了,有I/O事件發(fā)生了,卻并不知道是哪那幾個流(可能有一個,多個,甚至全部),我們只能無差別輪詢所有流,找出能讀出數(shù)據(jù),或者寫入數(shù)據(jù)的流,對他們進行操作。所以select具有O(n)的無差別輪詢復雜度,同時處理的流越多,無差別輪詢時間就越長。
epoll可以理解為event poll,不同于忙輪詢和無差別輪詢,epoll會把哪個流發(fā)生了怎樣的I/O事件通知我們。所以我們說epoll實際上是事件驅(qū)動(每個事件關(guān)聯(lián)上fd)的,此時我們對這些流的操作都是有意義的。(復雜度降低到了O(1))偽代碼如下:
while true { active_stream[] = epoll_wait(epollfd) for i in active_stream[] { read or write till } }
可以看到,select和epoll最大的區(qū)別就是:select只是告訴你一定數(shù)目的流有事件了,至于哪個流有事件,還得你一個一個地去輪詢,而 epoll會把發(fā)生的事件告訴你,通過發(fā)生的事件,就自然而然定位到哪個流了。不能不說epoll跟select相比,是質(zhì)的飛躍,這也是一種犧牲空間,換取時間的思想,畢竟現(xiàn)在硬件越來越便宜了。
????表面上看epoll的性能最好,但是在連接數(shù)少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數(shù)回調(diào)。
?
總結(jié)
以上是生活随笔為你收集整理的IO:select、poll、epoll的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: IO:同步,异步,阻塞,非阻塞
- 下一篇: IO:Reactor设计模式