linux核心虛擬記憶體之slub分配器
上一章主要講述以頁為最小單位進行記憶體分配的夥伴管理演算法,較大程度上避免了記憶體碎片問題。而實際上對記憶體的申請卻不是每次都申請一個頁面的(比如檔案節點,任務描述符等結構體記憶體),通常是遠小於一個記憶體頁面的大小,此外更可能會頻繁地申請釋放這些記憶體。對於這種情況,每次分配小於一個頁面的都統一分配一個頁面的空間是過於浪費且不切實際的,因此必須充分利用未被使用的空閒空間,同時也要避免過多地訪問操作頁面分配。基於該問題的考慮,核心需要一個緩衝池對小塊記憶體進行有效的管理起來,於是就有了slab記憶體分配演算法。每次小塊記憶體的分配優先來自於該記憶體分配器,小塊記憶體的釋放也是先快取至該記憶體分配器,留作下次申請時進行分配,避免了頻繁分配和釋放小塊記憶體所帶來的額外負載。而這些被管理的小塊記憶體在管理演算法中被視之為“物件”
slab/slub/slob(本系統用了slub):
slab是基礎,是最早從Sun OS那引進的;
slob是被改進的slab,佔用資源少,使用記憶體較少的嵌入式裝置
slub是在slab上進行的改進簡化,在大型機上表現出色,並且能更好的適應largeNUMA系統;SLUB相對於SLAB有5%-10%的效能提升和減少50%的記憶體佔用
slab分配器有三個基本目標:
A.減少夥伴系統分配小塊記憶體時所產生的內部碎片
B.把程序使用的物件快取起來,減少分配、初始化以及釋放物件的時間開銷
C.調整物件以更好的使用功能L1和L2硬體快取記憶體
1、快取記憶體
slab分配器形象得說就是先由夥伴演算法申請部分空閒記憶體空間,然後slab按照同樣資料型別大小對申請的記憶體進行分割,最後再用一些資料結構進行管理。這些進行分割後的記憶體稱之為快取記憶體。
每一種快取記憶體存放相同型別的物件,不同快取記憶體組成了slab的快取記憶體組,通過連結串列的形式組織,連結串列頭為slab_caches。通過/proc/slabinfo可以檢視當前系統所有的快取記憶體:
# name <active_objs><num_objs> <objsize> <objperslab> <pagesperslab> :tunables <limit> <batchcount> <sharedfactor> : slabdata<active_slabs> <num_slabs> <sharedavail>
kmalloc-8192 20 20 8192 4 8 : tunables 0 0 0 : slabdata 5 5 0
kmem_cache_node 192 192 64 64 1: tunables 0 0 0 : slabdata 3 3 0
kmem_cache 128 128 128 32 1 : tunables 0 0 0 : slabdata 4 4 0
name:快取記憶體名字
active_objs:正在使用的物件數目
num_objs:總共物件數目
objsize:每個物件大小
objperslab:每個slab物件數目
pageperslab:每個slab需要的pages數目
active_slabs:活動的slab數目
num_slabs:slab數目
從上面可知,每種快取記憶體由1個或多個slab組成,而每個slab又有一頁或多頁組成,最終被劃分為n個物件。如圖所示:
2、slab
(1)資料結構描述
從上面看快取記憶體和slab劃分很清晰,實際程式碼則比較模糊,主要通過kmem_cache的資料結構來描述每種slab。該結構定義如下(include/linux/slub_def.h):
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; //per CPU slab結構,用於各個CPU的快取管理
/* Used for retriving partial slabs etc */
unsigned long flags;
unsigned long min_partial;
int size; /* 物件大小,包括metadata元資料 */
int object_size; /* slab物件純大小 */
int offset; /* 空閒物件的指標偏移 */
int cpu_partial; /* 每個CPU持有量 */
struct kmem_cache_order_objects oo; //存放分配給slab頁框的階數(高16位)和slab中物件數量(低16位)
/* Allocation and freeing of slabs */
struct kmem_cache_order_objects max;
struct kmem_cache_order_objects min;
gfp_t allocflags; /* 申請頁面時使用的GFP標識 */
int refcount; /* 緩衝區計數器,當用戶請求建立新的緩衝區時SLUB分配器重用已建立的相似大小的緩衝區從而減少緩衝區個數 */
void (*ctor)(void *); //建立物件的回撥函式
int inuse; /* 元資料meta data偏移量 */
int align; /* 對齊值 */
int reserved; /* Reservedbytes at the end of slabs */
const char *name; /* slab快取名稱 */
struct list_head list; /* slabcaches管理連結串列 */
#ifdef CONFIG_SYSFS
struct kobject kobj; /* Forsysfs */
#endif
struct kmem_cache_node *node[MAX_NUMNODES]; //各個記憶體管理節點的slub資訊
};
其中cpu_slab的結構型別是kmem_cache_cpu,每個CPU型別資料,各個CPU都有自己獨立的一個結構,用於管理本地的物件快取。定義如下:
struct kmem_cache_cpu {
void **freelist; /* 空閒物件佇列的指標 */
unsigned long tid; /* Globally unique transaction id 標識CPU,保證只有一個且在正確的CPU上申請 */
struct page *page; /* 指向slab物件來源的記憶體頁面 */
struct page *partial; /* 指向曾分配完所有的物件,但當前已回收至少一個物件的page */
};
node用於管理節點所有物件的slab緩衝區,定義如下:
struct kmem_cache_node {
spinlock_t list_lock;
#ifdef CONFIG_SLUB
unsigned long nr_partial; //本節點的partial slab的數目
struct list_head partial; //partial slab的雙向迴圈佇列
#ifdef CONFIG_SLUB_DEBUG
atomic_long_t nr_slabs; //slab總數
atomic_long_t total_objects; //slab物件數目
struct list_head full; //slab full列表
#endif
#endif
};
Slub分配管理中,每個CPU都有自己的快取管理(即kmem_cache_cpu資料結構管理);而每個node節點也有自己的快取管理(即kmem_cache_node資料結構管理)。
分配物件:
A.當前CPU快取有滿足申請要求的物件時,將會首先從kmem_cache_cpu的空閒連結串列freelist將物件分配出去。
B.如果物件不夠時,將會向夥伴管理演算法中申請記憶體頁面,申請來的頁面將會先填充到node節點中,然後從node節點取出物件到CPU的快取空閒連結串列中
C.如果原來申請的node節點A的物件,現在改為申請node節點B的,那麼將會把node節點A的物件釋放後再申請。
釋放物件:
A.會先將物件釋放到CPU上面,如果釋放的物件恰好與CPU的快取來自相同的頁面,則直接新增到列表
B.如果釋放的物件不是當前CPU快取的頁面,則會把當前的CPU快取物件放到node節點上面,然後再把該物件釋放到本地的cache中
為了避免過多的空閒物件快取在管理框架中,slub設定的閾值,如果空閒物件個數達到了峰值,將會把當前快取釋放到node節點中,當node節點也過了閾值,將會把node節點的物件釋放到夥伴管理演算法中。
其實現框圖如下:
(2)slab初始化
在start_kernel中mm_init的kmem_cache_init實現了slab的初始化,它是在實現夥伴演算法之後進行初始化。kmem_cache_init在slab.c、slob.c和slub.c都有實現,不同演算法其初始化各異,本系統主要用了slub分配演算法,因此這裡分析slub.c中的實現。其程式碼原型如下:
void __init kmem_cache_init(void)
{
static __initdata struct kmem_cache boot_kmem_cache,
boot_kmem_cache_node;
if (debug_guardpage_minorder())
slub_max_order = 0;
/*由於在這之前只是初始化好夥伴演算法,因而無法使用slab分配快取記憶體kmem_cache結構體。這裡使用定義了一個靜態變數變數boot_kmem_cache_node和boot_kmem_cache來臨時管理slab。kmem_cache:主要用於kmem_cache_create建立快取記憶體時,從該快取記憶體分配物件kmem_cache,描述待建立的快取記憶體 */
kmem_cache_node = &boot_kmem_cache_node;
kmem_cache = &boot_kmem_cache;
/* 函式用於建立分配演算法快取,主要是把上面兩個變數結構初始化 */
create_boot_cache(kmem_cache_node, "kmem_cache_node",
sizeof(struct kmem_cache_node), SLAB_HWCACHE_ALIGN);
register_hotmemory_notifier(&slab_memory_callback_nb);
/* Able to allocate the per node structures */
slab_state = PARTIAL;
create_boot_cache(kmem_cache, "kmem_cache",
offsetof(struct kmem_cache, node) +
nr_node_ids * sizeof(structkmem_cache_node *),
SLAB_HWCACHE_ALIGN);
/*上面臨時初始化後就可以進行slab分配kmem_cache物件。該函式主要作用是將臨時kmem_cache和kmem_cache_node向最終的遷移,並修正相關指標,使其指向最終的kmem_cache和kmem_cache_node,並將兩者新增到slab_caches全域性連結串列中*/
kmem_cache = bootstrap(&boot_kmem_cache);
kmem_cache_node = bootstrap(&boot_kmem_cache_node);
/* Now we canuse the kmem_cache to allocate kmalloc slabs */
/* 初始化一批後期記憶體中需要用到的不同大小的slab快取(即kmalloc),可以通過/proc/slabinfo檢視當前kmalloc支援的大小{64,128,192,256,512,1024,4096,8192} */
create_kmalloc_caches(0);
#ifdef CONFIG_SMP
register_cpu_notifier(&slab_notifier);
#endif
printk(KERN_INFO
"SLUB: HWalign=%d, Order=%d-%d, MinObjects=%d,"
" CPUs=%d, Nodes=%d\n",
cache_line_size(),
slub_min_order, slub_max_order, slub_min_objects,
nr_cpu_ids, nr_node_ids);
}
上面初始化完後,就可以建立自己的快取記憶體,可以只用kmalloc申請記憶體空間。
(3)操作函式
A.kmem_cache_create
建立一個新的快取記憶體
B.kmem_cache_destroy
銷燬一個快取記憶體
C.kmem_cache_alloc
從快取記憶體中分配物件,當快取記憶體中空間不足時,會呼叫alloc_slab_page申請一個新的slab頁。
D.kmem_cache_free
釋放一個物件
(4)slab著色
在slub分配演算法中已經看不到著色相關資訊,也可能是水平有限未找到。但是這裡還是講一下slab著色原理,有助於對硬體cache理解。
假定L1 data cache為32K,cache line32B,就有1024個cache line(指一次性可以讀取/寫入的資料量)。CPU訪問時先訪問硬體cache,沒有命中時才訪問記憶體。
硬體cache對應到記憶體的位置不是任意的:
cache line0 對應到記憶體地址:0~31,32K~32K+31,…
cache line1 對應到記憶體地址:32~63,32K+32~32K+63,…
cache line2 對應到記憶體地址:64~95,32K+64~32K+95,…
…
cache line1023 對應到記憶體地址:32736~32767,32K+32736~32K+32767,…
從上可以看出假定slab物件A在0地址,物件B在32地址,而物件C在32K地址,那麼物件A和物件C則會使用相同的硬體cacheline0,如果頻繁的訪問A和C那麼就會不停的切入和換出A和C,導致命中效率低下。所謂著色就是將C進行偏移存放在cache line2,這樣就不會進行頻繁切換,提高命中效率。當然如果cache已經用完,那麼進行著色也無濟於事。
3、kmalloc和kfree
kmalloc是基於slab/slob/slub分配演算法上實現,如前面章節描述kmalloc存在不同固定大小的kmalloc-N的快取記憶體。kmalloc在slab/slob/slub上都有實現,這裡主要描述slub的實現方式:
static __always_inline void *kmalloc(size_tsize, gfp_t flags)
{
if (__builtin_constant_p(size)) { //gcc內建函式,用於判斷一個值是否為編譯時常量
if (size > KMALLOC_MAX_CACHE_SIZE) //大於8192(即kmalloc最大cache)通過kmalloc_large函式申請,該方式通過__get_free_pages介面,即通過buddy夥伴演算法申請所需記憶體空間(預設最大是8M)
return kmalloc_large(size, flags);
if (!(flags & GFP_DMA)) {
int index = kmalloc_index(size);
if (!index)
return ZERO_SIZE_PTR;
//如果<=8192則通過kmalloc cache進行分配
return kmem_cache_alloc_trace(kmalloc_caches[index],
flags, size);
}
}
return__kmalloc(size, flags);//也是上面的結合,只是在size非常量情況下。
}
size為表示申請的空間大小,而flags則表示分配標誌。分配標誌眾多,每個標誌標識特定的bit位,可以通過|進行組合,常見的有如下:
GFP_KERNEL:首選標誌,核心記憶體分配,會阻塞引起睡眠
GFP_ATOMIC:不會引起睡眠,但會使用緊急記憶體池,一般用於中斷處理等不能睡眠的地方
從上可以看出kmalloc實現較為簡單,分配說的的記憶體不僅虛擬地址是連續空間,實體地址上也是連續空間。
kfree則是用於釋放有kmalloc申請的記憶體塊,在slab/slob/slub都有對應的實現,這裡主要展示slub的實現:
void kfree(const void *x)
{
struct page *page;
void *object = (void *)x;
trace_kfree(_RET_IP_, x);
if (unlikely(ZERO_OR_NULL_PTR(x))) //判斷是否為NULL
return;
page = virt_to_head_page(x); //將虛擬地址轉化成對應的頁地址
if (unlikely(!PageSlab(page))) { //是否屬於slab管理頁,不屬於則進入if處理
BUG_ON(!PageCompound(page));
kmemleak_free(x); //對該虛擬地址進行釋放前處理
__free_memcg_kmem_pages(page, compound_order(page)); // 呼叫__free_pages釋放頁到夥伴演算法管理裡
return;
}
//如果屬於slab管理頁,就使用slab方式釋放物件
slab_free(page->slab_cache, page, object, _RET_IP_);
}
如果釋放的記憶體不是kmalloc分配的,或者想要釋放的記憶體早就被釋放了,那麼呼叫該函式會導致嚴重的後果。注意,呼叫kfree(NULL)是安全的。
相關推薦
linux核心虛擬記憶體之slub分配器
上一章主要講述以頁為最小單位進行記憶體分配的夥伴管理演算法,較大程度上避免了記憶體碎片問題。而實際上對記憶體的申請卻不是每次都申請一個頁面的(比如檔案節點,任務描述符等結構體記憶體),通常是遠小於一個記憶體頁面的大小,此外更可能會頻繁地申請釋放這些記憶體。對於這種情況,每次
【原創】(十一)Linux記憶體管理slub分配器
背景 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 核心模組程式設計之LED驅動程式(六)
我使用的是tiny6410的核心板,板子如下,淘寶可以買到 為了不與板子上的任何驅動發生IO衝突,我使用CON1那一排沒用到的IO口,引腳如下 LED1 LED2 LED3 LED4
linux 核心模組程式設計之核心符號匯出(五)
/proc/kallsyms 記錄了核心中所有匯出的符號的名字與地址 我們需要編譯2個核心模組,然後其中一個核心模組去呼叫另一個核心模組中的函式 hello.c程式碼如下 #include <linux/module.h> #include <linux/in
linux 核心模組程式設計之模組引數(四)
通過巨集module_param指定模組引數,模組引數用於在載入模組時傳遞給模組。 module_param(name, type, perm) name是模組引數的名字 type是這個引數的型別,常見值:bool、int、charp(字串型) perm是模組
linux 核心模組程式設計之編譯多個原始檔(三)
編譯擁有多個原始檔的核心模組的方式和編譯一個原始檔的方式差不多,我們先來看下我們需要的檔案都有哪些。 首先是main.c檔案 #include <linux/module.h> #include <linux/init.h> MODULE_LICENSE
linux 核心模組程式設計之hello word(二)
我們的目的是要編譯個hello.ko的檔案,然後安裝到核心中。 先來看下需要的程式碼,hello.c檔案如下 #include <linux/module.h> #include <linux/init.h> static int hello_init(vo
linux 核心模組程式設計之環境搭建(一)
這裡介紹些關於Tiny6410開發板核心的編譯,為後期驅動開發做前期的準備。 開發環境:64位的Ubuntu 14.01虛擬機器 目標機:友善之臂Tiny6410開發板 核心:linux-2.6.38-20110325.tar.gz 核心原始碼下載地址 htt
Linux核心完全註釋之Linux核心體系結構(續)
Linux核心完全註釋之Linux核心體系結構(續) 2.6 Linux 核心對記憶體的使用方法 2.8 Linux 核心原始碼的目錄結構 2.9 核心系統與使用者程式的關係 2.10 linux/Makefile 檔案 小結
Linux核心完全註釋之概述
1.1 Linux的誕生與發展 Linux創始人:Linus Toravlds Linux第一版釋出時間:1991年9月 Linux誕生髮展的五大支柱: UNIX作業系統 Ken. Thompson和Dennis Ritchie開發的分時作業系統 MINIX作業系統 A
【深入理解Linux核心】記憶體定址(一)
1. 邏輯地址:包含在機器語言指令中用來指定一個運算元或一條指令的地址。每一個邏輯地址都由一個段和偏移量組成。偏移量指明瞭從段開始的地方到實際地址之間的距離。 2. 線性地址:又稱虛擬地址,是一個32位無符號整數,也用來表示4GB的地址,範圍從0x00000000到0xffff
Linux核心同步機制之completion
#include <linux/module.h> #include <linux/init.h> #include <linux/sched.h> #include <linux/kernel.h> #includ
【轉】【Linux 核心】記憶體管理(二)夥伴演算法
通常情況下,一個高階作業系統必須要給程序提供基本的、能夠在任意時刻申請和釋放任意大小記憶體的功能,就像malloc 函式那樣,然而,實現malloc 函式並不簡單,由於程序申請記憶體的大小是任意的,如果作業系統對malloc 函式的實現方法不對,將直接導致
Linux核心很吊之 module_init解析 二
簡單來說上篇博文介紹module_init如何註冊驅動的init函式,這篇博文將詳細分析kernel啟動過程又是如何執行我們註冊的init函式。 如果瞭解過linux作業系統啟動流程,那麼當bootloader載入完kernel並解壓並放置與記憶體中準備開始執行,首先被呼叫的函式是start_k
Linux核心原始碼閱讀之開啟檔案篇
至此我們轉向最重要的程式碼__link_walk_path,該函式把傳進來的字串name,也就是使用者指定的路徑,按路徑分隔符分解成一系列小的component。比如使用者說,我要找/path/to/dest這個檔案,那麼我們的檔案系統就會按path,to,dest一個一個來找,知道最後一個分量是檔案或者查詢
虛擬記憶體之倒排頁表
傳統的頁表的大小都是和程序的虛擬地址空間成正比的。從而頁表非常大。一個解決辦法就是使用倒排頁表。 該方法的各種變種用於PowerPC,UltraSPARC和IA-64體系結構中.RT-PC的MAch作
Linux 核心裝置驅動之GPIO驅動之GPIO sysfs支援
需要核心配置CONFIG_GPIO_SYSFS int gpiochip_sysfs_register(struct gpio_device *gdev) { struct device *dev; struct device *parent; struct gpi
Linux核心學習實踐之GPIO面板按鍵
說明:本分析基於AM6C平臺Linux3.0.8核心,其他核心版本僅供參考。 一、platform設備註冊的按鍵對映 common/customer/boards/board-m6tv-h32.c
Linux 核心時鐘架構之時鐘 tick初始化
/** * tick_init - initialize the tick control */ void __init tick_init(void) { tick_broadcast_ini
探索 Linux 核心虛擬機器
簡介: Linux® 既有良好的靈活性,在虛擬化方面同樣出色。但是最近,隨著核心虛擬機器(KVM)的出現,Linux 虛擬化的前景發生了變化。KVM 是構成主流 Linux 核心(V2.6.20)一部分的第一個虛擬化解決方案。KVM 支援 Linux 客戶作業系統的虛擬化