91欧美超碰AV自拍|国产成年人性爱视频免费看|亚洲 日韩 欧美一厂二区入|人人看人人爽人人操aV|丝袜美腿视频一区二区在线看|人人操人人爽人人爱|婷婷五月天超碰|97色色欧美亚州A√|另类A√无码精品一级av|欧美特级日韩特级

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Linux內(nèi)核虛擬內(nèi)存管理中的mmu_gather操作

xCb1_yikoulinux ? 來(lái)源:Linux內(nèi)核遠(yuǎn)航者 ? 作者:Linux內(nèi)核遠(yuǎn)航者 ? 2022-05-20 14:37 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

1開場(chǎng)白

環(huán)境:

  • 處理器架構(gòu):arm64

  • 內(nèi)核源碼:linux-5.10.50

  • ubuntu版本:20.04.1

  • 代碼閱讀工具:vim+ctags+cscope

本文講解Linux內(nèi)核虛擬內(nèi)存管理中的mmu_gather操作,看看它是如何保證刷tlb和釋放物理頁(yè)的順序的,又是如何將更多的頁(yè)面聚集起來(lái)統(tǒng)一釋放的。

通常在進(jìn)程退出或者執(zhí)行munmap的時(shí)候,內(nèi)核會(huì)解除相關(guān)虛擬內(nèi)存區(qū)域的頁(yè)表映射,刷/無(wú)效tlb,并釋放/回收相關(guān)的物理頁(yè)面,這一過(guò)程的正確順序如下:

1)解除頁(yè)表映射

2)刷相關(guān)tlb

3)釋放物理頁(yè)面

在刷相關(guān)虛擬內(nèi)存區(qū)域tlb之前,絕對(duì)不能先釋放物理頁(yè)面,否則可能導(dǎo)致不正確的結(jié)果,而mmu-gather(mmu 積聚)的作用就是保證這種順序,并將需要釋放的相關(guān)的物理頁(yè)面聚集起來(lái)統(tǒng)一釋放。

2.源代碼解讀

2.1 重要數(shù)據(jù)結(jié)構(gòu)體

首先我們先介紹一下,與mmu-gather相關(guān)的一些重要結(jié)構(gòu)體,對(duì)于理解源碼很有幫助。

相關(guān)的主要數(shù)據(jù)結(jié)構(gòu)有三個(gè):

struct mmu_gather

struct mmu_table_batch

struct mmu_gather_batch

1)mmu_gather

來(lái)表示一次mmu積聚操作,在每次解除相關(guān)虛擬內(nèi)存區(qū)域時(shí)使用。

structmmu_gather{
structmm_struct*mm;

#ifdefCONFIG_MMU_GATHER_TABLE_FREE
structmmu_table_batch*batch;
#endif

unsignedlongstart;
unsignedlongend;
/*
|*weareinthemiddleofanoperationtoclear
|*afullmmandcanmakesomeoptimizations
|*/
unsignedintfullmm:1;

/*
|*wehaveperformedanoperationwhich
|*requiresacompleteflushofthetlb
|*/
unsignedintneed_flush_all:1;

/*
|*wehaveremovedpagedirectories
|*/
unsignedintfreed_tables:1;

/*
|*atwhichlevelshaveweclearedentries?
|*/
unsignedintcleared_ptes:1;
unsignedintcleared_pmds:1;
unsignedintcleared_puds:1;
unsignedintcleared_p4ds:1;

/*
|*tracksVM_EXEC|VM_HUGETLBintlb_start_vma
|*/
unsignedintvma_exec:1;
unsignedintvma_huge:1;

unsignedintbatch_count;
#ifndefCONFIG_MMU_GATHER_NO_GATHER
structmmu_gather_batch*active;
structmmu_gather_batchlocal;
 struct page *__pages[MMU_GATHER_BUNDLE];
...
#endif

};


其中,mm 表示操作哪個(gè)進(jìn)程的虛擬內(nèi)存;batch 用于積聚進(jìn)程各級(jí)頁(yè)目錄的物理頁(yè);start和end 表示操作的起始和結(jié)束虛擬地址,這兩個(gè)地址在處理過(guò)程中會(huì)被相應(yīng)的賦值;fullmm 表示是否操作整個(gè)用戶地址空間;freed_tables 表示我們已經(jīng)釋放了相關(guān)的頁(yè)目錄;cleared_ptes/pmds/puds/p4ds 表示我們?cè)谀膫€(gè)級(jí)別上清除了表項(xiàng);vma_exec 表示操作的是否為可執(zhí)行的vma;vma_huge 表示操作的是否為hugetlb的vma;batch_count 表示積聚了多少個(gè)“批次”,后面會(huì)講到 ;active、local和__pages 和多批次釋放物理頁(yè)面相關(guān);active表示當(dāng)前處理的批次,local表示“本地”批次,__pages表示“本地”批次積聚的物理頁(yè)面。

這里需要說(shuō)明一點(diǎn)就是,mmu積聚操作會(huì)涉及到local批次和多批次操作,local批次操作的物理頁(yè)面相關(guān)的struct page數(shù)組內(nèi)嵌到mmu_gather結(jié)構(gòu)的__pages中,且我們發(fā)現(xiàn)這個(gè)數(shù)組大小為8,也就是local批次最大積聚8 * 4k = 32k的內(nèi)存大小,這因?yàn)閙mu_gather結(jié)構(gòu)通常在內(nèi)核棧中分配,不能占用太多的內(nèi)核??臻g,而多批次由于動(dòng)態(tài)分配批次積聚結(jié)構(gòu)所以每個(gè)批次能積聚更多的頁(yè)面。

2)mmu_table_batch

用于積聚進(jìn)程使用的各級(jí)頁(yè)目錄的物理頁(yè),在釋放進(jìn)程相關(guān)的頁(yè)目錄的物理頁(yè)時(shí)使用(文章中稱為頁(yè)表批次的積聚結(jié)構(gòu))。

structmmu_table_batch{
#ifdefCONFIG_MMU_GATHER_RCU_TABLE_FREE
structrcu_headrcu;
#endif
unsignedintnr;
void*tables[0];
};

rcu 用于rcu延遲釋放頁(yè)目錄的物理頁(yè);

