系列文章目錄和關(guān)于我
零丶背景
最近有很多想學(xué)的,像netty的使用、原理源碼,但是苦于自己對(duì)于操作系統(tǒng)和nio了解不多,有點(diǎn)無從下手,遂學(xué)習(xí)之。
一丶網(wǎng)絡(luò)io的過程
上圖粗略描述了網(wǎng)絡(luò)io的過程,了解其中的拷貝過程有利于我們理解非阻塞io,以及IO多路復(fù)用的必要性。
-
數(shù)據(jù)從網(wǎng)卡到內(nèi)核緩沖區(qū)
網(wǎng)卡通過DMA的方式將網(wǎng)絡(luò)幀copy到內(nèi)核空間并不是拷貝到內(nèi)核空間就完事了,因?yàn)檫€需要根據(jù)協(xié)議對(duì)數(shù)據(jù)進(jìn)行處理。
所以網(wǎng)卡使用硬中斷通知cpu,cpu響應(yīng)后會(huì)使用網(wǎng)卡注冊(cè)函數(shù)進(jìn)行收包,然后協(xié)議層處理網(wǎng)絡(luò)幀。
-
數(shù)據(jù)從內(nèi)核緩沖區(qū)到用戶空間
根據(jù)協(xié)議處理好的數(shù)據(jù),還需要拷貝到用戶空間才能被運(yùn)行在內(nèi)核態(tài)的應(yīng)用程序使用==>cpu進(jìn)行數(shù)據(jù)拷貝。隨后內(nèi)核喚醒用戶進(jìn)程,相當(dāng)于我們的java程序從阻塞io中被喚醒,繼續(xù)執(zhí)行下一行代碼的執(zhí)行。
二丶Socket通信過程與其中的阻塞點(diǎn)
這其中有幾個(gè)阻塞的過程
-
accept 系統(tǒng)調(diào)用:等待客戶端建立tcp連接
這個(gè)問題不大,沒有連接那么阻塞服務(wù)端線程,可以節(jié)約cpu資源。
-
read系統(tǒng)調(diào)用:等待請(qǐng)求數(shù)據(jù)來到用戶空間
數(shù)據(jù)從網(wǎng)卡到用戶空間的過程,線程時(shí)阻塞的
-
Servlet#service 處理請(qǐng)求是一個(gè)同步過程
tomcat根據(jù)http協(xié)議構(gòu)造request,并和response作為參數(shù),找到對(duì)應(yīng)Servlet調(diào)用service方法,Servlet#service方法執(zhí)行結(jié)束,返回內(nèi)容才能通過write系統(tǒng)調(diào)用回應(yīng)數(shù)據(jù)。
這導(dǎo)致在業(yè)務(wù)處理上需要使用線程池來讓服務(wù)端可以處理多個(gè)并發(fā)請(qǐng)求。
-
write系統(tǒng)調(diào)用:響應(yīng)數(shù)據(jù)寫回
write系統(tǒng)調(diào)用將servlet處理后的響應(yīng)數(shù)據(jù),寫回到文件描述符中。
三丶NIO解決了什么問題
1.單線程監(jiān)測(cè)若干個(gè)文件描述符是否可以執(zhí)行IO操作
這就是常說的IO多路復(fù)用,那為什么需要IO多路復(fù)用?
盡量使用較少的系統(tǒng)資源處理更多的連接,如果當(dāng)前單臺(tái)服務(wù)器接收了1w個(gè)請(qǐng)求,服務(wù)端當(dāng)如何處理?
1.1 傳統(tǒng)BIO模型
上面是一段java BIO模型并發(fā)處理多請(qǐng)求的實(shí)例代碼,它有以下不足
- 大量的線程占用很大的內(nèi)存空間
- 線程切換會(huì)帶來很大的開銷
- process方法中需要需要調(diào)用read系統(tǒng)調(diào)用,阻塞直到可讀,并沒有真正進(jìn)行讀寫操作。
1.2. 非阻塞IO
上面是非阻塞IO的一個(gè)實(shí)例
socketChannel.configureBlocking(false)
可以讓后續(xù)的read在通道數(shù)據(jù)沒有就緒的時(shí)候直接返回-1,而不是讓線程阻塞。這個(gè)特性讓調(diào)度線程池中的線程減少了阻塞,從而節(jié)省了線程資源。
但是這種方式也不是沒有任何缺點(diǎn),多次系統(tǒng)意味著多次系統(tǒng)調(diào)用,每次系統(tǒng)調(diào)用都需要,用戶態(tài)<=>內(nèi)核態(tài)的來回切換,需要cpu保存進(jìn)程的上下文,調(diào)用結(jié)束還需要恢復(fù)進(jìn)程的上下文。
1.3 IO多路復(fù)用
如上是Java IO多路復(fù)用的簡(jiǎn)陋例子。操作系統(tǒng)提供了多路復(fù)用的機(jī)制,將連接上來的客戶端都進(jìn)行注冊(cè),然后不斷循環(huán)掃描各個(gè)客戶端連接,監(jiān)聽客戶端的請(qǐng)求。但是,多路復(fù)用輪詢掃描各個(gè)客戶端連接的過程在操作系統(tǒng)內(nèi)核中進(jìn)行
,極大的加快了多路復(fù)用的效率,減少了用戶態(tài)和內(nèi)核態(tài)的切換
。
2.減少堆內(nèi)內(nèi)存<=>堆外內(nèi)存的拷貝開銷
使用NIO Channel讀寫時(shí)需要需要先讀到堆外內(nèi)存,然后拷貝到堆內(nèi)內(nèi)存,如果直接使用堆外內(nèi)存則可以減少堆外到堆內(nèi)的拷貝過程。
下圖是將Channel數(shù)據(jù)讀取到Buffer,調(diào)用IOUtil#read的源碼
下圖是將Buffer數(shù)據(jù)寫入到Channel,調(diào)用IOUtil#write的源碼
2.1 為什么需要再堆外內(nèi)存和堆內(nèi)內(nèi)存來回捯飭?
寫入Buffer數(shù)據(jù)到文件描述符,or讀取文件描述符數(shù)據(jù)到Buffer都是需要進(jìn)行系統(tǒng)調(diào)用的,執(zhí)行系統(tǒng)調(diào)用依賴于執(zhí)行native方法,而執(zhí)行native方法的線程被認(rèn)為是處于SafePoint,處于SafePoint就有可能發(fā)生 GC 重排列對(duì)象內(nèi)存的情況。
并且這個(gè)寫入和讀取是針對(duì)地址的(如下圖,最終的native調(diào)用需要傳入地址)如果寫入或者讀取buffer由于gc移動(dòng),那么地址會(huì)改變,但是native方法調(diào)用可不管這個(gè),就導(dǎo)致讀寫出現(xiàn)錯(cuò)誤。因此需要依賴于堆外內(nèi)存。
2.2 為什么Socket基于Inpustream,OutputStream沒有這個(gè)問題
以SokcetInputStream的讀為例,讀最終調(diào)用socktRead0這個(gè)native方法,入?yún)d是當(dāng)前Socket對(duì)應(yīng)的文件描述符,byte數(shù)組就是數(shù)據(jù)最終讀入的目的地。
下圖是native 方法socketRead0的實(shí)現(xiàn)
可以看到,其實(shí)是先將socket fd內(nèi)容讀取到c語(yǔ)言聲明的數(shù)組,然后拷貝到Java byte[],這個(gè)c語(yǔ)言聲明的數(shù)組其實(shí)作用類似于直接內(nèi)存!
3.減少內(nèi)核空間和用戶空間的拷貝開銷
上面說了直接內(nèi)存的作用:減少堆外堆內(nèi)的拷貝開銷。無論堆外堆內(nèi),都是用戶空間的拷貝。
3.1 DMA控制器替CPU打工
上圖是讀取磁盤文件的時(shí)序圖,可以看到如果沒有DMA技術(shù),藍(lán)色部分需要CPU來完成,將浪費(fèi)寶貴的資源。
再DMA讀取到足夠數(shù)據(jù)后,會(huì)發(fā)送中斷信號(hào)給CPU,讓CPU將內(nèi)核緩沖區(qū)數(shù)據(jù),拷貝到用戶緩沖區(qū),隨后CPU再來調(diào)度Java程序,Java程序才能操作到用戶緩沖區(qū)的數(shù)據(jù)。
3.2 零拷貝
3.2.1 傳統(tǒng)文件傳輸
如下圖是我們使用IO流,讀取磁盤文件,通過Socket API 發(fā)送的流程,其中需要read,和 write 系統(tǒng)調(diào)用,每次系統(tǒng)調(diào)用都意味著用戶態(tài)與內(nèi)核態(tài)的上下文切換。
并且還有四次數(shù)據(jù)拷貝,其中兩次由DMA負(fù)責(zé)打工,兩次由CPU負(fù)責(zé)拷貝。
如何優(yōu)化:
- 如果Java程序不需要對(duì)磁盤數(shù)據(jù)內(nèi)容進(jìn)行再加工(業(yè)務(wù)操作)那么不需要拷貝到用戶空間,從而減少拷貝次數(shù)
- 由于用戶空間沒有操作網(wǎng)卡和磁盤的權(quán)限,操作這些設(shè)備需要由操作系統(tǒng)內(nèi)核完成,那么如果操作系統(tǒng)提供新的系統(tǒng)調(diào)用函數(shù),豈不是就可以減少用戶態(tài)與內(nèi)核態(tài)的上下文切換
3.2.2 mmap + write
- 應(yīng)用進(jìn)程調(diào)用了
mmap()
后,DMA 會(huì)把磁盤的數(shù)據(jù)拷貝到內(nèi)核的緩沖區(qū)里。接著,應(yīng)用進(jìn)程跟操作系統(tǒng)內(nèi)核共享這個(gè)緩沖區(qū); - 應(yīng)用進(jìn)程再調(diào)用
write()
,操作系統(tǒng)直接將內(nèi)核緩沖區(qū)的數(shù)據(jù)拷貝到 socket 緩沖區(qū)中,這一切都發(fā)生在內(nèi)核態(tài),由 CPU 來搬運(yùn)數(shù)據(jù); - 最后,把內(nèi)核的 socket 緩沖區(qū)里的數(shù)據(jù),拷貝到網(wǎng)卡的緩沖區(qū)里,這個(gè)過程是由 DMA 搬運(yùn)的
所以mmap優(yōu)化了什么?
mmap并沒有減少系統(tǒng)調(diào)用帶來的內(nèi)核態(tài)用戶態(tài)切換開銷,只是應(yīng)用程序和內(nèi)核共享緩沖區(qū),從而讓cpu可以直接將內(nèi)核緩沖區(qū)的數(shù)據(jù),拷貝到socket緩沖區(qū),不需要拷貝到用戶緩沖區(qū),再?gòu)挠脩艟彌_區(qū)拷貝到socket緩沖區(qū)。
3.2.3 sendfile
linux 提供sendfile系統(tǒng)調(diào)用,只需這一個(gè)系統(tǒng)調(diào)用就可以從一個(gè)文件描述符拷貝數(shù)據(jù)到另外一個(gè)文件描述符
sendfile可以減少write,read導(dǎo)致的系統(tǒng)調(diào)用,從而優(yōu)化效率。
如果網(wǎng)卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技術(shù),那么還可以進(jìn)一步優(yōu)化。
- 通過 DMA 將磁盤上的數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū)里;
- 緩沖區(qū)描述符和數(shù)據(jù)長(zhǎng)度傳到 socket 緩沖區(qū),這樣網(wǎng)卡的 SG-DMA 控制器就可以直接將內(nèi)核緩存中的數(shù)據(jù)拷貝到網(wǎng)卡的緩沖區(qū)里,此過程
不需要將數(shù)據(jù)從操作系統(tǒng)內(nèi)核緩沖區(qū)拷貝到 socket 緩沖區(qū)中
,這樣就減少了一次數(shù)據(jù)拷貝。
這便是所謂的零拷貝,減少內(nèi)存層面拷貝數(shù)據(jù)的次數(shù),以及系統(tǒng)調(diào)用內(nèi)核態(tài)用戶態(tài)的切換,從而優(yōu)化性能。
3.3 NIO中的零拷貝
3.3.1 FileChannel#map
NIO中的FileChannel.map()方法使用了mmap系統(tǒng)調(diào)用實(shí)現(xiàn)內(nèi)存映射方式
將內(nèi)核緩沖區(qū)的內(nèi)存和用戶緩沖區(qū)的內(nèi)存做了一個(gè)地址映射。這種方式適合讀取大文件,同時(shí)也能對(duì)文件內(nèi)容進(jìn)行更改,但是如果其后要通過SocketChannel發(fā)送,還是需要CPU進(jìn)行數(shù)據(jù)的拷貝。
如上是MappedByteBuffer的獲取方式,其實(shí)底層是通過反射調(diào)用DirectByteBuffer的構(gòu)造方法實(shí)現(xiàn)的,其中的cleaner是直接內(nèi)存的回收器,傳入的unmapper會(huì)被回調(diào),從而調(diào)用native方法實(shí)現(xiàn)資源釋放。
這種方式適合讀取大文件,同時(shí)也能對(duì)文件內(nèi)容進(jìn)行更改。
3.3.2 FileChannel#transferTo,transerFrom
在操作系統(tǒng)層面是調(diào)用的一個(gè)sendFile系統(tǒng)調(diào)用。通過這個(gè)系統(tǒng)調(diào)用,可以在內(nèi)核層直接完成文件內(nèi)容的拷貝。
4.FileChannel#force強(qiáng)制刷盤
由于CPU的運(yùn)行速度非常快,所以CPU在執(zhí)行指令時(shí),通常只能與緩存進(jìn)行交互,而不適合直接操作像磁盤、網(wǎng)卡這樣的硬件。也因此,在進(jìn)行文件寫入時(shí),操作系統(tǒng)也是先寫入到page cache中,緩存起來,然后再往硬件寫入。
緩存有利也有弊,使用page cache頁(yè)緩存,應(yīng)用程序?qū)?shù)據(jù)都寫入到了page cache中,但是卻沒有真正寫入磁盤。如果這個(gè)時(shí)候出現(xiàn)斷電,那么將出現(xiàn)緩存數(shù)據(jù)丟失。
FileChannel#force會(huì)進(jìn)行fsync系統(tǒng)調(diào)用
fsync可以實(shí)現(xiàn)將page cache緩存內(nèi)容進(jìn)行落盤,從而保證不丟失(redis aof可以設(shè)置持久化機(jī)制,通常設(shè)置每秒落盤一次,這里落盤也是fsync系統(tǒng)調(diào)用)。為了性能考慮,應(yīng)用程序不可能每寫入一點(diǎn)數(shù)據(jù)就調(diào)用fsync,fsync也是有性能損耗的。
四丶IO多路復(fù)用 select/poll/epoll
上面我們聊到了IO多路復(fù)用解決了什么問題,以及NIO Selector的基本使用,但是沒有探究在操作系統(tǒng)層面是如何實(shí)現(xiàn)的,下面來學(xué)習(xí)一下。
1.select系統(tǒng)調(diào)用
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout)
- nfds: 最大的文件描述符+1,代表監(jiān)聽這一組描述符(為什么要+1?因?yàn)槌水?dāng)前最大描述符之外,還有可能有新的fd連接上來)
- fd_set: 是一個(gè)位圖集合, 對(duì)于同一個(gè)文件描述符,可以監(jiān)聽不同的事件
- readfds:文件描述符“可讀”事件
- writefds:文件描述符“可寫”事件
- exceptfds:文件描述符“異?!笔录话銉?nèi)核用的,實(shí)際編程很少使用
- timeout:超時(shí)時(shí)間:0是立即返回,-1是一直阻塞,如果大于0,則達(dá)到設(shè)置值的微秒數(shù)即返回
- 返回值: 所監(jiān)聽的所有監(jiān)聽集合中滿足條件的總數(shù)(滿足條件的讀、寫、異常事件的總數(shù)),出錯(cuò)時(shí)返回-1,并設(shè)置errno。如果超時(shí)時(shí)間觸發(fā),則返回0
select 其實(shí)就是把NIO中用戶態(tài)要遍歷的fd數(shù)組拷貝到了內(nèi)核態(tài),讓內(nèi)核態(tài)來遍歷,因?yàn)橛脩魬B(tài)判斷socket是否有數(shù)據(jù)依舊需要通過系統(tǒng)調(diào)用,切換到內(nèi)核態(tài)進(jìn)行。
可以看到select依賴了很多位圖參數(shù),系統(tǒng)調(diào)用完后需要用戶程序遍歷一次位圖才能直到哪一個(gè)fd具備了io事件,并且這個(gè)位圖大小最大為1024,導(dǎo)致select用起來需要很多位操作并且最多只能支持1024路IO。
2.poll系統(tǒng)調(diào)用
int poll(struct pollfd *fds, nfds_t nfds/*最大監(jiān)聽的文件描述符個(gè)數(shù)*/, int timeout/*最大監(jiān)聽的文件描述符個(gè)數(shù)*/);
其中pollfd為:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
poll可以看作升級(jí)版select,它突破了1024
個(gè)文件描述符的限制,并且poll
函數(shù)的監(jiān)聽和返回是分開的,簡(jiǎn)化了代碼實(shí)現(xiàn)。
雖然poll
不需要遍歷所有的文件描述符了,只需要遍歷加入數(shù)組中的描述符,范圍縮小了很多,但缺點(diǎn)仍然是需要遍歷,當(dāng)加入數(shù)組描述符很多,但是存在事件的fd很少,這個(gè)遍歷操作還是有點(diǎn)不劃算的。
3.epoll系統(tǒng)調(diào)用
在linux環(huán)境下,java nio中的selector就是基于epoll實(shí)現(xiàn)的。
3.1 epoll_create
int epoll_create(int size)
//返回一個(gè)fd
//傳入大小作為參考值
epoll_create返回一個(gè)特殊的文件描述符,它代表紅黑樹的根節(jié)點(diǎn)。size
則是樹的大小,它代表你將監(jiān)聽多少個(gè)文件描述符。epoll_create
將按照傳入的大小,構(gòu)造出一棵大小為size
的紅黑樹。
3.2 epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// epfd 是epoll_create的返回值,也就說紅黑樹的根節(jié)點(diǎn)
// op 表示操作,比如增加,修改,刪除
//fd 是需要增加,修改,刪除的文件描述符
// struct epoll_event *event 是一個(gè)結(jié)構(gòu)體,如下
struct epoll_event {
uint32_t events; /* Epoll events 讀事件or寫事件,or 異常事件*/
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;//代表一個(gè)文件描述符,初始化的時(shí)候傳入需要監(jiān)聽的文件描述符,當(dāng)監(jiān)聽返回時(shí),此處會(huì)傳出一個(gè)有事件發(fā)生的文件描述符,因此,無需我們遍歷得到結(jié)果了
uint32_t u32;
uint64_t u64;
} epoll_data_t;
用來操作epoll
句柄,可以使用該函數(shù)往紅黑樹里增加文件描述符,修改文件描述符,和刪除文件描述符。
3.3 epoll_wait
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
//epfd 是epoll_create的返回值,也就說紅黑樹的根節(jié)點(diǎn)
// struct epoll_event *events 是一個(gè)數(shù)組,返回的所有觸發(fā)了事件的文件描述符集合
//maxevents代表這個(gè)數(shù)組的大小
//timeout 0代表立即返回,-1代表永久阻塞,如果大于0,則代表超時(shí)等待毫秒數(shù)
3.4 水平觸發(fā),邊緣觸發(fā)
epoll
有兩種觸發(fā)方式,分別為水平觸發(fā)和邊沿觸發(fā)。
-
水平觸發(fā)
只要有數(shù)據(jù)處于就緒狀態(tài),那么可讀事件就會(huì)一直觸發(fā)。
舉個(gè)例子,假設(shè)客戶端一次性發(fā)來了
4K
數(shù)據(jù) ,但是服務(wù)器recv
函數(shù)定義的buffer
大小僅為1024
字節(jié),那么一次肯定是不能將所有數(shù)據(jù)都讀取完的,這時(shí)候就會(huì)繼續(xù)觸發(fā)可讀事件,直到所有數(shù)據(jù)都處理完成。epoll
默認(rèn)的觸發(fā)方式就是水平觸發(fā)。 -
邊緣觸發(fā)
只有數(shù)據(jù)發(fā)送過來的時(shí)候會(huì)觸發(fā)一次,即使數(shù)據(jù)沒有讀取完,也不會(huì)繼續(xù)觸發(fā)。
-
觸發(fā)方式的設(shè)置:
水平觸發(fā)和邊沿觸發(fā)在內(nèi)核里 使用兩個(gè)
bit mask
區(qū)分,分別為:- EPOLLLT 水平 觸發(fā)
- EPOLLET 邊沿觸發(fā)
需要在注冊(cè)事件的時(shí)候?qū)⑵渑c需要注冊(cè)的事件做一個(gè)位或運(yùn)算即可:
ev.events = EPOLLIN; //LT ev.events = EPOLLIN | EPOLLET; //ET
4.總結(jié)
select函數(shù)需要一次性傳入所有需要監(jiān)控的連接(在內(nèi)核中是FD),并在內(nèi)核中對(duì)這些FD進(jìn)行持續(xù)的掃描。當(dāng)發(fā)現(xiàn)其中有FD不老實(shí)時(shí),就會(huì)通知應(yīng)用程序有客戶端事件發(fā)生了, 上層應(yīng)用接到通知后,就只能自己再去遍歷所有的FD,尋找有事件發(fā)生的連接,然后進(jìn)行業(yè)務(wù)處理。
但是select受限于操作系統(tǒng),掃描的FD個(gè)數(shù)是受限的。
于是出現(xiàn)了Poll函數(shù),解決了slelect文件描述符受限的問題。但是,上層應(yīng)用程序依然要自己去遍歷所有客戶端,尋找哪個(gè)客戶端上有事件發(fā) 生。高并發(fā)場(chǎng)景下,性能依然嚴(yán)重受限。
于是又出現(xiàn)了epoll機(jī)制。文章來源:http://www.zghlxwxcb.cn/news/detail-479135.html
epoll機(jī)制會(huì)直接返回有事件發(fā)生的FD。這樣就省掉了上層應(yīng)用頻繁掃描所有客戶端的消耗,進(jìn)一步解決多路復(fù)用的高并發(fā)問題。文章來源地址http://www.zghlxwxcb.cn/news/detail-479135.html
到了這里,關(guān)于Java NIO原理 (Selector、Channel、Buffer、零拷貝、IO多路復(fù)用)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!