一说到内存办理,咱们头脑中闪出的两个概念,便是虚拟内存,与物理内存。这两个概念首要来自于linux内核的支撑。
Linux在内存办理上份为两级,一级是线性区,类似于00c73000-00c88000,对应于虚拟内存,它实践上不占用实践物理内存;一级是详细的物理页面,它对应咱们机器上的物理内存。
这儿要说到一个很重要的概念,内存的推迟分配。Linux内核在用户请求内存的时分,仅仅给它分配了一个线性区(也便是虚存),并没有分配实践物理内存;只需当用户运用这块内存的时分,内核才会分配详细的物理页面给用户,这时分才占用名贵的物理内存。内核开释物理页面是经过开释线性区,找到其所对应的物理页面,将其悉数开释的进程。
[cpp] view plain copy
print?
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的负荷,许多都是互相矛盾的目标,这儿我就不细说了。
咱们在写程序时,触及内存时,有两个概念heap和stack。传统的说法stack的内存地址是向下增加的,heap的内存地址是向上增加的。
函数malloc和free,首要是针对heap进行操作,由程序员自主操控内存的拜访。
在这儿heap的内存地址向上增加,这句话不彻底正确。
glibc关于heap内存请求大于128k的内存请求,glibc选用mmap的办法向内核请求内存,这不能确保内存地址向上增加;小于128k的则选用brk,关于它来讲是正确的。128k的阀值,能够经过glibc的库函数进行设置。
这儿我先讲大块内存的请求,也即对应于mmap体系调用。
关于大块内存请求,glibc直接运用mmap体系调用为其划分出另一块虚拟地址,供进程独自运用;在该块内存开释时,运用unmmap体系调用将这块内存开释,这个进程中心不会发生内存碎块等问题。
针对小块内存的请求,在程序发动之后,进程会取得一个heap底端的地址,进程每次进行内存请求时,glibc会将堆顶向上增加来扩展内存空间,也便是咱们所说的堆地址向上增加。在对这些小块内存进行操作时,便会发生内存碎块的问题。实践上brk和sbrk体系调用,便是调整heap顶地址指针。
那么heap堆的内存是什么时分开释呢?
当glibc发现堆顶有接连的128k的空间是闲暇的时分,它就会经过brk或sbrk体系调用,来调整heap顶的方位,将占用的内存回来给体系。这时,内核会经过删去相应的线性区,来开释占用的物理内存。
下面我要讲一个内存空泛的问题:
一个场景,堆顶有一块正在运用的内存,而下面有很大的接连内存现已被开释掉了,那么这块内存是否能够被开释?其对应的物理内存是否能够被开释?
很惋惜,不能。
这也便是说,只需堆顶的部分请求内存还在占用,我在下面开释的内存再多,都不会被回来到体系中,依然占用着物理内存。为什么会这样呢?
这首要是与内核在处理堆的时分,过于简略,它只能经过调整堆顶指针的办法来调整调整程序占用的线性区;而又只能经过调整线性区的办法,来开释内存。所以只需堆顶不减小,占用的内存就不会开释。
提一个问题:
1
2
char *p=malloc(2);
free(p)
为什么请求内存的时分,需求两个参数,一个是内存巨细,一个是回来的指针;而开释内存的时分,却只需内存的指针呢?
这首要是和glibc的内存办理机制有关。glibc中,为每一块内存保护了一个chunk的结构。glibc在分配内存时,glibc先填写chunk结构中内存块的巨细,然后是分配给进程的内存。
1
2
chunk ——size
p———— content
在进程开释内存时,只需 指针-4 便能够找到该块内存的巨细,然后开释掉。
注:glibc在做内存请求时,最少分配16个字节,以便能够保护chunk结构。
glibc供给的调试东西:
为了便利调试,glibc 为用户供给了 malloc 等等函数的钩子(hook),如 __malloc_hook
对应的是一个函数指针,
1
void *function (size_t size, const void *caller)
其间 caller 是调用 malloc 回来值的接受者(一个指针的地址)。别的有 __malloc_iniTIalize_hook函数指针,仅仅会调用一次(榜首次分配动态内存时)。(malloc.h)
一些运用 malloc 的核算量(SVID 扩展)能够用 struct mallinfo 贮存,可调用取得。
1
struct mallinfo mallinfo (void)
怎么检测 memory leakage?glibc 供给了一个函数
void mtrace (void)及其反作用void muntrace (void)
这时会依靠于一个环境变量 MALLOC_TRACE 所指的文件,把一些信息记录在该文件中
用于侦测 memory leakage,其本质是安装了前面说到的 hook。一般将这些函数用
#ifdef DEBUGGING 包裹以便在非调试态下削减开支。发生的文件听说不主张自己去读,
而运用 mtrace 程序(perl 脚原本进行剖析)。下面用一个简略的比方阐明这个进程,这是
源程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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 = %p\nq = %p\n”, p, q ) ;
*p = 1 ;
*q = 2 ;
free( p ) ;
return 0 ;
}
很简略的程序,其间 q 没有被开释。咱们设置了环境变量后而且 touch 出该文件
履行成果如下:
p = 0x98c0378q = 0x98c0388
该文件内容如下
1
2
3
4
= Start
@./test30:[0x8048446] + 0x98c0378 0x4
@./test30:[0x8048455] + 0x98c0388 0x4
@./test30:[0x804848f] – 0x98c0378
到这儿我基本上讲完了,咱们写程序时,数据部分内存运用的问题。
代码占用的内存
数据部分占用内存,那么咱们写的程序是不是也占用内存呢?
在linux中,程序的加载,触及到两个东西,linker 和loader。Linker首要触及动态链接库的运用,loader首要触及软件的加载。
exec履行一个程序
elf为现在十分盛行的可履行文件的格局,它为程序运转划分了两个段,一个段是能够履行的代码段,它是只读,可履行;另一个段是数据段,它是可读写,不能履行。
loader会发动,经过mmap体系调用,将代码端和数据段映射到内存中,其实也便是为其分配了虚拟内存,留意这时分,还不占用物理内存;只需程序履行到了相应的当地,内核才会为其分配物理内存。
loader会去查找该程序依靠的链接库,首先看该链接库是否被映射进内存中,假如没有运用mmap,将代码段与数据段映射到内存中,不然仅仅将其参加进程的地址空间。这样比方glibc等库的内存地址空间是彻底相同。
因而一个2M的程序,履行时,并不意味着为其分配了2M的物理内存,这与其运转了的代码量,与其所依靠的动态链接库有关。
运转进程中链接动态链接库与编译进程中链接动态库的差异
咱们调用动态链接库有两种办法:一种是编译的时分,指明所依靠的动态链接库,这样loader能够在程序发动的时分,来一切的动态链接映射到内存中;一种是在运转进程中,经过dlopen和dlfree的办法加载动态链接库,动态将动态链接库加载到内存中。
这两种办法,从编程视点来讲,榜首种是最便利的,功率上影响也不大,在内存运用上有些不同。
榜首种办法,一个库的代码,只需运转过一次,便会占用物理内存,之后即便再也不运用,也会占用物理内存,直到进程的停止。
第二中办法,库代码占用的内存,能够经过dlfree的办法,开释掉,回来给物理内存。
这个不同首要关于那些寿数很长,但又会偶然调用各种库的进程有关。假如是这类进程,主张选用第二种办法调用动态链接库。
占用内存的丈量
丈量一个进程占用了多少内存,linux为咱们供给了一个很便利的办法,/proc目录为咱们供给了一切的信息,实践上top等东西也经过这儿来获取相应的信息。
1
2
3
4
5
/proc/meminfo 机器的内存运用信息
/proc/pid/maps pid为进程号,显现当时进程所占用的虚拟地址。
/proc/pid/statm 进程所占用的内存
[root@localhost ~]# cat /proc/self/statm
654 57 44 0 0 334 0
输出解说
CPU 以及CPU0。。。的每行的每个参数意思(以榜首行为例)为:
参数 解说 /proc//status
1
2
3
4
5
6
7
Size (pages) 使命虚拟地址空间的巨细 VmSize/4
Resident(pages) 应用程序正在运用的物理内存的巨细 VmRSS/4
Shared(pages) 同享页数 0
Trs(pages) 程序所具有的可履行虚拟内存的巨细 VmExe/4
Lrs(pages) 被映像到使命的虚拟内存空间的库的巨细 VmLib/4
Drs(pages) 程序数据段和用户态的栈的巨细 (VmData+ VmStk )4
dt(pages) 04
检查机器可用内存
1
2
3
4
5
/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的办法,来精确核算某一段虚拟地址所占用的内存,可能对咱们有用。