nr 表示頁(yè)目錄的物理頁(yè)的積聚結(jié)構(gòu)的page數(shù)組中頁(yè)面?zhèn)€數(shù);

tables 表示頁(yè)表積聚結(jié)構(gòu)的page數(shù)組。

3)mmu_gather_batch

表示物理頁(yè)的積聚批次,用于積聚進(jìn)程映射到用戶空間物理頁(yè)(文章中稱為批次的積聚結(jié)構(gòu))。

structmmu_gather_batch{
structmmu_gather_batch*next;
unsignedintnr;
unsignedintmax;
structpage*pages[0];
};

next 用于多批次積聚物理頁(yè)時(shí),連接下一個(gè)積聚批次結(jié)構(gòu) ;

nr 表示本次批次的積聚數(shù)組的頁(yè)面?zhèn)€數(shù);

max 表示本次批次的積聚數(shù)組最大的頁(yè)面?zhèn)€數(shù);

pages 表示本次批次積聚結(jié)構(gòu)的page數(shù)組。

2.2 總體調(diào)用

通常mmu-gather操作由一下幾部分函數(shù)組成:

tlb_gather_mmu

unmap_vmas

free_pgtables

tlb_finish_mmu

其中tlb_gather_mmu表示mmu-gather初始化,也就是struct mmu_gather的初始化;

unmap_vmas 表示解除相關(guān)虛擬內(nèi)存區(qū)域的頁(yè)表映射;

free_pgtables 表示釋放頁(yè)表操作 ;

tlb_finish_mmu 表示進(jìn)行刷tlb和釋放物理頁(yè)操作。

2.3 tlb_gather_mmu

這個(gè)函數(shù)主要是初始化從進(jìn)程內(nèi)核棧中傳遞過(guò)來(lái)的mmu_gather結(jié)構(gòu)。

voidtlb_gather_mmu(structmmu_gather*tlb,structmm_struct*mm,
unsignedlongstart,unsignedlongend)
{
tlb->mm=mm;//賦值進(jìn)程的內(nèi)存描述符

/*Isitfrom0to~0?*/
tlb->fullmm=!(start|(end+1));//如果是操作進(jìn)程整個(gè)地址空間,則start=0,end=-1,這個(gè)時(shí)候fullmm會(huì)被賦值1

#ifndefCONFIG_MMU_GATHER_NO_GATHER
tlb->need_flush_all=0;//初始化“本地”積聚相關(guān)成員
tlb->local.next=NULL;
tlb->local.nr=0;
tlb->local.max=ARRAY_SIZE(tlb->__pages);
tlb->active=&tlb->local;//active指向本地的積聚結(jié)構(gòu)
tlb->batch_count=0;
#endif

tlb_table_init(tlb);//tlb->batch=NULL,來(lái)表示先不使用多批次積聚
#ifdefCONFIG_MMU_GATHER_PAGE_SIZE
tlb->page_size=0;
#endif

__tlb_reset_range(tlb);//tlb->start/end和lb->freed_tables、tlb->cleared_xxx初始化
inc_tlb_flush_pending(tlb->mm);
}

下面給出tlb_gather_mmu時(shí)的圖解:

b85d28f6-d7f0-11ec-ba43-dac502259ad0.png

2.4 unmap_vmas

這個(gè)函數(shù)用于解除相關(guān)進(jìn)程虛擬內(nèi)存區(qū)域的頁(yè)表映射,還會(huì)將相關(guān)的物理頁(yè)面放入積聚結(jié)構(gòu)中,后面統(tǒng)一釋放。

下面我們來(lái)看下這個(gè)函數(shù):

voidunmap_vmas(structmmu_gather*tlb,
structvm_area_struct*vma,unsignedlongstart_addr,
unsignedlongend_addr)
{
...
for(;vma&&vma->vm_startvm_next)
unmap_single_vma(tlb,vma,start_addr,end_addr,NULL);
...
}

函數(shù)傳遞進(jìn)已經(jīng)初始化好的mmu積聚結(jié)構(gòu)、操作的起始vma、以及虛擬內(nèi)存范圍[start_addr, end_addr], 然后調(diào)用unmap_single_vma來(lái)操作這個(gè)范圍內(nèi)的每一個(gè)vma。

unmap_single_vma的實(shí)現(xiàn)相關(guān)代碼比較多,在此不在贅述,我們會(huì)分析關(guān)鍵代碼,它主要做的工作為:通過(guò)遍歷進(jìn)程的多級(jí)頁(yè)表,來(lái)找到vma中每一個(gè)虛擬頁(yè)對(duì)應(yīng)的物理頁(yè)(存在的話),然后解除虛擬頁(yè)到物理頁(yè)的映射關(guān)系,最后將物理頁(yè)放入積聚結(jié)構(gòu)中。

總體調(diào)用如下:

//mm/memory.c
unmap_vmas
->unmap_single_vma//處理單個(gè)vma
->unmap_page_range
->zap_p4d_range//遍歷pge頁(yè)目錄中每一個(gè)p4d表項(xiàng)
->zap_pud_range//遍歷p4d頁(yè)目錄中每一個(gè)pud表項(xiàng)
->zap_pmd_range//遍歷pud頁(yè)目錄中每一個(gè)pmd表項(xiàng)
->zap_pte_range//遍歷pmd頁(yè)目錄中每一個(gè)pmd表項(xiàng)

下面我們省略中間各級(jí)頁(yè)表的遍歷過(guò)程,重點(diǎn)看下最后一級(jí)頁(yè)表的處理(這段代碼相當(dāng)關(guān)):

zap_pte_range
->

staticunsignedlongzap_pte_range(structmmu_gather*tlb,
structvm_area_struct*vma,pmd_t*pmd,
unsignedlongaddr,unsignedlongend,
structzap_details*details)
{
...
again:
init_rss_vec(rss);
start_pte=pte_offset_map_lock(mm,pmd,addr,&ptl);//根據(jù)addr從pmd指向的頁(yè)表中獲得頁(yè)表項(xiàng)指針,并申請(qǐng)頁(yè)表的自旋鎖
pte=start_pte;
flush_tlb_batched_pending(mm);
arch_enter_lazy_mmu_mode();
do{
pte_tptent=*pte;//獲得頁(yè)表項(xiàng)
if(pte_none(ptent))//頁(yè)表項(xiàng)的內(nèi)容為空表示沒有映射過(guò),繼續(xù)下一個(gè)虛擬頁(yè)
continue;

...

if(pte_present(ptent)){//虛擬頁(yè)相關(guān)的物理頁(yè)在內(nèi)存中(如沒有被換出到swap)
structpage*page;

page=vm_normal_page(vma,addr,ptent);//獲得虛擬頁(yè)相關(guān)的物理頁(yè)
...

ptent=ptep_get_and_clear_full(mm,addr,pte,
tlb->fullmm);//將頁(yè)表項(xiàng)清空(即是解除了映射關(guān)系),并返回原來(lái)的頁(yè)表項(xiàng)的內(nèi)容
tlb_remove_tlb_entry(tlb,pte,addr);
if(unlikely(!page))
continue;

if(!PageAnon(page)){//如果是文件頁(yè)
if(pte_dirty(ptent)){//是臟頁(yè)
force_flush=1;//強(qiáng)制刷tlb
set_page_dirty(page);//臟標(biāo)志傳遞到page結(jié)構(gòu)
}
if(pte_young(ptent)&&
|likely(!(vma->vm_flags&VM_SEQ_READ)))//如果頁(yè)表項(xiàng)訪問(wèn)標(biāo)志置位,且是隨機(jī)訪問(wèn)的vma,則標(biāo)記頁(yè)面被訪問(wèn)
mark_page_accessed(page);
}
rss[mm_counter(page)]--;//進(jìn)程的相關(guān)rss做減1記賬
page_remove_rmap(page,false);//page->_mapcount--
if(unlikely(page_mapcount(page)if(unlikely(__tlb_remove_page(tlb,page))){//將物理頁(yè)記錄到積聚結(jié)構(gòu)中,如果分配不到mmu_gather_batch結(jié)構(gòu)或不支持返回true
force_flush=1;//強(qiáng)制刷tlb

addr+=PAGE_SIZE;//操作下一個(gè)虛擬頁(yè)
break;//退出循環(huán)
}
continue;//正常情況下,處理下一個(gè)虛擬頁(yè)
}

//下面處理虛擬頁(yè)相關(guān)的物理頁(yè)“不在”內(nèi)存中的情況,可能是交換到swap或者是遷移類型等
entry=pte_to_swp_entry(ptent);//頁(yè)表項(xiàng)得到swp_entry
if(is_device_private_entry(entry)){//處理設(shè)備內(nèi)存表項(xiàng)
structpage*page=device_private_entry_to_page(entry);

if(unlikely(details&&details->check_mapping)){
/*
|*unmap_shared_mapping_pages()wantsto
|*invalidatecachewithouttruncating:
|*unmapsharedbutkeepprivatepages.
|*/
if(details->check_mapping!=
|page_rmapping(page))
continue;
}

pte_clear_not_present_full(mm,addr,pte,tlb->fullmm);
rss[mm_counter(page)]--;
page_remove_rmap(page,false);
put_page(page);
continue;
}


....

if(!non_swap_entry(entry))//非遷移類型的swap_entry
rss[MM_SWAPENTS]--;//進(jìn)程相關(guān)的交換條目的rss減1
elseif(is_migration_entry(entry)){//遷移類型的表項(xiàng)
structpage*page;

page=migration_entry_to_page(entry);//得到對(duì)應(yīng)的物理頁(yè)
rss[mm_counter(page)]--;//進(jìn)程相關(guān)的物理頁(yè)類型的rss減1
}
if(unlikely(!free_swap_and_cache(entry)))//釋放swap條目
print_bad_pte(vma,addr,ptent,NULL);
pte_clear_not_present_full(mm,addr,pte,tlb->fullmm);//清除虛擬頁(yè)相關(guān)的物理頁(yè)的頁(yè)表映射
}while(pte++,addr+=PAGE_SIZE,addr!=end);//遍歷pmd表項(xiàng)管轄范圍內(nèi)的每一個(gè)虛擬頁(yè)

add_mm_rss_vec(mm,rss);//記錄到進(jìn)程的相關(guān)rss結(jié)構(gòu)中
arch_leave_lazy_mmu_mode();

/*DotheactualTLBflushbeforedroppingptl*/
if(force_flush)
tlb_flush_mmu_tlbonly(tlb);//如果是強(qiáng)制刷tlb,則刷tlb
pte_unmap_unlock(start_pte,ptl);//釋放進(jìn)程的頁(yè)表自旋鎖


/*
|*IfweforcedaTLBflush(eitherduetorunningoutof
|*batchbuffersorbecauseweneededtoflushdirtyTLB
|*entriesbeforereleasingtheptl),freethebatched
|*memorytoo.Restartifwedidn'tdoeverything.
|*/
if(force_flush){//如果是強(qiáng)制刷tlb,則釋放掉本次聚集的物理頁(yè)
force_flush=0;
tlb_flush_mmu(tlb);//釋放本次聚集的物理頁(yè)
}

...

returnaddr;

}

以上函數(shù),遍歷進(jìn)程相關(guān)頁(yè)表(一個(gè)pmd表項(xiàng)指向一個(gè)頁(yè)表)所描述的范圍的每一個(gè)虛擬頁(yè),如果之前已經(jīng)建立過(guò)映射,就將相關(guān)的頁(yè)表項(xiàng)清除,對(duì)于在內(nèi)存中物理頁(yè)來(lái)說(shuō),需要調(diào)用__tlb_remove_page將其加入到mmu的積聚結(jié)構(gòu)中,下面重點(diǎn)看下這個(gè)函數(shù):

__tlb_remove_page
->__tlb_remove_page_size//mm/mmu_gather.c
->
bool__tlb_remove_page_size(structmmu_gather*tlb,structpage*page,intpage_size)
{
structmmu_gather_batch*batch;

...

batch=tlb->active;//獲得當(dāng)前批次的積聚結(jié)構(gòu)
/*
|*Addthepageandcheckifwearefull.Ifso
|*forceaflush.
|*/
batch->pages[batch->nr++]=page;//將頁(yè)面加入到批次的積聚結(jié)構(gòu)的pages數(shù)組中,并增加batch->nr計(jì)數(shù)
if(batch->nr==batch->max){//如果當(dāng)前批次的積聚結(jié)構(gòu)的pages數(shù)組中積聚的頁(yè)面?zhèn)€數(shù)到達(dá)最大個(gè)數(shù)
if(!tlb_next_batch(tlb))//獲得下一個(gè)批次積聚結(jié)構(gòu)
returntrue;//獲得不成功返回true
batch=tlb->active;
}
VM_BUG_ON_PAGE(batch->nr>batch->max,page);

returnfalse;//獲得下一個(gè)批次批次積聚結(jié)構(gòu)成功,返回false;
}


我們?cè)賮?lái)看下tlb_next_batch的實(shí)現(xiàn):

