一篇讀懂Linux內(nèi)核-內(nèi)核地址空間分布和進程地址空間
內(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等。
進程的地址空間
linux采用虛擬內(nèi)存管理技術(shù),每一個進程都有一個3G大小的獨立的進程地址空間,這個地址空間就是用戶空間。每個進程的用戶空間都是完全獨立、互不相干的。進程訪問內(nèi)核空間的方式:系統(tǒng)調(diào)用和中斷。 創(chuàng)建進程等進程相關(guān)操作都需要分配內(nèi)存給進程。這時進程申請和獲得的不是物理地址,僅僅是虛擬地址。 實際的物理內(nèi)存只有當進程真的去訪問新獲取的虛擬地址時,才會由“請頁機制”產(chǎn)生“缺頁”異常,從而進入分配實際頁框的程序。該異常是虛擬內(nèi)存機制賴以存在的基本保證,它會告訴內(nèi)核去為進程分配物理頁,并建立對應(yīng)的頁表,這之后虛擬地址才實實在在的映射到了物理地址上。

【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)??


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,分配不多次機器就罷工了。 盡管僅僅在某些情況下才需要物理上連續(xù)的內(nèi)存塊,但是,很多內(nèi)核代碼都調(diào)用kmalloc(),而不是用vmalloc()獲得內(nèi)存。這主要是出于性能的考慮。vmalloc()函數(shù)為了把物理上不連續(xù)的頁面轉(zhuǎn)換為虛擬地址空間上連續(xù)的頁,必須專門建立頁表項。還有,通過 vmalloc()獲得的頁必須一個一個的進行映射(因為它們物理上不是連續(xù)的),這就會導(dǎo)致比直接內(nèi)存映射大得多的緩沖區(qū)刷新。因為這些原因,vmalloc()僅在絕對必要時才會使用,最典型的就是為了獲得大塊內(nèi)存時,例如,當模塊被動態(tài)插入到內(nèi)核中時,就把模塊裝載到由vmalloc()分配的內(nèi)存上。
進程地址空間
前邊我已經(jīng)說過了內(nèi)核是如何管理物理內(nèi)存。但事實是內(nèi)核是操作系統(tǒng)的核心,不光管理本身的內(nèi)存,還要管理進程的地址空間。linux操作系統(tǒng)采用虛擬內(nèi)存技術(shù),所有進程之間以虛擬方式共享內(nèi)存。進程地址空間由每個進程中的線性地址區(qū)組成,而且更為重要的特點是內(nèi)核允許進程使用該空間中的地址。通常情況況下,每個進程都有唯一的地址空間,而且進程地址空間之間彼此互不相干。但是進程之間也可以選擇共享地址空間,這樣的進程就叫做線程。 內(nèi)核使用內(nèi)存描述符結(jié)構(gòu)表示進程的地址空間,由結(jié)構(gòu)體mm_struct結(jié)構(gòu)體表示,定義在linux/sched.h中,如下:
mm_users記錄了正在使用該地址的進程數(shù)目(比如有兩個進程在使用,那就為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ù)器和使用該地址空間的進程的數(shù)目。mmap和mm_rb描述的都是同一個對象:該地址空間中的全部內(nèi)存區(qū)域。不同只是前者以鏈表,后者以紅黑樹的形式組織。所有的mm_struct結(jié)構(gòu)體都通過自身的mmlist域連接在一個雙向鏈表中,該鏈表的首元素是init_mm內(nèi)存描述符,它代表init進程的地址空間。另外需要注意,操作該鏈表的時候需要使用mmlist_lock鎖來防止并發(fā)訪問,該鎖定義在文件kernel/fork.c中。內(nèi)存描述符的總數(shù)在mmlist_nr全局變量中,該變量也定義在文件fork.c中。
我前邊說過的進程描述符中有一個mm域,這里邊存放的就是該進程使用的內(nèi)存描述符,通過current->mm便可以指向當前進程的內(nèi)存描述符。fork函數(shù)利用copy_mm()函數(shù)就實現(xiàn)了復(fù)制父進程的內(nèi)存描述符,而子進程中的mm_struct結(jié)構(gòu)體實際是通過文件kernel/fork.c中的allocate_mm()宏從mm_cachep slab緩存中分配得到的。通常,每個進程都有唯一的mm_struct結(jié)構(gòu)體。
前邊也說過,在linux中,進程和線程其實是一樣的,唯一的不同點就是是否共享這里的地址空間。這個可以通過CLONE_VM標志來實現(xiàn)。linux內(nèi)核并不區(qū)別對待它們,線程對內(nèi)核來說僅僅是一個共向特定資源的進程而已。好了,如果你設(shè)置這個標志了,似乎很多問題都解決了。不再要allocate_mm函數(shù)了,前邊剛說作用。而且在copy_mm()函數(shù)中將mm域指向其父進程的內(nèi)存描述符就可以了,如下:
最后,當進程退出的時候,內(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)核線程沒有進程地址空間,也沒有相關(guān)的內(nèi)存描述符,內(nèi)核線程對應(yīng)的進程描述符中mm域也為空。但內(nèi)核線程還是需要使用一些數(shù)據(jù)的,比如頁表,為了避免內(nèi)核線程為內(nèi)存描述符和頁表浪費內(nèi)存,也為了當新內(nèi)核線程運行時,避免浪費處理器周期向新地址空間進行切換,內(nèi)核線程將直接使用前一個進程的內(nèi)存描述符?;貞浺幌挛覄傉f的進程調(diào)度問題,當一個進程被調(diào)度時,進程結(jié)構(gòu)體中mm域指向的地址空間會被裝載到內(nèi)存,進程描述符中的active_mm域會被更新,指向新的地址空間。但我們這里的內(nèi)核是沒有mm域(為空),所以,當一個內(nèi)核線程被調(diào)度時,內(nèi)核發(fā)現(xiàn)它的mm域為NULL,就會保留前一個進程的地址空間,隨后內(nèi)核更新內(nèi)核線程對應(yīng)的進程描述符中的active域,使其指向前一個進程的內(nèi)存描述符。所以在需要的時候,內(nèi)核線程便可以使用前一個進程的頁表。因為內(nèi)核線程不妨問用戶空間的內(nèi)存,所以它們僅僅使用地址空間中和內(nèi)核內(nèi)存相關(guā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)體如下:
每個內(nèi)存描述符都對應(yīng)于地址進程空間中的唯一區(qū)間。vm_mm域指向和VMA相關(guān)的mm_struct結(jié)構(gòu)體。兩個獨立的進程將同一個文件映射到各自的地址空間,它們分別都會有一個vm_area_struct結(jié)構(gòu)體來標志自己的內(nèi)存區(qū)域;但是如果兩個線程共享一個地址空間,那么它們也同時共享其中的所有vm_area_struct結(jié)構(gòu)體。
在上面的vm_flags域中存放的是VMA標志,標志了內(nèi)存區(qū)域所包含的頁面的行為和信息,反映了內(nèi)核處理頁面所需要遵循的行為準則,如下表下述:

上表已經(jīng)相當詳細了,而且給出了說明,我就不說了。在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中,如下:
open:當指定的內(nèi)存區(qū)域被加入到一個地址空間時,該函數(shù)被調(diào)用。 close:當指定的內(nèi)存區(qū)域從地址空間刪除時,該函數(shù)被調(diào)用。 nopages:當要訪問的頁不在物理內(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)體的指針,僅僅組織方式不同而已。前者以鏈表的方式進行組織,所有的區(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)我就不細講了,以后可能會詳細說這個問題。內(nèi)核之所以采用這兩種結(jié)構(gòu)來表示同一內(nèi)存區(qū)域,主要是鏈表結(jié)構(gòu)便于遍歷所有節(jié)點,而紅黑樹結(jié)構(gòu)體便于在地址空間中定位特定內(nèi)存區(qū)域的節(jié)點。我么可以使用/proc文件系統(tǒng)和pmap工具查看給定進程的內(nèi)存空間和其中所包含的內(nèi)存區(qū)域。這里就不細說了。
內(nèi)核也為我們提供了對內(nèi)存區(qū)域操作的API,定義在linux/mm.h中:
記性好的你一定記得內(nèi)存描述符中的mmap和mm_rb域都獨立地指向與內(nèi)存描述符相關(guān)的全體內(nèi)存區(qū)域?qū)ο?。它們包含完全相同的vm_area_struct結(jié)構(gòu)體的指針,僅僅組織方式不同而已。前者以鏈表的方式進行組織,所有的區(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)我就不細講了,以后可能會詳細說這個問題。內(nèi)核之所以采用這兩種結(jié)構(gòu)來表示同一內(nèi)存區(qū)域,主要是鏈表結(jié)構(gòu)便于遍歷所有節(jié)點,而紅黑樹結(jié)構(gòu)體便于在地址空間中定位特定內(nèi)存區(qū)域的節(jié)點。我么可以使用/proc文件系統(tǒng)和pmap工具查看給定進程的內(nèi)存空間和其中所包含的內(nèi)存區(qū)域。這里就不細說了。 內(nèi)核也為我們提供了對內(nèi)存區(qū)域操作的API,定義在linux/mm.h中:
接下來要說的兩個函數(shù)就非常重要了,它們負責創(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ū)間加入到進程的地址空間中。這個函數(shù)定義在linux/mm.h中,如下:
這個函數(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標志,這些標志定義在asm/mman.h中,如下:

如果系統(tǒng)調(diào)用do_mmap的參數(shù)中有無效參數(shù),那么它返回一個負值;否則,它會在虛擬內(nèi)存中分配一個合適的新內(nèi)存區(qū)域,如果有可能的話,將新區(qū)域和臨近區(qū)域進行合并,否則內(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ù)往下走。 我們說既然有了創(chuàng)建,當然要有刪除了,是不?do_mummp()函數(shù)就是干這事的。它從特定的進程地址空間中刪除指定地址空間,該函數(shù)定義在文件linux/mm.h中,如下:
第一個參數(shù)指定要刪除區(qū)域所在的地址空間,刪除從地址start開始,長度為len字節(jié)的地址空間,如果成功,返回0,否則返回負的錯誤碼。與之相對應(yīng)的用戶空間系統(tǒng)調(diào)用是munmap。
下面開始最后一點內(nèi)容:頁表
我們知道應(yīng)用程序操作的對象是映射到物理內(nèi)存之上的虛擬內(nèi)存,但是處理器直接操作的確實物理內(nèi)存。所以當應(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ì)是一個緩沖器,作為一個將虛擬地址映射到物理地址的硬件緩存,當請求訪問一個虛擬地址時,處理器將首先檢查TLB中是否緩存了該虛擬地址到物理地址的映射,如果找到了,物理地址就立刻返回,否則,就需要再通過頁表搜索需要的物理地址。
