1. 程式人生 > 實用技巧 >Linux 記憶體管理【轉】

Linux 記憶體管理【轉】

轉自:https://www.jianshu.com/p/eecbb1506eee

Linux 記憶體管理

1 頁的概念

linux 核心中把物理頁作為記憶體分配的最小單位,32位CPU 頁的大小通常為4K,64位的CPU通常支援8K的也。記憶體管理單元MMU 同樣以頁為大小分配記憶體。

2 核心虛擬地址分割槽和實體記憶體分割槽

在32位核心中,核心虛擬地址空間為0-4G,其中使用者態為1-3G空間,核心態為3G-4G,核心空間根據實體地址的特性大概可以分為三個區:

描述32位系統實體記憶體大小
ZONE_DMA 和硬體操作相關的記憶體區域 < 16M
ZONE_NORMAL 核心正常對映的物理頁 16 - 896M
ZONE_HIGH 高階記憶體,由於核心空間大小的原理部分頁不能永久的對映到核心,需要動態對映的 > 896M

下面的圖描述了核心地址空間和實體記憶體的對映關係:


32位 核心記憶體分割槽

Linux 核心啟動後的mm 的初始化過程:

/*
 * Set up kernel memory allocators
 */
static void __init mm_init(void)
{
    /*
     * page_ext requires contiguous pages,
     * bigger than MAX_ORDER unless SPARSEMEM.
     */
    page_ext_init_flatmem();
    mem_init();
    kmem_cache_init();
    percpu_init_late();
    pgtable_init();
    vmalloc_init();
    ioremap_huge_init();
}

3夥伴系統演算法

3.1 簡介

在實際應用中,經常需要分配一組連續的頁,而頻繁地申請和釋放不同大小的連續頁,必然導致在已分配頁框的記憶體塊中分散了許多小塊的空閒頁框。這樣,即使這些頁框是空閒的,其他需要分配連續頁框的應用也很難得到滿足。為了避免出現這種情況,Linux核心中引入了夥伴系統演算法(buddy system)。把所有的空閒頁框分組為11個塊連結串列,每個塊連結串列分別包含大小為1,2,4,8,16,32,64,128,256,512和1024個連續頁框的頁框塊。最大可以申請1024個連續頁框,對應4MB大小的連續記憶體。每個頁框塊的第一個頁框的實體地址是該塊大小的整數倍。

假設要申請一個256個頁框的塊,先從256個頁框的連結串列中查詢空閒塊,如果沒有,就去512個頁框的連結串列中找,找到了則將頁框塊分為2個256個頁框的塊,一個分配給應用,另外一個移到256個頁框的連結串列中。如果512個頁框的連結串列中仍沒有空閒塊,繼續向1024個頁框的連結串列查詢,如果仍然沒有,則返回錯誤。頁框塊在釋放時,會主動將兩個連續的頁框塊合併為一個較大的頁框塊。

mem_init() 函式中會把核心啟動後的空閒記憶體用buddy 系統管理。

參考:mem_init bootmem 遷移至夥伴系統

3.2 夥伴系統演算法分配函式

mem_init 初始化完夥伴系統後通過 alloc_page(s) 函式分配夥伴系統記憶體池的記憶體。

函式描述
struct page * alloc_page(unsigned int gfp_mask) 分配一頁實體記憶體並返回該頁實體記憶體的page結構指標
struct page * alloc_pages(unsigned int gfp_mask, unsigned int order) 分配2的order次方連續的物理頁並返回分配的第一個物理頁的page結構指標
unsigned long get_free_page(unsigned int gfp_mask) 只分配一頁,返回頁的邏輯地址
unsigned long __get_free_pages(unsigned int gfp_mask, unsigned int order) 分配 2的order頁,返回也是邏輯地址

3.3 get_free_page(s)與alloc_page(s)的差異

alloc_page alloc_pages 分配後還不能直接使用, 需要得到該頁對應的虛擬地址

  • void *page_address(struct page *page);
  • 低端記憶體的對映方式:__va((unsigned long)(page - mem_map) << 12)
  • 高階記憶體到對映方式:struct page_address_map分配一個動態結構來管理高階記憶體。(核心是訪問不到vma的3G以下的虛擬地址的) 具體對映由kmap / kmap_atomic執行。

get_free_page(s)與alloc_page(s)系列最大的區別是無法申請高階記憶體,因為它返回到是一個邏輯地址,而高階記憶體是需要額外對映才可以

Android x86 的buffyinfo.
以Normal區域進行分析,第二列值為459,表示當前系統中normal區域,可用的連續兩頁的記憶體大小為459*2^1*PAGE_SIZE;第三列值為52,表示當前系統中normal區域,可用的連續四頁的記憶體大小為52*2^2*PAGE_SIZE

generic_x86:/ # cat /proc/buddyinfo
Node 0, zone      DMA      4      1      2      2      3      2      3      1      2      0      1
Node 0, zone   Normal   1186    459    220    142     25     13      2      0      1      2    138
Node 0, zone  HighMem     87     74     12      9      0      1      1      0      0      0      0

4 Slab 記憶體分配演算法

Slab 記憶體分配演算法 和Java中的物件池是一個概念。採用buddy演算法,解決了外碎片問題,這種方法適合大塊記憶體請求,不適合小記憶體區請求

4.1 Slab 記憶體分配演算法

slab分配器源於 Solaris 2.4 的分配演算法,工作於物理記憶體頁框分配器之上,管理特定大小物件的快取,進行快速而高效的記憶體分配。slab分配器為每種使用的核心物件建立單獨的緩衝區。Linux 核心已經採用了夥伴系統管理實體記憶體頁框,因此 slab分配器直接工作於夥伴系統之上。每種緩衝區由多個 slab 組成,每個 slab就是一組連續的實體記憶體頁框,被劃分成了固定數目的物件。根據物件大小的不同,預設情況下一個 slab 最多可以由 1024個頁框構成。出於對齊等其它方面的要求,slab 中分配給物件的記憶體可能大於使用者要求的物件實際大小,這會造成一定的記憶體浪費。

Linux 所使用的 slab 分配器的基礎是 Jeff Bonwick 為SunOS 作業系統首次引入的一種演算法。Jeff的分配器是圍繞物件快取進行的。在核心中,會為有限的物件集(例如檔案描述符和其他常見結構)分配大量記憶體。Jeff發現對核心中普通物件進行初始化所需的時間超過了對其進行分配和釋放所需的時間。因此他的結論是不應該將記憶體釋放回一個全域性的記憶體池,而是將記憶體保持為針對特定目而初始化的狀態。例如,如果記憶體被分配給了一個互斥鎖,那麼只需在為互斥鎖首次分配記憶體時執行一次互斥鎖初始化函式(mutex_init)即可。後續的記憶體分配不需要執行這個初始化函式,因為從上次釋放和呼叫析構之後,它已經處於所需的狀態中了。

4.2 Slab 記憶體結構

kmem_cache_alloc 分配的所有的記憶體塊在核心中以連結串列的形式組織。
kmem_cache_alloc 從buddy系統分配到記憶體後,在內部被分為 slab 單元,這是一段連續的記憶體塊(通常都是頁面)。所有的物件都分配在這些slab 單元上,這些slab 單元被組織為三個連結串列:

  1. slabs_full 完全分配的 slab
  2. slabs_partial 部分分配的 slab
  3. slabs_free 可以回收的 slab

4.3 slab 著色區和slab 結構

每個Slab的首部都有一個小小的區域是不用的,稱為“著色區(coloring area)”。著色區的大小使Slab中的每個物件的起始地址都按快取記憶體中的”快取行(cache line)”大小進行對齊(80386的一級快取記憶體行大小為16位元組,Pentium為32位元組)。因為Slab是由1個頁面或多個頁面(最多為32)組成,因此,每個Slab都是從一個頁面邊界開始的,它自然按快取記憶體的緩衝行對齊。但是,Slab中的物件大小不確定,設定著色區的目的就是將Slab中第一個物件的起始地址往後推到與緩衝行對齊的位置。每個Slab上最後一個物件以後也有個小小的區是不用的,這是對著色區大小的補償,其大小取決於著色區的大小,以及Slab與其每個物件的相對大小。

slab 記憶體分配

4.4 Slab 記憶體函式

mm_init --> kmem_cache_init(); kernel 初始化

  • kmem_cache_t* xx_cache; // 連結串列頭
  • 建立: xx_cache = kmem_cache_create("name", sizeof(struct xx), SLAB_HWCACHE_ALIGN, NULL, NULL);
  • 分配: kmem_cache_alloc(xx_cache, GFP_KERNEL);
  • 釋放: kmem_cache_free(xx_cache, addr);
    slab 記憶體用結構體 kmem_cache_t 表示:

4.5 slabinfo物件

從 /proc/slabinfo 中看一看出,核心為大結構體使用了slab 快取。如ext4_inode_cache vm_area_struct task_struct等。

generic_x86:/ # cat /proc/slabinfo
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
...
ext4_inode_cache    2025   2025    632   25    4 : tunables    0    0    0 : slabdata     81     81      0
ext4_allocation_context    156    156    104   39    1 : tunables    0    0    0 : slabdata      4      4      0
ext4_prealloc_space    224    224     72   56    1 : tunables    0    0    0 : slabdata      4      4      0
ext4_io_end          408    408     40  102    1 : tunables    0    0    0 : slabdata      4      4      0
ext4_extent_status   2048   2048     32  128    1 : tunables    0    0    0 : slabdata     16     16      0
...
vm_area_struct     20791  22402     88   46    1 : tunables    0    0    0 : slabdata    487    487      0
mm_struct             85     85    480   17    2 : tunables    0    0    0 : slabdata      5      5      0

...
task_struct          621    621   1184   27    8 : tunables    0    0    0 : slabdata     23     23      0
...
kmalloc-8192          28     28   8192    4    8 : tunables    0    0    0 : slabdata      7      7      0
kmalloc-4096          96    104   4096    8    8 : tunables    0    0    0 : slabdata     13     13      0
kmalloc-2048         128    128   2048   16    8 : tunables    0    0    0 : slabdata      8      8      0
kmalloc-1024         336    336   1024   16    4 : tunables    0    0    0 : slabdata     21     21      0
kmalloc-512          752    752    512   16    2 : tunables    0    0    0 : slabdata     47     47      0
kmalloc-256          698    752    256   16    1 : tunables    0    0    0 : slabdata     47     47      0
kmalloc-192          903    903    192   21    1 : tunables    0    0    0 : slabdata     43     43      0
kmalloc-128         1760   1760    128   32    1 : tunables    0    0    0 : slabdata     55     55      0
kmalloc-96          2100   2100     96   42    1 : tunables    0    0    0 : slabdata     50     50      0
kmalloc-64         14272  14272     64   64    1 : tunables    0    0    0 : slabdata    223    223      0
kmalloc-32         26182  28416     32  128    1 : tunables    0    0    0 : slabdata    222    222      0
kmalloc-16         15360  15360     16  256    1 : tunables    0    0    0 : slabdata     60     60      0
kmalloc-8           6656   6656      8  512    1 : tunables    0    0    0 : slabdata     13     13      0
kmem_cache_node      128    128     32  128    1 : tunables    0    0    0 : slabdata      1      1      0
kmem_cache           128    128    128   32    1 : tunables    0    0    0 : slabdata      4      4      0

5 kmalloc 和 vmalloc

5.1 kmalloc

從 4.5 節 /proc/slabinfo 物件也可以看出,kmalloc 的分配建立在 slab 記憶體物件池上。
在mm/slab_common.c 中 kmalloc 的分配定義如下:

// mm/slab_common.c
static struct {
    const char *name;
    unsigned long size;
} const kmalloc_info[] __initconst = {
    {NULL,                      0},     {"kmalloc-96",             96},
    {"kmalloc-192",           192},     {"kmalloc-8",               8},
    {"kmalloc-16",             16},     {"kmalloc-32",             32},
    {"kmalloc-64",             64},     {"kmalloc-128",           128},
    {"kmalloc-256",           256},     {"kmalloc-512",           512},
    {"kmalloc-1024",         1024},     {"kmalloc-2048",         2048},
    {"kmalloc-4096",         4096},     {"kmalloc-8192",         8192},
    {"kmalloc-16384",       16384},     {"kmalloc-32768",       32768},
    {"kmalloc-65536",       65536},     {"kmalloc-131072",     131072},
    {"kmalloc-262144",     262144},     {"kmalloc-524288",     524288},
    {"kmalloc-1048576",   1048576},     {"kmalloc-2097152",   2097152},
    {"kmalloc-4194304",   4194304},     {"kmalloc-8388608",   8388608},
    {"kmalloc-16777216", 16777216},     {"kmalloc-33554432", 33554432},
    {"kmalloc-67108864", 67108864}
};

kmalloc 獲取的是以位元組為單位的連續實體記憶體空間

5.2 gfp_t 結構體

// include/linux/slab.h 
void *kmalloc(size_t size, gfp_t flags)

在 alloc_page(s) get_free_page(s) kmalloc 函式的定義中 第二個引數型別為 gfp_t 型別;
gfp_t 標誌有3類:(所有的 GFP 標誌都在 <linux/gfp.h> 中定義)

  1. 行為標誌 :控制分配記憶體時,分配器的一些行為
  2. 區標誌 :控制記憶體分配在那個區(ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM 之類)
  3. 型別標誌 :由上面2種標誌組合而成的一些常用的場景

區標誌主要以下3種:

區域描述
__GFP_DMA 從 ZONE_DMA 分配
__GFP_DMA32 只在 ZONE_DMA32 分配
__GFP_HIGHMEM 從 ZONE_HIGHMEM 或者 ZONE_NORMAL 分配

__GFP_HIGHMEM 優先從 ZONE_HIGHMEM 分配,如果 ZONE_HIGHMEM 沒有多餘的頁則從 ZONE_NORMAL 分配

5.3 vmalloc

vmalloc 分配的記憶體和kmalloc 不同,vmalloc 在邏輯地址上是連續的,但是在物理地質上不一定連續。

/**
 *  vmalloc  -  allocate virtually contiguous memory
 *  @size:      allocation size
 *  Allocate enough pages to cover @size from the page level
 *  allocator and map them into contiguous kernel virtual space.
 *
 *  For tight control over page level allocator and protection flags
 *  use __vmalloc() instead.
 */
void *vmalloc(unsigned long size)
{
    return __vmalloc_node_flags(size, NUMA_NO_NODE,
                    GFP_KERNEL | __GFP_HIGHMEM);
}


static inline void *__vmalloc_node_flags(unsigned long size,
                    int node, gfp_t flags)
{
    return __vmalloc_node(size, 1, flags, PAGE_KERNEL,
                    node, __builtin_return_address(0));
}


void *__vmalloc_node_range(unsigned long size, unsigned long align,
            unsigned long start, unsigned long end, gfp_t gfp_mask,
            pgprot_t prot, unsigned long vm_flags, int node,
            const void *caller)
{
    struct vm_struct *area;
    void *addr;
    unsigned long real_size = size;

    size = PAGE_ALIGN(size);
    if (!size || (size >> PAGE_SHIFT) > totalram_pages)
        goto fail;

    area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED |
                vm_flags, start, end, node, gfp_mask, caller);
    if (!area)
        goto fail;

    addr = __vmalloc_area_node(area, gfp_mask, prot, node);
    if (!addr)
        return NULL;

    ......
}

static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
                 pgprot_t prot, int node)
{
    struct page **pages;
    unsigned int nr_pages, array_size, i;
    const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;
    const gfp_t alloc_mask = gfp_mask | __GFP_NOWARN;

    nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;
    array_size = (nr_pages * sizeof(struct page *));

    area->nr_pages = nr_pages;
    /* Please note that the recursion is strictly bounded. */
    if (array_size > PAGE_SIZE) {
        pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
                PAGE_KERNEL, node, area->caller);
    } else {
        pages = kmalloc_node(array_size, nested_gfp, node);
    }
    
    area->pages = pages;
    if (!area->pages) {
        remove_vm_area(area->addr);
        kfree(area);
        return NULL;
    }

    for (i = 0; i < area->nr_pages; i++) {
        struct page *page;

        if (node == NUMA_NO_NODE)
            page = alloc_page(alloc_mask);
        else
            page = alloc_pages_node(node, alloc_mask, 0);

        if (unlikely(!page)) {
            /* Successfully allocated i pages, free them in __vunmap() */
            area->nr_pages = i;
            goto fail;
        }
        area->pages[i] = page;
        if (gfpflags_allow_blocking(gfp_mask))
            cond_resched();
    }
    ......

從vmalloc 函式的實現看 最終呼叫了alloc_page 系列函式實現 從夥伴分配系統中分配記憶體。所以所vmalloc 適用了大塊非物理連續的記憶體分配。 __vmalloc_node_flags(size, NUMA_NO_NODE, GFP_KERNEL | __GFP_HIGHMEM) 函式中vmalloc 指定了從高階記憶體分配。

linux 記憶體分配結構

6 malloc

6.1 程式在記憶體中的地址

二進位制程式通常分為text, Data, Bss, 區, 堆和棧。載入到記憶體後的記憶體映象如圖所示:

程式在記憶體中的地址

==圖片來源於網路==

6.2 sbrk 系統呼叫 和 “program break" (程式間斷點)

程式間斷點在最開始指向堆區的起始位置,同時也是資料段的結尾。 malloc 分配記憶體後,指向分配的記憶體開始的位置。

linux 系統上malloc 的實現基於sbrk 系統呼叫。

p1 = sbrk(0);               //sbrk(0)返回當前的程式間斷點
p = sbrk(1)                 //將堆區的大小加1,但是返回的是p1的位置

參考
如何實現一個malloc

記憶體的頁對映

malloc 呼叫後,只是分配了記憶體的邏輯地址,在核心的mm_struct 連結串列中插入vm_area_struct結構體,沒有分配實際的記憶體。當分配的區域寫入資料是,引發頁中斷,建立物理頁和邏輯地址的對映。下圖表示了這個過程。

物理頁的分配過程

在Android 上通過procrank 檢視 Vss 和 Rss, Rss 總是小於Vss 就是這個原因。

generic_x86_64:/ # procrank
 PID       Vss      Rss      Pss      Uss  cmdline
1509  1077592K  117132K   66232K   57296K  system_server
1237   901952K   66596K   56300K   52884K  zygote
1623  1061168K   98892K   50847K   44164K  com.android.systemui
1236   916248K   78992K   29529K   20532K  zygote64
1780  1020240K   63484K   20138K   15684K  com.android.phone
2004  1014992K   66748K   20112K   14748K  com.android.launcher3
欄位含義
VSS Virtual Set Size 虛擬耗用記憶體(包含共享庫佔用的記憶體)
RSS Resident Set Size 實際使用實體記憶體(包含共享庫佔用的記憶體)
PSS Proportional Set Size 實際使用的實體記憶體(比例分配共享庫佔用的記憶體)
USS Unique Set Size 程序獨自佔用的實體記憶體(不包含共享庫佔用的記憶體)

一般來說記憶體佔用大小有如下規律:VSS >= RSS >= PSS >= USS
參考:
How the Kernel Manages Your Memory

部分內容來源於網路,沒有一一註明



作者:赤兔歡
連結:https://www.jianshu.com/p/eecbb1506eee
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。