內核除了管理本身的內存外,還必須管理進程的地址空間,即系統中每個用戶空間進程所看到的內存。Linux採用虛擬內存技術,系統中的所有進程之間以虛擬方式共享內存。對每個進程來說,它們好像都可以訪問整個系統的所有物理內存;即使單獨一個進程,它擁有的地址空間也可以遠遠大於系統物理內存。
進程地址空間由每個進程中的線性地址區組成,而且更爲重要的特點是內核允許進程使用該空間中的地址。每個進程都有一個32或64位的flat地址空間,空間的具體大小取決於體系結構。flat描述的是地址空間範圍是一個獨立的連續區間。通常情況下,每個進程都有惟一的這種flat地址空間,進程地址空間之間彼此互不相干。兩個不同的進程可以在鴿子地址空間的相同地址內存放不同的數據。進程之間也可以選擇共享地址空間,稱這樣的進程爲線程。
內存地址是一個給定的值,它要在地址空間範圍之內。在地址空間中,我們更爲關心的是進程有權訪問的虛擬內存地址區間,這些可被訪問的合法地址區間被稱爲內存區域(memory area),通過內核,進程可以給自己的地址空間動態地添加或減少內存區域。
進程只能訪問有效範圍內的內存地址。每個內存區域也具有相應進程必須遵循的特定訪問屬性,如果一個進程訪問了不在有效範圍中的地址,或以不正確的方式訪問有效地址,那麼內核就會終止該進程,並返回“段錯誤”信息。
內存區域可以包含各種內存對象,比如:
1. 可執行文件代碼的內存映射,稱爲代碼段(text section)
2. 可執行文件的已初始化全局變量的內存映射,稱爲數據段(data section)
3. 包含未初始化全局變量,也就是bss段的零頁(頁面中的信息全部爲0值,可用於映射bss段等目的)的內存映射。
術語“BSS”是block started by symbol的縮寫。因爲未初始化的變量沒有對應的值,所以不需要存放在可執行對象中。但是因爲C標準強制規定未初始化的全局變量要被賦予特殊的默認值,所以內核要將未賦值的變量從可執行代碼載入到內存中,然後將零頁映射到該片內存上,於是這些未初始化的變量就被賦予了0值,這樣避免了在目標文件中顯式地進行初始化,減少空間浪費。
4. 用於進程用戶空間棧(不要和進程內核棧混淆,進程的內核棧獨立存在並由內核維護)的零頁的內存映射。
5. 每一個諸如C庫或動態連接程序等共享庫的代碼段,數據段和bss也會被載入進程的地址空間。
6. 任何內存映射文件
7. 任何共享內存段
8. 任何匿名的內存映射,比如由malloc()分配的內存。
進程地址空間中的任何有效地址都只能位於惟一的區域。這些內存區域不能相互覆蓋。在執行的進程中,每個不同的內存片段都對應一個獨立的內存區域:棧、對象代碼、全局變量、被映射的文件等。
- 內存描述符
內核使用內存描述符結構體表示進程的地址空間,該結構包含了和進程地址空間相關的全部信息:
內核同時使用mm_count和mm_users這兩個計數器是爲了區別主使用計數器(mm_count)和使用該地址空間的進程數目(mm_users)。
mmap和mm_rb這兩個不同的數據結構體描述的對象是相同的:該地址空間中的全部內存區域。mmap是以鏈表形式存放的,這樣利於簡單高效地遍歷所有元素;而mm_rb以紅黑樹形式存放,適合搜索指定元素。
所有mm_struct結構體通過自身的mmlist域連接成一個雙向鏈表中,首元素是init_mm內存描述符,它代表init進程的地址空間。操作該鏈表的時候,需要使用mmlist_lock鎖(定義在kernel/fork.c中)來防止併發訪問。
內存描述符的總數存放在mmlist_nr全局變量(定義在kernel/fork.c中)中。
分配內存描述符進程的進程描述符中,mm域存放着該進程使用的內存描述符,所以current->mm便指向當前進程的內存描述符。
fork()函數利於copy_mm()函數複製父進程的內存描述符,就是將current->mm域給子進程,子進程中的mm_struct結構實際上是通過文件kernel/fork.c中的allocate_mm()宏從mm_cachep slab緩存中分配得到的。通常,每個進程都有唯一的mm_struct結構體,即唯一的進程地址空間。
如果父進程希望和子進程共享地址空間,可以在調用clone()時,設置CLONE_VM標誌。我們把這樣的進程稱爲線程。
- /* SLAB cache for mm_struct structures (tsk->mm) */
- static struct kmem_cache *mm_cachep;
- #define allocate_mm() (kmem_cache_alloc(mm_cachep, GFP_KERNEL))
- static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
- {
- struct mm_struct * mm, *oldmm;
- int retval;
- tsk->min_flt = tsk->maj_flt = 0;
- tsk->nvcsw = tsk->nivcsw = 0;
- tsk->mm = NULL;
- tsk->active_mm = NULL;
- /*
- * Are we cloning a kernel thread?
- *
- * We need to steal a active VM for that..
- */
- oldmm = current->mm;
- if (!oldmm)
- return 0;
- if (clone_flags & CLONE_VM) {
- atomic_inc(&oldmm->mm_users);
- mm = oldmm;
- goto good_mm;
- }
- retval = -ENOMEM;
- mm = dup_mm(tsk);
- if (!mm)
- goto fail_nomem;
- good_mm:
- /* Initializing for Swap token stuff */
- mm->token_priority = 0;
- mm->last_interval = 0;
- tsk->mm = mm;
- tsk->active_mm = mm;
- return 0;
- fail_nomem:
- return retval;
- }
當進程退出時,內核會調用exit_mm()函數,該函數執行一些常規的銷燬工作,同時更新一些統計量。
mm_struct與內核線程
內核線程沒有進程地址空間,也沒有相關的內存描述符。所以內核線程對應的進程描述符中mm域爲空。這正式內核線程的真正含義,它們沒有用戶上下文。
內核線程並不需要訪問任何用戶空間的內存,以爲內核線程在用戶空間沒有任何頁,所以它們並不需要有自己的內存描述符和頁表。儘管如此,即使訪問內核內存,內核線程餓還是需要使用一些數據的,比如頁表。爲了避免內核線程爲內存描述符和頁表浪費內存,也爲了當新內核線程運行時,避免浪費處理器週期向新地址空間進行切換,內核系拿出將直接使用前一個進程的內存描述符。
當一個進程被調度時,該進程的mm域執行的地址空間被裝載到內存,進程描述符中的active_mm域會被更新,指向新的地址空間。內核線程沒有地址空間,所以mm爲空。當內核線程被調度時,內核發現它的mm爲空,就會保留前一個進程的地址空間,隨後內核更新內核線程對應的進程描述符中的active_mm域,使其指向前一個進程的內存描述符,在需要時,內核線程可以使用前一個進程的頁表。因爲內核線程不訪問用戶空間的內存,所以它們僅僅使用地址空間中的和內核內存相關的信息。