在這篇計(jì)算機(jī)底層知識(shí)拾遺(五)理解塊IO層 中講了塊緩存buffer cache塊緩存,這篇說(shuō)說(shuō)頁(yè)緩存page cache以及相關(guān)的地址空間address_space的要點(diǎn)。
在Linux 2.4內(nèi)核中塊緩存buffer cache和頁(yè)緩存page cache是并存的,表現(xiàn)的現(xiàn)象是同一份文件的數(shù)據(jù),可能即出現(xiàn)在buffer cache中,又出現(xiàn)在頁(yè)緩存中,這樣就造成了物理內(nèi)存的浪費(fèi)。Linux 2.6內(nèi)核對(duì)兩個(gè)cache進(jìn)行了合并,統(tǒng)一使用頁(yè)緩存在做緩存,只有極少數(shù)的情況下才使用到buffer cache。后面會(huì)說(shuō)buffer cache和page cache的區(qū)別。先直觀看看兩者的容量是如何統(tǒng)計(jì)的。
在 /proc/meminfo中存儲(chǔ)了當(dāng)前系統(tǒng)的內(nèi)存使用情況,比如下面這個(gè)例子,
Buffers表示buffer cache的容量
Cached表示位于物理內(nèi)存中的頁(yè)緩存page cache
SwapCached表示位于磁盤交換區(qū)的頁(yè)緩存page cache
所以實(shí)際頁(yè)緩存page cache的容量 = Cached + SwapCached
buffer cache和page cache的區(qū)別
buffer cache是Unix和早期的Linux內(nèi)核中主要的緩存組件。我們要理解的是不管是buffer cache還是page cache都是為了處理塊設(shè)備和內(nèi)存交互時(shí)高速訪問(wèn)的問(wèn)題
1. buffer cache是面向底層塊設(shè)備的,所以它的粒度是文件系統(tǒng)的塊,塊設(shè)備和系統(tǒng)采用塊進(jìn)行交互。塊再轉(zhuǎn)換成磁盤的基本物理結(jié)構(gòu)扇區(qū)。扇區(qū)的大小是512KB,而文件系統(tǒng)的塊一般是2KB, 4KB, 8KB。扇區(qū)和塊之間是可以快速轉(zhuǎn)換的
隨著內(nèi)核的功能越來(lái)越完善,塊粒度的緩存已經(jīng)不能滿足性能的需要。內(nèi)核的內(nèi)存管理組件采用了比文件系統(tǒng)的塊更高級(jí)別的抽象,頁(yè)page,頁(yè)的大小一般從4KB到2MB,粒度更大,處理的性能更高。所以緩存組件為了和內(nèi)存管理組件更好地交互,創(chuàng)建了頁(yè)緩存page cache來(lái)代替原來(lái)的buffer cache。
頁(yè)緩存是面向文件,面向內(nèi)存的。通過(guò)一系列的數(shù)據(jù)結(jié)構(gòu),比如inode, address_space, page,將一個(gè)文件映射到頁(yè)的級(jí)別,通過(guò)page + offset就可以定位到一個(gè)文件的具體位置
2. buffer cache實(shí)際操作時(shí)按塊為基本單位,page cache操作時(shí)按頁(yè)為基本單位,新建了一個(gè)BIO的抽象,可以同時(shí)處理多個(gè)非連續(xù)的頁(yè)的IO操作,也就是所謂的scatter/gather IO
3. buffer cache目前主要用在需要按塊傳輸?shù)膱?chǎng)景下,比如超級(jí)塊的讀寫等。而page cache可以用在所有以文件為單元的場(chǎng)景下,比如網(wǎng)絡(luò)文件系統(tǒng)等等,緩存組件抽象了地址空間address_space這個(gè)概念來(lái)作為文件系統(tǒng)和頁(yè)緩存的中間適配器,屏蔽了底層設(shè)備的細(xì)節(jié)
4. buffer cache可以和page cache集成在一起,屬于一個(gè)page的塊緩存使用buffer_head鏈表的方式組織,page_cache維護(hù)了一個(gè)private指針指向這個(gè)buffer_head鏈表,buffer_head鏈表維護(hù)了一個(gè)指針指向這個(gè)頁(yè)page。這樣只需要在頁(yè)緩存中存儲(chǔ)一份數(shù)據(jù)即可
5. 文件系統(tǒng)的inode實(shí)際維護(hù)了這個(gè)文件所有的塊block的塊號(hào),通過(guò)對(duì)文件偏移量offset取模可以很快定位到這個(gè)偏移量所在的文件系統(tǒng)的塊號(hào),磁盤的扇區(qū)號(hào)。同樣,通過(guò)對(duì)文件偏移量offset進(jìn)行取??梢杂?jì)算出偏移量所在的頁(yè)的偏移量,地址空間address_space通過(guò)指針可以方便的獲取兩端inode和page的信息,所以可以很方便地定位到一個(gè)文件的offset在各個(gè)組件中的位置:
文件字節(jié)偏移量 --> 頁(yè)偏移量 --> 文件系統(tǒng)塊號(hào) block --> 磁盤扇區(qū)號(hào)
頁(yè)緩存page cache和地址空間address_space
上面比較page cache和buffer cache的時(shí)候基本把page cache的特點(diǎn)說(shuō)了,它是面向內(nèi)存,面向文件的。這正好說(shuō)明了頁(yè)緩存的作用,它位于內(nèi)存和文件之間,文件IO操作實(shí)際上只和頁(yè)緩存交互,不直接和內(nèi)存交互。
Linux內(nèi)核使用page數(shù)據(jù)結(jié)構(gòu)來(lái)描述物理內(nèi)存頁(yè)幀,內(nèi)核創(chuàng)建了mem_map數(shù)組來(lái)表示所有的物理頁(yè)幀,mem_map的數(shù)組項(xiàng)就是page。
page結(jié)構(gòu)不僅表示了物理內(nèi)存頁(yè)幀,
1. 一些標(biāo)志位flags來(lái)表示該頁(yè)是否是臟頁(yè),是否正在被寫回等等
2. _count, _mapcount表示這個(gè)頁(yè)被多少個(gè)進(jìn)程使用和映射
3. private指針指向了這個(gè)頁(yè)對(duì)應(yīng)的buffer cache的buffer_head鏈表,建立了頁(yè)緩存和塊緩存的聯(lián)系
4. mapping指向了地址空間address_space,表示這個(gè)頁(yè)是一個(gè)頁(yè)緩存中頁(yè),和一個(gè)文件的地址空間對(duì)應(yīng)
5. index是這個(gè)頁(yè)在文件中的頁(yè)偏移量,通過(guò)文件的字節(jié)偏移量可以計(jì)算出文件的頁(yè)偏移量
頁(yè)緩存實(shí)際上就是采用了一個(gè)基數(shù)樹結(jié)構(gòu)將一個(gè)文件的內(nèi)容組織起來(lái)存放在物理內(nèi)存page中。文件IO操作直接和頁(yè)緩存交互。采用緩存原理來(lái)管理塊設(shè)備的IO操作
一個(gè)文件inode對(duì)應(yīng)一個(gè)地址空間address_space。而一個(gè)address_space對(duì)應(yīng)一個(gè)頁(yè)緩存基數(shù)樹。這幾個(gè)組件的關(guān)系如下
再看一下地址空間address_space的概念。address_space是Linux內(nèi)核中的一個(gè)關(guān)鍵抽象,它是頁(yè)緩存和外部設(shè)備中文件系統(tǒng)的橋梁,可以說(shuō)關(guān)聯(lián)了內(nèi)存系統(tǒng)和文件系統(tǒng),文件系統(tǒng)可以理解成數(shù)據(jù)源。
1. inode指向這個(gè)地址空間的宿主,也就是數(shù)據(jù)源
2. page_tree指向了這個(gè)地址空間對(duì)應(yīng)的頁(yè)緩存的基數(shù)樹。這樣就可以通過(guò)inode --> address_space --> page_tree找打一個(gè)文件對(duì)應(yīng)的頁(yè)緩存頁(yè)
讀文件時(shí),首先通過(guò)要讀取的文件內(nèi)容的偏移量offset計(jì)算出要讀取的頁(yè),然后通過(guò)該文件的inode找到這個(gè)文件對(duì)應(yīng)的地址空間address_space,然后在address_space中訪問(wèn)該文件的頁(yè)緩存,如果頁(yè)緩存命中,那么直接返回文件內(nèi)容,如果頁(yè)緩存缺失,那么產(chǎn)生一個(gè)頁(yè)缺失異常,創(chuàng)業(yè)一個(gè)頁(yè)緩存頁(yè),然后從磁盤中讀取相應(yīng)文件的頁(yè)填充該緩存頁(yè),租后從頁(yè)缺失異常中恢復(fù),繼續(xù)往下讀。
寫文件時(shí),首先通過(guò)所寫內(nèi)容在文件中的偏移量計(jì)算出相應(yīng)的頁(yè),然后還是通過(guò)inode找到address_space,通過(guò)address_space找到頁(yè)緩存中頁(yè),如果頁(yè)緩存命中,直接把文件內(nèi)容修改更新在頁(yè)緩存的頁(yè)中。寫文件就結(jié)束了。這時(shí)候文件修改位于頁(yè)緩存,并沒(méi)有寫回writeback到磁盤文件中去。
一個(gè)頁(yè)緩存中的頁(yè)如果被修改,那么會(huì)被標(biāo)記成臟頁(yè)。臟頁(yè)需要寫回到磁盤中的文件塊。有兩種方式可以把臟頁(yè)寫回磁盤,也就是flush。
1. 手動(dòng)調(diào)用sync()或者fsync()系統(tǒng)調(diào)用把臟頁(yè)寫回
2. pdflush進(jìn)程會(huì)定時(shí)把臟頁(yè)寫回到磁盤
臟頁(yè)不能被置換出內(nèi)存,如果臟頁(yè)正在被寫回,那么會(huì)被設(shè)置寫回標(biāo)記,這時(shí)候該頁(yè)就被上鎖,其他寫請(qǐng)求被阻塞直到鎖釋放
在某些情況下我們可能需要繞過(guò)頁(yè)緩存機(jī)制,比如系統(tǒng)存在大日志的情況,比如數(shù)據(jù)庫(kù)系統(tǒng),日志不會(huì)被經(jīng)常重復(fù)讀取,如果都緩存在內(nèi)存中會(huì)影響系統(tǒng)的性能。內(nèi)核提供了直接IO的方式,O_DIRECT,可以繞過(guò)頁(yè)緩存,直接把文件內(nèi)容從堆中寫到磁盤文件。
關(guān)于文件IO我們常說(shuō)兩句話“普通文件IO需要復(fù)制兩次,內(nèi)存映射文件mmap復(fù)制一次”,"普通文件IO是堆內(nèi)操作,內(nèi)存映射文件是堆外操作"。我們來(lái)看一下這兩句話。
對(duì)于普通文件需要復(fù)制兩次,我們要理解到底是哪兩次,大部分的書都沒(méi)說(shuō)清楚,只說(shuō)是第一次復(fù)制是從磁盤到內(nèi)存緩沖區(qū),第二次是從內(nèi)存緩沖區(qū)到進(jìn)程的堆。這里的內(nèi)存緩沖區(qū)實(shí)際上就是頁(yè)緩存。
這篇文件Page Cache, the Affair Between Memory and Files中的幾張圖很形象,說(shuō)明白了這中間實(shí)際發(fā)生底層操作。
加入一個(gè)進(jìn)程render要讀取一個(gè)scene.dat文件,實(shí)際發(fā)生的步驟如下
1. render進(jìn)程向內(nèi)核發(fā)起讀scene.dat文件的請(qǐng)求
2. 內(nèi)核根據(jù)scene.dat的inode找到對(duì)應(yīng)的address_space,在address_space中查找頁(yè)緩存,如果沒(méi)有找到,那么分配一個(gè)內(nèi)存頁(yè)page加入到頁(yè)緩存
3. 從磁盤中讀取scene.dat文件相應(yīng)的頁(yè)填充頁(yè)緩存中的頁(yè),也就是第一次復(fù)制
4. 從頁(yè)緩存的頁(yè)復(fù)制內(nèi)容到render進(jìn)程的堆空間的內(nèi)存中,也就是第二次復(fù)制
最后物理內(nèi)存的內(nèi)容是這樣的,同一個(gè)文件scene.dat的內(nèi)容存在了兩份拷貝,一份是頁(yè)緩存,一份是用戶進(jìn)程的堆空間對(duì)應(yīng)的物理內(nèi)存空間
再來(lái)看看內(nèi)存映射文件mmap只復(fù)制一次是如何做的,mmap只有一次頁(yè)緩存的復(fù)制,從磁盤文件復(fù)制到也緩存中。
mmap會(huì)創(chuàng)建一個(gè)虛擬內(nèi)存區(qū)域vm_area_struct,進(jìn)程的task_struct維護(hù)著這個(gè)進(jìn)程所有的虛擬內(nèi)存區(qū)域信息,虛擬內(nèi)存區(qū)域會(huì)更新相應(yīng)的進(jìn)程頁(yè)表項(xiàng),讓這些頁(yè)表項(xiàng)直接指向頁(yè)緩存所在的物理頁(yè)page。mmap新建的這個(gè)虛擬內(nèi)存區(qū)域和進(jìn)程堆的虛擬內(nèi)存區(qū)域不是同一個(gè),所以mmap是在堆外空間。
最后明確幾個(gè)概念
1. 用戶進(jìn)程訪問(wèn)內(nèi)存只能通過(guò)頁(yè)表結(jié)構(gòu),內(nèi)核可以通過(guò)虛擬地址直接訪問(wèn)物理內(nèi)存。
2. 用戶進(jìn)程不能訪問(wèn)內(nèi)核的地址空間,這里的地址空間指的是虛擬地址空間,這是肯定的,因?yàn)橛脩暨M(jìn)程的虛擬地址空間和內(nèi)核的虛擬地址空間是不重合的,內(nèi)核虛擬地址空間必須特權(quán)訪問(wèn)
3. page結(jié)構(gòu)表示物理內(nèi)存頁(yè)幀,同一個(gè)物理內(nèi)存地址可以同時(shí)被內(nèi)核進(jìn)程和用戶進(jìn)程訪問(wèn),只要將用戶進(jìn)程的頁(yè)表項(xiàng)也指向這個(gè)物理內(nèi)存地址。也就是mmap的實(shí)現(xiàn)原理。
參考資料:
《Page Cache, the Affair Between Memory and Files》
《Linux Kernel: What is the major difference between the buffer cache and the page cache?》
《深入Linux內(nèi)核架構(gòu)》
聯(lián)系客服