啟動期間的記憶體管理之引導分配器bootmem--Linux記憶體管理(十)

来源:https://www.cnblogs.com/linhaostudy/archive/2018/11/30/10043876.html
-Advertisement-
Play Games

在記憶體管理的上下文中, 初始化(initialization)可以有多種含義. 在許多CPU上, 必須顯式設置適用於Linux內核的記憶體模型. 例如在x86_32上需要切換到保護模式, 然後內核才能檢測到可用記憶體和寄存器. 而我們今天要講的bootmem分配器就是系統初始化階段使用的記憶體分配器. 為 ...


在記憶體管理的上下文中, 初始化(initialization)可以有多種含義. 在許多CPU上, 必須顯式設置適用於Linux內核的記憶體模型. 例如在x86_32上需要切換到保護模式, 然後內核才能檢測到可用記憶體和寄存器.

而我們今天要講的bootmem分配器就是系統初始化階段使用的記憶體分配器.

為什麼要使用bootmem分配器,記憶體管理不是有buddy系統和slab分配器嗎?由於在系統初始化的時候需要執行一些記憶體管理,記憶體分配的任務,這個時候buddy系統,slab分配器等並沒有被初始化好,此時就引入了一種記憶體管理器bootmem分配器在系統初始化的時候進行記憶體管理與分配,當buddy系統和slab分配器初始化好後,在mem_init()中對bootmem分配器進行釋放,記憶體管理與分配由buddy系統,slab分配器等進行接管。

bootmem分配器使用一個bitmap來標記物理頁是否被占用,分配的時候按照第一適應的原則,從bitmap中進行查找,如果這位為1,表示已經被占用,否則表示未被占用。為什麼系統運行的時候不使用bootmem分配器呢?bootmem分配器每次在bitmap中進行線性搜索,效率非常低,而且在記憶體的起始端留下許多小的空閑碎片,在需要非常大的記憶體塊的時候,檢查點陣圖這一過程就顯得代價很高。bootmem分配器是用於在啟動階段分配記憶體的,對該分配器的需求集中於簡單性方面,而不是性能和通用性.

2. 引導記憶體分配器bootmem概述

由於硬體配置多種多樣, 所以在編譯時就靜態初始化所有的內核存儲結構是不現實的.

bootmem分配器是系統啟動初期的記憶體分配方式,在耳熟能詳的伙伴系統建立前記憶體都是利用bootmem分配器來分配的,伙伴系統框架建立起來後,bootmem會過度到伙伴系統.

2.1 初始化階段的引導記憶體分配器bootmem

在啟動過程期間, 儘管記憶體管理尚未初始化, 但是內核仍然需要分配記憶體以創建各種數據結構. 因此在系統啟動過程期間, 內核使用了一個額外的簡化形式的記憶體管理模塊引導記憶體分配器(boot memory allocator–bootmem分配器), 用於在啟動階段早期分配記憶體, 而在系統初始化完成後, 該分配器被內核拋棄, 然後初始化了一套新的更加完善的記憶體分配器.

顯然, 對該記憶體分配器的需求集中於簡單性方面, 而不是性能和通用性, 它僅用於初始化階段. 因此內核開發者決定實現一個最先適配(first-first)分配器用於在啟動階段管理記憶體. 這是可能想到的最簡單的方式.

引導記憶體分配器(boot memory allocator–bootmem分配器)基於最先適配(first-first)分配器的原理(這兒是很多系統的記憶體分配所使用的原理), 使用一個點陣圖來管理頁, 以點陣圖代替原來的空閑鏈表結構來表示存儲空間, 點陣圖的比特位的數目與系統中物理記憶體頁面數目相同. 若點陣圖中某一位是1, 則標識該頁面已經被分配(已用頁), 否則表示未被占有(未用頁).

在需要分配記憶體時, 分配器逐位的掃描點陣圖, 直至找到一個能提供足夠連續頁的位置, 即所謂的最先最佳(first-best)或最先適配位置.

該分配機制通過記錄上一次分配的頁面幀號(PFN)結束時的偏移量來實現分配大小小於一頁的空間, 連續的小的空閑空間將被合併存儲在一頁上.

