最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊

一文教你從Linux內(nèi)核角度探秘JDK NIO文件讀寫本質(zhì)(下)

2022-12-03 21:01 作者:補(bǔ)給站Linux內(nèi)核  | 我要投稿

接上文一文教你從Linux內(nèi)核角度探秘JDK NIO文件讀寫本質(zhì)(上)

10. JDK NIO 對(duì)普通文件的寫入


在對(duì)文件進(jìn)行讀寫之前,我們需要首先利用 RandomAccessFile 在內(nèi)核中打開指定的文件 file-read-write.txt ,并獲取到它的文件描述符 fd = 5000。

圖片
image.png

本例 heapByteBuffer 中存放著需要寫入文件的內(nèi)容,隨后來到 FileChannelImpl 實(shí)現(xiàn)類調(diào)用 IOUtil 觸發(fā)底層系統(tǒng)調(diào)用 write 來寫入文件。

NIO 中的所有 IO 操作全部封裝在 IOUtil 類中,而 NIO 中的 SocketChannel 以及這里介紹的 FileChannel 底層依賴的系統(tǒng)調(diào)用可能不同,這里會(huì)通過 NativeDispatcher 對(duì)具體 Channel 操作實(shí)現(xiàn)分發(fā),調(diào)用具體的系統(tǒng)調(diào)用。對(duì)于 FileChannel 來說 NativeDispatcher 的實(shí)現(xiàn)類為 FileDispatcher。對(duì)于 SocketChannel 來說 NativeDispatcher 的實(shí)現(xiàn)類為 SocketDispatcher。

在 IOUtil 中首先創(chuàng)建一個(gè)臨時(shí)的 DirectByteBuffer,然后將本例中 HeapByteBuffer 中的數(shù)據(jù)全部拷貝到這個(gè)臨時(shí)的 DirectByteBuffer 中。這個(gè) DirectByteBuffer 就是我們在 IO 系統(tǒng)調(diào)用中經(jīng)常提到的用戶空間緩沖區(qū)。

隨后在 writeFromNativeBuffer 方法中通過 ?FileDispatcher 觸發(fā) JNI 層的 native 方法執(zhí)行底層系統(tǒng)調(diào)用 write 。

NIO 中關(guān)于文件 IO 相關(guān)的系統(tǒng)調(diào)用全部封裝在 JNI 層中的 FileDispatcherImpl.c 文件中。里邊定義了各種 IO 相關(guān)的系統(tǒng)調(diào)用的 native 方法。

系統(tǒng)調(diào)用 write 在內(nèi)核中的定義如下所示:

現(xiàn)在我們就從用戶空間的 JDK NIO 這一層逐步來到了內(nèi)核空間的邊界處 --- OS 系統(tǒng)調(diào)用 write 這里,馬上就要進(jìn)入內(nèi)核了。

圖片
image.png

這一次我們來看一下當(dāng)系統(tǒng)調(diào)用 write 發(fā)起之后,用戶進(jìn)程在內(nèi)核態(tài)具體做了哪些事情?


【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!?。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)??


11. 從內(nèi)核角度探秘文件寫入本質(zhì)

現(xiàn)在讓我們再次進(jìn)入內(nèi)核,來看一下內(nèi)核中具體是如何處理文件寫入操作的,這個(gè)過程會(huì)比文件讀取要復(fù)雜很多,大家需要有點(diǎn)耐心~~

11.1 Buffered IO

圖片
image.png

使用 JDK NIO 中的 HeapByteBuffer 在對(duì)文件進(jìn)行寫入的過程,主要分為如下幾個(gè)核心步驟:

  1. 首先會(huì)在用戶空間的 JDK 層將位于 JVM 堆中的 HeapByteBuffer 中的待寫入數(shù)據(jù)拷貝到位于 OS 堆中的 DirectByteBuffer 中。這里發(fā)生第一次拷貝

  2. 隨后 NIO 會(huì)在用戶態(tài)通過系統(tǒng)調(diào)用 write 發(fā)起文件寫入的請求,此時(shí)發(fā)生第一次上下文切換

  3. 隨后用戶進(jìn)程進(jìn)入內(nèi)核態(tài),在虛擬文件系統(tǒng)層調(diào)用 vfs_write 觸發(fā)對(duì) page cache 寫入的操作。相關(guān)操作封裝在 generic_perform_write 函數(shù)中。這個(gè)后面筆者會(huì)細(xì)講,這里我們只關(guān)注核心總體流程。

  4. 內(nèi)核調(diào)用 iov_iter_copy_from_user_atomic 函數(shù)將用戶空間緩沖區(qū) DirectByteBuffer 中的待寫入數(shù)據(jù)拷貝到 page cache 中。發(fā)生第二次拷貝動(dòng)作,這里的操作就是我們常說的 CPU 拷貝。

  5. 當(dāng)待寫入數(shù)據(jù)拷貝到 page cache 中時(shí),內(nèi)核會(huì)將對(duì)應(yīng)的文件頁標(biāo)記為臟頁。

