Linux內核剖析 之 進程地址空間(二)
//接前一章,本節主要介紹線性區以及相關線性區的操作。
線性區
Linux通過類型為vm_area_struct的對象實現線性區。
vm_area_struct:
struct vm_area_struct {
struct mm_struct * vm_mm; /* The address space we belong to. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, listed below. */
struct rb_node vm_rb;
union {
struct {
struct list_head list;
void *parent; /* aligns with prio_tree_node parent */
struct vm_area_struct *head;
} vm_set;
struct raw_prio_tree_node prio_tree_node;
} shared;
struct list_head anon_vma_node; /* Serialized by anon_vma->lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* Function pointers to deal with this struct. */
struct vm_operations_struct * vm_ops;
/* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
unsigned long vm_truncate_count;/* truncate_count or restart_addr */
};
vm_area_struct字段:
|
類型 |
字段 |
說明 |
|
struct mm_struct * |
vm_mm |
指向線性區所在的內存描述符 |
|
unsigned long |
vm_start |
線性區內的第一個線性地址 |
|
unsigned long |
vm_end |
線性區後的第一個線性地址 |
|
struct vm_area_struct * |
vm_next |
進程擁有的線性區鏈表中的下一個線性區 |
|
pgprot_t |
vm_page_prot |
線性區中頁框的訪問許可權 |
|
unsigned long |
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 |
指向線性區的方法 |
|
unsigned long |
vm_pgoff |
在映射文件中的偏移量 |
|
struct file * |
vm_file |
指向映射文件的文件對象 |
|
void * |
vm_private_data |
指向內存區的私有數據 |
|
unsigned long |
vm_truncate_count |
釋放非線性文件內存映射中的一個線性地址區間時使用 |
每個線性區描述符表示一個線性地址區間。
vm_start字段指向線性區的第一個線性地址,而vm_end字段指向線性區之後的第一個線性地址。
vm_end - vm_start表示線性區的長度。
vm_mm字段指向擁有這個區間的進程的mm_struct內存描述符。
mmap_cache字段保存進程最後一次引用線性區的描述符地址,引用此字段可減少查找一個給定線性地址所在線性區花費的時間。
進程所擁有的線性區從來不重疊,並且內核盡力把新分配的線性區與緊鄰的現有線性區進行合並。兩個相鄰區的訪問權限如果相匹配,就能把它們合並在一起。
增加或刪除一個線性區,如圖:

vm_ops字段指向vm_operations_struct數據結構,該結構中存放的是線性區的方法。
*作用於線性區的方法:(可應用於UMA係統)
|
方法 |
說明 |
|
open |
當把線性區增加到進程所擁有的線性區集合時調用 |
|
close |
當從進程所擁有的線性區集合刪除線性區時調用 |
|
nopage |
當進程試圖訪問RAM中不存在的頁,但該頁的線性地址屬於線性區時,由缺頁異常處理程序調用 |
|
populate |
設置線性區的線性地址所對應的頁表項時調用。 |
線性區數據結構:
進程所擁有的所有線性區是通過一個簡單的鏈表連接在一起的。每個vm_area_struct元素的vm_next字段指向鏈表的下一個元素。
內核通過進程的內存描述符的nmap字段來查找線性區,其中nmap字段指向鏈表中的第一個線性區描述符。
內存描述符中的map_count字段存放進程所擁有的線性區數目。默認情況下,一個進程可以最多擁有65536個不同的線性區。
進程地址空間、內存描述符和線性區鏈表之間的關係:

用紅黑樹存儲內存描述符的原因:
內核頻繁執行查找包含指定線性地址的線性區。用鏈表進行查找操作其時間複雜度為O(N),而用紅黑樹則其時間複雜度為O(logN)。可見,在N很大時,用紅黑樹能極大地節省查找時間。
為了存放進程的線性區,Linux既使用了鏈表,也使用了紅黑樹。這兩種數據結構包含指向同一線性區描述符的指針,當插入或刪除一個線性區描述符時,內核通過紅黑樹搜索前後元素,並用搜索結果快速更新鏈表而不用掃描鏈表。
鏈表:鏈表的頭由內存描述符的nmap字段所指向。任何線性區對象都在vm_next字段存放指向鏈表下一個元素的指針。
紅黑樹:紅黑樹的首部由內存描述符中的mm_rb字段所指向。任何線性區對象都在類型為rb_node的vm_rb字段中存放節點顏色以及指向雙親、左孩子和右孩子的指針。
struct rb_node
{
struct rb_node *rb_parent;
int rb_color;
#define RB_RED 0
#define RB_BLACK 1
struct rb_node *rb_right;
struct rb_node *rb_left;
};
一般來說,紅黑樹用來確定含有指定地址的線性區,而鏈表通常在需要掃描整個線性區集合時來使用。
線性區訪問權限:
每個線性區都是由一組號碼連續的頁所構成。
與頁有關的標誌:
1.每個頁表項中存放的標誌,如Read/Write、Present和User/Supervisor.
——80x86硬件用來檢查能否執行所請求的尋址類型;
2.每個頁描述符flags字段中的一組標誌。
——Linux用於許多不同的目的;
3.與線性區的頁相關的那些標誌。存放在vm_area_struct描述符的vm_flags字段中。
——部分標誌將線性區頁的信息提供給內核,另外的用來描述線性區自身特性。
/* vm_flags */ #define VM_READ 0x00000001 /* currently active flags */ #define VM_WRITE 0x00000002 #define VM_EXEC 0x00000004 #define VM_SHARED 0x00000008 #define VM_MAYREAD 0x00000010 /* limits for mprotect() etc */ #define VM_MAYWRITE 0x00000020 #define VM_MAYEXEC 0x00000040 #define VM_MAYSHARE 0x00000080 #define VM_GROWSDOWN 0x00000100 /* general info on the segment */ #define VM_GROWSUP 0x00000200 #define VM_SHM 0x00000400 /* shared memory area, don't swap out */ #define VM_DENYWRITE 0x00000800 /* ETXTBSY on write attempts.. */ #define VM_EXECUTABLE 0x00001000 #define VM_LOCKED 0x00002000 #define VM_IO 0x00004000 /* Memory mapped I/O or similar */ /* Used by sys_madvise() */ #define VM_SEQ_READ 0x00008000 /* App will access data sequentially */ #define VM_RAND_READ 0x00010000 /* App will not benefit from clustered reads */ #define VM_DONTCOPY 0x00020000 /* Do not copy this vma on fork */ #define VM_DONTEXPAND 0x00040000 /* Cannot expand with mremap() */ #define VM_RESERVED 0x00080000 /* Don't unmap it from swap_out */ #define VM_ACCOUNT 0x00100000 /* Is a VM accounted object */ #define VM_HUGETLB 0x00400000 /* Huge TLB Page VM */ #define VM_NONLINEAR 0x00800000 /* Is non-linear (remap_file_pages) */線性區描述符所包含的頁訪問權限可以任意組合。
*** 頁訪問權限表示何種類型的訪問應該產生一個缺頁異常。
頁表標誌的初值存放在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() |
遍曆線性區鏈表並釋放它們的頁框 |
1.find_vma()——查找給定地址的最鄰近區
/* Look up the first VMA which satisfies addr < vm_end, NULL if none. */
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
{
struct vm_area_struct *vma = NULL;
if (mm) {
/* Check the cache first. */
/* (Cache hit rate is typically around 35%.) */
vma = mm->mmap_cache;
if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
struct rb_node * rb_node;
rb_node = mm->mm_rb.rb_node;
vma = NULL;
while (rb_node) {
struct vm_area_struct * vma_tmp;
vma_tmp = rb_entry(rb_node,
struct vm_area_struct, vm_rb);
if (vma_tmp->vm_end > addr) {
vma = vma_tmp;
if (vma_tmp->vm_start <= addr)
break;
rb_node = rb_node->rb_left;
} else
rb_node = rb_node->rb_right;
}
if (vma)
mm->mmap_cache = vma;
}
}
return vma;
}
目的:查找線性區的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_prev(struct mm_struct *mm,
unsigned long addr,
struct vm_area_struct **pprev)
find_vma_prepare():確定新葉子節點在與給定線性地址對應的紅黑樹中的位置,並返回前一個線性區描述符的地址和要插入的葉子節點的父節點地址。static struct vm_area_struct *
find_vma_prepare(struct mm_struct *mm, unsigned long addr,
struct vm_area_struct **pprev, struct rb_node ***rb_link,
struct rb_node ** rb_parent)
{
struct vm_area_struct * vma;
struct rb_node ** __rb_link, * __rb_parent, * rb_prev;
__rb_link = &mm->mm_rb.rb_node;
rb_prev = __rb_parent = NULL;
vma = NULL;
while (*__rb_link) {
struct vm_area_struct *vma_tmp;
__rb_parent = *__rb_link;
vma_tmp = rb_entry(__rb_parent, struct vm_area_struct, vm_rb);
if (vma_tmp->vm_end > addr) {
vma = vma_tmp;
if (vma_tmp->vm_start <= addr)
return vma;
__rb_link = &__rb_parent->rb_left;
} else {
rb_prev = __rb_parent;
__rb_link = &__rb_parent->rb_right;
}
}
*pprev = NULL;
if (rb_prev)
*pprev = rb_entry(rb_prev, struct vm_area_struct, vm_rb);
*rb_link = __rb_link;
*rb_parent = __rb_parent;
return vma;
}
2.find_vma_intersection()——查找一個給定的地址區間相重疊的線性區
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
/*線性地址strat_addr和end_addr指定線性區地址區間*/
{
struct vm_area_struct * vma = find_vma(mm,start_addr);
if (vma && end_addr <= vma->vm_start)
vma = NULL;
return vma;
}
目的:查找一個給定的地址區間相重疊的線性區。
參數: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。
3.get_unmapped_area()——查找一個空閑的地址空間
unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags)
{
if (flags & MAP_FIXED) {
unsigned long ret;
if (addr > TASK_SIZE - len)
return -ENOMEM;
if (addr & ~PAGE_MASK)
return -EINVAL;
if (file && is_file_hugepages(file)) {
ret = prepare_hugepage_range(addr, len);
} else {
ret = is_hugepage_only_range(addr, len);
}
if (ret)
return -EINVAL;
return addr;
}
if (file && file->f_op && file->f_op->get_unmapped_area)
return file->f_op->get_unmapped_area(file, addr, len, pgoff, flags);
return current->mm->get_unmapped_area(file, addr, len, pgoff, flags);
}
解析:如果參數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():
unsigned long
arch_get_unmapped_area(struct file *filp, unsigned long addr,
unsigned long len, unsigned long pgoff, unsigned long flags)
{ struct mm_struct *mm = current->mm;
struct vm_area_struct *vma;
unsigned long start_addr;
if (len > TASK_SIZE)
return -ENOMEM;
if (addr) {
/* in file /source/include/asm-i386/page.h : */
/* to align the pointer to the (next) page boundary */
/* #define PAGE_ALIGN(addr) (((addr)+PAGE_SIZE-1)&PAGE_MASK)*/
addr = PAGE_ALIGN(addr);
/* Equal to “addr = (addr + 0xfff) & 0xfffff000” */
vma = find_vma(mm, addr);
if (TASK_SIZE - len >= addr &&
(!vma || addr + len <= vma->vm_start))
return addr;
}
start_addr = addr = mm->free_area_cache;
full_search:
for (vma = find_vma(mm, addr); ; vma = vma->vm_next) {
/* At this point: (!vma || addr < vma->vm_end). */
if (TASK_SIZE - len < addr) {
/* #define TASK_UNMAPPED_BASE (PAGE_ALIGN(TASK_SIZE / 3))*/
if (start_addr != TASK_UNMAPPED_BASE) {
start_addr = addr = TASK_UNMAPPED_BASE;
goto full_search;
}
return -ENOMEM;
}
if (!vma || addr + len <= vma->vm_start) {
mm->free_area_cache = addr + len;
return addr;
}
addr = vma->vm_end;
}
}
目的:分配從低端地址向高端地址移動的線性區時使用的函數。
參數:len參數指定區間長度,addr參數指定必須從哪個地址開始進行查找。
步驟:
*函數檢查區間長度是否在用戶態下線性地址區間的限長TASK_SIZE(通常為3GB)之內。如果addr不為0,函數就試圖從addr開始分配區間,同時把addr值調整為4KB的倍數。
*如果addr等於NULL或前麵的搜索失敗返回NULL,函數arch_get_unmapped_area()掃描用戶態線性地址空間以查找一個可以包含新區的足夠大的線性地址範圍。如果函數找不到一個合適的線性地址範圍,就從用戶態地址空間的三分之一的開始處重新搜索,直到搜索到返回地址。
*更新free_area_cache字段。
4.insert_vm_struct——在內存描述符表中插入一個線性區
/* Insert vm structure into process list sorted by address and into the inode's i_mmap tree.
* If vm_file is non-NULL then i_mmap_lock is taken here. */
int insert_vm_struct(struct mm_struct * mm, struct vm_area_struct * vma)
{
struct vm_area_struct * __vma, * prev;
struct rb_node ** rb_link, * rb_parent;
if (!vma->vm_file) {
BUG_ON(vma->anon_vma);
vma->vm_pgoff = vma->vm_start >> PAGE_SHIFT;
}
__vma = find_vma_prepare(mm,vma->vm_start,&prev,&rb_link,&rb_parent);
if (__vma && __vma->vm_start < vma->vm_end)
return -ENOMEM;
vma_link(mm, vma, prev, rb_link, rb_parent);
return 0;
}
目的:向內存描述符鏈表中插入一個線性區。
參數: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()代碼:
static void vma_link(struct mm_struct *mm, struct vm_area_struct *vma,
struct vm_area_struct *prev, struct rb_node **rb_link,
struct rb_node *rb_parent)
{
struct address_space *mapping = NULL;
if (vma->vm_file)
mapping = vma->vm_file->f_mapping;
if (mapping)
{
spin_lock(&mapping->i_mmap_lock);
vma->vm_truncate_count = mapping->truncate_count;
}
anon_vma_lock(vma);
__vma_link(mm, vma, prev, rb_link, rb_parent);
__vma_link_file(vma);
anon_vma_unlock(vma);
if (mapping)
spin_unlock(&mapping->i_mmap_lock);
mm->map_count++;
validate_mm(mm);
}
5.do_mmap()——分配線性地址區間
static inline unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flag, unsigned long offset)
{
unsigned long ret = -EINVAL;
if ((offset + PAGE_ALIGN(len)) < offset)
goto out;
if (!(offset & ~PAGE_MASK))
ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
return ret;
}
目的:為當前進程創建並初始化一個新的線性區。不過,分配成功之後,可以把這個新的線性區與進程已有的其他線性區進行合並。
參數:
file和offset:如果新的線性區把一個文件映射到內存,使用文件描述符指針file和文件偏移量offset。
addr:指定從何處開始查找一個空閑的區間。
len:線性地址區間的長度。
prot:指定此線性區所包含的頁的訪問權限。
flag:指定線性區的其他標誌。
步驟:
**do_mmap()函數對offset值進行初步檢查。
**執行do_mmap_pgoff()函數:
*檢查參數的值是否正確,所提的要求是否能被滿足,尤其是要檢查不能滿足請求的條件;
*調用get_unmapped_area()獲得新線性區的線性地址區間;
*通過把存放在prot和flags參數中的值進行組合來計算新線性區描述符的標誌;
vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
if (flags & MAP_LOCKED) {
if (!can_do_mlock())
return -EPERM;
vm_flags |= VM_LOCKED;
}
*調用find_vma_prepare()確定處於新區間之前的線性區對象的位置,以及在紅黑樹中新線性區的位置;munmap_back:
vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);
if (vma && vma->vm_start < addr + len) {
if (do_munmap(mm, addr, len))
return -ENOMEM;
goto munmap_back;
}
*檢查插入新的線性區是否引起進程地址空間的大小超過存放在進程描述符signal->rlim[RLIMIT_AS].rlim_cur字段中的閾值。如果是,返回出錯碼-ENOMEM;/* Check against address space limit. */ if ((mm->total_vm << PAGE_SHIFT) + len > current->signal->rlim[RLIMIT_AS].rlim_cur) return -ENOMEM;*如果在flags參數中沒有設置MAP_NORESERVE標誌,新的線性區包含私有可寫項,並沒有足夠的空閑頁框,則返回出錯碼-ENOMEM。此過程由security_vm_enough_memory()函數實現;
if (accountable && (!(flags & MAP_NORESERVE) ||
sysctl_overcommit_memory == OVERCOMMIT_NEVER)) {
if (vm_flags & VM_SHARED) {
vm_flags |= VM_ACCOUNT;
} else if (vm_flags & VM_WRITE) {
charged = len >> PAGE_SHIFT;
if (security_vm_enough_memory(charged))
return -ENOMEM;
vm_flags |= VM_ACCOUNT;
}
}
*如果新區間是私有的(VM_SHARED未被設置),且映射的不是磁盤上的文件,則調用vma_merge()檢查前一個線性區是否可以以這樣的方式進行擴展以包含新的區間(線性區的擴展);if (!file && !(vm_flags & VM_SHARED) && vma_merge(mm, prev, addr, addr + len, vm_flags, NULL, NULL, pgoff, NULL)) goto out;*調用slab分配函數kmem_cache_alloc()為新的線性區分配一個vm_area_struct數據結構;
vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
*初始化新的線性區對象(由vma指向); memset(vma, 0, sizeof(*vma));
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = protection_map[vm_flags & 0x0f];
vma->vm_pgoff = pgoff;
*如果VM_SHARED標誌被設置(以及新的線性區不映射磁盤上的文件),則該線性區是一個共享區:調用shmem_zero_setup()對它進行初始化工作。共享匿名主要用於進程間通信; if (file) {
error = -EINVAL;
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
goto free_vma;
if (vm_flags & VM_DENYWRITE) {
error = deny_write_access(file);
if (error)
goto free_vma;
correct_wcount = 1;
}
vma->vm_file = file;
get_file(file);
error = file->f_op->mmap(file, vma);
if (error)
goto unmap_and_free_vma;
}
else if (vm_flags & VM_SHARED) {
error = shmem_zero_setup(vma);
if (error)
goto free_vma;
}
*調用vma_link()把新線性區插入到線性區鏈表和紅黑樹中;if (!file || !vma_merge(mm, prev, addr, vma->vm_end,
vma->vm_flags, NULL, file, pgoff, vma_policy(vma))) {
file = vma->vm_file;
vma_link(mm, vma, prev, rb_link, rb_parent);
if (correct_wcount)
atomic_inc(&inode->i_writecount);
}
*增加存放在內存描述符total_vm字段中的進程地址空間的大小;mm->total_vm += len >> PAGE_SHIFT;*如果設置了VM_LOCKED標誌,調用make_pages_present()連續分配線性區的所有頁,並把它們鎖在RAM中;
if (vm_flags & VM_LOCKED) {
mm->locked_vm += len >> PAGE_SHIFT;
make_pages_present(addr, addr + len);
}
*返回新線性區的線性地址。
6.do_munmap()——從當前進程的地址空間中刪除一個線性地址區間
/* Munmap is split into 2 main parts -- this part which finds
* what needs doing, and the areas themselves, which do the
* work. This now handles partial unmappings.
* Jeremy Fitzhardinge <jeremy@goop.org>
*/
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
{
unsigned long end;
struct vm_area_struct *mpnt, *prev, *last;
if ((start & ~PAGE_MASK) || start > TASK_SIZE || len > TASK_SIZE-start)
return -EINVAL;
if ((len = PAGE_ALIGN(len)) == 0)
return -EINVAL;
/* Find the first overlapping VMA */
mpnt = find_vma_prev(mm, start, &prev);
if (!mpnt)
return 0;
/* we have start < mpnt->vm_end */
/* if it doesn't overlap, we have nothing.. */
end = start + len;
if (mpnt->vm_start >= end)
return 0;
if (start > mpnt->vm_start) {
int error = split_vma(mm, mpnt, start, 0);
if (error)
return error;
prev = mpnt;
}
/* Does it split the last one? */
last = find_vma(mm, end);
if (last && end > last->vm_start) {
int error = split_vma(mm, last, end, 1);
if (error)
return error;
}
mpnt = prev? prev->vm_next: mm->mmap;
/* Remove the vma's, and unmap the actual pages */
detach_vmas_to_be_unmapped(mm, mpnt, prev, end);
spin_lock(&mm->page_table_lock);
unmap_region(mm, mpnt, prev, start, end);
spin_unlock(&mm->page_table_lock);
/* Fix up all other VM information */
unmap_vma_list(mm, mpnt);
return 0;
}
目的:從當前進程的地址空間中刪除一個線性地址區間。
參數:進程內存描述符的地址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()從進程的線性地址空間中刪除位於線性地址區間中的線性區;
/*
* Create a list of vma's touched by the unmap, removing them from the mm's
* vma list as we go..
*/
static void detach_vmas_to_be_unmapped(struct mm_struct *mm, struct vm_area_struct *vma,
struct vm_area_struct *prev, unsigned long end)
{
struct vm_area_struct **insertion_point;
struct vm_area_struct *tail_vma = NULL;
insertion_point = (prev ? &prev->vm_next : &mm->mmap);
do {
rb_erase(&vma->vm_rb, &mm->mm_rb);
mm->map_count--;
tail_vma = vma;
vma = vma->vm_next;
} while (vma && vma->vm_start < end);
*insertion_point = vma;
tail_vma->vm_next = NULL;
mm->mmap_cache = NULL; /* Kill the cache. */
}
*獲得mm->page_table_lock()自旋鎖;
*調用unmap_region()清除與線性地址區間對應的頁表項並釋放相應的頁框;
*釋放mm->page_table_lock()自旋鎖;
*調用unmap_vma_list()方法釋放在detach_vmas_to_be_unmapped()時建立鏈表時收集的線性區描述符;
static void unmap_vma_list(struct mm_struct *mm,
struct vm_area_struct *mpnt)
{
do {
struct vm_area_struct *next = mpnt->vm_next;
unmap_vma(mm, mpnt);
mpnt = next;
} while (mpnt != NULL);
validate_mm(mm);
}
*返回0(成功)。
7.spilt_vma()
/*
* Split a vma into two pieces at address 'addr', a new vma is allocated
* either for the first part or the the tail.
*/
int split_vma(struct mm_struct * mm, struct vm_area_struct * vma,
unsigned long addr, int new_below)
{
struct mempolicy *pol;
struct vm_area_struct *new;
if (is_vm_hugetlb_page(vma) && (addr & ~HPAGE_MASK))
return -EINVAL;
if (mm->map_count >= sysctl_max_map_count)
return -ENOMEM;
new = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
if (!new)
return -ENOMEM;
/* most fields are the same, copy all, and then fixup */
*new = *vma;
if (new_below)
new->vm_end = addr;
else {
new->vm_start = addr;
new->vm_pgoff += ((addr - vma->vm_start) >> PAGE_SHIFT);
}
pol = mpol_copy(vma_policy(vma));
if (IS_ERR(pol)) {
kmem_cache_free(vm_area_cachep, new);
return PTR_ERR(pol);
}
vma_set_policy(new, pol);
if (new->vm_file)
get_file(new->vm_file);
if (new->vm_ops && new->vm_ops->open)
new->vm_ops->open(new);
if (new_below)
vma_adjust(vma, addr, vma->vm_end, vma->vm_pgoff +
((addr - new->vm_start) >> PAGE_SHIFT), new);
else
vma_adjust(vma, vma->vm_start, addr, vma->vm_pgoff, new);
return 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()函數將新線性區描述符鏈接到線性區鏈表和紅黑樹;
*返回0(成功)。
8.unmap_region()
/*
* Get rid of page table information in the indicated region.
* Called with the page table lock held.
*/
static void unmap_region(struct mm_struct *mm,
struct vm_area_struct *vma,
struct vm_area_struct *prev,
unsigned long start,
unsigned long end)
{
struct mmu_gather *tlb;
unsigned long nr_accounted = 0;
lru_add_drain();
tlb = tlb_gather_mmu(mm, 0);
unmap_vmas(&tlb, mm, vma, start, end, &nr_accounted, NULL);
vm_unacct_memory(nr_accounted);
if (is_hugepage_only_range(start, end - start))
hugetlb_free_pgtables(tlb, prev, start, end);
else
free_pgtables(tlb, prev, start, end);
tlb_finish_mmu(tlb, start, end);
}
目的:遍曆線性區鏈表並釋放它們的頁框。
參數:內存描述符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()函數的工作。
最後更新:2017-04-03 05:39:40