Linux 核心利用虛擬記憶體機制,讓使用者程式和核心程式都能使用虛擬位址,透過 MMU 轉換成實體位址。這種機制讓程式可以使用比實體記憶體更大的空間,也讓多個程式能在獨立的虛擬記憶體空間中執行。核心空間和使用者空間使用不同的位址型別,例如核心邏輯位址、核心虛擬位址和使用者虛擬位址。核心空間透過 kmalloc() 等函式分配記憶體,而使用者空間則透過 malloc() 等函式分配記憶體。Linux 核心將實體記憶體劃分為 ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM 和 Memory-Mapped I/O 等區域,並對映到核心虛擬或邏輯位址空間。核心提供頁面分配器和 SLAB 分配器等機制來管理記憶體分配,其中 SLAB 分配器透過快取機制提高效率,而頁面分配器則使用夥伴演算法管理頁面。

Linux 核心記憶體分配機制解析

Linux 作業系統採用虛擬記憶體架構,這意味著使用者程式所見的位址並不直接對應到硬體使用的實體位址。核心與使用者程式都使用虛擬位址,而位址轉換則由硬體中的記憶體管理單元(MMU)負責。虛擬記憶體使得系統上的程式能夠分配遠超過實體記憶體的容量;甚至單一程式的虛擬位址空間也可以大於系統的實體記憶體。

ARM 架構下的虛擬記憶體管理

ARM 架構使用儲存在記憶體中的轉換表來將虛擬位址轉換為實體位址。當 MMU 需要進行位址轉換時,會自動讀取這些轉換表,這個過程被稱為「表查詢」(Table Walk)。MMU 的重要功能之一是使系統能夠以獨立的虛擬記憶體空間執行多個任務,這些任務無需瞭解系統的實體記憶體對映,即硬體使用的位址,或其他可能同時執行的程式。

虛擬記憶體與實體記憶體的區別

  • 虛擬位址是程式所使用的位址,而實體位址則是硬體實際使用的位址。
  • 每個使用者程式都有自己的虛擬位址空間,這些空間可以分享相同的虛擬位址。

Linux 中的位址型別

Linux 中使用了多種不同的位址型別,包括:

  1. 使用者虛擬位址:這些是使用者空間程式所看到的常規位址,其長度取決於底層硬體架構,每個程式都有自己的虛擬位址空間。虛擬位址空間被分割,上部用於核心,下部用於使用者空間。

  2. 實體位址:這些是用於處理器和系統記憶體之間的位址,可以是 32 位或 64 位的數量。

  3. 匯流排位址:這些是用於周邊匯流排和記憶體之間的位址,通常與處理器使用的實體位址相同,但並非總是如此。某些架構提供了 I/O 記憶體管理單元(IOMMU),可以重新對映匯流排和主記憶體之間的位址。

  4. 核心邏輯位址:這些構成了核心的正常位址空間,是虛擬位址空間中高於 CONFIG_PAGE_OFFSET 的部分。在大多數架構中,邏輯位址和其相關的實體位址之間僅相差一個常數偏移量。kmalloc() 函式傳回一個指向核心邏輯位址空間的指標變數,該空間對映到連續的實體頁面。核心邏輯記憶體無法被交換出去。

    void *kmalloc(size_t size, gfp_t flags);
    

    內容解密:

    • kmalloc() 是 Linux 核心中用於分配記憶體的函式,類別似於使用者空間的 malloc()
    • size 引數指定了要分配的記憶體大小。
    • flags 引數用於指定分配記憶體的行為,例如是否允許睡眠、是否需要零初始化等。
    • 傳回值為指向分配到的記憶體區塊的指標,如果分配失敗則傳回 NULL
  5. 核心虛擬位址:這些位址與邏輯位址相似,也是核心空間位址到實體位址的對映。然而,核心虛擬位址不一定具有與邏輯位址空間相同的線性、一對一對映特性。所有邏輯位址都是核心虛擬位址,但許多核心虛擬位址不是邏輯位址。

使用者程式虛擬到實體記憶體的對映

在 Linux 中,核心空間是核心執行並提供其服務的地方。核心空間受到保護,以防止使用者應用程式直接存取,而使用者空間則可以從核心模式執行的程式碼直接存取。每個使用者空間程式都有自己的虛擬記憶體佈局,包含四個邏輯區域:

  1. 文欄位:程式碼,儲存程式的二進位制映像。
  2. 資料段:在程式啟動或執行時建立和初始化的各種資料結構,例如堆積(heap)。堆積提供執行時的記憶體分配,用於存放必須比執行分配的函式壽命更長的資料。
  3. 記憶體對映段:核心將檔案內容直接對映到此段中的記憶體,可以透過呼叫 Linux 的 mmap() 系統呼叫來實作。
  4. 堆積疊段:位於程式可用區域的末端附近,並向下增長,用於存放大多數程式語言中的區域性變數和函式引數。

圖解虛擬記憶體佈局

此圖示展示了使用者虛擬位址空間和核心虛擬位址空間的大致佈局。

Linux 核心記憶體管理深度解析

Linux 核心記憶體管理是作業系統設計中的關鍵部分,負責高效分配和管理系統記憶體資源。本文將探討 Linux 核心的記憶體對映、分配器及其運作機制。

核心虛擬位址空間與實體記憶體對映

Linux 核心的虛擬位址空間起始於 0xc0000000,其組態可透過核心設定進行調整,以存取更多的實體記憶體。核心將實體記憶體劃分為四個區域:

  1. ZONE_DMA:對映至核心虛擬位址空間(HIGHMEM),用於 DMA 傳輸。由 dma_alloc_xxx 函式傳回虛擬 DMA 記憶體區域。
  2. ZONE_NORMAL:對映至核心邏輯位址空間(LOWMEM),用於核心內部資料結構及其他系統和使用者空間的分配。kmalloc() 函式從此區域分配連續記憶體。
  3. ZONE_HIGHMEM:對映至核心虛擬位址空間(HIGHMEM),專門用於系統分配(如檔案系統緩衝區、使用者空間分配等)。由 vmalloc 函式傳回核心虛擬位址。
  4. Memory-Mapped I/O:對映至核心虛擬位址空間(HIGHMEM),用於 I/O 操作。由 ioremap() 傳回的記憶體將動態放置在此區域。

記憶體對映圖示

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Linux 核心記憶體分配機制解析

package "Linux Shell 操作" {
    package "檔案操作" {
        component [ls/cd/pwd] as nav
        component [cp/mv/rm] as file
        component [chmod/chown] as perm
    }

    package "文字處理" {
        component [grep] as grep
        component [sed] as sed
        component [awk] as awk
        component [cut/sort/uniq] as text
    }

    package "系統管理" {
        component [ps/top/htop] as process
        component [systemctl] as service
        component [cron] as cron
    }

    package "管線與重導向" {
        component [| 管線] as pipe
        component [> >> 輸出] as redirect
        component [$() 命令替換] as subst
    }
}

nav --> file : 檔案管理
file --> perm : 權限設定
grep --> sed : 過濾處理
sed --> awk : 欄位處理
pipe --> redirect : 串接命令
process --> service : 服務管理

note right of pipe
  命令1 | 命令2
  前者輸出作為後者輸入
end note

@enduml

此圖示展示了實體記憶體如何被劃分為不同區域,並對映到核心虛擬或邏輯位址空間。

核心記憶體分配器

Linux 核心提供了多種記憶體分配方法,主要包括頁面分配器(Page Allocator)和 SLAB 分配器。

頁面分配器(Page Allocator)

頁面分配器負責管理整個系統的頁面分配。它使用二元夥伴分配演算法(Binary Buddy Allocator)來高效地分配和釋放頁面。

頁面分配器 API

核心提供了多個函式來分配頁面:

unsigned long get_zeroed_page(int flags); 
unsigned long __get_free_page(int flags); 
unsigned long __get_free_pages(int flags, unsigned int order);

這些函式的旗標引數包括:

  • GFP_KERNEL:標準的核心記憶體分配,可阻塞以尋找足夠的可用記憶體。適用於大多數情況,但不適用於中斷處理程式。
  • GFP_ATOMIC:在不能阻塞的程式碼中分配 RAM(如中斷處理程式或關鍵區段)。永不阻塞,可存取緊急池,但若無可用記憶體則可能失敗。
  • GFP_DMA:在用於 DMA 傳輸的實體記憶體區域中分配記憶體。

SLAB 分配器

SLAB 分配器為每種型別的核心物件建立快取(cache),每個快取包含多個 slab,而每個 slab 是一個或多個連續頁面,用於儲存初始化的物件。這種機制有效地管理了核心物件的分配和釋放,防止了記憶體碎片化。

SLAB 分配器的運作機制

SLAB 分配器根據物件的使用情況,將 slab 分為三類別:

  • 滿 slab:無可用物件。
  • 部分滿 slab:包含可用物件。
  • 空 slab:無已分配物件。

新的分配請求優先從部分滿的 slab 中進行,否則從空 slab 或新分配的 slab 中進行。

Linux 核心中有三種不同的 SLAB 分配器實作:

  • CONFIG_SLAB:傳統實作。
  • CONFIG_SLOB:簡單分配器,節省約 0.5MB 記憶體,但擴充套件性較差,用於非常小的系統。
  • CONFIG_SLUB:自 2.6.23 版起預設使用,比 SLAB 更簡單,擴充套件性更好。

Linux 核心記憶體分配:SLAB 分配器與 Kmalloc 分配器

Linux 核心提供了多種記憶體分配機制,其中最為重要的兩種是 SLAB 分配器和 Kmalloc 分配器。本文將探討這兩種分配器的工作原理、API 使用方法以及實驗室練習中的實際應用。

SLAB 分配器

SLAB 分配器是一種用於管理核心物件記憶體分配的子系統。它提供了一個通用的介面,用於建立和銷毀記憶體快取。SLAB 分配器的主要優點是可以提高記憶體分配的效率,減少記憶體碎片。

SLAB 分配器 API

  1. 建立記憶體快取kmem_cache_create() 函式用於建立一個新的記憶體快取。

    struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void*));
    
    • name:用於在 /proc/slabinfo 中識別該快取的字串。
    • size:在該快取中建立的物件大小。
    • align:為每個物件新增的額外空間,用於儲存其他資料。
    • flags:SLAB 旗標。
    • ctor:用於初始化物件的建構函式。
  2. 銷毀記憶體快取kmem_cache_destroy() 函式用於銷毀一個記憶體快取。

    void kmem_cache_destroy(struct kmem_cache *cp);
    
  3. 分配物件kmem_cache_alloc() 函式用於從快取中分配一個物件。

    void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags);
    
  4. 釋放物件kmem_cache_free() 函式用於釋放一個物件並將其放回快取中。

    void kmem_cache_free(struct kmem_cache *s, void *x);
    

SLAB 分配器工作原理

當建立一個新的記憶體快取時,SLAB 分配器會根據物件的大小計算所需的記憶體頁數,並建立多個 slab 來儲存物件。當需要分配一個新物件時,SLAB 分配器會檢查快取中是否有空閒的物件。如果有,則直接傳回;否則,SLAB 分配器會向頁面分配器請求更多的記憶體頁,並建立新的 slab 來儲存新的物件。

Kmalloc 分配器

Kmalloc 分配器是驅動程式中最常用的記憶體分配器。它依賴於頁面分配器和 SLAB 分配器,用於分配連續的實體記憶體。

Kmalloc 分配器 API

  1. 分配記憶體kmalloc()kzalloc() 函式用於分配記憶體。

    void *kmalloc(size_t size, int flags);
    void *kzalloc(size_t size, gfp_t flags);
    
    • size:要分配的位元組數。
    • flags:使用與頁面分配器相同的旗標。
  2. 釋放記憶體kfree() 函式用於釋放使用 kmalloc() 分配的記憶體。

    void kfree(const void *objp);
    
  3. 資源管理分配devm_kmalloc()devm_kzalloc() 函式用於將記憶體分配與裝置繫結,當裝置被解除安裝時,自動釋放分配的記憶體。

    void *devm_kmalloc(struct device *dev, size_t size, int flags);
    void *devm_kzalloc(struct device *dev, size_t size, int flags);
    

實驗室練習:鏈結串列記憶體分配

在這個實驗室中,我們將在核心記憶體中建立一個環形單鏈結串列,每個節點包含一個緩衝區指標和一個指向下一個節點的指標。我們將使用 devm_kmalloc() 函式來分配每個節點的記憶體。

鏈結串列結構定義

typedef struct dnode {
    char *buffer;
    struct dnode *next;
} data_node;

typedef struct lnode {
    data_node *head;
    data_node *cur_write_node;
    data_node *cur_read_node;
    int cur_read_offset;
    int cur_write_offset;
} liste;

建立鏈結串列

newNode = devm_kmalloc(&device->dev, sizeof(data_node), GFP_KERNEL);

鏈結串列操作

  • 寫入操作:驅動程式的 write() 回呼函式將使用者空間寫入的字元填入鏈結串列的每個節點緩衝區中,當節點緩衝區滿時,移動到下一個節點。
  • 讀取操作:驅動程式的 read() 回呼函式從第一個寫入的節點緩衝區開始讀取,直到最後一個寫入的節點緩衝區。讀取完成後,所有鏈結串列指標重置到第一個節點。

內容解密:

  1. 使用 devm_kmalloc() 分配節點記憶體,無需手動釋放,當模組被解除安裝時自動釋放。
  2. 鏈結串列結構設計允許高效地管理和存取節點資料。
  3. 驅動程式的 write()read() 回呼函式實作了對鏈結串列的有效操作。

Linux 核心記憶體分配實務:以環形鏈結串列為例

在 Linux 核心開發中,動態記憶體分配是一項關鍵技術。本文將探討如何使用 devm_kmalloc 函式在核心空間中分配記憶體,並透過一個環形鏈結串列的實作範例,展示其在實際驅動程式開發中的應用。

環形鏈結串列的設計與實作

本範例實作了一個環形鏈結串列,用於在核心空間中緩衝資料。該資料結構包含兩個主要部分:節點結構 data_node 和鏈結串列管理結構 liste

資料結構定義

typedef struct dnode {
    char *buffer;
    struct dnode *next;
} data_node;

typedef struct lnode {
    data_node *head;
    data_node *cur_write_node;
    data_node *cur_read_node;
    int cur_read_offset;
    int cur_write_offset;
} liste;

內容解密:

  1. data_node 結構代表鏈結串列中的一個節點,包含一個指向資料緩衝區的指標 buffer 和指向下一個節點的指標 next
  2. liste 結構用於管理整個環形鏈結串列,包含指向頭節點的指標 head、目前寫入節點 cur_write_node 和讀取節點 cur_read_node,以及對應的偏移量。

核心記憶體分配與鏈結串列建立

createlist 函式中,我們使用 devm_kmalloc 來動態分配記憶體,建立環形鏈結串列。

程式碼片段

static int createlist(struct platform_device *pdev) {
    data_node *newNode, *previousNode, *headNode;
    int i;

    newNode = devm_kmalloc(&pdev->dev, sizeof(data_node), GFP_KERNEL);
    if (newNode)
        newNode->buffer = devm_kmalloc(&pdev->dev, BlockSize * sizeof(char), GFP_KERNEL);
    if (!newNode || !newNode->buffer)
        return -ENOMEM;

    // 初始化第一個節點並建立環形鏈結串列
    newListe.head = newNode;
    headNode = newNode;
    previousNode = newNode;

    for (i = 1; i < BlockNumber; i++) {
        newNode = devm_kmalloc(&pdev->dev, sizeof(data_node), GFP_KERNEL);
        if (newNode)
            newNode->buffer = devm_kmalloc(&pdev->dev, BlockSize * sizeof(char), GFP_KERNEL);
        if (!newNode || !newNode->buffer)
            return -ENOMEM;

        previousNode->next = newNode;
        previousNode = newNode;
    }
    newNode->next = headNode; // 建立環形鏈結

    // 初始化讀寫指標
    newListe.cur_read_node = headNode;
    newListe.cur_write_node = headNode;
    newListe.cur_read_offset = 0;
    newListe.cur_write_offset = 0;

    return 0;
}

內容解密:

  1. 使用 devm_kmalloc 分配 data_node 結構和其 buffer,並檢查分配是否成功。
  2. 初始化第一個節點並設定頭指標。
  3. 透過迴圈建立剩餘的節點,並將它們串聯成環形鏈結串列。
  4. 初始化讀寫節點和偏移量。

驅動程式的檔案操作實作

本範例實作了基本的檔案操作函式,包括 my_dev_writemy_dev_read

寫入操作實作

static ssize_t my_dev_write(struct file *file, const char __user *buf, size_t size, loff_t *offset) {
    int size_to_copy;

    if (size < BlockSize - newListe.cur_write_offset)
        size_to_copy = size;
    else
        size_to_copy = BlockSize - newListe.cur_write_offset;

    if (copy_from_user(newListe.cur_write_node->buffer + newListe.cur_write_offset, buf, size_to_copy))
        return -EFAULT;

    newListe.cur_write_offset += size_to_copy;
    if (newListe.cur_write_offset == BlockSize) {
        newListe.cur_write_node = newListe.cur_write_node->next;
        newListe.cur_write_offset = 0;
    }

    return size_to_copy;
}

內容解密:

  1. 計算可寫入的大小,並使用 copy_from_user 將使用者空間的資料複製到核心空間的緩衝區。
  2. 更新寫入偏移量和寫入節點。

讀取操作實作

static ssize_t my_dev_read(struct file *file, char __user *buf, size_t count, loff_t *offset) {
    int size_to_copy;

    if (*offset < size_to_read) {
        if (count < BlockSize - newListe.cur_read_offset)
            size_to_copy = count;
        else
            size_to_copy = BlockSize - newListe.cur_read_offset;

        if (copy_to_user(buf, newListe.cur_read_node->buffer + newListe.cur_read_offset, size_to_copy))
            return -EFAULT;

        newListe.cur_read_offset += size_to_copy;
        if (newListe.cur_read_offset == BlockSize) {
            newListe.cur_read_node = newListe.cur_read_node->next;
            newListe.cur_read_offset = 0;
        }

        return size_to_copy;
    } else {
        return 0; // 無資料可讀
    }
}

內容解密:

  1. 檢查是否有資料可讀,並計算可讀取的大小。
  2. 使用 copy_to_user 將核心空間的資料複製到使用者空間。
  3. 更新讀取偏移量和讀取節點。