天天看點

linux核心--程序空間(二)

    核心處理管理本身的記憶體外,還必須管理使用者空間程序的記憶體。我們稱這個記憶體為程序位址空間,也就是系統中每個使用者空間程序所看到的記憶體。linux作業系統采用虛拟記憶體技術,是以,系統中的所有程序之間虛拟方式共享記憶體。對一個程序而言,它好像都可以通路整個系統的所有實體記憶體。即使單獨一個程序,它擁有的位址空間也可以遠遠大于系統實體記憶體。

一、位址空間

    每個程序都有一個32位或64位的平坦位址空間,空間的具體大小取決于體系結構。術語“平坦”指的是位址空間範圍是一個獨立的連續區間(比如,位址從0擴充到4294967295的32位位址空間)。一些作業系統提供了段位址空間,這種位址空間并非是一個獨立的線性區域,而是被分段的,但現代采用虛拟記憶體的作業系統通常都是使用平坦位址空間而不是分段式的記憶體模式。一個程序的位址空間與另一個程序的位址空間即使有相同的記憶體位址,實際上也彼此互不相幹。

    程序隻能通路有效記憶體區域内的記憶體位址。記憶體區域可以包含各種記憶體對象:代碼段、資料段、bss段、棧、堆。

二、記憶體描述符

    核心使用記憶體描述符結構體表示程序的位址空間,該結構包含了和程序位址空間有關的全部資訊。記憶體描述符由mm_struct結構體表示,定義在檔案<linux/shed.h>。在上一篇文章中介紹了這個結構體。

  1)配置設定記憶體描述符

    在程序的程序描述符(task_struct結構體就表示程序描述符)中,mm域存放着該程序使用的記憶體描述符,是以current->mm便指向目前程序的記憶體描述符。fork()函數利用copy_mm()函數複制父程序的記憶體描述符,也就是current->mm域給其子程序,而子程序中的mm_struct結構體實際是通過檔案kernel/fork.c中的allocate_mm()宏從mm_cachep_slab緩存中配置設定得到的。通常,每個程序都有唯一的mm_struct結構體,既唯一的程序位址空間。

  2)撤銷記憶體描述符

    當程序退出時,核心會調用定義在kernel/exit.c中的exit_mm()函數,該函數執行一些正常的撤銷工作,同時更新一些統計量。其中,該函數會調用mmput()函數減少記憶體描述符中的mm_users使用者計數,如果使用者計數為0,調用mmdrop()函數,減少mm_count使用計數。如果使用計數也等于0,說明該記憶體描述符不再有任何使用者了,那麼調用free_mm()宏通過kmem_cache_free()函數将mm_struct結構體歸還給mm_cachep_slab緩存中。

  3)mm_struct與核心線程

    核心線程沒有程序位址空間,也沒有相關的記憶體描述符。是以核心線程對應的程序描述符中mm域為空。因為核心線程并不需要通路任何使用者空間的記憶體而且因為核心線程在使用者空間中沒有任何頁,是以實際上它們并不需要有自己的記憶體描述符和頁表。盡管如此,即使通路核心記憶體,核心線程也還是需要使用一些資料的,比如頁表。

    當一個程序被排程時,該程序的mm域指向的位址空間被裝載到記憶體,程序描述符中的active_mm域會被更新,指向新的位址空間。核心線程沒有位址空間,是以mm域為NULL。當一個核心線程被排程時,核心發現它的mm域為NULL,就會保留前一個程序的位址空間,随後核心更新核心線程對應的程序描述符中的active_mm域,時期指向前一個程序的記憶體描述符。   

三、虛拟記憶體區域

    記憶體區域由vm_area_struct結構體描述,定義在檔案<linux/mm_types.h>中。記憶體區域在linux核心中也經常稱作虛拟記憶體區域(virtual memory Areas,VMAs)。

    vm_area_struct結構體描述了指定位址空間内連續區間上的一個獨立記憶體範圍。核心将每個記憶體區域作為一個單獨的記憶體對象管理,每個記憶體區域都擁有一緻的屬性,比如通路權限等,另外,相應的操作也都一緻。在上一篇文章中有結構體的定義。

    每個記憶體描述符都對應于程序位址空間中的唯一區間。vm_start域指向區間的首位址,vm_end是記憶體區間的結束位址,vm_mm域指向和VMA相關的mm_struct結構體,每個VMA對其相關的mm_struct結構體來說都是唯一的,是以即使兩個獨立的程序将同一個檔案映射到各自的位址空間,他們分别都會有一個vm_area_struct結構體來标志自己的記憶體區域;反過來,如果兩個線程共享一個位址空間,那麼他們也同時共享其中的所有vm_area_struct結構體。

  1)VMA操作

    vm_area_struct結構體中的vm_ops域指向與指定記憶體區域相關的操作函數表,核心使用表中的方法操作VMA。vm_area_struct作為通用對象代表了任何類型的記憶體區域,而操作表描述針對特定的對象執行個體的特定方法。

    操作函數表由vm_operations_struct結構體表示,定義在檔案<linux/mm.h>

下面介紹具體方法:

*void open (struct vm_area_struct *area)

當指定的記憶體區域被加入到一個位址空間時,該函數被調用。

*void close(struct vm_area_struct *area)

當指定的記憶體區域從位址空間删除時,該函數被調用。

*int fault(struct vm_area_struct *area,struct vm_fault *vmf)

當沒有出現在實體記憶體中的頁面被通路時,該函數被頁面故障處理調用。

*int page_mkwrite(struct vm_area_struct  *vmf)

當某個頁面為隻讀頁面時,該函數被頁面故障處理調用。

*int access(struct vm_area_struct *vma,unsigned long address,void *buf,int len,int write)

