微號(hào):moon聊技術(shù)
關(guān)注選擇“ 星標(biāo) ”, 重磅干貨,第一 時(shí)間送達(dá)!
[如果你覺得文章對(duì)你有幫助,歡迎關(guān)注,在看,點(diǎn)贊,轉(zhuǎn)發(fā)]
[TOC]
前言
大家好,我是 moon,上一次和大家聊了一下 socket(這次 moon 要把 socket 玩的明明白白),相信大家對(duì) socket 有了一定的認(rèn)識(shí),對(duì)于 socket 還不熟悉的同學(xué),可以先看看 socket 這篇文章,今天這篇文章是基于 socket,再和大家講一講網(wǎng)絡(luò)I/O相關(guān)的知識(shí),也剛好為后續(xù) netty 的文章做下鋪墊
什么是 I/O ?
I:其實(shí)就是 Input,輸入
O:其實(shí)就是 Output,輸出
所以 I/O 很好理解,就是輸入和輸出
生活中最簡(jiǎn)單的例子,你用微信和別人聊天,你發(fā)送信息給對(duì)方,就是輸入,對(duì)方回給你信息,你接受到了,就是輸出。
一般情況下,在軟件中我們常說的 I/O 是指網(wǎng)絡(luò) I/O 和磁盤 I/O,今天我們就來聊下網(wǎng)絡(luò) I/O
網(wǎng)絡(luò) I/O 又是什么?
其實(shí)網(wǎng)絡(luò) I/O 就是網(wǎng)絡(luò)中的輸入與輸出,我們?cè)僬f詳細(xì)點(diǎn),正常的網(wǎng)絡(luò)通信中,一條消息發(fā)送的過程中有一個(gè)很重要的媒介,叫做網(wǎng)卡,它的作用有兩個(gè)
- 一是將電腦的數(shù)據(jù)封裝為幀,并通過網(wǎng)線(對(duì)無線網(wǎng)絡(luò)來說就是電磁波)將數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)上去
- 二是接收網(wǎng)絡(luò)上其它設(shè)備傳過來的幀,并將幀重新組合成數(shù)據(jù),發(fā)送到所在的電腦中。

網(wǎng)卡能接收所有在網(wǎng)絡(luò)上傳輸?shù)男盘?hào),但正常情況下只接受發(fā)送到該電腦的幀和廣播幀,將其余的幀丟棄。
所以網(wǎng)絡(luò) I/O 其實(shí)網(wǎng)絡(luò)與服務(wù)端(電腦內(nèi)存)之間的輸入與輸出
為什么會(huì)有網(wǎng)絡(luò) I/O 模型?
一般情況下,一次網(wǎng)絡(luò)數(shù)據(jù)的傳輸會(huì)從客戶端發(fā)送給服務(wù)端,由服務(wù)端網(wǎng)卡接受,轉(zhuǎn)交給內(nèi)存,最后由 cpu 執(zhí)行相應(yīng)的業(yè)務(wù)操作
只要有一點(diǎn)電腦知識(shí)的讀者大多數(shù)都知道,cpu、顯卡、內(nèi)存等電腦中你數(shù)得上名字的模塊,運(yùn)行效率最高的就是 cpu 了,所以為了整個(gè)網(wǎng)絡(luò)傳輸?shù)奶嵝?,就誕生出了五種網(wǎng)絡(luò) I/O 模型
當(dāng)然還有一點(diǎn)原因

不管是客戶端還是服務(wù)端,在發(fā)送和接受數(shù)據(jù)的時(shí)候,都是要與 socket 緩沖區(qū)去進(jìn)行交互的,那么什么時(shí)候交互?是否需要等待響應(yīng)?基于這些不同的業(yè)務(wù)場(chǎng)景,也就有了五種網(wǎng)絡(luò) I/O 模型
其實(shí)這個(gè)結(jié)論是通用的,任何模型的調(diào)優(yōu)改造健全一般都是圍繞著性能、安全、業(yè)務(wù)場(chǎng)景這三個(gè)方向來前進(jìn)的
I/O 模型

好了,我們說完了 I/O 模型的誕生,就來具體的研究下具體是哪五種 I/O 模型
阻塞 I/O

阻塞 I/O,顧名思義,就是在各個(gè)狀態(tài)完全阻塞住整個(gè) I/O 過程是個(gè)串行化的,在數(shù)據(jù)傳輸開始時(shí),都要等到后續(xù)的每一個(gè)操作完成后才可以繼續(xù),這也是 linux 系統(tǒng)默認(rèn)的 I/O 模型,當(dāng)然,這種模型的問題就是效率非常的低
第二個(gè)問題就是,如果是在生產(chǎn)環(huán)境多線程的情況下,會(huì)有頻繁線程的上下文切換,而這個(gè)切換又是非常消耗資源的。
非阻塞 I/O
非阻塞 I/O 針對(duì)于阻塞 I/O 有了一些改進(jìn)

在系統(tǒng)調(diào)用內(nèi)核后,內(nèi)核如果沒有準(zhǔn)備好數(shù)據(jù),則直接會(huì)返回一個(gè)錯(cuò)誤碼,直到數(shù)據(jù)準(zhǔn)備好后,再走后續(xù)的流程
中間會(huì)有一個(gè)一直定時(shí)去詢問的動(dòng)作,所以它的缺點(diǎn)也就是在這里,需要一直去處理數(shù)據(jù)沒有準(zhǔn)備好的情況,也無法判斷該數(shù)據(jù)多久會(huì)準(zhǔn)備好。但是在并發(fā)的情況下相比阻塞 I/O 來說它的性能會(huì)好很多
異步 I/O
其實(shí)通過阻塞或者非阻塞 I/O,你能發(fā)現(xiàn)一個(gè)問題,其實(shí)拷貝數(shù)據(jù)都是由內(nèi)核完成的,那又何必讓應(yīng)用進(jìn)程去觸發(fā)拷貝這個(gè)操作呢,由內(nèi)核直接完成不就好了?
所以就出現(xiàn)了異步 I/O 模型

比如有一個(gè)讀取數(shù)據(jù)的請(qǐng)求,應(yīng)用只需要向內(nèi)核發(fā)送一個(gè) read,告訴內(nèi)核它要讀取數(shù)據(jù)后即刻返回。
之后內(nèi)核收到請(qǐng)求并且建立一個(gè)信號(hào)連接,當(dāng)數(shù)據(jù)準(zhǔn)備就緒,內(nèi)核會(huì)主動(dòng)把數(shù)據(jù)從內(nèi)核復(fù)制到用戶空間,等所有操作都完成之后,內(nèi)核再發(fā)起一個(gè)通知告訴應(yīng)用,完成了所有操作,應(yīng)用程序可以讀取數(shù)據(jù)了,這就是異步 I/O 模型。
異步 IO 的好處是將發(fā)送詢問請(qǐng)求、發(fā)送接收數(shù)據(jù)請(qǐng)求兩個(gè)請(qǐng)求合并為一次請(qǐng)求就可以完成狀態(tài)詢問和數(shù)拷貝的所有操作,并且無阻塞,內(nèi)核準(zhǔn)備好數(shù)據(jù)后直接通知。
信號(hào)驅(qū)動(dòng)模型

信號(hào)驅(qū)動(dòng)就是進(jìn)程發(fā)起一個(gè) IO 操作,會(huì)向內(nèi)核注冊(cè)一個(gè)信號(hào)處理程序,然后立即返回
當(dāng)內(nèi)核將數(shù)據(jù)報(bào)準(zhǔn)備好后會(huì)發(fā)送一個(gè)信號(hào)給進(jìn)程,這時(shí)候進(jìn)程便可以在信號(hào)處理程序中調(diào)用 IO 處理數(shù)據(jù)報(bào)。
I/O 多路復(fù)用

I/O 多路復(fù)用解決的問題就是單線程怎么管理多個(gè) socket 連接,內(nèi)核負(fù)責(zé)輪詢所有 socket,一個(gè)進(jìn)程可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫操作。
其主要有 select、poll、epoll 三種多路復(fù)用的網(wǎng)絡(luò)I/O模型。
select
select 的設(shè)計(jì)思路是喚醒模式,通過一個(gè) socket 列表維護(hù)所有的 socket,socket 對(duì)應(yīng)文件列表中的 fd,select 會(huì)默認(rèn)限制最大文件句柄數(shù)為 1024,間接控制 fd[] 最大為 1024。
- 如果列表中的 Socket 都沒有數(shù)據(jù),就掛起進(jìn)程。
- 如果有一個(gè) Socket 收到數(shù)據(jù),就喚醒進(jìn)程,將該線程從等待隊(duì)列中移除,加入到工作隊(duì)列,然后準(zhǔn)備執(zhí)行 socket 任務(wù)。

那么有個(gè)問題來了,如果有多個(gè) socket 任務(wù)同時(shí)喚醒怎么辦,也就是說說有多個(gè) socket 任務(wù)同時(shí)進(jìn)來,那到底執(zhí)行哪個(gè) socket 任務(wù)?
所以,當(dāng)進(jìn)程被喚醒后,至少有一個(gè) socket 是有數(shù)據(jù)的,這時(shí)候只需要遍歷一遍 socket 列表就知道了此次需要執(zhí)行哪些 socket 了。
缺點(diǎn):
每次 select 都需要將進(jìn)程加入到監(jiān)視 socket 的等待隊(duì)列,每次喚醒都要將進(jìn)程從 socket 待隊(duì)列移除。這里涉及兩次遍歷操作,而且每次都要將 FDS 列表傳遞給內(nèi)核,有一定的開銷。
進(jìn)程被喚醒后,只能知道有 socket 接收到了數(shù)據(jù),無法知道具體是哪一個(gè) socket 接收到了數(shù)據(jù),所以需要用戶進(jìn)程進(jìn)行遍歷,才能知道具體是哪個(gè) socket 接收到了數(shù)據(jù)。
poll
poll 其實(shí)內(nèi)部實(shí)現(xiàn)基本跟 select 一樣,區(qū)別在于它們底層組織 fd[] 的數(shù)據(jù)結(jié)構(gòu)不太一樣,從而實(shí)現(xiàn)了 poll 的最大文件句柄數(shù)量限制去除了
epoll
我們前面說到 select 是需要遍歷來解決查詢就緒 socket 的,效率很低,epoll 就對(duì)此做了改進(jìn)

- 1.拆分:epoll 將添加等待隊(duì)列和阻塞進(jìn)程拆分成兩個(gè)獨(dú)立的操作,不用每次都去重新維護(hù)等待隊(duì)列
- 先用 epoll_ctl 維護(hù)等待隊(duì)列 eventpoll,它通過紅黑樹存儲(chǔ) socket 對(duì)象,實(shí)現(xiàn)高效的查找,刪除和添加。
- 再調(diào)用 epoll_wait 阻塞進(jìn)程,底層是一個(gè)雙向鏈表。顯而易見地,效率就能得到提升。
select 的添加等待隊(duì)列和阻塞進(jìn)程是合并在一起的,每次調(diào)用select()操作時(shí)都得執(zhí)行一遍這兩個(gè)操作,從而導(dǎo)致每次都要將fd[]傳遞到內(nèi)核空間,并且遍歷fd[]的每個(gè)fd的等待隊(duì)列,將進(jìn)程放入各個(gè)fd的等待隊(duì)列中。
- 2.直接返回有數(shù)據(jù)的 fd[]:select 進(jìn)程被喚醒后,是需要遍歷一遍 socket 列表,手動(dòng)獲取有數(shù)據(jù)的 socket,而 epoll 是在喚醒時(shí)直接把有數(shù)據(jù)的 socket 返回給進(jìn)程,不需要自己去進(jìn)行遍歷查詢。
直接返回有數(shù)據(jù)的 socket 是怎么實(shí)現(xiàn)的?
其實(shí)就是 epoll 會(huì)先注冊(cè)一個(gè)文件描述符,一旦基于某個(gè)文件描述符就緒時(shí),內(nèi)核會(huì)采用類似 callback 的回調(diào)機(jī)制,迅速激活這個(gè)文件描述符,當(dāng)進(jìn)程調(diào)用 epoll_wait() 時(shí)便得到通知。
epoll對(duì)文件描述符的操作有兩種模式:LT(level trigger)和 ET(edge trigger)默認(rèn)為 LT :
- LT模式:當(dāng)epoll_wait檢測(cè)到描述符事件發(fā)生并將此事件通知應(yīng)用程序,應(yīng)用程序可以不立即處理該事件。下次調(diào)用epoll_wait時(shí),會(huì)再次響應(yīng)應(yīng)用程序并通知此事件。
- ET模式:當(dāng)epoll_wait檢測(cè)到描述符事件發(fā)生并將此事件通知應(yīng)用程序,應(yīng)用程序必須立即處理該事件。如果不處理,下次調(diào)用epoll_wait時(shí),不會(huì)再次響應(yīng)應(yīng)用程序并通知此事件。
結(jié)語
I/O 模型這塊兒可是重中之重,作為網(wǎng)絡(luò)通信的核心,只要你想去大廠一定會(huì)被問到,今天這篇文章應(yīng)該幫你梳理清楚了各個(gè)網(wǎng)絡(luò) I/O 模型之間的關(guān)系以及優(yōu)缺點(diǎn),讓你對(duì)網(wǎng)絡(luò) I/O 模型有了更進(jìn)一步的了解,下次我們準(zhǔn)備開始突擊 netty ~
在這里,送大家一份自己整理的面試資料,絕不是在網(wǎng)上那種打包下載的,而是自己需要學(xué)到某個(gè)方向知識(shí)的時(shí)候,需要看了,去網(wǎng)上挨個(gè)找的,最后匯總而成。這部分我是會(huì)不斷把它完善的,當(dāng)成自己的小電子書庫,不多,但貴在精。

關(guān)注企鵝公號(hào): moon聊技術(shù) ,回復(fù) 面試資料 立刻白嫖!