1

malloc()之后,内核发生了什么?

 2 years ago
source link: https://blogread.cn/it/article/6458?f=hot1
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

考虑这样一种常见的情况:用户进程调用malloc()动态分配了一块内存空间,再对这块内存进行访问。这些用户空间发生的事会引发内核空间的那些反映?本文将简单为您解答。

1.brk系统调用服务例程

malloc()是一个API,这个函数在库中封装了系统调用brk。因此如果调用malloc,那么首先会引发brk系统调用执行的过程。brk()在内核中对应的系统调用服务例程为SYSCALL_DEFINE1(brk, unsigned long, brk),参数brk用来指定heap段新的结束地址,也就是重新指定mm_struct结构中的brk字段。

brk系统调用服务例程首先会确定heap段的起始地址min_brk,然后再检查资源的限制问题。接着,将新老heap地址分别按照页大小对齐,对齐后的地址分别存储与newbrk和okdbrk中。

brk()系统调用本身既可以缩小堆大小,又可以扩大堆大小。缩小堆这个功能是通过调用do_munmap()完成的。如果要扩大堆的大小,那么必须先通过find_vma_intersection()检查扩大以后的堆是否与已经存在的某个虚拟内存重合,如何重合则直接退出。否则,调用do_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;

       down_write(&mm->mmap_sem);

#ifdef CONFIG_COMPAT_BRK

       min_brk = mm->end_code;

#else

       min_brk = mm->start_brk;

#endif

       if (brk < min_brk)

               goto out;

       rlim = rlimit(RLIMIT_DATA);

       if (rlim < RLIM_INFINITY && (brk - mm->start_brk) +

                       (mm->end_data - mm->start_data) > rlim)

       newbrk = PAGE_ALIGN(brk);

       oldbrk = PAGE_ALIGN(mm->brk);

       if (oldbrk == newbrk)

               goto set_brk;

       if (brk brk) {

               if (!do_munmap(mm, newbrk, oldbrk-newbrk))

                       goto set_brk;

               goto out;

       if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))

               goto out;

       if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)

               goto out;

set_brk:

       mm->brk = brk;

       retval = mm->brk;

       up_write(&mm->mmap_sem);

       return retval;

brk系统调用服务例程最后将返回堆的新结束地址。

2.扩大堆

用户进程调用malloc()会使得内核调用brk系统调用服务例程,因为malloc总是动态的分配内存空间,因此该服务例程此时会进入第二条执行路径中,即扩大堆。do_brk()主要完成以下工作:

1.通过get_unmapped_area()在当前进程的地址空间中查找一个符合len大小的线性区间,并且该线性区间的必须在addr地址之后。如果找到了这个空闲的线性区间,则返回该区间的起始地址,否则返回错误代码-ENOMEM;

2.通过find_vma_prepare()在当前进程所有线性区组成的红黑树中依次遍历每个vma,以确定上一步找到的新区间之前的线性区对象的位置。如果addr位于某个现存的vma中,则调用do_munmap()删除这个线性区。如果删除成功则继续查找,否则返回错误代码。

3.目前已经找到了一个合适大小的空闲线性区,接下来通过vma_merge()去试着将当前的线性区与临近的线性区进行合并。如果合并成功,那么该函数将返回prev这个线性区的vm_area_struct结构指针,同时结束do_brk()。否则,继续分配新的线性区。

4.接下来通过kmem_cache_zalloc()在特定的slab高速缓存vm_area_cachep中为这个线性区分配vm_area_struct结构的描述符。

5.初始化vma结构中的各个字段。

6.更新mm_struct结构中的vm_total字段,它用来同级当前进程所拥有的vma数量。

7.如果当前vma设置了VM_LOCKED字段,那么通过mlock_vma_pages_range()立即为这个线性区分配物理页框。否则,do_brk()结束。

可以看到,do_brk()主要是为当前进程分配一个新的线性区,在没有设置VM_LOCKED标志的情况下,它不会立刻为该线性区分配物理页框,而是通过vma一直将分配物理内存的工作进行延迟,直至发生缺页异常。

3.缺页异常的处理过程

经过上面的过程,malloc()返回了线性地址,如果此时用户进程访问这个线性地址,那么就会发生缺页异常(Page Fault)。整个缺页异常的处理过程非常复杂,我们这里只关注与malloc()有关的那一条执行路径。

当CPU产生一个异常时,将会跳转到异常处理的整个处理流程中。对于缺页异常,CPU将跳转到page_fault异常处理程序中:

//linux-2.6.34/arch/x86/kernel/entry_32.S

ENTRY(page_fault)

       RING0_EC_FRAME

       pushl $do_page_fault

       CFI_ADJUST_CFA_OFFSET 4

       ALIGN

error_code:

       jmp ret_from_exception

       CFI_ENDPROC

END(page_fault)

