用户申请 (malloc) glibc 内核

用户申请 (malloc) glibc 内核
2010-10-08 19:49
一提到内存管理就有两个概念,就是虚拟内存与物理内存。这两个概念主要来自于linux内核的支持。

Linux在内存管理上份为两级,一级是线性区,类似于00c73000-00c88000,对应于虚拟内存,它实际上不占用实际物理内存;一级是具体的物理页面,它对应我们机器上的物理内存。

这里要提到一个很重要的概念,内存的延迟分配。Linux内核在用户申请内存的时候,只是给它分配了一个线性区(也就是虚存),并没有分配实际物理内存;只有当用户使用这块内存的时候,内核才会分配具体的物理页面给用户,这时候才占用宝贵的物理内存。内核释放物理页面是通过释放线性区,找到其所对应的物理页面,将其全部释放的过程。

char *p=malloc(2048) //这里只是分配了虚拟内存2048,并不占用实际内存。
strcpy(p,”123”) //分配了物理页面,虽然只是使用了3个字节,但内存还是为它分配了2048字节的物理内存。
free(p) //通过虚拟地址,找到其所对应的物理页面,释放物理页面,释放线性区。
我们知道用户的进程和内核是运行在不同的级别,进程与内核之间的通讯是通过系统调用来完成的。进程在申请和释放内存,主要通过brk,sbrk,mmap,unmmap这几个系统调用,传递的参数主要是对应的虚拟内存。注意一点,在进程只能访问虚拟内存,它实际上是看不到内核物理内存的使用,这对于进程是完全透明的。

glibc内存管理器那么我们每次调用malloc来分配一块内存,都进行相应的系统调用呢?答案是否定的,这里我要引入一个新的概念,glibc的内存管理器
我们知道malloc和free等函数都是包含在glibc库里面的库函数,我们试想一下,每做一次内存操作,都要调用系统调用的话,那么程序将多么的低效。实际上glibc采用了一种批发和零售的方式来管理内存。glibc每次通过系统调用的方式申请一大块内存(虚拟内存),当进程申请内存时,glibc就从自己获得的内存中取出一块给进程。内存管理器面临的困难我们在写程序的时候,每次申请的内存块大小不规律,而且存在频繁的申请和释放,这样不可避免的就会产生内存碎块。而内存碎块,直接会导致大块内存申请无法满足,从而更多的占用系统资源;如果进行碎块整理的话,又会增加cpu的负荷,很多都是互相矛盾的指标,这里我就不细说了。我们在写程序时,涉及内存时,有两个概念heapstack。传统的说法stack的内存地址是向下增长的,heap的内存地址是向上增长的。函数malloc和free,主要是针对heap进行操作,由程序员自主控制内存的访问。在这里heap的内存地址向上增长,这句话不完全正确。

glibc对于heap内存申请大于128k(这是一个阈值,M_MMAP_THRESHOLD)的内存申请,glibc采用mmap的方式向内核申请内存,这不能保证内存地址向上增长;小于128k的则采用brk,对于它来讲是正确的。128k的阀值,可以通过glibc的库函数进行设置。这里我先讲大块内存的申请,也即对应于mmap系统调用。对于大块内存申请,glibc直接使用mmap系统调用为其划分出另一块虚拟地址,供进程单独使用;在该块内存释放时,使用unmmap系统调用将这块内存释放,这个过程中间不会产生内存碎块等问题。针对小块内存的申请,在程序启动之后,进程会获得一个heap底端的地址,进程每次进行内存申请时,glibc会将堆顶向上增长来扩展内存空间,也就是我们所说的堆地址向上增长。在对这些小块内存进行操作时,便会产生内存碎块的问题。实际上brk和sbrk系统调用,就是调整heap顶地址指针。那么heap堆的内存是什么时候释放呢?当glibc发现堆顶有连续的128k的空间是空闲的时候,它就会通过brk或sbrk系统调用,来调整heap顶的位置,将占用的内存返回给系统。这时,内核会通过删除相应的线性区,来释放占用的物理内存。
下面我要讲一个内存空洞的问题:一个场景,堆顶有一块正在使用的内存,而下面有很大的连续内存已经被释放掉了,那么这块内存是否能够被释放?其对应的物理内存是否能够被释放?很遗憾,不能。这也就是说,只要堆顶的部分申请内存还在占用,我在下面释放的内存再多,都不会被返回到系统中,仍然占用着物理内存。为什么会这样呢?这主要是与内核在处理堆的时候,过于简单,它只能通过调整堆顶指针的方式来调整调整程序占用的线性区;而又只能通过调整线性区的方式,来释放内存。所以只要堆顶不减小,占用的内存就不会释放。提一个问题:

char *p=malloc(2);
free(p)为什么申请内存的时候,需要两个参数,一个是内存大小,一个是返回的指针;而释放内存的时候,却只要内存的指针呢?这主要是和glibc的内存管理机制有关。glibc中,为每一块内存维护了一个chunk的结构。glibc在分配内存时,glibc先填写chunk结构中内存块的大小,然后是分配给进程的内存。

chunk ------size
p------------ content在进程释放内存时,只要 指针-4 便可以找到该块内存的大小,从而释放掉。注:glibc在做内存申请时,最少分配16个字节,以便能够维护chunk结构。

glibc提供的调试工具:为了方便调试,glibc 为用户提供了 malloc 等等函数的钩子(hook),如 __malloc_hook对应的是一个函数指针,void *function (size_t size, const void *caller)其中 caller 是调用 malloc 返回值的接受者(一个指针的地址)。另外有 __malloc_initialize_hook函数指针,仅仅会调用一次(第一次分配动态内存时)。(malloc.h)一些使用 malloc 的统计量(SVID 扩展)可以用 struct mallinfo 储存,可调用获得。

struct mallinfo mallinfo (void)如何检测 memory leakage?glibc 提供了一个函数void mtrace (void)及其反作用void muntrace (void)这时会依赖于一个环境变量 MALLOC_TRACE 所指的文件,把一些信息记录在该文件中用于侦测 memory leakage,其本质是安装了前面提到的 hook。一般将这些函数用#ifdef DEBUGGING 包裹以便在非调试态下减少开销。产生的文件据说不建议自己去读,而使用 mtrace 程序(perl 脚本来进行分析)。下面用一个简单的例子说明这个过程,这是源程序:

#include
#include
#include
intmain( int argc, char *argv[] )
{
int *p, *q ;
#ifdef DEBUGGING
mtrace( ) ;
#endif
p = malloc( sizeof( int ) ) ;
q = malloc( sizeof( int ) ) ;
printf( "p = %pnq = %pn", p, q ) ;
*p = 1 ;
*q = 2 ;
free( p ) ;
return 0 ;
}很简单的程序,其中 q 没有被释放。我们设置了环境变量后并且 touch 出该文件执行结果如下:

p = 0x98c0378q = 0x98c0388该文件内容如下

= Star
t@./test30:[0x8048446] + 0x98c0378 0x4
@ ./test30:[0x8048455] + 0x98c0388 0x4
@ ./test30:[0x804848f] - 0x98c0378
到这里我基本上讲完了,我们写程序时,数据部分内存使用的问题。代码占用的内存数据部分占用内存,那么我们写的程序是不是也占用内存呢?在linux中,程序的加载,涉及到两个工具,linker 和loader。Linker主要涉及动态链接库的使用,loader主要涉及软件的加载。

1、 exec执行一个程序

2、 elf为现在非常流行的可执行文件的格式,它为程序运行划分了两个段,一个段是可以执行的代码段,它是只读,可执行;另一个段是数据段,它是可读写,不能执行。

3、 loader会启动,通过mmap系统调用,将代码端和数据段映射到内存中,其实也就是为其分配了虚拟内存,注意这时候,还不占用物理内存;只有程序执行到了相应的地方,内核才会为其分配物理内存。

4、 loader会去查找该程序依赖的链接库,首先看该链接库是否被映射进内存中,如果没有使用mmap,将代码段与数据段映射到内存中,否则只是将其加入进程的地址空间。这样比如glibc等库的内存地址空间是完全一样。因此一个2M的程序,执行时,并不意味着为其分配了2M的物理内存,这与其运行了的代码量,与其所依赖的动态链接库有关。运行过程中链接动态链接库与编译过程中链接动态库的区别。我们调用动态链接库有两种方法:一种是编译的时候,指明所依赖的动态链接库,这样loader可以在程序启动的时候,来所有的动态链接映射到内存中;一种是在运行过程中,通过dlopen和dlfree的方式加载动态链接库,动态将动态链接库加载到内存中。这两种方式,从编程角度来讲,第一种是最方便的,效率上影响也不大,在内存使用上有些差别。第一种方式,一个库的代码,只要运行过一次,便会占用物理内存,之后即使再也不使用,也会占用物理内存,直到进程的终止。第二中方式,库代码占用的内存,可以通过dlfree的方式,释放掉,返回给物理内存。这个差别主要对于那些寿命很长,但又会偶尔调用各种库的进程有关。如果是这类进程,建议采用第二种方式调用动态链接库。占用内存的测量测量一个进程占用了多少内存,linux为我们提供了一个很方便的方法,/proc目录为我们提供了所有的信息,实际上top等工具也通过这里来获取相应的信息。

/proc/meminfo 机器的内存使用信息
/proc/pid/maps pid为进程号,显示当前进程所占用的虚拟地址。
/proc/pid/statm 进程所占用的内存



/proc/28248/>free
total used free shared buffers cached
Mem: 1023788 926400 97388 0 134668 503688
-/+ buffers/cache: 288044 735744
Swap: 1959920 89608 1870312我们通过free命令查看机器空闲内存时,会发现free的值很小。这主要是因为,在linux中有这么一种思想,内存不用白不用,因此它尽可能的cache和buffer一些数据,以方便下次使用。但实际上这些内存也是可以立刻拿来使用的。所以 空闲内存=free+buffers+cached=total-used查看进程使用的内存查看一个进程使用的内存,是一个很令人困惑的事情。因为我们写的程序,必然要用到动态链接库,将其加入到自己的地址空间中,但是/proc/pid/statm统计出来的数据,会将这些动态链接库所占用的内存也简单的算进来。这样带来的问题,动态链接库占用的内存有些是其他程序使用时占用的,却算在了你这里。你的程序中包含了子进程,那么有些动态链接库重用的内存会被重复计算。因此要想准确的评估一个程序所占用的内存是十分困难的,通过写一个module的方式,来准确计算某一段虚拟地址所占用的内存,可能对我们有用。

 

<think>好的,我现在需要帮助用户了解malloc接口在内核中的实现机制,特别是源码分析部分。首先,用户提供的引用内容中有关于brk、sbrk、sys_brk、do_brk等函数的提及,以及Glibcmalloc实现原理。还有Linux内核的动态内存分配方式,比如alloc_pages、vmalloc、kmalloc等。此外,还有关于鸿蒙系统的Newlib C和BSS段的信息。 首先,用户的问题集中在malloc内核实现机制,需要结合用户提供的引用内容来分析。根据引用1,malloc调用brk,最终通过sys_brk系统调用进入内核的do_brk函数,可能涉及vma_merge和kmem_cache_zalloc。引用2提到Glibcmalloc通常使用sbrk来调整program break的位置,扩展堆空间。而引用3则说明Linux内核的动态内存分配方式,如kmalloc基于slab分配器,这可能与用户空间的内存管理不同。 需要注意的是,用户可能混淆了用户空间的malloc实现和内核内部的内存分配机制。Glibcmalloc属于用户空间库函数,而内核中的内存分配由kmalloc、vmalloc等处理。用户的问题可能希望了解从用户调用malloc内核处理的整个流程,包括系统调用和内核内部机制。 接下来,我需要整理malloc用户空间如何通过brk/sbrk或mmap申请内存,然后通过系统调用进入内核内核如何处理这些请求。例如,sys_brk系统调用会调用do_brk函数来扩展进程的堆空间,涉及虚拟内存区域(VMA)的管理,如合并相邻区域(vma_merge)或分配新的VMA结构(可能使用kmem_cache_zalloc来分配结构体)。 另外,引用3提到kmalloc基于slab分配器,而用户空间的malloc可能通过brk扩展堆,或者使用mmap分配大块内存。需要区分用户空间和内核空间的不同机制,但用户的问题可能更关注用户空间malloc如何与内核交互,即系统调用部分。 此外,鸿蒙系统的引用4提到Newlib C的实现,可能涉及不同的内存管理策略,但用户的问题可能主要关注Linux的实现,所以需要集中在那部分。 总结回答结构:首先说明用户空间malloc通过brk/sbrk或mmap系统调用进入内核内核处理sys_brk调用,执行do_brk,管理VMA,然后讨论内核自身的内存分配机制如kmalloc、vmalloc的区别。同时需要引用用户提供的引用内容来支持分析,比如引用1中的函数调用链,引用2中关于brk和sbrk的解释,引用3中的kmalloc实现等。 在生成相关问题的时候,需要考虑用户可能的后续问题,比如malloc的两种实现方式(brk和mmap),系统调用过程,用户空间和内核内存管理的区别,以及kmalloc的实现机制等。</think>### malloc 内核实现机制分析 malloc 作为用户空间的内存分配接口,其在内核中的实现涉及**系统调用**与**内核内存管理机制**的协作。整个过程可分为两个层面:**用户空间库函数实现**(如 Glibc)和**内核空间内存分配机制**。 --- #### 1. **用户空间到内核空间的桥梁** 当用户调用 `malloc` 时,Glibc 根据请求的内存大小选择分配方式: - **小内存分配(< 128KB)**:通过 `brk`/`sbrk` 系统调用调整堆顶指针 `program break`,扩展进程的堆空间[^2]。 - **大内存分配(≥ 128KB)**:使用 `mmap` 直接映射匿名内存页[^2]。 以 `brk` 为例,其内核实现流程如下: ```c 用户调用 mallocGlibc 调用 sbrk() → sys_brk() 系统调用 → 内核执行 do_brk() → 更新进程虚拟地址空间 ``` --- #### 2. **内核中的 `do_brk()` 实现** `do_brk()` 是 `sys_brk` 的核心函数,负责扩展或收缩进程的堆空间。其关键步骤包括: 1. **虚拟内存区域(VMA)管理**: - 尝试合并相邻的虚拟内存区域(`vma_merge()`)以减少碎片[^1]。 - 若无法合并,则新建 VMA 描述符(通过 `kmem_cache_zalloc()` 分配)[^1]。 2. **页表更新**: - 仅标记新增的虚拟地址范围,**不立即分配物理内存**(延迟分配,按需触发缺页中断)。 --- #### 3. **内核自身的内存分配机制** 内核通过以下接口管理物理内存: - **`kmalloc`**:基于 Slab 分配器,分配**物理地址连续**的内存块,适用于小对象(如 VMA 结构体)[^3]。 - **`vmalloc`**:分配**虚拟地址连续但物理地址可能不连续**的内存块,适用于大内存请求[^3]。 - **`alloc_pages`**:以页为单位直接分配物理页,用于底层内存管理[^3]。 --- #### 4. **关键差异:用户空间 vs 内核空间** - **用户空间堆管理**:通过 `brk`/`mmap` 调整虚拟地址范围,依赖内核的缺页中断分配物理页。 - **内核空间内存分配**:直接调用 `kmalloc`/`vmalloc`,需显式管理物理内存。 --- #### 示例:`do_brk()` 简化逻辑 ```c // 内核源码片段(简化) unsigned long do_brk(unsigned long addr, unsigned long len) { struct vm_area_struct *vma; // 1. 尝试合并现有 VMA vma = vma_merge(current->mm, prev, addr, addr + len, VM_GROWSUP); if (vma) goto out; // 2. 新建 VMA vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL); vma->vm_start = addr; vma->vm_end = addr + len; vma->vm_flags = VM_READ | VM_WRITE; // 3. 插入进程的 VMA 链表 vma_link(current->mm, vma); out: return addr; } ``` ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值