staticbooltlb_next_batch(structmmu_gather*tlb)
{
structmmu_gather_batch*batch;

batch=tlb->active;
if(batch->next){//下一個(gè)批次積聚結(jié)構(gòu)存在
tlb->active=batch->next;//當(dāng)前的批次積聚結(jié)構(gòu)指向這個(gè)批次結(jié)構(gòu)
returntrue;
}

if(tlb->batch_count==MAX_GATHER_BATCH_COUNT)//如果批次數(shù)量達(dá)到最大值則返回false
returnfalse;

//批次還沒有到達(dá)最大值,則分配并初始化批次的積聚結(jié)構(gòu)
batch=(void*)__get_free_pages(GFP_NOWAIT|__GFP_NOWARN,0);//申請(qǐng)一個(gè)物理頁(yè)面由于存放mmu_gather_batch和page數(shù)組
if(!batch)
returnfalse;

tlb->batch_count++;//批次計(jì)數(shù)加1
batch->next=NULL;
batch->nr=0;
batch->max=MAX_GATHER_BATCH;//批次積聚結(jié)構(gòu)的page數(shù)組最大個(gè)數(shù)賦值為MAX_GATHER_BATCH

//插入到mmu積聚結(jié)構(gòu)的批次鏈表中
tlb->active->next=batch;
tlb->active=batch;

returntrue;
}

這里有幾個(gè)地方需要注意:MAX_GATHER_BATCH_COUNT 表示的是mmu積聚操作最多可以有多少個(gè)批次積聚結(jié)構(gòu),他的值為10000UL/MAX_GATHER_BATCH (考慮到非搶占式內(nèi)核的soft lockups的影響)。MAX_GATHER_BATCH 表示一個(gè)批次的積聚結(jié)構(gòu)的 page數(shù)組的最多元素個(gè)數(shù),他的值為((PAGE_SIZE - sizeof(struct mmu_gather_batch)) / sizeof(void *)),也就是物理頁(yè)面大小去除掉struct mmu_gather_batch結(jié)構(gòu)大小。

下面給出相關(guān)圖解:

解除頁(yè)表過(guò)程:

b873adb0-d7f0-11ec-ba43-dac502259ad0.png

添加的到積聚結(jié)構(gòu)page數(shù)組頁(yè)面小于等于8個(gè)的情況:

b889110a-d7f0-11ec-ba43-dac502259ad0.png

添加的到積聚結(jié)構(gòu)page數(shù)組頁(yè)面大于8個(gè)的情況:

1個(gè)批次積聚結(jié)構(gòu)->

b8ed8e14-d7f0-11ec-ba43-dac502259ad0.png

2個(gè)批次積聚結(jié)構(gòu)->

b91ca3ac-d7f0-11ec-ba43-dac502259ad0.png

更多批次積聚結(jié)構(gòu)加入->

b955ae40-d7f0-11ec-ba43-dac502259ad0.png

2.5 free_pgtables

unmap_vmas函數(shù)主要是積聚了一些相關(guān)的虛擬頁(yè)面對(duì)應(yīng)的物理頁(yè)面,但是我們還需要釋放各級(jí)頁(yè)表對(duì)應(yīng)的物理頁(yè)等。下面看下free_pgtables的實(shí)現(xiàn):

首先看下它的主要脈絡(luò):

//mm/memory.c
voidfree_pgtables(structmmu_gather*tlb,structvm_area_struct*vma,
unsignedlongfloor,unsignedlongceiling)
{
while(vma){//從起始的vma開始遍歷每個(gè)vma
structvm_area_struct*next=vma->vm_next;//獲得下一個(gè)vma
unsignedlongaddr=vma->vm_start;//獲得vma的起始地址

/*
|*Hidevmafromrmapandtruncate_pagecachebeforefreeing
|*pgtables
|*/
unlink_anon_vmas(vma);//解除匿名vma的反向映射關(guān)系
unlink_file_vma(vma);//解除文件vma反向映射關(guān)系

if(is_vm_hugetlb_page(vma)){
hugetlb_free_pgd_range(tlb,addr,vma->vm_end,
floor,next?next->vm_start:ceiling);
}else{
/*
|*Optimization:gathernearbyvmasintoonecalldown
|*/
while(next&&next->vm_start<=?vma->vm_end+PMD_SIZE
|&&!is_vm_hugetlb_page(next)){
vma=next;
next=vma->vm_next;
unlink_anon_vmas(vma);
unlink_file_vma(vma);
}
free_pgd_range(tlb,addr,vma->vm_end,
floor,next?next->vm_start:ceiling);//遍歷各級(jí)頁(yè)表
}
vma=next;
}
}


我們主要看free_pgd_range的實(shí)現(xiàn):

free_pgd_range
->free_p4d_range
->free_pud_range
->free_pmd_range
->free_pte_range
->....
->pud_clear(pud);////清除pud頁(yè)目錄中的對(duì)應(yīng)的pud表項(xiàng)
pmd_free_tlb(tlb,pmd,start);//pmd頁(yè)目錄的物理頁(yè)放入頁(yè)表的積聚結(jié)構(gòu)中
mm_dec_nr_pmds(tlb->mm)//進(jìn)程使用的頁(yè)表的物理頁(yè)統(tǒng)計(jì)減1
->p4d_clear(p4d);//清除p4d頁(yè)目錄中的對(duì)應(yīng)的p4d表項(xiàng)
pud_free_tlb(tlb,pud,start)//pud頁(yè)目錄的物理頁(yè)放入頁(yè)表的積聚結(jié)構(gòu)中
->pgd_clear(pgd);//清除pgd頁(yè)目錄中的對(duì)應(yīng)的pgd表項(xiàng)
p4d_free_tlb(tlb,p4d,start);//p4d頁(yè)目錄的物理頁(yè)放入頁(yè)表的積聚結(jié)構(gòu)中(存在p4d頁(yè)目錄的話)

我們以最后一級(jí)頁(yè)表(pmd表項(xiàng)指向)為例說(shuō)明:

staticvoidfree_pte_range(structmmu_gather*tlb,pmd_t*pmd,
|unsignedlongaddr)
{
pgtable_ttoken=pmd_pgtable(*pmd);//從相關(guān)的pmd表項(xiàng)指針中獲得頁(yè)表
pmd_clear(pmd);//清除pmd頁(yè)目錄中的對(duì)應(yīng)的pmd表項(xiàng),即是頁(yè)表指針
pte_free_tlb(tlb,token,addr);//存放頁(yè)表的物理頁(yè)放入頁(yè)表的積聚結(jié)構(gòu)中
mm_dec_nr_ptes(tlb->mm);//進(jìn)程使用的頁(yè)表的物理頁(yè)統(tǒng)計(jì)減1
}

看下pte_free_tlb函數(shù):

//include/asm-generic/tlb.h
#ifndefpte_free_tlb
#definepte_free_tlb(tlb,ptep,address)
do{
tlb_flush_pmd_range(tlb,address,PAGE_SIZE);//更新tlb->start和tlb->end,tlb->cleared_pmds=1
tlb->freed_tables=1;
__pte_free_tlb(tlb,ptep,address);//存放頁(yè)表的物理頁(yè)放入頁(yè)表的積聚結(jié)構(gòu)中
}while(0)
#endif

再看看__pte_free_tlb:

//arch/arm64/include/asm/tlb.h
__pte_free_tlb
->pgtable_pte_page_dtor(pte);//執(zhí)行釋放頁(yè)表的時(shí)候的構(gòu)造函數(shù),如釋放ptlock內(nèi)存,zone的頁(yè)表頁(yè)面統(tǒng)計(jì)減1等
tlb_remove_table(tlb,pte);//mm/mmu_gather.c
->structmmu_table_batch**batch=&tlb->batch;//獲得頁(yè)表的積聚結(jié)構(gòu)

if(*batch==NULL){//如何為空,則分配一個(gè)物理頁(yè),存放積聚結(jié)構(gòu)和積聚數(shù)組
*batch=(structmmu_table_batch*)__get_free_page(GFP_NOWAIT|__GFP_NOWARN);
if(*batch==NULL){
tlb_table_invalidate(tlb);
tlb_remove_table_one(table);
return;
}
(*batch)->nr=0;
}

(*batch)->tables[(*batch)->nr++]=table;//相關(guān)的頁(yè)目錄對(duì)應(yīng)的物理頁(yè)放入積聚數(shù)組中
if((*batch)->nr==MAX_TABLE_BATCH)//加入的物理頁(yè)達(dá)到最大值
tlb_table_flush(tlb);//做一次刷tlb和釋放當(dāng)前已經(jīng)積聚的頁(yè)目錄的物理頁(yè)

需要說(shuō)明的是:對(duì)于存放各級(jí)頁(yè)目錄的物理頁(yè)的釋放,每當(dāng)一個(gè)頁(yè)表積聚結(jié)構(gòu)填滿了就會(huì)釋放,不會(huì)構(gòu)建批次鏈表。

2.6 tlb_finish_mmu

通過(guò)上面的unmap_vmas和free_pgtables之后,我們積聚了大量的物理頁(yè)以及存放各級(jí)頁(yè)目錄的物理頁(yè),現(xiàn)在需要將這些頁(yè)面進(jìn)行釋放。

下面我們來(lái)看下tlb_finish_mmu做的mmu-gather的收尾動(dòng)作:

voidtlb_finish_mmu(structmmu_gather*tlb,
unsignedlongstart,unsignedlongend)
{
...

tlb_flush_mmu(tlb);//刷tlb和釋放所有積聚的物理頁(yè)

#ifndefCONFIG_MMU_GATHER_NO_GATHER
tlb_batch_list_free(tlb);//釋放各批次結(jié)構(gòu)對(duì)應(yīng)的物理頁(yè)
#endif
...
}


首先看下tlb_flush_mmu:

mm/mmu_gather.c

voidtlb_flush_mmu(structmmu_gather*tlb)
{
tlb_flush_mmu_tlbonly(tlb);//刷tlb
tlb_flush_mmu_free(tlb);//釋放各個(gè)批次積聚結(jié)構(gòu)的物理頁(yè)
}


tlb_flush_mmu_tlbonly的實(shí)現(xiàn):

staticinlinevoidtlb_flush_mmu_tlbonly(structmmu_gather*tlb)
{
/*
|*Anythingcalling__tlb_adjust_range()alsosetsatleastoneof
|*thesebits.
|*/
if(!(tlb->freed_tables||tlb->cleared_ptes||tlb->cleared_pmds||
|tlb->cleared_puds||tlb->cleared_p4ds))//有一個(gè)為0即返回
return;

tlb_flush(tlb);//刷tlb,和處理器架構(gòu)相關(guān)
...
__tlb_reset_range(tlb);//將tlb->start和tlb->end以及tlb->freed_tables,tlb->cleared_xxx復(fù)位
}

我們來(lái)看下tlb_flush:

arch/arm64/include/asm/tlb.h
staticinlinevoidtlb_flush(structmmu_gather*tlb)
{
structvm_area_structvma=TLB_FLUSH_VMA(tlb->mm,0);
boollast_level=!tlb->freed_tables;
unsignedlongstride=tlb_get_unmap_size(tlb);
inttlb_level=tlb_get_level(tlb);//得到刷tlb的級(jí)別,如只刷pte級(jí)別

/*
|*Ifwe'retearingdowntheaddressspacethenweonlycareabout
|*invalidatingthewalk-cache,sincetheASIDallocatorwon't
|*reallocateourASIDwithoutinvalidatingtheentireTLB.
|*/
if(tlb->fullmm){//刷整個(gè)mm的tlb
if(!last_level)
flush_tlb_mm(tlb->mm);
return;
}

//刷一個(gè)虛擬內(nèi)存范圍的tlb
__flush_tlb_range(&vma,tlb->start,tlb->end,stride,
|last_level,tlb_level);
}

