前邊我已經說過了核心是如何管理實體記憶體。但事實是核心是作業系統的核心,不光管理本身的記憶體,還要管理進程的地址空間。linux作業系統採用虛擬記憶體技術,所有進程之間以虛擬方式共用記憶體。進程地址空間由每個進程中的線性地址區組成,而且更為重要的特點是核心允許進程使用該空間中的地址。通常情況況下,每個進程都有唯一的地址空間,而且進程地址空間之間彼此互不相干。但是進程之間也可以選擇共用地址空間,這樣的進程就叫做線程。
核心使用記憶體描述符結構表示進程的地址空間,由結構體mm_struct結構體表示,定義在linux/sched.h中,如下:
struct mm_struct { struct vm_area_struct *mmap; /* list of memory areas */ struct rb_root mm_rb; /* red-black tree of VMAs */ struct vm_area_struct *mmap_cache; /* last used memory area */ unsigned long free_area_cache; /* 1st address space hole */ pgd_t *pgd; /* page global directory */ atomic_t mm_users; /* address space users */ atomic_t mm_count; /* primary usage counter */ int map_count; /* number of memory areas */ struct rw_semaphore mmap_sem; /* memory area semaphore */ spinlock_t page_table_lock; /* page table lock */ struct list_head mmlist; /* list of all mm_structs */ unsigned long start_code; /* start address of code */ unsigned long end_code; /* final address of code */ unsigned long start_data; /* start address of data */ unsigned long end_data; /* final address of data */ unsigned long start_brk; /* start address of heap */ unsigned long brk; /* final address of heap */ unsigned long start_stack; /* start address of stack */ unsigned long arg_start; /* start of arguments */ unsigned long arg_end; /* end of arguments */ unsigned long env_start; /* start of environment */ unsigned long env_end; /* end of environment */ unsigned long rss; /* pages allocated */ unsigned long total_vm; /* total number of pages */ unsigned long locked_vm; /* number of locked pages */ unsigned long def_flags; /* default access flags */ unsigned long cpu_vm_mask; /* lazy TLB switch mask */ unsigned long swap_address; /* last scanned address */ unsigned dumpable:1; /* can this mm core dump? */ int used_hugetlb; /* used hugetlb pages? */ mm_context_t context; /* arch-specific data */ int core_waiters; /* thread core dump waiters */ struct completion *core_startup_done; /* core start completion */ struct completion core_done; /* core end completion */ rwlock_t ioctx_list_lock; /* AIO I/O list lock */ struct kioctx *ioctx_list; /* AIO I/O list */ struct kioctx default_kioctx; /* AIO default I/O context */};
mm_users記錄了正在使用該地址的進程數目(比如有兩個進程在使用,那就為2)。mm_count是該結構的主引用計數,只要mm_users不為0,它就為1。但其為0時,後者就為0。這時也就說明再也沒有指向該mm_struct結構體的引用了,這時該結構體會被銷毀。核心之所以同時使用這兩個計數器是為了區別主使用計數器和使用該地址空間的進程的數目。mmap和mm_rb描述的都是同一個對象:該地址空間中的全部記憶體地區。不同只是前者以鏈表,後者以紅/黑樹狀結構的形式組織。所有的mm_struct結構體都通過自身的mmlist域串連在一個雙向鏈表中,該鏈表的首元素是init_mm記憶體描述符,它代表init進程的地址空間。另外需要注意,操作該鏈表的時候需要使用mmlist_lock鎖來防止並發訪問,該鎖定義在檔案kernel/fork.c中。記憶體描述符的總數在mmlist_nr全域變數中,該變數也定義在檔案fork.c中。
我前邊說過的進程描述符中有一個mm域,這裡邊存放的就是該進程使用的記憶體描述符,通過current->mm便可以指向當前進程的記憶體描述符。fork函數利用copy_mm()函數就實現了複製父進程的記憶體描述符,而子進程中的mm_struct結構體實際是通過檔案kernel/fork.c中的allocate_mm()宏從mm_cachep slab緩衝中分配得到的。通常,每個進程都有唯一的mm_struct結構體。
前邊也說過,在linux中,進程和線程其實是一樣的,唯一的不同點就是是否共用這裡的地址空間。這個可以通過CLONE_VM標誌來實現。linux核心並不區別對待它們,線程對核心來說僅僅是一個共向特定資源的進程而已。好了,如果你設定這個標誌了,似乎很多問題都解決了。不再要allocate_mm函數了,前邊剛說作用。而且在copy_mm()函數中將mm域指向其父進程的記憶體描述符就可以了,如下:
if (clone_flags & CLONE_VM) { /* * current is the parent process and * tsk is the child process during a fork() */ atomic_inc(¤t->mm->mm_users); tsk->mm = current->mm;}
最後,當進程退出的時候,核心調用exit_mm()函數,這個函數調用mmput()來減少記憶體描述符中的mm_users使用者計數。如果計數降為0,繼續調用mmdrop函數,減少mm_count使用計數。如果使用計數也為0,則調用free_mm()宏通過kmem_cache_free()函數將mm_struct結構體歸還到mm_cachep slab緩衝中。
但對於核心而言,核心線程沒有進程地址空間,也沒有相關的記憶體描述符,核心線程對應的進程描述符中mm域也為空白。但核心線程還是需要使用一些資料的,比如頁表,為了避免核心線程為記憶體描述符和頁表浪費記憶體,也為了當新核心線程運行時,避免浪費處理器周期向新地址空間進行切換,核心線程將直接使用前一個進程的記憶體描述符。回憶一下我剛說的進程調度問題,當一個進程被調度時,進程結構體中mm域指向的地址空間會被裝載到記憶體,進程描述符中的active_mm域會被更新,指向新的地址空間。但我們這裡的核心是沒有mm域(為空白),所以,當一個核心線程被調度時,核心發現它的mm域為NULL,就會保留前一個進程的地址空間,隨後核心更新核心線程對應的進程描述符中的active域,使其指向前一個進程的記憶體描述符。所以在需要的時候,核心線程便可以使用前一個進程的頁表。因為核心線程不妨問使用者空間的記憶體,所以它們僅僅使用地址空間中和核心記憶體相關的資訊,這些資訊的含義和普通進程完全相同。
記憶體地區由vm_area_struct結構體描述,定義在linux/mm.h中,記憶體地區在核心中也經常被稱作虛擬記憶體地區或VMA.它描述了指定地址空間內連續區間上的一個獨立記憶體範圍。核心將每個記憶體地區作為一個單獨的記憶體對象管理,每個記憶體地區都擁有一致的屬性。結構體如下:
struct vm_area_struct { struct mm_struct *vm_mm; /* associated mm_struct */ unsigned long vm_start; /* VMA start, inclusive */ unsigned long vm_end; /* VMA end , exclusive */ struct vm_area_struct *vm_next; /* list of VMA's */ pgprot_t vm_page_prot; /* access permissions */ unsigned long vm_flags; /* flags */ struct rb_node vm_rb; /* VMA's node in the tree */ union { /* links to address_space->i_mmap or i_mmap_nonlinear */ struct { struct list_head list; void *parent; struct vm_area_struct *head; } vm_set; struct prio_tree_node prio_tree_node; } shared; struct list_head anon_vma_node; /* anon_vma entry */ struct anon_vma *anon_vma; /* anonymous VMA object */ struct vm_operations_struct *vm_ops; /* associated ops */ unsigned long vm_pgoff; /* offset within file */ struct file *vm_file; /* mapped file, if any */ void *vm_private_data; /* private data */};
每個記憶體描述符都對應於地址進程空間中的唯一區間。vm_mm域指向和VMA相關的mm_struct結構體。兩個獨立的進程將同一個檔案對應到各自的地址空間,它們分別都會有一個vm_area_struct結構體來標誌自己的記憶體地區;但是如果兩個線程共用一個地址空間,那麼它們也同時共用其中的所有vm_area_struct結構體。
在上面的vm_flags域中存放的是VMA標誌,標誌了記憶體地區所包含的頁面的行為和資訊,反映了核心處理頁面所需要遵循的管理辦法,如下表下述:
上表已經相當詳細了,而且給出了說明,我就不說了。在vm_area_struct結構體中的vm_ops域指向域指定記憶體地區相關的操作函數表,核心使用表中的方法操作VMA。vm_area_struct作為通用對象代表了任何類型的記憶體地區,而動作表描述針對特定的對象執行個體的特定方法。操作函數表由vm_operations_struct結構體表示,定義在linux/mm.h中,如下:
struct vm_operations_struct { void (*open) (struct vm_area_struct *); void (*close) (struct vm_area_struct *); struct page * (*nopage) (struct vm_area_struct *, unsigned long, int); int (*populate) (struct vm_area_struct *, unsigned long, unsigned long,pgprot_t, unsigned long, int);};
open:當指定的記憶體地區被加入到一個地址空間時,該函數被調用。 close:當指定的記憶體地區從地址空間刪除時,該函數被調用。 nopages:當要訪問的頁不在實體記憶體中時,該函數被頁錯誤處理程式調用。 populate:該函數被系統調用remap_pages調用來為將要發生的缺頁中斷預映射一個新映射。 |
記性好的你一定記得記憶體描述符中的mmap和mm_rb域都獨立地指向與記憶體描述符相關的全體記憶體地區對象。它們包含完全相同的vm_area_struct結構體的指標,僅僅組織方式不同而已。前者以鏈表的方式進行組織,所有的地區按地址增長的方向排序,mmap域指向鏈表中第一個記憶體地區,鏈中最後一個VMA結構體指標指向空。而mm_rb域採用紅--黑樹串連所有的記憶體地區對象。它指向紅--黑輸的根節點。地址空間中每一個vm_area_struct結構體通過自身的vm_rb域串連到樹中。關於紅黑二叉樹結構我就不細講了,以後可能會詳細說這個問題。核心之所以採用這兩種結構來表示同一記憶體地區,主要是鏈表結構便於遍曆所有節點,而紅/黑樹狀結構結構體便於在地址空間中定位特定記憶體地區的節點。我麼可以使用/proc檔案系統和pmap工具查看給定進程的記憶體空間和其中所包含的記憶體地區。這裡就不細說了。
核心也為我們提供了對記憶體地區操作的API,定義在linux/mm.h中:
(1)find_vma<定義在mm/mmap.c>中,該函數在指定的地址空間中搜尋一個vm_end大於addr的記憶體地區。換句話說,該函數尋找第一個包含 addr或者首地址大於addr的記憶體地區,如果沒有發現這樣的地區,該函數返回NULL;否則返回指向匹配的記憶體地區的vm_area_struct結構 體指標。 (2)find_vma_prev().函數定義和聲明分別在檔案mm/mmap.c中和檔案linux/mm.h中,它和find_vma()工作方式相同,但返回的是第一個小於 addr的VMA. (3)find_vma_intersection().定義在檔案linux/mm.h中,返回第一個和指定地址區間相交的VMA,該函數是一個內斂函數。 |
接下來要說的兩個函數就非常重要了,它們負責建立和刪除地址空間。
核心使用do_mmap()函數建立一個新的線性地址空間。但如果建立的地址區間和一個已經存在的地址區間相鄰,並且它們具有相同的存取權限的話,那麼兩個區間將合并為一個。如果不能合并,那麼就確實需要建立一個新的vma了,但無論哪種情況,do_mmap()函數都會將一個地址區間加入到進程的地址空間中。這個函數定義在linux/mm.h中,如下:
unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot,unsigned long flag, unsigned long offset)
這個函數中由file指定檔案,具體映射的是檔案中從位移offset處開始,長度為len位元組的範圍內的資料,如果file參數是NULL並且offset參數也是0,那麼就代表這次映射沒有和檔案相關,該情況被稱作匿名映射。如果指定了檔案和位移量,那麼該映射被稱為檔案對應(file-backed mapping),其中參數prot指定記憶體地區中頁面的存取權限,這些存取權限定義在asm/mman.h中,如下:
flag參數指定了VMA標誌,這些標誌定義在asm/mman.h中,如下:
如果系統調用do_mmap的參數中有無效參數,那麼它返回一個負值;否則,它會在虛擬記憶體中分配一個合適的新記憶體地區,如果有可能的話,將新地區和臨近地區進行合并,否則核心從vm_area_cach
ep長位元組緩衝中分配一個vm_area_struct結構體,並且使用vma_link()函數將新分配的記憶體地區添加到地址空間的記憶體地區鏈表和紅/黑樹狀結構中,隨後還要更新記憶體描述符中的total_vm域,然後才返回新分配的地址區間的初始地址。在使用者空間,我們可以通過mmap()系統調用擷取核心功能do_mmap()的功能,這個在unix環境進階編程中講的很詳細,我就不好意思繼續說了。我們繼續往下走。
我們說既然有了建立,當然要有刪除了,是不?do_mummp()函數就是幹這事的。它從特定的進程地址空間中刪除指定地址空間,該函數定義在檔案linux/mm.h中,如下:
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
第一個參數指定要刪除地區所在的地址空間,刪除從地址start開始,長度為len位元組的地址空間,如果成功,返回0,否則返回負的錯誤碼。與之相對應的使用者空間系統調用是munmap。
下面開始最後一點內容:頁表
我們知道應用程式操作的對象是映射到實體記憶體之上的虛擬記憶體,但是處理器直接操作的確實實體記憶體。所以當應用程式訪問一個虛擬位址時,首先必須將虛擬位址轉化為物理地址,然後處理器才能解析地址訪問請求。這個轉換工作需要通過查詢頁面才能完成,概括地講,地址轉換需要將虛擬位址分段,使每段虛地址都作為一個索引指向頁表,而頁表項則指向下一層級的頁表或者指向最終的物理頁面。linux中使用三級頁表完成地址轉換。多數體繫結構中,搜尋網頁表的工作由硬體完成,下表描述了虛擬位址通過頁表找到物理地址的過程:
在上面這個圖中,頂級頁表是頁全域目錄(PGD),二級頁表是中間頁目錄(PMD).最後一級是頁表(PTE),該頁表結構指向物理頁。中的頁表對應的結構體定義在檔案asm/page.h中。為了加快尋找速度,在linux中實現了快表(TLB),其本質是一個緩衝器,作為一個將虛擬位址映射到物理地址的硬體緩衝,當請求訪問一個虛擬位址時,處理器將首先檢查TLB中是否緩衝了該虛擬位址到物理地址的映射,如果找到了,物理地址就立刻返回,否則,就需要再通過頁表搜尋需要的物理地址。