2.2 為什麼需要bootmem

2.3 為什麼在系統運行時拋棄bootmem

當系統運行時, 為何不繼續使用bootmem分配機制呢?

  • 其中一個關鍵原因在於 : 但它每次分配都必須從頭掃描點陣圖, 每次通過對記憶體域進行線性搜索來實現分配.
  • 其次首先適應演算法容易在記憶體的起始斷留下許多小的空閑碎片, 在需要分配較大的空間頁時, 檢查點陣圖的成本將是非常高的.

引導記憶體分配器bootmem分配器簡單卻非常低效, 因此在內核完全初始化之後, 不能將該分配器繼續歐諾個與記憶體管理, 而伙伴系統(連同slab, slub或者slob分配器)是一個好很多的備選方案.

3 引導記憶體分配器數據結構

內核用bootmem_data表示引導記憶體區域

即使是初始化用的最先適配分配器也必須使用一些數據結構存, 內核為系統中每一個結點都提供了一個struct bootmem_data結構的實例, 用於bootmem的記憶體管理. 它含有引導記憶體分配器給結點分配記憶體時所需的信息. 當然, 這時候記憶體管理還沒有初始化, 因而該結構所需的記憶體是無法動態分配的, 必須在編譯時分配給內核.

在UMA系統上該分配的實現與CPU無關, 而NUMA系統記憶體結點與CPU相關聯, 因此採用了特定體繫結構的解決方法.

3.1 bootmem_data描述記憶體引導區

bootmem_data的結構定義在include/linux/bootmem.h?v=4.7, line 28, 其定義如下所示

#ifndef CONFIG_NO_BOOTMEM
/*
* node_bootmem_map is a map pointer - the bits represent all physical 
* memory pages (including holes) on the node.
*/
typedef struct bootmem_data {
       unsigned long node_min_pfn;
       unsigned long node_low_pfn;
       void *node_bootmem_map;
       unsigned long last_end_off;
       unsigned long hint_idx;
       struct list_head list;
} bootmem_data_t;

extern bootmem_data_t bootmem_node_data[];

#endif
欄位 描述
node_min_pfn 節點起始地址
node_low_pfn 低端記憶體最後一個page的頁幀號
node_bootmem_map 指向記憶體中點陣圖bitmap所在的位置
last_end_off 分配的最後一個頁內的偏移,如果該頁完全使用,則offset為0
hint_idx
list

bootmem的點陣圖建立在從start_pfn開始的地方, 也就是說, 內核映像終點_end上方的地方. 這個點陣圖用來管理低區(例如小於 896MB), 因為在0到896MB的範圍內, 有些頁面可能保留, 有些頁面可能有空洞, 因此, 建立這個點陣圖的目的就是要搞清楚哪一些物理頁面是可以動態分配的

  • node_bootmem_map就是一個指向點陣圖的指針. node_min_pfn表示存放bootmem點陣圖的第一個頁面(即內核映像結束處的第一個頁面)
  • node_low_pfn 表示物理記憶體的頂點, 最高不超過896MB

4 初始化引導分配器

系統是從start_kernel開始啟動的, 在啟動過程中通過調用體繫結構相關的setup_arch函數, 來獲取初始化引導記憶體分配器所需的參數信息, 各種體繫結構都有對應的函數來獲取這些信息, 在獲取信息完成後, 內核首先初始化了bootmem自身, 然後接著又用bootmem分配和初始化了記憶體結點和管理域, 因此初始化bootmem的工作主要分成兩步

  • 初始化bootmem自身的數據結構
  • 用bootmem初始化記憶體結點管理域

bootmem分配器的初始化是一個特定於體繫結構的過程, 此外還取決於系統的記憶體佈局

系統是從start_kernel開始啟動的, 在啟動過程中通過調用體繫結構相關的setup_arch函數, 來獲取初始化引導記憶體分配器所需的參數信息, 各種體繫結構都有對應的函數來獲取這些信息.

4.1 IA-32的初始化

