Linux操作系統面試——虛擬內存45問
你是否好奇,為何在老舊設備上運行大型程序時,系統不會瞬間崩潰,而是勉力維持?為何多任務并行時,各個進程看似都能擁有充足內存空間?這背后,Linux 虛擬內存技術功不可沒。它讓每個進程都仿佛擁有一塊獨立且足夠大的內存空間,不必擔憂實際物理內存的局促與復雜布局。想象一下,你有一間堆滿書籍的小書房,空間有限,但又想存放更多知識。虛擬內存就像一個智能書架系統,它能巧妙規劃,將常用書籍擺在觸手可及之處,不常用的暫時存放到地下室,需要時再快速調取。
Linux 虛擬內存正是如此,通過頁表這一 “翻譯官”,將進程使用的虛擬地址精準轉換為實際物理地址,實現內存地址的靈活轉換與管理。當進程訪問的數據不在物理內存中,系統便會觸發缺頁異常,如同從地下室取書,從磁盤交換區或文件系統中加載相應數據到內存,更新頁表后,進程便能順利訪問。今天,就讓我們一同深入 Linux 虛擬內存的神秘世界,從原理、機制到實踐應用,全方位解析這一解決物理內存限制的關鍵技術,探尋它如何在有限資源下,為程序運行構建出無限可能的舞臺。
Part1.虛擬內存是什么?
1.1虛擬內存概述
虛擬內存,簡單來說,是一種內存管理技術,它讓操作系統能將硬盤空間當作額外的內存來使用。你可以把它想象成一個 “內存擴充器”,當計算機的物理內存(也就是我們常說的內存條提供的內存)不夠用時,虛擬內存就會把暫時用不到的數據從物理內存轉移到硬盤上的特定區域,這個區域就像是內存的 “倉庫”,我們稱之為交換空間(swap space) 。當程序需要這些數據時,再從交換空間把數據調回到物理內存。
圖片
打個比方,物理內存就像是你辦公桌上的桌面,空間有限,只能放一些當下馬上要處理的文件。而虛擬內存則像是辦公室里的文件柜,當桌面上堆滿了文件,你就可以把一些暫時不需要的文件放到文件柜里。當你需要這些文件時,再從文件柜中取出來放回桌面。這樣,即使你的桌面空間有限,也能處理更多的文件,就像計算機即使物理內存有限,也能運行更多的程序。
對于進程來說,虛擬內存提供了獨立的地址空間,每個進程都認為自己獨占了系統的所有內存資源,這樣可以避免不同進程之間的內存沖突,提高了系統的穩定性和安全性。就好比每個租客都覺得自己租下了一整套房子,有獨立的空間放置自己的物品,不用擔心和其他租客的物品混在一起。
1.2為什么需要使用虛擬內存
進程需要使用的代碼和數據都放在內存中,比放在外存中要快很多。問題是內存空間太小了,不能滿足進程的需求,而且現在都是多進程,情況更加糟糕。所以提出了虛擬內存,使得每個進程用于3G的獨立用戶內存空間和共享的1G內核內存空間。(每個進程都有自己的頁表,才使得3G用戶空間的獨立)這樣進程運行的速度必然很快了。而且虛擬內存機制還解決了內存碎片和內存不連續的問題。為什么可以在有限的物理內存上達到這樣的效果呢?
例如:對于程序計數器位數為32位的處理器來說,他的地址發生器所能發出的地址數目為2^32=4G個,于是這個處理器所能訪問的最大內存空間就是4G。在計算機技術中,這個值就叫做處理器的尋址空間或尋址能力。
照理說,為了充分利用處理器的尋址空間,就應按照處理器的最大尋址來為其分配系統的內存。如果處理器具有32位程序計數器,那么就應該按照下圖的方式,為其配備4G的內存:
圖片
這樣,處理器所發出的每一個地址都會有一個真實的物理存儲單元與之對應;同時,每一個物理存儲單元都有唯一的地址與之對應。這顯然是一種最理想的情況。
但遺憾的是,實際上計算機所配置內存的實際空間常常小于處理器的尋址范圍,這是就會因處理器的一部分尋址空間沒有對應的物理存儲單元,從而導致處理器尋址能力的浪費。例如:如下圖的系統中,具有32位尋址能力的處理器只配置了256M的內存儲器,這就會造成大量的浪費:
圖片
另外,還有一些處理器因外部地址線的根數小于處理器程序計數器的位數,而使地址總線的根數不滿足處理器的尋址范圍,從而處理器的其余尋址能力也就被浪費了。例如:Intel8086處理器的程序計數器位32位,而處理器芯片的外部地址總線只有20根,所以它所能配置的最大內存為1MB:
圖片
在實際的應用中,如果需要運行的應用程序比較小,所需內存容量小于計算機實際所配置的內存空間,自然不會出什么問題。但是,目前很多的應用程序都比較大,計算機實際所配置的內存空間無法滿足。
實踐和研究都證明:一個應用程序總是逐段被運行的,而且在一段時間內會穩定運行在某一段程序里。
這也就出現了一個方法:如下圖所示,把要運行的那一段程序自輔存復制到內存中來運行,而其他暫時不運行的程序段就讓它仍然留在輔存。
圖片
當需要執行另一端尚未在內存的程序段(如程序段2),如下圖所示,就可以把內存中程序段1的副本復制回輔存,在內存騰出必要的空間后,再把輔存中的程序段2復制到內存空間來執行即可:
圖片
在計算機技術中,把內存中的程序段復制回輔存的做法叫做“換出”,而把輔存中程序段映射到內存的做法叫做“換入”。經過不斷有目的的換入和換出,處理器就可以運行一個大于實際物理內存的應用程序了。或者說,處理器似乎是擁有了一個大于實際物理內存的內存空間。于是,這個存儲空間叫做虛擬內存空間,而把真正的內存叫做實際物理內存,或簡稱為物理內存。
那么對于一臺真實的計算機來說,它的虛擬內存空間又有多大呢?計算機虛擬內存空間的大小是由程序計數器的尋址能力來決定的。例如:在程序計數器的位數為32的處理器中,它的虛擬內存空間就為4GB。
可見,如果一個系統采用了虛擬內存技術,那么它就存在著兩個內存空間:虛擬內存空間和物理內存空間。虛擬內存空間中的地址叫做“虛擬地址”;而實際物理內存空間中的地址叫做“實際物理地址”或“物理地址”。處理器運算器和應用程序設計人員看到的只是虛擬內存空間和虛擬地址,而處理器片外的地址總線看到的只是物理地址空間和物理地址。
由于存在兩個內存地址,因此一個應用程序從編寫到被執行,需要進行兩次映射。第一次是映射到虛擬內存空間,第二次時映射到物理內存空間。在計算機系統中,第兩次映射的工作是由硬件和軟件共同來完成的。承擔這個任務的硬件部分叫做存儲管理單元MMU,軟件部分就是操作系統的內存管理模塊了。
在映射工作中,為了記錄程序段占用物理內存的情況,操作系統的內存管理模塊需要建立一個表格,該表格以虛擬地址為索引,記錄了程序段所占用的物理內存的物理地址。這個虛擬地址/物理地址記錄表便是存儲管理單元MMU把虛擬地址轉化為實際物理地址的依據,記錄表與存儲管理單元MMU的作用如下圖所示:
圖片
綜上所述,虛擬內存技術的實現,是建立在應用程序可以分成段,并且具有“在任何時候正在使用的信息總是所有存儲信息的一小部分”的局部特性基礎上的。它是通過用輔存空間模擬RAM來實現的一種使機器的作業地址空間大于實際內存的技術。
從處理器運算裝置和程序設計人員的角度來看,它面對的是一個用MMU、映射記錄表和物理內存封裝起來的一個虛擬內存空間,這個存儲空間的大小取決于處理器程序計數器的尋址空間。
可見,程序映射表是實現虛擬內存的技術關鍵,它可給系統帶來如下特點:
- 系統中每一個程序各自都有一個大小與處理器尋址空間相等的虛擬內存空間;
- 在一個具體時刻,處理器只能使用其中一個程序的映射記錄表,因此它只看到多個程序虛存空間中的一個,這樣就保證了各個程序的虛存空間時互不相擾、各自獨立的;
- 使用程序映射表可方便地實現物理內存的共享。
Part2.虛擬內存與物理內存
2.1兩者區別
物理內存是實實在在插在計算機主板內存槽上的內存條所提供的內存,是計算機硬件的一部分,由半導體芯片組成 ,CPU 可以直接進行尋址,用于存放正在運行的程序和臨時的數據。一旦電腦關閉或重啟,物理內存中的內容就會丟失。它的容量是固定的,取決于你安裝的內存條數量和容量大小。例如,你的電腦安裝了兩根 8GB 的內存條,那么物理內存就是 16GB。
而虛擬內存是一種內存管理技術,并不是真實的物理硬件。它通過將硬盤空間的一部分模擬為內存來擴展物理內存的容量,是操作系統為了擴大可用內存而創造的一種 “模擬” 擴展 。當物理內存不足時,操作系統會自動利用虛擬內存來存儲暫時不需要立即訪問的數據,把部分長期不用的數據從物理內存移動到硬盤上的虛擬內存區域(也就是交換空間) 。虛擬內存對于那些需要大量數據但仍受限于物理內存的應用特別有用,但其讀取速度遠低于物理內存,因為硬盤的讀寫速度要比內存慢得多。
2.2相互關系
在系統運行時,虛擬內存和物理內存緊密協作。當一個程序啟動時,操作系統會為其分配虛擬內存空間,程序在運行過程中訪問的都是虛擬地址。當程序需要訪問某個數據時,首先會通過虛擬地址去查找。CPU 會將虛擬地址發送給內存管理單元(MMU) ,MMU 通過查詢頁表(一種記錄虛擬地址和物理地址映射關系的數據結構),將虛擬地址轉換為對應的物理地址。如果所需的數據就在物理內存中,那么 CPU 就可以直接從物理內存中讀取數據,這就好比你在辦公桌上能直接找到需要的文件,速度很快。
但如果數據不在物理內存中,而是在虛擬內存(硬盤的交換空間)里,就會發生缺頁異常。這時,操作系統會從物理內存中選擇一個暫時不用的頁面(如果物理內存已滿的話),將其數據寫回到硬盤的交換空間,然后把程序需要的數據從交換空間讀取到物理內存中,并更新頁表中的映射關系。之后,CPU 就可以通過新的物理地址訪問數據了。這個過程就像是你在辦公桌上找不到文件,需要去文件柜(虛擬內存)里找,找到后把文件拿出來放在桌面上(物理內存),方便下次使用 。
可以說,虛擬內存是物理內存的補充和延伸,它們共同為程序的運行提供內存支持,使得計算機系統能夠更高效地運行多個程序,處理各種復雜的任務。
Part3.虛擬內存技術
3.1分頁機制
分頁是虛擬內存管理中的一種重要機制,它將內存空間劃分為固定大小的塊,這些塊就被稱為頁(page) 。在 Linux 系統中,常見的頁大小是 4KB(2^12 字節),不過在某些架構下,也支持如 64KB 或 2MB 的大頁(Huge Pages) 。之所以采用固定大小的頁,是為了簡化內存管理和提高內存分配的效率。
圖片
頁表(Page Table)是分頁機制的核心數據結構,它就像是一本 “地址字典”,記錄了虛擬頁與物理頁之間的映射關系 。每個進程都擁有自己獨立的頁表,當進程訪問內存時,CPU 會將虛擬地址發送給內存管理單元(MMU) ,MMU 通過查詢頁表,把虛擬地址轉換為對應的物理地址。例如,在 x86_64 架構中,Linux 使用四級頁表結構,分別為頁全局目錄(PGD)、頁上級目錄(PUD)、頁中間目錄(PMD)和頁表項(PTE) 。當 CPU 接收到一個虛擬地址時,首先會根據虛擬地址的最高幾位在 PGD 中找到對應的 PUD;然后依據虛擬地址的次高幾位在 PUD 中找到對應的 PMD;接著根據虛擬地址的再次高幾位在 PMD 中找到對應的 PTE;最后,PTE 中記錄了該虛擬頁對應的物理頁框地址,從而實現了虛擬地址到物理地址的轉換。
以一個簡單的例子來說明,假設我們有一個進程需要訪問虛擬地址 0x12345678。在四級頁表結構下,MMU 會先提取虛擬地址的高幾位(比如高 9 位,具體位數根據架構和頁表設計而定),通過這幾位索引 PGD,找到對應的 PUD;再從虛擬地址中提取接下來的幾位(同樣根據設計而定),在 PUD 中找到對應的 PMD;然后繼續提取相應位在 PMD 中找到 PTE;最終,PTE 中保存了物理頁框地址,再結合虛擬地址中剩下的偏移部分,就可以得到實際的物理地址,進而訪問到所需的數據 。
3.2分段機制
分段機制是另一種內存管理方式,它將程序的地址空間劃分為多個邏輯段,比如代碼段(存放程序的指令)、數據段(存放程序的全局變量和靜態變量等)、堆棧段(存放函數調用時的局部變量、返回地址等) 。每個段都有自己的起始地址和長度,通過段寄存器來標識和訪問。
圖片
與分頁機制不同,分頁是將內存劃分為固定大小的頁,主要目的是為了實現虛擬內存和內存管理的高效性;而分段是基于程序的邏輯結構進行劃分,更注重程序的模塊化和保護 。例如,代碼段可以設置為只讀,防止程序運行時被意外修改;數據段可以根據需要設置讀寫權限,以保證數據的安全性。
分頁機制消除了外部碎片,因為內存空間是預先劃分好的,頁與頁之間是緊密排列的。但分頁機制可能產生內部碎片,即當分配的頁面大小大于實際需要的內存大小時,剩余的空間將被浪費。
現代操作系統一般都采用段頁式存儲的方式來實現虛擬內存和物理內存的映射。段頁式存儲,顧名思義是一種結合了段式存儲管理和頁式存儲管理優點的內存管理技術。在段頁式存儲中,程序的邏輯地址空間被劃分為若干個段,每個段再被劃分為若干個固定大小的頁。同時,物理內存也被劃分為與頁面大小相同的物理塊。
圖片
- 當程序需要訪問某個邏輯地址時,首先根據段號找到對應的段表項。
- 從段表項中獲取該段的頁表起始地址,并根據段內頁號找到對應的頁表項。
- 從頁表項中獲取該頁對應的物理塊號。
- 最后,將物理塊號與頁內偏移量組合,得到物理地址,從而完成地址映射。
在 Linux 內核中,雖然 x86 架構支持分段機制,但 Linux 對分段機制的使用進行了簡化和弱化 。在 32 位的 x86 架構中,Linux 通常只使用了兩個段:一個是用戶數據段,用于存放用戶進程的數據;另一個是用戶代碼段,用于存放用戶進程的代碼。對于內核空間,也類似地使用兩個段。這種簡化的分段方式,使得 Linux 的內存管理更加簡單高效,同時也借助分頁機制來實現更強大的內存管理功能 。
虛擬內存區域(VMA)是 Linux 內核中用于管理進程虛擬內存的一種數據結構 。每個進程的虛擬地址空間被劃分為多個 VMA,每個 VMA 對應一個連續的虛擬地址范圍,并且具有相同的訪問權限和屬性 。例如,一個進程的代碼段、數據段、堆棧段等都可以分別對應一個 VMA。分段機制與 VMA 的關系在于,VMA 可以看作是對分段概念的一種擴展和細化,它更靈活地管理進程的虛擬內存,并且與分頁機制相結合,共同實現了 Linux 高效的內存管理 。
3.3內存對齊
內存對齊是指數據在內存中存儲時,按照一定的規則排列,使得數據的起始地址是特定值的倍數 。這個特定值通常是數據類型大小的倍數,比如在 32 位系統中,int 類型通常占 4 字節,那么 int 類型數據的起始地址通常會被對齊到 4 的倍數;在 64 位系統中,指針類型通常占 8 字節,指針數據的起始地址會被對齊到 8 的倍數 。
基本變量類型所占大小如下圖,不同的系統的區別在于long和point類型的大小:
圖片
內存對齊的重要性主要體現在以下幾個方面:一是提高訪問速度,現代處理器在訪問內存時,通常是以一定的塊大小(如 4 字節、8 字節等)進行讀取的。如果數據是對齊的,處理器可以一次讀取到完整的數據,而不需要進行額外的處理。相反,如果數據未對齊,可能需要多次讀取內存,并進行數據拼接,這會大大降低訪問速度 。二是硬件要求,某些處理器架構對數據的對齊有嚴格要求,如果數據未對齊,可能會導致硬件異常或性能下降 。三是優化存儲空間,合理的內存對齊可以減少內存碎片的產生,提高內存的利用率 。例如,在結構體中,如果各個成員按照其自身的對齊要求進行排列,可以減少結構體整體的大小,節省內存空間 。
Part4.頁面置換算法
當物理內存已滿,而又需要加載新的頁面時,操作系統就需要決定將哪個頁面從內存中置換出去,這就用到了頁面置換算法 。頁面置換算法的目標是盡可能減少缺頁中斷的次數,提高系統性能。
圖片
4.1 LRU(最近最少使用)算法
LRU 算法的核心思想是:如果一個頁面在最近一段時間內沒有被訪問,那么在未來它被訪問的概率也較低 ,所以當內存已滿需要置換頁面時,就選擇最近最少使用的頁面淘汰出去 。
假設內存中最多能容納 3 個頁面,頁面訪問序列為 1, 2, 3, 4, 2, 1, 5, 6, 2, 1, 2, 3, 7, 6, 3, 2, 1, 2, 3, 6 。
- 最初,內存為空,依次訪問頁面 1, 2, 3,此時內存中頁面為 1, 2, 3。
- 當訪問頁面 4 時,內存已滿,需要置換頁面。根據 LRU 算法,最近最少使用的頁面是 1(因為 1 最早進入內存且之后未被訪問),所以將 1 置換出去,內存中頁面變為 4, 2, 3 。
- 接著訪問頁面 2,2 在內存中,不需要置換,更新 2 的訪問時間,使其成為最近使用的頁面,內存中頁面順序變為 2, 4, 3 。
- 訪問頁面 1 時,內存中沒有 1,需要置換頁面。此時最近最少使用的是 3,將 3 置換出去,把 1 放入內存,內存中頁面變為 2, 4, 1 。
- 以此類推,隨著頁面的不斷訪問,LRU 算法會根據頁面的使用情況動態地置換頁面,以保證內存中始終是最近最常使用的頁面 。
在實際實現 LRU 算法時,常用的數據結構是雙向鏈表和哈希表 。雙向鏈表用于維護頁面的訪問順序,鏈表頭部是最近使用的頁面,鏈表尾部是最近最少使用的頁面 。哈希表用于快速查找某個頁面是否在內存中以及獲取其在雙向鏈表中的位置 。當訪問一個頁面時,如果頁面在內存中,通過哈希表找到其在鏈表中的位置,將其移動到鏈表頭部;如果頁面不在內存中,先從鏈表尾部刪除最近最少使用的頁面(同時更新哈希表),再將新頁面插入到鏈表頭部并更新哈希表 。
4.2其他常見算法
FIFO(先進先出)算法:這種算法非常直觀,它按照頁面進入內存的先后順序進行置換 。即最早進入內存的頁面最先被置換出去 。還是以上面的頁面訪問序列為例,最初內存為空,依次放入1, 2, 3 。當訪問 4 時,由于內存已滿,根據 FIFO 算法,最早進入的 1 被置換出去,內存變為 2, 3, 4 。FIFO 算法的優點是實現簡單,但它沒有考慮頁面的使用頻率,可能會把一些仍然被頻繁訪問的頁面置換出去,導致缺頁率較高 。例如,如果有一個程序需要頻繁訪問最早進入內存的某個頁面,FIFO 算法就會不斷地將其置換出去又換進來,增加了系統開銷 。
LFU(最不經常使用)算法:LFU 算法根據頁面的訪問頻率來決定置換哪個頁面 。它為每個頁面設置一個計數器,每當頁面被訪問時,計數器加 1 。當內存已滿需要置換頁面時,選擇計數器值最小(即訪問頻率最低)的頁面淘汰 。假設內存中已有頁面 1, 2, 3,它們的訪問次數分別為 3, 2, 1 。當需要置換頁面時,LFU 算法會選擇訪問次數為 1 的頁面 3 進行置換 。LFU 算法能較好地反映頁面的實際使用情況,但它需要維護每個頁面的訪問次數,實現相對復雜一些 。
Part5.虛擬內存與進程
5.1進程的虛擬地址空間布局
圖片
在 Linux 系統中,每個進程都擁有自己獨立的虛擬地址空間,就像每個租客都有自己獨立的房間,互不干擾 。以 32 位系統為例,這個虛擬地址空間的大小為 4GB(2^32 字節),它被劃分為不同的區域,每個區域都有特定的用途和特點 :
- 代碼段(Text Segment):這是程序的只讀部分,存放著程序的機器指令(也就是我們編寫的代碼被編譯后的二進制形式)和只讀數據,如字符串常量 。它的特點是只讀,這就像是一本被鎖起來的書,只能讀取內容,不能修改,這樣可以防止程序在運行時意外修改自身的代碼,保證了程序執行的穩定性和安全性 。每個進程只有一個代碼段,并且在內存中是共享的,例如多個進程同時運行同一個可執行文件,它們共享的就是同一段代碼段 。
- 數據段(Data Segment):用于存放已初始化的全局變量和靜態變量 。這些變量在程序編譯時就已經確定了初始值,并且在程序運行期間一直存在 。數據段屬于靜態內存分配,一旦程序加載到內存中,數據段的大小就基本固定下來了 。比如在C語言中定義的int global_variable = 10;,這個global_variable就存放在數據段中 。
- BSS 段(Block Started by Symbol Segment):主要存放未初始化的全局變量和靜態變量 。與數據段不同,BSS 段在可執行文件中并不占用實際的磁盤空間,只是記錄了這些變量所需的空間大小 。在程序開始執行前,系統會自動將 BSS 段中的變量初始化為 0 。例如,在 C 語言中定義的int uninitialized_global_variable;,它就位于 BSS 段 。BSS 段屬于靜態內存分配,它的存在可以節省可執行文件的大小,因為不需要為未初始化的變量在磁盤上存儲初始值 。
- 堆(Heap Segment):是進程運行時動態分配內存的區域,通過malloc、calloc、realloc等函數進行內存分配,使用free函數釋放內存 。堆的大小是動態變化的,可以根據程序的需求進行擴張或縮減 。它從低地址向高地址增長,就像一個可以不斷向上堆疊物品的貨架 。在 C語言中,使用malloc函數分配內存時,例如int *p = (int *)malloc(10 * sizeof(int));,這 10 個int類型大小的內存空間就是從堆中分配出來的 。不過,如果在使用堆內存時,忘記釋放不再使用的內存,就會導致內存泄漏 。
- 棧(Stack Segment):用于存放函數的局部變量、函數調用的參數、返回地址等信息 。它是一種后進先出(LIFO,Last In First Out)的數據結構,就像一個放盤子的棧,最后放上去的盤子最先被拿走 。棧由操作系統自動管理,當函數被調用時,相關的局部變量和參數等會被壓入棧中;函數執行結束后,這些數據會從棧中彈出,自動釋放內存 。棧的大小通常是固定的,在Linux系統中,一般默認的棧大小為 8MB 。如果在函數中定義了過多的局部變量或者遞歸調用層數過深,導致棧空間不夠用,就會發生棧溢出(Stack Overflow)錯誤 。
- 文件映射段(Memory - Mapped Segment):用于映射文件、共享內存、動態鏈接庫等 。通過mmap系統調用可以將文件的一部分或全部映射到進程的虛擬地址空間中,使得進程可以像訪問內存一樣訪問文件,提高了文件 I/O 的效率 。同時,共享內存也利用了這個區域,多個進程可以通過映射同一個共享內存區域來實現數據共享和進程間通信 。動態鏈接庫在加載時也會被映射到這個區域,實現代碼和數據的共享 。
5.2進程內存分配
進程在運行時,經常需要動態分配內存來存儲各種數據 。在 C 語言中,最常用的內存分配函數就是 malloc 。當我們調用 malloc 函數時,它會在堆上為我們分配一塊指定大小的內存空間 。例如, int *p = (malloc(10 * sizeof(int))); 這行代碼就會在堆上分配 10 個 int 類型大小的內存空間,并返回一個指向這塊內存起始地址的指針p 。
那么,malloc函數是如何在堆上分配內存的呢?實際上,malloc并不是直接與操作系統的內存管理機制打交道,而是通過 glibc(GNU C Library)來實現的 。在 glibc 中,維護了一個內存池,當我們調用malloc時,它首先會在內存池中查找是否有足夠的空閑內存來滿足請求 。如果內存池中有足夠的空閑內存,就直接從內存池中分配內存,并返回相應的指針 。這樣可以減少系統調用的開銷,提高內存分配的效率 。因為系統調用涉及到用戶態和內核態的切換,這種切換會帶來一定的性能損耗 。
然而,如果內存池中的空閑內存不足以滿足請求,malloc函數就會借助系統調用與操作系統進行交互 。在 Linux 系統中,主要涉及到兩個系統調用:brk和mmap 。
brk 系統調用:brk系統調用通過移動程序數據段的結束地址(也就是 “堆頂” 指針)來增加堆的大小,從而分配新的內存 。例如,假設當前堆的大小為 100 字節,當調用brk函數并傳入一個大于當前堆頂地址的值,如 150 字節時,堆就會擴展到 150 字節,新增加的 50 字節內存就可以用于分配 。brk分配的內存是連續的,適合小塊內存的頻繁分配和釋放 。但是,由于brk分配的內存是基于堆的連續擴展,如果頻繁地分配和釋放小塊內存,可能會導致堆內存碎片化,即堆中出現很多不連續的空閑小內存塊,這些小內存塊可能無法滿足后續較大內存塊的分配請求 。例如,先分配了一個 10 字節的內存塊,再釋放它,然后又分配一個 20 字節的內存塊,這樣在堆中就可能會產生一個 10 字節的空閑小內存塊,而如果后續需要分配一個 30 字節的內存塊,由于這個 10 字節的空閑塊無法滿足需求,且與其他空閑塊不連續,就可能導致分配失敗 。
mmap 系統調用:mmap系統調用則是通過在文件映射區域分配一塊內存來滿足請求 。它可以將文件的全部或部分內容映射到進程的虛擬內存中,進程可以像訪問內存一樣讀寫文件的內容,而不需要顯式地進行文件 I/O 操作 。同時,mmap也可以創建匿名映射,即不與任何文件關聯的內存映射,用于在進程間共享內存或作為大塊內存的分配器 。通常情況下,當請求的內存大小小于一定閾值(在大多數系統中,這個閾值通常為 128KB)時,malloc函數會優先使用brk系統調用來分配內存;當請求的內存大小大于這個閾值時,則會使用mmap系統調用 。這是因為mmap分配內存的開銷相對較大,對于小塊內存的分配不太劃算,而對于大塊內存的分配,mmap可以避免堆內存碎片化的問題,并且能更好地管理和釋放內存 。例如,當需要分配一個 1MB 的內存塊時,使用mmap可以直接在文件映射區域分配一塊連續的 1MB 內存,而如果使用brk,可能需要多次擴展堆,并且容易導致堆內存碎片化 。
Part6.交換空間
6.1交換空間的作用
交換空間(swap space)在虛擬內存中扮演著至關重要的角色,它就像是一個 “內存儲備倉庫” 。當系統的物理內存(RAM)不足以滿足所有正在運行的進程和應用程序的內存需求時,交換空間就會發揮作用 。
具體來說,Linux 內核會將那些暫時不活躍(也就是很長時間沒有被訪問)的內存頁(pages)從物理內存轉移到交換空間(通常是硬盤上的特定區域) ,這個過程被稱為 “換出”(swapping out)或 “頁面置換”(paging out) 。通過這種方式,物理內存中就騰出了空間,可供那些更活躍、更急需內存的進程使用 。
例如,當你同時打開了多個大型應用程序,如瀏覽器、視頻編輯軟件、音樂播放器等,物理內存可能會被迅速耗盡 。此時,系統會將一些暫時不需要訪問的內存數據,比如音樂播放器當前沒有播放的音頻數據、瀏覽器中暫時未顯示的網頁緩存數據等,轉移到交換空間中 。這樣,其他更需要內存的操作,如視頻編輯軟件的實時預覽、瀏覽器加載新的網頁,就可以在有限的物理內存中順利進行 。
而當進程需要訪問已經被交換到磁盤上的內存頁時,就會發生 “缺頁中斷”(page fault) 。這時,操作系統會從交換空間中把相應的內存頁讀取回物理內存,這個過程被稱為 “換入”(swapping in)或 “頁面調入”(paging in) 。
雖然交換空間能夠在物理內存不足時,提供額外的內存支持,防止系統因內存耗盡而崩潰,讓系統仍然可以繼續運行 。但是,由于硬盤的讀寫速度遠遠低于內存的讀寫速度,頻繁地使用交換空間會導致系統性能明顯下降 。所以,交換空間只是物理內存的一種補充手段,理想情況下,系統應該有足夠的物理內存,盡量減少對交換空間的依賴 。
6.2交換空間的類型
交換空間主要有兩種類型:交換分區和交換文件,它們各自有不同的特點。
①交換分區:交換分區是硬盤上專門劃分出來用于交換空間的獨立分區,在系統安裝過程中就可以進行設置 。比如在安裝 Linux 系統時,通過分區工具(如 fdisk、parted 等)將硬盤的一部分空間指定為交換分區,其分區類型一般為 “Linux swap” 。它獨立于系統的主文件系統運行,就像一個獨立的小倉庫,專門用來存放從物理內存中換出的內存頁 。
- 優點:交換分區的效率相對較高,因為它在硬盤上是連續的空間,沒有文件系統的額外開銷 。而且在安裝階段創建時,通常會被放置在硬盤驅動器的較快區域(更靠近外邊緣),這使得數據的訪問和寫入速度更快 。同時,它與主文件系統分開,能有效防止碎片化,減少對系統文件的干擾 ,就像一個獨立的小房間,不會和其他雜物混在一起 。
- 缺點:一旦創建,交換分區的大小就相對固定,如果想要更改其大小,就需要對磁盤進行重新分區 。這是一個比較復雜且有風險的操作,可能會導致數據丟失或系統故障 ,就好比你要擴大一個房間的面積,需要對整個房子的結構進行大改造,很容易出問題 。另外,如果交換分區設置得過大,而實際使用量很少,就會浪費磁盤空間;反之,如果設置得過小,在內存需求高峰期可能無法滿足系統的需求,限制系統性能 。
②交換文件:交換文件是在系統現有文件系統中的一種特殊文件,其作用和交換分區相同 。可以通過命令(如dd命令創建文件,再用mkswap命令將其設置為交換文件)在需要時創建 。例如,使用dd if=/dev/zero of=/swapfile bs=1024 count=8192命令創建一個大小約為 8MB 的交換文件/swapfile ,然后使用mkswap /swapfile將其初始化為交換文件 。
- 優點:交換文件具有很高的靈活性 。它可以根據系統的實際需求隨時調整大小、刪除或移動 。比如,當系統內存需求突然增加時,可以增大交換文件的大小;當內存需求減少時,又可以減小或刪除交換文件,這使得它非常適合內存需求不斷變化的系統 ,就像一個可以隨時調整大小的收納箱 。此外,交換文件使用現有文件系統中的空間,在不使用時不會浪費磁盤空間,并且可以根據內存需求動態增長 。
- 缺點:由于交換文件存在于文件系統中,文件系統的管理和維護會帶來一些額外的開銷,而且可能會產生碎片化問題 。在傳統的文件系統中,交換文件的性能通常比交換分區慢 。不過,隨著現代文件系統(如 ext4、Btrfs 等)的發展,這些問題得到了很大程度的緩解,現在交換文件和交換分區的性能差異已經不是很明顯 。但在高負載情況下,大量的交換操作仍可能對文件系統的正常文件操作產生干擾 。
6.3 Swappiness 參數
Swappiness 是 Linux 操作系統中一個非常重要的參數,它控制著內核將內存頁交換到磁盤(也就是使用交換空間)的傾向程度 。其取值范圍是 0 - 100,代表的是一個百分比 。
當 Swappiness 的值為 0 時,意味著內核盡可能地避免使用交換空間,即使物理內存非常緊張,也會優先嘗試其他方式來滿足內存需求,比如回收緩存等 。而當 Swappiness 的值為 100 時,則表示內核總是傾向于使用交換空間,即使物理內存還有較多的空閑空間,也可能會將內存頁交換到磁盤 。Swappiness 參數的調整對于系統性能有著直接且顯著的影響 。
在系統內存緊張時,合理設置 Swappiness 值可以幫助優化系統的反應速度和整體性能 。例如,對于高負載的數據庫服務器,由于數據庫操作對內存的讀寫速度要求極高,頻繁的磁盤 I/O 操作會嚴重影響性能,因此通常會將 Swappiness 值設置得很低(如 10 - 20) ,以減少交換的使用頻率,避免因頻繁訪問交換空間(磁盤)導致的性能瓶頸 。而在一些內存較大且對內存使用不太敏感的系統中,適當提高 Swappiness 值(如設置為 30 - 50) ,可以有效利用系統資源,將一些暫時不用的內存頁交換到磁盤,減少因內存資源閑置而造成的浪費 。
調整方法:查看當前系統的 Swappiness 值,可以使用命令cat /proc/sys/vm/swappiness 。如果想要臨時修改 Swappiness 值(重啟后失效) ,可以使用sysctl命令,例如將 Swappiness 值臨時設置為 10 ,命令為sysctl vm.swappiness=10 。如果希望永久修改 Swappiness 值,則需要編輯/etc/sysctl.conf文件,在文件中添加或修改vm.swappiness = 10這一行,然后執行sysctl -p使修改生效 。在調整 Swappiness 參數時,需要謹慎操作,因為不合適的設置可能會帶來一些問題 。
如果設置得過小,雖然減少了交換空間的使用,但可能會導致物理內存被過度使用,當物理內存耗盡時,系統可能會觸發 “內存不足(OOM,Out - Of - Memory)” 殺手機制,強制殺掉一些進程來釋放內存,這可能會影響系統的正常運行 。而如果設置得過大,系統會頻繁地進行內存頁的交換操作,由于磁盤 I/O 速度遠低于內存速度,會導致系統性能大幅下降,用戶會明顯感覺到系統變得卡頓 。所以,在調整 Swappiness 參數之前,需要對系統的內存使用情況、應用程序的特點等進行充分的了解和分析,并且在調整后密切監控系統的性能指標,如內存使用率、磁盤 I/O 情況、系統響應時間等,以確保調整后的參數能夠使系統達到最佳的性能狀態 。
Part7.內存管理單元(MMU)
7.1 MMU的功能
內存管理單元(Memory Management Unit,MMU)是計算機硬件中負責處理中央處理器(CPU)內存訪問請求的關鍵組件 ,在虛擬內存管理中扮演著核心角色。
圖片
它的首要功能是地址轉換,這是實現虛擬內存機制的基礎。在現代操作系統中,每個進程都擁有自己獨立的虛擬地址空間,程序在運行時使用的是虛擬地址 。而 MMU 的職責就是將這些虛擬地址轉換為實際的物理地址,以便 CPU 能夠正確訪問內存中的數據 。例如,在 x86 架構的計算機中,當一個進程嘗試訪問虛擬地址 0x12345678 時,MMU 會通過查詢頁表(Page Table),將這個虛擬地址映射到對應的物理地址上,如 0x87654321 ,從而實現進程對內存的訪問 。
MMU 還承擔著內存保護的重要任務 。它通過硬件機制來確保進程只能訪問被授權的內存區域,防止進程間的非法內存訪問 。比如,MMU 可以為每個內存頁面設置訪問權限,如只讀、讀寫、執行等 。當一個進程試圖以不被允許的方式訪問內存時,MMU 會觸發異常,通知操作系統進行處理 。假設一個進程嘗試寫入一個被設置為只讀的內存頁面,MMU 就會檢測到這個非法操作,并產生一個內存訪問錯誤異常,操作系統可以根據這個異常來采取相應的措施,如終止該進程,以保護系統的穩定性和安全性 。這種內存保護機制對于多任務操作系統來說至關重要,它使得多個進程能夠在同一臺計算機上安全、穩定地運行,避免了因一個進程的錯誤而導致整個系統崩潰的情況 。
mmu開啟以后會有以下特點:
- 多個程序獨立運行
- 虛擬地址是連續的(物理內存可以有碎片)
- 允許操作系統管理內存
下圖顯示的系統說明了內存的虛擬和物理視圖。單個系統中的不同處理器和設備可能具有不同的虛擬地址映射和物理地址映射。操作系統編寫程序,使MMU在這兩個內存視圖之間進行轉換:
圖片
要做到這一點,虛擬內存系統中的硬件必須提供地址轉換,即將處理器發出的虛擬地址轉換為主內存中的物理地址。MMU使用虛擬地址中最重要的位來索引轉換表中的條目,并確定正在訪問哪個塊。MMU將代碼和數據的虛擬地址轉換為實際系統中的物理地址。該轉換將在硬件中自動執行,并且對應用程序是透明的。除了地址轉換之外,MMU還可以控制每個內存區域的內存訪問權限、內存順序和緩存策略。
MMU對執行的任務或應用程序可以不了解系統的物理內存映射,也可以不了解同時運行的其他程序。每個程序可以使用相同的虛擬內存地址空間。即使物理內存是碎片化的,還可以使用一個連續的虛擬內存映射。此虛擬地址空間與系統中內存的實際物理映射分開的。應用程序被編寫、編譯和鏈接,以在虛擬內存空間中運行。
圖片
如上圖所示,TLB是MMU中最近訪問的頁面翻譯的緩存。對于處理器執行的每個內存訪問,MMU將檢查轉換是否緩存在TLB中。如果所請求的地址轉換在TLB中導致命中,則該地址的翻譯立即可用。TLB本質是一塊高速緩存。數據cache緩存地址(虛擬地址或者物理地址)和數據。TLB緩存虛擬地址和其映射的物理地址。TLB根據虛擬地址查找cache,它沒得選,只能根據虛擬地址查找。所以TLB是一個虛擬高速緩存。
每個TLB entry通常不僅包含物理地址和虛擬地址,還包含諸如內存類型、緩存策略、訪問權限、地址空間ID(ASID)和虛擬機ID(VMID)等屬性。如果TLB不包含處理器發出的虛擬地址的有效轉換,稱為TLB Miss,則將執行外部轉換頁表查找。MMU內的專用硬件使它能夠讀取內存中的轉換表。
然后,如果翻譯頁表沒有導致頁面故障,則可以將新加載的翻譯緩存在TLB中,以便進行后續的重用。簡單概括一下就是:硬件存在TLB后,虛擬地址到物理地址的轉換過程發生了變化。虛擬地址首先發往TLB確認是否命中cache,如果cache hit直接可以得到物理地址。否則,一級一級查找頁表獲取物理地址。并將虛擬地址和物理地址的映射關系緩存到TLB中。
如果操作系統修改了可能已經緩存在TLB中的轉換的entry,那么操作系統就有責任使這些未更新的TLB entry invaild。當執行A64代碼時,有一個TLBI,它是一個TLB無效的指令:
TLBI <type><level>{IS} {, <Xt>}TLB可以保存固定數量的entry。可以通過由轉換頁表遍歷引起的外部內存訪問次數和獲得高TLB命中率來獲得最佳性能。ARMv8-A體系結構提供了一個被稱為連續塊entry的特性,以有效地利用TLB空間。轉換表每個entry都包含一個連續的位。當設置時,這個位向TLB發出信號,表明它可以緩存一個覆蓋多個塊轉換的單個entry。查找可以索引到連續塊所覆蓋的地址范圍中的任何位置。因此,TLB可以為已定義的地址范圍緩存一個entry從而可以在TLB中存儲更大范圍的虛擬地址。
7.2 MMU 與 TLB(轉譯后備緩沖器)
轉譯后備緩沖器(Translation Lookaside Buffer,TLB)是 MMU 中的一個高速緩存 ,它在虛擬內存管理中與 MMU 協同工作,極大地提高了地址轉換的效率 。
TLB 中存儲了最近使用的頁表項(Page Table Entry,PTE) ,這些頁表項記錄了虛擬地址到物理地址的映射關系 。當 CPU 需要進行地址轉換時,MMU 首先會在 TLB 中查找對應的虛擬地址 。如果在 TLB 中找到了匹配的頁表項(即 TLB 命中) ,MMU 就可以直接獲取到對應的物理地址,而無需訪問內存中的頁表 ,這大大縮短了地址轉換的時間 。例如,假設 CPU 需要訪問虛擬地址 0x12345678,MMU 會先在 TLB 中查找這個虛擬地址 。如果 TLB 中已經緩存了該虛擬地址對應的頁表項,MMU 就能立即得到物理地址,直接從內存中讀取數據,整個過程非常快速 。
然而,如果在 TLB 中沒有找到匹配的頁表項(即 TLB 未命中) ,MMU 就需要從內存中的頁表中讀取相應的頁表項 。這個過程相對較慢,因為內存訪問的速度遠低于 TLB 的訪問速度 。在從內存中讀取到頁表項后,MMU 會將其存入 TLB 中,以便下次訪問相同的虛擬地址時能夠快速命中 。假設在上述例子中,TLB 未命中,MMU 就會訪問內存中的頁表,找到虛擬地址 0x12345678 對應的物理地址 。然后,MMU 會把這個頁表項存入 TLB 中,當下次 CPU 再次訪問這個虛擬地址時,就可以在 TLB 中快速找到對應的物理地址,提高了地址轉換的效率 。
LB的原理如下:
- 當CPU訪問一個虛擬地址時,首先檢查TLB中是否有對應的頁表項。
- 如果TLB中有對應的頁表項(即命中),則直接從TLB獲取物理地址。
- 如果TLB中沒有對應的頁表項(即未命中),則需要訪問內存來獲取正確的頁表項。
- 在未命中情況下,操作系統會進行相應處理,從主存中獲取正確的頁表項,并將其加載到TLB中以供后續使用。
- 一旦正確的頁表項加載到TLB中,CPU再次訪問相同虛擬地址時就可以直接在TLB中找到映射關系,提高了轉換效率。
TLB具有快速查找和高效緩存機制,能夠極大地減少查詢頁表所需的時間。然而,由于TLB是有限容量的,在大型程序或多任務環境下可能無法完全覆蓋所有需要轉換的頁面。當發生TLB未命中時,則會導致額外的內存訪問開銷;操作系統會負責管理和維護TLB,包括緩存策略、TLB的刷新機制等。常見的緩存策略有全相聯、組相聯和直接映射等。可以說,TLB 就像是 MMU 的 “高速助手”,通過緩存常用的頁表項,減少了 MMU 訪問內存中頁表的次數,從而顯著提高了虛擬地址到物理地址的轉換速度,進而提升了整個系統的性能 。
Part8.內存相關工具與命令
8.1查看內存使用情況的命令
①free:這是一個非常基礎且常用的命令,用于快速查看系統內存的使用情況,它的輸出結果是對/proc/meminfo文件信息的一個簡潔概述 。執行free命令后,會得到如下格式的輸出(以字節為單位):
total used free shared buff/cache available
Mem: 16355940352 2766014464 12694421504 126959616 909540432 13069271040
Swap: 2097147904 53391360 2043756544其中,total表示總內存大小;used表示已使用的內存大小;free表示空閑內存大小;shared表示共享內存大小;buff/cache表示緩沖區和緩存所占用的內存大小;available表示系統可用于分配給新進程的內存大小 。通過這些信息,我們可以快速了解系統內存的整體使用狀況,判斷是否存在內存不足或內存使用不合理的情況 。如果used接近或超過total,且available較小,可能意味著系統內存緊張,需要進一步排查和優化 。
②top:是一個動態顯示系統資源使用情況的命令,類似于 Windows 系統中的任務管理器 。它不僅能實時展示內存使用情況,還能查看 CPU 使用率、每個進程的資源占用等詳細信息 。在終端中輸入top命令后,會進入一個交互式界面,不斷實時更新系統狀態 。界面的主要部分包括:
top - 14:20:12 up 2 days, 1:23, 2 users, load average: 0.00, 0.01, 0.05
Tasks: 152 total, 1 running, 151 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.3 us, 0.3 sy, 0.0 ni, 99.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 8161844 total, 7433924 free, 149744 used, 578176 buff/cache
KiB Swap: 2097148 total, 2097148 free, 0 used. 7663360 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 12844 7448 4564 S 0.0 0.1 0:02.33 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd- 系統狀態信息:如系統當前時間、系統已運行時間、當前登錄用戶數、系統負載(1 分鐘、5 分鐘、15 分鐘的平均負載) 。例如,top - 15:32:45 up 2 days, 3:12, 1 user, load average: 0.05, 0.03, 0.01,這里顯示了系統在 15:32:45 時的狀態,已運行 2 天 3 小時 12 分鐘,有 1 個用戶登錄,1 分鐘、5 分鐘、15 分鐘的平均負載分別為 0.05、0.03、0.01 。
- 進程狀態信息:展示所有進程的總數、正在運行的進程數、睡眠的進程數、停止的進程數和僵尸進程數 。比如,Tasks: 200 total, 2 running, 198 sleeping, 0 stopped, 0 zombie,表示共有 200 個進程,2 個正在運行,198 個處于睡眠狀態,沒有停止和僵尸進程 。
- CPU 使用情況:顯示用戶空間(us)、內核空間(sy)、改變過優先級的進程(ni)、空閑(id)、等待 I/O(wa)、硬中斷(hi)、軟中斷(si)、虛擬 CPU 等待實際 CPU(st)等各部分占用 CPU 的百分比 。例如,%Cpu(s): 0.5 us, 0.3 sy, 0.0 ni, 99.1 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st,表示用戶空間占用 CPU 0.5%,內核空間占用 0.3%,空閑 CPU 為 99.1% 等 。
- 內存使用情況:呈現物理內存(Mem)和交換內存(Swap)的總量、已使用量、空閑量等 。例如,KiB Mem :16355940 total, 12694421 free, 2766014 used, 909540 buff/cache,KiB Swap:2097148 total, 2043756 free, 53391 used 。在top命令的交互界面中,還可以通過一些按鍵操作進行更多的功能設置 。比如按M鍵可以根據駐留內存大小對進程進行排序,方便快速找到占用內存較大的進程;按P鍵可以根據 CPU 使用百分比對進程排序;按1鍵可以顯示各個邏輯 CPU 的使用情況等 。
③htop:是top命令的增強版本,提供了更直觀、更豐富的信息展示 。它以彩色界面顯示,并且支持鼠標操作,使得用戶交互更加便捷 。在htop界面中,不僅可以看到每個進程的內存實時使用率,還能詳細了解每個進程的常駐內存大小(RES)、程序總內存大小(VIRT)、共享庫大小(SHR)等信息 。與top相比,htop的進程列表可以水平及垂直滾動,方便查看更多進程的詳細信息 。例如,在處理大量進程的服務器環境中,htop能夠更輕松地定位到需要關注的進程,并且其直觀的界面設計使得內存使用情況一目了然,對于系統管理員來說是一個非常實用的工具 。
8.2內存分析工具
Valgrind:是一款功能強大的內存調試、內存泄漏檢測以及性能分析工具 。它主要用于檢測 C 和 C++ 程序中的內存錯誤,包括內存泄漏、非法內存訪問(如越界訪問、使用未初始化的內存等) 。例如,在一個 C 語言程序中,如果使用malloc分配了內存,但在程序結束時沒有調用free釋放內存,就會導致內存泄漏 。使用 Valgrind 可以很容易地檢測到這種問題 。假設我們有一個簡單的 C 程序test.c:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = (int *)malloc(10 * sizeof(int));
// 這里沒有釋放p指向的內存
return 0;
}編譯該程序后,使用 Valgrind 運行:valgrind./test ,Valgrind 會輸出詳細的錯誤信息,指出內存泄漏的位置和大小,類似如下內容:
==12345== Memcheck, a memory error detector
==12345== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==12345== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==12345== Command:./test
==12345==
==12345== HEAP SUMMARY:
==12345== in use at exit: 40 bytes in 1 blocks
==12345== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==12345==
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345== by 0x10918F: main (test.c:6)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 40 bytes in 1 blocks
==12345== indirectly lost: 0 bytes in 0 blocks
==12345== possibly lost: 0 bytes in 0 blocks
==12345== still reachable: 0 bytes in 0 blocks
==12345== suppressed: 0 bytes in 0 blocks
==12345==
==12345== For lists of detected and suppressed errors, rerun with: -s
==12345== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)通過這些信息,我們可以清楚地知道在test.c的第 6 行分配的 40 字節內存沒有被釋放,從而能夠及時修復代碼中的內存泄漏問題 。除了檢測內存泄漏,Valgrind 還能檢測其他內存錯誤,如數組越界訪問:
#include <stdio.h>
int main() {
int arr[5];
arr[10] = 100; // 越界訪問
return 0;
}使用 Valgrind 運行該程序,它會輸出類似如下的錯誤提示:
==12345== Memcheck, a memory error detector
==12345== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==12345== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==12345== Command:./test
==12345==
==12345== Invalid write of size 4
==12345== at 0x10919D: main (test.c:5)
==12345== Address 0x4c38050 is 20 bytes after a block of size 20 alloc'd
==12345== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345== by 0x10918F: main (test.c:4)
==12345==
==12345== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)提示在test.c的第5行發生了無效的寫操作,因為訪問的數組下標超出了數組的范圍 。Valgrind的這些功能對于提高程序穩定性和可靠性非常重要,特別是在開發大型項目時,能夠幫助開發者及時發現和解決內存相關的問題 。
Part9.實際問題與案例分析
9.1內存泄漏問題
內存泄漏是指程序在申請內存后,無法釋放已申請的內存空間 ,導致這些內存被持續占用,無法被其他程序或進程使用 。從進程的角度來看,當一個進程不斷地分配內存,但沒有及時釋放不再使用的內存,隨著時間的推移,進程占用的內存會越來越多,而系統中可用于分配的內存則會逐漸減少 。例如,在一個長時間運行的服務器程序中,如果存在內存泄漏問題,可能會導致服務器的內存被逐漸耗盡,最終影響整個系統的穩定性和性能 。
在 Linux 系統中,可以使用 Valgrind 工具來檢測內存泄漏 。假設我們有一個簡單的 C 程序leak.c:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = (int *)malloc(10 * sizeof(int));
// 這里沒有釋放p指向的內存
return 0;
}編譯該程序后,使用 Valgrind 運行:valgrind --leak-check=full./leak ,Valgrind 會輸出詳細的內存泄漏信息,如下:
==1234== Memcheck, a memory error detector
==1234== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==1234== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==1234== Command:./leak
==1234==
==1234== HEAP SUMMARY:
==1234== in use at exit: 40 bytes in 1 blocks
==1234== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==1234==
==1234== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==1234== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==1234== by 0x10918F: main (leak.c:6)
==1234==
==1234== LEAK SUMMARY:
==1234== definitely lost: 40 bytes in 1 blocks
==1234== indirectly lost: 0 bytes in 0 blocks
==1234== possibly lost: 0 bytes in 0 blocks
==1234== still reachable: 0 bytes in 0 blocks
==1234== suppressed: 0 bytes in 0 blocks
==1234==
==1234== For lists of detected and suppressed errors, rerun with: -s
==1234== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)從輸出中可以看出,在leak.c的第 6 行分配了 40 字節的內存,但沒有釋放,導致內存泄漏 。
一旦檢測到內存泄漏,就需要及時處理 。對于簡單的內存泄漏問題,如上述例子,只需要在合適的位置添加內存釋放的代碼即可 。在上述程序中,我們可以在return 0;之前添加free(p);來釋放分配的內存 。但在實際的大型項目中,內存泄漏的排查和處理可能會比較復雜,需要仔細分析代碼邏輯,找出內存分配和釋放的不合理之處 。有時候,內存泄漏可能是由于復雜的數據結構或函數調用關系導致的,這就需要借助調試工具和技術,逐步跟蹤內存的分配和使用情況,以定位和解決內存泄漏問題 。
9.2 OOM(Out Of Memory)問題
OOM 即內存溢出,是指程序在申請內存時,沒有足夠的內存空間供其使用 。在 Linux 系統中,當系統內存嚴重不足時,內核有兩種主要的應對策略:一是直接觸發系統崩潰(panic) ,這是一種極端情況,通常只有在系統內存極度匱乏且無法通過其他方式解決時才會發生;二是啟動 OOM Killer(內存不足殺手)機制 ,內核會根據一定的算法選擇并殺掉一些占用內存較大的進程,以釋放內存,保證系統的基本運行 。
OOM Killer 在選擇要殺掉的進程時,會為每個進程計算一個 oom_score 值 。oom_score 的計算涉及多個因素,包括進程占用的物理內存頁數、交換區頁數以及頁表(Page Table)數量等 。例如,一個進程占用了大量的物理內存,且頻繁地進行內存交換操作,它的 oom_score 值就會相對較高,也就更容易被 OOM Killer 選中殺掉 。此外,每個進程還有一個 oom_score_adj 參數,用戶可以通過調整這個參數來改變進程被 OOM Killer 殺掉的優先級 。當 oom_score_adj 的值為 - 1000 時,表示該進程不會被 OOM Killer 殺掉 ;而值越大,進程被 OOM Killer 殺掉的可能性就越高 。
為了避免 OOM 問題的發生,可以采取以下措施:
- 優化程序內存使用:仔細檢查程序代碼,避免內存泄漏問題,及時釋放不再使用的內存 。合理設計數據結構和算法,減少不必要的內存占用 。例如,在處理大數據集時,可以采用分塊處理的方式,避免一次性加載過多數據到內存中 。
- 監控內存使用情況:使用如 top、htop、free 等命令實時監控系統和進程的內存使用情況 。通過監控數據,可以及時發現內存使用異常的進程,提前采取措施,如調整進程的內存分配或優化其算法 。
- 合理設置系統參數:根據系統的實際需求,合理調整 Swappiness 參數,平衡物理內存和交換空間的使用 。同時,也可以根據需要調整其他與內存管理相關的系統參數,如/proc/sys/vm/overcommit_memory 。該參數用于控制內存分配的策略,取值為 0 時,內核會盡量檢查是否有足夠的內存可供分配,只有在確定有足夠內存時才會分配;取值為 1 時,內核允許分配超過實際物理內存大小的內存,這可能會導致內存不足的風險增加,但在某些情況下可以提高系統的性能;取值為 2 時,內核會嚴格按照系統的物理內存和交換空間大小來分配內存,不允許超過這個范圍 。在實際應用中,需要根據系統的負載和內存需求,謹慎選擇合適的overcommit_memory值 。






















