Linux下进程内存管理之malloc和sbrk

简介: 之前自己突发兴趣想写一下malloc函数,顺便了解一下进程的内存管理。在写的过程中发现其实malloc只不过是通过调用Linux下的sbrk函数来实现内存的分配,只是在sbrk之上加了一层对所分配的内存的管理罢了,而sbrk以及brk是实现从虚拟内存到内存的映射的。在实际动手写之前先来了解一下Linux下一个进程的内存空间分配。 进程内存空间分配 Linux下每个进程所分配的虚拟内

之前自己突发兴趣想写一下malloc函数,顺便了解一下进程的内存管理。在写的过程中发现其实malloc只不过是通过调用Linux下的sbrk函数来实现内存的分配,只是在sbrk之上加了一层对所分配的内存的管理罢了,而sbrk以及brk是实现从虚拟内存到内存的映射的。在实际动手写之前先来了解一下Linux下一个进程的内存空间分配。


进程内存空间分配

Linux下每个进程所分配的虚拟内存空间是3G,但实际使用过程中不可能也没有必要为一个进程分配如此大的空间,毕竟内存是很宝贵的资源。当一个进程执行的时候系统为其分配的内存空间主要包括数据段,代码段,栈,堆等等。而malloc所申请的空间就是从堆中分配的。先来看下面这张图:

这就是一个进程的内存空间,其中的Data Segment出要是存放已经初始化的静态数据,而BSS segment则存放为初始化的静态数据,在此之上的堆,然后是栈。值得注意的是,堆和栈的增长方向正好是相反的。现在先通过一段简单的代码来看一下data segment 和BSS segment的分配。

  8 #include <stdio.h>
  9 #include <stdlib.h>
 10 
 11 int bssvar;
 18 int dataSegmentVar = 1;
 19 
 20 int main()
 21 {
 22     printf("bssvar:%p, dataSegmentVar:%p,gap:%d", &bssvar, &dataSegmentVar,     ((int)&bssvar - (int)&dataSegmentVar));
 23     return 0;
 24 }  
程序运行的结果是:

bssvar:0x6008c0, dataSegmentVar:0x6008ac,gap:20

可以看到dataSegment在BSS segment之下,他们之间的有20个字节的空间也即data segment的分配空间大小是20字节。但是这个大小并不是固定的,如果程序中的静态未初始化变量大于20个字节,那么data segment的空间会相应地增长。


  8 #include <stdio.h>
  9 #include <stdlib.h>                                                                                                                                                              
 10 
 11 int bssvar, bssvar1, bssvar2, bssvar3, bssvar4, bssvar5;
 12 char c;
 13 int dataSegmentVar = 1;
 14 
 15 int main()
 16 {
 17     printf("bssvar:%p, dataSegmentVar:%p,gap:%d", &bssvar, &dataSegmentVar, ((int)&bssvar - (int)&dataSegmentVar));
 18     return 0;
 19 }
程序运行的结果是:

bssvar:0x6008c4, dataSegmentVar:0x6008ac,gap:24


这个时候dataSegment的变量是5个int和一个char,总共的大小是21个字节,而此时dataSegment的大小是24个字节,空间超过20,但是为了对齐,所以不是21而是24。


另外在上图中还有一个值得注意的地方就是program break,这是进程堆的末尾地址。当用户通过malloc函数申请空间的时候,实际就是利用sbrk函数移动program break,使其向上增长,以获得更大的堆空间。所以看起来很神秘的内存申请只不过是移动一个指针而已,哈哈。

不过这只是对简单的原理,里面还有很多细节需要考虑,接下来还是用一段程序来说。

  8 #include <stdio.h>
  9 #include <stdlib.h>
 10 
 11 int main()
 12 {
 13     void* ptr, *ptr1;
 14     ptr = sbrk(0);
 15     printf("sbrk:%p\n", ptr);
 16     ptr1 = malloc(100);
 17     ptr = sbrk(0);
 18     printf("sbrk:%p, ptr1:%p\n", ptr, ptr1);
 19     free(ptr1);
 20     ptr = sbrk(0);
 21     printf("sbrk:%p\n",ptr);
 22 }
~     

程序中首先用sbrk(0)得到堆部分的末尾地址,然后利用malloc申请了一个100字节长度的空间,这个时候再来看堆空间的末尾地址以及所申请的空间的地址。最后,再释放所申请的空间然后再来看堆空间地址。

程序的运行结果:

sbrk:0x2439000

sbrk:0x245a000, ptr1:0x2439010

sbrk:0x245a000

一开始堆区的末尾地址是0x2439000,但是当利用malloc申请完100字节的空间之后,堆区的末尾地址变为了0x245a000,一下子变大了0x21000。另外还值得注意的就是malloc所申请的空间的起始地址是0x2439010,比一开始的堆末尾地址向后移动了16个字节。这个不难理解,每一段内存空间都需要有一些元数据去管理该空间,所以我猜想这16个字节就是用来记录malloc所分配这100个字节空间的信息,包括大小,状态等等。

那么为什么明明只申请了100个字节的空间,program break却向后移动了这么多?这个也不难理解,总不能每次用户申请一段小的空间都去调用一次sbrk吧,这样的开销太大。所以干脆一次性分配一段大空间出来,除了用户所申请的空间之外,剩下的空间可以用于之后的malloc空间申请。来看下一段程序:

  8 #include <stdio.h>
  9 #include <stdlib.h>
 10 
 11 int main()
 12 {
 13     void* ptr, *ptr1;
 14     ptr = sbrk(0);
 15     printf("sbrk:%p\n", ptr);
 16     ptr1 = malloc(100);
 17     ptr = sbrk(0);
 18     printf("sbrk:%p, ptr1:%p\n", ptr, ptr1);
 19     ptr1 = malloc(100);
 20     ptr = sbrk(0);
 21     printf("sbrk:%p, ptr1:%p\n",ptr, ptr1);                                                                                                                                      
 22     free(ptr1);
 23     ptr = sbrk(0);
 24     printf("sbrk:%p\n",ptr);
 25 }

运行结果:

sbrk:0x933000

sbrk:0x954000, ptr1:0x933010

sbrk:0x954000, ptr1:0x933080

sbrk:0x954000

可以看到,尽管通过malloc函数申请了两块100字节的空间,但是program break并未因此而移动两次。另外,第一块空间和第二块空间的地址相差的不是100个字节而是112个字节,究其原因估计还是因为对齐的问题吧。

通过上文的讲解,我们发现,其实malloc也没有这么神秘了,它只不过就是利用sbrk来申请了一段空间罢了。不过除了申请空间之外,还需要管理这些空间才是malloc真正核心的地方。这些问题将在下一篇博文《自己动手写malloc》中详细讲解。

关于sbrk还有brk的用法网上有一大堆,这里就不细讲了,推荐一篇文章吧:点击打开链接


相关文章
|
10月前
|
缓存 监控 Linux
Linux内存问题排查命令详解
Linux服务器卡顿?可能是内存问题。掌握free、vmstat、sar三大命令,快速排查内存使用情况。free查看实时内存,vmstat诊断系统整体性能瓶颈,sar实现长期监控,三者结合,高效定位并解决内存问题。
939 0
Linux内存问题排查命令详解
|
安全 C语言 C++
比较C++的内存分配与管理方式new/delete与C语言中的malloc/realloc/calloc/free。
在实用性方面,C++的内存管理方式提供了面向对象的特性,它是处理构造和析构、需要类型安全和异常处理的首选方案。而C语言的内存管理函数适用于简单的内存分配,例如分配原始内存块或复杂性较低的数据结构,没有构造和析构的要求。当从C迁移到C++,或在C++中使用C代码时,了解两种内存管理方式的差异非常重要。
423 26
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
485 67
|
缓存 Linux 数据安全/隐私保护
Linux环境下如何通过手动调用drop_caches命令释放内存
总的来说,记录住“drop_caches” 命令并理解其含义,可以让你在日常使用Linux的过程中更加娴熟和自如。
2000 23
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
446 16
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
290 20
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
320 0
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
382 0
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
254 0
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
330 0