臟頁表示內(nèi)存中的數(shù)據(jù)要比磁盤中對(duì)應(yīng)文件數(shù)據(jù)要新。

  1. 此時(shí)內(nèi)核會(huì)根據(jù)一定的閾值判斷是否要對(duì) page cache 中的臟頁進(jìn)行回寫,如果不需要同步回寫,進(jìn)程直接返回。文件寫入操作完成。這里發(fā)生第二次上下文切換

從這里我們看到在對(duì)文件進(jìn)行寫入時(shí),內(nèi)核只會(huì)將數(shù)據(jù)寫入到 page cache 中。整個(gè)寫入過程就完成了,并不會(huì)寫到磁盤中。

  1. 臟頁回寫又會(huì)根據(jù)臟頁數(shù)量在內(nèi)存中的占比分為:進(jìn)程同步回寫和內(nèi)核異步回寫。當(dāng)臟頁太多了,進(jìn)程自己都看不下去的時(shí)候,會(huì)同步回寫內(nèi)存中的臟頁,直到回寫完畢才會(huì)返回。在回寫的過程中會(huì)發(fā)生第三次拷貝,通過DMA 將 page cache 中的臟頁寫入到磁盤中。

所謂內(nèi)核異步回寫就是內(nèi)核會(huì)定時(shí)喚醒一個(gè) flusher 線程,定時(shí)將內(nèi)存中的臟頁回寫到磁盤中。這部分的內(nèi)容筆者會(huì)在后續(xù)的章節(jié)中詳細(xì)講解。

在 NIO 使用 HeapByteBuffer 在對(duì)文件進(jìn)行寫入的過程中,一般只會(huì)發(fā)生兩次拷貝動(dòng)作和兩次上下文切換,因?yàn)閮?nèi)核將數(shù)據(jù)拷貝到 page cache 中后,文件寫入過程就結(jié)束了。如果臟頁在內(nèi)存中的占比太高了,達(dá)到了進(jìn)程同步回寫的閾值,那么就會(huì)發(fā)生第三次 DMA 拷貝,將臟頁數(shù)據(jù)回寫到磁盤文件中。

如果進(jìn)程需要同步回寫臟頁數(shù)據(jù)時(shí),在本例中是要發(fā)生三次拷貝動(dòng)作。但一般情況下,在本例中只會(huì)發(fā)生兩次,沒有第三次的 DMA 拷貝。

11.2 Direct IO

在 JDK 10 中我們可以通過如下的方式采用 Direct IO 模式打開文件:

圖片
image.png

在 Direct IO 模式下的文件寫入操作最明顯的特點(diǎn)就是繞過 page cache 直接通過 DMA 拷貝將用戶空間緩沖區(qū) DirectByteBuffer 中的待寫入數(shù)據(jù)寫入到磁盤中。

  • 同樣發(fā)生兩次上下文切換、

  • 在本例中只會(huì)發(fā)生兩次數(shù)據(jù)拷貝,第一次是將 JVM 堆中的 HeapByteBuffer 中的待寫入數(shù)據(jù)拷貝到位于 OS 堆中的 DirectByteBuffer 中。第二次則是 DMA 拷貝,將用戶空間緩沖區(qū) DirectByteBuffer 中的待寫入數(shù)據(jù)寫入到磁盤中。

12. Talk is cheap ! show you the code

下面是系統(tǒng)調(diào)用 write 在內(nèi)核中的完整定義:

這里和文件讀取的流程基本一樣,也是通過 vfs_write 進(jìn)入虛擬文件系統(tǒng)層。

在虛擬文件系統(tǒng)層,通過 struct file 中定義的函數(shù)指針 file_operations 在具體的文件系統(tǒng)中執(zhí)行相應(yīng)的文件 IO 操作。我們還是以 ext4 文件系統(tǒng)為例。

在 ext4 文件系統(tǒng)中 .write_iter 函數(shù)指針指向的是 ext4_file_write_iter 函數(shù)執(zhí)行具體的文件寫入操作。

圖片
image.png

由于 ext4_file_operations 中只定義了 .write_iter 函數(shù)指針,所以在 __vfs_write 函數(shù)中流程進(jìn)入 else if {......} 分支來到 new_sync_write 函數(shù)中:

在文件讀取的相關(guān)章節(jié)中,我們介紹了用于封裝傳遞進(jìn)來的用戶空間緩沖區(qū) DirectByteBuffer 相關(guān)信息的 struct iovec 結(jié)構(gòu)體,也介紹了用于封裝文件 IO 相關(guān)操作的狀態(tài)和進(jìn)度信息的 struct kiocb 結(jié)構(gòu)體,這里筆者不在贅述。

不過在這里筆者還是想強(qiáng)調(diào)的一下,內(nèi)核中一般會(huì)使用 struct iov_iter 結(jié)構(gòu)體對(duì) struct iovec 進(jìn)行包裝,iov_iter 中包含多個(gè) iovec。

