天天看點

linux 記憶體位址空間管理 mm_struct

linux對于記憶體的管理涉及到非常多的方面,這篇文章首先從對程序虛拟位址空間的管理說起。(所依據的代碼是2.6.32.60)

無論是核心線程還是使用者程序,對于核心來說,無非都是task_struct這個資料結構的一個執行個體而已,task_struct被稱為程序描述符(process descriptor),因為它記錄了這個程序所有的context。其中有一個被稱為'記憶體描述符‘(memory descriptor)的資料結構mm_struct,抽象并描述了linux視角下管理程序位址空間的所有資訊。

mm_struct定義在include/linux/mm_types.h中,其中的域抽象了程序的位址空間,如下圖所示:

linux 記憶體位址空間管理 mm_struct

每個程序都有自己獨立的mm_struct,使得每個程序都有一個抽象的平坦的獨立的32或64位位址空間,各個程序都在各自的位址空間中相同的位址記憶體存放不同的資料而且互不幹擾。如果程序之間共享相同的位址空間,則被稱為線程。

其中[start_code,end_code)表示代碼段的位址空間範圍。

[start_data,end_start)表示資料段的位址空間範圍。

[start_brk,brk)分别表示heap段的起始空間和目前的heap指針。

[start_stack,end_stack)表示stack段的位址空間範圍。

mmap_base表示memory mapping段的起始位址。那為什麼mmap段沒有結束的位址呢?

bbs段是用來幹什麼的呢?bbs表示的所有沒有初始化的全局變量,這樣隻需要将它們匿名映射為‘零頁’,而不用在程式load過程中從磁盤檔案顯示的mapping,這樣既減少了elf二進制檔案的大小,也提高了程式加載的效率。在mm_struct中為什麼沒有bbs段的位址空間表示呢?

除此之外,mm_struct還定義了幾個重要的域:

這兩個counter乍看好像差不多,那linux使用中有什麼差別呢?看代碼就是最好的解釋了。

無論我們在調用fork,vfork,clone的時候最終會調用do_fork函數,差別在于vfork和clone會給copy_mm傳入一個clone_vm的flag,這個辨別表示父子程序都運作在同樣一個‘虛拟位址空間’上面(在linux稱之為lightweight process或者線程),當然也就共享同樣的實體位址空間(page frames)。

copy_mm函數中,如果建立線程中有clone_vm辨別,則表示父子程序共享位址空間和同一個記憶體描述符,并且隻需要将mm_users值+1,也就是說mm_users表示正在引用該位址空間的thread數目,是一個thread level的counter。

mm_count呢?mm_count的了解有點複雜。

對linux來說,使用者程序和核心線程(kernel thread)都是task_struct的執行個體,唯一的差別是kernel thread是沒有程序位址空間的,核心線程也沒有mm描述符的,是以核心線程的tsk->mm域是空(null)。核心scheduler在程序context switching的時候,會根據tsk->mm判斷即将排程的程序是使用者程序還是核心線程。但是雖然thread thread不用通路使用者程序位址空間,但是仍然需要page table來通路kernel自己的空間。但是幸運的是,對于任何使用者程序來說,他們的核心空間都是100%相同的,是以核心可以’borrow'上一個被調用的使用者程序的mm中的頁表來通路核心位址,這個mm就記錄在active_mm。

簡而言之就是,對于kernel thread,tsk->mm == null表示自己核心線程的身份,而tsk->active_mm是借用上一個使用者程序的mm,用mm的page table來通路核心空間。對于使用者程序,tsk->mm == tsk->active_mm。

為了支援這個特别,mm_struct裡面引入了另外一個counter,mm_count。剛才說過mm_users表示這個程序位址空間被多少線程共享或者引用,而mm_count則表示這個位址空間被核心線程引用的次數+1。

比如一個程序a有3個線程,那麼這個a的mm_struct的mm_users值為3,但是mm_count為1,是以mm_count是process level的counter。維護2個counter有何用處呢?考慮這樣的scenario,核心排程完a以後,切換到核心核心線程b,b ’borrow' a的mm描述符以通路核心空間,這時mm_count變成了2,同時另外一個cpu core排程了a并且程序a exit,這個時候mm_users變為了0,mm_count變為了1,但是核心不會因為mm_users==0而銷毀這個mm_struct,核心隻會當mm_count==0的時候才會釋放mm_struct,因為這個時候既沒有使用者程序使用這個位址空間,也沒有核心線程引用這個位址空間。

在初始化一個mm執行個體的時候,mm_users和mm_count都被初始化為1。

上面的代碼是linux scheduler進行的context switch的一小段,從unlike(!mm)開始,next->active_mm = oldmm表示如果将要切換倒核心線程,則‘借用’前一個擁護程序的mm描述符,并把他賦給active_mm,重點是将‘借用’的mm描述符的mm_counter加1。

下面我們看看在fork一個程序的時候,是怎樣處理的mm_struct的。

do_fork調用copy_process。

copy_process調用copy_mm,下面來分析copy_mm。

692,693行,對子程序或者線程的mm和active_mm初始化(null)。

700 - 708行,就是我們上面說的如果是建立線程,則新線程共享建立程序的mm,是以不需要進行下面的copy操作。

重點就是711行的dup_mm(tsk)。

633行,用slab配置設定了mm_struct的記憶體對象。

637行,對子程序的mm_struct程序指派,使其等于父程序,這樣子程序mm和父程序mm的每一個域的值都相同。

在copy_mm的實作中,主要是為了實作unix cow的語義,是以理論上我們隻需要父子程序mm中的start_x和end_x之類的域(像start_data,end_data)相等,而對其餘的域(像mm_users)則需要re-init,這個操作主要在mm_init中完成。

其中特别要關注的是467 - 471行的mm_alloc_pdg,也就是page table的拷貝,page table負責logic address到physical address的轉換。

拷貝的結果就是父子程序有獨立的page table,但是page table裡面的每個entries值都是相同的,也就是說父子程序獨立位址空間中相同logical address都對應于相同的physical address,這樣也就是實作了父子程序的cow(copy on write)語義。

事實上,vfork和fork相比,最大的開銷節省就是對page table的拷貝。

而在核心2.6中,由于page table的拷貝,fork在性能上是有所損耗的,是以核心社群裡面讨論過shared page table的實作

------------------越是喧嚣的世界,越需要甯靜的思考------------------

合抱之木,生于毫末;九層之台,起于壘土;千裡之行,始于足下。

積土成山,風雨興焉;積水成淵,蛟龍生焉;積善成德,而神明自得,聖心備焉。故不積跬步,無以至千裡;不積小流,無以成江海。骐骥一躍,不能十步;驽馬十駕,功在不舍。锲而舍之,朽木不折;锲而不舍,金石可镂。蚓無爪牙之利,筋骨之強,上食埃土,下飲黃泉,用心一也。蟹六跪而二螯,非蛇鳝之穴無可寄托者,用心躁也。

繼續閱讀