自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

頁面緩存、內(nèi)存和文件之間的那些事

系統(tǒng) Linux Windows
上一篇文章中我們學(xué)習(xí)了內(nèi)核怎么為一個用戶進(jìn)程 管理虛擬內(nèi)存,而沒有提及文件和 I/O。這一篇文章我們將專門去講這個重要的主題 —— 頁面緩存。文件和內(nèi)存之間的關(guān)系常常很不好去理解,而它們對系統(tǒng)性能的影響卻是非常大的。

[[225278]]

上一篇文章中我們學(xué)習(xí)了內(nèi)核怎么為一個用戶進(jìn)程 管理虛擬內(nèi)存,而沒有提及文件和 I/O。這一篇文章我們將專門去講這個重要的主題 —— 頁面緩存。文件和內(nèi)存之間的關(guān)系常常很不好去理解,而它們對系統(tǒng)性能的影響卻是非常大的。

在面對文件時,有兩個很重要的問題需要操作系統(tǒng)去解決。***個是相對內(nèi)存而言,慢的讓人發(fā)狂的硬盤驅(qū)動器,尤其是磁盤尋道。第二個是需要將文件內(nèi)容一次性地加載到物理內(nèi)存中,以便程序間共享文件內(nèi)容。如果你在 Windows 中使用 進(jìn)程瀏覽器 去查看它的進(jìn)程,你將會看到每個進(jìn)程中加載了大約 ~15MB 的公共 DLL。我的 Windows 機(jī)器上現(xiàn)在大約運(yùn)行著 100 個進(jìn)程,因此,如果不共享的話,僅這些公共的 DLL 就要使用高達(dá) ~1.5 GB 的物理內(nèi)存。如果是那樣的話,那就太糟糕了。同樣的,幾乎所有的 Linux 進(jìn)程都需要 ld.so 和 libc,加上其它的公共庫,它們占用的內(nèi)存數(shù)量也不是一個小數(shù)目。

幸運(yùn)的是,這兩個問題都用一個辦法解決了:頁面緩存 —— 保存在內(nèi)存中的頁面大小的文件塊。為了用圖去說明頁面緩存,我捏造出一個名為 render 的 Linux 程序,它打開了文件 scene.dat,并且一次讀取 512 字節(jié),并將文件內(nèi)容存儲到一個分配到堆中的塊上。***次讀取的過程如下:

Reading and the page cache

Reading and the page cache

  1. render 請求 scene.dat 從位移 0 開始的 512 字節(jié)。
  2. 內(nèi)核搜尋頁面緩存中 scene.dat 的 4kb 塊,以滿足該請求。假設(shè)該數(shù)據(jù)沒有緩存。
  3. 內(nèi)核分配頁面幀,初始化 I/O 請求,將 scend.dat 從位移 0 開始的 4kb 復(fù)制到分配的頁面幀。
  4. 內(nèi)核從頁面緩存復(fù)制請求的 512 字節(jié)到用戶緩沖區(qū),系統(tǒng)調(diào)用 read() 結(jié)束。

讀取完 12KB 的文件內(nèi)容以后,render 程序的堆和相關(guān)的頁面幀如下圖所示:

Non-mapped file read

Non-mapped file read

它看起來很簡單,其實(shí)這一過程做了很多的事情。首先,雖然這個程序使用了普通的讀?。?code>read)調(diào)用,但是,已經(jīng)有三個 4KB 的頁面幀將文件 scene.dat 的一部分內(nèi)容保存在了頁面緩存中。雖然有時讓人覺得很驚奇,但是,普通的文件 I/O 就是這樣通過頁面緩存來進(jìn)行的。在 x86 架構(gòu)的 Linux 中,內(nèi)核將文件認(rèn)為是一系列的 4KB 大小的塊。如果你從文件中讀取單個字節(jié),包含這個字節(jié)的整個 4KB 塊將被從磁盤中讀入到頁面緩存中。這是可以理解的,因?yàn)榇疟P通常是持續(xù)吞吐的,并且程序一般也不會從磁盤區(qū)域僅僅讀取幾個字節(jié)。頁面緩存知道文件中的每個 4KB 塊的位置,在上圖中用 #0#1 等等來描述。Windows 使用 256KB 大小的視圖view,類似于 Linux 的頁面緩存中的頁面page。

不幸的是,在一個普通的文件讀取中,內(nèi)核必須拷貝頁面緩存中的內(nèi)容到用戶緩沖區(qū)中,它不僅花費(fèi) CPU 時間和影響 CPU 緩存,在復(fù)制數(shù)據(jù)時也浪費(fèi)物理內(nèi)存。如前面的圖示,scene.dat 的內(nèi)存被存儲了兩次,并且,程序中的每個實(shí)例都用另外的時間去存儲內(nèi)容。我們雖然解決了從磁盤中讀取文件緩慢的問題,但是在其它的方面帶來了更痛苦的問題。內(nèi)存映射文件是解決這種痛苦的一個方法:

Mapped file read

Mapped file read

當(dāng)你使用文件映射時,內(nèi)核直接在頁面緩存上映射你的程序的虛擬頁面。這樣可以顯著提升性能:Windows 系統(tǒng)編程 報告指出,在相關(guān)的普通文件讀取上運(yùn)行時性能提升多達(dá) 30% ,在 Unix 環(huán)境中的高級編程 的報告中,文件映射在 Linux 和 Solaris 也有類似的效果。這取決于你的應(yīng)用程序類型的不同,通過使用文件映射,可以節(jié)約大量的物理內(nèi)存。

對高性能的追求是永恒不變的目標(biāo),測量是很重要的事情,內(nèi)存映射應(yīng)該是程序員始終要使用的工具。這個 API 提供了非常好用的實(shí)現(xiàn)方式,它允許你在內(nèi)存中按字節(jié)去訪問一個文件,而不需要為了這種好處而犧牲代碼可讀性。在一個類 Unix 的系統(tǒng)中,可以使用 mmap 查看你的 地址空間,在 Windows 中,可以使用 CreateFileMapping,或者在高級編程語言中還有更多的可用封裝。當(dāng)你映射一個文件內(nèi)容時,它并不是一次性將全部內(nèi)容都映射到內(nèi)存中,而是通過 頁面故障 來按需映射的。在 獲取 需要的文件內(nèi)容的頁面幀后,頁面故障句柄 映射你的虛擬頁面 到頁面緩存上。如果一開始文件內(nèi)容沒有緩存,這還將涉及到磁盤 I/O。

現(xiàn)在出現(xiàn)一個突發(fā)的狀況,假設(shè)我們的 render 程序的***一個實(shí)例退出了。在頁面緩存中保存著 scene.dat 內(nèi)容的頁面要立刻釋放掉嗎?人們通常會如此考慮,但是,那樣做并不是個好主意。你應(yīng)該想到,我們經(jīng)常在一個程序中創(chuàng)建一個文件,退出程序,然后,在第二個程序去使用這個文件。頁面緩存正好可以處理這種情況。如果考慮更多的情況,內(nèi)核為什么要清除頁面緩存的內(nèi)容?請記住,磁盤讀取的速度要慢于內(nèi)存 5 個數(shù)量級,因此,***一個頁面緩存是一件有非常大收益的事情。因此,只要有足夠大的物理內(nèi)存,緩存就應(yīng)該保持全滿。并且,這一原則適用于所有的進(jìn)程。如果你現(xiàn)在運(yùn)行 render 一周后, scene.dat 的內(nèi)容還在緩存中,那么應(yīng)該恭喜你!這就是什么內(nèi)核緩存越來越大,直至達(dá)到***限制的原因。它并不是因?yàn)椴僮飨到y(tǒng)設(shè)計的太“垃圾”而浪費(fèi)你的內(nèi)存,其實(shí)這是一個非常好的行為,因?yàn)椋尫盼锢韮?nèi)存才是一種“浪費(fèi)”。(LCTT 譯注:釋放物理內(nèi)存會導(dǎo)致頁面緩存被清除,下次運(yùn)行程序需要的相關(guān)數(shù)據(jù),需要再次從磁盤上進(jìn)行讀取,會“浪費(fèi)” CPU 和 I/O 資源)***的做法是盡可能多的使用緩存。

由于頁面緩存架構(gòu)的原因,當(dāng)程序調(diào)用 write() 時,字節(jié)只是被簡單地拷貝到頁面緩存中,并將這個頁面標(biāo)記為“臟”頁面。磁盤 I/O 通常并不會立即發(fā)生,因此,你的程序并不會被阻塞在等待磁盤寫入上。副作用是,如果這時候發(fā)生了電腦死機(jī),你的寫入將不會完成,因此,對于至關(guān)重要的文件,像數(shù)據(jù)庫事務(wù)日志,要求必須進(jìn)行 fsync()(仍然還需要去擔(dān)心磁盤控制器的緩存失敗問題),另一方面,讀取將被你的程序阻塞,直到數(shù)據(jù)可用為止。內(nèi)核采取預(yù)加載的方式來緩解這個矛盾,它一般提前預(yù)讀取幾個頁面并將它加載到頁面緩存中,以備你后來的讀取。在你計劃進(jìn)行一個順序或者隨機(jī)讀取時(請查看 madvise()readahead()、Windows 緩存提示 ),你可以通過提示hint幫助內(nèi)核去調(diào)整這個預(yù)加載行為。Linux 會對內(nèi)存映射的文件進(jìn)行 預(yù)讀取,但是我不確定 Windows 的行為。當(dāng)然,在 Linux 中它可能會使用 O_DIRECT 跳過預(yù)讀取,或者,在 Windows 中使用 NO_BUFFERING 去跳過預(yù)讀,一些數(shù)據(jù)庫軟件就經(jīng)常這么做。

一個文件映射可以是私有的,也可以是共享的。當(dāng)然,這只是針對內(nèi)存中內(nèi)容的更新而言:在一個私有的內(nèi)存映射上,更新并不會提交到磁盤或者被其它進(jìn)程可見,然而,共享的內(nèi)存映射,則正好相反,它的任何更新都會提交到磁盤上,并且對其它的進(jìn)程可見。內(nèi)核使用寫時復(fù)制copy on write(CoW)機(jī)制,這是通過頁面表?xiàng)l目page table entry(PTE)來實(shí)現(xiàn)這種私有的映射。在下面的例子中,render 和另一個被稱為 render3d 的程序都私有映射到 scene.dat 上。然后 render 去寫入映射的文件的虛擬內(nèi)存區(qū)域:

The Copy-On-Write mechanism

The Copy-On-Write mechanism

  1. 兩個程序私有地映射 scene.dat,內(nèi)核誤導(dǎo)它們并將它們映射到頁面緩存,但是使該頁面表?xiàng)l目只讀。
  2. render 試圖寫入到映射 scene.dat 的虛擬頁面,處理器發(fā)生頁面故障。
  3. 內(nèi)核分配頁面幀,復(fù)制 scene.dat 的第二塊內(nèi)容到其中,并映射故障的頁面到新的頁面幀。
  4. 繼續(xù)執(zhí)行。程序就當(dāng)做什么都沒發(fā)生。

上面展示的只讀頁面表?xiàng)l目并不意味著映射是只讀的,它只是內(nèi)核的一個用于共享物理內(nèi)存的技巧,直到盡可能的***一刻之前。你可以認(rèn)為“私有”一詞用的有點(diǎn)不太恰當(dāng),你只需要記住,這個“私有”僅用于更新的情況。這種設(shè)計的重要性在于,要想看到被映射的文件的變化,其它程序只能讀取它的虛擬頁面。一旦“寫時復(fù)制”發(fā)生,從其它地方是看不到這種變化的。但是,內(nèi)核并不能保證這種行為,因?yàn)樗窃? x86 中實(shí)現(xiàn)的,從 API 的角度來看,這是有意義的。相比之下,一個共享的映射只是將它簡單地映射到頁面緩存上。更新會被所有的進(jìn)程看到并被寫入到磁盤上。最終,如果上面的映射是只讀的,頁面故障將觸發(fā)一個內(nèi)存段失敗而不是寫到一個副本。

動態(tài)加載庫是通過文件映射融入到你的程序的地址空間中的。這沒有什么可奇怪的,它通過普通的 API 為你提供與私有文件映射相同的效果。下面的示例展示了映射文件的 render 程序的兩個實(shí)例運(yùn)行的地址空間的一部分,以及物理內(nèi)存,嘗試將我們看到的許多概念綜合到一起。

Mapping virtual memory to physical memory

Mapping virtual memory to physical memory

這是內(nèi)存架構(gòu)系列的第三部分的結(jié)論。我希望這個系列文章對你有幫助,對理解操作系統(tǒng)的這些主題提供一個很好的思維模型。 

責(zé)任編輯:龐桂玉 來源: Linux中國
相關(guān)推薦

2019-07-15 15:37:31

頁面緩存內(nèi)存

2022-07-08 15:09:26

Linux

2021-05-17 08:18:35

Java內(nèi)存模型JMM

2018-03-01 15:03:11

2019-11-19 14:48:00

Kafka文件存儲

2014-06-06 16:08:17

初志科技

2011-09-19 15:40:35

2020-07-29 08:14:59

云計算云遷移IT

2013-03-12 10:19:20

計算機(jī)內(nèi)存芯片數(shù)據(jù)

2012-05-31 09:53:38

IT風(fēng)云15年

2011-05-19 16:47:50

軟件測試

2012-05-01 08:06:49

手機(jī)

2015-08-20 09:17:36

Java線程池

2021-03-18 16:05:20

SSD存儲故障

2021-08-11 21:46:47

MySQL索引join

2009-02-19 10:21:00

路由多WAN口

2015-09-14 09:28:47

2017-03-08 08:53:44

Git命令 GitHub

2012-10-08 11:55:05

2015-08-13 10:54:46

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號