Linux核心:探秘malloc是如何申請記憶體的

今天分析下malloc申請記憶體時都發生了什麼,Let dot it

我們都清楚malloc申請的記憶體不是立刻就建立虛擬地址和物理地址的對映的,當int *p = malloc(100*1024)執行這條指令之後,只是在使用者空間給程式開闢一段100K左右的大小,然後就返回這段空間的首地址給程式設計師。

當我們嘗試第一次讀或者寫的時候,就會經過如下步驟的:

CPU將此虛擬地址,送到MMU上去

MMU會做虛擬到物理地址的轉化

MMU在操作時發現,此虛擬地址還沒有建立物理地址關係,則發生exception

CPU則會跳轉到exception table,根據出錯的型別執行相應的呼叫函式

此場景就會呼叫do_translation_fault

我們透過一個簡單的malloc例子來分析:

#include #include #include int main(){ int i=0; char *malloc_data=malloc(1024*200); printf(“malloc address=0x%lx\n”,malloc_data); getchar(); for(i=0; i<100; i++) malloc_data[i] = i+1; for(i=0; i<100; i++) printf(“data=%d\n”,malloc_data[i]); return 0;}

嵌入式進階教程分門別類整理好了,看的時候十分方便,由於內容較多,這裡就擷取一部分圖吧。

Linux核心:探秘malloc是如何申請記憶體的

需要的朋友私信【核心】即可領取。

核心學習地址:Linux核心原始碼/記憶體調優/檔案系統/程序管理/裝置驅動/網路協議棧-學習影片教程-騰訊課堂

當執行此程式碼後,會在使用者空間分配各個虛擬記憶體區域

Linux核心:探秘malloc是如何申請記憶體的

Linux核心:探秘malloc是如何申請記憶體的

可以看到虛擬地址是屬於紅色框之類的。有人就會說malloc為啥的不屬於heap? 當malloc申請的記憶體小於128K的時候是屬於heap的,自己可以動手實驗下。當申請的記憶體大於128K之後,就會從mmap區域申請記憶體的。

當我們嘗試寫這個虛擬地址的時候,就會發生上面一系列操作,我透過修改核心的程式碼,當在申請此虛擬地址的時候會發生panic,然後抓到dump。我們透過dump分析

Linux核心:探秘malloc是如何申請記憶體的

可以dump的時候此地址和上面例子的地址有差別,不影響我們分析。分析dump我們以dump的地址為準。

當寫malloc申請的記憶體0x76143BC000的時候,就會發生缺頁異常,發生page_fault。 先來看dump的呼叫棧

-005|panic()-006|do_anonymous_page(inline)-006|handle_pte_fault(vmf = 0xFFFFFF80202A3BF0)-007|handle_mm_fault(vma = 0xFFFFFFE314E27310, address = 0x00000076143BC000, flags = 0x55)-008|do_page_fault(addr = 0x00000076143BC008, esr = 0x92000047, regs = 0xFFFFFF80202A3EC0)-009|test_ti_thread_flag(inline)-009|do_translation_fault(addr = 0x00000076143BC008, esr = 0x92000047, regs = 0xFFFFFF80202A3EC0)-010|do_mem_abort(addr = 0x00000076143BC008, esr = 0x92000047, regs = 0xFFFFFF80202A3EC0)-011|el0_da(asm) ——>|exception

具體為啥會這樣,大家可以看下我前面的ARM64異常處理流程,咋們根據呼叫棧分析程式碼。

