天天看點

Linux核心剖析 之 程序位址空間(二)線性區

//接前一章,本節主要介紹線性區以及相關線性區的操作。

linux通過類型為vm_area_struct的對象實作線性區。

vm_area_struct:

vm_area_struct字段:

類型

字段

說明

struct mm_struct *

vm_mm

指向線性區所在的記憶體描述符

unsigned long

vm_start

線性區内的第一個線性位址

vm_end

線性區後的第一個線性位址

struct vm_area_struct *

vm_next

程序擁有的線性區連結清單中的下一個線性區

pgprot_t

vm_page_prot

線性區中頁框的通路許可權

vm_flags

線性區的标志

struct rb_node

vm_rb

用于紅黑樹的資料

union

shared

連結到反映射所使用的資料結構

struct list_head

anon_vma_node

指向匿名線性區連結清單的指針

struct anon_vma *

anon_vma

指向anon_vma資料結構的指針

struct vm_operation_struct *

vm_ops

指向線性區的方法

vm_pgoff

在映射檔案中的偏移量

struct file *

vm_file

指向映射檔案的檔案對象

void *

vm_private_data

指向記憶體區的私有資料

vm_truncate_count

釋放非線性檔案記憶體映射中的一個線性位址區間時使用

每個線性區描述符表示一個線性位址區間。

vm_start字段指向線性區的第一個線性位址,而vm_end字段指向線性區之後的第一個線性位址。

vm_end - vm_start表示線性區的長度。

vm_mm字段指向擁有這個區間的程序的mm_struct記憶體描述符。

mmap_cache字段儲存程序最後一次引用線性區的描述符位址,引用此字段可減少查找一個給定線性位址所線上性區花費的時間。

程序所擁有的線性區從來不重疊,并且核心盡力把新配置設定的線性區與緊鄰的現有線性區進行合并。兩個相鄰區的通路權限如果相比對,就能把它們合并在一起。

 增加或删除一個線性區,如圖:

Linux核心剖析 之 程式位址空間(二)線性區

vm_ops字段指向vm_operations_struct資料結構,該結構中存放的是線性區的方法。

*作用于線性區的方法:(可應用于uma系統)

方法

open

當把線性區增加到程序所擁有的線性區集合時調用

close

當從程序所擁有的線性區集合删除線性區時調用

nopage

當程序試圖通路ram中不存在的頁,但該頁的線性位址屬于線性區時,由缺頁異常處理程式調用

populate

設定線性區的線性位址所對應的頁表項時調用。

程序所擁有的所有線性區是通過一個簡單的連結清單連接配接在一起的。每個vm_area_struct元素的vm_next字段指向連結清單的下一個元素。

核心通過程序的記憶體描述符的nmap字段來查找線性區,其中nmap字段指向連結清單中的第一個線性區描述符。

記憶體描述符中的map_count字段存放程序所擁有的線性區數目。預設情況下,一個程序可以最多擁有65536個不同的線性區。

程序位址空間、記憶體描述符和線性區連結清單之間的關系:

Linux核心剖析 之 程式位址空間(二)線性區

用紅黑樹存儲記憶體描述符的原因:

核心頻繁執行查找包含指定線性位址的線性區。用連結清單進行查找操作其時間複雜度為o(n),而用紅黑樹則其時間複雜度為o(logn)。可見,在n很大時,用紅黑樹能極大地節省查找時間。

為了存放程序的線性區,linux既使用了連結清單,也使用了紅黑樹。這兩種資料結構包含指向同一線性區描述符的指針,當插入或删除一個線性區描述符時,核心通過紅黑樹搜尋前後元素,并用搜尋結果快速更新連結清單而不用掃描連結清單。

連結清單:連結清單的頭由記憶體描述符的nmap字段所指向。任何線性區對象都在vm_next字段存放指向連結清單下一個元素的指針。

紅黑樹:紅黑樹的首部由記憶體描述符中的mm_rb字段所指向。任何線性區對象都在類型為rb_node的vm_rb字段中存放節點顔色以及指向雙親、左孩子和右孩子的指針。

一般來說,紅黑樹用來确定含有指定位址的線性區,而連結清單通常在需要掃描整個線性區集合時來使用。