這是為了兼容 readv() ,writev() 等系統(tǒng)調(diào)用,它允許用戶使用多個(gè)緩存區(qū)去讀取文件中的數(shù)據(jù)或者從多個(gè)緩沖區(qū)中寫入數(shù)據(jù)到文件中。

  • JDK NIO Channel 支持的 Scatter 操作底層原理就是 readv 系統(tǒng)調(diào)用。

  • JDK NIO Channel 支持的 Gather 操作底層原理就是 writev 系統(tǒng)調(diào)用。

最終在 ?call_write_iter 中觸發(fā) ext4_file_write_iter 的調(diào)用,從虛擬文件系統(tǒng)層進(jìn)入到具體文件系統(tǒng) ext4 中。

我們看到在文件系統(tǒng) ext4 中調(diào)用的是 __generic_file_write_iter 方法。內(nèi)核針對(duì)文件寫入的所有邏輯都封裝在這里。

這里和我們在介紹文件讀取時(shí)候提到的 generic_file_read_iter 函數(shù)中的邏輯是一樣的。都會(huì)處理 Direct IO 和 Buffered IO 的場景。

這里對(duì)于 Direct IO 的處理都是一樣的,在 generic_file_direct_write 中也是會(huì)調(diào)用 address_space 中的 address_space_operations 定義的 .direct_IO 函數(shù)指針來繞過 page cache 直接寫入磁盤。

圖片
image.png

在 ext4 文件系統(tǒng)中實(shí)現(xiàn) Direct IO 的函數(shù)是 ext4_direct_IO,這里直接會(huì)調(diào)用到塊設(shè)備驅(qū)動(dòng)層,通過 do_blockdev_direct_IO 直接將用戶空間緩沖區(qū) DirectByteBuffer 中的內(nèi)容寫入磁盤中。do_blockdev_direct_IO 函數(shù)會(huì)等到所有的 Direct IO 寫入到磁盤之后才會(huì)返回。


Direct IO 是由 DMA ?直接從用戶空間緩沖區(qū) DirectByteBuffer 中拷貝到磁盤中。

下面我們主要介紹下 Buffered IO 的寫入邏輯 generic_perform_write 方法。

12.1 Buffered IO

圖片
image.png

由于本文中筆者是以 ext4 文件系統(tǒng)為例來介紹文件的讀寫流程,本小節(jié)中介紹的文件寫入流程涉及到與文件系統(tǒng)相關(guān)的兩個(gè)操作:write_begin,write_end。這兩個(gè)函數(shù)在不同的文件系統(tǒng)中都有不同的實(shí)現(xiàn),在不同的文件系統(tǒng)中,寫入每一個(gè)文件頁都需要調(diào)用一次 write_begin,write_end 這兩個(gè)方法。

下圖為本文中涉及文件讀寫的所有內(nèi)核數(shù)據(jù)結(jié)構(gòu)圖:

圖片
image.png

經(jīng)過前邊介紹文件讀取的章節(jié)我們知道在讀取文件的時(shí)候都是先從 page cache 中讀取,如果 page cache 正好緩存了文件頁就直接返回。如果沒有在進(jìn)行磁盤 IO。

文件的寫入過程也是一樣,內(nèi)核會(huì)將用戶緩沖區(qū) DirectByteBuffer 中的待寫數(shù)據(jù)先拷貝到 page cache 中,寫完就直接返回。后續(xù)內(nèi)核會(huì)根據(jù)一定的規(guī)則把這些文件頁回寫到磁盤中。

從這個(gè)過程我們可以看出,內(nèi)核將數(shù)據(jù)先是寫入 page cache 中但是不會(huì)立刻寫入磁盤中,如果突然斷電或者系統(tǒng)崩潰就可能導(dǎo)致文件系統(tǒng)處于不一致的狀態(tài)。

為了解決這種場景,于是 linux 內(nèi)核引入了 ext3 , ext4 等日志文件系統(tǒng)。而日志文件系統(tǒng)比非日志文件系統(tǒng)在磁盤中多了一塊 Journal 區(qū)域,Journal 區(qū)域就是存放管理文件元數(shù)據(jù)和文件數(shù)據(jù)操作日志的磁盤區(qū)域。

  • 文件元數(shù)據(jù)的日志用于恢復(fù)文件系統(tǒng)的一致性。

  • 文件數(shù)據(jù)的日志用于防止系統(tǒng)故障造成的文件內(nèi)容損壞,

ext3 , ext4 等日志文件系統(tǒng)分為三種模式,我們可以在掛載的時(shí)候選擇不同的模式。

  • 日志模式(Journal 模式):這種模式在將數(shù)據(jù)寫入文件系統(tǒng)前,必須等待元數(shù)據(jù)和數(shù)據(jù)的日志已經(jīng)落盤才能發(fā)揮作用。這樣性能比較差,但是最安全。

  • 順序模式(Order 模式):在 Order 模式不會(huì)記錄數(shù)據(jù)的日志,只會(huì)記錄元數(shù)據(jù)的日志,但是在寫元數(shù)據(jù)的日志前,必須先確保數(shù)據(jù)已經(jīng)落盤。這樣可以減少文件內(nèi)容損壞的機(jī)會(huì),這種模式是對(duì)性能的一種折中,是默認(rèn)模式。

  • 回寫模式(WriteBack 模式):WriteBack 模式 和 Order 模式一樣它們都不會(huì)記錄數(shù)據(jù)的日志,只會(huì)記錄元數(shù)據(jù)的日志,不同的是在 WriteBack 模式下不會(huì)保證數(shù)據(jù)比元數(shù)據(jù)先落盤。這個(gè)性能最好,但是最不安全。

而 write_begin,write_end 正是對(duì)文件系統(tǒng)中相關(guān)日志的操作,在 ext4 文件系統(tǒng)中對(duì)應(yīng)的是 ext4_write_begin,ext4_write_end。下面我們就來看一下在 Buffered IO 模式下對(duì)于 ext4 文件系統(tǒng)中的文件寫入的核心步驟。

12.2 ext4_write_begin

在寫入文件數(shù)據(jù)之前,內(nèi)核在 ext4_write_begin 方法中調(diào)用 ext4_journal_start 方法做一些相關(guān)日志的準(zhǔn)備工作。

還有一個(gè)重要的事情是在 grab_cache_page_write_begin 方法中從 page cache 中根據(jù) index 查找要寫入數(shù)據(jù)的文件緩存頁。

通過 pagecache_get_page 在 page cache 中查找要寫入數(shù)據(jù)的緩存頁。如果緩存頁不在 page cache 中,內(nèi)核則會(huì)首先會(huì)在物理內(nèi)存中分配一個(gè)內(nèi)存頁,然后將新分配的內(nèi)存頁加入到 page cache 中。

相關(guān)的查找過程筆者已經(jīng)在 《8. page cache 中查找緩存頁》小節(jié)中詳細(xì)介紹過了,這里不在贅述。

12.3 iov_iter_copy_from_user_atomic

這里就是寫入過程的關(guān)鍵所在,圖中描述的 CPU 拷貝是將用戶空間緩存區(qū) DirectByteBuffer 中的待寫入數(shù)據(jù)拷貝到內(nèi)核里的 page cache 中,這個(gè)過程就發(fā)生在這里。

但是這里不能直接進(jìn)行拷貝,因?yàn)榇藭r(shí)從 page cache 中取出的緩存頁 page 是物理地址,而在內(nèi)核中是不能夠直接操作物理地址的,只能操作虛擬地址。

那怎么辦呢?所以就需要調(diào)用 kmap_atomic 將緩存頁臨時(shí)映射到內(nèi)核空間的一段虛擬地址上,然后將用戶空間緩存區(qū) DirectByteBuffer 中的待寫入數(shù)據(jù)通過這段映射的虛擬地址拷貝到 page cache 中的相應(yīng)緩存頁中。這時(shí)文件的寫入操作就已經(jīng)完成了。

從這里我們看出,內(nèi)核對(duì)于文件的寫入只是將數(shù)據(jù)寫入到 page cache 中就完事了并沒有真正地寫入磁盤。

由于是臨時(shí)映射,所以在拷貝完成之后,調(diào)用 kunmap_atomic 將這段映射再解除掉。

12.4 ext4_write_end

在這里會(huì)對(duì)文件的寫入流程做一些收尾的工作,比如在 block_write_end 方法中會(huì)調(diào)用 mark_buffer_dirty 將寫入的緩存頁在 page cache 中標(biāo)記為臟頁。后續(xù)內(nèi)核會(huì)根據(jù)一定的規(guī)則將 page cache 中的這些臟頁回寫進(jìn)磁盤中。

具體的標(biāo)記過程筆者已經(jīng)在《7.1 radix_tree 的標(biāo)記》小節(jié)中詳細(xì)介紹過了,這里不在贅述。

圖片
image.png

另一個(gè)核心的步驟就是調(diào)用 ext4_journal_stop 完成相關(guān)日志的寫入。這里日志也只是會(huì)先寫到緩存里,不會(huì)直接落盤。

12.5 balance_dirty_pages_ratelimited

當(dāng)進(jìn)程將待寫數(shù)據(jù)寫入 page cache 中之后,相應(yīng)的緩存頁就變?yōu)榱伺K頁,我們需要找一個(gè)時(shí)機(jī)將這些臟頁回寫到磁盤中。防止斷電導(dǎo)致數(shù)據(jù)丟失。

本小節(jié)我們主要聚焦于臟頁回寫的主體流程,相應(yīng)細(xì)節(jié)部分以及內(nèi)核對(duì)臟頁的回寫時(shí)機(jī)我們放在下一小節(jié)中在詳細(xì)為大家介紹。

在 balance_dirty_pages_ratelimited ?會(huì)判斷如果臟頁數(shù)量在內(nèi)存中達(dá)到了一定的規(guī)模 ratelimit 就會(huì)觸發(fā) balance_dirty_pages 回寫臟頁邏輯。

如果達(dá)到了臟頁回寫的條件,那么內(nèi)核就會(huì)喚醒 flusher 線程去將這些臟頁異步回寫到磁盤中。

13. 內(nèi)核回寫臟頁的觸發(fā)時(shí)機(jī)

經(jīng)過前邊對(duì)文件寫入過程的介紹我們看到,用戶進(jìn)程在對(duì)文件進(jìn)行寫操作的時(shí)候只是將待寫入數(shù)據(jù)從用戶空間的緩沖區(qū) DirectByteBuffer 寫入到內(nèi)核中的 page cache 中就結(jié)束了。后面內(nèi)核會(huì)對(duì)臟頁進(jìn)行延時(shí)寫入到磁盤中。

當(dāng) page cache 中的緩存頁比磁盤中對(duì)應(yīng)的文件頁的數(shù)據(jù)要新時(shí),就稱這些緩存頁為臟頁。

延時(shí)寫入的好處就是進(jìn)程可以多次頻繁的對(duì)文件進(jìn)行寫入但都是寫入到 page cache 中不會(huì)有任何磁盤 IO 發(fā)生。隨后內(nèi)核可以將進(jìn)程的這些多次寫入操作轉(zhuǎn)換為一次磁盤 IO ,將這些寫入的臟頁一次性刷新回磁盤中,這樣就把多次磁盤 IO 轉(zhuǎn)換為一次磁盤 IO 極大地提升文件 IO 的性能。

那么內(nèi)核在什么情況下才會(huì)去觸發(fā) page cache 中的臟頁回寫呢?

  1. 內(nèi)核在初始化的時(shí)候,會(huì)創(chuàng)建一個(gè) timer 定時(shí)器去定時(shí)喚醒內(nèi)核 flusher 線程回寫臟頁。

  2. 當(dāng)內(nèi)存中臟頁的數(shù)量太多了達(dá)到了一定的比例,就會(huì)主動(dòng)喚醒內(nèi)核中的 flusher 線程去回寫臟頁。

  3. 臟頁在內(nèi)存中停留的時(shí)間太久了,等到 flusher 線程下一次被喚醒的時(shí)候就會(huì)回寫這些駐留太久的臟頁。

  4. 用戶進(jìn)程可以通過 sync() 回寫內(nèi)存中的所有臟頁和 fsync() 回寫指定文件的所有臟頁,這些是進(jìn)程主動(dòng)發(fā)起臟頁回寫請求。

  5. 在內(nèi)存比較緊張的情況下,需要回收物理頁或者將物理頁中的內(nèi)容 swap 到磁盤上時(shí),如果發(fā)現(xiàn)通過頁面置換算法置換出來的頁是臟頁,那么就會(huì)觸發(fā)回寫。

現(xiàn)在我們了解了內(nèi)核回寫臟頁的一個(gè)大概時(shí)機(jī),這里大家可能會(huì)問了:

  1. 內(nèi)核通過 timer 定時(shí)喚醒 flush 線程回寫臟頁,那么到底間隔多久喚醒呢?

  2. 內(nèi)存中的臟頁數(shù)量太多會(huì)觸發(fā)回寫,那么這里的太多指的具體是多少呢?

  3. 臟頁在內(nèi)存中駐留太久也會(huì)觸發(fā)回寫,那么這里的太久指的到底是多久呢?

其實(shí)這三個(gè)問題中涉及到的具體數(shù)值,內(nèi)核都提供了參數(shù)供我們來配置。這些參數(shù)的配置文件存在于?proc/sys/vm?目錄下:

圖片
image.png

下面筆者就為大家介紹下內(nèi)核回寫臟頁涉及到的這 6 個(gè)參數(shù),并解答上面我們提出的這三個(gè)問題。

13.1 內(nèi)核中的定時(shí)器間隔多久喚醒 flusher 線程

內(nèi)核中通過 dirty_writeback_centisecs 參數(shù)來配置喚醒 flusher 線程的間隔時(shí)間。

圖片
image.png

該參數(shù)可以通過修改?/proc/sys/vm/dirty_writeback_centisecs?文件來配置參數(shù),我們也可以通過 sysctl 命令或者通過修改?/etc/sysctl.conf?配置文件來對(duì)這些參數(shù)進(jìn)行修改。

這里我們先主要關(guān)注這些內(nèi)核參數(shù)的含義以及源碼實(shí)現(xiàn),文章后面筆者有一個(gè)專門的章節(jié)來介紹這些內(nèi)核參數(shù)各種不同的配置方式。

dirty_writeback_centisecs 內(nèi)核參數(shù)的默認(rèn)值為 500。單位為 0.01 s。也就是說內(nèi)核會(huì)每隔 5s 喚醒一次 flusher 線程來執(zhí)行相關(guān)臟頁的回寫。該參數(shù)在內(nèi)核源碼中對(duì)應(yīng)的變量名為 dirty_writeback_interval。

筆者這里在列舉一個(gè)生活中的例子來解釋下這個(gè) dirty_writeback_interval 的作用。

假設(shè)大家的工作都非常繁忙,于是大家就到家政公司請了專門的保潔阿姨(內(nèi)核 flusher 回寫線程)來幫助我們打掃房間衛(wèi)生(回寫臟頁)。你和保潔阿姨約定每周(dirty_writeback_interval)來你房間(內(nèi)存)打掃一次衛(wèi)生(回寫臟頁),保潔阿姨會(huì)固定每周日按時(shí)來到你房間打掃。記住這個(gè)例子,我們后面還會(huì)用到~~~

13.2 內(nèi)核中如何使用 dirty_writeback_interval 來控制 flusher 喚醒頻率

在磁盤中數(shù)據(jù)是以塊的形式存儲(chǔ)于扇區(qū)中的,前邊在介紹文件讀寫的章節(jié)中,讀寫流程的最后都會(huì)從文件系統(tǒng)層到塊設(shè)備驅(qū)動(dòng)層,由塊設(shè)備驅(qū)動(dòng)程序?qū)?shù)據(jù)寫入對(duì)應(yīng)的磁盤塊中存儲(chǔ)。

內(nèi)存中的文件頁對(duì)應(yīng)于磁盤中的一個(gè)數(shù)據(jù)塊,而這塊磁盤就是我們常說的塊設(shè)備。而每個(gè)塊設(shè)備在內(nèi)核中對(duì)應(yīng)一個(gè) backing_dev_info 結(jié)構(gòu)用于存儲(chǔ)相關(guān)信息。其中最重要的信息是 workqueue_struct *bdi_wq 用于緩存塊設(shè)備上所有的回寫臟頁異步任務(wù)的隊(duì)列。

在系統(tǒng)啟動(dòng)的時(shí)候,內(nèi)核會(huì)調(diào)用 default_bdi_init 來創(chuàng)建 bdi_wq 隊(duì)列和初始化 backing_dev_info。


在 bdi_init 中初始化 backing_dev_info 結(jié)構(gòu)的相關(guān)信息,并在 cgwb_bdi_init 中調(diào)用 wb_init 初始化回寫臟頁任務(wù) bdi_writeback *wb,并創(chuàng)建一個(gè) timer 用于定時(shí)啟動(dòng) flusher 線程。


bdi_writeback 有個(gè)成員變量 struct delayed_work dwork,bdi_writeback 就是把 delayed_work 結(jié)構(gòu)掛到 bdi_wq 隊(duì)列上的。

而 wb_workfn 函數(shù)則是 flusher 線程要執(zhí)行的回寫核心邏輯,全部封裝在 wb_workfn 函數(shù)中。

在 wb_workfn 中會(huì)不斷的循環(huán)執(zhí)行 work_list 中的臟頁回寫任務(wù)。當(dāng)這些回寫任務(wù)執(zhí)行完畢之后調(diào)用 wb_wakeup_delayed 延時(shí)喚醒 flusher線程。大家注意到這里的 dirty_writeback_interval 配置項(xiàng)終于出現(xiàn)了,后續(xù)會(huì)根據(jù) dirty_writeback_interval 計(jì)算下次喚醒 flusher 線程的時(shí)機(jī)。


13.3 臟頁數(shù)量多到什么程度會(huì)主動(dòng)喚醒 flusher 線程

這一節(jié)的內(nèi)容中涉及到四個(gè)內(nèi)核參數(shù)分別是:

drity_background_ratio :當(dāng)臟頁數(shù)量在系統(tǒng)的可用內(nèi)存 available 中占用的比例達(dá)到 drity_background_ratio 的配置值時(shí),內(nèi)核就會(huì)調(diào)用 wakeup_flusher_threads 來喚醒 flusher 線程異步回寫臟頁。默認(rèn)值為:10。表示如果 page cache 中的臟頁數(shù)量達(dá)到系統(tǒng)可用內(nèi)存的 10% 的話,就主動(dòng)喚醒 flusher 線程去回寫臟頁到磁盤。

圖片
image.png

系統(tǒng)的可用內(nèi)存 = 空閑內(nèi)存 + 可回收內(nèi)存??梢酝ㄟ^ free 命令的 available 項(xiàng)查看。

圖片
image.png

dirty_background_bytes :如果 page cache 中臟頁占用的內(nèi)存用量絕對(duì)值達(dá)到指定的 dirty_background_bytes。內(nèi)核就會(huì)調(diào)用 wakeup_flusher_threads 來喚醒 flusher 線程異步回寫臟頁。默認(rèn)為:0。

圖片
image.png

dirty_background_bytes 的優(yōu)先級(jí)大于 drity_background_ratio 的優(yōu)先級(jí)。

dirty_ratio :dirty_background_* 相關(guān)的內(nèi)核配置參數(shù)均是內(nèi)核通過喚醒 flusher 線程來異步回寫臟頁。下面要介紹的 dirty_* 配置參數(shù),均是由用戶進(jìn)程同步回寫臟頁。表示內(nèi)存中的臟頁太多了,用戶進(jìn)程自己都看不下去了,不用等內(nèi)核 flusher 線程喚醒,用戶進(jìn)程自己主動(dòng)去回寫臟頁到磁盤中。當(dāng)臟頁占用系統(tǒng)可用內(nèi)存的比例達(dá)到 dirty_ratio 配置的值時(shí),用戶進(jìn)程同步回寫臟頁。默認(rèn)值為:20 。

