參考
- https://www.cnblogs.com/hapjin/p/5736188.html
- https://www.cnblogs.com/jfqiu/p/3852230.html
- https://blog.csdn.net/u013096088/article/details/78774627
IO原理
-
用戶態(tài)
:對(duì)于操作系統(tǒng)而言杯道,JVM只是一個(gè)用戶進(jìn)程(應(yīng)用程序),處于用戶態(tài)空間中责蝠,而處于用戶態(tài)空間的進(jìn)程是不能直接操作底層的硬件(磁盤/網(wǎng)卡)蕉饼。 -
系統(tǒng)調(diào)用
:區(qū)別于用戶進(jìn)程調(diào)用虐杯,系統(tǒng)調(diào)用時(shí)操作系統(tǒng)級(jí)別的api,比如java IO的讀取數(shù)據(jù)過(guò)程(使用緩沖區(qū))昧港,用戶程序發(fā)起讀操作擎椰,導(dǎo)致“ syscall read ”系統(tǒng)調(diào)用,就會(huì)把數(shù)據(jù)搬入到 一個(gè)buffer中创肥;用戶發(fā)起寫(xiě)操作达舒,導(dǎo)致 “syscall write ”系統(tǒng)調(diào)用,將會(huì)把一個(gè) buffer 中的數(shù)據(jù) 搬出去(發(fā)送到網(wǎng)絡(luò)中 or 寫(xiě)入到磁盤文件) -
內(nèi)核態(tài)
:用戶態(tài)的進(jìn)程要訪問(wèn)磁盤/網(wǎng)卡(也就是操作IO)叹侄,必須通過(guò)系統(tǒng)調(diào)用巩搏,從用戶態(tài)切換到內(nèi)核態(tài)(中斷、trap)趾代,才能完成贯底。 -
局部性原理
:操作系統(tǒng)在訪問(wèn)磁盤時(shí),由于局部性原理撒强,操作系統(tǒng)不會(huì)每次只讀取一個(gè)字節(jié)(代價(jià)太大)禽捆,而是借助硬件直接存儲(chǔ)器存取(DMA)一次性讀取一片(一個(gè)或者若干個(gè)磁盤塊)數(shù)據(jù)飘哨。因此胚想,就需要有一個(gè)“中間緩沖區(qū)”--即內(nèi)核緩沖區(qū)。先把數(shù)據(jù)從磁盤讀到內(nèi)核緩沖區(qū)中芽隆,然后再把數(shù)據(jù)從內(nèi)核緩沖區(qū)搬到用戶緩沖區(qū)浊服。
- 從進(jìn)程看,分用戶態(tài)和內(nèi)核態(tài)
應(yīng)用程序?qū)儆谟脩魬B(tài)胚吁,系統(tǒng)調(diào)用則內(nèi)核態(tài) - 從內(nèi)存看牙躺,分用戶內(nèi)存和內(nèi)核內(nèi)存
用戶內(nèi)存 = 應(yīng)用內(nèi)存 = JVM內(nèi)存 = User space
內(nèi)核內(nèi)存 = 系統(tǒng)內(nèi)存 = Kernel space
引用一句話:
操作系統(tǒng)與Java基于流的I/O模型有些不匹配。操作系統(tǒng)要移動(dòng)的是大塊數(shù)據(jù)(緩沖區(qū))腕扶,這往往是在的協(xié)助下完成的述呐。I/O類喜歡操作小塊數(shù)據(jù)——單個(gè)字節(jié)、幾行文本蕉毯。結(jié)果,操作系統(tǒng)送來(lái)整緩沖區(qū)的數(shù)據(jù)思犁,java.io的流數(shù)據(jù)類再花大量時(shí)間把它們拆成小塊代虾,往往拷貝一個(gè)小塊就要往返于幾層對(duì)象。操作系統(tǒng)喜歡整卡車地運(yùn)來(lái)數(shù)據(jù)激蹲,java.io類則喜歡一鏟子一鏟子地加工數(shù)據(jù)棉磨。
—— 引自《JAVA NIO》
java舊的io方式,就是一個(gè)字節(jié)一個(gè)字節(jié)讀取數(shù)據(jù)学辱,這會(huì)引起頻繁的用戶態(tài)和系統(tǒng)態(tài)切換乘瓤,系統(tǒng)開(kāi)銷特別大环形,效率極低。而基于緩沖的讀寫(xiě)衙傀,減少這種不必要的切換抬吟,可以顯著提高IO效率。NIO里ByteBuffer的設(shè)計(jì)统抬,也是為了去貼近操作系統(tǒng)讀取一片數(shù)據(jù)的習(xí)慣火本。
如下,是使用緩沖區(qū)讀取數(shù)據(jù)的原理圖:
IO模型
https://www.cnblogs.com/dolphin0520/p/3916526.html
一共有5種io模型聪建,java nio使用的時(shí)多路復(fù)用模型钙畔。
- 阻塞io:用戶進(jìn)程阻塞,知道io就緒且io處理(讀寫(xiě))完成金麸,阻塞才解除擎析。
- 非阻塞IO:用戶進(jìn)程不阻塞,需要不斷輪詢io是否就緒挥下,很占用cpu資源揍魂,所以一般不用
- 多路復(fù)用IO:用戶進(jìn)程阻塞,也是輪詢io是否就緒见秽,但是和非阻塞IO不一樣的是愉烙,輪詢io的是系統(tǒng)進(jìn)程而非應(yīng)用進(jìn)程,實(shí)際使用了代理(select/poll/epoll)解取,可以避免cpu空轉(zhuǎn)步责,所以效率較高,而且1個(gè)進(jìn)程可以管理多個(gè)socket禀苦,java nio使用的就是多路復(fù)用模型蔓肯。
- 信號(hào)驅(qū)動(dòng)式 I/O:(有點(diǎn)類似回調(diào))用戶進(jìn)程不阻塞,而是安裝一個(gè)信號(hào)處理函數(shù)振乏,當(dāng)數(shù)據(jù)準(zhǔn)備好時(shí)蔗包,進(jìn)程會(huì)收到一個(gè) SIGIO 信號(hào),可以在信號(hào)處理函數(shù)中調(diào)用 I/O 操作函數(shù)處理數(shù)據(jù)慧邮。
- 異步io:用戶進(jìn)程不阻塞调限,發(fā)起io請(qǐng)求(讀寫(xiě))后就可以做自己的事了,io就緒等待和io操作交給操作系統(tǒng)去完成误澳,完成后調(diào)用用戶進(jìn)程處理函數(shù)耻矮,所以整個(gè)過(guò)程都是異步的。
多路復(fù)用IO和非阻塞比較
非阻塞是用戶進(jìn)程去輪詢io是否就緒忆谓,所以用戶進(jìn)程是不阻塞的裆装。
而多路復(fù)用,用戶進(jìn)程是阻塞的,輪詢io是否就緒是系統(tǒng)進(jìn)程(內(nèi)核)在進(jìn)行哨免。
多路復(fù)用IO為何比非阻塞IO模型的效率高是因?yàn)樵诜亲枞鸌O中茎活,不斷地詢問(wèn)socket狀態(tài)時(shí)通過(guò)用戶線程去進(jìn)行的,而在多路復(fù)用IO中琢唾,輪詢每個(gè)socket狀態(tài)是內(nèi)核在進(jìn)行的载荔,這個(gè)效率要比用戶線程要高的多。
select慧耍、poll和epoll
參考:
https://www.cnblogs.com/lojunren/p/3856290.html
https://blog.csdn.net/davidsguo008/article/details/73556811
select身辨、poll和epoll都是多路復(fù)用iO在linux系統(tǒng)上的實(shí)現(xiàn)技術(shù),以前使用select/poll芍碧,epoll是Linux 2.6內(nèi)核引入煌珊,它能顯著提高程序在大量并發(fā)連接中只有少量活躍的情況下的系統(tǒng)CPU利用率。
select和poll
- 場(chǎng)景
有100萬(wàn)個(gè)客戶端同時(shí)與一個(gè)服務(wù)器進(jìn)程保持著TCP連接泌豆。而每一時(shí)刻定庵,通常只有幾百上千個(gè)TCP連接是活躍的。 - 實(shí)現(xiàn)過(guò)程
返回的活躍連接 ==select(全部待監(jiān)控的連接)- 服務(wù)器進(jìn)程每次都把這100萬(wàn)個(gè)連接的套接字告訴操作系統(tǒng)(從用戶態(tài)復(fù)制句柄數(shù)據(jù)結(jié)構(gòu)到內(nèi)核態(tài))
- 操作系統(tǒng)內(nèi)核去輪詢這些套接字上是否有事件發(fā)生踪危,輪詢完后蔬浙,再將句柄數(shù)據(jù)復(fù)制到用戶態(tài)
- 用戶態(tài)的應(yīng)用程序輪詢處理已發(fā)生的網(wǎng)絡(luò)事件
這一過(guò)程資源消耗較大,因此贞远,select/poll一般只能處理幾千的并發(fā)連接畴博。
- 缺點(diǎn)
- 采用輪詢的方式掃描文件描述符,文件描述符數(shù)量越多蓝仲,性能越差俱病,時(shí)間復(fù)雜度為O(n);
- 內(nèi)核 / 用戶空間內(nèi)存拷貝問(wèn)題袱结,select需要復(fù)制大量的句柄數(shù)據(jù)結(jié)構(gòu)亮隙,產(chǎn)生巨大的開(kāi)銷;
- 返回的是含有整個(gè)句柄的數(shù)組垢夹,應(yīng)用程序需要遍歷整個(gè)數(shù)組才能發(fā)現(xiàn)哪些句柄發(fā)生了事件溢吻;
- 觸發(fā)方式是水平觸發(fā)(LT),應(yīng)用程序如果沒(méi)有完成對(duì)一個(gè)已經(jīng)就緒的文件描述符進(jìn)行IO操作果元,那么之后每次select調(diào)用還是會(huì)將這些文件描述符通知進(jìn)程促王。
- select和poll的區(qū)別
select單個(gè)進(jìn)程能夠監(jiān)視的文件描述符(也叫句柄,使用__FD_SETSIZE參數(shù)設(shè)置)的數(shù)量存在最大限制而晒,通常是1024蝇狼;
相比select模型,poll使用鏈表保存文件描述符欣硼,因此沒(méi)有了監(jiān)視文件數(shù)量的限制。
epolld
實(shí)現(xiàn)原理:三要素
mmap、紅黑色诈胜、鏈表-
實(shí)現(xiàn)過(guò)程:三部曲
- epoll_create()系統(tǒng)調(diào)用豹障,即新建epoll描述符,epoll是通過(guò)內(nèi)核與用戶空間
mmap(映射)
同一塊物理內(nèi)存實(shí)現(xiàn)的焦匈,從而減少用戶態(tài)和內(nèi)核態(tài)之間的數(shù)據(jù)交換血公。 - epoll_ctrl(epoll描述符,連接)系統(tǒng)調(diào)用缓熟,即向epoll對(duì)象中添加累魔、刪除、修改感興趣的連接(事件)够滑,epoll在實(shí)現(xiàn)上使用一顆
紅黑樹(shù)
來(lái)存儲(chǔ)所有監(jiān)聽(tīng)的連接垦写,每個(gè)連接是一個(gè)事件對(duì)象epitem,紅黑樹(shù)本身插入和刪除性能比較好彰触,時(shí)間復(fù)雜度O(logN)梯投。同時(shí),epoll中的每個(gè)epitem都會(huì)與設(shè)備(網(wǎng)卡)驅(qū)動(dòng)程序建立回調(diào)關(guān)系况毅,當(dāng)相應(yīng)的事件發(fā)生時(shí)會(huì)調(diào)用這個(gè)回調(diào)方法(ep_poll_callback),它會(huì)將發(fā)生的epitem添加到rdlist雙鏈表
中分蓖。 - 返回的活躍連接 ==epoll_wait( epoll描述符 )系統(tǒng)調(diào)用,epoll_wait檢查是否有事件發(fā)生時(shí)尔许,只需要檢查eventpoll對(duì)象中的rdlist雙鏈表中是否有epitem元素即可么鹤。如果rdlist不為空,則把發(fā)生的事件復(fù)制到用戶態(tài)味廊,同時(shí)將事件數(shù)量返回給用戶蒸甜。
- epoll_create()系統(tǒng)調(diào)用豹障,即新建epoll描述符,epoll是通過(guò)內(nèi)核與用戶空間
通過(guò)內(nèi)存映射機(jī)制,紅黑樹(shù)和雙鏈表數(shù)據(jù)結(jié)構(gòu)毡们,并結(jié)合回調(diào)機(jī)制迅皇,造就了epoll的高效。
- 優(yōu)點(diǎn)
- 使用內(nèi)存映射衙熔,減少用戶空間與內(nèi)核空間內(nèi)存拷貝登颓;
- 注冊(cè)操作與返回活躍連接分開(kāi),不需要每次都傳入所有文件描述符红氯;
- 使用事件回調(diào)機(jī)制框咙,系統(tǒng)調(diào)用epoll_waitepoll,只返回就緒的事件痢甘,不需要遍歷所有文件描述符喇嘱,時(shí)間復(fù)雜度是O(1)。
- epoll的觸發(fā)方式是邊緣觸發(fā)(ET)塞栅,在并發(fā)者铜、大流量的情況下,會(huì)比LT少很多epoll的系統(tǒng)調(diào)用,因此效率高作烟。
需要注意的是:epoll并不是在所有的應(yīng)用場(chǎng)景都會(huì)比select和poll高很多愉粤。尤其是當(dāng)活動(dòng)連接比較多的時(shí)候,回調(diào)函數(shù)被觸發(fā)得過(guò)于頻繁的時(shí)候拿撩,epoll的效率也會(huì)受到顯著影響衣厘!所以,epoll特別適用于連接數(shù)量多压恒,但活動(dòng)連接較少的情況影暴。
Reactor 反應(yīng)器模型
io多路復(fù)用模型+線程池復(fù)用 = Reactor反應(yīng)器模型
幾種io讀寫(xiě)方式
- io,面向流探赫,沒(méi)有使用Buffer(緩沖)型宙,一次就讀取1個(gè)字節(jié)。頻繁的調(diào)用系統(tǒng)函數(shù)(read/writer)期吓,用戶態(tài)和內(nèi)核態(tài)不斷上下文切換早歇,速度很慢。
- io讨勤,緩沖流箭跳,使用byte數(shù)組或緩沖流(比如BufferInputStream),實(shí)現(xiàn)了緩沖潭千,一次讀取多個(gè)字節(jié)到用戶緩沖區(qū)谱姓,減少系統(tǒng)調(diào)用次數(shù)來(lái)提高性能的。
- nio刨晴,HeapByteBuffer屉来,面向塊(緩沖區(qū))
- nio,DirectByteBuffer狈癞,使用直接內(nèi)存/堆外內(nèi)存
- nio茄靠,MappedByteBuffer,使用內(nèi)存映射文件蝶桶,不需要經(jīng)過(guò)內(nèi)核態(tài)的緩沖區(qū) 慨绳,其實(shí)也算是直接內(nèi)存的一種。
nio的byteBuffer和io的buffer
一直找不到有關(guān)這兩者區(qū)別的文章真竖,只有一句話有一些隱含的表達(dá):
nio中Buffer的引入脐雪,使得java的IO模型更貼近操作系統(tǒng)底層,面向Buffer的讀寫(xiě)操作更高效恢共,同時(shí)也在API層面避免了單字節(jié)操作战秋。
還有一句話,說(shuō)bytebuffer的非阻塞會(huì)引起很多問(wèn)題讨韭,導(dǎo)致數(shù)據(jù)讀取不完整脂信,不太理解
Java NIO的緩沖導(dǎo)向方法略有不同癣蟋。數(shù)據(jù)讀取到一個(gè)它稍后處理的緩沖區(qū),需要時(shí)可在緩沖區(qū)中前后移動(dòng)狰闪。這就增加了處理過(guò)程中的靈活性梢薪。但是,還需要檢查是否該緩沖區(qū)中包含所有您需要處理的數(shù)據(jù)尝哆。而且,需確保當(dāng)更多的數(shù)據(jù)讀入緩沖區(qū)時(shí)甜攀,不要覆蓋緩沖區(qū)里尚未處理的數(shù)據(jù)秋泄。
個(gè)人總結(jié),兩者背后使用的都是緩沖區(qū)方式规阀,實(shí)現(xiàn)原理應(yīng)該是一樣的恒序,不同之處應(yīng)該是API設(shè)計(jì)上,ByteBuffer是經(jīng)過(guò)特別封裝的緩沖區(qū)谁撼,支持指針式的讀取歧胁。
MappedByteBuffer內(nèi)存映射文件
內(nèi)存映射文件就是將某一段的虛擬地址和文件對(duì)象的某一部分建立起映射關(guān)系,此時(shí)并沒(méi)有拷貝數(shù)據(jù)到內(nèi)存中去厉碟,而是當(dāng)進(jìn)程代碼第一次引用這段代碼內(nèi)的虛擬地址時(shí)喊巍,觸發(fā)了缺頁(yè)異常,這時(shí)候OS根據(jù)映射關(guān)系直接將文件的相關(guān)部分?jǐn)?shù)據(jù)拷貝到進(jìn)程的用戶私有空間中去箍鼓,當(dāng)有操作第N頁(yè)數(shù)據(jù)的時(shí)候重復(fù)這樣的OS頁(yè)面調(diào)度程序操作崭参。原來(lái)內(nèi)存映射文件的效率比標(biāo)準(zhǔn)IO高的重要原因就是因?yàn)樯倭税褦?shù)據(jù)拷貝到OS內(nèi)核緩沖區(qū)這一步(可能還少了native堆中轉(zhuǎn)這一步)。
參考:https://blog.csdn.net/fcbayernmunchen/article/details/8635427
DirectByteBuffer和HeapByteBuffer的區(qū)別
ByteBuffer分兩種款咖,直接緩沖區(qū)和非直接緩沖區(qū)何暮。
- heap ByteBuffer:非直接緩存區(qū),也叫堆內(nèi)緩存铐殃,可以通過(guò)ByteBuffer.wrap(byte[] array);ByteBuffer.allocate(int capacity)這兩個(gè)方法來(lái)創(chuàng)建海洼,該類對(duì)象分配在JVM的堆內(nèi)存里面,直接由Java虛擬機(jī)負(fù)責(zé)垃圾回收
- direct ByteBuffer:直接緩存富腊,也叫堆外緩存坏逢,通過(guò)ByteBuffer.allocateDirect(int capacity)來(lái)創(chuàng)建,是借助JNI 從本機(jī)代碼創(chuàng)建在虛擬機(jī)外內(nèi)存蟹肘,堆外內(nèi)存通過(guò)full gc來(lái)回收內(nèi)存词疼,-XX:MaxDirectMemorySize參數(shù)可以限制直接緩存大小,含義是當(dāng)Direct ByteBuffer分配的堆外內(nèi)存到達(dá)指定大小后帘腹,觸發(fā)Full GC贰盗。
比較:
- Direct Buffer分配和取消分配的時(shí)間成本高、而且使用不安全阳欲,堆外內(nèi)存不好回收
- 速度問(wèn)題舵盈,因?yàn)镈irect Buffer減少了內(nèi)核空間拷貝的過(guò)程陋率,所以速度比Heap ByteBuffer明顯要快。
- 使用場(chǎng)景問(wèn)題
建議將直接緩沖區(qū)主要分配給那些易受基礎(chǔ)系統(tǒng)的本機(jī) I/O 操作影響的大型秽晚、持久的緩沖區(qū)瓦糟。
DirectByteBuffer主要用在設(shè)備間的i/o拷貝,比如網(wǎng)絡(luò)編程赴蝇,數(shù)據(jù)不需要再native memory和jvm memory中來(lái)回copy鄙才,實(shí)現(xiàn)zero copy援制。
其它場(chǎng)景不建議使用。
netty這些nio框架背后有使用DirectByteBuffer。
zerocopy技術(shù)
java.nio.channels.FileChannel類的transferTo()方法使用的就是zerocopy技術(shù)闲擦,可以直接將字節(jié)從讀的通道(Readable Channel)傳送到可寫(xiě)的通道中(Writable Channel)绍弟,并不需要將字節(jié)送入用戶程序空間(用戶緩沖區(qū))杏死。
- IO操作需要數(shù)據(jù)頻繁地在內(nèi)核緩沖區(qū)和用戶緩沖區(qū)之間拷貝厢岂,而zerocopy技術(shù)可以減少這種拷貝的次數(shù)
- 降低了上下文切換(用戶態(tài)與內(nèi)核態(tài)之間的切換)的次數(shù)
io是面向流,nio是面向緩沖楚堤,所以io性能差疫蔓,這是錯(cuò)誤的
實(shí)際上,io做文件讀寫(xiě)的速度并不比nio差身冬,因?yàn)閕o也可以使用buffer緩沖區(qū)來(lái)提高性能衅胀,使用buffer來(lái)讀寫(xiě)數(shù)據(jù)的io和nio的bytebuffer多大區(qū)別。人們常說(shuō)的舊io性能低下酥筝,其實(shí)指的是不使用buffer情況下拗小,io使用一個(gè)字節(jié)一個(gè)字節(jié)的讀寫(xiě)的方式。
因?yàn)樵?JDK 1.4 中原來(lái)的 I/O 包和 NIO 已經(jīng)很好地集成了樱哼。 java.io.* 已經(jīng)以 NIO 為基礎(chǔ)重新實(shí)現(xiàn)了哀九,所以現(xiàn)在它可以利用 NIO 的一些特性。例如搅幅, java.io.* 包中的一些類包含以塊的形式讀寫(xiě)數(shù)據(jù)的方法阅束,這使得即使在更面向流的系統(tǒng)中,處理速度也會(huì)更快茄唐。