每個線性區都是由一組号碼連續的頁所構成。

與頁有關的标志:

1.每個頁表項中存放的标志,如read/write、present和user/supervisor.

——80x86硬體用來檢查能否執行所請求的尋址類型;

2.每個頁描述符flags字段中的一組标志。

——linux用于許多不同的目的;

3.與線性區的頁相關的那些标志。存放在vm_area_struct描述符的vm_flags字段中。

——部分标志将線性區頁的資訊提供給核心,另外的用來描述線性區自身特性。

線性區描述符所包含的頁通路權限可以任意組合。

*** 頁通路權限表示何種類型的通路應該産生一個缺頁異常。

頁表标志的初值存放在vm_area_struct描述符的vm_page_prot字段中。當增加一個頁時,核心根據vm_page_prot字段的值設定相應頁表項中的标志。

如果核心沒有被編譯成支援pae,那麼linux采取以下規則以克服80x86微處理器的硬體限制:

*讀通路權限總是隐含着執行通路權限,反之亦然。

*寫通路權限總是隐含着讀通路權限。

反之,如果核心被編譯成支援pae,而且cpu有nx标志,linux就采取不同的規則:

*執行通路權限總是隐含着讀通路權限。

===>>>

nx位(全名“no execute bit”,即“禁止執行位”),是應用在cpu中的一種安全技術。支援nx技術的系統會把記憶體中的區域分類為隻供存儲處理器指令集與隻供存儲資料使用的兩種。任何标記了nx位的區塊代表僅供存儲資料使用而不是存儲處理器的指令集,處理器将不會将此處的資料作為代碼執行,以此這種技術可防止大多數的緩存溢出式攻擊。

讀+寫+執行+共享通路權限有16種可能組合,可根據規則進行精簡。

1.為什麼是16種?——排列組合

2.精簡規則有哪些?

==>>

*如果頁具有寫和共享兩種通路權限,則read/write位置位;

*如果頁具有讀或執行通路權限,但沒有寫和共享通路權限,則read/write位清零;

*如果cpu支援nx位,且頁沒有執行通路權限,則nx位置位;

*如果頁沒有任何通路權限,則present位清零以産生缺頁異常。同時,為了區分真正的頁框不存在的情況,linux還把page size位置位。

通路權限的每種組合所對應的精簡後的保護位存放在protection _map數組的16個元素中。

操作函數

find_vma()

查找給定位址的最鄰近區

find_vma_intersection()

查找一個給定的位址區間相重疊的線性區

get_unmapped_area()

查找一個空閑的位址空間

insert_vm_struct

在記憶體描述符表中插入一個線性區

do_mmap()

配置設定線性位址空間

do_munmap()

釋放線性位址空間

spilt_vma()

把與線性位址區間交叉的線性區劃分成兩個較小的區,一個線上性位址區間外部,另一個在區間内部。

unmap_region()

周遊線性區連結清單并釋放它們的頁框

目的:查找線性區的vm_end字段大于addr的第一個線性區的位置(addr不一定在該線性區中),并傳回這個線性區描述符的位址;如果沒有這樣的線性區存在,就傳回null指針。

參數:程序記憶體描述符位址mm和線性位址addr。

步驟:

*檢查mmap_cache所指定的線性區是否包含addr。如果是,傳回此線性區描述符的指針。

*否則,掃描程序線性區,在紅黑樹中查找線性區。如果找到,傳回線性區描述符指針;否則,傳回null指針。

其中,函數使用宏rb_entry,從指向紅黑樹中一個節點的指針導出相應線性區描述符的位址。

拓展:函數find_vma_prev()和find_vma_prepare()。

find_vma_prev():把函數選中的前一個線性區描述符的指針賦給附加參數*pprev。

find_vma_prepare():确定新葉子節點在與給定線性位址對應的紅黑樹中的位置,并傳回前一個線性區描述符的位址和要插入的葉子節點的父節點位址。

目的:查找一個給定的位址區間相重疊的線性區。

參數:mm參數指向程序的記憶體描述符,線性位址start_addr和end_addr指定線性區位址區間。

*在mm記憶體描述符中的線性區中,調用find_vma()函數找到vm_end字段大于start_addr的第一個線性區,并将傳回值存放在vma指針中。

*如果vma指針不為null,檢查end_addr與vm_start,如果end_addr不大于vm_start,則說明整個由start_addr和end_addr界定的線性區間不屬于vma,傳回null;否則,傳回vma。

解析:如果參數addr不等于null,函數檢查所指定的位址是否在使用者态空間并與頁邊界對齊。接下來,函數根據線性位址區間是否應該用于檔案記憶體映射或匿名記憶體映射,調用兩個方法(get_unmapped_area檔案操作(通路檔案)和記憶體描述符的get_unmapped_area方法)中的一個。

對于記憶體描述符的get_unmapped_area方法,由函數arch_get_unmapped_area()或arch_get_unmapped_area_topdown()實作get_unmapped_area方法。通過系統調用mmap()建立新的記憶體映射,每個程序都可能獲得兩種不同形式的線性區:一種從線性位址0x4000000開始向高端位址增長,另一種從使用者态堆棧開始向低端位址增長。

arch_get_unmapped_area():

目的:配置設定從低端位址向高端位址移動的線性區時使用的函數。

參數:len參數指定區間長度,addr參數指定必須從哪個位址開始進行查找。

*函數檢查區間長度是否在使用者态下線性位址區間的限長task_size(通常為3gb)之内。如果addr不為0,函數就試圖從addr開始配置設定區間,同時把addr值調整為4kb的倍數。

*如果addr等于null或前面的搜尋失敗傳回null,函數arch_get_unmapped_area()掃描使用者态線性位址空間以查找一個可以包含新區的足夠大的線性位址範圍。如果函數找不到一個合适的線性位址範圍,就從使用者态位址空間的三分之一的開始處重新搜尋,直到搜尋到傳回位址。

*更新free_area_cache字段。

目的:向記憶體描述符連結清單中插入一個線性區。

參數:mm指定程序記憶體描述符位址,vmp指定要插入的vm_area_struct對象的位址。

*函數調用find_vma_prepare()在紅黑樹mm->mm_rb中查找vma應該位于何處。

*insert_vm_struct()調用vma_link函數,執行以下操作:

在mm->mmap所指向的連結清單中插入線性區;在紅黑樹mm->mm_rb中插入線性區;如果線性區是匿名的,就将此線性區插入以相應anon_vma資料結構作為頭結點的連結清單中;遞增mm->map_count計數器。

函數vma_link()代碼:

目的:為目前程序建立并初始化一個新的線性區。不過,配置設定成功之後,可以把這個新的線性區與程序已有的其他線性區進行合并。

參數:

     file和offset:如果新的線性區把一個檔案映射到記憶體,使用檔案描述符指針file和檔案偏移量offset。

     addr:指定從何處開始查找一個空閑的區間。

     len:線性位址區間的長度。

     prot:指定此線性區所包含的頁的通路權限。

     flag:指定線性區的其他标志。

**do_mmap()函數對offset值進行初步檢查。

**執行do_mmap_pgoff()函數:

     *檢查參數的值是否正确,所提的要求是否能被滿足,尤其是要檢查不能滿足請求的條件;

     *調用get_unmapped_area()獲得新線性區的線性位址區間;

     *通過把存放在prot和flags參數中的值進行組合來計算新線性區描述符的标志;

*調用find_vma_prepare()确定處于新區間之前的線性區對象的位置,以及在紅黑樹中新線性區的位置;

    *檢查插入新的線性區是否引起程序位址空間的大小超過存放在程序描述符signal->rlim[rlimit_as].rlim_cur字段中的門檻值。如果是,傳回出錯碼-enomem;

    *如果在flags參數中沒有設定map_noreserve标志,新的線性區包含私有可寫項,并沒有足夠的空閑頁框,則傳回出錯碼-enomem。此過程由security_vm_enough_memory()函數實作;

    *如果新區間是私有的(vm_shared未被設定),且映射的不是磁盤上的檔案,則調用vma_merge()檢查前一個線性區是否可以以這樣的方式進行擴充以包含新的區間(線性區的擴充);

    *調用slab配置設定函數kmem_cache_alloc()為新的線性區配置設定一個vm_area_struct資料結構;

    *初始化新的線性區對象(由vma指向);

    *如果vm_shared标志被設定(以及新的線性區不映射磁盤上的檔案),則該線性區是一個共享區:調用shmem_zero_setup()對它進行初始化工作。共享匿名主要用于程序間通信;

    *調用vma_link()把新線性區插入到線性區連結清單和紅黑樹中;

    *增加存放在記憶體描述符total_vm字段中的程序位址空間的大小;

    *如果設定了vm_locked标志,調用make_pages_present()連續配置設定線性區的所有頁,并把它們鎖在ram中;

    *傳回新線性區的線性位址。

