malloc()之后,内核发生了什么?【转】
考虑这样一种常见的情况:用户进程调用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()进行接下来扩大堆的各种工作。
1 |
SYSCALL_DEFINE1(brk, unsigned long , brk) |
2 |
{ |
3 |
unsigned long rlim, retval; |
4 |
unsigned long newbrk, oldbrk; |
5 |
struct mm_struct *mm = current->mm; |
6 |
unsigned long min_brk; |
7 |
8 |
down_write(&mm->mmap_sem); |
9 |
10 |
#ifdef CONFIG_COMPAT_BRK |
11 |
min_brk = mm->end_code; |
12 |
#else |
13 |
min_brk = mm->start_brk; |
14 |
#endif |
15 |
if (brk < min_brk) |
16 |
goto out; |
17 |
18 |
rlim = rlimit(RLIMIT_DATA); |
19 |
if (rlim < RLIM_INFINITY && (brk - mm->start_brk) + |
20 |
(mm->end_data - mm->start_data) > rlim) |
21 |
22 |
newbrk = PAGE_ALIGN(brk); |
23 |
oldbrk = PAGE_ALIGN(mm->brk); |
24 |
if (oldbrk == newbrk) |
25 |
goto set_brk; |
26 |
27 |
if (brk brk) { |
28 |
if (!do_munmap(mm, newbrk, oldbrk-newbrk)) |
29 |
goto set_brk; |
30 |
goto out; |
31 |
} |
32 |
33 |
if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE)) |
34 |
goto out; |
35 |
36 |
if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk) |
37 |
goto out; |
38 |
set_brk: |
39 |
mm->brk = brk; |
40 |
out: |
41 |
retval = mm->brk; |
42 |
up_write(&mm->mmap_sem); |
43 |
return retval; |
44 |
} |
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异常处理程序中:
1 |
//linux-2.6.34/arch/x86/kernel/entry_32.S |
2 |
ENTRY(page_fault) |
3 |
RING0_EC_FRAME |
4 |
pushl $do_page_fault |
5 |
CFI_ADJUST_CFA_OFFSET 4 |
6 |
ALIGN |
7 |
error_code: |
8 |
………… |
9 |
jmp ret_from_exception |
10 |
CFI_ENDPROC |
11 |
END(page_fault) |
该异常处理程序会调用do_page_fault()函数,该函数通过读取CR2寄存器获得引起缺页的线性地址,通过各种条件判断以便确定一个合适的方案来处理这个异常。
3.1.do_page_fault()
该函数通过各种条件来检测当前发生异常的情况,但至少do_page_fault()会区分出引发缺页的两种情况:由编程错误引发异常,以及由进程地址空间中还未分配物理内存的线性地址引发。对于后一种情况,通常还分为用户空间所引发的缺页异常和内核空间引发的缺页异常。
内核引发的异常是由vmalloc()产生的,它只用于内核空间内存的分配。显然,我们这里需要关注的是用户空间所引发的异常情况。这部分工作从do_page_fault()中的good_area标号处开始执行,主要通过handle_mm_fault()完成。
1 |
//linux-2.6.34/arch/x86/mm/fault.c |
2 |
dotraplinkage void __kprobes |
3 |
do_page_fault( struct pt_regs *regs, unsigned long error_code) |
4 |
{ |
5 |
…… …… |
6 |
good_area: |
7 |
write = error_code & PF_WRITE; |
8 |
9 |
if (unlikely(access_error(error_code, write, vma))) { |
10 |
bad_area_access_error(regs, error_code, address); |
11 |
return ; |
12 |
} |
13 |
fault = handle_mm_fault(mm, vma, address, write ? FAULT_FLAG_WRITE : 0); |
14 |
…… …… |
15 |
} |
3.2.handle_mm_fault()
该函数的主要功能是为引发缺页的进程分配一个物理页框,它先确定与引发缺页的线性地址对应的各级页目录项是否存在,如何不存在则分进行分配。具体如何分配这个页框是通过调用handle_pte_fault()完成的。
1 |
int handle_mm_fault( struct mm_struct *mm, struct vm_area_struct *vma, |
2 |
unsigned long address, unsigned int flags) |
3 |
{ |
4 |
pgd_t *pgd; |
5 |
pud_t *pud; |
6 |
pmd_t *pmd; |
7 |
pte_t *pte; |
8 |
…… …… |
9 |
pgd = pgd_offset(mm, address); |
10 |
pud = pud_alloc(mm, pgd, address); |
11 |
if (!pud) |
12 |
return VM_FAULT_OOM; |
13 |
pmd = pmd_alloc(mm, pud, address); |
14 |
if (!pmd) |
15 |
return VM_FAULT_OOM; |
16 |
pte = pte_alloc_map(mm, pmd, address); |
17 |
if (!pte) |
18 |
return VM_FAULT_OOM; |
19 |
return handle_pte_fault(mm, vma, address, pte, pmd, flags); |
20 |
} |
3.3.handle_pte_fault()
该函数根据页表项pte所描述的物理页框是否在物理内存中,分为两大类:
请求调页:被访问的页框不再主存中,那么此时必须分配一个页框。
写时复制:被访问的页存在,但是该页是只读的,内核需要对该页进行写操作,此时内核将这个已存在的只读页中的数据复制到一个新的页框中。
用户进程访问由malloc()分配的内存空间属于第一种情况。对于请求调页,handle_pte_fault()仍然将其细分为三种情况:
1 |
static inline int handle_pte_fault( struct mm_struct *mm, |
2 |
struct vm_area_struct *vma, unsigned long address, |
3 |
pte_t *pte, pmd_t *pmd, unsigned int flags) |
4 |
{ |
5 |
…… …… |
6 |
if (!pte_present(entry)) { |
7 |
if (pte_none(entry)) { |
8 |
if (vma->vm_ops) { |
9 |
if (likely(vma->vm_ops->fault)) |
10 |
return do_linear_fault(mm, vma, address, |
11 |
pte, pmd, flags, entry); |
12 |
} |
13 |
return do_anonymous_page(mm, vma, address, |
14 |
pte, pmd, flags); |
15 |
} |
16 |
if (pte_file(entry)) |
17 |
return do_nonlinear_fault(mm, vma, address, |
18 |
pte, pmd, flags, entry); |
19 |
return do_swap_page(mm, vma, address, |
20 |
pte, pmd, flags, entry); |
21 |
} |
22 |
…… …… |
23 |
} |
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()。
1 |
static int do_anonymous_page( struct mm_struct *mm, struct vm_area_struct *vma, |
2 |
unsigned long address, pte_t *page_table, pmd_t *pmd, |
3 |
unsigned int flags) |
4 |
{ |
5 |
…… …… |
6 |
if (unlikely(anon_vma_prepare(vma))) |
7 |
goto oom; |
8 |
page = alloc_zeroed_user_highpage_movable(vma, address); |
9 |
if (!page) |
10 |
goto oom; |
11 |
…… …… |
12 |
} |
经过这样一个复杂的过程,用户进程所访问的线性地址终于对应到了一块物理内存。
参考:
1.《深入理解LINUX内核》
2.《深入LINUX内核架构》
malloc()之后,内核发生了什么?【转】的更多相关文章
- linux内核发生Oops时怎么办?
1. 定位发生Oops的代码 1.1 通过addr2line命令定位 aarch64-openwrt-linux-gnu-addr2line -e vmlinux ffff000008087f00 1 ...
- 现在的 Linux 内核和 Linux 2.6 的内核有多大区别?
作者:larmbr宇链接:https://www.zhihu.com/question/35484429/answer/62964898来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转 ...
- 初探内核之《Linux内核设计与实现》笔记下
定时器和时间管理 系统中有很多与时间相关的程序(比如定期执行的任务,某一时间执行的任务,推迟一段时间执行的任务),因此,时间的管理对于linux来说非常重要. 主要内容: 系统时间 定时器 定时器相关 ...
- linux内核升级图文攻略(转)
一.Linux内核概览Linux是一个一体化内核(monolithic kernel)系统.设备驱动程序可以完全访问硬件.Linux内的设备驱动程序可以方便地以模块化(modularize)的形式设置 ...
- 《Linux内核设计与实现》读书笔记(十八)- 内核调试
内核调试的难点在于它不能像用户态程序调试那样打断点,随时暂停查看各个变量的状态. 也不能像用户态程序那样崩溃后迅速的重启,恢复初始状态. 用户态程序和内核交互,用户态程序的各种状态,错误等可以由内核来 ...
- 戴文的Linux内核专题:08内核配置(5)
转自Linux中国 Linux内核拥有许多可以配置的特性,接下来我们还有许多要配置. 下一个可以配置的特性是x86的随机数生成器(x86 architectural random number gen ...
- (转)Mac OS X内核编程,MAC驱动开发资源汇总
一.Mac OS X内核编程开发官方文档: I/O Kit Fundamentals: I/O Kit基础 - Mac OS X系统内核编程 https://developer.apple.com ...
- linux内核升级图文攻略
Linux内核概览 Linux是一个一体化内核(monolithic kernel)系统. 设备驱动程序可以完全访问硬件. Linux内的设备驱动程序可以方便地以模块化(modularize)的形式设 ...
- Linux 内核综述
一.什么是Linux内核: 内核->操作系统中最重要的部分,内核将在系统引导时被装载进RAM,其中包含了很多关键的例程,以操作系统.内核是OS最为关键的部分,人们常将OS(操作系统)与内核等同. ...
随机推荐
- 笔记-docker-3 使用
笔记-docker-3 使用 1. 镜像 image是docker最重要的概念,docker运行容器前需要本地存在对应的镜像,如果没有,会尝试从默认镜像库下载. 1.1. 镜像获取 查 ...
- TouTiao开源项目 分析笔记9 实现一个问答主页面
1.根据API返回创建几个基础的Bean 1.1.WendaArticleDataBean类 API返回的数据如下: /** * cell_type : 36 * extra : {"wen ...
- Android 浮动按钮+上滑隐藏按钮+下滑显示按钮
1.效果演示 1.1.关注这个红色的浮动按钮 . 可以看到,上滑的时候浮动按钮消失,因为用户迫切想知道下面的东西,而不是回到顶部. 当下滑的时候,用户想回到原来的位置,就可以点击浮动按钮,快速回到顶部 ...
- Java与Scala的两种简易版连接池
Java版简易版连接池: import java.sql.Connection; import java.sql.DriverManager; import java.util.LinkedList; ...
- 剑指Offer - 九度1512 - 用两个栈实现队列
剑指Offer - 九度1512 - 用两个栈实现队列2013-11-29 21:23 题目描述: 用两个栈来实现一个队列,完成队列的Push和Pop操作.队列中的元素为int类型. 输入: 每个输入 ...
- jekens介绍及服务搭建
https://blog.csdn.net/achuo/article/details/51086599 https://blog.csdn.net/qq_37372007/article/detai ...
- 孤荷凌寒自学python第二十天python的匿名函数与偏函数
孤荷凌寒自学python第二十天python的匿名函数与偏函数 (完整学习过程屏幕记录视频地址在文末,手写笔记在文末) Python为使函数的使用更加方便高效,可以使用两种特殊的函数简化语句书写. 一 ...
- C#共享内存
百度:C#共享内存. 文章:C# .Net 多进程同步 通信 共享内存 内存映射文件 Memory Mapped 转 资料:UnmanagedMemoryAccessor 资料:MemoryMappe ...
- jQuery基础知识点(下)
在实际开发中,jQuery的实践性非常强大.上一篇本人已经整理出了一部分基础知识点,该文即是对以上的补充和扩展. 1.表单值的操作 //获取文本框的值 //txt.value var val = $( ...
- 01、dos命令行的常用命令
cd 进入指定目录cd.. 返回上一级目录cd\ 退回盘符根目录dir 列出当前目录下的文件以及文件夹md 创建目录rd 删除目录del 删除文件cls ...