最后我們看tlb_flush_mmu_free:

staticvoidtlb_flush_mmu_free(structmmu_gather*tlb)
{
tlb_table_flush(tlb);//釋放之前積聚的存放各級(jí)頁(yè)目錄的物理頁(yè)
#ifndefCONFIG_MMU_GATHER_NO_GATHER
tlb_batch_pages_flush(tlb);//釋放各個(gè)批次積聚結(jié)構(gòu)積聚的物理頁(yè)
#endif
}

tlb_table_flush的實(shí)現(xiàn):

staticvoidtlb_table_flush(structmmu_gather*tlb)
{
structmmu_table_batch**batch=&tlb->batch;//獲得當(dāng)前的頁(yè)表批次積聚結(jié)構(gòu)

if(*batch){
tlb_table_invalidate(tlb);//刷tlb
tlb_remove_table_free(*batch);//釋放頁(yè)目錄物理頁(yè)
*batch=NULL;
}
}

staticvoidtlb_remove_table_free(structmmu_table_batch*batch)
{
call_rcu(&batch->rcu,tlb_remove_table_rcu);//rsu延遲調(diào)用
->__tlb_remove_table_free(container_of(head,structmmu_table_batch,rcu));
->staticvoid__tlb_remove_table_free(structmmu_table_batch*batch)
{
inti;

for(i=0;inr;i++)//釋放頁(yè)表批次積聚結(jié)構(gòu)中的page數(shù)組中每一個(gè)物理頁(yè)
__tlb_remove_table(batch->tables[i]);

free_page((unsignedlong)batch);//釋放這個(gè)表批次積聚結(jié)構(gòu)對(duì)應(yīng)的物理頁(yè)
}

}

tlb_batch_pages_flush的實(shí)現(xiàn):

staticvoidtlb_batch_pages_flush(structmmu_gather*tlb)
{
structmmu_gather_batch*batch;

for(batch=&tlb->local;batch&&batch->nr;batch=batch->next){//遍歷積聚批次鏈表的每一個(gè)批次積聚結(jié)構(gòu)
free_pages_and_swap_cache(batch->pages,batch->nr);//釋放積聚結(jié)構(gòu)的page數(shù)組的每一個(gè)物理頁(yè)
batch->nr=0;
}
tlb->active=&tlb->local;
}

最終是:調(diào)用free_pages_and_swap_cache將物理頁(yè)的引用計(jì)數(shù)減1 ,引用計(jì)數(shù)為0時(shí)就將這個(gè)物理頁(yè)釋放,還給伙伴系統(tǒng)。

雖然上面已經(jīng)釋放了相關(guān)的各級(jí)頁(yè)表的物理頁(yè)和映射到進(jìn)程地址空間的物理頁(yè),但是存放積聚結(jié)構(gòu)和page數(shù)組的物理頁(yè)還沒有釋放,所以調(diào)用tlb_batch_list_free來(lái)做這個(gè)事情:

tlb_batch_list_free
->staticvoidtlb_batch_list_free(structmmu_gather*tlb)
{
structmmu_gather_batch*batch,*next;

for(batch=tlb->local.next;batch;batch=next){//釋放積聚結(jié)構(gòu)的物理頁(yè)從tlb->local.next開始的,遍歷所有批次的積聚結(jié)構(gòu)
next=batch->next;
free_pages((unsignedlong)batch,0);//釋放這個(gè)批次積聚結(jié)構(gòu)的物理頁(yè)
}
tlb->local.next=NULL;
}

于是相關(guān)的所有物理頁(yè)面都被釋放了(包括相關(guān)地址范圍內(nèi)進(jìn)程各級(jí)頁(yè)目錄對(duì)應(yīng)的物理頁(yè),映射到進(jìn)程地址空間的物理頁(yè),和各個(gè)積聚結(jié)構(gòu)所在的物理頁(yè))。

最后給出整體的圖解:

b9762a76-d7f0-11ec-ba43-dac502259ad0.png

tlb_flush_mmu函數(shù)的tlb_table_flush會(huì)將B鏈表中的相關(guān)物理頁(yè)面釋放(包括之前保存的各級(jí)頁(yè)表的頁(yè)面和mmu_table_batch結(jié)構(gòu)所在頁(yè)面),tlb_batch_pages_flush會(huì)將A鏈表的所有除了積聚結(jié)構(gòu)以外的所有物理頁(yè)面釋放,而tlb_batch_list_free會(huì)將A鏈表的所有批次積聚結(jié)構(gòu)(mmu_gather_batch)的物理頁(yè)面釋放。

3.應(yīng)用場(chǎng)景

使用mmu-gather的應(yīng)用場(chǎng)景主要是進(jìn)程退出,執(zhí)行execv和調(diào)用munmap等。

下面我們主要來(lái)看下他們的調(diào)用鏈:

3.1 進(jìn)程退出時(shí)

進(jìn)程退出時(shí)會(huì)釋放它的所有的相關(guān)聯(lián)的系統(tǒng)資源,其中就包括內(nèi)存資源:

kernel/exit.c
do_exit
->exit_mm
->mmput//kernel/fork.c
->if(atomic_dec_and_test(&mm->mm_users))//如果mm->mm_users減1為0時(shí),也就是當(dāng)前進(jìn)程是最后一個(gè)mm的使用者
__mmput(mm);//釋放mm
->exit_mmap//mm/mmap.c
->tlb_gather_mmu(&tlb,mm,0,-1);//初始化mmu_gather結(jié)構(gòu),start=0,end=-1表示釋放整個(gè)mm
->unmap_vmas(&tlb,vma,0,-1);//解除頁(yè)表映射,相關(guān)的物理頁(yè)放入積聚結(jié)構(gòu)中
>free_pgtables(&tlb,vma,FIRST_USER_ADDRESS,USER_PGTABLES_CEILING);//釋放各級(jí)頁(yè)表,頁(yè)表相關(guān)物理頁(yè)放入頁(yè)表積聚結(jié)構(gòu),滿則釋放
>tlb_finish_mmu(&tlb,0,-1);//刷mm的tlb,釋放所有積聚物理頁(yè),釋放所有積聚結(jié)構(gòu)相關(guān)物理頁(yè)

3.2 執(zhí)行execv時(shí)

執(zhí)行execv時(shí)進(jìn)程會(huì)將所有的mm釋放掉:

fs/exec.c

...
do_execveat_common
->bprm_execve
->exec_binprm
->search_binary_handler
...
->load_elf_binary//fs/binfmt_elf.c
->begin_new_exec
->exec_mmap
->mmput(old_mm)
->if(atomic_dec_and_test(&mm->mm_users))//如果mm->mm_users減1為0時(shí),也就是當(dāng)前進(jìn)程是最后一個(gè)mm的使用者
__mmput(mm);//釋放mm
->exit_mmap//mm/mmap.c
->tlb_gather_mmu(&tlb,mm,0,-1);//初始化mmu_gather結(jié)構(gòu),start=0,end=-1標(biāo)識(shí)釋放整個(gè)mm
->unmap_vmas(&tlb,vma,0,-1);//解除頁(yè)表映射,相關(guān)的物理頁(yè)放入積聚結(jié)構(gòu)中
->free_pgtables(&tlb,vma,FIRST_USER_ADDRESS,USER_PGTABLES_CEILING);//釋放各級(jí)頁(yè)表,頁(yè)表相關(guān)物理頁(yè)放入頁(yè)表積聚結(jié)構(gòu),滿則釋放
->tlb_finish_mmu(&tlb,0,-1);//刷mm的tlb,釋放所有積聚物理頁(yè),釋放所有積聚結(jié)構(gòu)相關(guān)物理頁(yè)

3.3 調(diào)用munmap時(shí)

執(zhí)行munmap時(shí),會(huì)將一個(gè)地址范圍的頁(yè)表解除并釋放相關(guān)的物理頁(yè)面:

mm/mmap.c

...
__do_munmap
->unmap_region(mm,vma,prev,start,end);
->tlb_gather_mmu(&tlb,mm,start,end);//初始化mmu_gather結(jié)構(gòu)
unmap_vmas(&tlb,vma,start,end);//解除頁(yè)表映射,相關(guān)的物理頁(yè)放入積聚結(jié)構(gòu)中
free_pgtables(&tlb,vma,prev?prev->vm_end:FIRST_USER_ADDRESS,
|next?next->vm_start:USER_PGTABLES_CEILING);//釋放各級(jí)頁(yè)表,頁(yè)表相關(guān)物理頁(yè)放入頁(yè)表積聚結(jié)構(gòu),滿則釋放
tlb_finish_mmu(&tlb,start,end);//刷mm的tlb,釋放所有積聚物理頁(yè),釋放所有積聚結(jié)構(gòu)相關(guān)物理頁(yè)

4.總結(jié)

Linux內(nèi)核mmu-gather用于積聚解除映射的相關(guān)物理頁(yè)面,并保證了刷tlb和釋放物理頁(yè)面的順序。首先解除掉相關(guān)虛擬頁(yè)面對(duì)應(yīng)物理頁(yè)面(如果有的話)的頁(yè)表映射關(guān)系,然后將相關(guān)的物理頁(yè)面保存在積聚結(jié)構(gòu)的數(shù)組中,接著將相關(guān)的各級(jí)頁(yè)目錄表項(xiàng)清除,并放入頁(yè)表相關(guān)的積聚結(jié)構(gòu)的數(shù)組中,最后刷對(duì)應(yīng)內(nèi)存范圍的tlb,釋放掉所有放在積聚結(jié)構(gòu)數(shù)組中的物理頁(yè)面。

原文標(biāo)題:4.總結(jié)

文章出處:【微信公眾號(hào):一口Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

審核編輯:湯梓紅
聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 內(nèi)核
    +關(guān)注

    關(guān)注

    4

    文章

    1467

    瀏覽量

    42864
  • Linux
    +關(guān)注

    關(guān)注

    88

    文章

    11756

    瀏覽量

    218995
  • 內(nèi)存管理
    +關(guān)注

    關(guān)注

    0

    文章

    171

    瀏覽量

    14877

原文標(biāo)題:4.總結(jié)