static int __kprobes do_translation_fault(unsigned long addr, unsigned int esr, struct pt_regs *regs){ if (addr < TASK_SIZE) return do_page_fault(addr, esr, regs); do_bad_area(addr, esr, regs); return 0;

這裡是判斷申請的記憶體屬於使用者空間還是核心空間,使用者空間的大小是TASK_SIZE的。小於此值就是使用者空間

-008|do_page_fault( | addr_=_0x00000076143BC008, //這就是我們上層傳遞下來的值,後面會將低12位清空的。 | esr = 0x92000047, //出錯狀態暫存器 | regs = 0xFFFFFF80202A3EC0) | vma = 0xFFFFFFE314E27310 //這段虛擬記憶體區域的vma | mm_flags = 0x55 | vm_flags = 0x2 | major = 0x0 | tsk = 0xFFFFFFE300786640 //所屬的task_struct | mm = 0xFFFFFFE2EBB33440 //所屬的mm_struct-009|test_ti_thread_flag(inline)-009|do_translation_fault( | addr = 0x00000076143BC008, | esr = 0x92000047, | regs = 0xFFFFFF80202A3EC0)-010|do_mem_abort( | addr = 0x00000076143BC008, | esr = 0x92000047, | regs = 0xFFFFFF80202A3EC0)-011|el0_da(asm) ——>|exception

此函式有點長,我們去掉不相關的,保留和我們有用的

static int __kprobes do_page_fault(unsigned long addr, unsigned int esr, struct pt_regs *regs){ struct task_struct *tsk; struct mm_struct *mm; struct siginfo si; vm_fault_t fault, major = 0; unsigned long vm_flags = VM_READ | VM_WRITE; unsigned int mm_flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE; struct vm_area_struct *vma = NULL; tsk = current; mm = tsk->mm; /* * If we‘re in an interrupt or have no user context, we must not take * the fault。 */ if (faulthandler_disabled() || !mm) //如果在中斷上下文或者是核心執行緒,就呼叫no_context處理 goto no_context; if (user_mode(regs)) //如果是使用者模式,則需要設定mm_flags位FAULT_FLAG_USER mm_flags |= FAULT_FLAG_USER; if (is_el0_instruction_abort(esr)) { //如果是el0的指令異常,設定flag vm_flags = VM_EXEC; } else if ((esr & ESR_ELx_WNR) && !(esr & ESR_ELx_CM)) { //esr暫存器判斷是否有寫許可權之類的 vm_flags = VM_WRITE; mm_flags |= FAULT_FLAG_WRITE; } if (addr < TASK_SIZE && is_el1_permission_fault(esr, regs, addr)) { //地址屬於使用者空間,但是出錯是在核心空間,也就是核心空間訪問了使用者空間的地址,報錯 /* regs->orig_addr_limit may be 0 if we entered from EL0 */ if (regs->orig_addr_limit == KERNEL_DS) die_kernel_fault(“access to user memory with fs=KERNEL_DS”, addr, esr, regs); if (is_el1_instruction_abort(esr)) die_kernel_fault(“execution of user memory”, addr, esr, regs); if (!search_exception_tables(regs->pc)) die_kernel_fault(“access to user memory outside uaccess routines”, addr, esr, regs); } if (!vma || !can_reuse_spf_vma(vma, addr)) //如果不存在vma,則透過地址找到vma,vma在mm_struct的紅黑樹中,只需要找此地址屬於start和end範圍內,就確定了vma vma = find_vma(mm, addr); fault = __do_page_fault(vma, addr, mm_flags, vm_flags, tsk); //真正處理do_page_fault major |= fault & VM_FAULT_MAJOR; //major意思是當發現此地址的轉化關係在頁表中,但是記憶體就找不到。說明swap到磁碟或者swap分割槽了。從磁碟將檔案swap進來叫major,從swap分割槽叫minor if (fault & VM_FAULT_RETRY) { //是否需要重試retry /* * If we need to retry but a fatal signal is pending, * handle the signal first。 We do not need to release * the mmap_sem because it would already be released * in __lock_page_or_retry in mm/filemap。c。 */ if (fatal_signal_pending(current)) { if (!user_mode(regs)) goto no_context; return 0; } /* * Clear FAULT_FLAG_ALLOW_RETRY to avoid any risk of * starvation。 */ if (mm_flags & FAULT_FLAG_ALLOW_RETRY) { mm_flags &= ~FAULT_FLAG_ALLOW_RETRY; mm_flags |= FAULT_FLAG_TRIED; /* * Do not try to reuse this vma and fetch it * again since we will release the mmap_sem。 */ vma = NULL; goto retry; } } up_read(&mm->mmap_sem); done: /* * Handle the “normal” (no error) case first。 */ if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP | VM_FAULT_BADACCESS)))) { /* * Major/minor page fault accounting is only done * once。 If we go through a retry, it is extremely * likely that the page will be found in page cache at * that point。 */ if (major) { //增減major的引用計數 tsk->maj_flt++; perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1, regs, addr); } else { tsk->min_flt++; //增加minor的引用計數 perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1, regs, addr); } return 0; } /* * If we are in kernel mode at this point, we have no context to * handle this fault with。 */ if (!user_mode(regs)) goto no_context; if (fault & VM_FAULT_OOM) { //也就是沒有記憶體了 /* * We ran out of memory, call the OOM killer, and return to * userspace (which will retry the fault, or kill us if we got * oom-killed)。 */ pagefault_out_of_memory(); return 0; } clear_siginfo(&si); si。si_addr = (void __user *)addr; if (fault & VM_FAULT_SIGBUS) { /* * We had some memory, but were unable to successfully fix up * this page fault。 */ si。si_signo = SIGBUS; si。si_code = BUS_ADRERR; } else if (fault & VM_FAULT_HWPOISON_LARGE) { unsigned int hindex = VM_FAULT_GET_HINDEX(fault); si。si_signo = SIGBUS; si。si_code = BUS_MCEERR_AR; si。si_addr_lsb = hstate_index_to_shift(hindex); } else if (fault & VM_FAULT_HWPOISON) { si。si_signo = SIGBUS; si。si_code = BUS_MCEERR_AR; si。si_addr_lsb = PAGE_SHIFT; } else { /* * Something tried to access memory that isn’t in our memory * map。 */ si。si_signo = SIGSEGV; //這就是寫應用程式,出錯後出現的段錯誤,核心直接回殺死此程序的 si。si_code = fault == VM_FAULT_BADACCESS ? SEGV_ACCERR : SEGV_MAPERR; } __do_user_fault(&si, esr); //訊號告知使用者層 return 0; no_context: __do_kernel_fault(addr, esr, regs); //處理核心的部分 return 0;}

此函式主要是確認下當前錯誤是來自核心還是應用層

當呼叫__do_page_fault處理完畢後,就會對結果做進一步處理

如果使用者空間,則後發訊號的方式告知的。

核心的話專門有__do_kernel_fault去處理的

static int __do_page_fault(struct vm_area_struct *vma, unsigned long addr, unsigned int mm_flags, unsigned long vm_flags, struct task_struct *tsk){ vm_fault_t fault; fault = VM_FAULT_BADMAP; if (unlikely(!vma)) goto out; if (unlikely(vma->vm_start > addr)) goto check_stack; /* * Ok, we have a good vm_area for this memory access, so we can handle * it。 */good_area: /* * Check that the permissions on the VMA allow for the fault which * occurred。 */ if (!(vma->vm_flags & vm_flags)) { fault = VM_FAULT_BADACCESS; goto out; } return handle_mm_fault(vma, addr & PAGE_MASK, mm_flags); check_stack: if (vma->vm_flags & VM_GROWSDOWN && !expand_stack(vma, addr)) goto good_area;out: return fault;}

檢查vma,以及起始地址

如果起始地址小於addr,則調到check_stack處,此情況針對棧需要擴張的情況

確定vma的許可權,比如此vma的許可權是沒有寫的,只讀的。如果你去寫的話就會報VM_FAULT_BADACCESS錯誤

則後續會呼叫handle_mm_fault處理

vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address, unsigned int flags){ vm_fault_t ret; __set_current_state(TASK_RUNNING); if (!arch_vma_access_permitted(vma, flags & FAULT_FLAG_WRITE, //許可權錯誤,直接SIGSEGV,段錯誤 flags & FAULT_FLAG_INSTRUCTION, flags & FAULT_FLAG_REMOTE)) return VM_FAULT_SIGSEGV; if (unlikely(is_vm_hugetlb_page(vma))) //巨型頁,先不考慮 ret = hugetlb_fault(vma->vm_mm, vma, address, flags); else ret = __handle_mm_fault(vma, address, flags); //正常處理流程 return ret;}

繼續分析__handle_mm_fault函式

static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma, unsigned long address, unsigned int flags){ struct vm_fault vmf = { //根據引數初始化vma_fault結構 。vma = vma, 。address = address & PAGE_MASK, 。flags = flags, 。pgoff = linear_page_index(vma, address), 。gfp_mask = __get_fault_gfp_mask(vma), 。vma_flags = vma->vm_flags, 。vma_page_prot = vma->vm_page_prot, }; unsigned int dirty = flags & FAULT_FLAG_WRITE; struct mm_struct *mm = vma->vm_mm; pgd_t *pgd; p4d_t *p4d; vm_fault_t ret; pgd = pgd_offset(mm, address); //根據虛擬地址和mm_struct結構找到pgd p4d = p4d_alloc(mm, pgd, address); //再接著找到p4d,模擬板目前只有3級頁表,也就是沒有p4d和pud,這裡的話p4d==pgd if (!p4d) return VM_FAULT_OOM; vmf。pud = pud_alloc(mm, p4d, address); if (!vmf。pud) return VM_FAULT_OOM; vmf。pmd = pmd_alloc(mm, vmf。pud, address); if (!vmf。pmd) return VM_FAULT_OOM; return handle_pte_fault(&vmf);}

pgd = pgd_offset(mm, address); 根據虛擬地址和mm_struct→pdg基地址就會算出pgd的值

p4d = p4d_alloc(mm, pgd, address); 分配p4d,目前沒用p4d,#define p4d_alloc(mm, pgd, address) (pgd) 直接返回的是pgd的值

vmf。pud = pud_alloc(mm, p4d, address);

#define pud_alloc(mm, p4d, address) \ ((unlikely(pgd_none(*(p4d))) && __pud_alloc(mm, p4d, address)) ? \ NULL : pud_offset(p4d, address))

是沒有p4d的時候,則分配pud,這裡因為p4d=pgd,則最後返回的是pgd裡面的值

vmf。pmd = pmd_alloc(mm, vmf。pud, address); 分配pmd, 會根據pud的值算出pmd的值

處理pte, 也就是說此函式就是算pgd, p4d, pud, pmd,儲存到vm_fault結構體中。

來看下dump中算好的結果。

-006|handle_pte_fault( | vmf = 0xFFFFFF80202A3BF0 -> ( | vma = 0xFFFFFFE314E27310, | flags = 0x55, | gfp_mask = 0x006000C0, | pgoff = 0x076143BC, | address = 0x00000076143BC000, | sequence = 0x2, | orig_pmd = (pmd = 0x0), | pmd = 0xFFFFFFE2E5E5D508 -> ( | pmd_=_0xE5E5A003), | pud = 0xFFFFFFE2E5D8BEC0 -> ( | pgd = (pgd = 0xE5E5D003)), | orig_pte = (pte = 0x0), | cow_page = 0x0, | memcg = 0x0, | page = 0x0, | pte = 0xFFFFFFE2E5E5ADE0 -> ( | pte = 0x00E800026F281F53), | ptl = 0xFFFFFFE3698EC318, | prealloc_pte = 0x0, | vma_flags = 0x00100073, | vma_page_prot = (pgprot = 0x0060000000000FD3)))-007|handle_mm_fault( | vma = 0xFFFFFFE314E27310, | address = 0x00000076143BC000, | flags = 0x55)

轉化過程可以參考我的ARM64虛擬地址到物理地址轉化文件(手動玩轉虛擬地址到物理地址轉化)

虛擬地址:0x00000076143BC000

mm_struct→pgd = rd(0xFFFFFFE2E5D8B000) = 0xE5D80003

pdg_index = (0x00000076143BC000 >> 30) & (0x200 - 1) = 0x01D8

pdg = 0xFFFFFFE2E5D8B000+ 0x01D8*8 = 0xFFFFFFE2E5D8BEC0 = rd(0xFFFFFFE2E5D8BEC0 ) = 0xE5E5D003

pmd_index = (0x00000076143BC000 >> 21) & (0x1FF ) = 0xA1

pmd = 0xE5E5D003+ 0xA1 * 8 = 0xE5E5D000+ 0xA1 * 8 = 0xE5E5D508 = rd(C:0xE5E5D508) = E5E5A003

透過我們手動計算和dump裡面的值是一樣的。繼續分析程式碼。

static vm_fault_t handle_pte_fault(struct vm_fault *vmf){ pte_t entry; int ret = 0; if (unlikely(pmd_none(*vmf->pmd))) { //如果pmd裡面的值是0的話,說明了pte是沒有的,則將vmf->pte設定為NULL vmf->pte = NULL; } else if (!(vmf->flags & FAULT_FLAG_SPECULATIVE)) { 。。。。 } if (!vmf->pte) { if (vma_is_anonymous(vmf->vma)) return do_anonymous_page(vmf); else return do_fault(vmf); } if (!pte_present(vmf->orig_pte)) return do_swap_page(vmf); entry = vmf->orig_pte; if (vmf->flags & FAULT_FLAG_WRITE) { if (!pte_write(entry)) return do_wp_page(vmf); entry = pte_mkdirty(entry); } entry = pte_mkyoung(entry); if (ptep_set_access_flags(vmf->vma, vmf->address, vmf->pte, entry, vmf->flags & FAULT_FLAG_WRITE)) { update_mmu_cache(vmf->vma, vmf->address, vmf->pte); } else { /* * This is needed only for protection faults but the arch code * is not yet telling us if this is a protection fault or not。 * This still avoids useless tlb flushes for 。text page faults * with threads。 */ if (vmf->flags & FAULT_FLAG_WRITE) flush_tlb_fix_spurious_fault(vmf->vma, vmf->address); if (vmf->flags & FAULT_FLAG_SPECULATIVE) ret = VM_FAULT_RETRY; }unlock: pte_unmap_unlock(vmf->pte, vmf->ptl); return ret;}

如果pmd裡面的值是NULL,所以pte不存在,設定pte為NULL

判斷此vma是否是匿名頁,透過判斷vma→vm_ops是否為NULL,

啥是匿名頁:

malloc申請的記憶體

stack裡申請的記憶體

mmap申請的匿名的記憶體對映

以上三種都屬於匿名頁

很明顯我們是malloc申請的記憶體,就會走到匿名頁裡面去

如果不是匿名頁,那就是有檔案背景的頁,就是和對映的時候有對應的實體,比如磁碟中的檔案

pte_present(vmf→orig_pte) 頁表存在,頁表項不存在,所以swap出去了,需要swap回來

如果頁表有寫FAULT_FLAG_WRITE許可權,則更新髒頁flag

pte_mkyoung(entry); 意思是頁表剛剛訪問過,比較young

設定訪問許可權,更新mmu cache等

原文地址:https://cloud。tencent。com/developer/article/1913493(版本歸原作者所有,侵權刪除)