您的位置:首页 > 其它

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

2015-06-29 17:02 295 查看
http://edsionte.com/techblog/archives/4174

2012年9月2日由edsionte留言»

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

1.brk系统调用服务例程

malloc()是一个API,这个函数在库中封装了系统调用brk。因此如果调用malloc,那么首先会引发brk系统调用执行的过程。brk()在内核中对应的系统调用服务例程为SYSCALL_DEFINE1(brk,unsignedlong,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
#ifdefCONFIG_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

(brkbrk){
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()返回了线性地址,如果此时用户进程访问这个线性地址,那么就会发生缺页异常(PageFault)。整个缺页异常的处理过程非常复杂,我们这里只关注与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_OFFSET4
6
ALIGN
7
error_code:
8
…………
9
jmpret_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内核架构》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: