整個虛擬內(nèi)存空間一分為二,一部分是用戶態(tài)地址空間,一部分是內(nèi)核態(tài)地址空間,這兩部分的分界線由 task_size 來定義。
struct task_struct => struct mm_struct *mm; => unsigned long task_size; /* size of task vm space */ => #ifdef CONFIG_X86_32 /* * User space process size: 3GB (default). */ #define TASK_SIZE PAGE_OFFSET #define TASK_SIZE_MAX TASK_SIZE /* config PAGE_OFFSET hex default 0xC0000000 depends on X86_32 */ #else /* * User space process size. 47bits minus one guard page. */ #define TASK_SIZE_MAX ((1UL << 47) - PAGE_SIZE) #define TASK_SIZE (test_thread_flag(TIF_ADDR32) ? \ IA32_PAGE_OFFSET : TASK_SIZE_MAX) ...... // 當(dāng)執(zhí)行一個新的進(jìn)程的時候,會設(shè)置 current->mm->task_size = TASK_SIZE;
用戶態(tài)布局
struct mm_struct
mmap_base:
malloc 申請一大塊內(nèi)存的時候,就是通過 mmap 在這里映射一塊區(qū)域到物理內(nèi)存;
加載動態(tài)鏈接庫 so 文件,也是在這個區(qū)域里面,映射一塊區(qū)域到 so 文件。
// 用戶態(tài)的堆、棧、內(nèi)存映射區(qū)等區(qū)域的統(tǒng)計信息和位置 unsigned long mmap_base; /* base of mmap area 虛擬地址空間中用于內(nèi)存映射的起始地址,從高地址到低地址增長 */ unsigned long total_vm; /* Total pages mapped 總共映射的頁數(shù) */ unsigned long locked_vm; /* Pages that have PG_mlocked set 被鎖定不能換出的頁數(shù) */ unsigned long pinned_vm; /* Refcount permanently increased 不能換出,也不能移動的頁數(shù) */ unsigned long data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK 存放數(shù)據(jù)的頁數(shù)*/ unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK 存放可執(zhí)行文件的頁數(shù) */ unsigned long stack_vm; /* VM_STACK 棧所占的頁數(shù) */ unsigned long start_code, end_code, start_data, end_data; /* 可執(zhí)行代碼, 已初始化數(shù)據(jù)的開始和結(jié)束位置 */ unsigned long start_brk, brk, start_stack; /* 堆的起始位置和堆當(dāng)前的結(jié)束位置;棧的起始位置,棧的結(jié)束位置在寄存器的棧頂指針中 */ unsigned long arg_start, arg_end, env_start, env_end; /* 參數(shù)列表, 環(huán)境變量的位置,位于棧中最高地址的地方 */ // 各區(qū)域的屬性 struct vm_area_struct *mmap; /* list of VMAs 用于將各區(qū)域串起來 */ struct rb_root mm_rb; // 紅黑樹,快速查找、修改一個內(nèi)存區(qū)域
struct vm_area_struct { /* The first cache line has the info for VMA tree walking. */ 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, *vm_prev; struct rb_node vm_rb; struct mm_struct *vm_mm; /* The address space we belong to. */ struct list_head anon_vma_chain; /* Serialized by mmap_sem & * page_table_lock */ struct anon_vma *anon_vma; /* Serialized by page_table_lock */ /* Function pointers to deal with this struct. */ const struct vm_operations_struct *vm_ops; struct file * vm_file; /* File we map to (can be NULL). */ void * vm_private_data; /* was vm_pte (shared mem) */ } __randomize_layout;
vm_start 和 vm_end 指定了該區(qū)域在用戶空間中的起始和結(jié)束地址。
vm_next 和 vm_prev 將這個區(qū)域串在鏈表上。
vm_rb 將這個區(qū)域放在紅黑樹上。vm_ops 里面是對這個內(nèi)存區(qū)域可以做的操作的定義。
虛擬內(nèi)存區(qū)域可以映射到物理內(nèi)存,也可以映射到文件,映射到物理內(nèi)存的時候稱為匿名映射。
anon_vma 中,anon 即 anonymous 匿名,映射到文件就需要有 vm_file 指定被映射的文件。
那這些 vm_area_struct 是如何和上面的內(nèi)存區(qū)域關(guān)聯(lián)的呢?這個事情是在 load_elf_binary 里面實現(xiàn)的。
沒錯,就是它。加載內(nèi)核的是它,啟動第一個用戶態(tài)進(jìn)程 init 的是它,fork 完了以后,調(diào)用 exec 運行一個二進(jìn)制程序的也是它。
當(dāng) exec 運行一個二進(jìn)制程序的時候,除了解析 ELF 的格式之外,另外一個重要的事情就是建立內(nèi)存映射。
static int load_elf_binary(struct linux_binprm *bprm) { ...... // 設(shè)置內(nèi)存映射區(qū) mmap_base setup_new_exec(bprm); ...... // 設(shè)置棧的 vm_area_struct, current->mm->arg_start = current->mm->start_stack指向棧底 retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack); ...... // 將 ELF 文件中的代碼部分映射到內(nèi)存中來 error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size); ...... // 設(shè)置堆的 vm_area_struct,current->mm->start_brk = current->mm->brk,即堆里面還是空的 retval = set_brk(elf_bss, elf_brk, bss_prot); ...... // 將依賴的 so 映射到內(nèi)存中的內(nèi)存映射區(qū)域。 elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_map_addr, load_bias, interp_elf_phdata); ...... current->mm->end_code = end_code; current->mm->start_code = start_code; current->mm->start_data = start_data; current->mm->end_data = end_data; current->mm->start_stack = bprm->p; ...... }
映射完畢后,什么情況下會修改呢?
第一種情況是函數(shù)調(diào)用,涉及函數(shù)棧的改變,主要是改變棧頂指針。
第二種情況是通過 malloc 申請一個堆內(nèi)的空間,當(dāng)然底層要么執(zhí)行 brk,要么執(zhí)行 mmap。
brk 系統(tǒng)調(diào)用實現(xiàn)的入口是 sys_brk 函數(shù)。
SYSCALL_DEFINE1(brk, unsigned long, brk) { unsigned long retval; unsigned long newbrk, oldbrk; struct mm_struct *mm = current->mm; struct vm_area_struct *next; ...... newbrk = PAGE_ALIGN(brk); // brk新的堆頂位置 oldbrk = PAGE_ALIGN(mm->brk); // mm->brk原來堆頂?shù)奈恢?/span> if (oldbrk == newbrk) goto set_brk; /* Always allow shrinking brk. */ if (brk <= mm->brk) { if (!do_munmap(mm, newbrk, oldbrk-newbrk, &uf)) goto set_brk; goto out; } /* Check against existing mmap mappings. */ next = find_vma(mm, oldbrk); if (next && newbrk + PAGE_SIZE > vm_start_gap(next)) goto out; /* Ok, looks good - let it rip. */ if (do_brk(oldbrk, newbrk-oldbrk, &uf) < 0) goto out; set_brk: mm->brk = brk; ...... return brk; out: retval = mm->brk; return retval }
堆是從低地址向高地址增長的,首先要將原來的堆頂和現(xiàn)在的堆頂,都按照頁對齊地址,然后比較大小。如果兩者相同,說明這次增加的堆的量很小,還在一個頁里面,不需要另行分配頁,直接跳到 set_brk 那里,設(shè)置 mm->brk 為新的 brk 就可以了。
如果發(fā)現(xiàn)新舊堆頂不在一個頁里面,說明要跨頁。如果新堆頂小于舊堆頂,說明是釋放內(nèi)存,至少要釋放一頁,于是調(diào)用 do_munmap 將這一頁的內(nèi)存映射去掉。
如果堆將要擴(kuò)大,就要調(diào)用 find_vma。如果打開這個函數(shù),看到的是對紅黑樹的查找,找到的是原堆頂所在的 vm_area_struct 的下一個 vm_area_struct,看當(dāng)前的堆頂和下一個 vm_area_struct 之間還能不能分配一個完整的頁。如果不能,沒辦法只好直接退出返回,內(nèi)存空間都被占滿了。如果還有空間,就調(diào)用 do_brk 進(jìn)一步分配堆空間,從舊堆頂開始,分配計算出的新舊堆頂之間的頁數(shù)。
static int do_brk(unsigned long addr, unsigned long len, struct list_head *uf) { return do_brk_flags(addr, len, 0, uf); } static int do_brk_flags(unsigned long addr, unsigned long request, unsigned long flags, struct list_head *uf) { struct mm_struct *mm = current->mm; struct vm_area_struct *vma, *prev; unsigned long len; struct rb_node **rb_link, *rb_parent; pgoff_t pgoff = addr >> PAGE_SHIFT; int error; len = PAGE_ALIGN(request); ...... find_vma_links(mm, addr, addr + len, &prev, &rb_link, &rb_parent); ...... vma = vma_merge(mm, prev, addr, addr + len, flags, NULL, NULL, pgoff, NULL, NULL_VM_UFFD_CTX); if (vma) goto out; ...... vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL); INIT_LIST_HEAD(&vma->anon_vma_chain); vma->vm_mm = mm; vma->vm_start = addr; vma->vm_end = addr + len; vma->vm_pgoff = pgoff; vma->vm_flags = flags; vma->vm_page_prot = vm_get_page_prot(flags); vma_link(mm, vma, prev, rb_link, rb_parent); out: perf_event_mmap(vma); mm->total_vm += len >> PAGE_SHIFT; mm->data_vm += len >> PAGE_SHIFT; if (flags & VM_LOCKED) mm->locked_vm += (len >> PAGE_SHIFT); vma->vm_flags |= VM_SOFTDIRTY; return 0; }
在 do_brk 中,調(diào)用 find_vma_links 找到將來的 vm_area_struct 節(jié)點在紅黑樹的位置,找到它的父節(jié)點、前序節(jié)點。
接下來調(diào)用 vma_merge,看這個新節(jié)點是否能夠和現(xiàn)有樹中的節(jié)點合并。
如果地址是連著的,能夠合并,則不用創(chuàng)建新的 vm_area_struct 了,直接跳到 out,更新統(tǒng)計值即可。
如果不能合并,則創(chuàng)建新的 vm_area_struct,既加到 anon_vma_chain 鏈表中,也加到紅黑樹中。
內(nèi)核態(tài)布局
在內(nèi)核里面,有兩個宏:
__pa(vaddr) 返回與虛擬地址 vaddr 相關(guān)的物理地址;
__va(paddr) 計算出對應(yīng)于物理地址 paddr 的虛擬地址。
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) #define __pa(x) __phys_addr((unsigned long)(x)) #define __phys_addr(x) __phys_addr_nodebug(x) #define __phys_addr_nodebug(x) ((x) - PAGE_OFFSET)
浙公網(wǎng)安備 33010602011771號