在使用bootmem, 內核在setup_arch函數中通過setup_memory來分析檢測到的記憶體區, 以找到低端記憶體區中最大的頁幀號。由於高端記憶體處理太麻煩,由此對bootmem分配器無用。全局變數max_low_pfn保存了可映射的最高頁的編號。內核會在啟動日誌中報告找到的記憶體的數量。

5 bootmem分配記憶體介面

bootmem提供了各種函數用於在初始化期間分配記憶體.

儘管mm/bootmem.c中提供了一些了的記憶體分配函數,但是這些函數大多數以__下劃線開頭, 這個標識告訴我們儘量不要使用他們, 他們過於底層, 往往是不安全的, 因此特定於某個體系架構的代碼並沒有直接調用它們,而是通過linux/bootmem.h提供的一系列的巨集

5.1 NUMA結構的分配函數

首先我們講解一下子在UMA系統中, 可供使用的函數.

5.1.1 從ZONE_NORMAL區域分配函數

下麵列出的這些函數可以從ZONE_NORMAL記憶體域分配指向大小的記憶體

函數 描述 定義
alloc_bootmem(size) 按照指定大小在ZONE_NORMAL記憶體域分配函數. 數據是對齊的, 這使得記憶體或者從可適用於L1高速緩存的理想位置開始 alloc_bootmem
__alloc_bootmem
___alloc_bootmem
alloc_bootmem_align(x, align) 同alloc_bootmem函數, 按照指定大小在ZONE_NORMAL記憶體域分配函數, 並按照align進行數據對齊 alloc_bootmem_align
基於__alloc_bootmem實現
alloc_bootmem_pages(size)) 同alloc_bootmem函數, 按照指定大小在ZONE_NORMAL記憶體域分配函數, 其中_page只是指定數據的對其方式從頁邊界(__pages)開始 alloc_bootmem_pages
基於__alloc_bootmem實現
alloc_bootmem_nopanic(size) alloc_bootmem_nopanic是最基礎的通用的,一個用來儘力而為分配記憶體的函數,它通過list_for_each_entry在全局鏈表bdata_list中分配記憶體. alloc_bootmem和alloc_bootmem_nopanic類似,它的底層實現首先通過alloc_bootmem_nopanic函數分配記憶體,但是一旦記憶體分配失敗,系統將通過panic(“Out of memory”)拋出信息,並停止運行 alloc_bootmem_nopanic
__alloc_bootmem_nopanic
___alloc_bootmem_nopanic

這些函數的定義在include/linux/bootmem.h

http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L122
#define alloc_bootmem(x) \
    __alloc_bootmem(x, SMP_CACHE_BYTES, BOOTMEM_LOW_LIMIT)
#define alloc_bootmem_align(x, align) \
    __alloc_bootmem(x, align, BOOTMEM_LOW_LIMIT)
#define alloc_bootmem_nopanic(x) \
    __alloc_bootmem_nopanic(x, SMP_CACHE_BYTES, BOOTMEM_LOW_LIMIT)
#define alloc_bootmem_pages(x) \
    __alloc_bootmem(x, PAGE_SIZE, BOOTMEM_LOW_LIMIT)
#define alloc_bootmem_pages_nopanic(x) \
    __alloc_bootmem_nopanic(x, PAGE_SIZE, BOOTMEM_LOW_LIMIT)

5.1.2 從ZONE_DMA區域分配函數

下麵的函數可以從ZONE_DMA中分配記憶體

函數 描述 定義
alloc_bootmem_low(size) 按照指定大小在ZONE_DMA記憶體域分配函數. 類似於alloc_bootmem, 數據是對齊的 alloc_bootmem_low_pages_nopanic
底層基於___alloc_bootmem
alloc_bootmem_low_pages_nopanic(size) 按照指定大小在ZONE_DMA記憶體域分配函數. 類似於alloc_bootmem_pages, 數據在頁邊界對齊, 並且錯誤後不輸出panic alloc_bootmem_low_pages_nopanic
底層基於__alloc_bootmem_low_nopanic
alloc_bootmem_low_pages(size) 按照指定大小在ZONE_DMA記憶體域分配函數. 類似於alloc_bootmem_pages, 數據在頁邊界對齊 alloc_bootmem_low_pages
底層基於__alloc_bootmem_low_nopanic

