2017-04-12
前篇文章對Linux進程地址空間的布局以及各個部分的功能做了簡要介紹,本文主要對各個部分的具體使用做下簡要分析,主要涉及三個方面:1、MMAP文件的映射過程 2、用戶 內存的動態分配
Text:進程代碼
Data:全局和靜態數據區,但是已初始化
BSS:全局和靜態數據區,但是未初始化
堆:動態內存分配
棧:函數參數,局部變量
1、MMAP文件映射過程
MMAP文件映射其實就是在磁盤文件和進程虛擬地址空間建立一種關系,用戶空間通過調用mmap函數實現,mmap()是C運行庫函數,實現把一個文件的某個區間映射到進程虛擬地址的某個區間,函數原型如下:void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
。在內核中對應於sys_mmap系統調用。該系統調用會調用sys_mmap_pgoff函數,而最終do_mmap_pgoff函數實現具體的功能。該函數主體包含兩部分,一部分是獲取到可用的虛擬地址 空間,一部分實現具體的映射。咱們一層一層的往下看
asmlinkage long sys_mmap(unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long fd, off_t off) { if (offset_in_page(off) != 0) return -EINVAL; return sys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT); }
經過sys_mmap_pgoff函數,內部調用了vm_mmap_pgoff函數,在經過安全檢查之后,調用do_mmap_pgoff函數,此時真正的工作開始了。
函數開始前期是針對保護位和flags做的一些處理
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC)) if (!(file && (file->f_path.mnt->mnt_flags & MNT_NOEXEC))) prot |= PROT_EXEC; if (!len) return -EINVAL; if (!(flags & MAP_FIXED)) addr = round_hint_to_min(addr); /* Careful about overflows.. */ len = PAGE_ALIGN(len); if (!len) return -ENOMEM; /* offset overflow? */ if ((pgoff + (len >> PAGE_SHIFT)) < pgoff) return -EOVERFLOW; /* Too many mappings? */ if (mm->map_count > sysctl_max_map_count) return -ENOMEM;
檢查下PROT_READ是否意味着可執行,如果是則給prot加上PROT_EXEC;如果映射的長度len為0,則返回;如果映射的地址可根據情況修正,則調用round_hint_to_min函數進行修正,具體來講先對hint進行頁對齊,如果對其后的地址小於mmap_min_addr,則根據mmap_min_addr來指定地址;同時對len也進行頁對齊操作;然后檢查頁內偏移是否溢出,坦白講這里我的確不太明白,pgoff加上一個樹還會小於pgoff?不太可能吧;接着檢查了當前進程映射的數目是否超額,如果超額,則返回。
經過一系列的檢查,就調用get_unmapped_area(file, addr, len, pgoff, flags);函數獲取一段可用的虛擬區間,進入函數內部
unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags) { unsigned long (*get_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); unsigned long error = arch_mmap_check(addr, len, flags); if (error) return error; /* Careful about overflows.. */ if (len > TASK_SIZE) return -ENOMEM; /*在進程結構的mm_stract中,有函數指針*/ get_area = current->mm->get_unmapped_area; /*如果文件結構指定了映射函數,優先使用文件結構中的*/ if (file && file->f_op && file->f_op->get_unmapped_area) get_area = file->f_op->get_unmapped_area; addr = get_area(file, addr, len, pgoff, flags); if (IS_ERR_VALUE(addr)) return addr; if (addr > TASK_SIZE - len) return -ENOMEM; if (addr & ~PAGE_MASK) return -EINVAL; addr = arch_rebalance_pgtables(addr, len); error = security_mmap_addr(addr); return error ? error : addr; }
這里內部檢查了映射的長度是否超額,即是否超過TASK_SIZE,如果合理,則獲取get_area函數,該函數在mm_struct中指定,當然如果file結構不為空且file操作表中也指定了該函數,則首選file結構中的函數。如果最終得到的addr>TASK_SIZE - len,即區間末尾溢出了,則返回;如果addr不是頁對齊的,也返回;不出意外,這里就返回地址addr了,后面的arch_rebalance_pgtables發現並沒有做什么,還有security_mmap_addr在沒有第三方的安全模塊時,才會進行適當的安全檢查;返回的結果要么是正確的addr,要么是返回的錯誤碼,那么到do_mmap_pgoff函數中繼續,if (addr & ~PAGE_MASK)其實是驗證是否是錯誤碼,如果錯誤就直接返回了;到這里獲取地址的工作就告一段落。
addr = get_unmapped_area(file, addr, len, pgoff, flags); if (addr & ~PAGE_MASK) return addr; /* Do simple checking here so the lower-level routines won't have * to. we assume access permissions have been handled by the open * of the memory object, so we don't do any here. */ 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; /* mlock MCL_FUTURE? */ if (vm_flags & VM_LOCKED) { unsigned long locked, lock_limit; locked = len >> PAGE_SHIFT; locked += mm->locked_vm; lock_limit = rlimit(RLIMIT_MEMLOCK); lock_limit >>= PAGE_SHIFT; if (locked > lock_limit && !capable(CAP_IPC_LOCK)) return -EAGAIN; }
接下來就是設置vm_flags,這里貌似並沒有做什么檢查,直接就設置了幾乎所有的標志。如果設置了MAP_LOCKED,還要檢查當前是否可以滿足條件:即當前是否有鎖定內存的能力,在這還要RLIMIT_MEMLOCK要大於0,否則返回;如果vm_flags包含了VM_LOCKED,需要當前lock后,lock的頁面是否超額。接下來就要進行具體映射了,當然前期還是進行一些檢查。這里分為兩部分,文件文件映射和匿名映射。
inode = file ? file_inode(file) : NULL; if (file) { switch (flags & MAP_TYPE) { case MAP_SHARED: if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE)) return -EACCES; /* * Make sure we don't allow writing to an append-only * file.. */ if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE)) return -EACCES; /* * Make sure there are no mandatory locks on the file. */ if (locks_verify_locked(inode)) return -EAGAIN; vm_flags |= VM_SHARED | VM_MAYSHARE; if (!(file->f_mode & FMODE_WRITE)) vm_flags &= ~(VM_MAYWRITE | VM_SHARED); /* fall through */ case MAP_PRIVATE: if (!(file->f_mode & FMODE_READ)) return -EACCES; if (file->f_path.mnt->mnt_flags & MNT_NOEXEC) { if (vm_flags & VM_EXEC) return -EPERM; vm_flags &= ~VM_MAYEXEC; } if (!file->f_op || !file->f_op->mmap) return -ENODEV; break; default: return -EINVAL; } } else { switch (flags & MAP_TYPE) { case MAP_SHARED: /* * Ignore pgoff. */ pgoff = 0; vm_flags |= VM_SHARED | VM_MAYSHARE; break; case MAP_PRIVATE: /* * Set pgoff according to addr for anon_vma. */ pgoff = addr >> PAGE_SHIFT; break; default: return -EINVAL; } } /* * Set 'VM_NORESERVE' if we should not account for the * memory use of this mapping. */ if (flags & MAP_NORESERVE) { /* We honor MAP_NORESERVE if allowed to overcommit */ if (sysctl_overcommit_memory != OVERCOMMIT_NEVER) vm_flags |= VM_NORESERVE; /* hugetlb applies strict overcommit unless MAP_NORESERVE */ if (file && is_file_hugepages(file)) vm_flags |= VM_NORESERVE; }
如果函數參數中file不為空,則肯定為文件映射,那么獲取其對應的inode節點。進入switch,根據不同的映射類型,進行檢查,然后主要是設置vm_flags。檢查類型如下:
MAP_SHARED:
如果保護位中允許寫而文件操作模式中不允許,則返回;如果文件模式允許寫,而inode節點是追加型節點,則返回;如果inode被強制加鎖,則返回;如果通過這些檢查,則給vm_flags添加VM_SHARED , VM_MAYSHARE;而file沒有寫權限,則從vm_flags去除VM_MAYWRITEVM_SHARED。
MAP_PRIVATE:
如果為私有映射,如果文件不允許讀,則返回;如果文件對應的文件系統沒有執行權且vm_flags中包含了執行權,則返回,如果沒有包含,則從vm_flags去除VM_MAYEXEC。接下來如果file中的f_op為空,或者f_op中的mmap函數為空,則返回。
如果file為空,則表明為匿名映射,映射的目的不是映射文件,而是划定一塊內存區,這種情況下進行下面判斷
MAP_SHARED:就忽略pgoff,增加vm_flags的共享權限。
MAP_PRIVATE:設置pgoff為 addr >> PAGE_SHIFT
接下來是對MAP_NORESERVE的判斷,根據情況判斷是否給vm_flags添加VM_NORESERVE,前期工作就到此為止了,剩下的調用了 mmap_region(file, addr, len, vm_flags, pgoff);
if (!may_expand_vm(mm, len >> PAGE_SHIFT)) { unsigned long nr_pages; /* * MAP_FIXED may remove pages of mappings that intersects with * requested mapping. Account for the pages it would unmap. */ if (!(vm_flags & MAP_FIXED)) return -ENOMEM; nr_pages = count_vma_pages_range(mm, addr, addr + len); if (!may_expand_vm(mm, (len >> PAGE_SHIFT) - nr_pages)) return -ENOMEM; }
第一部分檢查了地址空間的限制,如果已經映射的頁面加上本次需要的頁面數還小於指定額度,則沒問題,否則進入if判斷,在這里,因為已有的空間已經不足,如果沒有指定MAP_FIXED,就不能把發生沖突的map撤銷,就只能返回錯誤了;如果指定了MAP_FIXED,就調用count_vma_pages_range函數計算發生沖突的頁面數,然后預先作為這些頁面可用再次計算地址空間是否充足,如果仍然不夠,則只能返回錯誤,否則可以繼續。
munmap_back: if (find_vma_links(mm, addr, addr + len, &prev, &rb_link, &rb_parent)) { if (do_munmap(mm, addr, len)) return -ENOMEM; goto munmap_back; }
上面驗證結束后,就進入了munmap_back標記,這里就是找到沖突的映射,然后撤銷。但是這里還真有疑問,因為看find_vma_links函數,要么返回0,要么返回錯誤,不會返回正值,那么這里就if判斷始終是false,不曉得什么情況會進去!!
if (accountable_mapping(file, vm_flags)) { charged = len >> PAGE_SHIFT; if (security_vm_enough_memory_mm(mm, charged)) return -ENOMEM; vm_flags |= VM_ACCOUNT; } /* * Can we just expand an old mapping? */ vma = vma_merge(mm, prev, addr, addr + len, vm_flags, NULL, file, pgoff, NULL); if (vma) goto out; /* * Determine the object being mapped and call the appropriate * specific mapper. the address has already been validated, but * not unmapped, but the maps are removed from the list. */ vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL); if (!vma) { error = -ENOMEM; goto unacct_error; } vma->vm_mm = mm; vma->vm_start = addr; vma->vm_end = addr + len; vma->vm_flags = vm_flags; vma->vm_page_prot = vm_get_page_prot(vm_flags); vma->vm_pgoff = pgoff; INIT_LIST_HEAD(&vma->anon_vma_chain);
accountable_mapping函數檢查內存的可用性,具體還是不太明白。然后調用vma_merge函數嘗試擴展一個舊的vma結構,如果可以就goto到out,否則需要新創建一個vma,並進行設置。vma結構通過slab緩存管理,這里直接獲取一個即可。
if (file) { 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 = get_file(file); /*調用f_op->mmap進行映射*/ error = file->f_op->mmap(file, vma); if (error) goto unmap_and_free_vma; /* Can addr have changed?? * * Answer: Yes, several device drivers can do it in their * f_op->mmap method. -DaveM * Bug: If addr is changed, prev, rb_link, rb_parent should * be updated for vma_link() */ WARN_ON_ONCE(addr != vma->vm_start); addr = vma->vm_start; pgoff = vma->vm_pgoff; vm_flags = vma->vm_flags; } else if (vm_flags & VM_SHARED) { if (unlikely(vm_flags & (VM_GROWSDOWN|VM_GROWSUP))) goto free_vma; error = shmem_zero_setup(vma); if (error) goto free_vma; }
接下來又需要分情況討論,如果是文件映射,會調用file->f_op->mmap(file, vma)函數進行映射,否則對於匿名映射,調用shmem_zero_setup函數對其進行設置。
vma_link(mm, vma, prev, rb_link, rb_parent); file = vma->vm_file; /* Once vma denies write, undo our temporary denial count */ if (correct_wcount) atomic_inc(&inode->i_writecount); out: perf_event_mmap(vma); vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT); if (vm_flags & VM_LOCKED) { if (!((vm_flags & VM_SPECIAL) || is_vm_hugetlb_page(vma) || vma == get_gate_vma(current->mm))) mm->locked_vm += (len >> PAGE_SHIFT); else vma->vm_flags &= ~VM_LOCKED; } if (file) uprobe_mmap(vma); return addr; unmap_and_free_vma: if (correct_wcount) atomic_inc(&inode->i_writecount); vma->vm_file = NULL; fput(file); /* Undo any partial mapping done by a device driver. */ unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end); charged = 0; free_vma: kmem_cache_free(vm_area_cachep, vma); unacct_error: if (charged) vm_unacct_memory(charged); return error; }
2、用戶內存的動態分配
用戶內存的動態分配主要從來來自於兩個地方:堆、MMAP區域,C運行時庫對用戶提供了同一的動態分配接口malloc,而運行庫同樣有自己的內存管理器,內存管理器根據分配內存的大小采取不同的分配方式,具體來講調用不同的系統調用
當分配的內存<128KB時,從進程地址空間的堆空間分配。涉及函數brk(),sbrk()。
當分配的內存>=128KB時,從進程地址空間的MMAP區域分配。涉及函數mmap()。
這里先介紹下用戶空間的內存管理器,在一個進程首次調用malloc申請內存的時候,其會首次向內核申請遠大於用戶申請額度的內存區,申請之后把用戶請求的大小返回給用戶,而剩下的由內存管理器管理,在下次申請的時候就優先從這里申請而不用陷入到內核,這樣就減少了和內核交互的次數,從而提高性能。當前幾種常用的內存管理器有ptmallloc,temalloc和jcmalloc。
堆分配
堆的分配起始於brk系統調用,我們就跟着這條線看下
SYSCALL_DEFINE1(brk, unsigned long, brk) { unsigned long rlim, retval; unsigned long newbrk, oldbrk; struct mm_struct *mm = current->mm; unsigned long min_brk; bool populate; down_write(&mm->mmap_sem); #ifdef CONFIG_COMPAT_BRK /* * CONFIG_COMPAT_BRK can still be overridden by setting * randomize_va_space to 2, which will still cause mm->start_brk * to be arbitrarily shifted */ if (current->brk_randomized) min_brk = mm->start_brk; else min_brk = mm->end_data; #else min_brk = mm->start_brk; #endif if (brk < min_brk) goto out; /* * Check against rlimit here. If this check is done later after the test * of oldbrk with newbrk then it can escape the test and let the data * segment grow beyond its set limit the in case where the limit is * not page aligned -Ram Gupta */ rlim = rlimit(RLIMIT_DATA); if (rlim < RLIM_INFINITY && (brk - mm->start_brk) + (mm->end_data - mm->start_data) > rlim) goto out; newbrk = PAGE_ALIGN(brk); oldbrk = PAGE_ALIGN(mm->brk); if (oldbrk == newbrk) goto set_brk; /* Always allow shrinking brk. */ if (brk <= mm->brk) { if (!do_munmap(mm, newbrk, oldbrk-newbrk)) goto set_brk; goto out; } /* Check against existing mmap mappings. */ if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE)) goto out; /* Ok, looks good - let it rip. */ if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk) goto out; set_brk: mm->brk = brk; populate = newbrk > oldbrk && (mm->def_flags & VM_LOCKED) != 0; up_write(&mm->mmap_sem); if (populate) mm_populate(oldbrk, newbrk - oldbrk); return brk; out: retval = mm->brk; up_write(&mm->mmap_sem); return retval; }
函數不太長,就一次性列舉了。brk參數是數據段的終止位置。如果配置了棧兼容,代碼首先判斷是否有隨機地址,如果有則指定min_brk為mm->start_brk,否則指定為 mm->end_data。這里可以結合前面咱們的圖。如果沒有棧兼容,則直接指定為mm->start_brk。作為堆的起始,申請地址肯定必須大於等於起始地址的。同時每個進程有個RLIMIT_DATA的限制,用於限制data段的長度,這里主要包括堆、已經初始化的變量區、未初始化的變量區,分配后堆的大小加上已經初始化的數據區的大小不能大於數據段的大小限制,否則不允許。但是這里我沒明白為啥沒加上未經初始化的區的大小。如果通過就對新的brk位置和舊的位置進行對齊操作。如果兩個相等,就直接設置brk,實際上是返回當前堆結束地址;如果newbrk<oldbrk就需要釋放多余的堆了,調用do_munmap函數撤銷映射。接下來就檢查下申請的區間(本次擴充的堆空間)是否已經存在映射(存在VMA),如果存在就直接返回,否則調用do_brk函數進行映射,然后移動mm_struct中的brk指針,並返回最終的brk結束地址,到這里可以看到,堆空間的增長和減少都是線性的,即不存在從中間某個位置分配或者回收的情況,而這一機制需要用戶空間的內存分配器進行保證,我們常見的malloc和free並不會直接和堆打交道,而是直接和用戶空間的分配器如ptmalloc交互,對於用戶空間分配器,需要管理其從內核申請的堆空間,而面對內核,其需要做的規整一些,即不管是申請還是回收,直接給出堆的結束地址(細細品味)。do_brk函數即mmap函數的實現很類似,具體可參考前面的分析。
MMAP分配
當用戶申請的空間大於128kb時,內存管理器會直接調用mmap函數進行映射,不過不是映射文件,僅僅是占用進程地址空間的一段內存區。具體同樣在前面的分析中,感興趣可以參考。
參考:
linux3.10.1內核源碼