當get_user_page()函數調用失敗時,該函數被access_process_vm()函數調用。

  2)記憶體區域的樹形結果和記憶體區域的連結清單結構

    通過記憶體描述符中的mmap()和mm_rb域之一通路記憶體區域。這兩個域各自獨立地指向與記憶體描述符相關的全體記憶體區域對象。其實,他們包含完全相同的vm_area-struct結構體的指針,僅僅組織方法不同。mmap域使用單獨連結清單連接配接所有的記憶體區域對象。每一個vm_area_struct結構體通過自身的vm_next域被連傳入連結表,所有的區域按位址增長的方向排序,mmap域指向連結清單中第一個記憶體區域,鍊中最後一個結構體指針指向空。

    mm_rb域使用紅-黑樹連接配接所有的記憶體區域對象。mm_rb域指向宏-黑樹的根節點,位址空間中每一個vm_area_struct結構體通過自身的vm_rb域連接配接到樹中。

    紅-黑樹是一種二叉樹,樹中的每一個元素稱為一個節點,最初的節點稱為樹根。紅-黑樹的多數節點由兩個子節點:一個左子節點和一個右子節點,不過也有節點隻有一個子節點的情況。紅-黑樹中的所有節點都遵從:左邊節點值小于右邊節點值;另外每個節點都被配以紅色或黑色。配置設定的規則為:紅節點的子節點為黑色,并且樹中的任何一條從節點到葉子的路徑必須包含同樣數目的黑色節點。根節點總為紅色。紅-黑樹的搜尋、插入、删除等操作的複雜度都為O(logn)。

    連結清單用于需要周遊全部節點的時候,而紅-黑樹使用于在位址空間定位特定記憶體區域的時候。核心為了記憶體區域上的各種不同操作都獲得高性能,是以同時使用了這兩種資料結構。

  3)實際使用中的記憶體區域

    可以使用/proc檔案系統和pmap(1)工具檢視給定程序的記憶體空間和其中所含的記憶體區域。

    每個和程序相關的記憶體區域都對應于一個vm_area_struct結構體。另外程序不同于線程,程序結構體stask_struc包含唯一的mm_struct結構體引用

四、操作記憶體區域

    核心和時常需要在某個記憶體區域上執行一些操作,比如某個指定的位址是否包含在某個記憶體區域中。這類操作非常頻繁,另外它們也是mmap()例程的基礎。

  1)find_vma()

    為了找到一個給定的記憶體位址屬于哪一個記憶體區域,核心提供了find_vma()函數,該函數在檔案<mm/mmap.c>中:

    struct vm_area_struct *find_vma(struct mm_struct *mm,unsigned long addr);

    該函數在指定的位址空間中搜尋第一個vm_end大于addr的記憶體區域。

五、mmap()和do_mmap():建立位址區間

    核心使用do_mmap()建立一個新的線性位址區間。但是說該函數建立了一個新VMA并不非常準确。

unsigned long do_map(struct file *file,unsigned long addr,unsigned long len,unsigned long port,unsigned long flag,unsigned long offset);

    該函數映射由file指定的檔案,具體映射的是檔案中從偏移offset處開始,長度為len位元組的範圍内的資料。

六、mummap()和do_mummap():删除位址區間

    do_mummap()函數從特定的程序位址空間中删除指定位址區間,該函數定義在檔案<linux/mm.h>

    int do_mummap(struct mm_struct *mm,unsigned long start,size_t len);

    第一個參數指定要删除區域所在的位址空間,删除從位址start開始,長度為len位元組的位址區間。

    系統調用munmap()給使用者空間程式提供了一種從自身位址空間中删除指定位址區間的方法,它和系統調用mmap()的作用相反:

    int munmap(void *start,size_t length);

七、頁表

    雖然應用程式操作的對象是映射到實體記憶體之上的虛拟記憶體,但是處理器直接操作的卻是實體記憶體。是以當用程式通路一個虛拟位址時,首先必須将虛拟位址轉化成實體位址,然後處理器才能解析位址通路請求。位址的轉換工作需要通過查詢頁表才能完成,概括地将,位址轉換需要将虛拟位址分段,使每段虛拟位址都作為一個索引指向頁表,而頁表則指向下一級别的頁表或者指向最終的實體頁面。

    linux中使用三級頁表完成位址轉換。利用多級頁表能夠節約位址轉換需占用的存放空間。如果利用三級頁表轉換位址,即使64位機器,占用的空間也很有限。linux使用的機制:

    頂級頁表示頁全局目錄(PGD),它包含一個pgd_t類型數組,多數體系結構中pgd_t類型等同于無符号長整型。PGD中的表項指向二級頁目錄中的表項:PMD

    二級頁表是中間頁目錄(PMD),它是個pmd_t類型資料,其中的表項指向PTE中的表項。

    最後一級的頁表簡稱頁表,其中包含了pte_t類型的頁表項,該頁表項指向實體頁面。多數體系結構中,搜尋頁表的工作由硬體完成。每個程序都有自己的頁表,記憶體描述符的pgd域指向的就是程序的頁全局目錄。

linux核心--程式空間(二)

    由于幾乎每次對虛拟記憶體中的頁面通路都必須先解析它,進而得到實體記憶體中的對應位址,是以頁表操作的性能非常關鍵。搜尋記憶體中的實體位址速度很有限,是以為了加快搜尋,多數體系結構都實作了一個翻譯後緩沖器(TLB)。TLB作為一個将虛拟位址映射到實體位址的硬體緩存,當請求通路一個虛拟位址時,處理器首先檢查TLB是否緩存了該虛拟位址到實體位址的映射。

繼續閱讀