作者:povcfe
本文爲作者投稿,Seebug Paper 期待你的分享,凡經採用即有禮品相送!
投稿郵箱:[email protected]
linux作爲開源內核,被廣泛使用。同時隨着用戶態安全機制的逐漸完善,攻擊成本逐年升高,越來越多的黑客將目光投向linux內核,linux內核安全問題也隨之被越來越多的安全研究人員關注。但作爲一個規模宏大的開源項目,linux內核安全研究存在非常高的研究門檻,不管是針對特定模塊的漏洞挖掘,還是cve復現,對內核的理解限制了絕大多數安全研究人員。而本文則希望通過對內核源碼做詳細分析來讓更多的安全研究人員越過內核門檻。 這篇文章的貢獻如下:
(1)硬件層面上分析linux內核頁表
(2)從buddy源碼分析linux內核頁管理
(3)從slub源碼分析linux內核小內存管理
(4)從vma源碼分析linux內核對於進程內存空間的佈局及管理
(5)分析缺頁中斷流程
(6)從ptmalloc源碼分析用戶態內存管理機制,以及如何通過特定api與linux內核交互
頁表
頁表查詢--以x86_64下的4級頁表舉例(硬件)
- 流程總覽(定義虛擬地址virt_addr, 尋找對應的物理地址phy_addr)
- 順序: TLB -> 頁表結構cache -> 頁表(MMU硬件實現)
- MMU = TLB(Translation Lookaside Buffer) + table walk unit
TLB轉換
- 明確概念:
VPN(virtual page number), PPN(physical page number),VPO(virtual page offset)和PPO(physical page offset)
- 對於線性地址和物理地址而言, 都是以page爲最小的管理單元, 那麼也就是說如果線性地址可以映射到某物理地址, 則兩者頁偏移相同(默認page size = 4K, 佔用低12bits), 即VPO = PPO
- TLB可以簡單理解成VPN->PPN(36bits), 實現了一個線性映射, 基本結構如下:
-
通過VPN(virt_addr[12:48])定位表項
-
全相連(full associative)-- VPN可以被填充在TLB中的任何位置
- 定位VPN對應的表項需要遍歷TLB中的所有表項
-
直接匹配-- VPN被映射在特定位置
- 如果TLB存在n個表項, 那麼VPN%n即爲該VPN的索引
- 定位到索引後, 查看VPN是否匹配, 如果不匹配則TLB miss
-
組相連(set-associative)-- 全相連和直接匹配相結合
- TLB被劃分爲m組, 每個組存在n表項, VPN分爲set(VPN[47-log2(m):48]), tag(VPN[12:48-log2(m)])
- VPN[47-log2(m):48]%m爲該VPN的set索引
- 定位到索引後, 查看set內是否存在tag, 如果不存在則TLB miss
頁錶轉換
-
明確概念:
-
對於四級頁表: PGD(page global directory), PUD(page upper directory), PMD(page middle directory), PTE(page table entry), 每個頁表佔9bits, 支持48bits虛擬地址
-
對於五級頁表:添加P4D表項, 支持57位虛擬地址
-
通過virt_addr[12:48]定位page table entry
-
CR3寄存器存儲PGD物理地址, virt_addr[12:21]爲PGD_index, PGD+PGD_index=PGD_addr
- virt_addr[21:30]爲PUD_index, PGD_addr+PUD_index=PUD_addr
- virt_addr[30:39]爲PME_index, PUD_addr+PME_index=PME_addr
-
virt_addr[39:48]爲PTE_index, PME_addr+PTE_index=PTE_addr
-
PTE_addr即爲page table entry是一個表項映射到PPN
頁表結構cache轉換
- 明確概念:
-
如果某些虛擬地址臨近, 那麼很有可能他們會有相同的頁表項(PGD or PUD or PMD or PTE),對於這種情況如果還是依次查詢頁表就會浪費大量時間, 所以存在頁表結構cache, 用來緩存頁表
-
cache種類:
- PDE cache(virt_addr[22:48]作爲tag, 映射PME entry地址)
- PDPTE cache(virt_addr[31:48]作爲tag, 映射PUD entry地址)
- PML4 cache(virt_addr[40:48]作爲tag, 映射PGD entry地址)
拓展
普通頁表cache
- 明確概念:
-
頁表保存在內存中, 可以被緩存到普通cache
-
各級頁表中存在PCD(page-level cache disable)標誌位, 控制下一級頁表是否需要被緩存
Huge_Page
- 明確概念:
- 頁表中指向下一級的地址是按頁對齊的, 也就是低12bits無效, 可以用作flag標誌位
-
page size flag爲1時表示當前頁表的下級地址對應大頁地址而不是頁表
-
x86兩級頁表支持4MB大頁(跳過PTE, 4K*2^10=4MB)
-
x86_64四級頁表支持2MB大頁(跳過PTE, 4K*2^9=2MB), 1GB大頁(跳過PME, 2M*2^9=1GB)
頁表標誌位
-
P(Present) - 爲1表明該page存在於當前物理內存中, 爲0則觸發page fault。
-
G(Global)- 標記kernel對應的頁, 也存在於TLB entry, 表示該頁不會被flush掉。
-
A(Access) - 當page被訪問(讀/寫)過後, 硬件置1。軟件可置0, 然後對應的TLB將會被flush掉。
-
D(Dirty)- 對寫回的page有作用。當page被寫入後, 硬件置1, 表明該page的內容比外部disk/flash對應部分要新, 當系統內存不足, 要將該page回收的時候, 需首先將其內容flush到外部存儲, 之後軟件將該標誌位清0。
-
R/W和U/S屬於權限控制類:
-
R/W(Read/Write) - 置1表示該page是writable的, 置0是readonly。
-
U/S(User/Supervisor) - 置0表示只有supervisor(比如操作系統中的kernel)纔可訪問該page, 置1表示user也可以訪問。
-
PCD和PWT和cache屬性相關:
-
PCD(Page Cache Disabled)- 置1表示disable, 即該page中的內容是不可以被cache的。如果置0(enable), 還要看CR0寄存器中的CD位這個總控開關是否也是0。
-
PWT (Page Write Through)- 置1表示該page對應的cache部分採用write through的方式, 否則採用write back。
-
64位特有:
-
XD (Execute Disable)- 在bit[63]中
-
CR3支持PCID:
-
CR4寄存器的PCIDE位 = 1, 則CR3低12位表示PCID(覆蓋PCD和PWT--CR3低12bits只有PCD和PWT標誌位)
夥伴算法(buddy)
- alloc_pages(內存分配)概略圖
+ __free_pages(內存釋放)縮略圖
alloc_pages源碼分析
- pol變量保存內存分配策略(man set_mempolicy)
- MPOL_DEFAULT: 默認策略, 優先從當前結點分配內存, 若當前結點無空閒內存, 則從最近的有空閒內存的結點分配
- MPOL_BIND: 指定內存分配結點集合, 若該集合內無空閒內存, 則分配失敗
- MPOL_INTERLEAVE: 內存分配要覆蓋所有結點, 且每個結點使用的內存大小相同, 常用於共享內存區域
- MPOL_PREFERRED: 從指定結點上分配內存, 若該結點無空閒內存, 則從其他結點分配
- MPOL_LOCAL: 同MPOL_DEFAULT
- water_mark
enum zone_watermarks {
WMARK_MIN,
WMARK_LOW,
WMARK_HIGH,
NR_WMARK
};
- WMARK_MIN: 當空閒頁面的數量降到WMARK_MIN時, 喚醒 kswapd 守護進程以同步的方式進行直接內存回收, 同時只有GFP_ATOMIC可以在這種情況下分配內存
- WMARK_LOW: 當空閒頁面的數量降到WMARK_LOW時, 喚醒 kswapd 守護進程進行內存回收
-
WMARK_HIGH: kswapd進程休眠
-
自旋鎖(spin_lock)
- 爲什麼使用自旋鎖:
- 使用常規鎖會發生上下文切換,時間不可預期,對於一些簡單的、極短的臨界區來說是一種性能損耗
- 中斷上下文是不允許睡眠的,除了自旋鎖以外的其他鎖都有可能導致睡眠或者進程切換,這是違背了中斷的設計初衷,會發生不可預知的錯誤
- 自旋鎖的功能: 一直輪詢等待檢查臨界區是否可用, 直至時間片用完
- 自旋鎖使用原則:
- 禁止搶佔: 如果A, B同時訪問臨界區, A進程首先獲得自旋鎖, B進程輪詢等待, B搶佔A後, B無法獲得自旋鎖, 造成死鎖
- 禁止睡眠: 如果自旋鎖鎖住以後進入睡眠,而又不能進行處理器搶佔,內核的調取器無法調取其他進程獲得該CPU,從而導致該CPU被掛起;同時該進程也無法自喚醒且一直持有該自旋鎖,進一步會導致其他使用該自旋鎖的位置出現死鎖
- 自旋鎖的幾個實現:
- spin_lock: 只禁止內核搶佔, 不會關閉本地中斷
- spin_lock_irq: 禁止內核搶佔, 且關閉本地中斷
- spin_lock_irqsave: 禁止內核搶佔, 關閉中斷, 保存中斷狀態寄存器的標誌位
- spin_lock與spin_lock_irq的區別:
- 禁止中斷與禁止搶佔的原因相同
- spin_lock_irq與spin_lock_irqsave的區別:
- 假設臨界區被兩把spin_lock_irq(a->b)鎖定, 當b解鎖後(a還在加鎖中), 不會保存a加鎖後的中斷寄存器狀態(直接開中斷), 也就是鎖a在加鎖時, 中斷被打開, 導致spin_lock_irq在功能上和spin_lock相同, 也就具備了spin_lock的中斷隱患
alloc_pages_current
struct page *alloc_pages_current(gfp_t gfp, unsigned order)
{
// pol變量保存內存分配策略(默認爲default_policy)
struct mempolicy *pol = &default_policy;
struct page *page;
// 如果不在中斷狀態下且未指定在當前結點分配內存時, 使用get_task_policy獲得當前進程內存分配策略
if (!in_interrupt() && !(gfp & __GFP_THISNODE))
pol = get_task_policy(current);
// 如果內存分配策略爲MPOL_INTERLEAVE, 則進入alloc_page_interleave
if (pol->mode == MPOL_INTERLEAVE)
page = alloc_page_interleave(gfp, order, interleave_nodes(pol));
else
page = __alloc_pages_nodemask(gfp, order,
policy_node(gfp, pol, numa_node_id()),
policy_nodemask(gfp, pol));
return page;
}
__alloc_pages_nodemask
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, int preferred_nid,
nodemask_t *nodemask)
{
struct page *page;
unsigned int alloc_flags = ALLOC_WMARK_LOW;
gfp_t alloc_mask; /* The gfp_t that was actually used for allocation */
struct alloc_context ac = { };
// 如果order大於MAX_ORDER(11), 則內存分配失敗
if (unlikely(order >= MAX_ORDER)) {
WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN));
return NULL;
}
// 添加gfp_allowed_mask標誌位
gfp_mask &= gfp_allowed_mask;
alloc_mask = gfp_mask;
// 填充ac參數(用於內存分配), 並做一些檢查
if (!prepare_alloc_pages(gfp_mask, order, preferred_nid, nodemask, &ac, &alloc_mask, &alloc_flags))
return NULL;
// 決定是否平衡各個zone中的髒頁, 確定zone(相當於對prepare_alloc_pages的補充)
finalise_ac(gfp_mask, &ac);
// 給alloc_flags添加ALLOC_NOFRAGMENT標誌位(不使用zone備用遷移類型), 如果遍歷完本地zone後仍然無法分配內存則取消該標誌位, 該方案是爲了減少內存碎片
alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp_mask);
// 通過快分配分配內存頁
page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
if (likely(page))
goto out;
alloc_mask = current_gfp_context(gfp_mask);
ac.spread_dirty_pages = false;
if (unlikely(ac.nodemask != nodemask))
ac.nodemask = nodemask;
// 通過慢分配分配內存頁
page = __alloc_pages_slowpath(alloc_mask, order, &ac);
out:
if (memcg_kmem_enabled() && (gfp_mask & __GFP_ACCOUNT) && page &&
unlikely(__memcg_kmem_charge(page, gfp_mask, order) != 0)) {
__free_pages(page, order);
page = NULL;
}
trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype);
return page;
}
- prepare_alloc_pages
static inline bool prepare_alloc_pages(gfp_t gfp_mask, unsigned int order,
int preferred_nid, nodemask_t *nodemask,
struct alloc_context *ac, gfp_t *alloc_mask,
unsigned int *alloc_flags)
{
// ac填充從gfp_mask獲取的內存分配參數
// 獲得當前nodemask對應的zone的max_index
ac->high_zoneidx = gfp_zone(gfp_mask);
// 獲得node對應的zone_list
ac->zonelist = node_zonelist(preferred_nid, gfp_mask);
ac->nodemask = nodemask;
// 選擇遷移類型
ac->migratetype = gfpflags_to_migratetype(gfp_mask);
// 判斷是否存在cpuset機制
if (cpusets_enabled()) {
*alloc_mask |= __GFP_HARDWALL;
if (!ac->nodemask)
ac->nodemask = &cpuset_current_mems_allowed;
else
*alloc_flags |= ALLOC_CPUSET;
}
// 函數未實現
fs_reclaim_acquire(gfp_mask);
fs_reclaim_release(gfp_mask);
// 如果內存緊張可能會休眠
might_sleep_if(gfp_mask & __GFP_DIRECT_RECLAIM);
// 對gfp_mask, ord做檢查(默認沒有開啓CONFIG_FAIL_PAGE_ALLOC的情況下, 直接return false)
if (should_fail_alloc_page(gfp_mask, order))
return false;
// 匹配CMA機制
if (IS_ENABLED(CONFIG_CMA) && ac->migratetype == MIGRATE_MOVABLE)
*alloc_flags |= ALLOC_CMA;
return true;
}
- finalise_ac
static inline void finalise_ac(gfp_t gfp_mask, struct alloc_context *ac)
{
/* Dirty zone balancing only done in the fast path */
ac->spread_dirty_pages = (gfp_mask & __GFP_WRITE);
// 從zone_list頭部開始尋找匹配nodemask的zoneref
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->high_zoneidx, ac->nodemask);
}
get_page_from_freelist
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
const struct alloc_context *ac)
{
struct zoneref *z;
struct zone *zone;
struct pglist_data *last_pgdat_dirty_limit = NULL;
bool no_fallback;
retry:
/*
* Scan zonelist, looking for a zone with enough free.
*/
// ALLOC_NOFRAGMENT標誌位由alloc_flags_nofragment()函數設置
// no_fallback: node->node_zonelists[]包含本node的zones以及備用zones, 設置fallback後可使用備用zones
no_fallback = alloc_flags & ALLOC_NOFRAGMENT;
z = ac->preferred_zoneref;
// 遍歷zone
for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx,
ac->nodemask) {
struct page *page;
unsigned long mark;
// 判斷cpuset是否開啓且當前CPU是否允許在內存域zone所在結點中分配內存
if (cpusets_enabled() &&
(alloc_flags & ALLOC_CPUSET) &&
!__cpuset_zone_allowed(zone, gfp_mask))
continue;
// ac->spread_dirty_pages不爲0表示gfp_mask存在__GFP_WRITE標誌位, 有可能增加髒頁
if (ac->spread_dirty_pages) {
if (last_pgdat_dirty_limit == zone->zone_pgdat)
continue;
// 如果zone對應的node髒頁超標則使用last_pgdat_dirty_limit標識, 並跳過該zone
if (!node_dirty_ok(zone->zone_pgdat)) {
last_pgdat_dirty_limit = zone->zone_pgdat;
continue;
}
}
// 如果設置no_fallback且當前zone並非preferred_zone, 則索引zone->node, 如果該node並非preferred_zone->node, 則取消ALLOC_NOFRAGMENT標誌位即設置fallback(因爲相比於內存碎片, 內存局部性更重要)
if (no_fallback && nr_online_nodes > 1 &&
zone != ac->preferred_zoneref->zone) {
int local_nid;
local_nid = zone_to_nid(ac->preferred_zoneref->zone);
if (zone_to_nid(zone) != local_nid) {
alloc_flags &= ~ALLOC_NOFRAGMENT;
goto retry;
}
}
// 獲取該zone的水準, 並檢查該zone的水位是否水準之上
mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);
if (!zone_watermark_fast(zone, order, mark,
ac_classzone_idx(ac), alloc_flags,
gfp_mask)) {
int ret;
// 如果存在ALLOC_NO_WATERMARKS標誌位則忽略水位, 進入try_this_zone
if (alloc_flags & ALLOC_NO_WATERMARKS)
goto try_this_zone;
/*
static bool zone_allows_reclaim(struct zone *local_zone, struct zone *zone)
{
return node_distance(zone_to_nid(local_zone), zone_to_nid(zone)) <=
node_reclaim_distance;
}
*/
// 如果系統不允許回收內存或者preferred->zone與當前zone的node_distance大於node_reclaim_distance(默認30), 則更換zone
if (node_reclaim_mode == 0 ||
!zone_allows_reclaim(ac->preferred_zoneref->zone, zone))
continue;
// 內存回收
ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
switch (ret) {
case NODE_RECLAIM_NOSCAN:
/* did not scan */
continue;
case NODE_RECLAIM_FULL:
/* scanned but unreclaimable */
continue;
default:
// 內存回收後, 水位正常
if (zone_watermark_ok(zone, order, mark,
ac_classzone_idx(ac), alloc_flags))
goto try_this_zone;
continue;
}
}
try_this_zone:
// 夥伴算法開始分配頁內存
page = rmqueue(ac->preferred_zoneref->zone, zone, order,
gfp_mask, alloc_flags, ac->migratetype);
if (page) {
prep_new_page(page, order, gfp_mask, alloc_flags);
if (unlikely(order && (alloc_flags & ALLOC_HARDER)))
reserve_highatomic_pageblock(page, zone, order);
return page;
} else {
}
rmqueue
static inline
struct page *rmqueue(struct zone *preferred_zone,
struct zone *zone, unsigned int order,
gfp_t gfp_flags, unsigned int alloc_flags,
int migratetype)
{
unsigned long flags;
struct page *page;
//如果分配單頁, 則進入rmqueue_pcplist
if (likely(order == 0)) {
page = rmqueue_pcplist(preferred_zone, zone, gfp_flags,
migratetype, alloc_flags);
goto out;
}
// 不能使用__GFP_NOFAIL, 分配order>1的頁
WARN_ON_ONCE((gfp_flags & __GFP_NOFAIL) && (order > 1));
// 使用自旋鎖加鎖zone資源
spin_lock_irqsave(&zone->lock, flags);
do {
page = NULL;
// ALLOC_HARDER表示高優先級分配, 進入__rmqueue_smallest
if (alloc_flags & ALLOC_HARDER) {
page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);
if (page)
// 用於debug的插樁設計
trace_mm_page_alloc_zone_locked(page, order, migratetype);
}
// 不滿足上訴條件或page未分配成功, 進入__rmqueue
if (!page)
page = __rmqueue(zone, order, migratetype, alloc_flags);
} while (page && check_new_pages(page, order));
// check_new_pages遍歷page_block中的struct page, 檢查page成員, 如果出錯則打印錯誤原因
spin_unlock(&zone->lock);
if (!page)
goto failed;
// page_block被分配後更新zone成員信息
__mod_zone_freepage_state(zone, -(1 << order),
get_pcppage_migratetype(page));
__count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order);
// 如果系統是NUMA架構, 則更新NUMA hit/miss 數據
zone_statistics(preferred_zone, zone);
// 恢復中斷信息
local_irq_restore(flags);
out:
/* Separate test+clear to avoid unnecessary atomics */
if (test_bit(ZONE_BOOSTED_WATERMARK, &zone->flags)) {
clear_bit(ZONE_BOOSTED_WATERMARK, &zone->flags);
wakeup_kswapd(zone, 0, 0, zone_idx(zone));
}
// 編譯階段的變量類型檢查
VM_BUG_ON_PAGE(page && bad_range(zone, page), page);
return page;
failed:
local_irq_restore(flags);
return NULL;
}
rmqueue_pcplis
static struct page *rmqueue_pcplist(struct zone *preferred_zone,
struct zone *zone, gfp_t gfp_flags,
int migratetype, unsigned int alloc_flags)
{
struct per_cpu_pages *pcp;
struct list_head *list;
struct page *page;
unsigned long flags;
// 禁用全部中斷, 並將當前中斷狀態保存至flags
local_irq_save(flags);
// 獲得當前cpu的pcp結構體(熱頁)
pcp = &this_cpu_ptr(zone->pageset)->pcp;
// 根據遷移類型選擇熱頁鏈表
list = &pcp->lists[migratetype];
// 在list中分配內存頁
page = __rmqueue_pcplist(zone, migratetype, alloc_flags, pcp, list);
if (page) {
__count_zid_vm_events(PGALLOC, page_zonenum(page), 1);
// Update NUMA hit/miss statistics
zone_statistics(preferred_zone, zone);
}
// 恢復中斷狀態並開中斷
local_irq_restore(flags);
return page;
}
- __rmqueue_pcplist
static struct page *__rmqueue_pcplist(struct zone *zone, int migratetype,
unsigned int alloc_flags,
struct per_cpu_pages *pcp,
struct list_head *list)
{
struct page *page;
do {
// 如果列表爲空, 則使用rmqueue_bulk裝載內存頁進入列表
if (list_empty(list)) {
pcp->count += rmqueue_bulk(zone, 0,
pcp->batch, list,
migratetype, alloc_flags);
if (unlikely(list_empty(list)))
return NULL;
}
// 獲得lru列表首部頁結點
page = list_first_entry(list, struct page, lru);
// 將頁結點從page->lru列表刪除
list_del(&page->lru);
// 空閒page計數器-1
pcp->count--;
// 對page做安全檢查
} while (check_new_pcp(page));
return page;
}
- rmqueue_bulk
static int rmqueue_bulk(struct zone *zone, unsigned int order,
unsigned long count, struct list_head *list,
int migratetype, unsigned int alloc_flags)
{
int i, alloced = 0;
// 對zone資源加鎖
spin_lock(&zone->lock);
for (i = 0; i < count; ++i) {
// 從zone中取出page放入pcp熱頁緩存列表, 直至pcp被填滿
struct page *page = __rmqueue(zone, order, migratetype,
alloc_flags);
if (unlikely(page == NULL))
break;
// check_pcp_refill封裝check_new_page
if (unlikely(check_pcp_refill(page)))
continue;
// 添加page至list->lru
list_add_tail(&page->lru, list);
alloced++;
// 如果page位於cma中, 則更新NR_FREE_CMA_PAGES
if (is_migrate_cma(get_pcppage_migratetype(page)))
__mod_zone_page_state(zone, NR_FREE_CMA_PAGES,
-(1 << order));
}
// 從zone摘取page_block過程循環了i次, 每個page_block包含2^i個page, NR_FREE_PAGES-i << order, 更新NR_FREE_PAGES
__mod_zone_page_state(zone, NR_FREE_PAGES, -(i << order));
// 解鎖
spin_unlock(&zone->lock);
return alloced;
}
__rmqueue_smallest
static __always_inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
int migratetype)
{
unsigned int current_order;
struct free_area *area;
struct page *page;
// 從指定order到MAX_ORDER遍歷zone->free_area[]
for (current_order = order; current_order < MAX_ORDER; ++current_order) {
area = &(zone->free_area[current_order]);
// 從zone->free_area[][migratetype]->lru鏈表頭部獲得page()
page = get_page_from_free_area(area, migratetype);
if (!page)
continue;
// 從zone->free_area[][migratetype]->lru中刪除page, 更新zone成員
del_page_from_free_area(page, area);
// 將current_order階的page_block拆成小塊,並將小塊放到對應的階的鏈表中去
expand(zone, page, order, current_order, area, migratetype);
// 設置page遷移類型
set_pcppage_migratetype(page, migratetype);
return page;
}
return NULL;
}
__rmqueue
static __always_inline struct page *
__rmqueue(struct zone *zone, unsigned int order, int migratetype,
unsigned int alloc_flags)
{
struct page *page;
retry:
// 使用__rmqueue_smallest獲得page
page = __rmqueue_smallest(zone, order, migratetype);
if (unlikely(!page)) {
// page分配失敗後, 如果遷移類型是MIGRATE_MOVABLE, 進入__rmqueue_cma_fallback
if (migratetype == MIGRATE_MOVABLE)
page = __rmqueue_cma_fallback(zone, order);
// page分配再次失敗後使用判斷是否可以使用備用遷移類型(如果可以則修改order, migratetype)然後跳轉進入retry
if (!page && __rmqueue_fallback(zone, order, migratetype,
alloc_flags))
goto retry;
}
trace_mm_page_alloc_zone_locked(page, order, migratetype);
return page;
}
- __rmqueue_fallback
- 備用遷移類型
static int fallbacks[MIGRATE_TYPES][4] = {
[MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
[MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES },
[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
#ifdef CONFIG_CMA
#endif
#ifdef CONFIG_MEMORY_ISOLATION
#endif
};
- __rmqueue_fallback
static __always_inline bool
__rmqueue_fallback(struct zone *zone, int order, int start_migratetype,
unsigned int alloc_flags)
{
struct free_area *area;
int current_order;
int min_order = order;
struct page *page;
int fallback_mt;
bool can_steal;
// 如果設置alloc_flags爲ALLOC_NOFRAGMENT(內存碎片優化), min_order=pageblock_order(MAX_ORDER-1)---儘可能分配大頁
if (alloc_flags & ALLOC_NOFRAGMENT)
min_order = pageblock_order;
// 遍歷zone->free_area[order](order=MAX_ORDER-1~min_order)
for (current_order = MAX_ORDER - 1; current_order >= min_order;
--current_order) {
area = &(zone->free_area[current_order]);
// 查找可以盜取的遷移類型
fallback_mt = find_suitable_fallback(area, current_order,
start_migratetype, false, &can_steal);
if (fallback_mt == -1)
continue;
// 如果can_steal=0且遷移類型爲MIGRATE_MOVABLE, 當前所在的order大於需求order, 跳轉進入find_smallest
// 這裏的can_steal=0並不表示不能盜取, 只是對於遷移類型爲MIGRATE_MOVABLE的內存分配需求有更好的解決方法(竊取和拆分最小的可用頁塊而不是最大的可用頁塊)所以單獨列出
if (!can_steal && start_migratetype == MIGRATE_MOVABLE
&& current_order > order)
goto find_smallest;
goto do_steal;
}
return false;
find_smallest:
// 從最小的order開始遍歷
for (current_order = order; current_order < MAX_ORDER;
current_order++) {
area = &(zone->free_area[current_order]);
fallback_mt = find_suitable_fallback(area, current_order,
start_migratetype, false, &can_steal);
if (fallback_mt != -1)
break;
}
VM_BUG_ON(current_order == MAX_ORDER);
do_steal:
// 獲得備用遷移類型對應的page_block
page = get_page_from_free_area(area, fallback_mt);
// 判斷直接盜取(改變page_block的遷移類型), 還是借用(分配但不改變頁塊遷移類型)
steal_suitable_fallback(zone, page, alloc_flags, start_migratetype,
can_steal);
trace_mm_page_alloc_extfrag(page, order, current_order,
start_migratetype, fallback_mt);
return true;
}
- find_suitable_fallback
int find_suitable_fallback(struct free_area *area, unsigned int order,
int migratetype, bool only_stealable, bool *can_steal)
{
int i;
int fallback_mt;
// 判斷該order內存鏈表是否爲空
if (area->nr_free == 0)
return -1;
*can_steal = false;
for (i = 0;; i++) {
// 遍歷備用遷移類型
fallback_mt = fallbacks[migratetype][i];
// MIGRATE_TYPES表示不可用, 退出
if (fallback_mt == MIGRATE_TYPES)
break;
// 如果area->free_list[fallback_mt]爲空, 遍歷下一個備用遷移類型
if (free_area_empty(area, fallback_mt))
continue;
// 判斷是否可盜取
if (can_steal_fallback(order, migratetype))
*can_steal = true;
if (!only_stealable)
return fallback_mt;
if (*can_steal)
return fallback_mt;
}
return -1;
}
- can_steal_fallback
static bool can_steal_fallback(unsigned int order, int start_mt)
{
// 判斷order是否大於等於MAX_ORDER-1
if (order >= pageblock_order)
return true;
// 如果order>=(MAX_ORDER-1)/2 或者 遷移類型爲MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE 或者 page_group_by_mobility_disabled=1(gdb動調發現默認爲0) 則表示可以盜取
if (order >= pageblock_order / 2 ||
start_mt == MIGRATE_RECLAIMABLE ||
start_mt == MIGRATE_UNMOVABLE ||
page_group_by_mobility_disabled)
return true;
return false;
}
_alloc_pages_slowpath
static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
struct alloc_context *ac)
{
bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
// PAGE_ALLOC_COSTLY_ORDER=3
const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;
struct page *page = NULL;
unsigned int alloc_flags;
unsigned long did_some_progress;
enum compact_priority compact_priority;
enum compact_result compact_result;
int compaction_retries;
int no_progress_loops;
unsigned int cpuset_mems_cookie;
int reserve_flags;
// 如果內存分配標誌包含__GFP_ATOMIC(來自不能阻塞或延遲和失敗沒有回調的原子上下文的請求), __GFP_DIRECT_RECLAIM(可以直接回收, 表示有回收需要時會阻塞請求), 明顯二者衝突, 此處做一個校驗
if (WARN_ON_ONCE((gfp_mask & (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)) ==
(__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)))
gfp_mask &= ~__GFP_ATOMIC;
retry_cpuset:
compaction_retries = 0;
no_progress_loops = 0;
compact_priority = DEF_COMPACT_PRIORITY;
cpuset_mems_cookie = read_mems_allowed_begin();
// 降低要求, 重新構建標誌位
alloc_flags = gfp_to_alloc_flags(gfp_mask);
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->high_zoneidx, ac->nodemask);
if (!ac->preferred_zoneref->zone)
goto nopage;
// 如果設置了ALLOC_KSWAPD, 則喚醒交換進程
if (alloc_flags & ALLOC_KSWAPD)
wake_all_kswapds(order, gfp_mask, ac);
// 內存調整後再次分配
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page)
goto got_pg;
// 如果滿足以下條件則嘗試進行內存壓縮
// 1. 如果標識__GFP_DIRECT_RECLAIM&ALLOC_NO_WATERMARK且order>3(costly_order=1)則進入__alloc_pages_direct_compact
// 2. 如果標識__GFP_DIRECT_RECLAIM&ALLOC_NO_WATERMARK且order<3(costly_order=0)且遷移類型不爲MIGRATE_MOVABLE則進入__alloc_pages_direct_compact
if (can_direct_reclaim &&
(costly_order ||
(order > 0 && ac->migratetype != MIGRATE_MOVABLE))
&& !gfp_pfmemalloc_allowed(gfp_mask)) {
page = __alloc_pages_direct_compact(gfp_mask, order,
alloc_flags, ac,
INIT_COMPACT_PRIORITY,
&compact_result);
if (page)
goto got_pg;
// 設置壓縮參數, 後面會專門講解這部分
if (order >= pageblock_order && (gfp_mask & __GFP_IO) &&
!(gfp_mask & __GFP_RETRY_MAYFAIL)) {
if (compact_result == COMPACT_SKIPPED ||
compact_result == COMPACT_DEFERRED)
goto nopage;
}
if (costly_order && (gfp_mask & __GFP_NORETRY)) {
if (compact_result == COMPACT_DEFERRED)
goto nopage;
compact_priority = INIT_COMPACT_PRIORITY;
}
}
retry:
// 再次喚醒交換進程
if (alloc_flags & ALLOC_KSWAPD)
wake_all_kswapds(order, gfp_mask, ac);
reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
if (reserve_flags)
alloc_flags = reserve_flags;
// 如果cpu不允許在zone所在node中分配內存且可以進行no_water_mark分配則通過ac->nodemask = NULL降低內存分配標準, 再次分配
if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
ac->nodemask = NULL;
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->high_zoneidx, ac->nodemask);
}
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page)
goto got_pg;
if (!can_direct_reclaim)
goto nopage;
if (current->flags & PF_MEMALLOC)
goto nopage;
// 內存回收後分配內存
page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
&did_some_progress);
if (page)
goto got_pg;
// 內存壓縮後分配內存
page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
compact_priority, &compact_result);
if (page)
goto got_pg;
if (gfp_mask & __GFP_NORETRY)
goto nopage;
if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
goto nopage;
// 分析是否應該再次內存回收
if (should_reclaim_retry(gfp_mask, order, ac, alloc_flags,
did_some_progress > 0, &no_progress_loops))
goto retry;
// 分析是否應該再次內存壓縮
if (did_some_progress > 0 &&
should_compact_retry(ac, order, alloc_flags,
compact_result, &compact_priority,
&compaction_retries))
goto retry;
if (check_retry_cpuset(cpuset_mems_cookie, ac))
goto retry_cpuset;
// 殺死一些進程以獲得內存
page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
if (page)
goto got_pg;
if (tsk_is_oom_victim(current) &&
(alloc_flags == ALLOC_OOM ||
(gfp_mask & __GFP_NOMEMALLOC)))
goto nopage;
if (did_some_progress) {
no_progress_loops = 0;
goto retry;
}
nopage:
if (check_retry_cpuset(cpuset_mems_cookie, ac))
goto retry_cpuset;
if (gfp_mask & __GFP_NOFAIL) {
if (WARN_ON_ONCE(!can_direct_reclaim))
goto fail;
WARN_ON_ONCE(current->flags & PF_MEMALLOC);
WARN_ON_ONCE(order > PAGE_ALLOC_COSTLY_ORDER);
// 使用ALLOC_HARDER標誌進行內存分配
page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac);
if (page)
goto got_pg;
cond_resched();
goto retry;
}
fail:
warn_alloc(gfp_mask, ac->nodemask,
"page allocation failure: order:%u", order);
got_pg:
return page;
}
_free_pages源碼分析
void __free_pages(struct page *page, unsigned int order)
{
// 檢查並更新(-1)page->_refcount, 當page->_refcount=0時, return true
if (put_page_testzero(page))
// 如果order=0 --> free_unref_page
// 如果order>0 --> __free_pages_ok
free_the_page(page, order);
}
free_unref_page
- free_unref_page -> free_unref_page_commit
static void free_unref_page_commit(struct page *page, unsigned long pfn)
{
struct zone *zone = page_zone(page);
struct per_cpu_pages *pcp;
int migratetype;
// 獲得遷移類型
migratetype = get_pcppage_migratetype(page);
__count_vm_event(PGFREE);
// pcp_list 只放置unmovable, reclaimable, movable類型page
// 大於等於MIGRATE_PCPTYPES的遷移類型中MIGRATE_ISOLATE不能被放入pcp
if (migratetype >= MIGRATE_PCPTYPES) {
if (unlikely(is_migrate_isolate(migratetype))) {
// 放入夥伴系統
free_one_page(zone, page, pfn, 0, migratetype);
return;
}
migratetype = MIGRATE_MOVABLE;
}
pcp = &this_cpu_ptr(zone->pageset)->pcp;
// 將page放入pcp->lists[migratetype]鏈表表頭
list_add(&page->lru, &pcp->lists[migratetype]);
pcp->count++;
// 如果pcp->count(pcp中頁數目) >= pcp->high(pcp中最大頁數目), 則將多餘的page放入夥伴系統
if (pcp->count >= pcp->high) {
unsigned long batch = READ_ONCE(pcp->batch);
free_pcppages_bulk(zone, batch, pcp);
}
}
free_pcppages_bulk
static void free_pcppages_bulk(struct zone *zone, int count,
struct per_cpu_pages *pcp)
{
int migratetype = 0;
int batch_free = 0;
int prefetch_nr = 0;
bool isolated_pageblocks;
struct page *page, *tmp;
LIST_HEAD(head);
count = min(pcp->count, count);
// 通過循環遍歷遷移類型列表, 依次遞增刪除頁數(batch_free)
while (count) {
struct list_head *list;
do {
batch_free++;
// 循環查詢pcp->lists[migratetype]
if (++migratetype == MIGRATE_PCPTYPES)
migratetype = 0;
list = &pcp->lists[migratetype];
} while (list_empty(list));
// 只有一個遷移類型非空, 在這裏釋放全部count
if (batch_free == MIGRATE_PCPTYPES)
batch_free = count;
do {
// 從列表尾部獲得page
page = list_last_entry(list, struct page, lru);
list_del(&page->lru);
pcp->count--;
if (bulkfree_pcp_prepare(page))
continue;
// 將取出的page全部放入以head爲頭的鏈表中
list_add_tail(&page->lru, &head);
// 數據預取可以加快速度
if (prefetch_nr++ < pcp->batch)
prefetch_buddy(page);
} while (--count && --batch_free && !list_empty(list));
}
spin_lock(&zone->lock);
isolated_pageblocks = has_isolate_pageblock(zone);
list_for_each_entry_safe(page, tmp, &head, lru) {
// 獲得遷移類型
int mt = get_pcppage_migratetype(page);
// 遷移類型不能是isolated
VM_BUG_ON_PAGE(is_migrate_isolate(mt), page);
if (unlikely(isolated_pageblocks))
mt = get_pageblock_migratetype(page);
// 釋放page進入夥伴算法
__free_one_page(page, page_to_pfn(page), zone, 0, mt);
trace_mm_page_pcpu_drain(page, 0, mt);
}
spin_unlock(&zone->lock);
}
__free_pages_ok
- __free_pages_ok -> free_one_page
static void free_one_page(struct zone *zone,
struct page *page, unsigned long pfn,
unsigned int order,
int migratetype)
{
spin_lock(&zone->lock);
// 判斷zone是否存在isolate遷移類型, page是否是isolate遷移類型(一般沒有這個配置)
if (unlikely(has_isolate_pageblock(zone) ||
is_migrate_isolate(migratetype))) {
migratetype = get_pfnblock_migratetype(page, pfn);
}
__free_one_page(page, pfn, zone, order, migratetype);
spin_unlock(&zone->lock);
}
__free_one_page
static inline void __free_one_page(struct page *page,
unsigned long pfn,
struct zone *zone, unsigned int order,
int migratetype)
{
unsigned long combined_pfn;
unsigned long uninitialized_var(buddy_pfn);
struct page *buddy;
unsigned int max_order;
struct capture_control *capc = task_capc(zone);
max_order = min_t(unsigned int, MAX_ORDER, pageblock_order + 1);
VM_BUG_ON(!zone_is_initialized(zone));
VM_BUG_ON_PAGE(page->flags & PAGE_FLAGS_CHECK_AT_PREP, page);
VM_BUG_ON(migratetype == -1);
if (likely(!is_migrate_isolate(migratetype)))
// 更新zone狀態
__mod_zone_freepage_state(zone, 1 << order, migratetype);
VM_BUG_ON_PAGE(pfn & ((1 << order) - 1), page);
VM_BUG_ON_PAGE(bad_range(zone, page), page);
continue_merging:
// 循環遍歷直到order = max_order - 1
while (order < max_order - 1) {
if (compaction_capture(capc, page, order, migratetype)) {
__mod_zone_freepage_state(zone, -(1 << order),
migratetype);
return;
}
// buddy_pfn = page_pfn ^ (1 << order);
// 定位兄弟頁
buddy_pfn = __find_buddy_pfn(pfn, order);
// 獲得兄弟頁的struct page
buddy = page + (buddy_pfn - pfn);
// 判斷buddy_pfn是否有效
if (!pfn_valid_within(buddy_pfn))
goto done_merging;
// 1. buddy_ord == order
// 2. buddy_zone == zone
// 3. buddy->_refcount == 0
// 若滿足以上條件則buddy可合併
if (!page_is_buddy(page, buddy, order))
goto done_merging;
// it is CONFIG_DEBUG_PAGEALLOC guard page
if (page_is_guard(buddy))
clear_page_guard(zone, buddy, order, migratetype);
else
// 將buddy從對應free_area[order]中刪除
del_page_from_free_area(buddy, &zone->free_area[order]);
// 設置合併頁的struct page以及pfn
combined_pfn = buddy_pfn & pfn;
page = page + (combined_pfn - pfn);
pfn = combined_pfn;
order++;
}
if (max_order < MAX_ORDER) {
/* If we are here, it means order is >= pageblock_order.
* We want to prevent merge between freepages on isolate
* pageblock and normal pageblock. Without this, pageblock
* isolation could cause incorrect freepage or CMA accounting.
*
* We don't want to hit this code for the more frequent
* low-order merging.
*/
if (unlikely(has_isolate_pageblock(zone))) {
int buddy_mt;
buddy_pfn = __find_buddy_pfn(pfn, order);
buddy = page + (buddy_pfn - pfn);
buddy_mt = get_pageblock_migratetype(buddy);
if (migratetype != buddy_mt
&& (is_migrate_isolate(migratetype) ||
is_migrate_isolate(buddy_mt)))
goto done_merging;
}
max_order++;
goto continue_merging;
}
done_merging:
// 設置page的階數, 將page標記爲夥伴系統頁
set_page_order(page, order);
// 如果page並不是最大的page, 檢查夥伴頁是否是free狀態的, 如果是, 但是上述步驟合併失敗則有可能夥伴頁正在被釋放, 這時候應該把page放在zone->free_area[order]尾部(延緩page被分配出去), 這樣等夥伴頁釋放完成後就可以一起被合併成更大的page了
if ((order < MAX_ORDER-2) && pfn_valid_within(buddy_pfn)
&& !is_shuffle_order(order)) {
struct page *higher_page, *higher_buddy;
combined_pfn = buddy_pfn & pfn;
higher_page = page + (combined_pfn - pfn);
buddy_pfn = __find_buddy_pfn(combined_pfn, order + 1);
higher_buddy = higher_page + (buddy_pfn - combined_pfn);
if (pfn_valid_within(buddy_pfn) &&
page_is_buddy(higher_page, higher_buddy, order + 1)) {
// 把page置入zone->free_area[order]鏈表尾部
add_to_free_area_tail(page, &zone->free_area[order],
migratetype);
return;
}
}
if (is_shuffle_order(order))
// 獲得隨機數, 隨機決定放在頭還是尾???
add_to_free_area_random(page, &zone->free_area[order],
migratetype);
else
// 把page置入zone->free_area[order]鏈表頭部
add_to_free_area(page, &zone->free_area[order], migratetype);
}
slub算法
- slab_debug 下的object:
- kmem_cache緩衝區建立後, 所有內存空間用POISON_INUSE(0X5a)填充
- object被釋放後用POISON_FREE(0X6b)填充
- read_left_pad, red_zone用特殊字節填充, 用作magic_num
- kmem_cache_alloc概略圖
- kmem_cache_free概略圖
結構體解析
- kmem_cache結構體
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; // per cpu變量, cpu本地內存緩存池, 存儲slab
slab_flags_t flags; // object分配掩碼
unsigned long min_partial; // kmem_cache_node中的partial鏈表slab的數量上限, 超過限度多餘的slab會被釋放
unsigned int size; // 被分配的object真實大小
unsigned int object_size; // 用戶申請的obj_size
unsigned int offset; // slub將要被分配出去的obj中存儲下一個空閒obj指針(next_obj), 而存儲這個空閒obj指針的地址就用obj->offset來表示
#ifdef CONFIG_SLUB_CPU_PARTIAL
unsigned int cpu_partial; // 如果cpu_slab中存在partial鏈表, 那麼該值將作爲partial鏈表數量上限, 超過上限後全部slab將被轉移到kmem_cache_node中的partial鏈表
#endif
struct kmem_cache_order_objects oo; // 低16位代表一個slab中所有object的數量(oo & ((1 << 16) - 1)), 高16位代表一個slab管理的page數量((2^(oo 16)) pages)
struct kmem_cache_order_objects max; // max=oo, 表示最大分配量
struct kmem_cache_order_objects min; // min功能與oo, max相同, 表示最小分配量
gfp_t allocflags; // 從夥伴系統繼承的內存掩碼
int refcount; // 重用計數器, 當用戶請求創建的slub分配器大小與已經創建的slub分配器相似時, 計數+1, 進行slub重用
unsigned int inuse; // 元數據偏移量
unsigned int align; // 字節對齊大小
const char *name; // sysfs文件系統顯示使用
struct list_head list; // 掛載所有的slab
...
struct kmem_cache_node *node[MAX_NUMNODES]; // slab節點
};
- kmem_cache_cpu結構體
struct kmem_cache_cpu {
void **freelist; // 指向下一個可用空閒obj
unsigned long tid; // 相當於cpu的標識, 用於辨別cpu是否被搶佔, 用於同步
struct page *page; // 當前正在被分配的slab
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct page *partial; // 指向曾分配完所有的obj,但當前已回收至少一個對象的slab
#endif
};
- kmem_cache_node結構體
struct kmem_cache_node {
spinlock_t list_lock; // 保護node資源的自旋鎖
#ifdef CONFIG_SLUB
unsigned long nr_partial; // 本結點partial_slab數目
struct list_head partial; // partial_slab鏈表
#endif
};
kmem_cache_alloc源碼分析
- kmem_cache_alloc() -> slab_alloc() -> slab_alloc_node()
static __always_inline void *slab_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int node, unsigned long addr)
{
void *object;
struct kmem_cache_cpu *c;
struct page *page;
unsigned long tid;
// 對keme_cache做預處理
s = slab_pre_alloc_hook(s, gfpflags);
if (!s)
return NULL;
redo:
// tid, c是通過兩次讀取cpu獲得, 如果搶佔模式被開啓, 有可能兩次獲取的cpu不同, 這裏每次讀取tid和c之後都會比較tid是否等於c->tid, 如果不相等, 則說明兩次數據讀取對應的cpu不同, 則再次讀取數據, 直至相同(構造的很精巧, 比關閉搶佔提升了效率)
do {
tid = this_cpu_read(s->cpu_slab->tid);
c = raw_cpu_ptr(s->cpu_slab);
} while (IS_ENABLED(CONFIG_PREEMPT) &&
unlikely(tid != READ_ONCE(c->tid)));
// 屏障, 保證上面和下面的代碼因爲優化而相互影響
barrier();
object = c->freelist;
page = c->page;
// 如果當前cpu的空閒列表爲空或當前正在使用的頁爲空或page->node與node不匹配則進入__slab_alloc慢分配
if (unlikely(!object || !page || !node_match(page, node))) {
object = __slab_alloc(s, gfpflags, node, addr, c);
stat(s, ALLOC_SLOWPATH);
} else {
// freepointer_addr = (unsigned long)object + s->offset;
// probe_kernel_read(&p, (void **)freepointer_addr, sizeof(p));
// return freelist_ptr(s, p, freepointer_addr);
// get_freepointer_safe: 通過s->offset偏移獲得存儲下一個空閒obj的地址, 然後使用probe_kernel_read安全的將obj地址寫入p中, freelist_ptr在沒有定義CONFIG_SLAB_FREELIST_HARDENED時直接返回p
void *next_object = get_freepointer_safe(s, object);
// 判斷this_cpu(s->cpu_slab->freelist)是否等於object且this_cpu(s->cpu_slab->tid)是否等於tid, 如果成立則this_cpu(s->cpu_slab->freelist)=next_object, this_cpu(s->cpu_slab->tid)=tid+1, 否則return false
// this_cpu_cmpxchg_double將上訴操作變成原子操作
if (unlikely(!this_cpu_cmpxchg_double(
s->cpu_slab->freelist, s->cpu_slab->tid,
object, tid,
next_object, next_tid(tid)))) {
// 如果失敗則重新獲取obj
note_cmpxchg_failure("slab_alloc", s, tid);
goto redo;
}
// 預熱鏈表, 增加下次命中機率
prefetch_freepointer(s, next_object);
// 記錄狀態
stat(s, ALLOC_FASTPATH);
}
maybe_wipe_obj_freeptr(s, object);
if (unlikely(slab_want_init_on_alloc(gfpflags, s)) && object)
memset(object, 0, s->object_size);
// 分析了以下這裏kasan_slab_alloc直接返回原值, kmemleak_alloc_recursive爲空, 如果slab開始分配時memcg_kmem_enabled有意義, 這裏再做一下後續的掃尾工作(因爲是hook函數所以初始功能極少)
slab_post_alloc_hook(s, gfpflags, 1, &object);
return object;
}
__slab_alloc
- slab_alloc -> _slab_alloc
static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
unsigned long addr, struct kmem_cache_cpu *c)
{
void *freelist;
struct page *page;
// 如果c->page爲空, 代表cpu_slab中沒有可用slab, 進入new_slab向cpu_slab中填充可用slab
page = c->page;
if (!page) {
// 如果node不在線或者node沒有正常內存, 則忽略node約束
if (unlikely(node != NUMA_NO_NODE &&
!node_state(node, N_NORMAL_MEMORY)))
node = NUMA_NO_NODE;
goto new_slab;
}
redo:
// 判斷page->node與node是否相同
if (unlikely(!node_match(page, node))) {
// node_state: return 0
if (!node_state(node, N_NORMAL_MEMORY)) {
node = NUMA_NO_NODE;
goto redo;
} else {
// 記錄狀態node_miss_match
stat(s, ALLOC_NODE_MISMATCH);
// 將cpu_slab中的page放入node中
deactivate_slab(s, page, c->freelist, c);
goto new_slab;
}
}
// PF_MEMALLOC: 忽略內存管理的水印進行分配, 分配失敗則不再嘗試, 如果當前page是pfmemalloc屬性, 則調用deactivate_slab
if (unlikely(!pfmemalloc_match(page, gfpflags))) {
deactivate_slab(s, page, c->freelist, c);
goto new_slab;
}
// 檢查freelist, 防止cpu遷移或中斷導致freelist非空
freelist = c->freelist;
if (freelist)
goto load_freelist;
// 從c->page中獲得freelist
freelist = get_freelist(s, page);
if (!freelist) {
c->page = NULL;
stat(s, DEACTIVATE_BYPASS);
goto new_slab;
}
stat(s, ALLOC_REFILL);
load_freelist:
// c->page對應被分配的obj所在的page, 應該被cpu凍結
VM_BUG_ON(!c->page->frozen);
// 更新cpu_slab的freelist, tid
c->freelist = get_freepointer(s, freelist);
c->tid = next_tid(c->tid);
return freelist;
new_slab:
// 判斷cpu_slab是否存在partial_slab(部分空間被使用的page)
if (slub_percpu_partial(c)) {
// 將partial_slab作爲c->page(用來分配obj)
page = c->page = slub_percpu_partial(c);
// #define slub_set_percpu_partial(c, p) (slub_percpu_partial(c) = (p)->next;})
// 更新partial鏈表頭爲page->next
slub_set_percpu_partial(c, page);
stat(s, CPU_PARTIAL_ALLOC);
goto redo;
}
// new_slab_objects: 1. get_partial(從node->partial獲取page) 2. new_slab(夥伴算法獲取page)
// 從上述page中獲得freelist
freelist = new_slab_objects(s, gfpflags, node, &c);
if (unlikely(!freelist)) {
// 內存分配失敗
// 配置CONFIG_SLUB_DEBUG後會打印報錯信息
slab_out_of_memory(s, gfpflags, node);
return NULL;
}
page = c->page;
if (likely(!kmem_cache_debug(s) && pfmemalloc_match(page, gfpflags)))
goto load_freelist;
// kmem_cache_debug判斷kmem_cache標誌位是否包含SLAB_DEBUG_FLAGS
// alloc_debug_processing: return 0
if (kmem_cache_debug(s) &&
!alloc_debug_processing(s, page, freelist, addr))
goto new_slab;
deactivate_slab(s, page, get_freepointer(s, freelist), c);
return freelist;
}
get_freelist
static inline void *get_freelist(struct kmem_cache *s, struct page *page)
{
struct page new;
unsigned long counters;
void *freelist;
do {
freelist = page->freelist;
counters = page->counters;
// 獲得下一個freelist
new.counters = counters;
VM_BUG_ON(!new.frozen);
new.inuse = page->objects;
// The page is still frozen if the return value is not NULL.
new.frozen = freelist != NULL;
// page->freelist=NULL, page->counters=new.counters
// 將page->freelist從page中摘除, 後續會放進cpu_slab->freelist中
} while (!__cmpxchg_double_slab(s, page,
freelist, counters,
NULL, new.counters,
"get_freelist"));
return freelist;
}
kmem_cache_free源碼分析
- kmem_cache_free -> cache_from_obj(定位目標kmem_cache)
- kmem_cache_free -> 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);
}
- slab_free -> slab_free_freelist_hook
- slab_free -> do_slab_free
cache_from_obj
static inline struct kmem_cache *cache_from_obj(struct kmem_cache *s, void *x)
{
struct kmem_cache *cachep;
// 如果memcg沒有開啓且沒有配置CONFIG_SLAB_FREELIST_HARDENED,kem_cache沒有配置SLAB_CONSISTENCY_CHECKS, 則直接返回用戶選擇的kmem_cache
if (!memcg_kmem_enabled() &&
!IS_ENABLED(CONFIG_SLAB_FREELIST_HARDENED) &&
!unlikely(s->flags & SLAB_CONSISTENCY_CHECKS))
return s;
// virt_to_cache -> virt_to_head_page -> virt_to_page獲得page
// 返回page->slab_cache作爲kmem_cache(因爲用戶選擇的kmem_cache不可信)
cachep = virt_to_cache(x);
WARN_ONCE(cachep && !slab_equal_or_root(cachep, s),
"%s: Wrong slab cache. %s but object is from %s\n",
__func__, s->name, cachep->name);
return cachep;
}
slab_free_freelist_hook
static inline bool slab_free_freelist_hook(struct kmem_cache *s,
void **head, void **tail)
{
void *object;
void *next = *head;
void *old_tail = *tail ? *tail : *head;
int rsize;
*head = NULL;
*tail = NULL;
do {
// 依次遍歷freelist
object = next;
next = get_freepointer(s, object);
if (slab_want_init_on_free(s)) {
// 將object清空(red_zone區域除外)
memset(object, 0, s->object_size);
rsize = (s->flags & SLAB_RED_ZONE) ? s->red_left_pad
: 0;
memset((char *)object + s->inuse, 0,
s->size - s->inuse - rsize);
}
// slab_free_hook內部功能函數實現爲空 return false
if (!slab_free_hook(s, object)) {
// *object->offset=*head
set_freepointer(s, object, *head);
*head = object;
if (!*tail)
*tail = object;
}
} while (object != old_tail);
if (*head == *tail)
*tail = NULL;
return *head != NULL;
}
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;
struct kmem_cache_cpu *c;
unsigned long tid;
redo:
// 使用tid保持cpu同步
do {
tid = this_cpu_read(s->cpu_slab->tid);
c = raw_cpu_ptr(s->cpu_slab);
} while (IS_ENABLED(CONFIG_PREEMPT) &&
unlikely(tid != READ_ONCE(c->tid)));
// 和slab_alloc_node()中的barrier作用相同
barrier();
// 如果待釋放obj所屬的page並不是cpu_slab對應的page則進入__slab_free慢釋放
if (likely(page == c->page)) {
void **freelist = READ_ONCE(c->freelist);
// tail_obj是待插入的obj, set_freepointer: *(tail_obj->offset)=freelist(原freelist)
set_freepointer(s, tail_obj, freelist);
// 驗證cpu沒有被搶佔後, 使得s->cpu_slab->freelist=head(tail_obj), tid=next_tid(tid), tail_obj成功插入
if (unlikely(!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);
}
__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;
// uninitialized_var消除沒有初始化的警告
unsigned long uninitialized_var(flags);
stat(s, FREE_SLOWPATH);
if (kmem_cache_debug(s) &&
!free_debug_processing(s, page, head, tail, cnt, addr))
return;
do {
// n置空, 釋放free_debug_processing()設置的自旋鎖
if (unlikely(n)) {
spin_unlock_irqrestore(&n->list_lock, flags);
n = NULL;
}
prior = page->freelist;
counters = page->counters;
// tail是待插入的obj, set_freepointer: *(tail_obj->offset)=freelist(原freelist)
set_freepointer(s, tail, prior);
new.counters = counters;
was_frozen = new.frozen;
// inuse_obj = inuse_obj - cnt, 當前page釋放了cnt(1)個obj
new.inuse -= cnt;
// 如果該page(不存在正被使用的obj或者無可被使用的obj)且沒有被凍結(不屬於cpu_slab), 說明正在被釋放的obj是該page的最後一個被使用的obj, 該page可被放入buddy
if ((!new.inuse || !prior) && !was_frozen) {
// 如果kmem_cache存在cpu_slab->partial且該page無可用obj則凍結page, 後續會被放入cpu_slab->partial
if (kmem_cache_has_cpu_partial(s) && !prior) {
new.frozen = 1;
} else {
// 獲得node, 加鎖node資源區
n = get_node(s, page_to_nid(page));
spin_lock_irqsave(&n->list_lock, flags);
}
}
// 釋放head(正在被釋放的obj)進入page(page->freelist=head, page->counters=new.counters)
} while (!cmpxchg_double_slab(s, page,
prior, counters,
head, new.counters,
"__slab_free"));
if (likely(!n)) {
// 如果page沒有被凍結, 則將page掛載進入cpu_slab->partial
if (new.frozen && !was_frozen) {
put_cpu_partial(s, page, 1);
stat(s, CPU_PARTIAL_FREE);
}
// page被凍結後只更新"FREE_FROZEN"信息
if (was_frozen)
stat(s, FREE_FROZEN);
return;
}
// 如果page無obj被使用, 且kmem_cache的半滿page超過臨界點(n->nr_partial >= s->min_partial), 則進行page釋放
if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))
goto slab_empty;
// 釋放obj後slab從full變爲partial
if (!kmem_cache_has_cpu_partial(s) && unlikely(!prior)) {
// 將slab從full鏈表刪除, 插入n->partial鏈表尾部
remove_full(s, n, page);
add_partial(n, page, DEACTIVATE_TO_TAIL);
stat(s, FREE_ADD_PARTIAL);
}
// 解鎖node資源區
spin_unlock_irqrestore(&n->list_lock, flags);
return;
slab_empty:
if (prior) {
// 如果該page存在可用obj, 則該page會在partial鏈表, 所以在partial鏈表中將page刪除
remove_partial(n, page);
stat(s, FREE_REMOVE_PARTIAL);
} else {
// 將page從full鏈表中刪除
remove_full(s, n, page);
}
spin_unlock_irqrestore(&n->list_lock, flags);
stat(s, FREE_SLAB);
discard_slab(s, page);
}
discard_slab
- discard_slab->dec_slabs_node(更新node信息)
- discard_slab->free_slab->__free_slab
static void __free_slab(struct kmem_cache *s, struct page *page)
{
// 獲得page_order
int order = compound_order(page);
int pages = 1 << order;
if (s->flags & SLAB_CONSISTENCY_CHECKS) {
void *p;
// 對page做安全檢查
slab_pad_check(s, page);
// 對page中的每個obj進行安全檢測
for_each_object(p, s, page_address(page),
page->objects)
check_object(s, page, p, SLUB_RED_INACTIVE);
}
// 清除page標誌位
__ClearPageSlabPfmemalloc(page);
__ClearPageSlab(page);
// page不再被引用
page->mapping = NULL;
// 更新內存回收狀態
if (current->reclaim_state)
current->reclaim_state->reclaimed_slab += pages;
// 更新系統狀態
uncharge_slab_page(page, order, s);
// 夥伴算法釋放內存
__free_pages(page, order);
}
slab_pad_check
// 當slab_debug開啓後, kmem_cache建立時, 內存空間全部被覆寫成0x5a, 一個slab被切割成obj時有可能不能被完全利用, 可能會剩餘一些空間(padding), 又因爲padding區域在內存分配期間不會被修改, 所以應該一直是0x5a, 本函數通過對0x5a進行檢測, 試圖發現溢出覆寫錯誤
static int slab_pad_check(struct kmem_cache *s, struct page *page)
{
u8 *start;
u8 *fault;
u8 *end;
u8 *pad;
int length;
int remainder;
// 如果kmem_cache沒有配置SLAB_POISON則直接返回
if (!(s->flags & SLAB_POISON))
return 1;
start = page_address(page);
length = page_size(page);
end = start + length;
// 獲得切割obj後slab的剩餘空間
remainder = length % s->size;
if (!remainder)
return 1;
pad = end - remainder;
metadata_access_enable();
// 訪問元數據查看POISON_INUSE magic是否被修改, 定位錯誤的起始位置
fault = memchr_inv(pad, POISON_INUSE, remainder);
metadata_access_disable();
if (!fault)
return 1;
// 定位數據覆蓋的結尾
while (end > fault && end[-1] == POISON_INUSE)
end--;
// 拋出錯誤, 打印錯誤覆蓋區間
slab_err(s, page, "Padding overwritten. 0x%p-0x%p", fault, end - 1);
print_section(KERN_ERR, "Padding ", pad, remainder);
restore_bytes(s, "slab padding", POISON_INUSE, fault, end);
return 0;
}
check_object
// 講解slub算法的開頭, 列出了有關slab_debug所用到的magic_num以及obj內存佈局, 本函數對magic_num和freelist進行安全檢測
static int check_object(struct kmem_cache *s, struct page *page,
void *object, u8 val)
{
u8 *p = object;
u8 *endobject = object + s->object_size;
// 如果kmem_cache區域配置了SLAB_RED_ZONE, 則對相應的magic_num進行檢測
if (s->flags & SLAB_RED_ZONE) {
// 檢測red_left_pad
if (!check_bytes_and_report(s, page, object, "Redzone",
object - s->red_left_pad, val, s->red_left_pad))
return 0;
// 檢測Redzone
if (!check_bytes_and_report(s, page, object, "Redzone",
endobject, val, s->inuse - s->object_size))
return 0;
} else {
if ((s->flags & SLAB_POISON) && s->object_size < s->inuse) {
// 檢測padding區域
check_bytes_and_report(s, page, p, "Alignment padding",
endobject, POISON_INUSE,
s->inuse - s->object_size);
}
}
if (s->flags & SLAB_POISON) {
if (val != SLUB_RED_ACTIVE && (s->flags & __OBJECT_POISON) &&
// 檢測obj是否早已被free檢測obj[-1]是否爲POISON_END(0xa5)
(!check_bytes_and_report(s, page, p, "Poison", p,
POISON_FREE, s->object_size - 1) ||
!check_bytes_and_report(s, page, p, "Poison",
p + s->object_size - 1, POISON_END, 1)))
return 0;
check_pad_bytes(s, page, p);
}
if (!freeptr_outside_object(s) && val == SLUB_RED_ACTIVE)
/*
* Object and freepointer overlap. Cannot check
* freepointer while object is allocated.
*/
return 1;
// 檢查freelist是否有效
if (!check_valid_pointer(s, page, get_freepointer(s, p))) {
object_err(s, page, p, "Freepointer corrupt");
// 如果無效則丟棄該freelist鏈後續object
set_freepointer(s, p, NULL);
return 0;
}
return 1;
}
進程vma
-
進程由許多的segment組成, 例如text segment,data segment, bss segment等, segment中被填充各種功能的數據, 每個segment具有不同的權限(r, w, x)
-
對於進程來說segment由什麼結構來標識? 這就是接下來要將的進程vma
vm_area_struct 結構體
-
在進程中每個segment都被描述爲vm_area_struct
-
task_struct -> mm_struct -> vm_area_struct
struct vm_area_struct {
// 第一個cache line
unsigned long vm_start;
unsigned long vm_end; // vm_area_struct所對應的vma在進程地址空間中的起始和結束地址
struct vm_area_struct *vm_next, *vm_prev; // 按照vma在進程地址空間中的順序, 將vma鏈入雙鏈表,
struct rb_node vm_rb; // 紅黑樹結點
unsigned long rb_subtree_gap; // 記錄該vma與上一個vma(可以選擇雙鏈表中或者紅黑樹中)之間的空閒空間大小,
// 第二個cache line
struct mm_struct *vm_mm; // 指向該vma對應的進程的mm_struct結構體
pgprot_t vm_page_prot; // 該vma訪問權限
unsigned long vm_flags; // 描述該vma標誌位
const struct vm_operations_struct *vm_ops; // 指向function集合, 虛表
unsigned long vm_pgoff; // 以page爲單位的文件映射偏移量
struct file * vm_file; // 指向被映射的文件
...
}
find_vma(vma查找)
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
struct rb_node *rb_node;
struct vm_area_struct *vma;
// 在cache中尋找vma
vma = vmacache_find(mm, addr);
if (likely(vma))
return vma;
// 定位紅黑樹根節點
rb_node = mm->mm_rb.rb_node;
// 在紅黑樹中查找vma
while (rb_node) {
struct vm_area_struct *tmp;
// 獲得當前結點的vm_area_struct
tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);
if (tmp->vm_end > addr) {
vma = tmp;
if (tmp->vm_start <= addr)
break;
rb_node = rb_node->rb_left;
} else
rb_node = rb_node->rb_right;
}
// 如果查找到的vma有效, 則更新cache
if (vma)
vmacache_update(addr, vma);
return vma;
}
vmacache_find
struct vm_area_struct *vmacache_find(struct mm_struct *mm, unsigned long addr)
{
// 通過左移addr, 定位addr對應的index(這個位置可能會存在對應的vma)
int idx = VMACACHE_HASH(addr);
int i;
// 記錄事件
count_vm_vmacache_event(VMACACHE_FIND_CALLS);
// 檢測mm是否是當前進程的mm_struct, 如果是第一次觸發cache, 將進行初始化
if (!vmacache_valid(mm))
return NULL;
// 遍歷current->vmacache.vmas[](從idx開始, 因爲inx對應的位置cache hit可能性最大)
for (i = 0; i < VMACACHE_SIZE; i++) {
struct vm_area_struct *vma = current->vmacache.vmas[idx];
if (vma) {
#ifdef CONFIG_DEBUG_VM_VMACACHE
if (WARN_ON_ONCE(vma->vm_mm != mm))
break;
#endif
// 判斷vma是否匹配
if (vma->vm_start <= addr && vma->vm_end > addr) {
count_vm_vmacache_event(VMACACHE_FIND_HITS);
return vma;
}
}
// inx遍歷到VMACACHE_SIZE後, 歸0繼續遍歷(idx是從中間開始遍歷的)
if (++idx == VMACACHE_SIZE)
idx = 0;
}
return NULL;
}
insert_vm_struct(vma插入)
int insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vma)
{
struct vm_area_struct *prev;
struct rb_node **rb_link, *rb_parent;
// 定位插入vma的目標位置
// prev = rb_parent對應的vm_area_struct
// rb_link = 待插入的位置
// rb_parent = rb_link的父結點
if (find_vma_links(mm, vma->vm_start, vma->vm_end,
&prev, &rb_link, &rb_parent))
return -ENOMEM;
if ((vma->vm_flags & VM_ACCOUNT) &&
security_vm_enough_memory_mm(mm, vma_pages(vma)))
return -ENOMEM;
// 匿名vma要設置頁偏移
if (vma_is_anonymous(vma)) {
BUG_ON(vma->anon_vma);
vma->vm_pgoff = vma->vm_start >> PAGE_SHIFT;
}
// 將vma插入vma管理體系
// vma_link -> __vma_link -> __vma_link_list, 將vma插入雙鏈表
// vma_link -> __vma_link -> __vma_link_rb, 將vma插入紅黑樹
vma_link(mm, vma, prev, rb_link, rb_parent);
return 0;
}
find_vma_links
static int find_vma_links(struct mm_struct *mm, unsigned long addr,
unsigned long end, struct vm_area_struct **pprev,
struct rb_node ***rb_link, struct rb_node **rb_parent)
{
struct rb_node **__rb_link, *__rb_parent, *rb_prev;
__rb_link = &mm->mm_rb.rb_node;
rb_prev = __rb_parent = NULL;
while (*__rb_link) {
struct vm_area_struct *vma_tmp;
__rb_parent = *__rb_link;
vma_tmp = rb_entry(__rb_parent, struct vm_area_struct, vm_rb);
if (vma_tmp->vm_end > addr) {
// 如果vma_tmp_end_addr大於vma_end_addr且vma_start_end_addr小於vma_start_addr, 說明vma_tmp和vma重合, 函數報錯
if (vma_tmp->vm_start < end)
return -ENOMEM;
// 繼續遍歷左節點
__rb_link = &__rb_parent->rb_left;
} else {
rb_prev = __rb_parent;
// 繼續遍歷右節點
__rb_link = &__rb_parent->rb_right;
}
}
// 當__rb_link爲空, 即對應結點爲空時, 退出遍歷紅黑樹循環
// __rb_link對應空結點, 即目標插入位置
// __rb_parent對應__rb_link的父結點
// pprev對應rb_prev指向的vm_arena_struct
*pprev = NULL;
if (rb_prev)
*pprev = rb_entry(rb_prev, struct vm_area_struct, vm_rb);
*rb_link = __rb_link;
*rb_parent = __rb_parent;
return 0;
}
紅黑樹規則
-
紅黑樹規則:
- 節點是紅色或黑色
- 根節點是黑色
- 每個葉節點都是黑色的空節點
- 每個紅色節點的兩個子節點都是黑色(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
- 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點
-
插入節點時通過變色或者旋轉維持紅黑樹規則
缺頁中斷
/*
* Page fault error code bits:
*
* bit 0 == 0: no page found 1: protection fault
* bit 1 == 0: read access 1: write access
* bit 2 == 0: kernel-mode access 1: user-mode access
* bit 3 == 1: use of reserved bit detected
* bit 4 == 1: fault was an instruction fetch
* bit 5 == 1: protection keys block access
*/
enum x86_pf_error_code {
X86_PF_PROT = 1 << 0,
X86_PF_WRITE = 1 << 1,
X86_PF_USER = 1 << 2,
X86_PF_RSVD = 1 << 3,
X86_PF_INSTR = 1 << 4,
X86_PF_PK = 1 << 5,
};
__do_page_fault源碼分析
static noinline void
__do_page_fault(struct pt_regs *regs, unsigned long hw_error_code,
unsigned long address)
{
// 將變量放入cache, 加快速度
prefetchw(¤t->mm->mmap_sem);
// mmiotrace跟蹤器, 用於跟蹤基於內存映射的io設備
if (unlikely(kmmio_fault(regs, address)))
return;
// 判斷缺頁中斷髮生在內核態還是用戶態
if (unlikely(fault_in_kernel_space(address)))
do_kern_addr_fault(regs, hw_error_code, address);
else
do_user_addr_fault(regs, hw_error_code, address);
}
fault_in_kernel_space
- vsyscall和vdso的作用與區別
- 作用: 一般來說, 用戶態與內核態通過系統調用進行交互, 但是這種交互非常浪費時間, 那麼對於需要實時性的api如gettimeofday等就不太適用, 使用vsyscall或vdso可以加速
- 區別:
- vsyscall是一種比較古老的機制, 他在固定地址映射內核內存頁實現快速執行內核功能, 但安全性不高, 被vdso逐漸替代, 但是vdso只存在與動態鏈接, 靜態鏈接程序沒有vdso, 所以爲了兼容性, vsyscall被保留下來
- vdso可以應用aslr實現地址隨機化, 而且無需考慮cpu差異性
static int fault_in_kernel_space(unsigned long address)
{
// x86_64架構下vsyscall在TASK_SIZE_MAX之上, 但並非內核空間, 所以單獨列出
// #define VSYSCALL_ADDR (-10UL << 20), VSYSCALL_ADDR即爲vsyscall固定地址(在x86_64架構下(-10)用8字節存儲, VSYSCALL_ADDR=0xffffffffff600000)
if (IS_ENABLED(CONFIG_X86_64) && is_vsyscall_vaddr(address))
return false;
// 將大於TASK_SIZE_MAX的地址視爲內核空間
return address >= TASK_SIZE_MAX;
}
do_kern_addr_fault
static void
do_kern_addr_fault(struct pt_regs *regs, unsigned long hw_error_code,
unsigned long address)
{
// X86_PF_PK存在於用戶頁, 並非內核頁
WARN_ON_ONCE(hw_error_code & X86_PF_PK);
// 檢測錯誤是否由於vmalloc fault導致
if (!(hw_error_code & (X86_PF_RSVD | X86_PF_USER | X86_PF_PROT))) {
if (vmalloc_fault(address) >= 0)
return;
}
// 檢測錯誤是否是由於TLB表項陳舊(內核頁權限變更後, TLB沒有更新)
// 如果TLB表項陳舊導致錯誤, 那麼只有兩種可能. 1. 數據寫入時TLB(), 2. 指令執行時TLB(NX)
if (spurious_kernel_fault(hw_error_code, address))
return;
// 判斷kprobe是否hook了缺頁錯誤
if (kprobe_page_fault(regs, X86_TRAP_PF))
return;
// 非法地址訪問導致頁錯誤
bad_area_nosemaphore(regs, hw_error_code, address);
}
vmalloc_fault
static noinline int vmalloc_fault(unsigned long address)
{
pgd_t *pgd, *pgd_k;
p4d_t *p4d, *p4d_k;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
// #define high_memory (-128UL << 20)
// #define VMALLOC_START (((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1))
// VMALLOC空間和前面保留8M的hole保證安全性
// 經計算VMALLOC_START=0xfffffffff8000000+8M
if (!(address >= VMALLOC_START && address < VMALLOC_END))
return -1;
// 使用vmalloc分配內存後, 內存映射會被直接寫入全局內核頁表init_mm
// 進程的內核頁表是在進程被創建時, 直接複製內核頁表獲得, 不具備實時性, 所以只有當發生vmalloc缺頁中斷時, 纔會把對應的全局內核頁表項複製到進程內核頁表
// 獲得進程內核頁表
pgd = (pgd_t *)__va(read_cr3_pa()) + pgd_index(address);
// 獲得全局內核頁表
pgd_k = pgd_offset_k(address);
// 將與addr關聯的全局內核頁表項複製到進程內核頁表
if (pgd_none(*pgd_k))
return -1;
if (pgtable_l5_enabled()) {
if (pgd_none(*pgd)) {
set_pgd(pgd, *pgd_k);
arch_flush_lazy_mmu_mode();
} else {
BUG_ON(pgd_page_vaddr(*pgd) != pgd_page_vaddr(*pgd_k));
}
}
// 獲得進程內核頁表和全局內核頁表對應的p4d, 令p4d_proc=p4d_kern實現頁表項複製
p4d = p4d_offset(pgd, address);
p4d_k = p4d_offset(pgd_k, address);
if (p4d_none(*p4d_k))
return -1;
if (p4d_none(*p4d) && !pgtable_l5_enabled()) {
set_p4d(p4d, *p4d_k);
arch_flush_lazy_mmu_mode();
} else {
BUG_ON(p4d_pfn(*p4d) != p4d_pfn(*p4d_k));
}
BUILD_BUG_ON(CONFIG_PGTABLE_LEVELS < 4);
// 對pud, pmd, pte做安全檢測
pud = pud_offset(p4d, address);
if (pud_none(*pud))
return -1;
if (pud_large(*pud))
return 0;
pmd = pmd_offset(pud, address);
if (pmd_none(*pmd))
return -1;
if (pmd_large(*pmd))
return 0;
pte = pte_offset_kernel(pmd, address);
// 如果pte對應的page並非正在被使用則缺頁
if (!pte_present(*pte))
return -1;
return 0;
}
spurious_kernel_fault
static noinline int
spurious_kernel_fault(unsigned long error_code, unsigned long address)
{
pgd_t *pgd;
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
int ret;
// 由於TLB表項陳舊導致的虛假錯誤, 給出的錯誤原因只有兩種:寫入(X86_PF_WRITE), 指令執行(X86_PF_INSTR)
if (error_code != (X86_PF_WRITE | X86_PF_PROT) &&
error_code != (X86_PF_INSTR | X86_PF_PROT))
return 0;
// 定位內核頁表
pgd = init_mm.pgd + pgd_index(address);
// 判斷pgd是否在內存中
if (!pgd_present(*pgd))
return 0;
// 通過偏移獲得p4d表項
p4d = p4d_offset(pgd, address);
if (!p4d_present(*p4d))
return 0;
// 如果在p4d表項處開啓huge page機制, 則直接進入spurious_kernel_fault_check
/* spurious_kernel_fault_check:
if ((error_code & X86_PF_WRITE) && !pte_write(*pte))
return 0;
if ((error_code & X86_PF_INSTR) && !pte_exec(*pte))
return 0;
return 1;
*/
// 檢測是否是因爲tlb項老舊導致錯誤
if (p4d_large(*p4d))
return spurious_kernel_fault_check(error_code, (pte_t *) p4d);
// 以下處理流程相同, 不再描述
pud = pud_offset(p4d, address);
if (!pud_present(*pud))
return 0;
if (pud_large(*pud))
return spurious_kernel_fault_check(error_code, (pte_t *) pud);
pmd = pmd_offset(pud, address);
if (!pmd_present(*pmd))
return 0;
if (pmd_large(*pmd))
return spurious_kernel_fault_check(error_code, (pte_t *) pmd);
pte = pte_offset_kernel(pmd, address);
if (!pte_present(*pte))
return 0;
ret = spurious_kernel_fault_check(error_code, pte);
if (!ret)
return 0;
ret = spurious_kernel_fault_check(error_code, (pte_t *) pmd);
// 如果在pte階段還是沒有檢測到虛假錯誤則報一個bug
WARN_ONCE(!ret, "PMD has incorrect permission bits\n");
return ret;
}
bad_area_nosemaphore
- bad_area_nosemaphore -> __bad_area_nosemaphore
static void
__bad_area_nosemaphore(struct pt_regs *regs, unsigned long error_code,
unsigned long address, u32 pkey, int si_code)
{
struct task_struct *tsk = current;
// 如果請求來自用戶態, 說明想要越界訪問內核空間
if (user_mode(regs) && (error_code & X86_PF_USER)) {
local_irq_enable();
/*
* Valid to do another page fault here because this one came
* from user space:
*/
// 不忽略nx頁上的取指錯誤
if (is_prefetch(regs, error_code, address))
return;
if (is_errata100(regs, address))
return;
// 爲了防止內核頁表佈局被泄露, 這裏把用戶態訪問內核空間錯誤僞造成protection faults
if (address >= TASK_SIZE_MAX)
error_code |= X86_PF_PROT;
if (likely(show_unhandled_signals))
// 打印錯誤信息
show_signal_msg(regs, error_code, address, tsk);
set_signal_archinfo(address, error_code);
if (si_code == SEGV_PKUERR)
force_sig_pkuerr((void __user *)address, pkey);
// 發送SIGSEGV信號
force_sig_fault(SIGSEGV, si_code, (void __user *)address);
return;
}
if (is_f00f_bug(regs, address))
return;
// 在內核中發生內核缺頁
no_context(regs, error_code, address, SIGSEGV, si_code);
}
no_context
static noinline void
no_context(struct pt_regs *regs, unsigned long error_code,
unsigned long address, int signal, int si_code)
{
struct task_struct *tsk = current;
unsigned long flags;
int sig;
if (user_mode(regs)) {
// 用戶模式訪問內核態, 直接oops
goto oops;
}
// 搜索異常表, 試圖找到一個對應該異常的例程來進行修正
if (fixup_exception(regs, X86_TRAP_PF, error_code, address)) {
// 任何中斷錯誤都會在fixup_exception中獲得處理, 下面的錯誤處理流程只用於任務上下文中的錯誤
if (in_interrupt())
return;
if (current->thread.sig_on_uaccess_err && signal) {
set_signal_archinfo(address, error_code);
force_sig_fault(signal, si_code, (void __user *)address);
}
return;
}
/*
* 32-bit:
*
* Valid to do another page fault here, because if this fault
* had been triggered by is_prefetch fixup_exception would have
* handled it.
*
* 64-bit:
*
* Hall of CPU/BIOS bugs.
*/
if (is_prefetch(regs, error_code, address))
return;
if (is_errata93(regs, address))
return;
// 固件訪問錯誤恢復
if (IS_ENABLED(CONFIG_EFI))
efi_recover_from_page_fault(address);
oops:
// 確定時內核缺陷, 使用oops打印錯誤...
flags = oops_begin();
show_fault_oops(regs, error_code, address);
if (task_stack_end_corrupted(tsk))
printk(KERN_EMERG "Thread overran stack, or stack corrupted\n");
sig = SIGKILL;
if (__die("Oops", regs, error_code))
sig = 0;
/* Executive summary in case the body of the oops scrolled away */
printk(KERN_DEFAULT "CR2: %016lx\n", address);
oops_end(flags, regs, sig);
}
do_user_addr_fault
static inline
void do_user_addr_fault(struct pt_regs *regs,
unsigned long hw_error_code,
unsigned long address)
{
struct vm_area_struct *vma;
struct task_struct *tsk;
struct mm_struct *mm;
vm_fault_t fault, major = 0;
unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;
tsk = current;
mm = tsk->mm;
// 判斷kprobe是否hook了缺頁錯誤
if (unlikely(kprobe_page_fault(regs, X86_TRAP_PF)))
return;
// Reserved bits不會被設置在用戶的頁表項, 如果存在Reserved bits則發生頁表錯誤
if (unlikely(hw_error_code & X86_PF_RSVD))
pgtable_bad(regs, hw_error_code, address);
// 如果開啓smap且kernel(supervisor)訪問用戶態地址(X86_PF_USER=0)則進入bad_area_nosemaphore
if (unlikely(cpu_feature_enabled(X86_FEATURE_SMAP) &&
!(hw_error_code & X86_PF_USER) &&
!(regs->flags & X86_EFLAGS_AC)))
{
bad_area_nosemaphore(regs, hw_error_code, address);
return;
}
/*
* If we're in an interrupt, have no user context or are running
* in a region with pagefaults disabled then we must not take the fault
*/
if (unlikely(faulthandler_disabled() || !mm)) {
bad_area_nosemaphore(regs, hw_error_code, address);
return;
}
// 因爲到達這一步時cr2中的虛擬地址已經被另存且vmalloc_fault被處理所以開啓中斷是安全可行的
if (user_mode(regs)) {
local_irq_enable();
flags |= FAULT_FLAG_USER;
} else {
if (regs->flags & X86_EFLAGS_IF)
local_irq_enable();
}
// 記錄事件
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, address);
// 更新flags標誌位(後面引用)
if (hw_error_code & X86_PF_WRITE)
flags |= FAULT_FLAG_WRITE;
if (hw_error_code & X86_PF_INSTR)
flags |= FAULT_FLAG_INSTRUCTION;
#ifdef CONFIG_X86_64
// vsyscall沒有vma, 所以在find_vma之前對vsyscall做模擬
if (is_vsyscall_vaddr(address)) {
if (emulate_vsyscall(hw_error_code, regs, address))
return;
}
#endif
// 1. 內核只能訪問用戶地址空間的在exception_tables上定義的指令, 如果直接進行這樣的嘗試(但指令卻在exception_tables之外)則會因爲持有了mmap_sem鎖, 而讓系統死鎖
// 2. 所以, 只有當獲得mmap_sem鎖失敗後, 才能嘗試使用1.
if (unlikely(!down_read_trylock(&mm->mmap_sem))) {
if (!user_mode(regs) && !search_exception_tables(regs->ip)) {
bad_area_nosemaphore(regs, hw_error_code, address);
return;
}
retry:
down_read(&mm->mmap_sem);
} else {
// 如果獲得mmap_sem鎖成功則會錯過down_read內的might_sleep, 這裏補一個might_sleep
might_sleep();
}
// 判斷vma是否合法
vma = find_vma(mm, address);
if (unlikely(!vma)) {
bad_area(regs, hw_error_code, address);
return;
}
// 做簡單的安全檢測
if (likely(vma->vm_start <= address))
goto good_area;
// 判斷vma是否向下增加
if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
bad_area(regs, hw_error_code, address);
return;
}
// 如果address在棧空間, 則根據address和vma->start的關係決定是否擴充棧
// 如果address < vma->start, 則另vma->start=address向下擴充stack
if (unlikely(expand_stack(vma, address))) {
bad_area(regs, hw_error_code, address);
return;
}
good_area:
// 判斷是否因爲頁操作與vma權限不符(保留X86_PF_PK錯誤, 因爲這是留給寫時複製的, 並非權限錯誤)
if (unlikely(access_error(hw_error_code, vma))) {
bad_area_access_error(regs, hw_error_code, address, vma);
return;
}
// 缺頁處理
fault = handle_mm_fault(vma, address, flags);
...
handle_mm_fault
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
struct vm_fault vmf = {
.vma = vma,
.address = address & PAGE_MASK,
.flags = flags,
.pgoff = linear_page_index(vma, address),
.gfp_mask = __get_fault_gfp_mask(vma),
};
unsigned int dirty = flags & FAULT_FLAG_WRITE;
// 以vma->vm_mm爲根結點遍歷頁表, 定位到pmd
struct mm_struct *mm = vma->vm_mm;
pgd_t *pgd;
p4d_t *p4d;
vm_fault_t ret;
// 定位pgd
pgd = pgd_offset(mm, address);
// 如果沒開5級頁表直接返回pgd
p4d = p4d_alloc(mm, pgd, address);
if (!p4d)
return VM_FAULT_OOM;
// 定位pud
vmf.pud = pud_alloc(mm, p4d, address);
if (!vmf.pud)
return VM_FAULT_OOM;
// 中間表項爲空, 且開啓huge_page, 設置
// 如果pud爲空, 且vma可以創建透明的huge_page, 則create_huge_pud觸發huge_page錯誤(匿名頁不支持)
if (pud_none(*vmf.pud) && __transparent_hugepage_enabled(vma)) {
ret = create_huge_pud(&vmf);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
pud_t orig_pud = *vmf.pud;
barrier();
// pud具有_PAGE_PSE標誌位, 且pud爲devmap
if (pud_trans_huge(orig_pud) || pud_devmap(orig_pud)) {
// pud將要被更新爲髒頁
if (dirty && !pud_write(orig_pud)) {
// 觸發huge_page錯誤(匿名頁不支持)
ret = wp_huge_pud(&vmf, orig_pud);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
huge_pud_set_accessed(&vmf, orig_pud);
return 0;
}
}
}
vmf.pmd = pmd_alloc(mm, vmf.pud, address);
if (!vmf.pmd)
return VM_FAULT_OOM;
// 如果pmd爲空, 且vma可以創建透明的huge_page, 則create_huge_pmd創建大頁
if (pmd_none(*vmf.pmd) && __transparent_hugepage_enabled(vma)) {
ret = create_huge_pmd(&vmf);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
pmd_t orig_pmd = *vmf.pmd;
barrier();
// 判斷pmd是否在swap分區(不在內存中)
if (unlikely(is_swap_pmd(orig_pmd))) {
// 如果支持遷移但並非遷移pmd入口, 則上報bug
VM_BUG_ON(thp_migration_supported() &&
!is_pmd_migration_entry(orig_pmd));
if (is_pmd_migration_entry(orig_pmd))
pmd_migration_entry_wait(mm, vmf.pmd);
return 0;
}
// pud具有_PAGE_PSE標誌位, 且pud爲devmap
if (pmd_trans_huge(orig_pmd) || pmd_devmap(orig_pmd)) {
if (pmd_protnone(orig_pmd) && vma_is_accessible(vma))
return do_huge_pmd_numa_page(&vmf, orig_pmd);
if (dirty && !pmd_write(orig_pmd)) {
ret = wp_huge_pmd(&vmf, orig_pmd);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
huge_pmd_set_accessed(&vmf, orig_pmd);
return 0;
}
}
}
// vmf被填充, 下一步根據vmf分配物理頁
return handle_pte_fault(&vmf);
}
handle_pte_fault
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
pte_t entry;
// 若pmd不存在, 則pte不存在
if (unlikely(pmd_none(*vmf->pmd))) {
vmf->pte = NULL;
} else {
// pmd_devmap_trans_unstable{return pmd_devmap(*pmd) || pmd_trans_unstable(pmd);}
// pmd_devmap檢測pmd是否爲_PAGE_DEVMAP, 如果是則直接返回1
//
// pmd_trans_unstable->pmd_none_or_trans_huge_or_clear_bad{...}
// 檢測pmd是否爲空, 或者是否可以轉換爲huge_page, 否則進入pmd_clear_bad
// 提前檢測_PAGE_DEVMAP, 可以避免後面devmap頁進入pmd_none_or_trans_huge_or_clear_bad後陷入pmd_clear_bad, 而濫用dmesg打印錯誤
// pmd_devmap_trans_unstable=pmd_devmap+pmd_trans_unstable 可這個命名太抽象 =.=
if (pmd_devmap_trans_unstable(vmf->pmd))
return 0;
// 此時pmd存在且不可能變成huge_pmd, 使用pte_offset_map是安全的
vmf->pte = pte_offset_map(vmf->pmd, vmf->address);
vmf->orig_pte = *vmf->pte;
barrier();
if (pte_none(vmf->orig_pte)) {
pte_unmap(vmf->pte);
vmf->pte = NULL;
}
}
// pte爲空, 分配頁表
if (!vmf->pte) {
if (vma_is_anonymous(vmf->vma))
// 處理匿名頁
return do_anonymous_page(vmf);
else
// 處理文件映射頁
return do_fault(vmf);
}
// ----------- 物理頁存在 -----------
// 頁表已建立, 但不存在與內存, 做頁交換
if (!pte_present(vmf->orig_pte))
return do_swap_page(vmf);
if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
// 維持node平衡, 進行頁遷移
return do_numa_page(vmf);
vmf->ptl = pte_lockptr(vmf->vma->vm_mm, vmf->pmd);
spin_lock(vmf->ptl);
entry = vmf->orig_pte;
// 鎖定資源區後, 檢測pte是否發生變化, 如果發生, 直接解鎖資源區 return
if (unlikely(!pte_same(*vmf->pte, entry)))
goto unlock;
// 因寫入而觸發中斷
if (vmf->flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry))
// 寫時複製缺頁中斷
return do_wp_page(vmf);
// 標記髒頁
entry = pte_mkdirty(entry);
}
entry = pte_mkyoung(entry);
// 如果pte內容沒有變化進入else
if (ptep_set_access_flags(vmf->vma, vmf->address, vmf->pte, entry,
vmf->flags & FAULT_FLAG_WRITE)) {
// pte內容更改, 刷新mmu
update_mmu_cache(vmf->vma, vmf->address, vmf->pte);
} else {
// 如果pte沒有發生變化, 且是寫中斷錯誤, 這裏可能對應寫時複製, 所以更新tlb
if (vmf->flags & FAULT_FLAG_WRITE)
flush_tlb_fix_spurious_fault(vmf->vma, vmf->address);
}
unlock:
pte_unmap_unlock(vmf->pte, vmf->ptl);
return 0;
}
ptmalloc
ptmalloc是linux 用戶態堆管理機制, 也是本文源碼分析的最後一部分, 鏈接如下:
雜記
- 山高路遠不畏險
- 還有很多需要寫的東西, 留給下一篇文章
本文由 Seebug Paper 發佈,如需轉載請註明來源。本文地址:https://paper.seebug.org/1458/