Linux kernel 3.10内核源码分析--TLB相关--TLB概念、flush、TLB lazy模式
生活随笔
收集整理的這篇文章主要介紹了
Linux kernel 3.10内核源码分析--TLB相关--TLB概念、flush、TLB lazy模式
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
一、概念及基本原理
TLB即Translation Lookaside Buffer,是MMU中的一種硬件cache,用于緩存頁表,即緩存線性地址(虛擬地址)到物理地址的映射關系。
如果沒有TLB,那么正常的內存數據訪問前需要先通過線性地址查進程頁表將其轉換為物理地址,頁表實際也是放在物理內存中的,頁表分級存放,一次地址轉換需要經過多次內存訪問,效率不高,尤其是類似的操作非常頻繁,由此帶來的性能損耗不小。
有了TLB之后,內存數據訪問前只需要先從TLB中查找相應的匹配項,找到后即可跳轉頁表查找的操作,由于TLB是硬件cache,相對于內存訪問來說,效率要高許多,所以通過TLB能較大程度改善地址轉換效率。
TLB中保存著線性地址(前20位)和物理頁框號(pfn)的對映關系,在TLB中查找時,通過匹配線性地址的前20位,如果匹配即可獲取pfn,通過pfn與虛擬地址后12位的偏移組合即可得到最終的物理地址。
如果在TLB中沒有找到匹配的entry,即出現TLB miss,此時仍需通過查找頁表來進行線性地址到物理地址的轉換,此時硬件會自動將相應的映射關系緩存到TLB中。
不同的硬件環境中TLB中的entry數量不一,x86中相對較多。
二、TLB刷新
軟件(OS)對于TLB的控制只有一種方式:TLB刷新(flush),即使TLB失效。失效后,需要重新通過頁表進行地址轉換,同時會生成相應的新的TLB entry。TLB刷新會帶來一定的性能損失,但當頁表被修改時,或發生進程切換時,由于原有TLB中緩存的內容已經失效,此時必須通過軟件觸發TLB刷新操作。
1、TLB刷新的方式
Intel x86架構CPU硬件提供的TLB刷新的方式有多種,軟件(OS)可以根據實際場景選擇使用。
1)INVLPG
flush參數指定的線性地址對應的單個TLB entry,如果相應的TLB entry有global標記(表示該entry可能由多個進程共享),也會被flush。同時,還會invalidates所有paging-structure caches(PML4 cache、PDPTE cache和PDE cache)中的所有與當前PCID相關的entry,而不管其是否與參數指定的線性地址對應。
2)mov to CR3(Linux中task switch時使用)
flush當前CPU上除帶global標記外的所有TLB entry,同時還會invalidates所有paging-structure caches中的所有與當前PCID相關的entry。具體邏輯還取決于CR4.PCIDE的設置,詳見Intel的SDM手冊,這里不詳述。
3)mov to CR4
當修改CR4.PGE位時,flush當前CPU上所有TLB entry,同時還會invalidates所有paging-structure caches(for all PCIDs)。在Linux中,效果跟mov to CR3效果差不多,主要差別在于其會flush掉帶global標記的TLB entry,實際邏輯還跟具體操作相關,詳見Intel的SDM手冊,這里不詳述。
mov to CR0:當將原有的CR0.PG的值從1改為0時(關閉分頁支持?),會flush掉當前CPU所有的TLB entry(包括帶global標記的TLB entry)和paging-structure caches。
4)INVPCID
Linux中通常不使用,這里不詳述。
5)VMX transitions
虛擬化相關,詳見Intel的SDM手冊,這里不詳述。
另外,根據SDM描述,硬件flush TLB和paging-structure caches是比較free的,不一定會嚴格遵循上述規則,比如mov to CR3,也是有可能會flush掉global TLB entry的。所以,最好不好做肯定的假設。
2)內核實現
Linux 3.10內核代碼中對于TLB刷新,定義了如下接口:
/*flush mm相關的TLB項,即flush指定進程相關的TLB項*/
static inline void flush_tlb_mm(struct mm_struct *mm)
{
/*要求被flush的mm必須是當前的active mm,因為只有active mm對應的映射才存在于硬件TLB中*/
if (mm == current->active_mm)
__flush_tlb();
}
/*flush指定vma中的虛擬地址對應的TLB項*/
static inline void flush_tlb_page(struct vm_area_struct *vma,
?unsigned long addr)
{
if (vma->vm_mm == current->active_mm)
__flush_tlb_one(addr);
}
/*
??* flush指定虛擬地址范圍對應的TLB項,目前未實現,等價于__flush_tlb,最終通過load cr3將所有
??* 的TLB(不包括global項)flush
??*/
static inline void flush_tlb_range(struct vm_area_struct *vma,
??unsigned long start, unsigned long end)
{
if (vma->vm_mm == current->active_mm)
__flush_tlb();
}
/*flush 指定進程虛擬地址空間中start-end之間的線性地址對應的TLB項*/
static inline void flush_tlb_mm_range(struct mm_struct *mm,
??unsigned long start, unsigned long end, unsigned long vmflag)
{
if (mm == current->active_mm)
__flush_tlb();
}
/* 通過向其它CPU發送IPI(核間中斷)來使其它CPU flush 指定地址范圍內的TLB*/
void native_flush_tlb_others(const struct cpumask *cpumask,
struct mm_struct *mm, unsigned long start,
unsigned long end)
{
...
}
最終都是通過如下幾個接口實現最終的flush操作。
1)flush除global外的所有TLB entry,通過mov to cr3方法實現。
/* 等價于__flush_tlb,最終通過load cr3將當前CPU對應的所有的TLB(不包括global項)flush*/
static inline void __native_flush_tlb(void)
{
/*通過重新加載cr3實現,flush除global項之外的所有TLB*/
native_write_cr3(native_read_cr3());
}
/*將val的值寫入CR3寄存器*/
static inline void native_write_cr3(unsigned long val)
{
asm volatile("mov %0,%%cr3": : "r" (val), "m" (__force_order));
}
2)flush包括global項的所有TLB entry,通過mov to cr4方法實現。
/*通過Read-modify-write to CR4來flush TLB,包括global項*/
static inline void __native_flush_tlb_global(void)
{
unsigned long flags;
/*
* Read-modify-write to CR4 - protect it from preemption and
* from interrupts. (Use the raw variant because this code can
* be called from deep inside debugging code.)
*/
/*關中斷,防止競爭*/
raw_local_irq_save(flags);
__native_flush_tlb_global_irq_disabled();
/*開中斷*/
raw_local_irq_restore(flags);
}
static inline void __native_flush_tlb_global_irq_disabled(void)
{
unsigned long cr4;
/*讀取CR4*/
cr4 = native_read_cr4();
/* clear PGE */
/*
?* 清除掉X86_CR4_PGE位后,重新將其寫入CR4,清除X86_CR4_PGE位的目的是intel x86架構CPU中,當使用mov to cr4的方式flush TLB時,
?* 只有當CR4.PGE位有修改時,才會觸發TLB flush。詳見Intel sdm 4.10節。
?*/
native_write_cr4(cr4 & ~X86_CR4_PGE);
/* write old PGE again and flush TLBs */
/*
?* 重新將原CR4內容寫入CR4,因為上一行代碼將CR4.PGE位清除掉了,需要恢復重新寫入,才能恢復正常狀態,此處其實
?* 再次發生了TLB flush,也就是說Linux中通過mov to cr4的方法實現的TLB flush實際進行了兩次flush操作。
?* Fixme:是否能優化下?
?*/
native_write_cr4(cr4);
}
3)flush單個TLB entry
/*flush單個TLB項,由入參addr指定具體的TLB項*/
static inline void __native_flush_tlb_single(unsigned long addr)
{
/*invlpg指令flush指定的TLB項,不考慮該TLB是否有Global標記*/
asm volatile("invlpg (%0)" ::"r" (addr) : "memory");
}
三、TLB lazy模式
1、原理
由于TLB刷新會帶來一定的性能損失,所以,需要盡量減少使用。
當內核中進行進程上下文切換時,有如下兩種情況,實際上是不需要立刻進行TLB刷新的,可以避免應TLB刷新代理店額性能損失,Linux充分考慮了這些情況,可謂將
相關性能進行了充分發揮:
1)當從普通進程切換到內核線程時。由于Linux中,所有進程共享內核地址空間,內核線程并不使用用戶態部分的地址空間,只使用內核部分,所以,當從普通進程切換到內核線程時,內核線程繼續沿用prev進程的用戶態地址空間,但是并不訪問,其只訪問內核部分。因此,這種情況下,實際不不需要立刻flush TLB。
2)當新切換的next進程和prev進程使用相同的頁表時,比如同一進程中的線程,共享地址空間。此時也不需要進行TLB刷新。
對于上述的第2中情況,由于不會重新加載CR3,不會切換頁表,自然也不會觸發TLB刷新。
對于上述的第1中情況,如果不進行特殊處理,實際是會在重新加載CR3時觸發TLB刷新的,從而導致性能損失。TLB lazy刷新模式即針對這種情況設計。其基本原理為:
當發生內核調度,從普通進程切換到內核線程時,則當前CPU進入TLB lazy模式,當切換到普通進程時退出lazy模式。進入TLB lazy模式后,如果其它CPU通過IPI(核間中斷)通知當前CPU進行TLB flush時,在IPI的中斷處理函數中,將本CPU對應的active_mm的mask中的相應位清除,因此,當其它CPU再次對該mm進行TLB flush操作時,將不會再向本CPU發送IPI,此后至本CPU退出TLB lazy模式前,本CPU將不再收到來自其它CPU的TLB flush請求,由此實現lazy,提升效率。
值得注意的是,在進入TLB lazy模式后,當第一次收到TLB flush的IPI時,本CPU重新新加載主內核頁目錄swapper_pg_dir到CR3中,從而將本CPU的TLB刷新一次(不包括Global項)。如此操作的目的注意是因為擔心X86架構CPU的超長指令預取,預取的指令可能會訪問到需要刷新的TLB entry對應的物理內存,此時如果不flush TLB,可能會出現一致性問題。Linux內核中采用這種相對比較暴力的方式避免了這種情況,雖然看似有點暴力,實則是沒有更好的其他做法的無奈之舉。
所以,在TLB lazy模式下,如果收到TLB flush請求,實際上還是會刷新一次,看起來好像不怎么lazy,但由于清除了active_mm中相應的cpu mask位,可以避免后續的TLB flush,實際還是有點效果的。
2、代碼實現
內核中定義了相應的數據結構用于表示CPU的模式:
/*表示CPU的TLB模式的結構體*/
struct tlb_state {
/*當前CPU上的active mm,通常通過讀取相應的per CPU變量獲取*/
struct mm_struct *active_mm;
/*模式,可選狀態為TLBSTATE_OK或TLBSTATE_LAZY(lazy模式)*/
int state;
};
可選的模式有:
/*TLB 非lazy模式,即正常模式*/
#define TLBSTATE_OK????1
/*TLB lazy刷新模式*/
#define TLBSTATE_LAZY????2
同時,定義相應的per-CPU變量,用于存放當前CPU的TLB模式信息
/*定義per CPU變量,用于存放當前CPU的TLB模式信息*/
DECLARE_PER_CPU_SHARED_ALIGNED(struct tlb_state, cpu_tlbstate);
多核間通過IPI進行TLB flush的相關代碼流程如下:
flush_tlb_mm->
????flush_tlb_mm_range->
????????flush_tlb_others->
????????????native_flush_tlb_others->
????????????????smp_call_function_many ->
????????????????????smp_call_function_single->
????????????????????????generic_exec_single->
????????????????????????????arch_send_call_function_single_ipi->
????????????????????????????????send_call_func_single_ipi->
????????????????????????????????????native_send_call_func_single_ipi->
最終的IPI發送通過apic的send_IPI_mask接口
void native_send_call_func_single_ipi(int cpu)
{
apic->send_IPI_mask(cpumask_of(cpu), CALL_FUNCTION_SINGLE_VECTOR);
}
flush_tlb_mm->flush_tlb_mm_range->flush_tlb_others->native_flush_tlb_others():
/* 通過向其它CPU發送IPI(核間中斷)來使其它CPU flush 指定地址范圍內的TLB*/
void native_flush_tlb_others(const struct cpumask *cpumask,
struct mm_struct *mm, unsigned long start,
unsigned long end)
{
struct flush_tlb_info info;
info.flush_mm = mm;
info.flush_start = start;
info.flush_end = end;
if (is_uv_system()) {
unsigned int cpu;
cpu = smp_processor_id();
cpumask = uv_flush_tlb_others(cpumask, mm, start, end, cpu);
if (cpumask)
smp_call_function_many(cpumask, flush_tlb_func,
&info, 1);
return;
}
/*
?* 通過smp_call_function_many機制,向其它核發送ipi,使其執行指定的函數(flush_tlb_func),
?* 最后一個入參表示是否wait,此處傳入1,表示需要阻塞等待
?* 所有核都執行完成后才繼續后面的流程。
?*/
smp_call_function_many(cpumask, flush_tlb_func, &info, 1);
}
其它CPU收到IPI后執行的函數為flush_tlb_func
flush_tlb_mm->flush_tlb_mm_range->flush_tlb_others->native_flush_tlb_others->smp_call_function_many->flush_tlb_func():
/*
?* TLB flush funcation:
?* 1) Flush the tlb entries if the cpu uses the mm that's being flushed.
?* 2) Leave the mm if we are in the lazy tlb mode.
?*/
/*刷新當前CPU的TLB,如果當前處于lazy模式,則調用leave_mm*/
static void flush_tlb_func(void *info)
{
struct flush_tlb_info *f = info;
inc_irq_stat(irq_tlb_count);
if (f->flush_mm != this_cpu_read(cpu_tlbstate.active_mm))
return;
/*判斷當前CPU是否處于TLB lazy模式*/
if (this_cpu_read(cpu_tlbstate.state) == TLBSTATE_OK) {
/*flush當前CPU的所有TLB項(不包括global項)*/
if (f->flush_end == TLB_FLUSH_ALL)
local_flush_tlb();
else if (!f->flush_end)
/*當沒有指定flush_end時,flush flush_start對應的單個TLB entry*/
__flush_tlb_single(f->flush_start);
else {
/*當指定了flush_end時,循環flush 從flush_start到flush_end的地址范圍對應的所有TLB entry*/
unsigned long addr;
addr = f->flush_start;
while (addr < f->flush_end) {
__flush_tlb_single(addr);
addr += PAGE_SIZE;
}
}
} else
/*TLB lazy模式,重新加載CR3,并清除掉當前mm中相應的cpu mask位,防止其它CPU再次向本CPU發送TLB flush的IPI*/
leave_mm(smp_processor_id());
}
flush_tlb_mm->flush_tlb_mm_range->flush_tlb_others->native_flush_tlb_others->smp_call_function_many->flush_tlb_func->leave_mm():
/*
?* We cannot call mmdrop() because we are in interrupt context,
?* instead update mm->cpu_vm_mask.
?*/
/*重新加載CR3 刷新當前CPU TLB(不包括global項),并清除掉當前mm中相應的cpu mask位,防止其它CPU再次向本CPU發送TLB flush的IPI*/
void leave_mm(int cpu)
{
/*獲取當前的active_mm*/
struct mm_struct *active_mm = this_cpu_read(cpu_tlbstate.active_mm);
/*Fixme:一定是lazy模式使用?*/
if (this_cpu_read(cpu_tlbstate.state) == TLBSTATE_OK)
BUG();
if (cpumask_test_cpu(cpu, mm_cpumask(active_mm))) {
/*
?*去除當前CPU對該當前active_mm的引用,這樣的話,當其它CPU再次通過IPI請求flush TLB時,本CPU就不會收到相應的IPI,也就不會再刷新TLB了,因為這里
?*已經刷過了,這也是lazy TLB模式的核心所在
?*/
cpumask_clear_cpu(cpu, mm_cpumask(active_mm));
/*并通過重新加載cr3(主內核頁目錄)刷新TLB(不包括Global的TLB項)*/
load_cr3(swapper_pg_dir);
}
}
TLB即Translation Lookaside Buffer,是MMU中的一種硬件cache,用于緩存頁表,即緩存線性地址(虛擬地址)到物理地址的映射關系。
如果沒有TLB,那么正常的內存數據訪問前需要先通過線性地址查進程頁表將其轉換為物理地址,頁表實際也是放在物理內存中的,頁表分級存放,一次地址轉換需要經過多次內存訪問,效率不高,尤其是類似的操作非常頻繁,由此帶來的性能損耗不小。
有了TLB之后,內存數據訪問前只需要先從TLB中查找相應的匹配項,找到后即可跳轉頁表查找的操作,由于TLB是硬件cache,相對于內存訪問來說,效率要高許多,所以通過TLB能較大程度改善地址轉換效率。
TLB中保存著線性地址(前20位)和物理頁框號(pfn)的對映關系,在TLB中查找時,通過匹配線性地址的前20位,如果匹配即可獲取pfn,通過pfn與虛擬地址后12位的偏移組合即可得到最終的物理地址。
如果在TLB中沒有找到匹配的entry,即出現TLB miss,此時仍需通過查找頁表來進行線性地址到物理地址的轉換,此時硬件會自動將相應的映射關系緩存到TLB中。
不同的硬件環境中TLB中的entry數量不一,x86中相對較多。
二、TLB刷新
軟件(OS)對于TLB的控制只有一種方式:TLB刷新(flush),即使TLB失效。失效后,需要重新通過頁表進行地址轉換,同時會生成相應的新的TLB entry。TLB刷新會帶來一定的性能損失,但當頁表被修改時,或發生進程切換時,由于原有TLB中緩存的內容已經失效,此時必須通過軟件觸發TLB刷新操作。
1、TLB刷新的方式
Intel x86架構CPU硬件提供的TLB刷新的方式有多種,軟件(OS)可以根據實際場景選擇使用。
1)INVLPG
flush參數指定的線性地址對應的單個TLB entry,如果相應的TLB entry有global標記(表示該entry可能由多個進程共享),也會被flush。同時,還會invalidates所有paging-structure caches(PML4 cache、PDPTE cache和PDE cache)中的所有與當前PCID相關的entry,而不管其是否與參數指定的線性地址對應。
2)mov to CR3(Linux中task switch時使用)
flush當前CPU上除帶global標記外的所有TLB entry,同時還會invalidates所有paging-structure caches中的所有與當前PCID相關的entry。具體邏輯還取決于CR4.PCIDE的設置,詳見Intel的SDM手冊,這里不詳述。
3)mov to CR4
當修改CR4.PGE位時,flush當前CPU上所有TLB entry,同時還會invalidates所有paging-structure caches(for all PCIDs)。在Linux中,效果跟mov to CR3效果差不多,主要差別在于其會flush掉帶global標記的TLB entry,實際邏輯還跟具體操作相關,詳見Intel的SDM手冊,這里不詳述。
mov to CR0:當將原有的CR0.PG的值從1改為0時(關閉分頁支持?),會flush掉當前CPU所有的TLB entry(包括帶global標記的TLB entry)和paging-structure caches。
4)INVPCID
Linux中通常不使用,這里不詳述。
5)VMX transitions
虛擬化相關,詳見Intel的SDM手冊,這里不詳述。
另外,根據SDM描述,硬件flush TLB和paging-structure caches是比較free的,不一定會嚴格遵循上述規則,比如mov to CR3,也是有可能會flush掉global TLB entry的。所以,最好不好做肯定的假設。
2)內核實現
Linux 3.10內核代碼中對于TLB刷新,定義了如下接口:
點擊(此處)折疊或打開
最終都是通過如下幾個接口實現最終的flush操作。
1)flush除global外的所有TLB entry,通過mov to cr3方法實現。
點擊(此處)折疊或打開
2)flush包括global項的所有TLB entry,通過mov to cr4方法實現。
點擊(此處)折疊或打開
3)flush單個TLB entry
點擊(此處)折疊或打開
三、TLB lazy模式
1、原理
由于TLB刷新會帶來一定的性能損失,所以,需要盡量減少使用。
當內核中進行進程上下文切換時,有如下兩種情況,實際上是不需要立刻進行TLB刷新的,可以避免應TLB刷新代理店額性能損失,Linux充分考慮了這些情況,可謂將
相關性能進行了充分發揮:
1)當從普通進程切換到內核線程時。由于Linux中,所有進程共享內核地址空間,內核線程并不使用用戶態部分的地址空間,只使用內核部分,所以,當從普通進程切換到內核線程時,內核線程繼續沿用prev進程的用戶態地址空間,但是并不訪問,其只訪問內核部分。因此,這種情況下,實際不不需要立刻flush TLB。
2)當新切換的next進程和prev進程使用相同的頁表時,比如同一進程中的線程,共享地址空間。此時也不需要進行TLB刷新。
對于上述的第2中情況,由于不會重新加載CR3,不會切換頁表,自然也不會觸發TLB刷新。
對于上述的第1中情況,如果不進行特殊處理,實際是會在重新加載CR3時觸發TLB刷新的,從而導致性能損失。TLB lazy刷新模式即針對這種情況設計。其基本原理為:
當發生內核調度,從普通進程切換到內核線程時,則當前CPU進入TLB lazy模式,當切換到普通進程時退出lazy模式。進入TLB lazy模式后,如果其它CPU通過IPI(核間中斷)通知當前CPU進行TLB flush時,在IPI的中斷處理函數中,將本CPU對應的active_mm的mask中的相應位清除,因此,當其它CPU再次對該mm進行TLB flush操作時,將不會再向本CPU發送IPI,此后至本CPU退出TLB lazy模式前,本CPU將不再收到來自其它CPU的TLB flush請求,由此實現lazy,提升效率。
值得注意的是,在進入TLB lazy模式后,當第一次收到TLB flush的IPI時,本CPU重新新加載主內核頁目錄swapper_pg_dir到CR3中,從而將本CPU的TLB刷新一次(不包括Global項)。如此操作的目的注意是因為擔心X86架構CPU的超長指令預取,預取的指令可能會訪問到需要刷新的TLB entry對應的物理內存,此時如果不flush TLB,可能會出現一致性問題。Linux內核中采用這種相對比較暴力的方式避免了這種情況,雖然看似有點暴力,實則是沒有更好的其他做法的無奈之舉。
所以,在TLB lazy模式下,如果收到TLB flush請求,實際上還是會刷新一次,看起來好像不怎么lazy,但由于清除了active_mm中相應的cpu mask位,可以避免后續的TLB flush,實際還是有點效果的。
2、代碼實現
內核中定義了相應的數據結構用于表示CPU的模式:
點擊(此處)折疊或打開
可選的模式有:
點擊(此處)折疊或打開
同時,定義相應的per-CPU變量,用于存放當前CPU的TLB模式信息
點擊(此處)折疊或打開
多核間通過IPI進行TLB flush的相關代碼流程如下:
flush_tlb_mm->
????flush_tlb_mm_range->
????????flush_tlb_others->
????????????native_flush_tlb_others->
????????????????smp_call_function_many ->
????????????????????smp_call_function_single->
????????????????????????generic_exec_single->
????????????????????????????arch_send_call_function_single_ipi->
????????????????????????????????send_call_func_single_ipi->
????????????????????????????????????native_send_call_func_single_ipi->
最終的IPI發送通過apic的send_IPI_mask接口
點擊(此處)折疊或打開
flush_tlb_mm->flush_tlb_mm_range->flush_tlb_others->native_flush_tlb_others():
點擊(此處)折疊或打開
其它CPU收到IPI后執行的函數為flush_tlb_func
flush_tlb_mm->flush_tlb_mm_range->flush_tlb_others->native_flush_tlb_others->smp_call_function_many->flush_tlb_func():
點擊(此處)折疊或打開
flush_tlb_mm->flush_tlb_mm_range->flush_tlb_others->native_flush_tlb_others->smp_call_function_many->flush_tlb_func->leave_mm():
點擊(此處)折疊或打開
原文地址: http://blog.chinaunix.net/uid-14528823-id-4808877.html
總結
以上是生活随笔為你收集整理的Linux kernel 3.10内核源码分析--TLB相关--TLB概念、flush、TLB lazy模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: kernel 3.10内核源码分析--内
- 下一篇: Linux kernel 3.10内核源