內(nèi)核地址空間分布
直接映射區(qū):線性空間中從3G開始最大896M的區(qū)間,為直接內(nèi)存映射區(qū),該區(qū)域的線性地址和物理地址存在線性轉(zhuǎn)換關(guān)系:線性地址=3G+物理地址。
動態(tài)內(nèi)存映射區(qū):該區(qū)域由內(nèi)核函數(shù)VMALLOC來分配,特點是:線性空間連續(xù),但是對應(yīng)的物理空間不一定連續(xù)。vmalloc分配的線性地址所對應(yīng)的物理頁可能處于低端內(nèi)存,也可能處于高端內(nèi)存。
永久內(nèi)存映射區(qū):該區(qū)域可訪問高端內(nèi)存。訪問方法是使用alloc_page(_GFP_HIGHMEM)分配高端內(nèi)存頁或者使用kmap函數(shù)將分配到的高端內(nèi)存映射到該區(qū)域。
固定映射區(qū):該區(qū)域和4G的頂端只有4k的隔離帶,其每個地址項都服務(wù)于特定的用途,如ACPI_BASE等。
進(jìn)程的地址空間
linux采用虛擬內(nèi)存管理技術(shù),每一個進(jìn)程都有一個3G大小的獨立的進(jìn)程地址空間,這個地址空間就是用戶空間。每個進(jìn)程的用戶空間都是完全獨立、互不相干的。進(jìn)程訪問內(nèi)核空間的方式:系統(tǒng)調(diào)用和中斷。
創(chuàng)建進(jìn)程等進(jìn)程相關(guān)操作都需要分配內(nèi)存給進(jìn)程。這時進(jìn)程申請和獲得的不是物理地址,僅僅是虛擬地址。
實際的物理內(nèi)存只有當(dāng)進(jìn)程真的去訪問新獲取的虛擬地址時,才會由“請頁機(jī)制”產(chǎn)生“缺頁”異常,從而進(jìn)入分配實際頁框的程序。該異常是虛擬內(nèi)存機(jī)制賴以存在的基本保證,它會告訴內(nèi)核去為進(jìn)程分配物理頁,并建立對應(yīng)的頁表,這之后虛擬地址才實實在在的映射到了物理地址上。
vmalloc和kmalloc區(qū)別
1,kmalloc對應(yīng)于kfree,分配的內(nèi)存處于3GB~high_memory之間,這段內(nèi)核空間與物理內(nèi)存的映射一一對應(yīng),可以分配連續(xù)的物理內(nèi)存; vmalloc對應(yīng)于vfree,分配的內(nèi)存在VMALLOC_START~4GB之間,分配連續(xù)的虛擬內(nèi)存,但是物理上不一定連續(xù)。
2,vmalloc() 分配的物理地址無需連續(xù),而kmalloc() 確保頁在物理上是連續(xù)的
3,kmalloc分配內(nèi)存是基于slab,因此slab的一些特性包括著色,對齊等都具備,性能較好。物理地址和邏輯地址都是連續(xù)的。
4,最主要的區(qū)別是分配大小的問題,比如你需要28個字節(jié),那一定用kmalloc,如果用vmalloc,分配不多次機(jī)器就罷工了。
盡管僅僅在某些情況下才需要物理上連續(xù)的內(nèi)存塊,但是,很多內(nèi)核代碼都調(diào)用kmalloc(),而不是用vmalloc()獲得內(nèi)存。這主要是出于性能的考慮。vmalloc()函數(shù)為了把物理上不連續(xù)的頁面轉(zhuǎn)換為虛擬地址空間上連續(xù)的頁,必須專門建立頁表項。還有,通過 vmalloc()獲得的頁必須一個一個的進(jìn)行映射(因為它們物理上不是連續(xù)的),這就會導(dǎo)致比直接內(nèi)存映射大得多的緩沖區(qū)刷新。因為這些原因,vmalloc()僅在絕對必要時才會使用,最典型的就是為了獲得大塊內(nèi)存時,例如,當(dāng)模塊被動態(tài)插入到內(nèi)核中時,就把模塊裝載到由vmalloc()分配的內(nèi)存上。
進(jìn)程地址空間
前邊我已經(jīng)說過了內(nèi)核是如何管理物理內(nèi)存。但事實是內(nèi)核是操作系統(tǒng)的核心,不光管理本身的內(nèi)存,還要管理進(jìn)程的地址空間。linux操作系統(tǒng)采用虛擬內(nèi)存技術(shù),所有進(jìn)程之間以虛擬方式共享內(nèi)存。進(jìn)程地址空間由每個進(jìn)程中的線性地址區(qū)組成,而且更為重要的特點是內(nèi)核允許進(jìn)程使用該空間中的地址。通常情況況下,每個進(jìn)程都有唯一的地址空間,而且進(jìn)程地址空間之間彼此互不相干。但是進(jìn)程之間也可以選擇共享地址空間,這樣的進(jìn)程就叫做線程。
內(nèi)核使用內(nèi)存描述符結(jié)構(gòu)表示進(jìn)程的地址空間,由結(jié)構(gòu)體mm_struct結(jié)構(gòu)體表示,定義在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記錄了正在使用該地址的進(jìn)程數(shù)目(比如有兩個進(jìn)程在使用,那就為2)。mm_count是該結(jié)構(gòu)的主引用計數(shù),只要mm_users不為0,它就為1。但其為0時,后者就為0。這時也就說明再也沒有指向該mm_struct結(jié)構(gòu)體的引用了,這時該結(jié)構(gòu)體會被銷毀。內(nèi)核之所以同時使用這兩個計數(shù)器是為了區(qū)別主使用計數(shù)器和使用該地址空間的進(jìn)程的數(shù)目。mmap和mm_rb描述的都是同一個對象:該地址空間中的全部內(nèi)存區(qū)域。不同只是前者以鏈表,后者以紅黑樹的形式組織。所有的mm_struct結(jié)構(gòu)體都通過自身的mmlist域連接在一個雙向鏈表中,該鏈表的首元素是init_mm內(nèi)存描述符,它代表init進(jìn)程的地址空間。另外需要注意,操作該鏈表的時候需要使用mmlist_lock鎖來防止并發(fā)訪問,該鎖定義在文件kernel/fork.c中。內(nèi)存描述符的總數(shù)在mmlist_nr全局變量中,該變量也定義在文件fork.c中。
我前邊說過的進(jìn)程描述符中有一個mm域,這里邊存放的就是該進(jìn)程使用的內(nèi)存描述符,通過current->mm便可以指向當(dāng)前進(jìn)程的內(nèi)存描述符。fork函數(shù)利用copy_mm()函數(shù)就實現(xiàn)了復(fù)制父進(jìn)程的內(nèi)存描述符,而子進(jìn)程中的mm_struct結(jié)構(gòu)體實際是通過文件kernel/fork.c中的allocate_mm()宏從mm_cachep slab緩存中分配得到的。通常,每個進(jìn)程都有唯一的mm_struct結(jié)構(gòu)體。
前邊也說過,在linux中,進(jìn)程和線程其實是一樣的,唯一的不同點就是是否共享這里的地址空間。這個可以通過CLONE_VM標(biāo)志來實現(xiàn)。linux內(nèi)核并不區(qū)別對待它們,線程對內(nèi)核來說僅僅是一個共向特定資源的進(jìn)程而已。好了,如果你設(shè)置這個標(biāo)志了,似乎很多問題都解決了。不再要allocate_mm函數(shù)了,前邊剛說作用。而且在copy_mm()函數(shù)中將mm域指向其父進(jìn)程的內(nèi)存描述符就可以了,如下:
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; }
最后,當(dāng)進(jìn)程退出的時候,內(nèi)核調(diào)用exit_mm()函數(shù),這個函數(shù)調(diào)用mmput()來減少內(nèi)存描述符中的mm_users用戶計數(shù)。如果計數(shù)降為0,繼續(xù)調(diào)用mmdrop函數(shù),減少mm_count使用計數(shù)。如果使用計數(shù)也為0,則調(diào)用free_mm()宏通過kmem_cache_free()函數(shù)將mm_struct結(jié)構(gòu)體歸還到mm_cachep slab緩存中。
但對于內(nèi)核而言,內(nèi)核線程沒有進(jìn)程地址空間,也沒有相關(guān)的內(nèi)存描述符,內(nèi)核線程對應(yīng)的進(jìn)程描述符中mm域也為空。但內(nèi)核線程還是需要使用一些數(shù)據(jù)的,比如頁表,為了避免內(nèi)核線程為內(nèi)存描述符和頁表浪費內(nèi)存,也為了當(dāng)新內(nèi)核線程運行時,避免浪費處理器周期向新地址空間進(jìn)行切換,內(nèi)核線程將直接使用前一個進(jìn)程的內(nèi)存描述符。回憶一下我剛說的進(jìn)程調(diào)度問題,當(dāng)一個進(jìn)程被調(diào)度時,進(jìn)程結(jié)構(gòu)體中mm域指向的地址空間會被裝載到內(nèi)存,進(jìn)程描述符中的active_mm域會被更新,指向新的地址空間。但我們這里的內(nèi)核是沒有mm域(為空),所以,當(dāng)一個內(nèi)核線程被調(diào)度時,內(nèi)核發(fā)現(xiàn)它的mm域為NULL,就會保留前一個進(jìn)程的地址空間,隨后內(nèi)核更新內(nèi)核線程對應(yīng)的進(jìn)程描述符中的active域,使其指向前一個進(jìn)程的內(nèi)存描述符。所以在需要的時候,內(nèi)核線程便可以使用前一個進(jìn)程的頁表。因為內(nèi)核線程不妨問用戶空間的內(nèi)存,所以它們僅僅使用地址空間中和內(nèi)核內(nèi)存相關(guān)的信息,這些信息的含義和普通進(jìn)程完全相同。
內(nèi)存區(qū)域由vm_area_struct結(jié)構(gòu)體描述,定義在linux/mm.h中,內(nèi)存區(qū)域在內(nèi)核中也經(jīng)常被稱作虛擬內(nèi)存區(qū)域或VMA.它描述了指定地址空間內(nèi)連續(xù)區(qū)間上的一個獨立內(nèi)存范圍。內(nèi)核將每個內(nèi)存區(qū)域作為一個單獨的內(nèi)存對象管理,每個內(nèi)存區(qū)域都擁有一致的屬性。結(jié)構(gòu)體如下:
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 */ };
每個內(nèi)存描述符都對應(yīng)于地址進(jìn)程空間中的唯一區(qū)間。vm_mm域指向和VMA相關(guān)的mm_struct結(jié)構(gòu)體。兩個獨立的進(jìn)程將同一個文件映射到各自的地址空間,它們分別都會有一個vm_area_struct結(jié)構(gòu)體來標(biāo)志自己的內(nèi)存區(qū)域;但是如果兩個線程共享一個地址空間,那么它們也同時共享其中的所有vm_area_struct結(jié)構(gòu)體。
在上面的vm_flags域中存放的是VMA標(biāo)志,標(biāo)志了內(nèi)存區(qū)域所包含的頁面的行為和信息,反映了內(nèi)核處理頁面所需要遵循的行為準(zhǔn)則,如下表下述:
上表已經(jīng)相當(dāng)詳細(xì)了,而且給出了說明,我就不說了。在vm_area_struct結(jié)構(gòu)體中的vm_ops域指向域指定內(nèi)存區(qū)域相關(guān)的操作函數(shù)表,內(nèi)核使用表中的方法操作VMA。vm_area_struct作為通用對象代表了任何類型的內(nèi)存區(qū)域,而操作表描述針對特定的對象實例的特定方法。操作函數(shù)表由vm_operations_struct結(jié)構(gòu)體表示,定義在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:當(dāng)指定的內(nèi)存區(qū)域被加入到一個地址空間時,該函數(shù)被調(diào)用。 close:當(dāng)指定的內(nèi)存區(qū)域從地址空間刪除時,該函數(shù)被調(diào)用。 nopages:當(dāng)要訪問的頁不在物理內(nèi)存中時,該函數(shù)被頁錯誤處理程序調(diào)用。 populate:該函數(shù)被系統(tǒng)調(diào)用remap_pages調(diào)用來為將要發(fā)生的缺頁中斷預(yù)映射一個新映射。
記性好的你一定記得內(nèi)存描述符中的mmap和mm_rb域都獨立地指向與內(nèi)存描述符相關(guān)的全體內(nèi)存區(qū)域?qū)ο蟆K鼈儼耆嗤膙m_area_struct結(jié)構(gòu)體的指針,僅僅組織方式不同而已。前者以鏈表的方式進(jìn)行組織,所有的區(qū)域按地址增長的方向排序,mmap域指向鏈表中第一個內(nèi)存區(qū)域,鏈中最后一個VMA結(jié)構(gòu)體指針指向空。而mm_rb域采用紅--黑樹連接所有的內(nèi)存區(qū)域?qū)ο蟆K赶蚣t--黑輸?shù)母?jié)點。地址空間中每一個vm_area_struct結(jié)構(gòu)體通過自身的vm_rb域連接到樹中。關(guān)于紅黑二叉樹結(jié)構(gòu)我就不細(xì)講了,以后可能會詳細(xì)說這個問題。內(nèi)核之所以采用這兩種結(jié)構(gòu)來表示同一內(nèi)存區(qū)域,主要是鏈表結(jié)構(gòu)便于遍歷所有節(jié)點,而紅黑樹結(jié)構(gòu)體便于在地址空間中定位特定內(nèi)存區(qū)域的節(jié)點。我么可以使用/proc文件系統(tǒng)和pmap工具查看給定進(jìn)程的內(nèi)存空間和其中所包含的內(nèi)存區(qū)域。這里就不細(xì)說了。
內(nèi)核也為我們提供了對內(nèi)存區(qū)域操作的API,定義在linux/mm.h中:
記性好的你一定記得內(nèi)存描述符中的mmap和mm_rb域都獨立地指向與內(nèi)存描述符相關(guān)的全體內(nèi)存區(qū)域?qū)ο蟆K鼈儼耆嗤膙m_area_struct結(jié)構(gòu)體的指針,僅僅組織方式不同而已。前者以鏈表的方式進(jìn)行組織,所有的區(qū)域按地址增長的方向排序,mmap域指向鏈表中第一個內(nèi)存區(qū)域,鏈中最后一個VMA結(jié)構(gòu)體指針指向空。而mm_rb域采用紅--黑樹連接所有的內(nèi)存區(qū)域?qū)ο蟆K赶蚣t--黑輸?shù)母?jié)點。地址空間中每一個vm_area_struct結(jié)構(gòu)體通過自身的vm_rb域連接到樹中。關(guān)于紅黑二叉樹結(jié)構(gòu)我就不細(xì)講了,以后可能會詳細(xì)說這個問題。內(nèi)核之所以采用這兩種結(jié)構(gòu)來表示同一內(nèi)存區(qū)域,主要是鏈表結(jié)構(gòu)便于遍歷所有節(jié)點,而紅黑樹結(jié)構(gòu)體便于在地址空間中定位特定內(nèi)存區(qū)域的節(jié)點。我么可以使用/proc文件系統(tǒng)和pmap工具查看給定進(jìn)程的內(nèi)存空間和其中所包含的內(nèi)存區(qū)域。這里就不細(xì)說了。 內(nèi)核也為我們提供了對內(nèi)存區(qū)域操作的API,定義在linux/mm.h中:
接下來要說的兩個函數(shù)就非常重要了,它們負(fù)責(zé)創(chuàng)建和刪除地址空間。
內(nèi)核使用do_mmap()函數(shù)創(chuàng)建一個新的線性地址空間。但如果創(chuàng)建的地址區(qū)間和一個已經(jīng)存在的地址區(qū)間相鄰,并且它們具有相同的訪問權(quán)限的話,那么兩個區(qū)間將合并為一個。如果不能合并,那么就確實需要創(chuàng)建一個新的vma了,但無論哪種情況,do_mmap()函數(shù)都會將一個地址區(qū)間加入到進(jìn)程的地址空間中。這個函數(shù)定義在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)
這個函數(shù)中由file指定文件,具體映射的是文件中從偏移offset處開始,長度為len字節(jié)的范圍內(nèi)的數(shù)據(jù),如果file參數(shù)是NULL并且offset參數(shù)也是0,那么就代表這次映射沒有和文件相關(guān),該情況被稱作匿名映射。如果指定了文件和偏移量,那么該映射被稱為文件映射(file-backed mapping),其中參數(shù)prot指定內(nèi)存區(qū)域中頁面的訪問權(quán)限,這些訪問權(quán)限定義在asm/mman.h中,如下:
flag參數(shù)指定了VMA標(biāo)志,這些標(biāo)志定義在asm/mman.h中,如下:
如果系統(tǒng)調(diào)用do_mmap的參數(shù)中有無效參數(shù),那么它返回一個負(fù)值;否則,它會在虛擬內(nèi)存中分配一個合適的新內(nèi)存區(qū)域,如果有可能的話,將新區(qū)域和臨近區(qū)域進(jìn)行合并,否則內(nèi)核從vm_area_cach
ep長字節(jié)緩存中分配一個vm_area_struct結(jié)構(gòu)體,并且使用vma_link()函數(shù)將新分配的內(nèi)存區(qū)域添加到地址空間的內(nèi)存區(qū)域鏈表和紅黑樹中,隨后還要更新內(nèi)存描述符中的total_vm域,然后才返回新分配的地址區(qū)間的初始地址。在用戶空間,我們可以通過mmap()系統(tǒng)調(diào)用獲取內(nèi)核函數(shù)do_mmap()的功能,這個在unix環(huán)境高級編程中講的很詳細(xì),我就不好意思繼續(xù)說了。我們繼續(xù)往下走。
我們說既然有了創(chuàng)建,當(dāng)然要有刪除了,是不?do_mummp()函數(shù)就是干這事的。它從特定的進(jìn)程地址空間中刪除指定地址空間,該函數(shù)定義在文件linux/mm.h中,如下:
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
第一個參數(shù)指定要刪除區(qū)域所在的地址空間,刪除從地址start開始,長度為len字節(jié)的地址空間,如果成功,返回0,否則返回負(fù)的錯誤碼。與之相對應(yīng)的用戶空間系統(tǒng)調(diào)用是munmap。
下面開始最后一點內(nèi)容:頁表
我們知道應(yīng)用程序操作的對象是映射到物理內(nèi)存之上的虛擬內(nèi)存,但是處理器直接操作的確實物理內(nèi)存。所以當(dāng)應(yīng)用程序訪問一個虛擬地址時,首先必須將虛擬地址轉(zhuǎn)化為物理地址,然后處理器才能解析地址訪問請求。這個轉(zhuǎn)換工作需要通過查詢頁面才能完成,概括地講,地址轉(zhuǎn)換需要將虛擬地址分段,使每段虛地址都作為一個索引指向頁表,而頁表項則指向下一級別的頁表或者指向最終的物理頁面。linux中使用三級頁表完成地址轉(zhuǎn)換。多數(shù)體系結(jié)構(gòu)中,搜索頁表的工作由硬件完成,下表描述了虛擬地址通過頁表找到物理地址的過程:
在上面這個圖中,頂級頁表是頁全局目錄(PGD),二級頁表是中間頁目錄(PMD).最后一級是頁表(PTE),該頁表結(jié)構(gòu)指向物理頁。上圖中的頁表對應(yīng)的結(jié)構(gòu)體定義在文件asm/page.h中。為了加快查找速度,在linux中實現(xiàn)了快表(TLB),其本質(zhì)是一個緩沖器,作為一個將虛擬地址映射到物理地址的硬件緩存,當(dāng)請求訪問一個虛擬地址時,處理器將首先檢查TLB中是否緩存了該虛擬地址到物理地址的映射,如果找到了,物理地址就立刻返回,否則,就需要再通過頁表搜索需要的物理地址。