這些函數的定義在include/linux/bootmem.h

#define alloc_bootmem_low(x) \
    __alloc_bootmem_low(x, SMP_CACHE_BYTES, 0)
#define alloc_bootmem_low_pages_nopanic(x) \
    __alloc_bootmem_low_nopanic(x, PAGE_SIZE, 0)
#define alloc_bootmem_low_pages(x) \
    __alloc_bootmem_low(x, PAGE_SIZE, 0)

5.1.3 函數實現方式

通過分析我們可以看到alloc_bootmem_nopanic的底層實現函數__alloc_bootmem_nopanic實現了一套最基礎的記憶體分配函數, 而___alloc_bootmem函數則通過_alloc_bootmem_nopanic函數實現, 它首先通過_alloc_bootmem_nopanic函數分配記憶體,但是一旦記憶體分配失敗,系統將通過panic("Out of memory")拋出信息,並停止運行, 其他的記憶體分配函數除了都是基於alloc_bootmem_nopanic族的函數, 都是基於__alloc_bootmem的. 那麼所有的函數都是間接的基於___alloc_bootmem_nopanic實現的

static void * __init ___alloc_bootmem(unsigned long size, unsigned long align,
                    unsigned long goal, unsigned long limit)
{
    void *mem = ___alloc_bootmem_nopanic(size, align, goal, limit);

    if (mem)
        return mem;
    /*
     * Whoops, we cannot satisfy the allocation request.
     */
    pr_alert("bootmem alloc of %lu bytes failed!\n", size);
    panic("Out of memory");
    return NULL;
}

那麼我們現在就進入分配函數的核心___alloc_bootmem_node_nopanic, 它定義在mm/nobootmem.c?v=4.7, line 317

void * __init ___alloc_bootmem_node_nopanic(pg_data_t *pgdat,
                unsigned long size, unsigned long align,
                unsigned long goal, unsigned long limit)
{
    void *ptr;

    if (WARN_ON_ONCE(slab_is_available()))
        return kzalloc(size, GFP_NOWAIT);
again:

    /* do not panic in alloc_bootmem_bdata() */
    if (limit && goal + size > limit)
        limit = 0;

    ptr = alloc_bootmem_bdata(pgdat->bdata, size, align, goal, limit);
    if (ptr)
        return ptr;

    ptr = alloc_bootmem_core(size, align, goal, limit);
    if (ptr)
        return ptr;

    if (goal) {
        goal = 0;
        goto again;
    }

    return NULL;
}

我們可以看到UMA下底層的分配函數_alloc_bootmem_nopanic與NUMA下的函數_alloc_bootmem_node_nopanic實現方式基本類似. 參數也基本相同

參數 描述
pgdat 要分配的結點, 在UMA結構中, 它被預設掉了, 因此其預設值是contig_page_data
size 要分配的記憶體區域大小
align 要求對齊的位元組數. 如果分配的空間比較小, 就用SMP_CACHE_BYTES, 它一般是硬體一級高速緩存的對齊方式, 而PAGE_SIZE則表示要在頁邊界對齊
goal 最佳分配的起始地址, 一般設置(normal)BOOTMEM_LOW_LIMIT / (low)ARCH_LOW_ADDRESS_LIMIT

5.2 __alloc_memory_core進行記憶體分配

函數 描述 定義
alloc_bootmem_bdata - mm/bootmem.c?v=4.7, line 500
alloc_bootmem_core - mm/bootmem.c, line 607

__alloc_memory_core函數的功能相對而言很廣泛(在啟動期間不需要太高的效率), 該函數基於最先適配演算法, 但是該分配器不僅可以分配整個記憶體頁, 還能分配頁的一部分. 它遍歷所有的bootmem list然後找到一個合適的記憶體區域, 然後通過 alloc_bootmem_bdata來完成分配

