對于io模型這塊內(nèi)容之前基本完全沒有接觸過,有了些許了解之后還是很困昏均蜜,select李剖、poll、epoll的關(guān)系以及服務(wù)器ngnix囤耳、apache的工作機(jī)制篙顺,還有JAVA NIO、BIO充择、AIO這么多的概念混雜起來實(shí)在是不好理解德玫,想稍微梳理一下。
網(wǎng)絡(luò)IO模型
網(wǎng)絡(luò)IO的本質(zhì)是socket的讀取聪铺,socket在linux系統(tǒng)被抽象為流化焕,IO可以理解為對流的操作
IO其實(shí)我們并不陌生,站在操作系統(tǒng)的角度上說铃剔,io一般指訪問磁盤數(shù)據(jù)撒桨,可以分為兩步,以read操作舉例的話:
- 第一階段:等待數(shù)據(jù)準(zhǔn)備 (Waiting for the data to be ready)键兜。
- 第二階段:將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中 (Copying the data from the kernel to the process)凤类。
而網(wǎng)絡(luò)IO也是如此,只不過它是讀取的不是磁盤普气,而是socket:
- 第一步:通常涉及等待網(wǎng)絡(luò)上的數(shù)據(jù)分組到達(dá)谜疤,然后被復(fù)制到內(nèi)核的某個(gè)緩沖區(qū)。
- 第二步:把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū)。
在理解網(wǎng)絡(luò)IO模型之前夷磕,我們得先準(zhǔn)備些IO模型的基礎(chǔ)知識(shí)
IO模型
Unix 有五種 I/O 模型:
- 阻塞IO(bloking IO)
- 非阻塞IO(non-blocking IO)
- 多路復(fù)用IO(multiplexing IO)
- 信號(hào)驅(qū)動(dòng)式IO(signal-driven IO)
- 異步IO(asynchronous IO)
每個(gè) IO 模型都有自己的使用模式履肃,它們對于特定的應(yīng)用程序都有自己的優(yōu)點(diǎn)。下面提供一個(gè)簡單的圖片以供了解坐桩。
阻塞式 IO
應(yīng)用進(jìn)程被阻塞尺棋,直到數(shù)據(jù)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū)中才返回。
應(yīng)該注意到绵跷,在阻塞的過程中膘螟,其它程序還可以執(zhí)行,因此阻塞不意味著整個(gè)操作系統(tǒng)都被阻塞碾局。因?yàn)槠渌绦蜻€可以執(zhí)行荆残,因此不消耗 CPU 時(shí)間,這種模型的 CPU 利用率效率會(huì)比較高净当。
下圖中内斯,recvfrom 用于接收 Socket 傳來的數(shù)據(jù),并復(fù)制到應(yīng)用進(jìn)程的緩沖區(qū) buf 中蚯瞧。這里把 recvfrom() 當(dāng)成系統(tǒng)調(diào)用嘿期。
[圖片上傳失敗...(image-111ada-1538122299754)]
非阻塞式 IO
應(yīng)用進(jìn)程執(zhí)行系統(tǒng)調(diào)用之后,內(nèi)核返回一個(gè)錯(cuò)誤碼埋合。應(yīng)用進(jìn)程可以繼續(xù)執(zhí)行备徐,但是需要不斷的執(zhí)行系統(tǒng)調(diào)用來獲知 I/O 是否完成,這種方式稱為輪詢(polling)甚颂。
由于 CPU 要處理更多的系統(tǒng)調(diào)用蜜猾,因此這種模型的 CPU 利用率是比較低的。
[圖片上傳失敗...(image-76babc-1538122299754)]
多路復(fù)用IO
由于阻塞式IO通過輪詢得到的只是一個(gè)IO任務(wù)是否完成振诬,而可能有多個(gè)任務(wù)在同時(shí)進(jìn)行蹭睡,因此就想到了能否輪詢多個(gè)IO任務(wù)的狀態(tài),只要有任何一個(gè)任務(wù)完成赶么,就去處理它肩豁。這就是所謂的IO多路復(fù)用。LINUX下具體的實(shí)現(xiàn)方式就是select辫呻、poll清钥、epoll。
這種機(jī)制可以讓單個(gè)進(jìn)程具有處理多個(gè) IO 事件的能力放闺。又被稱為 Event Driven IO祟昭,即事件驅(qū)動(dòng) IO。
最實(shí)際的應(yīng)用場景就是web服務(wù)器響應(yīng)連接的方式怖侦,IO 復(fù)用可支持更多的連接篡悟,同時(shí)不需要進(jìn)程線程創(chuàng)建和切換的開銷谜叹,系統(tǒng)開銷更小。
[站外圖片上傳中...(image-3a854e-1538122299754)]
多路復(fù)用的特點(diǎn)是通過一種機(jī)制一個(gè)進(jìn)程能同時(shí)等待多個(gè)IO文件描述符搬葬,內(nèi)核監(jiān)視這些文件描述符(套接字描述符)诊霹,其中的任意一個(gè)進(jìn)入讀就緒狀態(tài)磨取,select串述, poll酱讶,epoll函數(shù)就可以返回励背。對于監(jiān)視的方式挨队,又可以分為 select平斩, poll恼策, epoll三種方式码倦。
在IO多路復(fù)用中企孩,實(shí)際中,對于每一個(gè)socket袁稽,一般都設(shè)置成為non-blocking勿璃,但是,如上圖所示推汽,整個(gè)用戶的進(jìn)程其實(shí)是一直被block的补疑。只不過進(jìn)程是被select這個(gè)函數(shù)block,而不是被socket IO給block歹撒。所以IO多路復(fù)用是阻塞在select莲组,epoll這樣的系統(tǒng)調(diào)用之上,而沒有阻塞在真正的I/O系統(tǒng)調(diào)用如recvfrom之上暖夭。
信號(hào)驅(qū)動(dòng) IO
應(yīng)用進(jìn)程使用 sigaction 系統(tǒng)調(diào)用锹杈,內(nèi)核立即返回,應(yīng)用進(jìn)程可以繼續(xù)執(zhí)行迈着,也就是說等待數(shù)據(jù)階段應(yīng)用進(jìn)程是非阻塞的竭望。內(nèi)核在數(shù)據(jù)到達(dá)時(shí)向應(yīng)用進(jìn)程發(fā)送 SIGIO 信號(hào),應(yīng)用進(jìn)程收到之后在信號(hào)處理程序中調(diào)用 recvfrom 將數(shù)據(jù)從內(nèi)核復(fù)制到應(yīng)用進(jìn)程中裕菠。
相比于非阻塞式 I/O 的輪詢方式咬清,信號(hào)驅(qū)動(dòng) I/O 的 CPU 利用率更高。
[圖片上傳失敗...(image-8f9a6f-1538122299754)]
異步 IO
應(yīng)用進(jìn)程執(zhí)行 aio_read 系統(tǒng)調(diào)用會(huì)立即返回奴潘,應(yīng)用進(jìn)程可以繼續(xù)執(zhí)行旧烧,不會(huì)被阻塞,內(nèi)核會(huì)在所有操作完成之后向應(yīng)用進(jìn)程發(fā)送信號(hào)萤彩。
異步 IO 與信號(hào)驅(qū)動(dòng) IO 的區(qū)別在于粪滤,異步 IO 的信號(hào)是通知應(yīng)用進(jìn)程 IO 完成,而信號(hào)驅(qū)動(dòng) IO 的信號(hào)是通知應(yīng)用進(jìn)程可以開始 IO雀扶。
[圖片上傳失敗...(image-326ee-1538122299754)]
五大 IO 模型比較
前四種 I/O 模型的主要區(qū)別在于第一個(gè)階段杖小,而第二個(gè)階段是一樣的:將數(shù)據(jù)從內(nèi)核復(fù)制到應(yīng)用進(jìn)程過程中肆汹,應(yīng)用進(jìn)程會(huì)被阻塞。
[圖片上傳失敗...(image-377ab2-1538122299754)]
blocking和non-blocking區(qū)別
調(diào)用blocking IO會(huì)一直block住對應(yīng)的進(jìn)程直到操作完成予权,而non-blocking IO在kernel還準(zhǔn)備數(shù)據(jù)的情況下會(huì)立刻返回昂勉。
synchronous IO和asynchronous IO區(qū)別
在說明synchronous IO和asynchronous IO的區(qū)別之前,需要先給出兩者的定義扫腺。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;
- 同步 I/O:應(yīng)用進(jìn)程在調(diào)用 recvfrom 操作時(shí)會(huì)阻塞岗照。
- 異步 I/O:不會(huì)阻塞。
阻塞式 I/O笆环、非阻塞式 I/O攒至、I/O 復(fù)用和信號(hào)驅(qū)動(dòng) I/O 都是同步 I/O,雖然非阻塞式 I/O 和信號(hào)驅(qū)動(dòng) I/O 在等待數(shù)據(jù)階段不會(huì)阻塞躁劣,但是在之后的將數(shù)據(jù)從內(nèi)核復(fù)制到應(yīng)用進(jìn)程這個(gè)操作會(huì)阻塞迫吐。
select,poll账忘,epoll比較
select志膀,poll,epoll都是IO多路復(fù)用的機(jī)制鳖擒。I/O多路復(fù)用就通過一種機(jī)制溉浙,可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(一般是讀就緒或者寫就緒)蒋荚,能夠通知程序進(jìn)行相應(yīng)的讀寫操作戳稽。
select
select的調(diào)用過程如下所示:
(1)使用copy_from_user從用戶空間拷貝fd_set到內(nèi)核空間
(2)注冊回調(diào)函數(shù)__pollwait
(3)遍歷所有fd,調(diào)用其對應(yīng)的poll方法(對于socket期升,這個(gè)poll方法是sock_poll广鳍,sock_poll根據(jù)情況會(huì)調(diào)用到tcp_poll,udp_poll或者datagram_poll)
(4)以tcp_poll為例,其核心實(shí)現(xiàn)就是__pollwait吓妆,也就是上面注冊的回調(diào)函數(shù)赊时。
(5)__pollwait的主要工作就是把current(當(dāng)前進(jìn)程)掛到設(shè)備的等待隊(duì)列中,不同的設(shè)備有不同的等待隊(duì)列行拢,對于tcp_poll來說祖秒,其等待隊(duì)列是sk->sk_sleep(注意把進(jìn)程掛到等待隊(duì)列中并不代表進(jìn)程已經(jīng)睡眠了)。在設(shè)備收到一條消息(網(wǎng)絡(luò)設(shè)備)或填寫完文件數(shù)據(jù)(磁盤設(shè)備)后舟奠,會(huì)喚醒設(shè)備等待隊(duì)列上睡眠的進(jìn)程竭缝,這時(shí)current便被喚醒了。
(6)poll方法返回時(shí)會(huì)返回一個(gè)描述讀寫操作是否就緒的mask掩碼沼瘫,根據(jù)這個(gè)mask掩碼給fd_set賦值抬纸。
(7)如果遍歷完所有的fd,還沒有返回一個(gè)可讀寫的mask掩碼耿戚,則會(huì)調(diào)用schedule_timeout是調(diào)用select的進(jìn)程(也就是current)進(jìn)入睡眠湿故。當(dāng)設(shè)備驅(qū)動(dòng)發(fā)生自身資源可讀寫后阿趁,會(huì)喚醒其等待隊(duì)列上睡眠的進(jìn)程。如果超過一定的超時(shí)時(shí)間(schedule_timeout指定)坛猪,還是沒人喚醒脖阵,則調(diào)用select的進(jìn)程會(huì)重新被喚醒獲得CPU,進(jìn)而重新遍歷fd墅茉,判斷有沒有就緒的fd命黔。
(8)把fd_set從內(nèi)核空間拷貝到用戶空間。
總結(jié):
select的幾大缺點(diǎn):
(1)每次調(diào)用select就斤,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài)悍募,這個(gè)開銷在fd很多時(shí)會(huì)很大
(2)同時(shí)每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來的所有fd,這個(gè)開銷在fd很多時(shí)也很大
(3)select支持的文件描述符數(shù)量太小了洋机,默認(rèn)是1024
poll
poll的實(shí)現(xiàn)和select非常相似搜立,只是描述fd集合的方式不同,poll使用pollfd結(jié)構(gòu)而不是select的fd_set結(jié)構(gòu)槐秧,其他的都差不多。
epoll
epoll既然是對select和poll的改進(jìn)忧设,就應(yīng)該能避免上述的三個(gè)缺點(diǎn)刁标。那epoll都是怎么解決的呢?在此之前址晕,我們先看一下epoll和select和poll的調(diào)用接口上的不同膀懈,select和poll都只提供了一個(gè)函數(shù)——select或者poll函數(shù)。而epoll提供了三個(gè)函數(shù)谨垃,epoll_create,epoll_ctl和epoll_wait启搂,epoll_create是創(chuàng)建一個(gè)epoll句柄;epoll_ctl是注冊要監(jiān)聽的事件類型刘陶;epoll_wait則是等待事件的產(chǎn)生胳赌。
對于第一個(gè)缺點(diǎn),epoll的解決方案在epoll_ctl函數(shù)中匙隔。每次注冊新的事件到epoll句柄中時(shí)(在epoll_ctl中指定EPOLL_CTL_ADD)疑苫,會(huì)把所有的fd拷貝進(jìn)內(nèi)核,而不是在epoll_wait的時(shí)候重復(fù)拷貝纷责。epoll保證了每個(gè)fd在整個(gè)過程中只會(huì)拷貝一次捍掺。
對于第二個(gè)缺點(diǎn),epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應(yīng)的設(shè)備等待隊(duì)列中再膳,而只在epoll_ctl時(shí)把current掛一遍(這一遍必不可少)并為每個(gè)fd指定一個(gè)回調(diào)函數(shù)挺勿,當(dāng)設(shè)備就緒,喚醒等待隊(duì)列上的等待者時(shí)喂柒,就會(huì)調(diào)用這個(gè)回調(diào)函數(shù)不瓶,而這個(gè)回調(diào)函數(shù)會(huì)把就緒的fd加入一個(gè)就緒鏈表)禾嫉。epoll_wait的工作實(shí)際上就是在這個(gè)就緒鏈表中查看有沒有就緒的fd(利用schedule_timeout()實(shí)現(xiàn)睡一會(huì),判斷一會(huì)的效果湃番,和select實(shí)現(xiàn)中的第7步是類似的)夭织。
對于第三個(gè)缺點(diǎn),epoll沒有這個(gè)限制吠撮,它所支持的FD上限是最大可以打開文件的數(shù)目尊惰,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬左右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大泥兰。
總結(jié)
(1)select弄屡,poll實(shí)現(xiàn)需要自己不斷輪詢所有fd集合,直到設(shè)備就緒鞋诗,期間可能要睡眠和喚醒多次交替膀捷。而epoll其實(shí)也需要調(diào)用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替削彬,但是它是設(shè)備就緒時(shí)全庸,調(diào)用回調(diào)函數(shù),把就緒fd放入就緒鏈表中融痛,并喚醒在epoll_wait中進(jìn)入睡眠的進(jìn)程壶笼。雖然都要睡眠和交替,但是select和poll在“醒著”的時(shí)候要遍歷整個(gè)fd集合雁刷,而epoll在“醒著”的時(shí)候只要判斷一下就緒鏈表是否為空就行了覆劈,這節(jié)省了大量的CPU時(shí)間。這就是回調(diào)機(jī)制帶來的性能提升沛励。
(2)select责语,poll每次調(diào)用都要把fd集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次,并且要把current往設(shè)備等待隊(duì)列中掛一次目派,而epoll只要一次拷貝坤候,而且把current往等待隊(duì)列上掛也只掛一次(在epoll_wait的開始,注意這里的等待隊(duì)列并不是設(shè)備等待隊(duì)列企蹭,只是一個(gè)epoll內(nèi)部定義的等待隊(duì)列)铐拐。這也能節(jié)省不少的開銷。
應(yīng)用場景
很容易產(chǎn)生一種錯(cuò)覺認(rèn)為只要用 epoll 就可以了练对,select 和 poll 都已經(jīng)過時(shí)了遍蟋,其實(shí)它們都有各自的使用場景。
1. select 應(yīng)用場景
select 的 timeout 參數(shù)精度為 1ns螟凭,而 poll 和 epoll 為 1ms虚青,因此 select 更加適用于實(shí)時(shí)要求更高的場景,比如核反應(yīng)堆的控制螺男。
select 可移植性更好棒厘,幾乎被所有主流平臺(tái)所支持纵穿。
2. poll 應(yīng)用場景
poll 沒有最大描述符數(shù)量的限制,如果平臺(tái)支持并且對實(shí)時(shí)性要求不高奢人,應(yīng)該使用 poll 而不是 select谓媒。
需要同時(shí)監(jiān)控小于 1000 個(gè)描述符,就沒有必要使用 epoll何乎,因?yàn)檫@個(gè)應(yīng)用場景下并不能體現(xiàn) epoll 的優(yōu)勢句惯。
需要監(jiān)控的描述符狀態(tài)變化多,而且都是非常短暫的支救,也沒有必要使用 epoll抢野。因?yàn)?epoll 中的所有描述符都存儲(chǔ)在內(nèi)核中,造成每次需要對描述符的狀態(tài)改變都需要通過 epoll_ctl() 進(jìn)行系統(tǒng)調(diào)用各墨,頻繁系統(tǒng)調(diào)用降低效率指孤。并且epoll 的描述符存儲(chǔ)在內(nèi)核,不容易調(diào)試贬堵。
3. epoll 應(yīng)用場景
只需要運(yùn)行在 Linux 平臺(tái)上恃轩,并且有非常大量的描述符需要同時(shí)輪詢,而且這些連接最好是長連接黎做。
web服務(wù)器設(shè)計(jì)模型
首先我們先明確并發(fā)和并行的概念叉跛。
并發(fā)&并行
并發(fā)是指宏觀上在一段時(shí)間內(nèi)能同時(shí)運(yùn)行多個(gè)程序,而并行則指同一時(shí)刻能運(yùn)行多個(gè)指令引几。
并行需要硬件支持,如多流水線或者多處理器挽铁。
操作系統(tǒng)通過引入進(jìn)程和線程伟桅,使得程序能夠并發(fā)運(yùn)行。
對于web服務(wù)而言叽掘,并發(fā)是指同時(shí)進(jìn)行的任務(wù)數(shù)(如同時(shí)服務(wù)的 HTTP 請求)楣铁,而并行是可以同時(shí)工作的物理資源數(shù)量(如 CPU 核數(shù))。
而針對并發(fā)IO而言更扁,Reactor模型是一種常見的處理方式
Reactor模型
Reactor的中心思想是將所有要處理的I/O事件注冊到一個(gè)中心I/O多路復(fù)用器上盖腕,同時(shí)主線程/進(jìn)程阻塞在多路復(fù)用器上;一旦有I/O事件到來或是準(zhǔn)備就緒(文件描述符或socket可讀浓镜、寫)溃列,多路復(fù)用器返回并將事先注冊的相應(yīng)I/O事件分發(fā)到對應(yīng)的處理器中。
Reactor是一種事件驅(qū)動(dòng)機(jī)制膛薛,用“好萊塢原則”來形容Reactor再合適不過了:不要打電話給我們听隐,我們會(huì)打電話通知你。
Reactor模式與Observer模式在某些方面極為相似:當(dāng)一個(gè)主體發(fā)生改變時(shí)哄啄,所有依屬體都得到通知雅任。不過风范,觀察者模式與單個(gè)事件源關(guān)聯(lián),而反應(yīng)器模式則與多個(gè)事件源關(guān)聯(lián) 沪么。
在Reactor模式中硼婿,有5個(gè)關(guān)鍵的參與者:
- 描述符(handle):由操作系統(tǒng)提供的資源,用于識(shí)別每一個(gè)事件禽车,如Socket描述符寇漫、文件描述符、信號(hào)的值等哭当。在Linux中猪腕,它用一個(gè)整數(shù)來表示。事件可以來自外部钦勘,如來自客戶端的連接請求陋葡、數(shù)據(jù)等。事件也可以來自內(nèi)部彻采,如信號(hào)腐缤、定時(shí)器事件。
- 同步事件多路分離器(event demultiplexer):事件的到來是隨機(jī)的肛响、異步的岭粤,無法預(yù)知程序何時(shí)收到一個(gè)客戶連接請求或收到一個(gè)信號(hào)。所以程序要循環(huán)等待并處理事件特笋,這就是事件循環(huán)剃浇。在事件循環(huán)中,等待事件一般使用I/O復(fù)用技術(shù)實(shí)現(xiàn)猎物。在linux系統(tǒng)上一般是select虎囚、poll、epol_waitl等系統(tǒng)調(diào)用蔫磨,用來等待一個(gè)或多個(gè)事件的發(fā)生淘讥。I/O框架庫一般將各種I/O復(fù)用系統(tǒng)調(diào)用封裝成統(tǒng)一的接口,稱為事件多路分離器堤如。調(diào)用者會(huì)被阻塞蒲列,直到分離器分離的描述符集上有事件發(fā)生。
- 事件處理器(event handler):I/O框架庫提供的事件處理器通常是由一個(gè)或多個(gè)模板函數(shù)組成的接口搀罢。這些模板函數(shù)描述了和應(yīng)用程序相關(guān)的對某個(gè)事件的操作蝗岖,用戶需要繼承它來實(shí)現(xiàn)自己的事件處理器,即具體事件處理器榔至。因此剪侮,事件處理器中的回調(diào)函數(shù)一般聲明為虛函數(shù),以支持用戶拓展。
- 具體的事件處理器(concrete event handler):是事件處理器接口的實(shí)現(xiàn)瓣俯。它實(shí)現(xiàn)了應(yīng)用程序提供的某個(gè)服務(wù)杰标。每個(gè)具體的事件處理器總和一個(gè)描述符相關(guān)。它使用描述符來識(shí)別事件彩匕、識(shí)別應(yīng)用程序提供的服務(wù)腔剂。
- Reactor 管理器(reactor):定義了一些接口,用于應(yīng)用程序控制事件調(diào)度驼仪,以及應(yīng)用程序注冊掸犬、刪除事件處理器和相關(guān)的描述符。它是事件處理器的調(diào)度核心绪爸。 Reactor管理器使用同步事件分離器來等待事件的發(fā)生湾碎。一旦事件發(fā)生,Reactor管理器先是分離每個(gè)事件奠货,然后調(diào)度事件處理器介褥,最后調(diào)用相關(guān)的模 板函數(shù)來處理這個(gè)事件。
[圖片上傳失敗...(image-740c74-1538122299754)]
可以看出递惋,是Reactor管理器并不是應(yīng)用程序負(fù)責(zé)等待事件柔滔、分離事件和調(diào)度事件。Reactor并沒有被具體的事件處理器調(diào)度萍虽,而是管理器調(diào)度具體的事件處理器睛廊,由事件處理器對發(fā)生的事件作出處理,這就是Hollywood原則杉编。應(yīng)用程序要做的僅僅是實(shí)現(xiàn)一個(gè)具體的事件處理器超全,然后把它注冊到Reactor管理器中。接下來的工作由管理器來完成:如果有相應(yīng)的事件發(fā)生邓馒,Reactor會(huì)主動(dòng)調(diào)用具體的事件處理器嘶朱,由事件處理器對發(fā)生的事件作出處理。
為什么使用Reactor
有了I/O復(fù)用绒净,有了epoll已經(jīng)可以使服務(wù)器并發(fā)幾十萬連接的同時(shí)见咒,維持高TPS了偿衰,難道這還不夠嗎挂疆?
答案是,技術(shù)層面足夠了下翎,但在軟件工程層面卻是不夠的缤言。
程序使用IO復(fù)用的難點(diǎn)在哪里呢?
1個(gè)請求可能由多次IO處理完成视事,但相比傳統(tǒng)的單線程完整處理請求生命期的方法胆萧,IO復(fù)用在人的大腦思維中并不自然,因?yàn)椋绦騿T編程中跌穗,處理請求A的時(shí)候订晌,假定A請求必須經(jīng)過多個(gè)IO操作A1-An(兩次IO間可能間隔很長時(shí)間),每經(jīng)過一次IO操作蚌吸,再調(diào)用IO復(fù)用時(shí)锈拨,IO復(fù)用的調(diào)用返回里,非掣耄可能不再有A奕枢,而是返回了請求B。即請求A會(huì)經(jīng)常被請求B打斷佩微,處理請求B時(shí)缝彬,又被C打斷。這種思維下哺眯,編程容易出錯(cuò)谷浅。
在程序中:
某一瞬間,服務(wù)器共有10萬個(gè)并發(fā)連接族购,此時(shí)壳贪,一次IO復(fù)用接口的調(diào)用返回了100個(gè)活躍的連接等待處理。先根據(jù)這100個(gè)連接找出其對應(yīng)的對象寝杖,這并不難违施,epoll的返回連接數(shù)據(jù)結(jié)構(gòu)里就有這樣的指針可以用。接著瑟幕,循環(huán)的處理每一個(gè)連接磕蒲,找出這個(gè)對象此刻的上下文狀態(tài),再使用read只盹、write這樣的網(wǎng)絡(luò)IO獲取此次的操作內(nèi)容辣往,結(jié)合上下文狀態(tài)查詢此時(shí)應(yīng)當(dāng)選擇哪個(gè)業(yè)務(wù)方法處理,調(diào)用相應(yīng)方法完成操作后殖卑,若請求結(jié)束站削,則刪除對象及其上下文。
這樣孵稽,我們就陷入了面向過程編程方法之中了许起,在面向應(yīng)用、快速響應(yīng)為王的移動(dòng)互聯(lián)網(wǎng)時(shí)代菩鲜,這樣做早晚得把自己玩死园细。我們的主程序需要關(guān)注各種不同類型的請求,在不同狀態(tài)下接校,對于不同的請求命令選擇不同的業(yè)務(wù)處理方法猛频。這會(huì)導(dǎo)致隨著請求類型的增加,請求狀態(tài)的增加,請求命令的增加鹿寻,主程序復(fù)雜度快速膨脹睦柴,導(dǎo)致維護(hù)越來越困難,苦逼的程序員再也不敢輕易接新需求毡熏、重構(gòu)爱只。
反應(yīng)堆是解決上述軟件工程問題的一種途徑,它也許并不優(yōu)雅招刹,開發(fā)效率上也不是最高的恬试,但其執(zhí)行效率與面向過程的使用IO復(fù)用卻幾乎是等價(jià)的,所以疯暑,無論是nginx训柴、memcached、redis等等這些高性能組件的代名詞妇拯,都義無反顧的一頭扎進(jìn)了反應(yīng)堆的懷抱中幻馁。
反應(yīng)堆模式可以在軟件工程層面,將事件驅(qū)動(dòng)框架分離出具體業(yè)務(wù)越锈,將不同類型請求之間用OO的思想分離仗嗦。通常,反應(yīng)堆不僅使用IO復(fù)用處理網(wǎng)絡(luò)事件驅(qū)動(dòng)甘凭,還會(huì)實(shí)現(xiàn)定時(shí)器來處理時(shí)間事件的驅(qū)動(dòng)(請求的超時(shí)處理或者定時(shí)任務(wù)的處理)
Reactor的幾種模式
1 單線程模式
這是最簡單的單Reactor單線程模型稀拐。Reactor線程是個(gè)多面手,負(fù)責(zé)多路分離套接字丹弱,Accept新連接德撬,并分派請求到處理器鏈中。該模型適用于處理器鏈中業(yè)務(wù)處理組件能快速完成的場景躲胳。不過這種單線程模型不能充分利用多核資源蜓洪,所以實(shí)際使用的不多。
2 多線程模式(單Reactor)
該模型在事件處理器(Handler)鏈部分采用了多線程(線程池)坯苹,也是后端程序常用的模型隆檀。
3 多線程模式(多個(gè)Reactor)
比起第二種模型,它是將Reactor分成兩部分粹湃,mainReactor負(fù)責(zé)監(jiān)聽并accept新連接恐仑,然后將建立的socket通過多路復(fù)用器(Acceptor)分派給subReactor。subReactor負(fù)責(zé)多路分離已連接的socket再芋,讀寫網(wǎng)絡(luò)數(shù)據(jù)菊霜;業(yè)務(wù)處理功能坚冀,其交給worker線程池完成济赎。通常,subReactor個(gè)數(shù)上可與CPU個(gè)數(shù)等同。
Proacotr模型
Proactor是和異步I/O相關(guān)的司训。
比較
以讀操作為例:
在Reactor(同步)中實(shí)現(xiàn)讀:
- 注冊讀就緒事件和相應(yīng)的事件處理器
- 事件分離器等待事件
- 事件到來构捡,激活分離器,分離器調(diào)用事件對應(yīng)的處理器壳猜。
- 事件處理器完成實(shí)際的讀操作勾徽,處理讀到的數(shù)據(jù),注冊新的事件统扳,然后返還控制權(quán)喘帚。
Proactor(異步)中的讀:
- 處理器發(fā)起異步讀操作(注意:操作系統(tǒng)必須支持異步IO)。在這種情況下咒钟,處理器無視IO就緒事件吹由,它關(guān)注的是完成事件。
- 事件分離器等待操作完成事件
- 在分離器等待過程中朱嘴,操作系統(tǒng)利用并行的內(nèi)核線程執(zhí)行實(shí)際的讀操作倾鲫,并將結(jié)果數(shù)據(jù)存入用戶自定義緩沖區(qū),最后通知事件分離器讀操作完成萍嬉。
- 事件分離器呼喚處理器乌昔。
- 事件處理器處理用戶自定義緩沖區(qū)中的數(shù)據(jù),然后啟動(dòng)一個(gè)新的異步操作壤追,并將控制權(quán)返回事件分離器磕道。
JAVA NIO
在 JDK 1. 4 中 新 加入 了 NIO( New Input/ Output) 類, 引入了一種基于通道和緩沖區(qū)的 I/O 方式,它可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存行冰,然后通過一個(gè)存儲(chǔ)在 Java 堆的 DirectByteBuffer 對象作為這塊內(nèi)存的引用進(jìn)行操作捅厂,避免了在 Java 堆和 Native 堆中來回復(fù)制數(shù)據(jù)。
要想了解JAVA NIO资柔,首先得掌握以下幾個(gè)關(guān)鍵的概念:Buffer焙贷, Channel, Selector
Buffer
前面提到了贿堰,JAVA NIO是一種基于緩沖區(qū)的I/O 方式辙芍,這是因?yàn)楫?dāng)一個(gè)鏈接建立完成后,IO的數(shù)據(jù)未必會(huì)馬上到達(dá)羹与,當(dāng)數(shù)據(jù)到達(dá)時(shí)故硅,為了不低效的讓線程阻塞等待,可以預(yù)先把數(shù)據(jù)寫入緩沖區(qū),再由緩沖區(qū)交給線程毁兆,因此線程無需阻塞地等待IO派桩。
Channel
通道是 I/O 傳輸發(fā)生時(shí)通過的入口,而緩沖區(qū)是這些數(shù) 據(jù)傳輸?shù)膩碓椿蚰繕?biāo)徘层。
對于離開緩沖區(qū)的傳輸峻呕,您想傳遞出去的數(shù)據(jù)被置于一個(gè)緩沖區(qū),被傳送到通道趣效。對于傳回緩沖區(qū)的傳輸瘦癌,一個(gè)通道將數(shù)據(jù)放置在您所提供的緩沖區(qū)中。
可以理解為在NIO中:如果想將Data發(fā)到目標(biāo)端跷敬,則需要將存儲(chǔ)該Data的Buffer讯私,寫入到目標(biāo)端的Channel中,然后再從Channel中讀取數(shù)據(jù)到目標(biāo)端的Buffer中西傀。
Selector
通道和緩沖區(qū)的機(jī)制斤寇,使得線程無需阻塞地等待IO事件的就緒,但是總是要有人來監(jiān)管這些IO事件拥褂。這個(gè)工作就交給了selector來完成抡驼,這就是所謂的同步。
Selector允許單線程處理多個(gè) Channel肿仑。如果你的應(yīng)用打開了多個(gè)連接(通道)致盟,但每個(gè)連接的流量都很低,使用Selector就會(huì)很方便尤慰。
要使用Selector馏锡,得向Selector注冊Channel,然后調(diào)用它的select()方法伟端。這個(gè)方法會(huì)一直阻塞到某個(gè)注冊的通道有事件就緒杯道,這就是所說的輪詢。一旦這個(gè)方法返回责蝠,線程就可以處理這些事件党巾。
Selector中注冊的感興趣事件有:
- OP_ACCEPT
- OP_CONNEC
- OP_READ
- OP_WRITE
很關(guān)鍵的一點(diǎn)是Selector為前文所述的Reactor模型提供了基礎(chǔ),因此常常會(huì)將Selector優(yōu)化成Reactor模型
NIO&epoll:
可以這么理解NIO是JAVA的IO模型霜医,而epoll是Linux內(nèi)核的IO模型齿拂。它們之間有很深的因緣,因?yàn)閺膶?shí)現(xiàn)方式來看肴敛,它們其實(shí)是很相似的署海,都是基于“通道”和緩沖區(qū)的,也有selector医男,只是在epoll中砸狞,通道實(shí)際上是操作系統(tǒng)的“管道”。和NIO不同的是镀梭,NIO中刀森,解放了線程,但是需要由selector阻塞式地輪詢IO事件的就緒报账;而epoll中研底,IO事件就緒后埠偿,會(huì)自動(dòng)發(fā)送消息,通知selector:“我已經(jīng)就緒了飘哨。”可以認(rèn)為琐凭,Linux的epoll是一種效率更高的NIO芽隆。