Linux 內存管理:深度解析與探索

你是否想過,在 Linux 系統中,當你打開一個程序、瀏覽網頁或者處理文件時,這些數據都存放在哪裏呢?答案就是內存。Linux 內存管理就像是一個超級大管家,它負責管理着系統中所有數據的 “家”。這個 “家” 的空間有限,卻要容納各種各樣的數據,而且要保證每個數據都能被快速準確地找到和使用。它需要智慧地分配房間(內存空間),合理地安排住戶(進程),還要及時清理不再需要的雜物(回收內存)。今天,我們就一起深入瞭解這個神奇的 Linux 內存管理機制。

一、概述

Linux 內存管理是操作系統核心功能之一,涉及虛擬內存、物理內存及多種分配回收機制,確保系統高效穩定運行;在 Linux 系統中,內存被劃分爲多個區域,每個區域有不同的作用。其中,虛擬內存是一種計算機系統的內存管理技術,它擴展了系統可用內存的容量,併爲每個進程提供了獨立的地址空間。虛擬內存將物理內存和磁盤空間結合使用,使得進程能夠訪問超出物理內存限制的數據。

虛擬內存通過分頁機制實現,將虛擬內存分割成固定大小的內存塊,稱爲 “頁”(page),物理內存也同樣劃分爲與頁大小相同的 “頁框”(page frame)。當進程需要訪問數據時,內核將虛擬地址的頁映射到物理內存中的頁框。例如,在 32 位的系統中,頁的大小通常爲 4KB,虛擬地址被分爲高位部分(頁號)和低位部分(頁內偏移)。通過頁表來完成虛擬地址到物理地址的映射,頁表中的每一項包含了虛擬頁到物理頁框的映射信息。

現代 Linux 內核使用多級頁表來提高查找效率,如二級頁表或四級頁表,通過分級的方式減少查找開銷。同時,還使用頁表緩存(TLB,Translation Lookaside Buffer)來加速地址轉換。TLB 是一塊高速緩存,用於存儲最近使用的頁表項,減少對頁表的訪問次數。

Linux 內存管理的目標是提高內存利用率,減少內存碎片,最大限度地利用可用內存,同時保證系統的穩定和可靠性。通過合理的內存管理策略和機制,Linux 系統可以有效地管理系統的內存資源,提高系統的性能和穩定性。

二、關鍵概念解析

2.1 虛擬內存

虛擬內存是 Linux 內存管理的核心概念之一。它通過將物理內存和存儲設備(如硬盤)結合起來,爲每個進程提供了獨立的、連續的內存空間。這極大地簡化了內存管理,並提升了系統的安全性和穩定性。

在虛擬內存機制中,進程訪問的是虛擬地址,而不是直接訪問物理內存地址。內核會將虛擬地址映射到實際的物理地址,這個過程通過頁表來完成。虛擬內存使得操作系統可以提供更大的內存空間,即使物理內存不足,虛擬內存可以通過將部分數據存儲在硬盤的交換分區(swap)中來擴展內存空間。例如,當物理內存耗盡時,操作系統可以將不常用的頁面移到交換分區,爲新的頁面騰出空間。這樣,即使物理內存只有 4GB,一個進程也可以認爲它擁有更大的內存空間。

此外,虛擬內存還實現了進程隔離。每個進程都有獨立的虛擬地址空間,防止了不同進程之間的內存訪問衝突。不同進程的虛擬地址可以相同,但通過頁表映射到不同的物理地址,從而保證了進程之間的獨立性。據統計,在實際應用中,虛擬內存機制可以使系統同時運行更多的進程,提高了系統的併發性能。

虛擬內存的整體流程可分爲 4 步:

  1. CPU 產生一個虛擬地址

  2. MMU 從 TLB 中獲取頁表,翻譯成物理地址

  3. MMU 把物理地址發送給主存

  4. 主存將地址對應的數據返回給 CPU

虛擬地址與物理地址的映射關係如下圖所示:

虛擬內存系統將虛擬內存分割成固定大小的塊,也被稱爲虛擬頁。類似地,物理內存也被分割成固定大小的塊,稱爲物理頁。要實現虛擬頁與物理頁之間的映射關係,我們需要一種叫作頁表的數據結構。頁表實際上就是一個 ** 頁表條目(Page Table Entry,PTE)** 的數組。

每條 PTE 都由一個有效位和一個 n 位地址組成。如果 PTE 的有效位爲 1,則 n 位地址表示相應物理頁的起始位置,即虛擬地址能夠在物理內存中找到相應的物理頁。如果 PTE 的有效位爲 0,且後面跟着的地址爲空,那麼表示該虛擬地址指向的虛擬頁還沒有被分配。如果 PTE 的有效位爲 0,且後面跟着指向虛擬頁的地址,表示該虛擬地址在物理內存中沒有相對應的物理地址,指向該虛擬頁在磁盤上的起始位置,我們通常把這種情況稱爲缺頁。

此時,若出現缺頁現象,MMU 會發出一個缺頁異常,缺頁異常調用內核中的缺頁處理異常程序,該程序會選擇主存的一個犧牲頁,將我們需要的虛擬頁替換到原犧牲頁的位置。我們目前爲止討論的只是單頁表,但在實際的環境中虛擬空間地址都是很大的(一個 32 位系統的地址空間有 2^32 = 4GB,更別說 64 位系統了)。在這種情況下,使用一個單頁表明顯是效率低下的。

常用方法是使用層次結構的頁表。假設我們的環境爲一個 32 位的虛擬地址空間,它有如下形式:虛擬地址空間被分爲 4KB 的頁,每個 PTE 都是 4 字節。內存的前 2K 個頁面分配給了代碼和數據. 之後的 6K 個頁面還未被分配。再接下來的 1023 個頁面也未分配,其後的 1 個頁面分配給了用戶棧。

虛擬地址空間構造的二級頁表層次結構(真實情況中多爲四級或更多),一級頁表( 1024 個 PTE 正好覆蓋 4GB 的虛擬地址空間,同時每個 PTE 只有 4 字節,這樣一級頁表與二級頁表的大小也正好與一個頁面的大小一致都爲 4KB)的每個 PTE 負責映射虛擬地址空間中一個 4MB 的片(chunk),每一片都由 1024 個連續的頁面組成。二級頁表中的每個 PTE 負責映射一個 4KB 的虛擬內存頁面。

2.2 分頁機制

我們知道,當一個程序運行時,在某個時間段內,它只是頻繁地用到了一小部分數據,也就是說,程序的很多數據其實在一個時間段內都不會被用到。以整個程序爲單位進行映射,不僅會將暫時用不到的數據從磁盤中讀取到內存,也會將過多的數據一次性寫入磁盤,這會嚴重降低程序的運行效率。

現代計算機都使用分頁(Paging)的方式對虛擬地址空間和物理地址空間進行分割和映射,以減小換入換出的粒度,提高程序運行效率。

分頁是虛擬內存實現的重要機制之一。它將虛擬內存分割成固定大小的內存塊,稱爲 “頁”(page),物理內存也同樣劃分爲與頁大小相同的 “頁框”(page frame)。在 Linux 系統中,頁的大小通常爲 4KB。。如此,就能夠以頁爲單位對內存進行換入換出:

頁的大小是固定的,由硬件決定,或硬件支持多種大小的頁,由操作系統選擇決定頁的大小。比如 Intel Pentium 系列處理器支持 4KB 或 4MB 的頁大小,那麼操作系統可以選擇每頁大小爲 4KB,也可以選擇每頁大小爲 4MB,但是在同一時刻只能選擇一種大小,所以對整個系統來說,也就是固定大小的。

目前幾乎所有 PC 上的操作系統都是用 4KB 大小的頁。假設我們使用的 PC 機是 32 位的,那麼虛擬地址空間總共有 4GB,按照 4KB 每頁分的話,總共有 2^32 / 2^12 = 2^20 = 1M = 1048576 個頁;物理內存也是同樣的分法。

分頁帶來的好處包括提高內存利用率。分頁機制允許物理內存與虛擬內存不連續,從而減少了內存碎片。因爲頁面是固定大小的,操作系統可以更有效地管理內存,將不連續的頁框分配給不同的頁面。

同時,分頁支持按需分配。Linux 支持按需分頁,即當某頁被訪問時,纔將它從磁盤加載到物理內存中,這大大提升了內存的使用效率。例如,一個大型程序可能只使用了一部分內存,如果沒有按需分頁,整個程序可能會在啓動時全部加載到物理內存中,浪費大量內存資源。而有了按需分頁,只有當程序實際訪問到某一頁時,纔會將該頁加載到內存中。

2.3 頁表

頁表是 Linux 內存管理中用於存儲虛擬地址到物理地址映射關係的數據結構。每個進程都有自己的頁表,內核根據頁表來完成虛擬內存到物理內存的轉換。頁表中的每一項包含了虛擬頁到物理頁框的映射信息。

爲了提高查找效率,現代 Linux 內核使用多級頁表(如二級頁表或四級頁表),通過分級的方式減少查找開銷。例如,在 64 位的系統中,通常使用四級目錄,分別是全局頁目錄項 PGD(Page Global Directory)、上層頁目錄項 PUD(Page Upper Directory)、中間頁目錄項 PMD(Page Middle Directory)和頁表項 PTE(Page Table Entry)。這樣可以有效地減少頁表項的數量,提高訪問速度。

同時,Linux 還使用了頁表緩存(TLB,Translation Lookaside Buffer)來加速地址轉換。TLB 是一塊高速緩存,用於存儲最近使用的頁表項,減少對頁表的訪問次數。當進程訪問一個虛擬地址時,首先在 TLB 中查找,如果找到相應的頁表項,則直接得到物理地址,否則再去訪問頁表。據測試,使用 TLB 可以大大提高地址轉換的速度,減少系統的響應時間。

三、內存分配與釋放

3.1 分配機制

Linux 使用虛擬內存系統,將進程虛擬內存空間劃分爲多個分區,如代碼段、數據段、堆、棧等。對於不同的分區,Linux 採用不同的內存分配算法和系統調用進行內存分配。

在堆的分配中,通常使用動態分配算法,如 malloc 函數。當程序請求內存時,內核會根據請求的大小在堆中尋找合適的連續空間進行分配。例如,當一個程序需要分配大量的動態內存時,Linux 會通過遍歷堆的空閒鏈表,找到足夠大的連續空間來滿足請求。如果找不到合適的連續空間,可能會觸發內存碎片整理或者向系統請求更多的物理內存。

對於棧的分配,是自動進行的,當函數被調用時,棧空間會自動增長以存儲局部變量和函數調用的上下文。棧的大小通常是有限的,一般爲 8MB。如果棧空間耗盡,會導致棧溢出錯誤。

在系統調用方面,mmap 函數可以將文件或設備映射到進程的虛擬地址空間,實現高效的文件訪問和內存共享。例如,在加載動態庫時,Linux 使用 mmap 將動態庫文件映射到進程的虛擬地址空間,多個進程可以共享同一個動態庫的內存映射,節省了內存空間。

3.2 釋放機制

在內存釋放方面,Linux 默認情況下會進行一定程度的自動釋放。當內存不再被使用時,系統會自動回收相應的內存空間。例如,當一個進程結束時,其佔用的內存會被自動釋放。

對於 cache memory,Linux 會自動管理其釋放。當系統內存緊張時,會優先釋放不活躍的 cache memory,將髒數據寫入文件中再釋放內存頁。而對於活躍的 cache memory,會在適當的時候進行釋放,以滿足其他進程的內存需求。

用戶也可以手動釋放不同類型的緩存。例如,可以使用 echo 1 > /proc/sys/vm/drop_caches 釋放頁緩存,echo 2 > /proc/sys/vm/drop_caches 釋放目錄和索引節點緩存,echo 3 > /proc/sys/vm/drop_caches 同時釋放頁、目錄和索引節點緩存。但需要注意的是,這些操作是無害的,只會釋放完全沒有使用的內存對象。髒對象將繼續被使用直到它們被寫入到磁盤中。

在物理內存緊張時,Linux 採用置換算法來選擇一些頁面置換到輔助存儲器上。常用的置換算法包括最近未使用(LRU)算法和最不常用(LFU)算法。這些算法根據頁面的訪問頻率和時間等參數來進行頁面置換,以儘量減少對磁盤的訪問,提高系統的響應速度。

3.3 碎片問題

⑴內存碎片的產生原因

①內存分配與釋放的動態性

Linux 系統中的進程在運行過程中不斷地申請和釋放內存。由於進程的內存需求大小和時間都是隨機的,這就導致內存空間被分割成各種大小不一的塊。例如,一個進程先申請了一大塊內存,使用一段時間後釋放了其中的一部分。接着,另一個進程申請一個較小的內存塊,它佔用了之前釋放的部分空間。隨着這樣的操作不斷重複,內存空間逐漸變得碎片化。

就好比在一個倉庫中,不同的貨物(進程)在不同的時間進出倉庫,貨物大小各異。一開始倉庫空間是整齊的,但隨着貨物的頻繁進出,倉庫裏就會出現很多大小不一的空位,使得倉庫空間難以有效利用。

②內存分配算法的侷限性

Linux 常用的內存分配算法雖然能夠高效地處理大多數內存分配請求,但在某些情況下,也會導致碎片的產生。例如,夥伴系統(Buddy System)在分配內存時是以固定大小的塊(頁)爲單位進行的。當進程申請的內存大小不是這些固定塊大小的整數倍時,可能會剩餘一些小的內存碎片。

以分配積木爲例,積木有固定的幾種大小規格(類似於夥伴系統中的固定頁大小)。如果要搭建一個形狀不規則的模型(類似於進程的不規則內存需求),在使用積木搭建過程中,就會剩下一些無法再用於搭建該模型的小積木塊,這些小積木塊就類似於內存碎片。

⑵內存碎片的類型

①內部碎片

內部碎片主要是指在分配給進程的內存塊中,有一部分空間沒有被有效利用。這通常是由於內存分配單位和進程實際需求之間的差異造成的。例如,在使用某些內存分配器時,它們分配內存是以固定大小的單元進行的。如果進程所需的內存小於這個單元大小,就會產生內部碎片。

還是以積木爲例,假設每個積木單元是固定大小爲 10 個小方塊,而一個搭建任務只需要 7 個小方塊,那麼分配給這個任務一個積木單元后,就會剩下 3 個小方塊的內部碎片。

②外部碎片

外部碎片是指在內存空間中,存在許多小的空閒內存塊,但是這些空閒塊由於不連續,無法滿足一些較大的內存分配請求。比如,系統中有多個小的空閒內存塊,每個塊的大小都小於一個正在申請內存的大型進程所需要的大小,即使這些小空閒塊的總和足夠該進程使用,也無法將它們分配給這個進程。

這就好比倉庫中有很多分散在不同角落的小空位,但沒有一塊足夠大的連續空位來存放一個大型貨物。

⑶內存碎片問題的解決方案

①調整內核參數

通過修改一些內核參數,可以在一定程度上緩解內存碎片問題。例如,調整內存分配器的參數,優化內存分配的策略。對於夥伴系統,可以調整頁的大小和夥伴合併的閾值等參數。這樣可以根據系統的實際運行情況,使內存分配更加合理,減少碎片的產生。

②定期內存規整

Linux 系統提供了一些工具和機制來進行內存規整。內存規整就像是對倉庫進行重新整理,將分散的小空閒內存塊合併成較大的連續內存塊。通過內存遷移技術,將佔用內存的進程從碎片化的區域遷移到連續的空閒區域,從而釋放出連續的內存空間,以滿足較大的內存分配請求。

③使用夥伴系統和 Slab 分配器

夥伴系統可以有效地管理內存塊,它以頁爲單位進行內存分配,當一個內存塊被釋放時,它會嘗試與相鄰的同大小的空閒塊合併,形成更大的空閒塊,從而減少外部碎片。Slab 分配器則側重於按對象緩衝管理內存,它針對特定大小的對象進行分配,有助於減少內部碎片。這兩種分配器相互配合,能夠在一定程度上優化內存的分配和利用。

④監控和優化內存使用

使用系統監控工具,如 sar、vmstat 等,定期監測內存的使用情況和碎片程度。根據監控結果,對系統中的進程進行優化,例如優化進程的內存申請和釋放邏輯,避免不必要的小內存塊申請和頻繁的內存釋放操作。

⑤定期重啓服務

在一些長期運行的系統中,定期重啓某些服務也是一種簡單有效的減少內存碎片的方法。當服務重啓時,其佔用的內存空間會被重新分配,之前產生的碎片會被清除。不過這種方法可能會對系統的可用性產生一定的影響,需要謹慎使用。

四、內存管理的其他方面

4.1 內存映射類型

內存映射主要有四種類型:私有匿名映射、共享匿名映射、私有文件映射和共享文件映射。

在 Linux 中,可以使用 pmap 命令查看進程的內存映射情況。

4.2 進程地址空間

進程地址空間由多個區域組成,包括代碼段區域、數據段區域、堆區、棧區及 mmap 開闢的內存映射區域。

⑴代碼段區域

這是進程地址空間中存放可執行代碼的部分。當一個程序被加載到內存中時,其機器語言指令就存放在代碼段。就像是一本書的正文部分,包含了程序運行所需要執行的操作步驟。這個區域通常是隻讀的,這意味着程序在運行過程中不能隨意修改自己的代碼(當然,在一些特殊的情況下,如自修改代碼的程序,會有特殊的處理機制)。

例如,一個簡單的 C 語言程序中的函數定義部分就存放在代碼段。像 “int add (int a, int b) { return a + b; }” 這樣的函數代碼會被加載到代碼段區域,供 CPU 讀取並執行。

⑵數據段區域

數據段主要用於存儲程序中已經初始化的全局變量和靜態變量。可以把它想象成一個數據倉庫,裏面存放着程序在整個生命週期內都需要使用的各種數據。這些數據在程序啓動時就被初始化,並且在程序運行過程中可以被修改。

以一個簡單的程序爲例,定義了全局變量 “int global_variable = 10;”,這個變量就會存儲在數據段區域。在程序運行過程中,如果有其他函數修改了這個變量的值,修改操作也是在數據段區域內完成的。

⑶堆區

堆區是進程地址空間中一個非常靈活的部分,用於動態內存分配。程序可以在運行過程中通過系統調用(如 malloc 函數)在堆區申請內存空間,用於存儲各種數據結構,如鏈表、樹等。堆區的內存分配和釋放是由程序員手動控制的,這就像在一片空地上,根據自己的需求建造各種建築物,需要的時候就申請一塊土地來建造,不需要的時候就拆除(釋放內存)。

例如,在一個處理大量數據的程序中,可能會使用鏈表來存儲數據。通過 “struct node *new_node = (struct node *) malloc (sizeof (struct node));” 這樣的操作,就在堆區申請了一塊內存用於存儲一個鏈表節點,並且可以根據數據量的多少,多次申請內存來構建完整的鏈表。

⑷棧區

棧區主要用於存儲函數調用的相關信息,包括函數的參數、局部變量和返回地址等。它的工作方式就像一個棧結構,遵循後進先出(LIFO)的原則。當一個函數被調用時,相關的信息就被壓入棧中,函數執行完畢後,這些信息又會按照相反的順序從棧中彈出。

比如,在一個函數 “void function (int param) { int local_variable; local_variable = param + 1; }” 中,參數 “param” 和局部變量 “local_variable” 都會存儲在棧區。每次調用這個函數時,都會在棧區爲這些變量開闢新的空間,函數結束後,這些空間就會被釋放。

⑸mmap 開闢的內存映射區域

mmap 是一種內存映射機制,它允許進程將文件或者其他對象映射到自己的地址空間中。這個區域可以用於高效地訪問文件內容,就好像文件的內容直接在內存中一樣。通過這種方式,進程可以以內存訪問的方式來讀取和修改文件,大大提高了文件訪問的效率。

例如,在一個數據庫應用程序中,可能會使用 mmap 將數據庫文件映射到進程地址空間。這樣,在查詢或者更新數據庫記錄時,就可以像訪問普通內存數據一樣快速方便地進行操作。

⑹堆區和棧區的特性

①生長方向

堆區是從低地址向高地址方向生長的。這意味着隨着程序不斷地在堆區申請更多的內存,堆區所佔用的內存地址會越來越高。而棧區則是從高地址向低地址方向生長。當一個函數調用另一個函數時,新函數的棧幀(包含函數的參數、局部變量等信息)會在棧區的較低地址處開闢,就像在一個向下挖掘的坑中堆放新的物品。

這種相反的生長方向有助於防止堆區和棧區在生長過程中相互衝突,保證了它們在進程地址空間中的獨立性。

②brk 地址

在堆區的管理中,brk 地址是一個重要的概念。它代表了堆區的邊界,當程序通過系統調用(如 sbrk)來擴展堆區時,實際上是在移動 brk 地址。例如,當調用 “void *ptr = sbrk (1024);” 時,系統會將 brk 地址向上移動 1024 字節(假設字節單位),從而爲堆區增加了 1024 字節的可用空間。

理解進程地址空間的各個組成部分及其特性,對於深入理解 Linux 內存管理以及編寫高效、穩定的程序至關重要。它就像是一張地圖,指導着程序在內存這個複雜的領域中有序地活動。

4.3 內存分配方式

⑴小內存分配

當程序需要分配小內存時,通常會使用諸如malloc函數(在 C 語言環境下)。malloc函數的底層實現是通過系統調用與內存分配器進行交互來獲取內存。

在 Linux 中,對於小內存分配,內存分配器會採用一種高效的策略。它會從預先分配好的內存池中獲取合適大小的內存塊。這個內存池就像是一個裝滿各種規格積木的盒子,當程序需要一塊小內存時,就從盒子裏挑選合適大小的積木(內存塊)。

例如,在 C 語言中編寫一個簡單的程序,需要動態分配一個小結構體的內存空間:

#include <stdio.h>
#include <stdlib.h>

struct small_struct {
    int num;
};

int main() {
    struct small_struct *ptr = (struct small_struct *)malloc(sizeof(struct small_struct));
    if (ptr == NULL) {
        perror("malloc");
        return 1;
    }
    ptr->num = 10;
    printf("Allocated memory and set value: %d\n", ptr->num);
    free(ptr);
    return 0;
}

當用戶分配小內存時,通常使用 malloc 函數,malloc 函數會根據 struct small_struct 的大小從內存池中獲取合適的內存塊來分配給 ptr,malloc 在分配一塊小型內存(小於或等於 128kb)時,會調用 brk 函數將堆頂指針向高地址移動,獲得新的內存空間。

⑵內存分配

當涉及到大內存分配時,系統會採用不同的策略。通常會直接從操作系統的物理內存中分配連續的內存頁。這是因爲對於大內存需求,保證內存的連續性可以提高內存訪問效率。

以一個處理大型圖像數據的程序爲例。如果程序需要加載一個高分辨率的圖像,可能需要分配一大塊連續的內存空間來存儲圖像的像素數據。此時,系統會從物理內存中劃分出連續的頁來滿足這個需求。

在 Linux 中,分配大內存可能會涉及到系統調用,如mmap(內存映射)或brk/sbrk系統調用的特殊用法。mmap可以將文件或者匿名內存區域映射到進程的地址空間,用於分配大內存塊非常有效。

例如,通過mmap分配大內存的簡單示例:

#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define MEM_SIZE (1024 * 1024)  // 1MB的內存大小

int main() {
    void *ptr = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
        return 1;
    }
    // 可以在這裏使用分配的大內存,比如存儲大型數據結構
    // ......
    if (munmap(ptr, MEM_SIZE) == -1) {
        perror("munmap");
        return 1;
    }
    return 0;
}

當用戶分配大內存時,malloc 會調用 mmap 以 “私有匿名映射” 的方式,在文件映射區分配一塊內存,mmap函數用於分配了 1MB 的匿名內存空間,這個空間可以用於存儲大型的數據結構或者文件內容。

⑶malloc 的工作原理

malloc 函數是用戶空間中常用的內存分配函數,它的工作原理較爲複雜。

malloc 會首先檢查是否有足夠的空閒內存塊可以滿足請求。如果有,它會從空閒內存塊列表中選擇一個合適的塊,並將其返回給用戶程序。這個空閒內存塊列表是由內存分配器維護的,它會記錄內存池中各種大小的空閒內存塊

如果沒有足夠的空閒內存塊,malloc 會向操作系統請求更多的內存。這可能涉及到系統調用,以獲取新的內存頁或者調整內存池的大小。