目的:從目前程序的位址空間中删除一個線性位址區間。

參數:程序記憶體描述符的位址mm,位址區間的起始位址start和長度len。

*對參數值進行初步檢查:如果線性位址區間所含的位址大于task_size,如果start不是4096的倍數,或者如果線性位址區間的長度為0,則函數傳回一個錯誤代碼-einval;

*調用函數find_vma_prev()确定要删除的線性位址區間之後第一個線性區mpnt的位置;

*如果沒有這樣的線性區,也沒有與線性位址區間重疊的線性區,就什麼都不做,因為在該區間上沒有線性區;

*如果線性區的起始位址線上性區mpnt内,調用split_vma()把線性區mpnt劃分為兩個較小的區:一個區線上性位址區間外部,另一個區線上性位址區間内部;

*如果線性位址區間的結束位址在另一個線性區内部,就再次調用split_vma()把最後重疊的那個線性區劃分為兩個較小的區:一個區線上性位址區間内部,另一個區線上性位址區間外部;

*更新mpnt的值,是它指向線性位址區間的第一個線性區。如果prev為null,即沒有上述線性區,就從mm->mmap獲得第一個線性區的位址;

*調用detach_vmas_to_be_unmapped()從程序的線性位址空間中删除位于線性位址區間中的線性區;

*獲得mm->page_table_lock()自旋鎖;

*調用unmap_region()清除與線性位址區間對應的頁表項并釋放相應的頁框;

*釋放mm->page_table_lock()自旋鎖;

*調用unmap_vma_list()方法釋放在detach_vmas_to_be_unmapped()時建立連結清單時收集的線性區描述符;

*傳回0(成功)。

目的:把與線性位址區間交叉的線性區劃分為兩個較小的區,一個線上性位址區間外部,另一個在區間内部。

參數:記憶體描述符指針mm,線性區描述符vma(辨別要被劃分的線性區),表示區間與線性區間之間交叉點的位址addr,以及表示區間與線性區間之間交叉點在區間起始處還是結束處的标志new_below。

*調用kmem_cache_alloc()獲得線性區描述符vm_area_struct,并把它的位址存在新的局部變量中,如果沒有可用的空閑空間,傳回-enomem;

*用vma描述符的字段值初始化新描述符的字段;

*如果标志new_below為0,說明線性位址區間的起始位址在vma線性區的内部,是以必須把新線性區放在vma線性區之後,函數把new->vm_end字段指派為addr。相反,如果标志new_below為1,說明線性位址區間的結束在vma線性區的内部,是以必須把新線性區放在vma線性區之前,函數把new->vm_start字段都指派為addr;

*如果定義了新線性區的open方法,函數就執行它;

*通過vma_adjust()函數将新線性區描述符連結到線性區連結清單和紅黑樹;

目的:周遊線性區連結清單并釋放它們的頁框。

參數:記憶體描述符mm,指向第一個被删除線性區描述符的指針vma,指向程序連結清單中vma前面的線性區的指針prev,以及兩個位址start和end,它們界定被删除線性位址區間的範圍。

*調用lru_add_drain()函數;

*調用tlb_gather_mmu()函數初始化每cpu變量mmu_gathers;

*把mmu_gathers變量的位址儲存在局部變量tlb中;

*調用unmap_vmas()掃描線性位址空間的所有頁表項:如果隻有一個有效cpu,函數就調用free_swap_and_cache()反複釋放相應的頁;否則,函數就把相應頁描述符的指針儲存在局部變量mmu_gathers中;

*調用free_pgtables()回收上一步已清空的程序頁表;

*調用tlb_finish_mmu()函數結束unmap_region()函數的工作。

繼續閱讀