圖片
image.png

dirty_bytes :如果 page cache 中臟頁占用的內(nèi)存用量絕對(duì)值達(dá)到指定的 dirty_bytes。用戶進(jìn)程同步回寫臟頁。默認(rèn)值為:0。

*_bytes 相關(guān)配置參數(shù)的優(yōu)先級(jí)要大于 *_ratio 相關(guān)配置參數(shù)。

圖片
image.png

我們繼續(xù)使用上小節(jié)中保潔阿姨的例子說明:

之前你們已經(jīng)約定好了,保潔阿姨會(huì)每周日固定(dirty_writeback_centisecs)來到你的房間打掃衛(wèi)生(臟頁),但是你周三回家的時(shí)候,發(fā)現(xiàn)屋子里太臟了,實(shí)在是臟到一定程度了(drity_background_ratio ,dirty_background_bytes),你實(shí)在是看不去了,這時(shí)你就不會(huì)等這周日(dirty_writeback_centisecs)保潔阿姨過來才打掃,你會(huì)直接給阿姨打電話讓阿姨周三就來打掃一下(內(nèi)核主動(dòng)喚醒 flusher 線程異步回寫臟頁)。

還有一種更極端的情況就是,你的房間已經(jīng)臟到很夸張的程度了(dirty_ratio ,dirty_byte)連你自己都忍不了了,于是你都不用等保潔阿姨了(內(nèi)核 flusher 回寫線程),你自己就乖乖的開始打掃房間衛(wèi)生了。這就是用戶進(jìn)程同步回寫臟頁。

13.4 內(nèi)核如何主動(dòng)喚醒 flusher 線程

通過 《12.5 balance_dirty_pages_ratelimited》小節(jié)的介紹,我們知道在 generic_perform_write 函數(shù)的最后一步會(huì)調(diào)用 balance_dirty_pages_ratelimited 來判斷是否要觸發(fā)臟頁回寫。

這里會(huì)觸發(fā) balance_dirty_pages 函數(shù)進(jìn)行臟頁回寫。


在 balance_dirty_pages 中首先通過 global_dirtyable_memory() 獲取系統(tǒng)當(dāng)前可用內(nèi)存。在 domain_dirty_limits 函數(shù)中根據(jù)前邊我們介紹的 ?*_ratio 或者 *_bytes 相關(guān)內(nèi)核配置計(jì)算臟頁回寫觸發(fā)的閾值。

domain_dirty_limits 函數(shù)會(huì)分別計(jì)算用戶進(jìn)程同步回寫臟頁的相關(guān)閾值 thresh 以及內(nèi)核異步回寫臟頁的相關(guān)閾值 bg_thresh。邏輯比較好懂,筆者將每一步的注釋已經(jīng)為大家標(biāo)注出來了。這里只列出幾個(gè)關(guān)鍵核心點(diǎn):

  • 從源碼中的 if (bytes) {....} else {.....} 分支以及 if (bg_bytes) {....} else {.....} 我們可以看出內(nèi)核配置 *_bytes 相關(guān)的優(yōu)先級(jí)會(huì)高于 *_ratio 相關(guān)配置的優(yōu)先級(jí)。

  • *_bytes 相關(guān)配置我們只會(huì)指定臟頁占用內(nèi)存的 bytes 閾值,但在內(nèi)核實(shí)現(xiàn)中會(huì)將其轉(zhuǎn)換為 頁 為單位。(每頁 4K 大小)。

  • 內(nèi)核中對(duì)于臟頁回寫閾值的判斷是通過 ratio 比例來進(jìn)行判斷的。

  • 內(nèi)核異步回寫的閾值要小于進(jìn)程同步回寫的閾值,如果超過,那么內(nèi)核異步回寫的閾值將會(huì)被設(shè)置為進(jìn)程通過回寫的一半。

如果是異步回寫,內(nèi)核則喚醒 flusher 線程開始異步回寫臟頁,直到臟頁數(shù)量低于閾值或者全部回寫到磁盤。


13.5 臟頁到底在內(nèi)存中能駐留多久

內(nèi)核為了避免 page cache 中的臟頁在內(nèi)存中長久的停留,所以會(huì)給臟頁在內(nèi)存中的駐留時(shí)間設(shè)置一定的期限,這個(gè)期限可由前邊提到的 dirty_expire_centisecs 內(nèi)核參數(shù)配置。默認(rèn)為:3000。單位為:0.01 s。

圖片
image.png

也就是說在默認(rèn)配置下,臟頁在內(nèi)存中的駐留時(shí)間為 30 s。超過 30 s 之后,flusher 線程將會(huì)在下次被喚醒的時(shí)候?qū)⑦@些臟頁回寫到磁盤中。

這些過期的臟頁最終會(huì)在 flusher 線程下一次被喚醒時(shí)候被 flusher 線程回寫到磁盤中。而前邊我們也多次提到過 flusher 線程執(zhí)行邏輯全部封裝在 wb_workfn 函數(shù)中。接下來的調(diào)用鏈為 wb_workfn->wb_do_writeback->wb_writeback。在 wb_writeback 中會(huì)判斷根據(jù) ?dirty_expire_interval 判斷哪些是過期的臟頁。

13.6 臟頁回寫參數(shù)的相關(guān)配置方式

前面的幾個(gè)小節(jié)筆者結(jié)合內(nèi)核源碼實(shí)現(xiàn)為大家介紹了影響內(nèi)核回寫臟頁時(shí)機(jī)的六個(gè)參數(shù)。

內(nèi)核越頻繁的觸發(fā)臟頁回寫,數(shù)據(jù)的安全性就越高,但是同時(shí)系統(tǒng)性能會(huì)消耗很大。所以我們在日常工作中需要結(jié)合數(shù)據(jù)的安全性和 IO 性能綜合考慮這六個(gè)內(nèi)核參數(shù)的配置。

本小節(jié)筆者就為大家介紹一下配置這些內(nèi)核參數(shù)的方式,前面的小節(jié)中也提到過,內(nèi)核提供的這些參數(shù)存在于?proc/sys/vm?目錄下。

圖片
image.png

比如我們直接將要配置的具體數(shù)值寫入對(duì)應(yīng)的配置文件中:

我們還可以使用 sysctl 來對(duì)這些內(nèi)核參數(shù)進(jìn)行配置:

sysctl 命令中定義的這些變量 variable 全部定義在內(nèi)核?kernel/sysctl.c?源文件中。

  • 其中 .procname 定義的就是 sysctl 命令中指定的配置變量名字。

  • .data 定義的是內(nèi)核源碼中引用的變量名字。這在前邊我們介紹內(nèi)核代碼的時(shí)候介紹過了。比如配置參數(shù) dirty_writeback_centisecs 在內(nèi)核源碼中的變量名為 dirty_writeback_interval , dirty_ratio 在內(nèi)核中的變量名為 vm_dirty_ratio。

而前邊介紹的這兩種配置方式全部是臨時(shí)的,我們可以通過編輯 ?/etc/sysctl.conf?文件來永久的修改內(nèi)核相關(guān)的配置。

我們也可以在目錄?/etc/sysctl.d/下創(chuàng)建自定義的配置文件。

在?/etc/sysctl.conf?文件中直接以?variable = value?的形式添加到文件的末尾。

圖片
image.png

最后調(diào)用?sysctl -p /etc/sysctl.conf?使?/etc/sysctl.conf?配置文件中新添加的那些配置生效。

總結(jié)

本文筆者帶大家從 Linux 內(nèi)核的角度詳細(xì)解析了 JDK NIO 文件讀寫在 Buffered IO 以及 Direct IO ?這兩種模式下的內(nèi)核源碼實(shí)現(xiàn),探秘了文件讀寫的本質(zhì)。并對(duì)比了 Buffered IO 和 Direct IO 的不同之處以及各自的適用場景。

在這個(gè)過程中又詳細(xì)地介紹了與 Buffered IO 密切相關(guān)的文件頁高速緩存 page cache 在內(nèi)核中的實(shí)現(xiàn)以及相關(guān)操作。

最后我們詳細(xì)介紹了影響文件 IO 的兩個(gè)關(guān)鍵步驟:文件預(yù)讀和臟頁回寫的詳細(xì)內(nèi)核源碼實(shí)現(xiàn),以及內(nèi)核中影響臟頁回寫時(shí)機(jī)的 6 個(gè)關(guān)鍵內(nèi)核配置參數(shù)相關(guān)的實(shí)現(xiàn)及應(yīng)用。

  • dirty_background_bytes

  • dirty_background_ratio

  • dirty_bytes

  • dirty_ratio

  • dirty_expire_centisecs

  • dirty_writeback_centisecs

以及關(guān)于內(nèi)核參數(shù)的三種配置方式:

  • 通過直接修改?proc/sys/vm?目錄下的相關(guān)參數(shù)配置文件。

  • 使用 sysctl 命令來對(duì)相關(guān)參數(shù)進(jìn)行修改。

  • 通過編輯?/etc/sysctl.conf?文件來永久的修改內(nèi)核相關(guān)配置。

原文作者:bin的技術(shù)小屋



一文教你從Linux內(nèi)核角度探秘JDK NIO文件讀寫本質(zhì)(下)的評(píng)論 (共 條)

分享到微博請遵守國家法律
乌鲁木齐县| 沈阳市| 宁晋县| 保定市| 阿鲁科尔沁旗| 镇原县| 克什克腾旗| 娄烦县| 邵阳市| 张北县| 微博| 昌吉市| 华池县| 龙陵县| 专栏| 偏关县| 陵水| 璧山县| 祁东县| 绥德县| 邵东县| 子洲县| 东方市| 台州市| 华宁县| 安丘市| 澳门| 阿鲁科尔沁旗| 高碑店市| 山丹县| 郯城县| 蛟河市| 息烽县| 阿拉善右旗| 营口市| 锦屏县| 阿城市| 淮北市| 荣昌县| 柳州市| 定襄县|