柚子快報(bào)邀請(qǐng)碼778899分享:網(wǎng)絡(luò) io,nio,aio總結(jié)
柚子快報(bào)邀請(qǐng)碼778899分享:網(wǎng)絡(luò) io,nio,aio總結(jié)
I/O簡(jiǎn)介
I/O(Input/Outpu) 即輸入/輸出 。
我們先從計(jì)算機(jī)結(jié)構(gòu)的角度來(lái)解讀一下 I/O。
根據(jù)馮.諾依曼結(jié)構(gòu),計(jì)算機(jī)結(jié)構(gòu)分為 5 大部分:運(yùn)算器、控制器、存儲(chǔ)器、輸入設(shè)備、輸出設(shè)備。
輸入設(shè)備(比如鍵盤(pán))和輸出設(shè)備(比如顯示器)都屬于外部設(shè)備。網(wǎng)卡、硬盤(pán)這種既可以屬于輸入設(shè)備,也可以屬于輸出設(shè)備。
輸入設(shè)備向計(jì)算機(jī)輸入數(shù)據(jù),輸出設(shè)備接收計(jì)算機(jī)輸出的數(shù)據(jù)。
從計(jì)算機(jī)結(jié)構(gòu)的視角來(lái)看的話(huà), I/O 描述了計(jì)算機(jī)系統(tǒng)與外部設(shè)備之間通信的過(guò)程。
我們?cè)傧葟膽?yīng)用程序的角度來(lái)解讀一下 I/O。
根據(jù)大學(xué)里學(xué)到的操作系統(tǒng)相關(guān)的知識(shí):為了保證操作系統(tǒng)的穩(wěn)定性和安全性,一個(gè)進(jìn)程的地址空間劃分為 用戶(hù)空間(User space) 和 內(nèi)核空間(Kernel space ) 。
像我們平常運(yùn)行的應(yīng)用程序都是運(yùn)行在用戶(hù)空間,只有內(nèi)核空間才能進(jìn)行系統(tǒng)態(tài)級(jí)別的資源有關(guān)的操作,比如文件管理、進(jìn)程通信、內(nèi)存管理等等。也就是說(shuō),我們想要進(jìn)行 IO 操作,一定是要依賴(lài)內(nèi)核空間的能力。
并且,用戶(hù)空間的程序不能直接訪問(wèn)內(nèi)核空間。
當(dāng)想要執(zhí)行 IO 操作時(shí),由于沒(méi)有執(zhí)行這些操作的權(quán)限,只能發(fā)起系統(tǒng)調(diào)用請(qǐng)求操作系統(tǒng)幫忙完成。
因此,用戶(hù)進(jìn)程想要執(zhí)行 IO 操作的話(huà),必須通過(guò) 系統(tǒng)調(diào)用 來(lái)間接訪問(wèn)內(nèi)核空間
我們?cè)谄匠i_(kāi)發(fā)過(guò)程中接觸最多的就是 磁盤(pán) IO(讀寫(xiě)文件) 和 網(wǎng)絡(luò) IO(網(wǎng)絡(luò)請(qǐng)求和響應(yīng))。
從應(yīng)用程序的視角來(lái)看的話(huà),我們的應(yīng)用程序?qū)Σ僮飨到y(tǒng)的內(nèi)核發(fā)起 IO 調(diào)用(系統(tǒng)調(diào)用),操作系統(tǒng)負(fù)責(zé)的內(nèi)核執(zhí)行具體的 IO 操作。也就是說(shuō),我們的應(yīng)用程序?qū)嶋H上只是發(fā)起了 IO 操作的調(diào)用而已,具體 IO 的執(zhí)行是由操作系統(tǒng)的內(nèi)核來(lái)完成的。
當(dāng)應(yīng)用程序發(fā)起 I/O 調(diào)用后,會(huì)經(jīng)歷兩個(gè)步驟:
內(nèi)核等待 I/O 設(shè)備準(zhǔn)備好數(shù)據(jù)內(nèi)核將數(shù)據(jù)從內(nèi)核空間拷貝到用戶(hù)空間。
同步/異步,阻塞/非阻塞
同步和異步關(guān)注的是消息通信機(jī)制.
同步是指: 發(fā)送方發(fā)出數(shù)據(jù)后, 等待接收方發(fā)回響應(yīng)后才發(fā)下一個(gè)數(shù)據(jù)包的通訊方式. 就是在發(fā)出一個(gè)調(diào)用時(shí), 在沒(méi)有得到結(jié)果之前, 該調(diào)用就不返回, 但是一旦調(diào)用返回, 就得到返回值了. 也就是由"調(diào)用者"主動(dòng)等待這個(gè)"調(diào)用"的結(jié)果.
異步是指: 發(fā)送方發(fā)出數(shù)據(jù)后, 不等待接收方發(fā)回響應(yīng), 接著發(fā)送下個(gè)數(shù)據(jù)包的通訊方式. 當(dāng)一個(gè)異步過(guò)程調(diào)用發(fā)出后, 調(diào)用者不會(huì)立刻得到結(jié)果. 而是在調(diào)用發(fā)出后, "被調(diào)用者"通過(guò)狀態(tài)、通知來(lái)通知調(diào)用者, 或通過(guò)回調(diào)函數(shù)處理這個(gè)調(diào)用.
阻塞和非阻塞屬于進(jìn)程API執(zhí)行動(dòng)作的方式, 關(guān)注的是程序在等待調(diào)用結(jié)果時(shí)的狀態(tài).
阻塞是指: 調(diào)用結(jié)果返回之前, 當(dāng)前線(xiàn)程會(huì)被掛起. 函數(shù)只有在得到結(jié)果之后才會(huì)返回, 線(xiàn)程需要等待結(jié)果.
非阻塞是指: 與阻塞的概念相對(duì)應(yīng), 指在不能立刻得到結(jié)果之前, 該函數(shù)不會(huì)阻塞當(dāng)前線(xiàn)程, 而會(huì)立刻返回. 線(xiàn)程不需要等待結(jié)果.
常見(jiàn)的 IO 模型
UNIX 系統(tǒng)下, IO 模型一共有 5 種: 同步阻塞 I/O、同步非阻塞 I/O、I/O 多路復(fù)用、信號(hào)驅(qū)動(dòng) I/O 和異步 I/O。
這也是我們經(jīng)常提到的 5 種 IO 模型。
java中的bio,nio,aio的簡(jiǎn)介
BIO (Blocking I/O)
BIO 屬于同步阻塞 IO 模型 。
同步阻塞 IO 模型中,應(yīng)用程序發(fā)起 read 調(diào)用后,會(huì)一直阻塞,直到內(nèi)核把數(shù)據(jù)拷貝到用戶(hù)空間。
在客戶(hù)端連接數(shù)量不高的情況下,是沒(méi)問(wèn)題的。但是,當(dāng)面對(duì)十萬(wàn)甚至百萬(wàn)級(jí)連接的時(shí)候,傳統(tǒng)的 BIO 模型是無(wú)能為力的。因此,我們需要一種更高效的 I/O 處理模型來(lái)應(yīng)對(duì)更高的并發(fā)量。
BIO的優(yōu)點(diǎn):
程序簡(jiǎn)單,在阻塞等待數(shù)據(jù)期間,用戶(hù)線(xiàn)程掛起。用戶(hù)線(xiàn)程基本不會(huì)占用 CPU 資源。
BIO的缺點(diǎn):
一般情況下,會(huì)為每個(gè)連接配套一條獨(dú)立的線(xiàn)程,或者說(shuō)一條線(xiàn)程維護(hù)一個(gè)連接成功的IO流的讀寫(xiě)。在并發(fā)量小的情況下,這個(gè)沒(méi)有什么問(wèn)題。但是,當(dāng)在高并發(fā)的場(chǎng)景下,需要大量的線(xiàn)程來(lái)維護(hù)大量的網(wǎng)絡(luò)連接,內(nèi)存、線(xiàn)程切換開(kāi)銷(xiāo)會(huì)非常巨大。因此,基本上,BIO模型在高并發(fā)場(chǎng)景下是不可用的。
NIO (Non-blocking/New I/O)
Java 中的 NIO 于 Java 1.4 中引入,對(duì)應(yīng) java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解為 Non-blocking,不單純是 New。它是支持面向緩沖的,基于通道的 I/O 操作方法。 對(duì)于高負(fù)載、高并發(fā)的(網(wǎng)絡(luò))應(yīng)用,應(yīng)使用 NIO 。流行基于Java NIO通信框架有Mina、Netty、Grizzly等
Java 中的 NIO 可以看作是 I/O 多路復(fù)用模型。也有很多人認(rèn)為,Java 中的 NIO 屬于同步非阻塞 IO 模型。
我們先來(lái)看看 同步非阻塞 IO 模型。
同步非阻塞 IO 模型中,應(yīng)用程序會(huì)一直發(fā)起 read 調(diào)用,等待數(shù)據(jù)從內(nèi)核空間拷貝到用戶(hù)空間的這段時(shí)間里,線(xiàn)程依然是阻塞的,直到在內(nèi)核把數(shù)據(jù)拷貝到用戶(hù)空間。
相比于同步阻塞 IO 模型,同步非阻塞 IO 模型確實(shí)有了很大改進(jìn)。通過(guò)輪詢(xún)操作,避免了一直阻塞。
但是,這種 IO 模型同樣存在問(wèn)題:應(yīng)用程序不斷進(jìn)行 I/O 系統(tǒng)調(diào)用輪詢(xún)數(shù)據(jù)是否已經(jīng)準(zhǔn)備好的過(guò)程是十分消耗 CPU 資源的。
NIO的優(yōu)點(diǎn):每次發(fā)起的 IO 系統(tǒng)調(diào)用,在內(nèi)核的等待數(shù)據(jù)過(guò)程中可以立即返回。用戶(hù)線(xiàn)程不會(huì)阻塞,實(shí)時(shí)性較好。
NIO的缺點(diǎn):需要不斷的重復(fù)發(fā)起IO系統(tǒng)調(diào)用,這種不斷的輪詢(xún),將會(huì)不斷地詢(xún)問(wèn)內(nèi)核,這將占用大量的 CPU 時(shí)間,系統(tǒng)資源利用率較低。
總之,NIO模型在高并發(fā)場(chǎng)景下,也是不可用的。一般 Web 服務(wù)器不使用這種 IO 模型。一般很少直接使用這種模型,而是在其他IO模型中使用非阻塞IO這一特性。java的實(shí)際開(kāi)發(fā)中,也不會(huì)涉及這種IO模型。
再次說(shuō)明,Java NIO(New IO) 不是IO模型中的NIO模型,而是另外的一種模型,叫做IO多路復(fù)用模型( IO multiplexing )。
I/O 多路復(fù)用模型
IO 多路復(fù)用模型中,線(xiàn)程首先發(fā)起 select 調(diào)用,詢(xún)問(wèn)內(nèi)核數(shù)據(jù)是否準(zhǔn)備就緒,等內(nèi)核把數(shù)據(jù)準(zhǔn)備好了,用戶(hù)線(xiàn)程再發(fā)起 read 調(diào)用。read 調(diào)用的過(guò)程(數(shù)據(jù)從內(nèi)核空間 -> 用戶(hù)空間)還是阻塞的。
目前支持 IO 多路復(fù)用的系統(tǒng)調(diào)用,有 select,epoll 等等。select 系統(tǒng)調(diào)用,目前幾乎在所有的操作系統(tǒng)上都有支持。
select 調(diào)用 :內(nèi)核提供的系統(tǒng)調(diào)用,它支持一次查詢(xún)多個(gè)系統(tǒng)調(diào)用的可用狀態(tài)。幾乎所有的操作系統(tǒng)都支持。epoll 調(diào)用 :linux 2.6 內(nèi)核,屬于 select 調(diào)用的增強(qiáng)版本,優(yōu)化了 IO 的執(zhí)行效率。
IO 多路復(fù)用模型,通過(guò)減少無(wú)效的系統(tǒng)調(diào)用,減少了對(duì) CPU 資源的消耗。
Java 中的 NIO ,有一個(gè)非常重要的選擇器 ( Selector ) 的概念,也可以被稱(chēng)為 多路復(fù)用器。通過(guò)它,只需要一個(gè)線(xiàn)程便可以管理多個(gè)客戶(hù)端連接。當(dāng)客戶(hù)端數(shù)據(jù)到了之后,才會(huì)為其服務(wù)。
多路復(fù)用IO的特點(diǎn):
IO多路復(fù)用模型,建立在操作系統(tǒng)kernel內(nèi)核能夠提供的多路分離系統(tǒng)調(diào)用select/epoll基礎(chǔ)之上的。多路復(fù)用IO需要用到兩個(gè)系統(tǒng)調(diào)用(system call), 一個(gè)select/epoll查詢(xún)調(diào)用,一個(gè)是IO的讀取調(diào)用。
和NIO模型相似,多路復(fù)用IO需要輪詢(xún)。負(fù)責(zé)select/epoll查詢(xún)調(diào)用的線(xiàn)程,需要不斷的進(jìn)行select/epoll輪詢(xún),查找出可以進(jìn)行IO操作的連接。
另外,多路復(fù)用IO模型與前面的NIO模型,是有關(guān)系的。對(duì)于每一個(gè)可以查詢(xún)的socket,一般都設(shè)置成為non-blocking模型。只是這一點(diǎn),對(duì)于用戶(hù)程序是透明的(不感知)。
多路復(fù)用IO的優(yōu)點(diǎn):
用select/epoll的優(yōu)勢(shì)在于,它可以同時(shí)處理成千上萬(wàn)個(gè)連接(connection)。與一條線(xiàn)程維護(hù)一個(gè)連接相比,I/O多路復(fù)用技術(shù)的最大優(yōu)勢(shì)是:系統(tǒng)不必創(chuàng)建線(xiàn)程,也不必維護(hù)這些線(xiàn)程,從而大大減小了系統(tǒng)的開(kāi)銷(xiāo)。
Java的NIO(new IO)技術(shù),使用的就是IO多路復(fù)用模型。在linux系統(tǒng)上,使用的是epoll系統(tǒng)調(diào)用。
多路復(fù)用IO的缺點(diǎn):
本質(zhì)上,select/epoll系統(tǒng)調(diào)用,屬于同步IO,也是阻塞IO。都需要在讀寫(xiě)事件就緒后,自己負(fù)責(zé)進(jìn)行讀寫(xiě),也就是說(shuō)這個(gè)讀寫(xiě)過(guò)程是阻塞的。
AIO (Asynchronous I/O)
AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改進(jìn)版 NIO 2,它是異步 IO 模型。
異步 IO 是基于事件和回調(diào)機(jī)制實(shí)現(xiàn)的,也就是應(yīng)用操作之后會(huì)直接返回,不會(huì)堵塞在那里,當(dāng)后臺(tái)處理完成,操作系統(tǒng)會(huì)通知相應(yīng)的線(xiàn)程進(jìn)行后續(xù)的操作。
異步IO模型的特點(diǎn):
在內(nèi)核kernel的等待數(shù)據(jù)和復(fù)制數(shù)據(jù)的兩個(gè)階段,用戶(hù)線(xiàn)程都不是block(阻塞)的。用戶(hù)線(xiàn)程需要接受kernel的IO操作完成的事件,或者說(shuō)注冊(cè)IO操作完成的回調(diào)函數(shù),到操作系統(tǒng)的內(nèi)核。所以說(shuō),異步IO有的時(shí)候,也叫做信號(hào)驅(qū)動(dòng) IO 。
異步IO模型缺點(diǎn):
需要完成事件的注冊(cè)與傳遞,這里邊需要底層操作系統(tǒng)提供大量的支持,去做大量的工作。
目前來(lái)說(shuō), Windows 系統(tǒng)下通過(guò) IOCP 實(shí)現(xiàn)了真正的異步 I/O。但是,就目前的業(yè)界形式來(lái)說(shuō),Windows 系統(tǒng),很少作為百萬(wàn)級(jí)以上或者說(shuō)高并發(fā)應(yīng)用的服務(wù)器操作系統(tǒng)來(lái)使用。
而在 Linux 系統(tǒng)下,異步IO模型在2.6版本才引入,目前并不完善。所以,這也是在 Linux 下,實(shí)現(xiàn)高并發(fā)網(wǎng)絡(luò)編程時(shí)都是以 IO 復(fù)用模型模式為主
目前來(lái)說(shuō) AIO 的應(yīng)用還不是很廣泛。Netty 之前也嘗試使用過(guò) AIO,不過(guò)又放棄了。這是因?yàn)?,Netty 使用了 AIO 之后,在 Linux 系統(tǒng)上的性能并沒(méi)有多少提升。
Java 中的 BIO、NIO、AIO對(duì)比。
select,poll,epoll 對(duì)比
select,poll,epoll都是IO多路復(fù)用的機(jī)制。I/O多路復(fù)用就是通過(guò)一種機(jī)制,一個(gè)進(jìn)程可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(一般是讀就緒或者寫(xiě)就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫(xiě)操作。但select,poll,epoll本質(zhì)上都是同步I/O,因?yàn)樗麄兌夹枰谧x寫(xiě)事件就緒后自己負(fù)責(zé)進(jìn)行讀寫(xiě),也就是說(shuō)這個(gè)讀寫(xiě)過(guò)程是阻塞的,而異步I/O則無(wú)需自己負(fù)責(zé)進(jìn)行讀寫(xiě),異步I/O的實(shí)現(xiàn)會(huì)負(fù)責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶(hù)空間。
select
#include
#include
int select(int maxfd_add_1, fd_set *readset, fd_set *write_set, fd_set * exceptset,
const struct timeval *timeout);
struct timeval{
long tv_sec;
long tv_usec;
};
/*設(shè)置fd_set,下面的四個(gè)函數(shù)都是宏,不能取地址*/
//清空
void FD_ZERO(fd_set *fdset);
//設(shè)置一個(gè)位
void FD_SET(int fd, fd_set *fdset);
//清除一個(gè)位
void FD_CLR(int fd, fd_set *fdset);
/*檢查某個(gè)位是否被設(shè)置,可用于函數(shù)返回時(shí)判斷那個(gè)文件描述符就緒*/
int FD_ISSET(int fd,fd_set *fdset);
maxfd_add_1后面三個(gè)集中設(shè)置了的文件描述符的最大值+1,因?yàn)檫@個(gè)值表示的是個(gè)數(shù)。這個(gè)參數(shù)存在的意義就是內(nèi)核在每次喚醒的時(shí)候需要遍歷的文件描述符個(gè)數(shù),因而不用全部遍歷所有1024個(gè)文件描述符的狀態(tài)
存在這個(gè)參數(shù)的原因純粹是為了效率。因?yàn)閒d_set的最大值典型的是1024.為了避免所有的都被檢查,因此使用這個(gè)。
最大值為宏FD_SETSIZE
readset、write_set、exceptset這三個(gè)fd_set類(lèi)型是一個(gè)位圖。返回的時(shí)候,如果對(duì)應(yīng)的位被設(shè)置,那么表示文件描述符就緒,因此需要用戶(hù)再次手動(dòng)遍歷依次。
select的缺點(diǎn): select支持的文件描述符數(shù)量太小了,默認(rèn)是1024
每次調(diào)用select都需要把fd集合從用戶(hù)態(tài)拷貝到內(nèi)核態(tài),這個(gè)開(kāi)銷(xiāo)在fd很多時(shí)會(huì)很大
每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來(lái)的所有fd,查看有沒(méi)有就緒的fd,這個(gè)開(kāi)銷(xiāo)在fd很多時(shí)也很大,效率隨FD數(shù)目增加而線(xiàn)性下降
poll
#include
int poll(struct pollfd *fdarray, unsigned long n, int timeout);
//成功個(gè)數(shù),錯(cuò)誤-1,超時(shí)0
struct pollfd{
int fd;
short events;
short revents;
};
fdarray是一個(gè)pollfd類(lèi)型的數(shù)組(首指針),n表示這個(gè)數(shù)組的個(gè)數(shù)。pollfd的events成員是要測(cè)試的條件,而revents是內(nèi)核要填充的,表示文件描述符當(dāng)前的讀寫(xiě)狀態(tài)。
poll方法不再使用位圖的方式傳入文件描述符符,而是采用一個(gè)結(jié)構(gòu)體的方式,因而在遍歷每個(gè)文件描述符的poll方法的時(shí)候,就從結(jié)構(gòu)體的數(shù)組中依次遍歷,因此突破了select文件描述符的限制。
poll的缺點(diǎn): 每次調(diào)用select都需要把fd集合從用戶(hù)態(tài)拷貝到內(nèi)核態(tài),這個(gè)開(kāi)銷(xiāo)在fd很多時(shí)會(huì)很大
每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來(lái)的所有fd,查看有沒(méi)有就緒的fd,這個(gè)開(kāi)銷(xiāo)在fd很多時(shí)也很大,效率隨FD數(shù)目增加而線(xiàn)性下降
epoll
源碼
include
// 創(chuàng)建 epollfd 的函數(shù)
int epoll_create(int size);
int epoll_create1(int flags);
// 對(duì)要監(jiān)聽(tīng)的文件描述符的的增加修改刪除
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 開(kāi)始阻塞
int epoll_wait(int epfd, struct epoll_event *events, int maxevents,
int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents,
int timeout, const sigset_t *sigmask);
// epoll_wait 返回的時(shí)候,events表示發(fā)生的、
//時(shí)間 data則表示與文件描述符相關(guān)的信息,可以指向一個(gè)結(jié)構(gòu)體,
//也可以是直接的文件描述符
typedef union epoll_data {
void *ptr; // 常用這個(gè)
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
//感興趣的事件如EPOLLIN,EPOLLOUT,EPOLLPRI等
uint32_t events;
//一般使用data的fd成員表示感興趣的socket
epoll_data_t data;
};
epoll_ctl() 這個(gè)函數(shù)是修改epoll關(guān)注描述符的。
epfd是epoll_create創(chuàng)建的文件描述符op表示要修改的方式:
POLL_CTL_ADD:添加EPOLL_CTL_MOD:修改EPOLL_CTL_DEL:刪除 fd參數(shù)是要修改關(guān)注的文件描述符event則是要關(guān)注的事件了。
epoll_wait() 就是開(kāi)始阻塞的函數(shù)了。
events是已經(jīng)分配好的epoll_event結(jié)構(gòu)體。這個(gè)是一個(gè)空的,內(nèi)核負(fù)責(zé)幫我們填充.events不可以是空指針,內(nèi)核只負(fù)責(zé)把數(shù)據(jù)復(fù)制到這個(gè)events數(shù)組中,不會(huì)去幫助我們?cè)谟脩?hù)態(tài)中分配內(nèi)存 maxevents是傳入要填充的數(shù)組的大小。
為什么高效
每次調(diào)用不需要傳入所有的文件描述符
select/poll每次調(diào)用都要傳遞所要監(jiān)控的所有fd給select/poll系統(tǒng)調(diào)用(這意味著每次調(diào)用都要將fd列表從用戶(hù)態(tài)拷貝到內(nèi)核態(tài),當(dāng)fd數(shù)目很多時(shí),這會(huì)造成低效)。而每次調(diào)用epoll_wait時(shí)(作用相當(dāng)于調(diào)用select/poll),不需要再傳遞fd列表給內(nèi)核,因?yàn)橐呀?jīng)在epoll_ctl中將需要監(jiān)控的fd告訴了內(nèi)核(epoll_ctl不需要每次都拷貝所有的fd,只需要進(jìn)行增量式操作)。所以,在調(diào)用epoll_create之后,內(nèi)核已經(jīng)在內(nèi)核態(tài)開(kāi)始準(zhǔn)備數(shù)據(jù)結(jié)構(gòu)存放要監(jiān)控的fd了。每次epoll_ctl只是對(duì)這個(gè)數(shù)據(jù)結(jié)構(gòu)進(jìn)行簡(jiǎn)單的維護(hù)。
每次epoll_wait()被喚醒,不需要去遍歷所有文件描述符 epoll沒(méi)有文件描述符數(shù)量上的限制。
epoll兩種工作方式LT和ET
水平觸發(fā)(LT)是epoll**缺省的工作方式,**支持阻塞和非阻塞文件描述符。當(dāng)數(shù)據(jù)可讀寫(xiě)的時(shí)候就喚醒epoll_wait,如果不對(duì)這個(gè)文件描述符作任何操作,內(nèi)核還是會(huì)繼續(xù)通知,所以,這種模式編程出錯(cuò)誤可能性要小一點(diǎn)。傳統(tǒng)的select/poll都使用這種模型。
邊緣觸發(fā)(ET)是高速工作方式,只支持非阻塞模式。當(dāng)一個(gè)新的事件到來(lái)時(shí),ET模式下當(dāng)然可以從epoll_wait調(diào)用中獲取到這個(gè)事件,可是如果這次沒(méi)有把這個(gè)事件對(duì)應(yīng)的套接字緩沖區(qū)處理完,在這個(gè)套接字中沒(méi)有新的事件再次到來(lái)時(shí),在ET模式下是無(wú)法再次從epoll_wait調(diào)用中獲取這個(gè)事件的。
LT模式下開(kāi)發(fā)基于epoll的應(yīng)用要簡(jiǎn)單些,不太容易出錯(cuò)。而在ET模式下事件發(fā)生時(shí),如果沒(méi)有徹底地將緩沖區(qū)數(shù)據(jù)處理完,則會(huì)導(dǎo)致緩沖區(qū)中的用戶(hù)請(qǐng)求得不到響應(yīng)。
epoll的跟select和poll對(duì)比的優(yōu)點(diǎn):
支持一個(gè)進(jìn)程打開(kāi)大數(shù)目的socket描述符。它所支持的FD上限是最大可以打開(kāi)文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)遠(yuǎn)于2048,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來(lái)說(shuō)這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。
select和poll每次調(diào)用都需要把所有要監(jiān)聽(tīng)的fd重新拷貝到內(nèi)核空間;epoll只在調(diào)用epoll_ctl時(shí)拷貝一次要監(jiān)聽(tīng)的fd,調(diào)用epoll_wait時(shí)不需要每次把所有要監(jiān)聽(tīng)的fd重復(fù)拷貝到內(nèi)核空間。
IO效率不隨FD數(shù)目增加而線(xiàn)性下降。傳統(tǒng)的select/poll另一個(gè)致命弱點(diǎn)就是當(dāng)你擁有一個(gè)很大的socket集合,任一時(shí)間只有部分的socket是”活躍”的,但是select/poll每次調(diào)用都會(huì)線(xiàn)性?huà)呙枞康募?,?dǎo)致效率呈現(xiàn)線(xiàn)性下降。但是epoll不存在這個(gè)問(wèn)題,它只會(huì)對(duì)”活躍”的socket進(jìn)行操作。這是因?yàn)樵趦?nèi)核實(shí)現(xiàn)中epoll是根據(jù)每個(gè)fd上面的callback函數(shù)實(shí)現(xiàn)的。那么,只有”活躍”的socket才會(huì)主動(dòng)的去調(diào)用callback函數(shù),其他idle狀態(tài)socket則不會(huì)。
select,poll,epoll區(qū)別
零拷貝
零拷貝基本概念:
零拷貝是指計(jì)算機(jī)執(zhí)行IO操作時(shí),CPU不需要將數(shù)據(jù)從一個(gè)存儲(chǔ)區(qū)域復(fù)制到另一個(gè)存儲(chǔ)區(qū)域,進(jìn)而減少上下文切換以及CPU的拷貝時(shí)間。它是一種IO操作優(yōu)化技術(shù)。
nio中epoll零拷貝原理解析:
mmap 通過(guò)內(nèi)存映射,將文件映射到 內(nèi)核緩沖區(qū),同時(shí),用戶(hù)空間可以共享內(nèi)核空間的數(shù)據(jù)。這樣,在進(jìn)行網(wǎng)絡(luò)傳輸時(shí),就可以減少內(nèi)核空間到用戶(hù)控件的拷貝次數(shù)。
零拷貝代碼演示:
/**
* 零拷貝代碼演示
*/
private static void zeroCopyDemo() throws IOException {
// 構(gòu)建零拷貝的輸入輸出
FileChannel inChannel = FileChannel.open(Paths.get("C:\\Users\\yojofly\\Desktop\\sps-plugins\\learn\\test.csv"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("C:\\Users\\yojofly\\Desktop\\sps-plugins\\learn\\test_cp.csv"),StandardOpenOption.READ,StandardOpenOption.CREATE,StandardOpenOption.WRITE);
// 緩沖區(qū)初始化,可對(duì)比為文件Io讀取時(shí)候的 new byte[1024]
MappedByteBuffer inMapperBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMapperBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
// 獲取輸入文件的字節(jié)長(zhǎng)度limit
byte[] bytes = new byte[inMapperBuffer.limit()];
// 文件讀入緩存中
inMapperBuffer.get(bytes);
// 從緩存中取出文件內(nèi)容寫(xiě)入到新的文件
outMapperBuffer.put(bytes);
inChannel.close();
outChannel.close();
}
nio中channel,buffer,selector
Channel
Channel是數(shù)據(jù)傳輸?shù)碾p向通道,可以從 channel 將數(shù)據(jù)讀入 buffer,也可以將 buffer 的數(shù)據(jù)寫(xiě)入 channel。 常見(jiàn)的Channel有:
FileChannel 文件傳輸 DatagramChannel(通常用于UDP) SocketChannel(通常用于TCP) ServerSocketChannel(通常用于TCP)
Buffer
內(nèi)存緩存區(qū),用于暫存從Channel中讀取的數(shù)據(jù)、以及即將寫(xiě)入Channel中的數(shù)據(jù)。
常見(jiàn)的實(shí)現(xiàn)類(lèi)Buffer:
ByteBuffer 字節(jié)
MappedByteBuffer DirectByteBuffer HeapByteBuffer ShortBuffer IntBuffer LongBuffer FloatBuffer DoubleBuffer CharBuffer 從名字上可以看出,這些Buffer支持的是不同數(shù)據(jù)類(lèi)型的緩沖區(qū),如short、int等。
常用方法:
put: 寫(xiě)入數(shù)據(jù)
get:讀取數(shù)據(jù)
flip:將Buffer從寫(xiě)模式切換到讀模式。調(diào)用flip()方法會(huì)將position設(shè)回0,并將limit設(shè)置成之前position的值
clear:方法,position將被設(shè)回0,limit被設(shè)置成 capacity的值
compact:方法將所有未讀的數(shù)據(jù)拷貝到Buffer起始處。然后將position設(shè)到最后一個(gè)未讀元素正后面
mark :添加標(biāo)記 reset:恢復(fù)到標(biāo)記位置
通過(guò)調(diào)用Buffer.mark()方法,可以標(biāo)記Buffer中的一個(gè)特定position。之后可以通過(guò)調(diào)用Buffer.reset()方法恢復(fù)到這個(gè)position。
內(nèi)部結(jié)構(gòu)
ByteBuffer的內(nèi)部結(jié)構(gòu)類(lèi)似于一個(gè)數(shù)組,主要有3個(gè)重要屬性:
capacity:代表buffer的容量。
position:一個(gè)讀寫(xiě)指針,可以理解為讀寫(xiě)到buffer中的哪個(gè)下標(biāo)。
limit:讀寫(xiě)限制,position不能超過(guò)limit。
當(dāng)我們新建一個(gè)buffer時(shí),它的內(nèi)部結(jié)構(gòu)如下:
寫(xiě)模式下,position 是寫(xiě)入位置,limit 等于容量,下圖表示寫(xiě)入了 4 個(gè)字節(jié)后的狀態(tài):
flip 動(dòng)作發(fā)生后,position 切換為讀取位置,limit 切換為讀取限制:
讀取 4 個(gè)字節(jié)后,狀態(tài)變成:
clear 動(dòng)作發(fā)生后,狀態(tài)變成:
compact 方法,是把未讀完的部分向前壓縮,然后切換至寫(xiě)模式
Selector
在NIO出現(xiàn)之前,在開(kāi)發(fā)服務(wù)器端程序時(shí),會(huì)采用一種叫做“多線(xiàn)程版設(shè)計(jì)”的思路,即服務(wù)器端每當(dāng)需要和一個(gè)新的客戶(hù)端進(jìn)行通信時(shí),就會(huì)開(kāi)啟一個(gè)新的Socket連接(通過(guò)Socket就能讀寫(xiě)數(shù)據(jù)了),并啟動(dòng)一個(gè)新的線(xiàn)程來(lái)為這個(gè)Socket服務(wù)(執(zhí)行讀寫(xiě)操作等)。
這種思路的弊端在于,如果客戶(hù)端太多的話(huà),那么服務(wù)器端就需要開(kāi)啟許多線(xiàn)程才能進(jìn)行服務(wù),這會(huì)帶來(lái)以下問(wèn)題:
內(nèi)存占用高。 線(xiàn)程上下文切換成本高。 只適合連接數(shù)少的場(chǎng)景。 注意:線(xiàn)程并不是越多越好,因?yàn)镃PU能并行處理的線(xiàn)程是有限的,一旦線(xiàn)程數(shù)量超出CPU能并行處理的數(shù)量,多出來(lái)的線(xiàn)程就需要等待CPU,而這就會(huì)導(dǎo)致線(xiàn)程上下文切換,從而帶來(lái)額外開(kāi)銷(xiāo)
SelectionKey的四個(gè)常量
SelectionKey.OP_CONNECTSelectionKey.OP_ACCEPTSelectionKey.OP_READSelectionKey.OP_WRITE
分別表示監(jiān)聽(tīng)四種不同類(lèi)型的事件:
ConnectAcceptReadWrite
selector常用方法:
open: 創(chuàng)建一個(gè)selector連接
select():阻塞到至少有一個(gè)通道在你注冊(cè)的事件上就緒了。
select(long timeout):select()一樣,除了最長(zhǎng)會(huì)阻塞timeout毫秒(參數(shù))。
selectNow():不會(huì)阻塞,不管什么通道就緒都立刻返回(此方法執(zhí)行非阻塞的選擇操作。如果自從前一次選擇操作后,沒(méi)有通道變成可選擇的,則此方法直接返回零)。
selectedKeys():返回所有selector里注冊(cè)的SelectionKey
代碼示例:模擬Nio中網(wǎng)絡(luò)中的客戶(hù)端和服務(wù)端通信
SelectorClientDemo.java
package com.yojofly.server.learn.nio.selector;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class SelectorClientDemo {
static Selector selector;
public static void main(String[] args) throws IOException {
selector = Selector.open();
SocketChannel socketChannel = SocketChannel.open();
// 設(shè)置為非阻塞io
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("localhost",8000));
socketChannel.register(selector, SelectionKey.OP_CONNECT);
while (true){
selector.select(); //阻塞
Set
Iterator
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isConnectable()){ // 連接事件
// 處理連接事件
handleConnect(selectionKey);
}else if (selectionKey.isReadable()){ // 讀事件
// 處理讀事件
handleRead(selectionKey);
}
}
}
}
private static void handleConnect(SelectionKey selectionKey) throws IOException{
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 連接中斷
if (socketChannel.isConnectionPending()){
socketChannel.finishConnect();
}
// 設(shè)置io非阻塞狀態(tài)
socketChannel.configureBlocking(false);
socketChannel.write(ByteBuffer.wrap("hello server,i am nio client with accept".getBytes()));
socketChannel.register(selector,SelectionKey.OP_READ);
}
private static void handleRead(SelectionKey selectionKey) throws IOException{
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
socketChannel.read(byteBuffer);
System.out.println("client receive Msg:" +new String(byteBuffer.array()));
}
}
SelectorServerDemo.java
package com.yojofly.server.learn.nio.selector;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class SelectorServerDemo {
static Selector selector;
public static void main(String[] args) throws IOException {
selector = Selector.open();
ServerSocketChannel socketChannel = ServerSocketChannel.open();
// 設(shè)置為非阻塞io
socketChannel.configureBlocking(false);
socketChannel.socket().bind(new InetSocketAddress(8000));
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
selector.select(); //阻塞
Set
Iterator
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isAcceptable()){ // 連接事件
// 處理連接事件
handleConnect(selectionKey);
}else if (selectionKey.isReadable()){ // 讀事件
// 處理讀事件
handleRead(selectionKey);
}
}
}
}
private static void handleConnect(SelectionKey selectionKey) throws IOException{
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.write(ByteBuffer.wrap("hello client,i am nio server with connect".getBytes()));
socketChannel.register(selector,SelectionKey.OP_READ);
}
private static void handleRead(SelectionKey selectionKey) throws IOException{
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
socketChannel.read(byteBuffer);
System.out.println("server receive Msg:" +new String(byteBuffer.array()));
}
}
文章參考:
同步異步,阻塞非阻塞 https://blog.csdn.net/qq_39515350/article/details/120854214 io多路復(fù)用中詳解: https://blog.csdn.net/adminpd/article/details/124553590 io基礎(chǔ)知識(shí)原理分析: https://www.cnblogs.com/crazymakercircle/p/10225159.html select,poll.epoll對(duì)比 https://blog.csdn.net/xx_yTm/article/details/54801977 select,poll,epoll源碼分析 http://t.zoukankan.com/perfy576-p-8554734.html nio中channel,buffer,selector的作用: https://blog.csdn.net/lucas161543228/article/details/125172566
柚子快報(bào)邀請(qǐng)碼778899分享:網(wǎng)絡(luò) io,nio,aio總結(jié)
精彩內(nèi)容
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。
轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。