文章出處:【微信號(hào):yikoulinux,微信公眾號(hào):一口Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評(píng)論

    相關(guān)推薦
    熱點(diǎn)推薦

    Linux內(nèi)核伙伴系統(tǒng)內(nèi)存申請(qǐng)函數(shù)詳解:從原理到實(shí)戰(zhàn)

    Linux 內(nèi)核,內(nèi)存管理是整個(gè)系統(tǒng)穩(wěn)定運(yùn)行的基石,而伙伴系統(tǒng)(Buddy System) 作為內(nèi)
    的頭像 發(fā)表于 02-10 16:58 ?3626次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核</b>伙伴系統(tǒng)<b class='flag-5'>內(nèi)存</b>申請(qǐng)函數(shù)詳解:從原理到實(shí)戰(zhàn)

    RDMA設(shè)計(jì)37:RoCE v2 子系統(tǒng)模型設(shè)計(jì)

    發(fā)送數(shù)據(jù)包時(shí),將數(shù)據(jù)包存放到輸出緩沖,輸出緩沖內(nèi)的數(shù)據(jù)包將按照存放順序依次發(fā)出。 虛擬內(nèi)存管理器:用于模擬遠(yuǎn)程主機(jī)的內(nèi)存。由于 RoCE v2 協(xié)議是直接
    發(fā)表于 02-06 16:19

    【「Linux 設(shè)備驅(qū)動(dòng)開發(fā)(第 2 版)」閱讀體驗(yàn)】充分發(fā)揮硬件潛力

    Linux內(nèi)核內(nèi)存分配 Linux系統(tǒng)使用了一種稱為“虛擬內(nèi)存”的機(jī)制。虛擬內(nèi)存機(jī)制使得每個(gè)
    發(fā)表于 02-04 22:30

    Linux內(nèi)核的“心跳”:jiffies如何為系統(tǒng)計(jì)時(shí)?

    Linux 內(nèi)核的世界里,有一個(gè)默默工作的 "計(jì)時(shí)器"——jiffies。它不像我們手機(jī)上的時(shí)鐘那樣顯示年月日,卻掌控著內(nèi)核絕大多數(shù)時(shí)間相關(guān)的
    的頭像 發(fā)表于 02-04 16:27 ?809次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核</b>的“心跳”:jiffies如何為系統(tǒng)計(jì)時(shí)?

    【「Linux 設(shè)備驅(qū)動(dòng)開發(fā)(第 2 版)」閱讀體驗(yàn)】+讀深入理解Linux內(nèi)核內(nèi)存分配

    作者引入內(nèi)存相關(guān)術(shù)語(yǔ),物理地址標(biāo)識(shí)物理內(nèi)存位置。由于虛擬內(nèi)存機(jī)制,用戶和內(nèi)核從不直接訪問(wèn)物理地址,而是通過(guò)相應(yīng)的邏輯地址來(lái)訪問(wèn)的。MMU(
    發(fā)表于 01-16 20:05

    rk基于linux/android內(nèi)存管理

    內(nèi)核對(duì)容量的識(shí)別), 64 位平臺(tái)上認(rèn)為所有內(nèi)存都可 用。然后通過(guò)一系列 reserve_xxx() 接口從內(nèi)存末尾往前預(yù)留需要的內(nèi)存,最后把自己 relocate 到某段 reser
    的頭像 發(fā)表于 12-15 10:42 ?202次閱讀
    rk基于<b class='flag-5'>linux</b>/android<b class='flag-5'>內(nèi)存</b><b class='flag-5'>管理</b>

    FLASH的代碼是如何得到運(yùn)行的呢

    指令的地址。正常情況下自動(dòng)加“4”,遇到分支跳轉(zhuǎn)的時(shí)候,由跳轉(zhuǎn)指令設(shè)置值。那么指針是什么?指針是一個(gè)變量的地址,在含有操作系統(tǒng)(比如Linux、Windows)即硬件層面含有內(nèi)存管理
    發(fā)表于 12-04 08:06

    Linux驅(qū)動(dòng)開發(fā)的必備知識(shí)

    內(nèi)核基礎(chǔ)知識(shí): 1、熟悉 Linux 內(nèi)核的架構(gòu)、模塊系統(tǒng)、進(jìn)程管理、內(nèi)存管理等。 了解
    發(fā)表于 12-04 07:58

    為什么單片機(jī)很少使用malloc,而PC程序頻繁使用呢?

    系統(tǒng)(如Linux、Windows)會(huì)通過(guò)虛擬內(nèi)存內(nèi)存分頁(yè)和內(nèi)存保護(hù)機(jī)制管理內(nèi)存,極大地降低了
    發(fā)表于 11-20 06:55

    單片機(jī)的操作系統(tǒng)

    搶占和輪轉(zhuǎn)調(diào)度,適用于低資源消耗場(chǎng)景(如STM32F0系列)。 ? ? μC/OS-II ?:搶占式多任務(wù)內(nèi)核,支持64個(gè)任務(wù),內(nèi)存分區(qū)管理避免碎片,中斷嵌套深度達(dá)255層,適合工業(yè)控制系統(tǒng)
    發(fā)表于 11-14 06:18

    Perforce QAC 2025.2版本更新:虛擬內(nèi)存優(yōu)化、100%覆蓋CERT C規(guī)則、CI構(gòu)建性能提升等

    【產(chǎn)品更新】Perforce QAC更新至2025.2版本,安裝路徑和許可證都有變化!重點(diǎn):虛擬內(nèi)存占用降低、100%覆蓋CERT C規(guī)則,C23支持增強(qiáng)、CI構(gòu)建性能提升等。建議盡快評(píng)估升級(jí)。
    的頭像 發(fā)表于 09-09 14:40 ?669次閱讀
    Perforce QAC 2025.2版本更新:<b class='flag-5'>虛擬內(nèi)存</b>優(yōu)化、100%覆蓋CERT C規(guī)則、CI構(gòu)建性能提升等

    華納云服務(wù)器Linux系統(tǒng)電源管理與節(jié)能優(yōu)化配置方法

    與優(yōu)化服務(wù)配置,可顯著降低云服務(wù)器能耗,同時(shí)保障業(yè)務(wù)連續(xù)性,實(shí)現(xiàn)綠色計(jì)算與成本控制的雙重目標(biāo)。 Linux電源管理架構(gòu)解析 現(xiàn)代云服務(wù)器Linux系統(tǒng)采用ACPI(高級(jí)配置與電源接口)作為底層
    的頭像 發(fā)表于 08-21 15:09 ?915次閱讀

    Linux內(nèi)核編譯失?。恳苿?dòng)硬盤和虛擬機(jī)的那些事兒

    Linux開發(fā),編譯內(nèi)核是一項(xiàng)常見任務(wù),但不少開發(fā)者在移動(dòng)硬盤或虛擬機(jī)環(huán)境下嘗試時(shí)會(huì)遭遇失敗。本文將簡(jiǎn)要探討這些問(wèn)題的成因,并介紹一些虛擬
    的頭像 發(fā)表于 04-11 11:36 ?991次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核</b>編譯失敗?移動(dòng)硬盤和<b class='flag-5'>虛擬</b>機(jī)的那些事兒

    嵌入式學(xué)習(xí)-飛凌嵌入式ElfBoard ELF 1板卡-Linux設(shè)備驅(qū)動(dòng)的分類

    內(nèi)核模塊嵌入到Linux內(nèi)核,位于內(nèi)核空間。它們直接與內(nèi)核進(jìn)行交互,通過(guò)
    發(fā)表于 03-12 10:20

    飛凌嵌入式ElfBoard ELF 1板卡-Linux設(shè)備驅(qū)動(dòng)的分類

    內(nèi)核模塊嵌入到Linux內(nèi)核,位于內(nèi)核空間。它們直接與內(nèi)核進(jìn)行交互,通過(guò)
    發(fā)表于 03-10 17:00