在談及網(wǎng)絡(luò)IO的時(shí)候總避不開阻塞奠货、非阻塞介褥、同步座掘、異步、IO多路復(fù)用柔滔、select溢陪、poll、epoll等這幾個(gè)詞語睛廊。在面試的時(shí)候也會(huì)被經(jīng)常問到這幾個(gè)的區(qū)別形真。本文就來講一下這幾個(gè)詞語的含義、區(qū)別以及使用方式超全。
Unix網(wǎng)絡(luò)編程一書中作者給出了五種IO模型:
1咆霜、BlockingIO - 阻塞IO
2、NoneBlockingIO - 非阻塞IO
3嘶朱、IO multiplexing - IO多路復(fù)用
4蛾坯、signal driven IO - 信號(hào)驅(qū)動(dòng)IO
5、asynchronous IO - 異步IO
這五種IO模型中前四個(gè)都是同步的IO疏遏,只有最后一個(gè)是異步IO脉课。信號(hào)驅(qū)動(dòng)IO使用的比較少,重點(diǎn)介紹其他幾種IO以及在Java中的應(yīng)用财异。
阻塞倘零、非阻塞、同步戳寸、異步以及IO多路復(fù)用
在進(jìn)行網(wǎng)絡(luò)IO的時(shí)候會(huì)涉及到用戶態(tài)和內(nèi)核態(tài)呈驶,并且在用戶態(tài)和內(nèi)核態(tài)之間會(huì)發(fā)生數(shù)據(jù)交換,從這個(gè)角度來說我們可以把IO抽象成兩個(gè)階段:1疫鹊、用戶態(tài)等待內(nèi)核態(tài)數(shù)據(jù)準(zhǔn)備好袖瞻,2跌穗、將數(shù)據(jù)從內(nèi)核態(tài)拷貝到用戶態(tài)。之所以會(huì)有同步虏辫、異步蚌吸、阻塞和非阻塞這幾種說法就是根據(jù)程序在這兩個(gè)階段的處理方式不同而產(chǎn)生的。
同步阻塞
顯示大圖
當(dāng)在用戶態(tài)調(diào)用read操作的時(shí)候砌庄,如果這時(shí)候kernel還沒有準(zhǔn)備好數(shù)據(jù)羹唠,那么用戶態(tài)會(huì)一直阻塞等待,直到有數(shù)據(jù)返回娄昆。當(dāng)kernel準(zhǔn)備好數(shù)據(jù)之后佩微,用戶態(tài)繼續(xù)等待kernel把數(shù)據(jù)從內(nèi)核態(tài)拷貝到用戶態(tài)之后才可以使用。這里會(huì)發(fā)生兩種等待:一個(gè)是用戶態(tài)等待kernel有數(shù)據(jù)可以讀萌焰,另外一個(gè)是當(dāng)有數(shù)據(jù)可讀時(shí)用戶態(tài)等待kernel把數(shù)據(jù)拷貝到用戶態(tài)哺眯。
在Java中同步阻塞的實(shí)現(xiàn)對(duì)應(yīng)的是傳統(tǒng)的文件IO操作以及Socket的accept的過程。在Socket調(diào)用accept的時(shí)候扒俯,程序會(huì)一直等待知道有描述符就緒奶卓,并且把就緒的數(shù)據(jù)拷貝到用戶態(tài),然后程序中就可以拿到對(duì)應(yīng)的數(shù)據(jù)撼玄。
同步非阻塞
對(duì)比第一張同步阻塞IO的圖就會(huì)發(fā)現(xiàn)夺姑,在同步非阻塞模型下第一個(gè)階段是不等待的,無論有沒有數(shù)據(jù)準(zhǔn)備好掌猛,都是立即返回盏浙。第二個(gè)階段仍然是需要等待的,用戶態(tài)需要等待內(nèi)核態(tài)把數(shù)據(jù)拷貝過來才能使用荔茬。對(duì)于同步非阻塞模式的處理废膘,需要每隔一段時(shí)間就去詢問一下內(nèi)核數(shù)據(jù)是不是可以讀了,如果內(nèi)核說可以慕蔚,那么就開始第二階段等待丐黄。
IO多路復(fù)用
IO多路復(fù)用也是同步的。
IO多路復(fù)用的方式看起來跟同步阻塞是一樣的坊萝,兩個(gè)階段都是阻塞的孵稽,但是IO多路復(fù)用可以實(shí)現(xiàn)以較小的代價(jià)同時(shí)監(jiān)聽多個(gè)IO。通常情況下是通過一個(gè)線程來同時(shí)監(jiān)聽多個(gè)描述符十偶,只要任何一個(gè)滿足就緒條件菩鲜,那么內(nèi)核態(tài)就返回。IO多路復(fù)用使得傳統(tǒng)的每請(qǐng)求每線程的處理方式得到解耦惦积,一個(gè)線程可以同時(shí)處理多個(gè)IO請(qǐng)求接校,然后交到后面的線程池里處理,這也是netty等框架的處理方式,所謂的reactor模式蛛勉。IO多路復(fù)用的實(shí)現(xiàn)依賴于操作系統(tǒng)的select鹿寻、poll和epoll,后面會(huì)詳細(xì)介紹這幾個(gè)系統(tǒng)調(diào)用诽凌。
IO多路復(fù)用在Java中的實(shí)現(xiàn)方式是在Socket編程中使用非阻塞模式毡熏,然后配置感興趣的事件,通過調(diào)用select函數(shù)來實(shí)現(xiàn)侣诵。select函數(shù)就是對(duì)應(yīng)的第一個(gè)階段痢法。如果給select配置了超時(shí)參數(shù),在指定時(shí)間內(nèi)沒有感興趣事件發(fā)生的話杜顺,select調(diào)用也會(huì)返回财搁,這也是為什么要做非阻塞模式下運(yùn)行。
異步IO
異步模式下躬络,前面提到的兩個(gè)階段都不會(huì)等待尖奔。使用異步模式,用戶態(tài)調(diào)用read方法的時(shí)候穷当,相當(dāng)于告訴內(nèi)核數(shù)據(jù)發(fā)送給我之后告訴我一聲我先去干別的事情了提茁。在這兩個(gè)階段都不會(huì)等待,只需要在內(nèi)核態(tài)通知數(shù)據(jù)準(zhǔn)備好之后使用即可膘滨。通常情況下使用異步模式都會(huì)使用callback甘凭,當(dāng)數(shù)據(jù)可用之后執(zhí)行callback函數(shù)稀拐。
IO多路復(fù)用
現(xiàn)在用Java開發(fā)的網(wǎng)絡(luò)服務(wù)器通常采用IO多路復(fù)用的方式來加快網(wǎng)絡(luò)IO操作火邓,例如Netty、Tomcat等德撬。IO多路復(fù)用的基礎(chǔ)是select铲咨、poll和epoll。這三個(gè)函數(shù)是從操作系統(tǒng)的角度上支持的IO多路復(fù)用的操作蜓洪,下面就分別來看一下這三個(gè)函數(shù)纤勒。
select
函數(shù)簽名如下:
int select(int maxfdp1, fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
maxfdp1為指定的待監(jiān)聽的描述符的個(gè)數(shù),因?yàn)槊枋龇菑?開始的隆檀,所以需要加1
readset為要監(jiān)聽的讀描述符
writeset為要監(jiān)聽的寫描述符
exceptset為要監(jiān)聽的異常描述符
timeout監(jiān)聽沒有準(zhǔn)備好的描述符的話摇天,多久可以返回,支持按照秒或者毫秒來配置時(shí)間
select操作的邏輯是首先將要監(jiān)聽的讀恐仑、寫以及異常描述符拷貝到內(nèi)核空間泉坐,然后遍歷所有的描述符,如果有感興趣的事件發(fā)生裳仆,那么就返回腕让。
select在使用的過程中有三個(gè)問題:
1、被監(jiān)控的fds(描述符)集合限制為1024歧斟,1024太小了
2纯丸、需要將描述符集合從用戶空間拷貝到內(nèi)核空間
3偏形、當(dāng)有描述符可操作的時(shí)候都需要遍歷一下整個(gè)描述符集合才能知道哪個(gè)是可操作的,效率很低觉鼻。
poll
函數(shù)簽名如下:
int poll(struct pollfd[] fds, unsigned int nfds, int timeout);
poll操作與select操作類似俊扭,仍舊避免不了描述符從用戶空間拷貝到內(nèi)核空間,但是poll不再有1024個(gè)描述符的限制坠陈。對(duì)于事件的觸發(fā)通知還是使用遍歷所有描述符的方式统扳,因此在大量連接的情況下也存在遍歷低效的問題。poll函數(shù)在傳遞參數(shù)的時(shí)候統(tǒng)一的將要監(jiān)聽的描述符和事件封裝在了pollfd結(jié)構(gòu)體數(shù)組中畅姊。
epoll
epoll有三個(gè)方法:epoll_create咒钟、epoll_ctl和epoll_wait。epoll_create是創(chuàng)建一個(gè)epoll句柄若未;epoll_ctl是注冊(cè)要監(jiān)聽的事件類型朱嘴;epoll_wait則是等待事件的產(chǎn)生。 通過這三個(gè)方法epoll解決了select的三個(gè)問題粗合。
1萍嬉、1024數(shù)量限制的問題
通過epoll_create方法來創(chuàng)建一個(gè)epoll句柄,這個(gè)句柄監(jiān)聽的描述符的數(shù)量不再有限制隙疚。
2壤追、文件描述符頻繁從用戶空間拷貝到內(nèi)核空間的問題
通過觀察select的操作會(huì)發(fā)現(xiàn)描述符從用戶空間到內(nèi)核空間拷貝發(fā)生在調(diào)用select方法的時(shí)候,只要沒有注冊(cè)新的事件或者取消注冊(cè)事件供屉,每次拷貝的描述符都是一樣的行冰。因此epoll引入了epoll_ctl調(diào)用,該方法用于注冊(cè)新事件和取消注冊(cè)事件伶丐。而在epoll_wait的時(shí)候并不會(huì)拷貝描述符悼做,描述符始終存在于內(nèi)核空間,當(dāng)需要修改的時(shí)候只要調(diào)用epoll_ctl修改一下內(nèi)核的描述符即可哗魂。如此一來便省去了描述符來回拷貝的開銷肛走。
3、文件描述符可操作的時(shí)候遍歷整個(gè)描述符集合的問題
在調(diào)用epoll_ctl注冊(cè)感興趣的事件的時(shí)候录别,實(shí)際上會(huì)為設(shè)置的事件添加一個(gè)回調(diào)函數(shù)朽色,當(dāng)對(duì)應(yīng)的感興趣的事件發(fā)生的時(shí)候,回調(diào)函數(shù)就會(huì)觸發(fā)组题,然后將自己加到一個(gè)鏈表中葫男。epoll_wait函數(shù)的作用就是去查看這個(gè)鏈表中有沒有已經(jīng)準(zhǔn)備就緒的事件,如果有的話就通知應(yīng)用程序處理往踢,如此操作epoll_wait只需要遍歷就緒的事件描述符即可腾誉。
epoll在Java中的使用
目前針對(duì)Java服務(wù)器的非阻塞編程基本都是基于epoll的。在進(jìn)行非阻塞編程的時(shí)候有兩個(gè)步驟:1、注冊(cè)感興趣的事情利职;2趣效、調(diào)用select方法,查找感興趣的事件猪贪。
注冊(cè)感興趣的事件
我們?cè)诰帉慡ocket的非阻塞代碼的時(shí)候需要在Selector上注冊(cè)感興趣的事情跷敬,通常寫法是
serverSocketChannel.register(selector, SelectionKey.XXX)。來看一下這行代碼背后的執(zhí)行邏輯是什么樣的热押。
注冊(cè)的時(shí)候?qū)嶋H執(zhí)行的是EPollSelectorImp西傀。該方法主要有以下三步:
1、implRegister方法桶癣。在fdToKey的Map中插入channel對(duì)應(yīng)的文件描述法和SelectionKey的映射拥褂,當(dāng)做注冊(cè)Channel、關(guān)閉Channel牙寞、取消注冊(cè)等操作是都是操作此Map饺鹃。
2、往pollWrapper[Epoll實(shí)例]中放入channel實(shí)例间雀。
3悔详、往keys[HashSet]中放入SelectionKey
select方法
通過Java的Selector.select方法來獲取準(zhǔn)備好的鍵的時(shí)候?qū)嶋H執(zhí)行的代碼如下:
首先調(diào)用EPollArrayWrapper的poll方法,該方法做兩件事:1惹挟、調(diào)用epollCtl方法向epoll中注冊(cè)感興趣的事件茄螃;2、調(diào)用epollWait方法返回已就緒的文件描述符集合
然后調(diào)用updateSelectedKeys方法調(diào)用把epoll中就緒的文件描述符加到ready隊(duì)列中等待上層應(yīng)用處理, updateSelectedKeys通過fdToKey查找文件描述符對(duì)應(yīng)的SelectionKey连锯,并在SelectionKey對(duì)應(yīng)的channel中添加對(duì)應(yīng)的事件到ready隊(duì)列归苍。
水平觸發(fā)LT與邊緣觸發(fā)ET
epoll支持兩種觸發(fā)模式,分別是水平觸發(fā)和邊緣觸發(fā)萎庭。
LT是缺省的工作方式霜医,并且同時(shí)支持block和no-block socket。在這種做法中驳规,內(nèi)核告訴你一個(gè)文件描述符是否就緒了,然后你可以對(duì)這個(gè)就緒的fd進(jìn)行IO操作署海。如果你不作任何操作吗购,內(nèi)核還是會(huì)繼續(xù)通知你的。
ET是高速工作方式砸狞,只支持no-block socket捻勉。在這種模式下,當(dāng)描述符從未就緒變?yōu)榫途w時(shí)刀森,內(nèi)核會(huì)通知你一次踱启,并且除非你做了某些操作導(dǎo)致那個(gè)文件描述符不再為就緒狀態(tài)了,否則不會(huì)再次發(fā)送通知。
可以看到埠偿,本來內(nèi)核在被DMA中斷透罢,捕獲到IO設(shè)備來數(shù)據(jù)后,只需要查找這個(gè)數(shù)據(jù)屬于哪個(gè)文件描述符冠蒋,進(jìn)而通知線程里等待的函數(shù)即可羽圃,但是,LT要求內(nèi)核在通知階段還要繼續(xù)再掃描一次剛才所建立的內(nèi)核fd和io對(duì)應(yīng)的那個(gè)數(shù)組抖剿,因?yàn)閼?yīng)用程序可能沒有真正去讀上次通知有數(shù)據(jù)后的那些fd朽寞,這種溝通方式效率是很低下的,只是方便編程而已斩郎;
JDK并沒有實(shí)現(xiàn)邊緣觸發(fā)脑融,關(guān)于邊緣觸發(fā)和水平觸發(fā)的差異簡單列舉如下,邊緣觸發(fā)的性能更高缩宜,但編程難度也更高吨掌,netty就重新實(shí)現(xiàn)了Epoll機(jī)制,采用邊緣觸發(fā)方式脓恕;另外像nginx等也采用的是邊緣觸發(fā)膜宋。