linux 內存布局以及tlb更新的一些理解


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 等函數。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM