在討論IO的時(shí)候,參與者通常有兩個(gè)角色:系統(tǒng)內(nèi)核和用戶進(jìn)程。用戶進(jìn)程發(fā)送 IO請(qǐng)求過(guò)后,系統(tǒng)內(nèi)核在準(zhǔn)備好IO數(shù)據(jù)后,會(huì)通過(guò)內(nèi)存拷貝的方式,將準(zhǔn)備好的緩存IO數(shù)據(jù)共享給用戶進(jìn)程緩存。
調(diào)用InputStream.read()或者OutputStream.write()時(shí),用戶進(jìn)程會(huì)阻塞住直到數(shù)據(jù)就緒,相當(dāng)于一線程一連接的方式。所以在采用Java IO時(shí),在Server端通常會(huì)采用對(duì)于每個(gè)新連接,起一個(gè)新的線程去處理,這樣后來(lái)的連接就不用等到之前的完成才能操作。但也帶來(lái)了問(wèn)題,畢竟線程是系統(tǒng)的稀缺資源,數(shù)量上會(huì)有瓶頸,達(dá)到一定數(shù)量后,性能急劇下降,內(nèi)存崩潰。不能應(yīng)對(duì)大量連接的情況,而且線程切換很耗費(fèi)系統(tǒng)資源。
基于Java IO的缺點(diǎn),NIO采用了新的設(shè)計(jì)方式,核心在Channel,Buffer,Selector。非阻塞主要依靠Selector,Channel在Selector上注冊(cè)自己感興趣的事件,然后Selector線程會(huì)輪詢注冊(cè)在自己身上的Channel,當(dāng)有數(shù)據(jù)準(zhǔn)備就緒時(shí),就通知相應(yīng)的Channel。這樣一個(gè)Selector可以管理多個(gè)Channel,但實(shí)際上還是阻塞的,現(xiàn)在不阻塞IO層面了,阻塞在Selector線程上了。而且采用輪詢的方式,效率比較低。
在Java NIO的基礎(chǔ)上,增加了AsynchronousChannelGroup,CompletionHandler,其中AsynchronousChannelGroup起到了事件收集和任務(wù)分發(fā)的作用,而CompletionHandler是綁定在事件上回調(diào)機(jī)制,從而達(dá)到異步。能否真正實(shí)現(xiàn)異步,關(guān)鍵還要看系統(tǒng)底層的實(shí)現(xiàn),當(dāng)前來(lái)看只有window的iocp實(shí)現(xiàn)了真正的異步,linux上還是通過(guò)epoll來(lái)模擬,是一種偽異步。
Select/Poll, epoll/kqueue, iocp
select模型
1. 最大并發(fā)數(shù)限制,因?yàn)橐粋€(gè)進(jìn)程所打開(kāi)的FD(文件描述符)是有限制的,由FD_SETSIZE設(shè)置,默認(rèn)值是1024/2048,因此Select模型的最大并發(fā)數(shù)就被相應(yīng)限制了。自己改改這個(gè)FD_SETSIZE?想法雖好,可是先看看下面吧…
2. 效率問(wèn)題,select每次調(diào)用都會(huì)線性掃描全部的FD集合,這樣效率就會(huì)呈現(xiàn)線性下降,把FD_SETSIZE改大的后果就是,大家都慢慢來(lái),什么?都超時(shí)了??!!
3. 內(nèi)核/用戶空間 內(nèi)存拷貝問(wèn)題,如何讓內(nèi)核把FD消息通知給用戶空間呢?在這個(gè)問(wèn)題上select采取了內(nèi)存拷貝方法。
基本上效率和select是相同的,select缺點(diǎn)的2和3它都沒(méi)有改掉。
把其他模型逐個(gè)批判了一下,再來(lái)看看Epoll的改進(jìn)之處吧,其實(shí)把select的缺點(diǎn)反過(guò)來(lái)那就是Epoll的優(yōu)點(diǎn)了。
1. Epoll沒(méi)有最大并發(fā)連接的限制,上限是最大可以打開(kāi)文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于2048, 一般來(lái)說(shuō)這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大,具體數(shù)目可以cat /proc/sys/fs/file-max察看。
2. 效率提升,Epoll最大的優(yōu)點(diǎn)就在于它只管你“活躍”的連接,而跟連接總數(shù)無(wú)關(guān),因此在實(shí)際的網(wǎng)絡(luò)環(huán)境中,Epoll的效率就會(huì)遠(yuǎn)遠(yuǎn)高于select和poll。
3. 內(nèi)存拷貝,Epoll在這點(diǎn)上使用了“共享內(nèi)存”,這個(gè)內(nèi)存拷貝也省略了
首先application調(diào)用 recvfrom()轉(zhuǎn)入kernel,注意kernel有2個(gè)過(guò)程,wait for data和copy data from kernel to user。直到最后copy complete后,recvfrom()才返回。此過(guò)程一直是阻塞的
可以看見(jiàn),如果直接操作它,那就是個(gè)輪詢。。直到內(nèi)核緩沖區(qū)有數(shù)據(jù)。
I/O multiplexing (select and poll)
select先阻塞,有活動(dòng)套接字才返回。與blocking I/O相比,select會(huì)有兩次系統(tǒng)調(diào)用,但是select能處理多個(gè)套接字。
signal driven I/O (SIGIO) :只有Unix系統(tǒng)支持
與I/O multiplexing (select and poll)相比,它的優(yōu)勢(shì)是,免去了select的阻塞與輪詢,當(dāng)有活躍套接字時(shí),由注冊(cè)的handler處理。
asynchronous I/O (the POSIX aio_functions)
很少有*nix系統(tǒng)支持,windows的IOCP則是此模型
完全異步的I/O復(fù)用機(jī)制,因?yàn)榭v觀上面其它四種模型,至少都會(huì)在由kernel copy data to appliction時(shí)阻塞。而該模型是當(dāng)copy完成后才通知application,可見(jiàn)是純異步的。好像只有windows的完成端口是這個(gè)模型,效率也很出色。
服務(wù)器程序策略
服務(wù)器程序策略主要指的是網(wǎng)絡(luò)編程時(shí)的開(kāi)發(fā)策略,記得原來(lái)畢業(yè)找工作面試后端開(kāi)發(fā)職位的時(shí)候這是一定會(huì)被問(wèn)到。在討論這個(gè)問(wèn)題之前,必須要說(shuō)的就是著名的C10K問(wèn)題。
C10K是current 10k connection的簡(jiǎn)寫(xiě),描述的是服務(wù)端如何處理同時(shí)到來(lái)的上萬(wàn)個(gè)client連接的問(wèn)題,簡(jiǎn)而言之就是高并發(fā)的問(wèn)題。為了解決這個(gè)問(wèn)題,有以下幾種經(jīng)典的服務(wù)端策略:
1. 為每個(gè)連接分配一個(gè)線程/進(jìn)程來(lái)處理
這個(gè)策略是指服務(wù)器為每一個(gè)到來(lái)的連接都分配一個(gè)新的線程/進(jìn)程,使用阻塞式的I/O來(lái)處理。Java和Apache都是這種策略,這種策略簡(jiǎn)單并且,能實(shí)現(xiàn)比較復(fù)雜的交互。然后系統(tǒng)分配進(jìn)程/線程是需要資源的,而且因?yàn)槭褂昧俗枞降腎/O,當(dāng)有大量連接到來(lái)時(shí)候系統(tǒng)資源會(huì)是性能瓶頸。
這個(gè)思路是最直觀,最容易想到的,以至于造成最大的誤解:
當(dāng)我們聽(tīng)說(shuō)Node.js是單線程模型的時(shí)候就認(rèn)定了他和開(kāi)發(fā)服務(wù)器程序是無(wú)緣的。
第一種策略在高并發(fā)時(shí)會(huì)創(chuàng)建過(guò)多的線程,然而這些線程大部分時(shí)間都是在block等待數(shù)據(jù)。這種策略只用一個(gè)線程來(lái)監(jiān)聽(tīng)多個(gè)socket連接,如果有數(shù)據(jù)到來(lái)就處理,讀寫(xiě)完成后再次進(jìn)入block監(jiān)聽(tīng)。這種策略使用select/poll/epoll這樣的多路復(fù)用I/O來(lái)實(shí)現(xiàn)。
同樣是只用一個(gè)線程來(lái)處理所有的客戶端連接,使用非阻塞I/O和事件機(jī)制來(lái)通知。Node.js就是使用了這種策略,這種策略實(shí)現(xiàn)簡(jiǎn)單,方便移植。缺點(diǎn)是只有一個(gè)線程所以不能充分利用CPU的多核性能,這也是Node.js用來(lái)開(kāi)發(fā)服務(wù)器程序被吐槽最多的地方。
這是對(duì)第二種策略的改進(jìn)型,分配多個(gè)線程,每個(gè)線程負(fù)責(zé)一定量的連接請(qǐng)求,使用非阻塞的I/O和事件機(jī)制。
與第三種策略相比,這種策略使用異步的I/O,由內(nèi)核來(lái)完成數(shù)據(jù)的準(zhǔn)備和copy。這種策略在支持異步I/O的操作系統(tǒng)上效率很高,用戶無(wú)需任何操作就能完成數(shù)據(jù)的收發(fā),這一切都由內(nèi)核默默的完成。
同樣的用上面那個(gè)小郵局取快遞的例子再來(lái)說(shuō)明下以上這四種網(wǎng)絡(luò)編程策略:
每個(gè)線程同步阻塞處理一個(gè)連接:每個(gè)碼農(nóng)都在小郵局等自己的快遞到來(lái),快遞到了之后自己把包裹取走;這種做法比較低效,大家都在等快遞,都沒(méi)時(shí)間搬磚了;
一個(gè)線程處理所有連接:讓碼農(nóng)小明一個(gè)人在小郵局等所有的快遞,只要有包裹來(lái)了就幫忙取走送到相應(yīng)人座位上;這種方式只需要小明一個(gè)人,但是如果小明送包裹的速度不夠快就會(huì)導(dǎo)致新來(lái)的包裹積壓,不能及時(shí)送達(dá);(這種需要很長(zhǎng)時(shí)間才能送達(dá)的包裹就是CPU密集型的task)
多個(gè)線程,每個(gè)線程負(fù)責(zé)一組連接:這種方式就是多找一個(gè)小明一起來(lái)完成,是第二種方式的改進(jìn)型;
多個(gè)線程,每個(gè)線程使用異步I/O負(fù)責(zé)一組連接:這種方式不用小明了,而是讓小郵局的工作人員來(lái)做之前小明的工作,這樣每個(gè)碼農(nóng)都能安心搬磚了,最大化了工作效率;
epoll也是IO復(fù)用,比傳統(tǒng)的Select/poll有很大的優(yōu)化,Linux2.6開(kāi)始都使用了epoll,而且并不是信號(hào)驅(qū)動(dòng)的,只是可以被中斷信號(hào)中斷。IO復(fù)用是調(diào)用方被動(dòng)接受內(nèi)核的通知,需要阻塞在Select/poll/epoll函數(shù)上,并不是單個(gè)IO上,就像你調(diào)用一個(gè)函數(shù)等待它返回一樣,它返回的時(shí)候就是知道內(nèi)核有數(shù)據(jù)到來(lái)的時(shí)候,它會(huì)通知你有哪些你注冊(cè)過(guò)的IO上有數(shù)據(jù)了,但是數(shù)據(jù)存在內(nèi)核中,需要你自己去從內(nèi)核拷貝數(shù)據(jù)到用戶空間(同步方式),這點(diǎn)也是同步和異步的區(qū)別,異步方式的話,當(dāng)你收到通知的時(shí)候數(shù)據(jù)已經(jīng)在用戶空間了;信號(hào)驅(qū)動(dòng)方式是基于系統(tǒng)的信號(hào)機(jī)制,你要先注冊(cè)一個(gè)信號(hào)處理函數(shù)(當(dāng)你收到系統(tǒng)信號(hào)時(shí)進(jìn)行何種反應(yīng)),處理函數(shù)會(huì)在另一個(gè)線程中執(zhí)行,所以并不會(huì)阻塞當(dāng)前的進(jìn)程,當(dāng)內(nèi)核知道有數(shù)據(jù)到來(lái),就向?qū)?yīng)的進(jìn)程發(fā)送信號(hào),通知他數(shù)據(jù)來(lái)了,快來(lái)讀,進(jìn)程收到信號(hào),啟動(dòng)一個(gè)線程執(zhí)行對(duì)應(yīng)的信號(hào)處理函數(shù)。
http://www.programgo.com/article/63742464168/