前邊我已經(jīng)說(shuō)過(guò)了內核是如何管理物理內存。但事實(shí)是內核是操作系統的核心,不光管理本身的內存,還要管理進(jìn)程的地址空間。linux操作系統采用虛擬內存技術(shù),所有進(jìn)程之間以虛擬方式共享內存。進(jìn)程地址空間由每個(gè)進(jìn)程中的線(xiàn)性地址區組成,而且更為重要的特點(diǎn)是內核允許進(jìn)程使用該空間中的地址。通常情況況下,每個(gè)進(jìn)程都有唯一的地址空間,而且進(jìn)程地址空間之間彼此互不相干。但是進(jìn)程之間也可以選擇共享地址空間,這樣的進(jìn)程就叫做線(xiàn)程。
內核使用內存描述符結構表示進(jìn)程的地址空間,由結構體mm_struct結構體表示,定義在linux/sched.h中,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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記錄了正在使用該地址的進(jìn)程數目(比如有兩個(gè)進(jìn)程在使用,那就為2)。mm_count是該結構的主引用計數,只要mm_users不為0,它就為1。但其為0時(shí),后者就為0。這時(shí)也就說(shuō)明再也沒(méi)有指向該mm_struct結構體的引用了,這時(shí)該結構體會(huì )被銷(xiāo)毀。內核之所以同時(shí)使用這兩個(gè)計數器是為了區別主使用計數器和使用該地址空間的進(jìn)程的數目。mmap和mm_rb描述的都是同一個(gè)對象:該地址空間中的全部?jì)却鎱^域。不同只是前者以鏈表,后者以紅黑樹(shù)的形式組織。所有的mm_struct結構體都通過(guò)自身的mmlist域連接在一個(gè)雙向鏈表中,該鏈表的首元素是init_mm內存描述符,它代表init進(jìn)程的地址空間。另外需要注意,操作該鏈表的時(shí)候需要使用mmlist_lock鎖來(lái)防止并發(fā)訪(fǎng)問(wèn),該鎖定義在文件kernel/fork.c中。內存描述符的總數在mmlist_nr全局變量中,該變量也定義在文件fork.c中。
我前邊說(shuō)過(guò)的進(jìn)程描述符中有一個(gè)mm域,這里邊存放的就是該進(jìn)程使用的內存描述符,通過(guò)current->mm便可以指向當前進(jìn)程的內存描述符。fork函數利用copy_mm()函數就實(shí)現了復制父進(jìn)程的內存描述符,而子進(jìn)程中的mm_struct結構體實(shí)際是通過(guò)文件kernel/fork.c中的allocate_mm()宏從mm_cachep slab緩存中分配得到的。通常,每個(gè)進(jìn)程都有唯一的mm_struct結構體。
前邊也說(shuō)過(guò),在linux中,進(jìn)程和線(xiàn)程其實(shí)是一樣的,唯一的不同點(diǎn)就是是否共享這里的地址空間。這個(gè)可以通過(guò)CLONE_VM標志來(lái)實(shí)現。linux內核并不區別對待它們,線(xiàn)程對內核來(lái)說(shuō)僅僅是一個(gè)共向特定資源的進(jìn)程而已。好了,如果你設置這個(gè)標志了,似乎很多問(wèn)題都解決了。不再要allocate_mm函數了,前邊剛說(shuō)作用。而且在copy_mm()函數中將mm域指向其父進(jìn)程的內存描述符就可以了,如下:
1
2
3
4
5
6
7
8
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;
}
最后,當進(jìn)程退出的時(shí)候,內核調用exit_mm()函數,這個(gè)函數調用mmput()來(lái)減少內存描述符中的mm_users用戶(hù)計數。如果計數降為0,繼續調用mmdrop函數,減少mm_count使用計數。如果使用計數也為0,則調用free_mm()宏通過(guò)kmem_cache_free()函數將mm_struct結構體歸還到mm_cachep slab緩存中。
但對于內核而言,內核線(xiàn)程沒(méi)有進(jìn)程地址空間,也沒(méi)有相關(guān)的內存描述符,內核線(xiàn)程對應的進(jìn)程描述符中mm域也為空。但內核線(xiàn)程還是需要使用一些數據的,比如頁(yè)表,為了避免內核線(xiàn)程為內存描述符和頁(yè)表浪費內存,也為了當新內核線(xiàn)程運行時(shí),避免浪費處理器周期向新地址空間進(jìn)行切換,內核線(xiàn)程將直接使用前一個(gè)進(jìn)程的內存描述符?;貞浺幌挛覄傉f(shuō)的進(jìn)程調度問(wèn)題,當一個(gè)進(jìn)程被調度時(shí),進(jìn)程結構體中mm域指向的地址空間會(huì )被裝載到內存,進(jìn)程描述符中的active_mm域會(huì )被更新,指向新的地址空間。但我們這里的內核是沒(méi)有mm域(為空),所以,當一個(gè)內核線(xiàn)程被調度時(shí),內核發(fā)現它的mm域為NULL,就會(huì )保留前一個(gè)進(jìn)程的地址空間,隨后內核更新內核線(xiàn)程對應的進(jìn)程描述符中的active域,使其指向前一個(gè)進(jìn)程的內存描述符。所以在需要的時(shí)候,內核線(xiàn)程便可以使用前一個(gè)進(jìn)程的頁(yè)表。因為內核線(xiàn)程不妨問(wèn)用戶(hù)空間的內存,所以它們僅僅使用地址空間中和內核內存相關(guān)的信息,這些信息的含義和普通進(jìn)程完全相同。
內存區域由vm_area_struct結構體描述,定義在linux/mm.h中,內存區域在內核中也經(jīng)常被稱(chēng)作虛擬內存區域或VMA.它描述了指定地址空間內連續區間上的一個(gè)獨立內存范圍。內核將每個(gè)內存區域作為一個(gè)單獨的內存對象管理,每個(gè)內存區域都擁有一致的屬性。結構體如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 */
};
每個(gè)內存描述符都對應于地址進(jìn)程空間中的唯一區間。vm_mm域指向和VMA相關(guān)的mm_struct結構體。兩個(gè)獨立的進(jìn)程將同一個(gè)文件映射到各自的地址空間,它們分別都會(huì )有一個(gè)vm_area_struct結構體來(lái)標志自己的內存區域;但是如果兩個(gè)線(xiàn)程共享一個(gè)地址空間,那么它們也同時(shí)共享其中的所有vm_area_struct結構體。
在上面的vm_flags域中存放的是VMA標志,標志了內存區域所包含的頁(yè)面的行為和信息,反映了內核處理頁(yè)面所需要遵循的行為準則,如下表下述:
上表已經(jīng)相當詳細了,而且給出了說(shuō)明,我就不說(shuō)了。在vm_area_struct結構體中的vm_ops域指向域指定內存區域相關(guān)的操作函數表,內核使用表中的方法操作VMA。vm_area_struct作為通用對象代表了任何類(lèi)型的內存區域,而操作表描述針對特定的對象實(shí)例的特定方法。操作函數表由vm_operations_struct結構體表示,定義在linux/mm.h中,如下:
1
2
3
4
5
6
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:當指定的內存區域被加入到一個(gè)地址空間時(shí),該函數被調用。
close:當指定的內存區域從地址空間刪除時(shí),該函數被調用。
nopages:當要訪(fǎng)問(wèn)的頁(yè)不在物理內存中時(shí),該函數被頁(yè)錯誤處理程序調用。
populate:該函數被系統調用remap_pages調用來(lái)為將要發(fā)生的缺頁(yè)中斷預映射一個(gè)新映射。
記性好的你一定記得內存描述符中的mmap和mm_rb域都獨立地指向與內存描述符相關(guān)的全體內存區域對象。它們包含完全相同的vm_area_struct結構體的指針,僅僅組織方式不同而已。前者以鏈表的方式進(jìn)行組織,所有的區域按地址增長(cháng)的方向排序,mmap域指向鏈表中第一個(gè)內存區域,鏈中最后一個(gè)VMA結構體指針指向空。而mm_rb域采用紅--黑樹(shù)連接所有的內存區域對象。它指向紅--黑輸的根節點(diǎn)。地址空間中每一個(gè)vm_area_struct結構體通過(guò)自身的vm_rb域連接到樹(shù)中。關(guān)于紅黑二叉樹(shù)結構我就不細講了,以后可能會(huì )詳細說(shuō)這個(gè)問(wèn)題。內核之所以采用這兩種結構來(lái)表示同一內存區域,主要是鏈表結構便于遍歷所有節點(diǎn),而紅黑樹(shù)結構體便于在地址空間中定位特定內存區域的節點(diǎn)。我么可以使用/proc文件系統和pmap工具查看給定進(jìn)程的內存空間和其中所包含的內存區域。這里就不細說(shuō)了。
內核也為我們提供了對內存區域操作的API,定義在linux/mm.h中:
(1)find_vma<定義在mm/mmap.c>中,該函數在指定的地址空間中搜索一個(gè)vm_end大于addr的內存區域。換句話(huà)說(shuō),該函數尋找第一個(gè)包含
addr或者首地址大于addr的內存區域,如果沒(méi)有發(fā)現這樣的區域,該函數返回NULL;否則返回指向匹配的內存區域的vm_area_struct結構
體指針。
(2)find_vma_prev().函數定義和聲明分別在文件mm/mmap.c中和文件linux/mm.h中,它和find_vma()工作方式相同,但返回的是第一個(gè)小于
addr的VMA.
(3)find_vma_intersection().定義在文件linux/mm.h中,返回第一個(gè)和指定地址區間相交的VMA,該函數是一個(gè)內斂函數。
接下來(lái)要說(shuō)的兩個(gè)函數就非常重要了,它們負責創(chuàng )建和刪除地址空間。
內核使用do_mmap()函數創(chuàng )建一個(gè)新的線(xiàn)性地址空間。但如果創(chuàng )建的地址區間和一個(gè)已經(jīng)存在的地址區間相鄰,并且它們具有相同的訪(fǎng)問(wèn)權限的話(huà),那么兩個(gè)區間將合并為一個(gè)。如果不能合并,那么就確實(shí)需要創(chuàng )建一個(gè)新的vma了,但無(wú)論哪種情況,do_mmap()函數都會(huì )將一個(gè)地址區間加入到進(jìn)程的地址空間中。這個(gè)函數定義在linux/mm.h中,如下:
1
unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot,unsigned long flag, unsigned long offset)
這個(gè)函數中由file指定文件,具體映射的是文件中從偏移offset處開(kāi)始,長(cháng)度為len字節的范圍內的數據,如果file參數是NULL并且offset參數也是0,那么就代表這次映射沒(méi)有和文件相關(guān),該情況被稱(chēng)作匿名映射。如果指定了文件和偏移量,那么該映射被稱(chēng)為文件映射(file-backed mapping),其中參數prot指定內存區域中頁(yè)面的訪(fǎng)問(wèn)權限,這些訪(fǎng)問(wèn)權限定義在asm/mman.h中,如下:
flag參數指定了VMA標志,這些標志定義在asm/mman.h中,如下:
如果系統調用do_mmap的參數中有無(wú)效參數,那么它返回一個(gè)負值;否則,它會(huì )在虛擬內存中分配一個(gè)合適的新內存區域,如果有可能的話(huà),將新區域和臨近區域進(jìn)行合并,否則內核從vm_area_cach
ep長(cháng)字節緩存中分配一個(gè)vm_area_struct結構體,并且使用vma_link()函數將新分配的內存區域添加到地址空間的內存區域鏈表和紅黑樹(shù)中,隨后還要更新內存描述符中的total_vm域,然后才返回新分配的地址區間的初始地址。在用戶(hù)空間,我們可以通過(guò)mmap()系統調用獲取內核函數do_mmap()的功能,這個(gè)在unix環(huán)境高級編程中講的很詳細,我就不好意思繼續說(shuō)了。我們繼續往下走。
我們說(shuō)既然有了創(chuàng )建,當然要有刪除了,是不?do_mummp()函數就是干這事的。它從特定的進(jìn)程地址空間中刪除指定地址空間,該函數定義在文件linux/mm.h中,如下:
1
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
第一個(gè)參數指定要刪除區域所在的地址空間,刪除從地址start開(kāi)始,長(cháng)度為len字節的地址空間,如果成功,返回0,否則返回負的錯誤碼。與之相對應的用戶(hù)空間系統調用是munmap。
下面開(kāi)始最后一點(diǎn)內容:頁(yè)表
我們知道應用程序操作的對象是映射到物理內存之上的虛擬內存,但是處理器直接操作的確實(shí)物理內存。所以當應用程序訪(fǎng)問(wèn)一個(gè)虛擬地址時(shí),首先必須將虛擬地址轉化為物理地址,然后處理器才能解析地址訪(fǎng)問(wèn)請求。這個(gè)轉換工作需要通過(guò)查詢(xún)頁(yè)面才能完成,概括地講,地址轉換需要將虛擬地址分段,使每段虛地址都作為一個(gè)索引指向頁(yè)表,而頁(yè)表項則指向下一級別的頁(yè)表或者指向最終的物理頁(yè)面。linux中使用三級頁(yè)表完成地址轉換。多數體系結構中,搜索頁(yè)表的工作由硬件完成,下表描述了虛擬地址通過(guò)頁(yè)表找到物理地址的過(guò)程:
在上面這個(gè)圖中,頂級頁(yè)表是頁(yè)全局目錄(PGD),二級頁(yè)表是中間頁(yè)目錄(PMD).最后一級是頁(yè)表(PTE),該頁(yè)表結構指向物理頁(yè)。上圖中的頁(yè)表對應的結構體定義在文件asm/page.h中。為了加快查找速度,在linux中實(shí)現了快表(TLB),其本質(zhì)是一個(gè)緩沖器,作為一個(gè)將虛擬地址映射到物理地址的硬件緩存,當請求訪(fǎng)問(wèn)一個(gè)虛擬地址時(shí),處理器將首先檢查T(mén)LB中是否緩存了該虛擬地址到物理地址的映射,如果找到了,物理地址就立刻返回,否則,就需要再通過(guò)頁(yè)表搜索需要的物理地址。