內存分配後,malloc 會對內存塊進行一些標記和組織。它會在內存塊的頭部存儲一些元數據,如內存塊的大小、是否被使用等信息。這些元數據對於後續的內存管理操作,如 free 函數釋放內存時非常重要。

例如,當 free 函數被調用時,它會根據內存塊頭部的元數據來確定該內存塊的大小和狀態,然後將其放回空閒內存塊列表中,以便下次 malloc 請求時可以再次使用。

malloc 的工作原理是先從預先分配的內存池中尋找合適的內存塊,如果沒有找到,則通過系統調用進行內存分配。內存塊通常以鏈表的形式組織,根據大小進行分類管理。

⑷內存塊的組織方式

在內存分配過程中,內存塊的組織方式也很關鍵。Linux 中的內存分配器通常會採用不同的策略來組織內存塊。

一種常見的方式是使用鏈表來組織空閒內存塊。每個空閒內存塊都包含一個指向下一個空閒內存塊的指針,這樣就形成了一個鏈表結構。當需要分配內存時,內存分配器可以沿着這個鏈表查找合適大小的內存塊。

另外,還會採用一些分區策略。例如,將內存池按照內存塊大小進行分區,不同大小範圍的內存塊存放在不同的分區中。這樣可以提高內存分配的效率,當請求特定大小範圍的內存時,可以直接在對應的分區中查找。

4.4 夥伴系統與 Slab 分配器

在 Linux 內存管理的複雜體系中,夥伴系統(Buddy System)和 Slab 分配器(Slab Allocator)發揮着至關重要的作用。它們就像兩個經驗豐富的 “管家”,分別從不同角度對內存進行高效的管理和分配。

⑴夥伴系統

夥伴系統是以頁(Page)爲單位來分配內存的。在 Linux 系統中,物理內存被劃分成固定大小的頁,夥伴系統利用這些頁來構建內存分配的基本框架。它的核心思想是基於一種 “夥伴” 關係。

當一個進程申請內存時,夥伴系統會嘗試找到一個大小合適的內存塊來滿足需求。這個內存塊是由連續的頁組成的。如果沒有合適大小的內存塊,它會向上尋找更大的內存塊,直到找到一個可用的。例如,如果進程需要 2 個頁大小的內存,而系統中沒有直接可用的 2 個頁大小的內存塊,它可能會從 4 個頁大小的內存塊中劃分出 2 個頁來滿足需求。

當一個內存塊被釋放時,夥伴系統會檢查它的 “夥伴” 內存塊是否空閒。如果 “夥伴” 也是空閒的,那麼這兩個內存塊就會合併成一個更大的空閒內存塊,這種合併操作可以有效地減少外部碎片。就好像兩個相鄰的空閒地塊,當它們都空着的時候,就會被合併成一個更大的地塊,方便後續的分配。

優勢

應用場景

內核內存分配:在 Linux 內核中,對於一些需要較大塊連續物理內存的情況,如設備驅動程序爲硬件設備分配緩衝區等,夥伴系統是一種理想的選擇。例如,爲網絡設備分配用於接收和發送數據包的緩衝區,夥伴系統可以快速提供合適大小的連續內存塊。

⑵Slab 分配器

Slab 分配器主要側重於按對象緩衝(Object - Caching)來管理內存。它針對特定大小的對象進行分配,這些對象可以是內核中的數據結構,如進程描述符、文件描述符等。

Slab 分配器會預先分配一定數量的內存塊,這些內存塊被組織成 “Slab”。每個 Slab 包含多個相同大小的對象,這些對象的大小是根據系統中常見的內存使用場景預先確定的。當需要分配一個特定大小的對象時,Slab 分配器會從相應大小的 Slab 中獲取一個空閒對象。

例如,對於內核中的進程描述符對象,Slab 分配器會創建一個專門用於存儲進程描述符的 Slab。當創建一個新進程時,就從這個 Slab 中取出一個進程描述符對象來使用。當進程結束時,這個對象又會被放回 Slab 中,以便下次使用。

優勢

應用場景

內核對象分配:在 Linux 內核中,大量的內核對象需要頻繁地創建和銷燬。Slab 分配器非常適合這種場景,比如用於分配和管理文件系統中的 inode 對象。當文件系統需要創建一個新的文件或目錄時,就從 inode - Slab 中獲取一個 inode 對象,提高了文件系統操作的效率。

⑶夥伴系統與 Slab 分配器的配合

在 Linux 內存管理中,夥伴系統和 Slab 分配器並不是相互獨立的,而是相互配合的關係。

夥伴系統主要負責爲 Slab 分配器提供大塊的物理內存。當 Slab 分配器需要更多的內存來創建新的 Slab 時,它會向夥伴系統請求。而 Slab 分配器則專注於對這些內存進行細分,按照對象的大小進行更精細的分配和管理,以滿足內核和用戶程序對不同大小內存對象的需求。

這種配合使得 Linux 內存管理能夠在保證有足夠連續物理內存的同時,又能高效地分配和管理各種大小的內存對象,有效地提高了內存的利用率和系統的整體性能。它們共同構成了 Linux 內存管理的堅實基礎,確保系統在各種複雜的內存需求場景下都能穩定高效地運行。

4.5 虛擬地址與物理地址映射關係

虛擬地址與物理地址不直接一一映射的原因主要是爲了提高內存利用率、實現進程隔離和方便內存管理。多級映射的優勢在於可以減少頁表項的數量,提高查找效率。

TLB(Translation Lookaside Buffer)塊表的作用是加速地址轉換,它是一塊高速緩存,用於存儲最近使用的頁表項,減少對頁表的訪問次數。相同虛擬地址在 TLB 中的區分方法是通過不同的進程標識和頁表項中的其他信息。

不同進程訪問共享內核地址空間時,由於每個虛擬內存中的內核地址關聯的都是相同的物理內存,所以進程切換到內核態後,可以很方便地訪問內核空間內存,提高了效率。

寫時複製的意義在於減少內存的複製操作,當多個進程共享同一塊內存區域時,只有在某個進程試圖修改該區域的內容時,纔會真正複製一份新的內存區域給該進程使用。

在內存訪問時,先通過 MMU(Memory Management Unit)進行地址轉換,然後再訪問 cache。cache 緩存大地址空間的原理是通過將最近訪問過的內存區域的內容存儲在 cache 中,下次訪問時可以直接從 cache 中獲取,提高訪問速度。

五、全文總結

Linux 內存管理是一個複雜而高效的系統,它通過虛擬內存、分頁機制、頁表等技術,爲系統提供了高效的內存管理方案。同時,通過內存分配與釋放、內存映射、夥伴系統與 Slab 分配器等機制,進一步提高了內存的利用率和系統的性能。

然而,Linux 內存管理也面臨着一些挑戰。例如,內存碎片問題仍然存在,雖然可以通過調整內核參數、定期內存規整等方法來緩解,但在一些高負載的系統中,仍然可能會影響系統的性能。此外,隨着系統的不斷髮展,對內存的需求也在不斷增加,如何更好地管理和優化內存,以滿足不斷增長的需求,也是一個需要持續研究的問題。

展望未來,Linux 內存管理有望在以下幾個方面得到進一步的發展和優化。

首先,隨着硬件技術的不斷進步,如更大容量的內存、更快的存儲設備等,Linux 內存管理可以更好地利用這些硬件資源,提高系統的性能和穩定性。例如,可以通過優化頁表結構,提高地址轉換的速度;可以利用新的存儲設備,如 NVM(Non-Volatile Memory),實現更快的內存訪問。

其次,隨着雲計算和容器技術的發展,對內存管理的要求也在不斷提高。Linux 內存管理可以更好地支持容器化環境,實現更高效的內存隔離和資源分配。例如,可以通過優化內存分配算法,提高容器的啓動速度和內存利用率;可以利用容器的特性,實現更靈活的內存管理策略。

最後,隨着人工智能和大數據技術的發展,對內存的需求也在不斷增加。Linux 內存管理可以更好地支持這些應用場景,實現更高效的內存管理和資源分配。例如,可以通過優化內存分配算法,提高大數據處理的效率;可以利用人工智能的技術,實現更智能的內存管理策略。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/2wWmkfpjeQxD4I3H-fkV2A