该异常处理程序会调用do_page_fault()函数,该函数通过读取CR2寄存器获得引起缺页的线性地址,通过各种条件判断以便确定一个合适的方案来处理这个异常。

3.1.do_page_fault()

该函数通过各种条件来检测当前发生异常的情况,但至少do_page_fault()会区分出引发缺页的两种情况:由编程错误引发异常,以及由进程地址空间中还未分配物理内存的线性地址引发。对于后一种情况,通常还分为用户空间所引发的缺页异常和内核空间引发的缺页异常。

内核引发的异常是由vmalloc()产生的,它只用于内核空间内存的分配。显然,我们这里需要关注的是用户空间所引发的异常情况。这部分工作从do_page_fault()中的good_area标号处开始执行,主要通过handle_mm_fault()完成。

//linux-2.6.34/arch/x86/mm/fault.c

dotraplinkage void __kprobes

do_page_fault(struct pt_regs *regs, unsigned long error_code)

good_area:

       write = error_code & PF_WRITE;

       if (unlikely(access_error(error_code, write, vma))) {

               bad_area_access_error(regs, error_code, address);

               return;

       fault = handle_mm_fault(mm, vma, address, write ? FAULT_FLAG_WRITE : 0);

3.2.handle_mm_fault()

该函数的主要功能是为引发缺页的进程分配一个物理页框,它先确定与引发缺页的线性地址对应的各级页目录项是否存在,如何不存在则分进行分配。具体如何分配这个页框是通过调用handle_pte_fault()完成的。

int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,

               unsigned long address, unsigned int flags)

       pgd_t *pgd;

       pud_t *pud;

       pmd_t *pmd;

       pte_t *pte;

       pgd = pgd_offset(mm, address);

       pud = pud_alloc(mm, pgd, address);

       if (!pud)

               return VM_FAULT_OOM;

       pmd = pmd_alloc(mm, pud, address);

       if (!pmd)

               return VM_FAULT_OOM;

       pte = pte_alloc_map(mm, pmd, address);

       if (!pte)

               return VM_FAULT_OOM;

         return handle_pte_fault(mm, vma, address, pte, pmd, flags);

3.3.handle_pte_fault()

该函数根据页表项pte所描述的物理页框是否在物理内存中,分为两大类:

请求调页:被访问的页框不再主存中,那么此时必须分配一个页框。

写时复制:被访问的页存在,但是该页是只读的,内核需要对该页进行写操作,此时内核将这个已存在的只读页中的数据复制到一个新的页框中。

用户进程访问由malloc()分配的内存空间属于第一种情况。对于请求调页,handle_pte_fault()仍然将其细分为三种情况:

static inline int handle_pte_fault(struct mm_struct *mm,

               struct vm_area_struct *vma, unsigned long address,

               pte_t *pte, pmd_t *pmd, unsigned int flags)

       if (!pte_present(entry)) {

               if (pte_none(entry)) {

                       if (vma->vm_ops) {

                               if (likely(vma->vm_ops->fault))

                                       return do_linear_fault(mm, vma, address,

                                               pte, pmd, flags, entry);

                       return do_anonymous_page(mm, vma, address,

                                                pte, pmd, flags);

               if (pte_file(entry))

                       return do_nonlinear_fault(mm, vma, address,

                                       pte, pmd, flags, entry);

               return do_swap_page(mm, vma, address,

                                       pte, pmd, flags, entry);

1.如果页表项确实为空(pte_none(entry)),那么必须分配页框。如果当前进程实现了vma操作函数集合中的fault钩子函数,那么这种情况属于基于文件的内存映射,它调用do_linear_fault()进行分配物理页框。否则,内核将调用针对匿名映射分配物理页框的函数do_anonymous_page()。

2.如果检测出该页表项为非线性映射(pte_file(entry)),则调用do_nonlinear_fault()分配物理页。

3.如果页框事先被分配,但是此刻已经由主存换出到了外存,则调用do_swap_page()完成页框分配。

由malloc分配的内存将会调用do_anonymous_page()分配物理页框。

3.4.do_anonymous_page()

此时,缺页异常处理程序终于要为当前进程分配物理页框了。它通过alloc_zeroed_user_highpage_movable()来完成这个过程。我们层层拨开这个函数的外衣,发现它最终调用了alloc_pages()。

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,

               unsigned long address, pte_t *page_table, pmd_t *pmd,

               unsigned int flags)

       if (unlikely(anon_vma_prepare(vma)))

               goto oom;

       page = alloc_zeroed_user_highpage_movable(vma, address);

       if (!page)

               goto oom;

经过这样一个复杂的过程,用户进程所访问的线性地址终于对应到了一块物理内存。

1.《深入理解LINUX内核》

2.《深入LINUX内核架构》

建议继续学习:

  1. 如何实现一个malloc    (阅读:4479)
QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK