linux進程地址空間--vma的基本操作


在32位的系統上,線性地址空間可達到4GB,這4GB一般按照3:1的比例進行分配,也就是說用戶進程享有前3GB線性地址空間,而內核獨享最后1GB線性地址空間。由於虛擬內存的引入,每個進程都可擁有3GB的虛擬內存,並且用戶進程之間的地址空間是互不可見、互不影響的,也就是說即使兩個進程對同一個地址進行操作,也不會產生問題。在前面介紹的一些分配內存的途徑中,無論是伙伴系統中分配頁的函數,還是slab分配器中分配對象的函數,它們都會盡量快速地響應內核的分配請求,將相應的內存提交給內核使用,而內核對待用戶空間顯然不能如此。用戶空間動態申請內存時往往只是獲得一塊線性地址的使用權,而並沒有將這塊線性地址區域與實際的物理內存對應上,只有當用戶空間真正操作申請的內存時,才會觸發一次缺頁異常,這時內核才會分配實際的物理內存給用戶空間。

       用戶進程的虛擬地址空間包含了若干區域,這些區域的分布方式是特定於體系結構的,不過所有的方式都包含下列成分:

可執行文件的二進制代碼,也就是程序的代碼段
存儲全局變量的數據段
用於保存局部變量和實現函數調用的棧
環境變量和命令行參數
程序使用的動態庫的代碼
用於映射文件內容的區域
由此可以看到進程的虛擬內存空間會被分成不同的若干區域,每個區域都有其相關的屬性和用途,一個合法的地址總是落在某個區域當中的,這些區域也不會重疊。在linux內核中,這樣的區域被稱之為虛擬內存區域(virtual memory areas),簡稱vma。一個vma就是一塊連續的線性地址空間的抽象,它擁有自身的權限(可讀,可寫,可執行等等) ,每一個虛擬內存區域都由一個相關的struct vm_area_struct結構來描述

[cpp] view plain copy
<span style="font-size:12px;">struct vm_area_struct {  
    struct mm_struct * vm_mm;   /* 所屬的內存描述符 */  
    unsigned long vm_start;    /* vma的起始地址 */  
    unsigned long vm_end;       /* vma的結束地址 */  
  
    /* 該vma的在一個進程的vma鏈表中的前驅vma和后驅vma指針,鏈表中的vma都是按地址來排序的*/  
    struct vm_area_struct *vm_next, *vm_prev;  
  
    pgprot_t vm_page_prot;      /* vma的訪問權限 */  
    unsigned long vm_flags;    /* 標識集 */  
  
    struct rb_node vm_rb;      /* 紅黑樹中對應的節點 */  
  
    /* 
     * For areas with an address space and backing store, 
     * linkage into the address_space->i_mmap prio tree, or 
     * linkage to the list of like vmas hanging off its node, or 
     * linkage of vma in the address_space->i_mmap_nonlinear list. 
     */  
    /* shared聯合體用於和address space關聯 */  
    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;/*線性映射則鏈入i_mmap優先樹*/  
    } shared;  
  
    /* 
     * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma 
     * list, after a COW of one of the file pages.  A MAP_SHARED vma 
     * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack 
     * or brk vma (with NULL file) can only be in an anon_vma list. 
     */  
    /*anno_vma_node和annon_vma用於管理源自匿名映射的共享頁*/  
    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. */  
    /*該vma上的各種標准操作函數指針集*/  
    const struct vm_operations_struct *vm_ops;  
  
    /* Information about our backing store: */  
    unsigned long vm_pgoff;     /* 映射文件的偏移量,以PAGE_SIZE為單位 */  
    struct file * vm_file;          /* 映射的文件,沒有則為NULL */  
    void * vm_private_data;     /* was vm_pte (shared mem) */  
    unsigned long vm_truncate_count;/* truncate_count or restart_addr */  
  
#ifndef CONFIG_MMU  
    struct vm_region *vm_region;    /* NOMMU mapping region */  
#endif  
#ifdef CONFIG_NUMA  
    struct mempolicy *vm_policy;    /* NUMA policy for the VMA */  
#endif  
};  
</span>  

 

進程的若干個vma區域都得按一定的形式組織在一起,這些vma都包含在進程的內存描述符中,也就是struct mm_struct中,這些vma在mm_struct以兩種方式進行組織,一種是鏈表方式,對應於mm_struct中的mmap鏈表頭,一種是紅黑樹方式,對應於mm_struct中的mm_rb根節點,和內核其他地方一樣,鏈表用於遍歷,紅黑樹用於查找。

 

下面以文件映射為例,來闡述文件的address_space和與其建立映射關系的vma是如何聯系上的。首先來看看struct address_space中與vma相關的變量

[cpp] view plain copy
struct address_space {  
    struct inode        *host;      /* owner: inode, block_device */  
    ...  
    struct prio_tree_root   i_mmap;     /* tree of private and shared mappings */  
    struct list_head    i_mmap_nonlinear;          /*list VM_NONLINEAR mappings */  
    ...  
} __attr  

與此同時,struct file和struct inode中都包含有一個struct address_space的指針,分別為f_mapping和i_mapping。struct file是一個特定於進程的數據結構,而struct inode則是一個特定於文件的數據結構。每當進程打開一個文件時,都會將file->f_mapping設置到inode->i_mapping,下圖則給出了文件和與其建立映射關系的vma的聯系

 

 

下面來看幾個vma的基本操作函數,這些函數都是后面實現具體功能的基礎

find_vma()用來尋找一個針對於指定地址的vma,該vma要么包含了指定的地址,要么位於該地址之后並且離該地址最近,或者說尋找第一個滿足addr<vma_end的vma

[cpp] view plain copy
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; //首先嘗試mmap_cache中緩存的vma  
        /*如果不滿足下列條件中的任意一個則從紅黑樹中查找合適的vma 
          1.緩存vma不存在 
          2.緩存vma的結束地址小於給定的地址 
          3.緩存vma的起始地址大於給定的地址*/  
        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,   //獲取節點對應的vma  
                        struct vm_area_struct, vm_rb);  
  
                /*首先確定vma的結束地址是否大於給定地址,如果是的話,再確定 
                  vma的起始地址是否小於給定地址,也就是優先保證給定的地址是 
                  處於vma的范圍之內的,如果無法保證這點,則只能找到一個距離 
                  給定地址最近的vma並且該vma的結束地址要大於給定地址*/  
                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;  
}  

 

當一個新區域被加到進程的地址空間時,內核會檢查它是否可以與一個或多個現存區域合並,vma_merge()函數在可能的情況下,將一個新區域與周邊區域進行合並。參數:

mm:新區域所屬的進程地址空間

prev:在地址上緊接着新區域的前面一個vma

addr:新區域的起始地址

end:新區域的結束地址

vm_flags:新區域的標識集

anon_vma:新區域所屬的匿名映射

file:新區域映射的文件

pgoff:新區域映射文件的偏移

policy:和NUMA相關

 

[cpp] view plain copy
struct vm_area_struct *vma_merge(struct mm_struct *mm,  
            struct vm_area_struct *prev, unsigned long addr,  
            unsigned long end, unsigned long vm_flags,  
                struct anon_vma *anon_vma, struct file *file,  
            pgoff_t pgoff, struct mempolicy *policy)  
{  
    pgoff_t pglen = (end - addr) >> PAGE_SHIFT;  
    struct vm_area_struct *area, *next;  
  
    /* 
     * We later require that vma->vm_flags == vm_flags, 
     * so this tests vma->vm_flags & VM_SPECIAL, too. 
     */  
    if (vm_flags & VM_SPECIAL)  
        return NULL;  
  
    if (prev)//指定了先驅vma,則獲取先驅vma的后驅vma  
        next = prev->vm_next;  
    else     //否則指定mm的vma鏈表中的第一個元素為后驅vma  
        next = mm->mmap;  
    area = next;  
  
    /*后驅節點存在,並且后驅vma的結束地址和給定區域的結束地址相同, 
      也就是說兩者有重疊,那么調整后驅vma*/  
    if (next && next->vm_end == end)     /* cases 6, 7, 8 */  
        next = next->vm_next;  
  
    /* 
     * 先判斷給定的區域能否和前驅vma進行合並,需要判斷如下的幾個方面: 
       1.前驅vma必須存在 
       2.前驅vma的結束地址正好等於給定區域的起始地址 
       3.兩者的struct mempolicy中的相關屬性要相同,這項檢查只對NUMA架構有意義 
       4.其他相關項必須匹配,包括兩者的vm_flags,是否映射同一個文件等等 
     */  
    if (prev && prev->vm_end == addr &&  
            mpol_equal(vma_policy(prev), policy) &&  
            can_vma_merge_after(prev, vm_flags,  
                        anon_vma, file, pgoff)) {  
        /* 
         *確定可以和前驅vma合並后再判斷是否能和后驅vma合並,判斷方式和前面一樣, 
          不過這里多了一項檢查,在給定區域能和前驅、后驅vma合並的情況下還要檢查 
          前驅、后驅vma的匿名映射可以合並 
         */  
        if (next && end == next->vm_start &&  
                mpol_equal(policy, vma_policy(next)) &&  
                can_vma_merge_before(next, vm_flags,  
                    anon_vma, file, pgoff+pglen) &&  
                is_mergeable_anon_vma(prev->anon_vma,  
                              next->anon_vma)) {  
                            /* cases 1, 6 */  
            vma_adjust(prev, prev->vm_start,  
                next->vm_end, prev->vm_pgoff, NULL);  
        } else                  /* cases 2, 5, 7 */  
            vma_adjust(prev, prev->vm_start,  
                end, prev->vm_pgoff, NULL);  
        return prev;  
    }  
  
    /* 
     * Can this new request be merged in front of next? 
     */  
     /*如果前面的步驟失敗,那么則從后驅vma開始進行和上面類似的步驟*/  
    if (next && end == next->vm_start &&  
            mpol_equal(policy, vma_policy(next)) &&  
            can_vma_merge_before(next, vm_flags,  
                    anon_vma, file, pgoff+pglen)) {  
        if (prev && addr < prev->vm_end)  /* case 4 */  
            vma_adjust(prev, prev->vm_start,  
                addr, prev->vm_pgoff, NULL);  
        else                    /* cases 3, 8 */  
            vma_adjust(area, addr, next->vm_end,  
                next->vm_pgoff - pglen, NULL);  
        return area;  
    }  
  
    return NULL;  
}  

vma_adjust會執行具體的合並調整操作

[cpp] view plain copy
void vma_adjust(struct vm_area_struct *vma, unsigned long start,  
    unsigned long end, pgoff_t pgoff, struct vm_area_struct *insert)  
{  
    struct mm_struct *mm = vma->vm_mm;  
    struct vm_area_struct *next = vma->vm_next;  
    struct vm_area_struct *importer = NULL;  
    struct address_space *mapping = NULL;  
    struct prio_tree_root *root = NULL;  
    struct file *file = vma->vm_file;  
    struct anon_vma *anon_vma = NULL;  
    long adjust_next = 0;  
    int remove_next = 0;  
  
    if (next && !insert) {  
        /*指定的范圍已經跨越了整個后驅vma,並且有可能超過后驅vma*/  
        if (end >= next->vm_end) {  
            /* 
             * vma expands, overlapping all the next, and 
             * perhaps the one after too (mprotect case 6). 
             */  
again:          remove_next = 1 + (end > next->vm_end);//確定是否超過了后驅vma  
            end = next->vm_end;  
            anon_vma = next->anon_vma;  
            importer = vma;  
        } else if (end > next->vm_start) {/*指定的區域和后驅vma部分重合*/  
          
            /* 
             * vma expands, overlapping part of the next: 
             * mprotect case 5 shifting the boundary up. 
             */  
            adjust_next = (end - next->vm_start) >> PAGE_SHIFT;  
            anon_vma = next->anon_vma;  
            importer = vma;  
        } else if (end < vma->vm_end) {/*指定的區域沒到達后驅vma的結束處*/  
            /* 
             * vma shrinks, and !insert tells it's not 
             * split_vma inserting another: so it must be 
             * mprotect case 4 shifting the boundary down. 
             */  
            adjust_next = - ((vma->vm_end - end) >> PAGE_SHIFT);  
            anon_vma = next->anon_vma;  
            importer = next;  
        }  
    }  
  
    if (file) {//如果有映射文件  
        mapping = file->f_mapping;//獲取文件對應的address_space  
        if (!(vma->vm_flags & VM_NONLINEAR))  
            root = &mapping->i_mmap;  
        spin_lock(&mapping->i_mmap_lock);  
        if (importer &&  
            vma->vm_truncate_count != next->vm_truncate_count) {  
            /* 
             * unmap_mapping_range might be in progress: 
             * ensure that the expanding vma is rescanned. 
             */  
            importer->vm_truncate_count = 0;  
        }  
        /*如果指定了待插入的vma,則根據vma是否以非線性的方式映射文件來選擇是將 
        vma插入file對應的address_space的優先樹(對應線性映射)還是雙向鏈表(非線性映射)*/  
        if (insert) {  
            insert->vm_truncate_count = vma->vm_truncate_count;  
            /* 
             * Put into prio_tree now, so instantiated pages 
             * are visible to arm/parisc __flush_dcache_page 
             * throughout; but we cannot insert into address 
             * space until vma start or end is updated. 
             */  
            __vma_link_file(insert);  
        }  
    }  
  
    /* 
     * When changing only vma->vm_end, we don't really need 
     * anon_vma lock. 
     */  
    if (vma->anon_vma && (insert || importer || start != vma->vm_start))  
        anon_vma = vma->anon_vma;  
    if (anon_vma) {  
        spin_lock(&anon_vma->lock);  
        /* 
         * Easily overlooked: when mprotect shifts the boundary, 
         * make sure the expanding vma has anon_vma set if the 
         * shrinking vma had, to cover any anon pages imported. 
         */  
        if (importer && !importer->anon_vma) {  
            importer->anon_vma = anon_vma;  
            __anon_vma_link(importer);//將importer插入importer的anon_vma匿名映射鏈表中  
        }  
    }  
  
    if (root) {  
        flush_dcache_mmap_lock(mapping);  
        vma_prio_tree_remove(vma, root);  
        if (adjust_next)  
            vma_prio_tree_remove(next, root);  
    }  
  
    /*調整vma的相關量*/  
    vma->vm_start = start;  
    vma->vm_end = end;  
    vma->vm_pgoff = pgoff;  
    if (adjust_next) {//調整后驅vma的相關量  
        next->vm_start += adjust_next << PAGE_SHIFT;  
        next->vm_pgoff += adjust_next;  
    }  
  
    if (root) {  
        if (adjust_next)//如果后驅vma被調整了,則重新插入到優先樹中  
            vma_prio_tree_insert(next, root);  
        vma_prio_tree_insert(vma, root);//將vma插入到優先樹中  
        flush_dcache_mmap_unlock(mapping);  
    }  
  
    if (remove_next) {//給定區域與后驅vma有重合  
        /* 
         * vma_merge has merged next into vma, and needs 
         * us to remove next before dropping the locks. 
         */  
        __vma_unlink(mm, next, vma);//將后驅vma從紅黑樹中刪除  
        if (file)//將后驅vma從文件對應的address space中刪除  
            __remove_shared_vm_struct(next, file, mapping);  
        if (next->anon_vma)//將后驅vma從匿名映射鏈表中刪除  
            __anon_vma_merge(vma, next);  
    } else if (insert) {  
        /* 
         * split_vma has split insert from vma, and needs 
         * us to insert it before dropping the locks 
         * (it may either follow vma or precede it). 
         */  
        __insert_vm_struct(mm, insert);//將待插入的vma插入mm的紅黑樹,雙向鏈表以及  
                                       //匿名映射鏈表  
    }  
  
    if (anon_vma)  
        spin_unlock(&anon_vma->lock);  
    if (mapping)  
        spin_unlock(&mapping->i_mmap_lock);  
  
    if (remove_next) {  
        if (file) {  
            fput(file);  
            if (next->vm_flags & VM_EXECUTABLE)  
                removed_exe_file_vma(mm);  
        }  
        mm->map_count--;  
        mpol_put(vma_policy(next));  
        kmem_cache_free(vm_area_cachep, next);  
        /* 
         * In mprotect's case 6 (see comments on vma_merge), 
         * we must remove another next too. It would clutter 
         * up the code too much to do both in one go. 
         */  
        if (remove_next == 2) {//還有待刪除的區域  
            next = vma->vm_next;  
            goto again;  
        }  
    }  
  
    validate_mm(mm);  
}  

 

insert_vm_struct()函數用於插入一塊新區域

 

[cpp] view plain copy
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;  
  
    /* 
     * The vm_pgoff of a purely anonymous vma should be irrelevant 
     * until its first write fault, when page's anon_vma and index 
     * are set.  But now set the vm_pgoff it will almost certainly 
     * end up with (unless mremap moves it elsewhere before that 
     * first wfault), so /proc/pid/maps tells a consistent story. 
     * 
     * By setting it to reflect the virtual start address of the 
     * vma, merges and splits can happen in a seamless way, just 
     * using the existing file pgoff checks and manipulations. 
     * Similarly in do_mmap_pgoff and in do_brk. 
     */  
    if (!vma->vm_file) {  
        BUG_ON(vma->anon_vma);  
        vma->vm_pgoff = vma->vm_start >> PAGE_SHIFT;  
    }  
    /*__vma用來保存和vma->start對應的vma(與find_vma()一樣),同時獲取以下信息: 
      1.prev用來保存對應的前驅vma 
      2.rb_link保存該vma區域插入對應的紅黑樹節點 
      3.rb_parent保存該vma區域對應的父節點*/  
    __vma = find_vma_prepare(mm,vma->vm_start,&prev,&rb_link,&rb_parent);  
    if (__vma && __vma->vm_start < vma->vm_end)  
        return -ENOMEM;  
    if ((vma->vm_flags & VM_ACCOUNT) &&  
         security_vm_enough_memory_mm(mm, vma_pages(vma)))  
        return -ENOMEM;  
    vma_link(mm, vma, prev, rb_link, rb_parent);//將vma關聯到所有的數據結構中  
    return 0;  
}  

 

[cpp] view plain copy
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插入到相應的數據結構中--雙向鏈表,紅黑樹和匿名映射鏈表*/  
    __vma_link(mm, vma, prev, rb_link, rb_parent);  
    __vma_link_file(vma);//將vma插入到文件地址空間的相應數據結構中  
  
    anon_vma_unlock(vma);  
    if (mapping)  
        spin_unlock(&mapping->i_mmap_lock);  
  
    mm->map_count++;  
    validate_mm(mm);  
}  

在創建新的vma區域之前先要尋找一塊足夠大小的空閑區域,該項工作由get_unmapped_area()函數完成,而實際的工作將會由mm_struct中定義的輔助函數來完成。根據進程虛擬地址空間的布局,會選擇使用不同的映射函數,在這里考慮大多數系統上采用的標准函數arch_get_unmapped_area();

[cpp] view plain copy
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 (flags & MAP_FIXED)  
        return addr;  
  
    if (addr) {  
        addr = PAGE_ALIGN(addr);//將地址按頁對齊  
        vma = find_vma(mm, addr);//獲取一個vma,該vma可能包含了addr也可能在addr后面並且離addr最近  
        /*這里確定是否有一塊適合的空閑區域,先要保證addr+len不會 
          超過進程地址空間的最大允許范圍,然后如果前面vma獲取成功的話則要保證 
          vma位於addr的后面並且addr+len不會延伸到該vma的區域*/  
        if (TASK_SIZE - len >= addr &&  
            (!vma || addr + len <= vma->vm_start))  
            return addr;  
    }  
    /*前面獲取不成功的話則要調整起始地址了,根據情況選擇緩存的空閑區域地址 
      或者TASK_UNMAPPED_BASE=TASK_SIZE/3*/  
    if (len > mm->cached_hole_size) {  
            start_addr = addr = mm->free_area_cache;  
    } else {  
            start_addr = addr = TASK_UNMAPPED_BASE;  
            mm->cached_hole_size = 0;  
    }  
  
full_search:  
    /*從addr開始遍歷用戶地址空間*/  
    for (vma = find_vma(mm, addr); ; vma = vma->vm_next) {  
        /* At this point:  (!vma || addr < vma->vm_end). */  
        if (TASK_SIZE - len < addr) {//這里判斷是否已經遍歷到了用戶地址空間的末端  
            /* 
             * Start a new search - just in case we missed 
             * some holes. 
             */  
             //如果上次不是從TAKS_UNMAPPED_BASE開始遍歷的,則嘗試從TASK_UNMAPPED_BASE開始遍歷  
            if (start_addr != TASK_UNMAPPED_BASE) {  
                addr = TASK_UNMAPPED_BASE;  
                    start_addr = addr;  
                mm->cached_hole_size = 0;  
                goto full_search;  
            }  
            return -ENOMEM;  
        }  
        if (!vma || addr + len <= vma->vm_start) {//判斷是否有空閑區域  
            /* 
             *找到空閑區域的話則記住我們搜索的結束處,以便下次搜索 
             */  
            mm->free_area_cache = addr + len;  
            return addr;  
        }  
        /*該空閑區域不符合大小要求,但是如果這個空閑區域大於之前保存的最大值的話 
          則將這個空閑區域保存,這樣便於前面確定從哪里開始搜索*/  
        if (addr + mm->cached_hole_size < vma->vm_start)  
                mm->cached_hole_size = vma->vm_start - addr;  
        addr = vma->vm_end;  
    }  
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM