x86架構,問題:
1.內核線程是否有vma線性區?
2.單線程的一個進程,它修改了自己的頁表,是否需要發送ipi來通知其他核更新tlb?
3.普通進程,在32位和64位,對應的線性區的最大地址能到多少?
在64位中,linux內核默認的內存布局是:
ffffffff ffffffff _____________
| |
| 內核空間 |
ffff8000 00000000 |____________|
| |
| 未使用 |
| 的空間 |
| |
00007fff ffffffff |____________|
| |
| 用戶空間 |
00000000 00000000 |____________|
也就是用戶空間占用的位數是47位,內核空間也是47位,所以整體可以尋址的是2的48次方,也就是256T,足夠用了。
那另外0xffff,8800,0000,0000 – 0xffff,c7ff,ffff,ffff這64T直接和物理內存進行映射,0xffff,c900,0000,0000 – 0xffff,e8ff,ffff,ffff這32T用於vmalloc/ioremap的地址空間。
而32位地址空間時,當物理內存大於896M時(Linux2.4內核是896M,3.x內核是884M,是個經驗值),由於地址空間的限制,內核只會將0~896M的地址進行映射,而896M以上的空間用做一些固定映射和vmalloc/ioremap。而64位地址時是將所有物理內存都進行映射。
64位里面沒有了那些高端內存的說法,所以內存管理和映射反而簡單了。借用csdn中搜索的圖(圖中有對應水印),用戶態地址布局如下:
內核態地址布局如下:
現在看第一個問題:內核線程,借用了別人的mm作為active_mm,它其實沒有vma的,vma里面都是關於用戶態地址的。由於內核線程不訪問用戶態地址空間,則不需要vma。(有同事提醒說,如果一個內核線程故意去訪問用戶態的數據區,這種也需要訪問用戶態地址空間,但我沒見着這么用過的,自己寫的內核線程倒是可以這么做)
一個多線程的進程,在內核中,看到的mm_struct(簡寫為mm,下同)中的計數如下:
crash> mm_struct.mm_users 0xffff8857b5ef92c0 mm_users = { counter = 904 } crash> mm_struct.mm_count 0xffff8857b5ef92c0 mm_count = { counter = 46 }
mm_user是這個進程中的線程個數的體現,也就是這些線程共用這個mm,
如果是內核線程借用mm作為active_mm,則計數增加是在mm_count中,對於只有一個線程的進程而言,如果調度出去,那么它的mm如果被內核線程借用的話,則mm有兩個計數,應該分別為:
mm_user =1,而mm_count=2.(其中一個是本身的計數,一個是內核線程引用的計數)。
初始狀態下,這兩個值都是1。當 mm_users 減為 0 時,mm_count 會自減 1。也即是僅當 mm_users 減為 0 時,mm_count 才可能為 0。
mm_count 代表了對 mm 本身的引用,而 mm_users 代表對 mm 相關資源的引用,分了兩個層次。
可能存在這樣的情況,mm 已經沒有人使用了(mm_users為0),所以其相關資源被釋放掉(如vma);但是 mm 本身可能還在被人借用(比如被一個內核線程,它不會使用vma),所以 mm 本身還不能被釋放;這句話不知道怎么寫比較好,就像中國人說,夏天能穿多少穿多少,冬天能穿多少穿多少一個道理。
現在來看第二個問題,關於tlb的刷新問題,首先要明白tlb中,有標記為Global(后面簡稱G)的部分,也有不是的,為G的屬於內核,所有進程通用。ipi是實現tlb更新的一種方式,但不是唯一方式。處理器不能自動同步他們自己的tlb,因為決定線性地址與物理地址之間的映射有效性的,是內核,而不是硬件本身。
當這個單線程的進程A調度到其他的cpu上,他的mm可能還被其他內核線程借用着,這個時候如果該線程A修改了自己的頁表集,不管是G的條目還是非G的條目,自然要體現在當前cpu的tlb上,如果是G項,則會flush_tlb_all,此時的目的cpu是所有cpu,如果非G項,就會發送ipi通知相關的cpu,發送的目的cpu,就在mm的cpumask_allocation成員中(2.6的代碼是在cpu_vm_mask),由於被借用,所以如果借用的內核線程運行的cpu和當前A線程運行的cpu不一致的話,則cpumask_allocation 中就有兩個cpu位被置位。此時使用的函數主要有flush_tlb_mm和flush_tlb_range等,針對的是非G的條目。關於cpumask_allocation,可以用如下的crash來驗證:
crash> mm_struct.cpumask_allocation 0xffff8852eb41abc0 cpumask_allocation = { bits = {562949953421312, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} } crash> px 562949953421312 $2 = 0x2000000000000 fpu = { last_cpu = 49, has_fpu = 1, state = 0xffff8823f52dc980 },
借用A的mm的內核線程B,因為啟用lazytlb模式,雖然收到了ipi請求刷新tlb,非G條目,就不會更新其tlb,並將其cpu從對應的mm->cpu_vm_mask刪除。一旦刪除,以后都不會再收到非G條目的更新了,因為發送方是根據cpu_vm_mask(3.10是cpumask_allocation)來發送ipi請求的。當該內核線程被切換出去的時候,如果被替換的mm剛好和自己的mm一樣,則內核調用__flush_tlb 來使得該cpu的所有非全局tlb表項失效。
第一次看到這個地方,還很迷糊,明明和自己的mm一樣,為啥還要刷,后來想了下,由於lazytlb模式,自己當時已經不刷tlb了,也就是雖然mm一樣,但和該進程的mm里面的各個vma對應的頁表已經不一樣了,存在臟的tlb,如果不刷,有可能導致訪問異常。就是偷懶了沒刷,到頭來發現mm一樣,還得刷,但這種概率不高,特別是單線程的程序。
如果mm不一樣,自然天生要刷。這個就是lazytlb的。
最后來看第三個問題,由於線性區里面對應的地址都是用戶態的,所以應該一個是3G,一個是2的47次方了,跟布局有關系。這個和第一個問題有些相關。
ps:lazytlb,在vmalloc中也有體現,就是vfree流程中,對於tlbflush操作,采用lazy模式,此時並不真正free,等收集一部分之后,再free,同時刷tlb,防止每次free都去刷tlb,提高了性能。具體可以查看 free_vmap_area_noflush ,lazy_max_pages 等函數。