背景
省流
如果對代碼細節(jié)不感興趣,可以直接跳轉底部內(nèi)存釋放邏輯總結。
前情回顧
關于slab幾個結構體的關系和初始化和內(nèi)存分配的邏輯請見:
[linux kernel]slub內(nèi)存管理分析(0) 導讀
[linux kernel]slub內(nèi)存管理分析(1) 結構體
[linux kernel]slub內(nèi)存管理分析(2) 初始化
[linux kernel]slub內(nèi)存管理分析(2.5) slab重用
[linux kernel]slub內(nèi)存管理分析(3) kmalloc
[linux kernel]slub內(nèi)存管理分析(4) 細節(jié)操作以及安全加固
描述方法約定
PS:為了方便描述,這里我們將一個用來切割分配內(nèi)存的page 稱為一個slab page,而struct kmem_cache
我們這里稱為slab管理結構,它管理的真?zhèn)€slab 體系成為slab cache,struct kmem_cache_node
這里就叫node。單個堆塊稱為object或者堆塊或內(nèi)存對象。
kfree 操作總覽
簡介
kfree
是用來回收kmalloc
分配的內(nèi)存的函數(shù),這里詳細分析流程。
kfree
之中同樣有很多設計cpu搶占、NUMA架構node相關的邏輯,這里不詳細分析了,因為我們分析的目的是為了搞內(nèi)核安全,而寫漏洞利用的時候可以通過將程序綁定在一個cpu 來避免涉及這些邏輯的代碼發(fā)生。
還有一些和Kasan、slub_debug 等檢測內(nèi)存越界等的操作,也不過多分析。
邏輯圖預覽
釋放邏輯
slab page各個狀態(tài)轉化
調(diào)用棧
-
kfree
-
__free_pages
釋放頁面(大塊) -
slab_free
釋放slab 的內(nèi)存slab_free_freelist_hook
-
do_slab_free
入口,和cpu_slab
快速釋放-
memcg_slab_free_hook memcg
相關計數(shù)操作 -
__slab_free
慢速釋放-
kfence_free
、kmem_cache_debug
、free_debug_processing
檢查 - 釋放邏輯
-
put_cpu_partial
unfreeze_partials
-
discard_slab slab
為空則銷毀該slab page-
free_slab
->__free_slab
-
-
-
-
詳細分析
kfree
kfree
總入口:
linux\mm\slub.c : kfree
void kfree(const void *x)
{
struct page *page;
void *object = (void *)x;
trace_kfree(_RET_IP_, x);
if (unlikely(ZERO_OR_NULL_PTR(x)))
return;
page = virt_to_head_page(x);//[1]根據(jù)地址找到對應的page結構體
if (unlikely(!PageSlab(page))) {//[2]如果該page 不是slab,那么就是大塊內(nèi)存了,調(diào)用free_page釋放
unsigned int order = compound_order(page);
BUG_ON(!PageCompound(page));
kfree_hook(object);
mod_lruvec_page_state(page, NR_SLAB_UNRECLAIMABLE_B,
-(PAGE_SIZE << order));
__free_pages(page, order);
return;
}
slab_free(page->slab_cache, page, object, NULL, 1, _RET_IP_);//[3]釋放slab 分配的內(nèi)存塊
}
[1] 調(diào)用kfree
的都是要釋放的內(nèi)存地址,所以首先要根據(jù)內(nèi)存虛擬地址找到它所在頁的頁結構體。
[2] 判斷該頁是不是slab page,如果不是,說明這個要釋放的內(nèi)存不是slab切割分配的,而是一個大內(nèi)存塊,當初是直接分配的頁,所以這里頁調(diào)用__free_pages
來釋放內(nèi)存頁。
[3] 否則說明這個內(nèi)存塊是slab page分配的內(nèi)存塊,進入slab 的釋放流程slab_free
。參數(shù)是通過page
獲得page
的slab管理結構體kmem_cache
。
slab_free
釋放slab 內(nèi)存的入口slab_free
:
linux\mm\slub.c : slab_free
static __always_inline void slab_free(struct kmem_cache *s, struct page *page,
void *head, void *tail, int cnt,
unsigned long addr)
{
if (slab_free_freelist_hook(s, &head, &tail))
do_slab_free(s, page, head, tail, cnt, addr);//[1]調(diào)用do_slab_free
}
[1] slab_free_freelist_hook
是些kasan 的東西沒啥用,這里直接調(diào)用了do_slab_free
,傳入?yún)?shù)有head
和tail
,由于slab_free
設計上可以釋放多個內(nèi)存塊,所以這里head
和tail
和釋放多個內(nèi)存塊相關,如果只釋放一個的話,不影響。
linux\mm\slub.c : do_slab_free
static __always_inline void do_slab_free(struct kmem_cache *s,
struct page *page, void *head, void *tail,
int cnt, unsigned long addr)
{
void *tail_obj = tail ? : head;//[1]獲得尾部,這里tail 傳入是0, 所以tail_obj 就是head
struct kmem_cache_cpu *c;
unsigned long tid;
memcg_slab_free_hook(s, &head, 1);//[2]memcg 相關
redo:
/*
* Determine the currently cpus per cpu slab.
* The cpu may change afterward. However that does not matter since
* data is retrieved via this pointer. If we are on the same cpu
* during the cmpxchg then the free will succeed.
*/
do {//[3]獲取cpu_slab
tid = this_cpu_read(s->cpu_slab->tid);
c = raw_cpu_ptr(s->cpu_slab);
} while (IS_ENABLED(CONFIG_PREEMPTION) &&
unlikely(tid != READ_ONCE(c->tid)));
/* Same with comment on barrier() in slab_alloc_node() */
barrier();//同步一下
if (likely(page == c->page)) {//[4]釋放的對象正好屬于當前cpu_slab正在使用的slab則快速釋放
void **freelist = READ_ONCE(c->freelist);//[4.1]獲取freelist
set_freepointer(s, tail_obj, freelist);//[4.2]將新釋放內(nèi)存塊插入freelist 開頭
if (unlikely(!this_cpu_cmpxchg_double(//[4.3]this_cpu_cmpxchg_double()原子指令操作存放
s->cpu_slab->freelist, s->cpu_slab->tid,
freelist, tid,
head, next_tid(tid)))) {
note_cmpxchg_failure("slab_free", s, tid);//失敗記錄
goto redo;
}
stat(s, FREE_FASTPATH);
} else
__slab_free(s, page, head, tail_obj, cnt, addr);//[5]否則說明釋放的內(nèi)存不是當前cpu_slab立馬能釋放的
}
[1] 獲取一下尾部,由于我們傳入的tail
指針為0,則獲取head
指針,也就是要釋放的內(nèi)存地址。換句話說,當只釋放一個內(nèi)存塊的時候,head=tail
。
[2] memcg相關,這里不詳細分析。
[3] 跟kmalloc
的時候一樣,獲取一下當前使用cpu的cpu_slab
。
[4] 如果要釋放的內(nèi)存塊的所在page
正好是當前cpu_slab
管理的slab,那么可以快速釋放。
? [4.1] 獲取cpu_slab
的freelist
? [4.2] 調(diào)用set_freepointer
將要釋放的內(nèi)存塊放到freelist
鏈表的開頭,在前一章分析過set_freepointer
了。
? [4.3] 跟kmalloc
時候相同的原子操作,這里等價于,也就是更新一下cpu_slab->freelist
:
freelist = s->cpu_slab->freelist;
s->cpu_slab->freelist=head;
tid=s->cpu_slab->tid;
s->cpu_slab->tid=next_tid(tid)
[5] 到這里說明釋放的內(nèi)存不是當前cpu_slab
立馬能釋放的,那么調(diào)用__slab_free
慢速分配。
__slab_free
linux\mm\slub.c : __slab_free
static void __slab_free(struct kmem_cache *s, struct page *page,
void *head, void *tail, int cnt,
unsigned long addr)
{
void *prior;
int was_frozen;
struct page new;
unsigned long counters;
struct kmem_cache_node *n = NULL;
unsigned long flags;
stat(s, FREE_SLOWPATH);
if (kfence_free(head))
return;
if (kmem_cache_debug(s) && //這兩個里面就是各種檢測,實際沒有對free環(huán)節(jié)有影響的操作
!free_debug_processing(s, page, head, tail, cnt, addr))
return;
do {//[1] 嘗試釋放內(nèi)存塊到所屬page的freelist
if (unlikely(n)) {//循環(huán)了多次才會走到這,加鎖操作
spin_unlock_irqrestore(&n->list_lock, flags);
n = NULL;
}
prior = page->freelist;//[1.1]獲取當前page的freelist
counters = page->counters;//獲取當前page的counters,這是一個聯(lián)合體,包括inuse、frozen等
set_freepointer(s, tail, prior);//將freelist 接到要釋放內(nèi)存塊的后面*tail=prior
new.counters = counters;
was_frozen = new.frozen;
new.inuse -= cnt;//正在使用內(nèi)存塊減掉要釋放的數(shù)量
if ((!new.inuse || !prior) && !was_frozen) {//[2]釋放之后全空或釋放之前為滿并且不在cpu_slab中
//釋放前為滿有2種情況:游離中,在cpu_slab->page中,但走到這里確定不在cpu_slab中
//調(diào)試模式時可能在node->full中,默認不開啟,不考慮
if (kmem_cache_has_cpu_partial(s) && !prior) {//[2.1]釋放前為滿(不管你釋放后空不空)
//并且開啟了cpu_slab->partial
//走到這可以確定slab page為游離狀態(tài)
/*
* Slab was on no list before and will be
* partially empty
* We can defer the list move and instead
* freeze it.
*/
new.frozen = 1;//準備放入cpu_slab中
} else { /* Needs to be taken off a list *///[2.2]釋放之后就為空slab了
n = get_node(s, page_to_nid(page));//獲取page所在node
/*
* Speculatively acquire the list_lock.
* If the cmpxchg does not succeed then we may
* drop the list_lock without any processing.
*
* Otherwise the list_lock will synchronize with
* other processors updating the list of slabs.
*/
spin_lock_irqsave(&n->list_lock, flags);
}
}
} while (!cmpxchg_double_slab(s, page, //[1.2]成功之前一直嘗試
prior, counters,
head, new.counters,
"__slab_free"));
/* if (page->freelist == prior && page->counters == counters)
* page->freelist = head
* page->counters = new.counters 完成釋放
*/
if (likely(!n)) {//[3]n為空,說明這里釋放之前為滿,或釋放之前之后都半滿
if (likely(was_frozen)) {//[3.1]原本就在cpu_slab中,啥也不用操作
/* 走到這里說明該slab page在當前cpu_slab->partial中或其他cpu_slab的page或partial中
直接釋放到page->freelist即可(已經(jīng)釋放完畢),其他cpu我們管不到
* The list lock was not taken therefore no list
* activity can be necessary.
*/
stat(s, FREE_FROZEN);
} else if (new.frozen) {//[3.2]釋放之前是滿的也就是游離狀態(tài),放入cpu_slab->partial中
/*
* If we just froze the page then put it onto the
* per cpu partial list.
*/
put_cpu_partial(s, page, 1);//放到cpu_slab->partial中,這里有一個bug,他沒有考慮node->full的情況
stat(s, CPU_PARTIAL_FREE);
}
return;//[3.3]釋放之前在node->partial中,并且釋放之后也不為空,直接返回啥也不用干
}
if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))//[4]釋放之后為空 node 中的partial 滿足最小要求
goto slab_empty;//去釋放這個page
/*
* Objects left in the slab. If it was not on the partial list before
* then add it.
*/
if (!kmem_cache_has_cpu_partial(s) && unlikely(!prior)) {//[5]釋放前為full,但沒開啟cpu_slab->partial
remove_full(s, n, page);//從full 中移除,如果是游離狀態(tài)不在鏈表中會直接返回
add_partial(n, page, DEACTIVATE_TO_TAIL); //添加到partial中
stat(s, FREE_ADD_PARTIAL);
}
spin_unlock_irqrestore(&n->list_lock, flags);
return;
slab_empty:
if (prior) {//[4.1]釋放之前不是full,在partial中
/*
* Slab on the partial list.
*/
remove_partial(n, page);//從partial 刪除
stat(s, FREE_REMOVE_PARTIAL);
} else {//[4.2]釋放之前在full 中
/* Slab must be on the full list */
remove_full(s, n, page);//從full 中刪除
}
spin_unlock_irqrestore(&n->list_lock, flags);
stat(s, FREE_SLAB);
discard_slab(s, page);//[4]slab為空,則直接刪除整個空slab
}
[1] 嘗試釋放內(nèi)存塊到所屬page
的freelist
,由于涉及到鎖的操作/原子操作,這里會一直循環(huán)嘗試直到成功為止
? [1.1] 獲取一下釋放堆塊之前page
的freelist
、counters
等信息,counters
是一個聯(lián)合體,同時包括page
的frozen
(代表page
是在cpu_slab
中還是在node
中)、inuse
(代表正在使用的內(nèi)存數(shù)量)等。然后將page->freelist
拼到準備釋放的內(nèi)存塊后面,也就是新釋放的放到freelist
表頭,但目前還沒更新到page
,然后計算新inuse
等。
? [1.2] 進行原子操作嘗試完成分配,cmpxchg_double_slab
函數(shù)完成的操作等價于如下操作,即將新freelist
頭更新到page
中,再更新counters
:
if (page->freelist == prior && page->counters == counters)
{
page->freelist = head;
page->counters = new.counters;
}
目前已經(jīng)將堆塊釋放到page->freelist
中了,接下來要對page
的種類進行判斷。目前一共有6種(有一種是DEBUG場景)可能:
- 在當前
cpu_slab->partial
中 - 是
cpu_slab->page
- 在其他
cpu_slab->partial
中 - 游離狀態(tài)(分配滿了),不在任何列表之中
- 在所屬
node
的partial
列表中 - 如果開啟
CONFIG_SLUB_DEBUG
,則可能會是分配滿的狀態(tài)而在所屬node的full
列表中,默認不開啟
[2] !new.inuse
代表釋放之后已經(jīng)沒有inuse
的內(nèi)存了,說明釋放之后該slab里面全是free 的堆塊,也就是釋放后整個page
都空了;!prior
說明釋放之前沒有freelist
,即釋放之前該slab 為分配滿的slab page;!was_frozen
說明釋放之前該slab不在cpu_slab
之中。也就是說,當該slab 不在是cpu_slab
正在使用的slab 時,如果釋放后為空slab或釋放前為full slab,則說明游離狀態(tài)、node->full
中或node->partial
中的page
滿足該分支條件:
? [2.1] 釋放前為full slab,對應游離狀態(tài)和node->full
的slab page;kmem_cache_has_cpu_partial
判斷是否開啟了cpu_slab->partial
;若開啟則new.frozen
設置為1,打算將該slab 放入cpu_slab->partial
中,因為釋放后就變成了半滿狀態(tài)了,可以使用了。這里為什么放入cpu_slab
的partial
而不是放入node的partial
,是因為cpu_slab
永遠都是最近在使用的slab。
? [2.2] 接下來就是slab page在node->partial
中且釋放之后就為空slab page的情況,獲取一下page
所屬node。方便后續(xù)操作。
[3] n
為空,說明之前沒有獲取node,也就是說不是釋放之后就為空slab。滿足這個分支的有三種cpu_slab
中的情況和游離狀態(tài)的slab page情況:
? [3.1] 原本就在cpu_slab
中(的三種情況),什么也不用改變,因為我們已經(jīng)將堆塊釋放到page->freelist
中了,如果在本cpu_slab->partial
無論釋放完是否為空都無關緊要,因為不需要釋放page
;其次在其他cpu_slab
中的話,不管是在page
還是在partial
中我們都是無權干涉的,直接釋放到page->freelist
然后返回即可。
? [3.2] 走到這里說明**page
是申請滿的游離狀態(tài)或node->full
中**,則釋放后為可用狀態(tài),將其放入cpu_slab->partial
中然后返回(put_cpu_partial
函數(shù)邏輯不止于此,下面會詳細分析)。這個地方有一個問題就是,他沒有考慮如果開啟了CONFIG_SLUB_DEBUG
的情況下,滿的page可能會存在于node->full
列表中,如果補充node->full
雙向鏈表中把page
刪掉直接加入到cpu_slab->partial
中會破壞node->full
雙鏈表,后續(xù)插入操作可能造成unlink崩潰,由于默認不開啟該CONFIG,再加上page
進入node->full
需要觸發(fā)強制下架的時候cpu_slab->page
必須是滿的,概率非常低,用戶也不可控,一般不影響。不過內(nèi)核最新版已經(jīng)重寫了開啟CONFIG_SLUB_DEBUG
的邏輯,不存在這樣的問題了,我這里的代碼是5.13 版本,不影響其他邏輯。
? [3.3] 最后說明釋放之前在node->partial
中,并且釋放之后也不為空,那么還繼續(xù)呆在node->partial
中就行,直接啥也不用干返回。
[4] 走到這里說明slab page在node->partial
中(小概率在node->full
中),且釋放之后為空且node 中當前partial slab數(shù)量比slab 規(guī)定的最小partial
數(shù)量多,可以釋放空的slab page,則進入空slab處理流程:
? [4.1] 釋放之前的狀態(tài)是半滿,則調(diào)用remove_partial
從node->partial
列表中刪除,然后調(diào)用discard_slab
釋放該空slab page。
? [4.2] 釋放之前在node->full
中,則調(diào)用remove_full
從full
列表中刪除,之所以會有釋放之后直接從full 變?yōu)榭?,我猜是考慮到這個函數(shù)可以一次釋放多個堆塊的原因。還有一點就是,正常full狀態(tài)的page
在上面就會加入到cpu_slab->partial
或者node->partial
中了,無論釋放完是否為空,所以是走不到這個分支的,可見這個版本的代碼對CONFIG_SLUB_DEBUG
的處理還是比較粗糙的,目前已經(jīng)在后續(xù)版本都會更新完善。
[5] 否則說明**cpu_slab
沒開啟partial
列表,游離狀態(tài)的slab page釋放后仍需要添加到一個列表中**,則將其加入node->partial
中。
put_cpu_partial
put_cpu_partial
函數(shù)不止是將slab page 放到cpu_slab->partial
鏈表中,如果cpu_slab->partial
鏈表中的頁面數(shù)量已經(jīng)達到最大值,則還會進行洗牌操作:
mm\slub.c : put_cpu_partial
static void put_cpu_partial(struct kmem_cache *s, struct page *page, int drain)
{
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct page *oldpage;
int pages; //[1] 代表這個slab 后面大概還有多少個page
int pobjects;//代表這個slab 后面還有多少個object 可用
preempt_disable();
do {
pages = 0;
pobjects = 0;
oldpage = this_cpu_read(s->cpu_slab->partial);//獲取cpu->partial列表頭部的那個
if (oldpage) {//[1] 獲取之前的pages 和pobjects 值用來之后更新
pobjects = oldpage->pobjects;
pages = oldpage->pages;
if (drain && pobjects > slub_cpu_partial(s)) {//[2] 當前cpu_slab->partial中的頁數(shù)量已經(jīng)達到cpu_partial最大值
unsigned long flags;
/*
* partial array is full. Move the existing
* set to the per node partial list.
*/
local_irq_save(flags);
unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));//把cpu_slab->partial中現(xiàn)存的slab page都放到node->partial中
local_irq_restore(flags);
oldpage = NULL;
pobjects = 0;//cpu_slab->partial數(shù)量清0
pages = 0;
stat(s, CPU_PARTIAL_DRAIN);
}
}
pages++;//[3]更新pages和pobjects數(shù)量
pobjects += page->objects - page->inuse;
page->pages = pages; //設置當前slab 的pages 和pobjects
page->pobjects = pobjects;
page->next = oldpage;//加入鏈表頭部
} while (this_cpu_cmpxchg(s->cpu_slab->partial, oldpage, page)//[4] 最后將當前頁放入cpu_slab->partial中
!= oldpage);
if (unlikely(!slub_cpu_partial(s))) {
unsigned long flags;
local_irq_save(flags);
unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));
local_irq_restore(flags);
}
preempt_enable();
#endif /* CONFIG_SLUB_CPU_PARTIAL */
}
[1] cpu_slab->partial
中每個頁面中的pobjects
和pages
都代表這個頁以及后面的單鏈表中有多少內(nèi)存塊數(shù)和頁數(shù),這樣我們只訪問第一個頁即可了解全部信息。
[2] 如果cpu_slab->partial
中的頁數(shù)已經(jīng)達到了slab 規(guī)定的cpu_partial
最大值,則要進行unfreeze_partial
操作。下面分析該函數(shù)。主要邏輯就是將cpu_slab->partial
中的slab page都放到node->partial
中,然后更新統(tǒng)計信息,當前cpu_slab->partial
為空。
[3] 將該slab page加入到cpu_slab->partial
鏈表頭部。
[4] 最后將整個鏈表更新到cpu_slab->partial
中
unfreeze_partials
unfreeze_partials
函數(shù)主要是在cpu_slab->partial
中slab page數(shù)量已經(jīng)達到最大值的時候將cpu_slab->partial
中的slab page都轉移到node->partial
中,如果遇到空的slab page,則會釋放掉。
mm\slub.c : unfreeze_partials
static void unfreeze_partials(struct kmem_cache *s,
struct kmem_cache_cpu *c)
{
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct kmem_cache_node *n = NULL, *n2 = NULL;
struct page *page, *discard_page = NULL;
while ((page = slub_percpu_partial(c))) {//[1] 遍歷cpu_slab->partial中的page
struct page new;
struct page old;
slub_set_percpu_partial(c, page);//取出頭部的,更新新頭部
n2 = get_node(s, page_to_nid(page));//獲取page所在node
if (n != n2) {
if (n)
spin_unlock(&n->list_lock);
n = n2;
spin_lock(&n->list_lock);
}
do {
old.freelist = page->freelist;
old.counters = page->counters;//[2] 獲取page 的基本信息聯(lián)合體
VM_BUG_ON(!old.frozen);
new.counters = old.counters;
new.freelist = old.freelist;
new.frozen = 0;//要從cpu_slab->partial中拿出來,肯定要解凍
} while (!__cmpxchg_double_slab(s, page,
old.freelist, old.counters,//更新page 信息
new.freelist, new.counters,
"unfreezing slab"));
if (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) {//[3] 如果page 為空,并且node->partial數(shù)量滿足最小要求
page->next = discard_page;//把page加入到要釋放的page列表
discard_page = page;
} else {//[3.1] 否則就加入到node的的partial中
add_partial(n, page, DEACTIVATE_TO_TAIL);
stat(s, FREE_ADD_PARTIAL);
}
}
if (n)
spin_unlock(&n->list_lock);
while (discard_page) {//[4] 如果釋放page 列表不為空
page = discard_page;
discard_page = discard_page->next;
stat(s, DEACTIVATE_EMPTY);
discard_slab(s, page);//依次調(diào)用discard_slab 釋放掉slab page
stat(s, FREE_SLAB);
}
#endif /* CONFIG_SLUB_CPU_PARTIAL */
}
[1] 遍歷cpu_slab->partial
中的每一個slab page,并從鏈表中依次取出
[2] 獲取page
的基本信息,然后更新基本信息,這里是將frozen
設為0,也就是解凍,從cpu_slab
中移除
[3] 如果slab page的inuse
為0,說明該slab page已經(jīng)為空,并且如果node->partial
頁滿足最小數(shù)量要求,則將其加入到待釋放的slab page的列表
? [3.1] 否則將其加入到node->partial
[4] 將所有空的可以釋放的page
調(diào)用discard_slab
釋放掉
discard_slab->free_slab
linux\mm\slub.c : discard_slab
static void discard_slab(struct kmem_cache *s, struct page *page)
{
dec_slabs_node(s, page_to_nid(page), page->objects);//更新node中的slab 和object數(shù)量,釋放了要減少
free_slab(s, page);//free_slab 釋放slab
}
discard_slab
中先調(diào)用dec_slabs_node
進行一些統(tǒng)計,更新一下slab page被釋放后node中的slab page數(shù)量和堆塊數(shù)量。
然后調(diào)用free_slab
進行正式的slab page釋放:
linux\mm\slub.c : free_slab
static void free_slab(struct kmem_cache *s, struct page *page)
{
if (unlikely(s->flags & SLAB_TYPESAFE_BY_RCU)) {//RCU延遲釋放
call_rcu(&page->rcu_head, rcu_free_slab);
} else
__free_slab(s, page);
}
如果開啟了SLAB_TYPESAFE_BY_RCU
則使用RCU延遲釋放slab page,不太關心,默認不開啟。正常流程調(diào)用__free_slab
釋放slab page:
linux\mm\slub.c : __free_slab
static void __free_slab(struct kmem_cache *s, struct page *page)
{
int order = compound_order(page); //獲取slab所屬page階數(shù)
int pages = 1 << order; //page 頁數(shù)
if (kmem_cache_debug_flags(s, SLAB_CONSISTENCY_CHECKS)) {//如果開啟了debug,則進行一些檢測
void *p;
slab_pad_check(s, page);//slab 檢測
for_each_object(p, s, page_address(page),
page->objects)
check_object(s, page, p, SLUB_RED_INACTIVE);//slab中的object 檢測
}
__ClearPageSlabPfmemalloc(page);//這兩個都是修改page->flags的宏,清楚slab相關標志位
__ClearPageSlab(page);
/* In union with page->mapping where page allocator expects NULL */
page->slab_cache = NULL;//取消page指向slab 的指針
if (current->reclaim_state)
current->reclaim_state->reclaimed_slab += pages;//更新數(shù)據(jù)
unaccount_slab_page(page, order, s);//如果開啟了memcg相關會進行memcg去注冊
__free_pages(page, order); //釋放掉page
}
邏輯很簡單,因為在之前已經(jīng)將該slab page從node中unlink
出來了,那么接下來只需要釋放該page
即可。在__free_slab
中進行一些常規(guī)的檢查和統(tǒng)計更新,最后調(diào)用__free_pages
將頁面釋放。
特殊slab的釋放
特殊slab使用kmem_cache_free
函數(shù)來釋放堆塊:
static void file_free_rcu(struct rcu_head *head)
{
struct file *f = container_of(head, struct file, f_u.fu_rcuhead);
put_cred(f->f_cred);
kmem_cache_free(filp_cachep, f);//專屬slab的釋放函數(shù)
}
kmem_cache_free
雖然也是直接封裝的slab_free
,但釋放前會使用cache_from_obj
函數(shù)校驗一下,避免其他堆塊的釋放到錯誤的slab cache中:
void kmem_cache_free(struct kmem_cache *s, void *x)
{
s = cache_from_obj(s, x);//先校驗一下是不是這個slab的堆塊
if (!s)
return;
//調(diào)用slab_free正常free掉x
slab_free(s, virt_to_head_page(x), x, NULL, 1, _RET_IP_);
trace_kmem_cache_free(_RET_IP_, x, s->name);
}
校驗邏輯無非就是根據(jù)堆塊找page,根據(jù)page找slab_cache,不多贅述了。
內(nèi)存釋放邏輯總結
內(nèi)存對象釋放主要思路就是,如果是頁面對象,則直接伙伴系統(tǒng)釋放,如果是slab 對象,如果在cpu_slab中,在cpu_slab中釋放,如果不是,則根據(jù)釋放之前之后的狀態(tài)(為空、為滿、半滿),進行不同的操作。
-
首先找到釋放的內(nèi)存對象所在的page 的page結構體。如果該page不是slab,也就是說內(nèi)存對象不是通過slab分配的,而是直接分配的頁(大塊內(nèi)存),那么直接釋放頁。
- 大塊內(nèi)存分配的時候就是從伙伴系統(tǒng)直接分配的頁,釋放的時候頁通過伙伴系統(tǒng)釋放頁面(
__free_pages
)
- 大塊內(nèi)存分配的時候就是從伙伴系統(tǒng)直接分配的頁,釋放的時候頁通過伙伴系統(tǒng)釋放頁面(
-
其他內(nèi)存是通過slab分配,則通過slab釋放(
slab_free
)-
memcg相關處理(
memcg_slab_free_hook
) -
獲取當前
cpu_slab
,如果要釋放的內(nèi)存對象正好屬于當前cpu_slab
(可以理解為是否是從當前cpu_slab
分配的),則快速釋放- 獲取
cpu_slab
的freelist
,將該內(nèi)存對象插入freelist
頭部,刷新cpu_slab
相關信息(do_slab_free
)
- 獲取
-
如果要釋放的內(nèi)存對象不屬于當前
cpu_slab
,(當前slab page在cpu_slab->partial
、別的cpu_slab->page
、別的cpu_slab->partial
、游離狀態(tài)、node->partial
、node->full
6種情況),需要慢速釋放(__slab_free
)- 先把內(nèi)存對象釋放到slab page的
freelist
頭部,更新slab page相關統(tǒng)計信息 - 如果該slab page為凍結狀態(tài)(說明是在
cpu_slab
中的三種情況)- 則直接結束(已經(jīng)將
object
放到page->freelist
了,剩下的就不用管了)
- 則直接結束(已經(jīng)將
- 如果釋放前該slab page是滿的(freelist為空),則說明
page
目前是游離狀態(tài)(不在任何列表中)或node->full
中- 如果開啟
cpu->partial
,則將該slab page放到cpu_slab->partial
中(從node->full
中移除)- 如果
cpu_slab->partial
滿了,則要將當前cpu_slab->partial
中的所有slab page放到node->partial
中,然后再將新的slab page放到cpu_slab->partial
中
- 如果
- 否則放入
node->partial
中(從node->full
中移除)
- 如果開啟
- 如果釋放后為空,則說明目前該slab page一定處在
node->partial
列表中,因為如果在cpu_slab
或者游離狀態(tài)或node->full
中不管釋放完是否為空,都會在上面的步驟中處理完畢- 如果當前slab 管理的
partial
頁面數(shù)量滿足最小要求,則將該釋放后為空的slab page釋放掉(__free_pages
) - 否則不變,繼續(xù)呆在
node->partial
中
- 如果當前slab 管理的
- 否則說明該
page
是本來就呆在node->partial
中的半滿page
,并且釋放后還是半滿,則什么也不操作。
- 先把內(nèi)存對象釋放到slab page的
-
slab page狀態(tài)轉換關系圖
結合kmalloc
和kfree
的邏輯,可以畫出slab page的狀態(tài)轉換關系圖:文章來源:http://www.zghlxwxcb.cn/news/detail-407581.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-407581.html
到了這里,關于[linux kernel]slub內(nèi)存管理分析(5) kfree的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!