該函數主要執行如下操作

  • list_for_each_entry從goal開始掃描為圖, 查找滿足分配請求的空閑記憶體區

  • 然後通過alloc_bootmem_bdata完成記憶體的分配
    1. 如果目標頁緊接著上一次分配的頁即last_end_off, 則內核會判斷所需的記憶體(包括對齊數據所需的記憶體)是否能夠在上一頁分配或者從上一頁開始分配
    2. 新分配的頁在點陣圖中對應位置設置為1,, 如果該頁未完全分配, 則相應的偏移量保存在bootmem_data->last_end_off中; 否則, 該值設為0

6 bootmem釋放記憶體

內核提供了free_bootmem函數來釋放記憶體

它需要兩個參數:需要釋放的記憶體區的起始地址和長度。不出意外,NUMA系統上等價函數的名稱為free_bootmem_node,它需要一個額外的參數來指定結點

//  http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L422
void free_bootmem(unsigned long addr, unsigned long size);
void free_bootmem_node(pg_data_t *pgdat, unsigned long addr, unsigned long size);

7 停用bootmem

在系統初始化進行到伙伴系統分配器能夠承擔記憶體管理的責任後,必須停用bootmem分配器,畢竟不能同時用兩個分配器管理記憶體。在UMA和NUMA系統上,停用是由free_all_bootmem完成。在伙伴系統建立之後,特定於體繫結構的初始化代碼需要調用這個函數

首先掃描bootmem分配器的頁點陣圖,釋放每個未用的頁。到伙伴系統的介面是__free_pages_bootmem函數,該函數對每個空閑頁調用。該函數內部依賴於標準函數__free_page。它使得這些頁併入伙伴系統的數據結構,在其中作為空閑頁管理,可用於分配。

在頁點陣圖已經完全掃描之後,它占據的記憶體空間也必須釋放。此後,只有伙伴系統可用於記憶體分配。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、前言 最近需要在虛擬機當中裝個 的系統,但是在虛擬機安裝的時候,並不像 那樣能一步步的進行配置,因此導致裝好後的虛擬機是動態IP地址。而該虛擬機要作為測試伺服器來使用,所以要將IP地址設置為靜態IP。 二、環境 系統:Ubuntu Server 16.04 虛擬機:VM 15.X 三、解決方案 ...
  • nxpSecBoot是一個專為NXP MCU安全加密啟動而設計的工具,其特性與NXP MCU里BootROM功能相對應,目前主要支持i.MXRT系列MCU晶元,與NXP官方提供的標準安全加密配套工具集(OpenSSL, CST, sdphost, blhost, elftosb, BD, MfgTo... ...
  • umask命令 作用:用於顯示、設置文件的預設許可權 格式:umask [-S] -S表示以rwx形式顯示新建文件預設許可權 系統的預設掩碼是0022 文件創建時的預設許可權 = 0666 - umask 目錄創建時的預設許可權 = 0777 - umask 所以創建文件的許可權是 0666 - 0022 = ...
  • $ : > filename $ > filename $ echo "" > filename $ echo > filename $ cat /dev/null > filename ...
  • 項目中遇到的比較奇葩的問題,從網上找到一份源碼,https://blog.csdn.net/qq125096885/article/details/70766206 稍微整理了下,VS可以直接編譯 ...
  • 安裝mysql 1、檢測是否已安裝mysql [root@localhost bin]# rpm -qa | grep mysql mysql-libs-5.1.71-1.el6.i686 [root@localhost bin] 2、安裝mysql和mysql-server [root@local ...
  • atril、gimp和evince,三者均可以打開application/pdf格式文件。gimp為一款圖像處理軟體;atril為mate環境下常用的文檔查看器;evince為gnome環境下常用的文檔查看器。 某mate桌面和gnome伺服器環境中配置文件mimeapps.list都定義了appl ...
  • naconda修改國內鏡像源 國外網路有時太慢,可以通過配置把下載源改為國內的通過 conda config 命令生成配置文件,這裡使用清華的鏡像: https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ 首先,打開Anaconda Prom ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...