1. 程式人生 > >Linux記憶體管理(2)

Linux記憶體管理(2)

本文目的在於分析Linux記憶體管理機制中的夥伴系統。核心版本為2.6.31。

1. 夥伴系統的概念

在系統執行過程中,經常需要分配一組連續的頁,而頻繁的申請和釋放記憶體頁會導致記憶體中散佈著許多不連續的頁,這樣,當某一時刻要申請一塊較大的連續記憶體時,雖然系統記憶體餘量足夠,即很多頁是空閒的,但找不到一大塊連續的記憶體供使用。

Linux核心中使用夥伴系統(buddy system)演算法來管理記憶體頁。它把所有的空閒頁放到11個連結串列中,每個連結串列分別管理大小為1,2,4,8,16,32,64,128,256,512,1024個頁的記憶體塊。當系統需要分配記憶體時,就可以從buddy系統中獲取。例如,要申請一塊包含4個頁的連續記憶體,就直接從buddy系統中管理4個頁連續記憶體的連結串列中獲取。當系統釋放記憶體時,則將釋放的記憶體放回buddy系統對應的連結串列中,如果釋放記憶體後發現有兩塊相鄰的記憶體又可以合併為一個更高階的記憶體塊,例如釋放4個頁,而恰好相鄰的記憶體也為4個頁的空閒記憶體,則合併這兩塊記憶體並放到buddy系統管理8個連續頁的連結串列中。同樣的,如果系統需要申請3個頁的連續記憶體,則只能在4個頁的連結串列中獲取,剩下的一個頁被放到buddy系統中管理1個頁的連結串列中。

buddy分配器分配的最小單位是一個頁。要分配小於一頁的記憶體需要用到slab分配器,而slab是基於buddy分配器的。

struct zone {
   ......
   struct free_area   free_area[MAX_ORDER];
   ......
}____cacheline_internodealigned_in_smp;

struct zone的free_area[]陣列成員存放了各階的空閒記憶體列表,陣列下標可取0~MAX_ORDER-1,(MAX_ORDER=11)。所以,每個階(order)的記憶體連結串列使用struct free_area結構來記錄。

struct free_area {
   struct list_head  free_list[MIGRATE_TYPES];
   unsigned long     nr_free;
};

struct free_area有兩個成員,free_list[]是不同migrate type(遷移型別)頁連結串列的陣列(我們先不關注什麼是遷移型別,後面會講到),每種遷移型別都是一個struct page的連結串列,由每個struct page的page->lru連起來。nr_free表示這個order空閒頁的數量,例如,階為2的連續頁塊共有3個,則nr_free=3,實際上這個階的空閒頁數為(2^2)*3=12。

2. per-cpu的冷熱頁連結串列

 struct zone結構有一個pageset[]成員:

struct zone {
   ......
struct per_cpu_pageset  pageset[NR_CPUS];
......
}____cacheline_internodealigned_in_smp;
 
struct per_cpu_pageset {
   struct per_cpu_pages pcp;
} ____cacheline_aligned_in_smp;
 
struct per_cpu_pages {
   int count;    /* number ofpages in the list */
   int high;     /* highwatermark, emptying needed */
   int batch;    /* chunk sizefor buddy add/remove */
   struct list_head list;   /*the list of pages */
};

為了方便,我下文將per cpu pageset簡稱為pcp。

pageset[]陣列用於存放per cpu的冷熱頁。當CPU釋放一個頁時,如果這個頁仍在快取記憶體中,就認為它是熱的,之後很可能又很快被訪問,於是將它放到pageset列表中,其他的認為是冷頁。pageset中的冷熱頁連結串列元素數量是有限制的,由per_cpu_pages的high成員控制,畢竟如果熱頁太多,實際上最早加進來的頁已經不熱了。

在CPU釋放一個頁的時候,不會急著釋放到buddy系統中,而是會先試圖將頁作為熱頁或冷頁放到pcp連結串列中,直到超出數量限制。而釋放多個頁時則直接釋放到buddy系統中。

per_cpu_pages的count成員表示連結串列中頁的數量。batch表示有時需要從夥伴系統中拿一些頁放到冷熱頁連結串列中時,一次拿多少個頁。list成員是冷熱頁連結串列,越靠近表頭的越熱。

一般情況下,當核心想申請一個頁的記憶體時,就先從CPU的冷熱頁連結串列中申請。但是,有時直接申請冷頁會更合理一些,因為有時cache中的頁肯定是無效的,所以核心在申請記憶體頁時提供了一個標記GPF_COLD來指明要申請冷頁。

注意,冷熱頁分配只針對分配和回收一個頁的時候,多個頁則直接操作buddy。

3. alloc_pages_node()在buddy系統上分配頁

static inline struct page* alloc_pages_node(int nid, gfp_t gfp_mask,
                     unsigned int order)
{
   /* Unknown node is current node */
   if (nid < 0)
       nid = numa_node_id();
 
   return __alloc_pages(gfp_mask, order, node_zonelist(nid,gfp_mask));
}

這個函式的三個引數為:

nid:節點id,UMA系統為0。

gfp_mask:GFP(get free page)掩碼,在include/linux/gfp.h中定義。

order:分配階,如分配4個頁,則order=2。

node_zonelist()返回節點的zone備用列表,即NODE_DATA(nid)->node_zonelists[]。

該函式最終呼叫__alloc_pages_nodemask()做實際的分配工作,這個函式的註釋為“This is the 'heart' of the zoned buddy allocator”。這個函式根據gpf_flags尋找合適的zone,然後呼叫函式get_page_from_freelist()進行接下來的工作,這個函式簡化後的實現如下:

static struct page *
get_page_from_freelist(gfp_tgfp_mask, nodemask_t *nodemask, unsigned int order,
       struct zonelist *zonelist, int high_zoneidx, int alloc_flags,
       struct zone *preferred_zone, int migratetype)
{
   struct zoneref *z;
   struct page *page = NULL;
   int classzone_idx;
   struct zone *zone;
 
   /* 只有ZONE_NORMAL,所以都返回0 */
   classzone_idx = zone_idx(preferred_zone);
 
/*
    * Scan zonelist, lookingfor a zone with enough free.
    * See alsocpuset_zone_allowed() comment in kernel/cpuset.c.
    */
   for_each_zone_zonelist_nodemask(zone, z, zonelist,
                     high_zoneidx, nodemask) {
       /* 如果沒有設定NO_WATERMARKS */
       if (!(alloc_flags & ALLOC_NO_WATERMARKS)) {
          unsigned long mark;
          int ret;
 
          /* 獲得設定的水印位 */
          mark = zone->watermark[alloc_flags &ALLOC_WMARK_MASK];
          /* 判斷是否超出了水印設定的限制 */
          if (zone_watermark_ok(zone,order, mark,
                 classzone_idx, alloc_flags))
              goto try_this_zone;
       }
 
try_this_zone:
       page = buffered_rmqueue(preferred_zone,zone, order,
                     gfp_mask, migratetype);
       if (page)
          break;
   }
   return page;
}

其中呼叫的函式主要就兩個:

zone_watermark_ok()用來判斷水印限制(zone-> watermark[]),如果要分配的order超出了水印限制,說明系統中可用記憶體頁不夠了,不能繼續分配。

/*
 * Return 1 if free pages are above 'mark'.This takes into account the order
 * of the allocation.
 */
int zone_watermark_ok(struct zone *z, int order, unsigned long mark,
             int classzone_idx,int alloc_flags)
{
   /* free_pages my go negative - that's OK */
   long min = mark;
   long free_pages = zone_page_state(z, NR_FREE_PAGES) - (1 <<order) + 1; /* 為毛加1 */
   int o;
 
   if (alloc_flags & ALLOC_HIGH) /* 如果有ALLOC_HIGH,就降低限制 */
       min -= min / 2;
   if (alloc_flags & ALLOC_HARDER) /* 如果有ALLOC_HARDER,就再降低限制 */
       min -= min / 4;
 
   /* 如果分配完,剩餘的小於限制了 */
   if (free_pages <= min + z->lowmem_reserve[classzone_idx])
       return 0;
 
   /* 如果分配完,比order小的夥伴擁有的頁數佔多數,也不行。 */
   for (o = 0; o < order; o++) {
       /* At the next order, this order's pages become unavailable */
       free_pages -= z->free_area[o].nr_free << o;
 
       /* Require fewer higher order pages to be free */
       min >>= 1;
 
       if (free_pages <= min)
          return 0;
   }
   return 1;
}

注意,在init_per_zone_wmark_min()函式中初始化了每個zone的水印以及lowmem_reserve等限制。這個函式被module_init()了,並且內嵌編到核心,所以在系統啟動時自動執行。

buffered_rmqueue()進行實際的分配頁的工作。

static inline
struct page *buffered_rmqueue(structzone *preferred_zone,
          struct zone *zone, int order, gfp_t gfp_flags,
          int migratetype)
{
   unsigned long flags;
   struct page *page;
   int cold = !!(gfp_flags & __GFP_COLD);/* 是否設定COLD位 */
   int cpu;
 
again:
   cpu  = get_cpu();
   if (likely(order == 0)) { /* 如果需要分配一頁 */
       /* 在pcp上分配 */
   } else { /* 如果需要分配多頁 */
       /* 在buddy上分配 */
   }
 
   __count_zone_vm_events(PGALLOC, zone, 1 << order);
   zone_statistics(preferred_zone, zone); /* 更新統計資料. */
   local_irq_restore(flags);
   put_cpu();
 
   VM_BUG_ON(bad_range(zone, page));
   /* 其他工作 */
   if (prep_new_page(page, order, gfp_flags))
       goto again;
   return page;
 
failed:
   local_irq_restore(flags);
   put_cpu();
   return NULL;
}

在分配頁的時候分為兩種情況,如果只需分配一頁,則直接在pcp上進行。如果需要分配多頁,則在buddy系統上分配。

申請一個頁,在pcp上分配:

1.   通過&zone_pcp(zone,cpu)->pcp獲取pcp連結串列。

2.   如果pcp->count==0,即pcp連結串列為空,則使用rmqueue_bulk()函式在buddy上獲取batch個單頁放到pcp連結串列中,並將這些頁從buddy上移除,同時更新zone的vm_stat統計資料。

3.   根據gfp_flags有沒有GPF_COLD標誌,判斷如果需要分配冷頁就從pcp連結串列的末尾取一個頁,如果需要熱頁就從連結串列頭取一個頁,獲得的頁賦值給page。

4.   將page從pcp連結串列中刪除:list_del(&page->lru),同時pcp->count--。

申請多個頁,在buddy上分配:

1.   如果設定有__GFP_NOFAIL標記,並且order>1,則給出警告。

2.   使用__rmqueue()分配2^order個頁。

3.   更新zone的vm_stat統計資料。

在buddy上申請2^order個頁都是通過__rmqueue()函式完成的,它主要分三步工作:

1.   呼叫__rmqueue_smallest()在指定zone的free_area[order]上特定migratetype連結串列上嘗試分配2^order個頁,如果order階沒有足夠記憶體,就嘗試在order+1階的特定migratetype連結串列上分配2^order個頁,依次類推直到分配到想要的頁。

2.   將被申請的頁從buddy系統上清除。同時,申請之後可能buddy系統需要重新調整,例如,本來想分配2^1=2個頁,而buddy已經沒有2個頁的夥伴了,所以在2^2=4個頁的夥伴上申請,那申請完剩下的兩個頁需要從free_area[2]上刪除,並且放到free_area[1]連結串列中,這個工作是由expand()完成的。

3.   如果在當前zone的buddy上特定migratetype的連結串列中沒有分配成功,並且migratetype != MIGRATE_RESERVE,就使用__rmqueue_fallback()在備用列表中分配。

在這裡我們需要說明struct page的三個成員:

lru:連結串列節點,在存放struct page的連結串列中都是以lru為節點的,如buddy和pcp中的連結串列。

private:這個成員有多重意思,我們在這裡看到,如果page在buddy系統中,private就是這個頁所在free_area的階數。如果page在pcp冷熱頁連結串列中,private就是migratetype。

flags:如果頁在buddy系統中,PG_buddy標記就會被設定,否則被清除。

4. 釋放頁

釋放頁的介面為__free_pages(),它的引數為第一個頁的指標page,以及order。

void __free_pages(structpage *page, unsigned int order)
{
   if (put_page_testzero(page)) {
       if (order == 0)
          free_hot_page(page);
       else
          __free_pages_ok(page,order);
   }
}

如果釋放一個頁,則先嚐試新增到pcp中,超過pcp限制再往buddy系統中新增。如果釋放多個頁,則通過__free_pages_ok()釋放。

將頁回收至buddy系統中的介面為__free_one_page()。就是一個查詢page idx和合並原有buddy的過程。

static inline void__free_one_page(struct page *page,
       struct zone *zone, unsigned int order,
       int migratetype)
{
   unsigned long page_idx;
 
   if (unlikely(PageCompound(page)))
       if (unlikely(destroy_compound_page(page, order)))
          return;
 
   VM_BUG_ON(migratetype == -1);
 
   /* 由mem_map得到頁的index */
   page_idx = page_to_pfn(page) & ((1 << MAX_ORDER) - 1);
 
   /* 這句判斷的意思是page_idx必須是order對齊的? */
   VM_BUG_ON(page_idx & ((1 << order) - 1));
   VM_BUG_ON(bad_range(zone, page));
 
   /* 從order階開始嘗試合併 */
   while (order < MAX_ORDER-1) {
       unsigned long combined_idx;
       struct page *buddy;
 
       /* 找到和當前page同階的buddy的理論位置 */
       buddy = __page_find_buddy(page, page_idx, order);
      
       /* 看page和buddy能否合併,不能就結束了 */
       if (!page_is_buddy(page, buddy, order))
          break;
      
       /* 可以合併,則把buddy釋放 */
       /* Our buddy is free, merge with it and move up one order. */
       list_del(&buddy->lru);
       zone->free_area[order].nr_free--;
       rmv_page_order(buddy);
 
       /* page和buddy合併,起始index就是page或buddy的index */
       combined_idx = __find_combined_index(page_idx, order);
       page = page + (combined_idx - page_idx);
       page_idx = combined_idx;
 
       /* 繼續看能否再往高階合併 */
       order++;
   }
 
/* 合併完成,設定最終buddy的order並新增到響應order陣列的連結串列中。 */
   set_page_order(page, order);
   list_add(&page->lru,
       &zone->free_area[order].free_list[migratetype]);
   zone->free_area[order].nr_free++;
}

這段程式碼邏輯很清晰。注意,只有相鄰地址的buddy才能合併,所以實際上待釋放page的buddy的頁的index是可以計算出來的:

/*
 * Locate the struct page for both the matchingbuddy in our
 * pair (buddy1) and the combined O(n+1) pagethey form (page).
 *
 * 1) Any buddy B1 will have an order O twin B2which satisfies
 * the following equation:
 *     B2= B1 ^ (1 << O)
 * For example, if the starting buddy (buddy2)is #8 its order
 * 1 buddy is #10:
 *     B2= 8 ^ (1 << 1) = 8 ^ 2 = 10
 *
 * 2) Any buddy B will have an order O+1 parentP which
 * satisfies the following equation:
 *     P= B & ~(1 << O)
 *
 * Assumption: *_mem_map is contiguous at leastup to MAX_ORDER
 */
static inline struct page *
__page_find_buddy(structpage *page, unsigned long page_idx, unsigned int order)
{
   unsigned long buddy_idx = page_idx ^ (1<< order);
 
   return page + (buddy_idx - page_idx);
}

合併兩個buddy得到合併後buddy的起始頁index的函式:

static inline unsigned long
__find_combined_index(unsignedlong page_idx, unsigned int order)
{
   return (page_idx & ~(1 <<order));
}

判斷是否可以合併的函式:

/*
 * This function checks whether a page is free&& is the buddy
 * we can do coalesce a page and its buddy if
 * (a) the buddy is not in a hole &&
 * (b) the buddy is in the buddy system&&
 * (c) a page and its buddy have the same order&&
 * (d) a page and its buddy are in the samezone.
 *
 * For recording whether a page is in the buddysystem, we use PG_buddy.
 * Setting, clearing, and testing PG_buddy isserialized by zone->lock.
 *
 * For recording page's order, we usepage_private(page).
 */
static inline intpage_is_buddy(struct page *page, struct page *buddy,
                            int order)
{
   /* buddy的頁的index是否合法。 */
   if (!pfn_valid_within(page_to_pfn(buddy)))
       return 0;
 
   /* 是否屬於同一個zone。 */
   if (page_zone_id(page) != page_zone_id(buddy))
       return 0;
 
   /* 目標buddy必須設定了PG_buddy標記。並且和page是同order的。 */
   if (PageBuddy(buddy) && page_order(buddy) == order) {
       VM_BUG_ON(page_count(buddy) != 0);
       return 1;
   }
   return 0;
}

相關推薦

Linux記憶體管理(2)

本文目的在於分析Linux記憶體管理機制中的夥伴系統。核心版本為2.6.31。1. 夥伴系統的概念在系統執行過程中,經常需要分配一組連續的頁,而頻繁的申請和釋放記憶體頁會導致記憶體中散佈著許多不連續的頁,這樣,當某一時刻要申請一塊較大的連續記憶體時,雖然系統記憶體餘量足夠,即

linux記憶體管理2記憶體對映和需求分頁(英文名字:demand Paging,又叫:缺頁中斷)

        圖 10-5 vm_area_struct 資料結構示意圖當可執行映象對映到程序的虛擬地址空間時,將產生一組 vm_area_struct 結構來描述虛擬記憶體區域的起始點和終止點,每個 vm_struct 結構代表可執行映象的一部分,可能是可執行程式碼,也可能是初始化的變數或未初始化的資料。

【原創】(七)Linux記憶體管理 - zoned page frame allocator - 2

背景 Read the fucking source code! --By 魯迅 A picture is worth a thousand words. --By 高爾基 說明: Kernel版本:4.14 ARM64處理器,Contex-A53,雙核 使用工具:Source Insight 3.5,

Linux系統管理2—文件系統管理

分類 系統 nfs 關閉 ifs 管理工具 cif gfs super 理論: 1.常見文件系統 Linux文件系統: ext2, ext3, ext4, xfs, btrfs, reiserfs, jfs, swap(交換分區)iso9660(光盤) Wi

Linux記憶體管理(最透徹的一篇)

摘要:本章首先以應用程式開發者的角度審視Linux的程序記憶體管理,在此基礎上逐步深入到核心中討論系統實體記憶體管理和核心記憶體的使用方法。力求從外到內、水到渠成地引導網友分析Linux的記憶體管理與使用。在本章最後,我們給出一個記憶體對映的例項,幫助網友們理解核心記憶體管理與使用者記憶體管理之

【轉】Linux記憶體管理

摘要:本章首先以應用程式開發者的角度審視Linux的程序記憶體管理,在此基礎上逐步深入到核心中討論系統實體記憶體管理和核心記憶體的使用方法。力求從外到內、水到渠成地引導網友分析Linux的記憶體管理與使用。在本章最後,我們給出一個記憶體對映的例項,幫助網友們理解核心記憶體管理與使用者記憶體管理之間的

linux記憶體管理的 夥伴系統和slab機制

夥伴系統 Linux核心中採用了一種同時適用於32位和64位系統的記憶體分頁模型,對於32位系統來說,兩級頁表足夠用了,而在x86_64系統中,用到了四級頁表。四級頁表分別為:  頁全域性目錄(Page Global Directory) 頁上級目錄(Page Upper Director

linux記憶體管理---虛擬地址 邏輯地址 線性地址 實體地址的區別(一)

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

Linux記憶體管理之malloc實現

程序虛擬地址空間由一個一個VMA來表示,這裡先接收VMA相關操作. 1.1 查詢VMA函式find_vma() struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr) 找到的vma結果需滿足條件:

linux 系統管理(2) 檔案或目錄數量統計

統計某資料夾下檔案的個數 ls -l |grep "^-"|wc -l 統計某資料夾下目錄的個數 ls -l |grep "^d"|wc -l 統計資料夾下檔案的個數,包括子資料夾裡的 ls -lR|grep "^-"|wc -l 如統計/home/han目錄(包含子目錄)下的所有js檔案則:

Linux記憶體描述之記憶體節點node--Linux記憶體管理(二)

1 記憶體節點node 1.1 為什麼要用node來描述記憶體 這點前面是說的很明白了, NUMA結構下, 每個處理器CPU與一個本地記憶體直接相連, 而不同處理器之前則通過匯流排進行進一步的連線, 因此相對於任何一個CPU訪問本地記憶體的速度比訪問遠端記憶體的速度要快 Linux適用於各種不同的體系結

Linux記憶體描述之記憶體區域zone--Linux記憶體管理(三)

1 記憶體管理域zone 為了支援NUMA模型,也即CPU對不同記憶體單元的訪問時間可能不同,此時系統的實體記憶體被劃分為幾個節點(node), 一個node對應一個記憶體簇bank,即每個記憶體簇被認為是一個節點 首先, 記憶體被劃分為結點. 每個節點關聯到系統中的一個處理器, 核心中表示為pg_

Linux記憶體描述之記憶體頁面page--Linux記憶體管理(四)

1 Linux如何描述實體記憶體 Linux把實體記憶體劃分為三個層次來管理 層次 描述 儲存節點(Node) CPU被劃分為多個節點(node), 記憶體則被分簇, 每個CPU對應一個本地實體記憶體, 即一個CPU-node對應

Linux分頁機制之概述--Linux記憶體管理(六)

1 分頁機制 在虛擬記憶體中,頁表是個對映表的概念, 即從程序能理解的線性地址(linear address)對映到儲存器上的實體地址(phisical address). 很顯然,這個頁表是需要常駐記憶體的東西, 以應對頻繁的查詢對映需要(實際上,現代支援VM的處理器都有一個叫TLB的硬體級頁表快取部件

Linux記憶體管理 (透徹分析)

摘要: 本章首先以應用程式開發者的角度審視Linux的程序記憶體管理,在此基礎上逐步深入到核心中討論系統實體記憶體管理和核心記憶體的使用方法。力求從外到內、水到渠成地引導網友分析Linux的記憶體管理與使用。在本章最後,我們給出一個記憶體對映的例項,幫助網友們理解核心記憶體管理

Linux記憶體管理 (26)記憶體相關工具

  1. vmstat 參照《Linux CPU佔用率監控工具小結-vmstat》 2. memstat memstat可以通過sudo apt install memstat安裝,安裝包括兩個檔案memstat和memstat.conf。 其中memstat.conf是memstat配置

Linux分頁機制之分頁機制的演變--Linux記憶體管理(七)

1 頁式管理 1.1 分段機制存在的問題 分段,是指將程式所需要的記憶體空間大小的虛擬空間,通過對映機制對映到某個實體地址空間(對映的操作由硬體完成)。分段對映機制解決了之前作業系統存在的兩個問題: 地址空間沒有隔離 程式執行的地址不確定 不過分段方法存在一個嚴重的問題:記憶體的使用效率

Linux-記憶體管理子系統

Linux-記憶體管理子系統 記憶體管理子系統職能: 1. 管理虛擬地址和實體地址的對映;2. 管理實體記憶體的分配 虛擬記憶體空間 空間分佈: 1. 使用者空間 如 0-3G地址空間   被使用者程序所使用與核心的直接對映區使用的是同

啟動期間的記憶體管理之bootmem_init初始化記憶體管理Linux記憶體管理(十二)

1. 啟動過程中的記憶體初始化 首先我們來看看start_kernel是如何初始化系統的, start_kerne定義在init/main.c?v=4.7, line 479 其程式碼很複雜, 我們只截取出其中與記憶體管理初始化相關的部分, 如下所示 table th:nth-of-type(1){

啟動期間的記憶體管理之build_zonelists初始化備用記憶體域列表zonelists--Linux記憶體管理(十三)

1. 今日內容(第二階段(二)–初始化備用記憶體域列表zonelists) 我們之前講了在memblock完成之後, 記憶體初始化開始進入第二階段, 第二階段是一個漫長的過程, 它執行了一系列複雜的操作, 從體系結構相關資訊的初始化慢慢向上層展開, 其主要執行了如下操作 特定於體系結構的設定 在完成了基