1.如何使用 malloc 函数
malloc是一个函数,专门用来从堆上分配内存。使用malloc函数需要几个要求:
内存分配给谁?
分配多大内存?
是否还有足够内存分配?
内存的将用来存储什么格式的数据,即内存用来做什么?
分配好的内存在哪里?
如果这五点都确定,那内存就能分配。下面先看malloc函数的原型:
1 |
|
看到了没有,这里的返回类型是(void *),巧妙的一个设计啊。
malloc函数的返回值是一个void类型的指针,参数为int类型数据,即申请分配的内存大小,单位是byte。内存分配成功之后,malloc函数返回这块内存的首地址。你需要一个指针来接收这个地址。但是由于函数的返回值是void *类型的,所以必须强制转换成你所接收的类型。也就是说,这块内存将要用来存储什么类型的数据。比如:
1 |
|
在堆上分配了100个字节内存,返回这块内存的首地址,把地址强制转换成char *类型后赋给char *类型的指针变量p。同时告诉我们这块内存将用来存储char类型的数据。也就是说你只能通过指针变量p来操作这块内存。这块内存本身并没有名字,对它的访问是匿名访问。
上面就是使用malloc函数成功分配一块内存的过程。但是,每次你都能分配成功吗?
不一定。
函数同样要注意这点:如果所申请的内存块大于目前堆上剩余内存块(整块),则内存分配会失败,函数返回NULL。注意这里说的“堆上剩余内存块”不是所有剩余内存块之和,因为malloc函数申请的是连续的一块内存。既然malloc函数申请内存有不成功的可能,那我们在使用指向这块内存的指针时,必须用if(NULL!=p)语句来验证内存确实分配成功了。
2. 用 malloc 函数申请 0 字节内存
另外还有一个问题:用malloc函数申请0字节内存会返回NULL指针吗?
可以测试一下,也可以去查找关于malloc函数的说明文档。申请0字节内存,函数并不返回NULL,而是返回一个正常的内存地址。但是你却无法使用这块大小为0的内存。这好尺子上的某个刻度,刻度本身并没有长度,只有某两个刻度一起才能量出长度。对于这一点一定要小心,因为这时候if(NULL!=p)语句校验将不起作用。
3.内存释放
既然有分配,那就必须有释放。不然的话,有限的内存总会用光,而没有释放的内存却在空闲。与malloc对应的就是free函数了。free函数只有一个参数,就是所要释放的内存块的首地址。比如上例:
1 |
|
free函数看上去挺狠的,但它到底作了什么呢?
其实它就做了一件事:斩断指针变量与这块内存的关系。
比如上面的例子,我们可以说malloc函数分配的内存块是属于p的,因为我们对这块内存的访问都需要通过p来进行。free函数就是把这块内存和p之间的所有关系斩断。从此p和那块内存之间再无瓜葛。至于指针变量p本身保存的地址并没有改变,但是它对这个地址处的那块内存却已经没有所有权了。那块被释放的内存里面保存的值也没有改变,只是再也没有办法使用了。
这就是free函数的功能。按照上面的分析,如果对p连续两次以上使用free函数,肯定会发生错误。因为第一使用free函数时,p所属的内存已经被释放,第二次使用时已经无内存可释放了。关于这点,我(陈正冲老师)上课时让学生记住的是:一定要一夫一妻制,不然肯定出错。
malloc两次只free一次会内存泄漏;malloc一次free两次肯定会出错。也就是说,在程序中malloc的使用次数一定要和free相等,否则必有错误。这种错误主要发生在循环使用malloc函数时,往往把malloc和free次数弄错了。
4.内存释放之后
既然使用free函数之后指针变量p本身保存的地址并没有改变,那我们就需要重新把p的值变为NULL:
1 |
|
这个NULL就是我们前面所说的“栓野狗的树桩”。如果你不栓起来迟早会出问题的。比如:
在free(p)之后,你用if(NULL!=p)这样的校验语句还能起作用吗?
例如:
1 2 3 4 5 6 7 8 9 |
|
释放完块内存之后,没有把指针置NULL,这个指针就成为了“野指针”,也有书叫“悬垂指针”。这是很危险的,而且也是经常出错的地方。所以一定要记住一条:free完之后,一定要给指针置NULL。
5.内存已经被释放了,但是继续通过指针来使用
这里一般有三种情况:
第一种:就是上面所说的,free(p)之后,继续通过p指针来访问内存。解决的办法就是给p置NULL。
第二种:函数返回栈内存。这是初学者最容易犯的错误。比如在函数内部定义了一个数组,却用return语句返回指向该数组的指针。解决的办法就是弄明白栈上变量的生命周期。
第三种:内存使用太复杂,弄不清到底哪块内存被释放,哪块没有被释放。解决的办法是重新设计程序,改善对象之间的调用关系。
对c语言野指针的解释
那到底什么是野指针呢?怎么去理解这个“野”呢?我们先看别的两个关于“野”的词:
野狗:没有主人的狗,没有链子锁着的狗,喜欢四处咬人。
对付野狗最好的办法就是拿条狗链把它拴在树桩上,不让它四处乱跑。
前面我们把内存比作尺子,很轻松的理解了内存。尺子上的0毫米处就是内存的0地址处,也就是NULL地址处。
在C语言中定义指针变量的同时最好初始化为NULL,用完指针free(p)之后也将指针变量的值设置为p=NULL。这样就把p这条狗成功地栓到NULL这个树桩上,那它就没有机会成为野狗了。所以说NULL可以看作专门栓“野指针”(野狗)的树桩。也就是说除了在使用时,别的时间都把指针“栓”到0地址处。这样它就老实了。
- malloc 是如何分配内存的?
- malloc 分配的是物理内存吗?
- malloc(1) 会分配多大的内存?
- free 释放内存,会归还给操作系统吗?
- free() 函数只传入一个内存地址,为什么能知道要释放多大的内存?
Linux 进程的内存分布长什么样?
在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址空间的范围也不同。比如最常见的 32 位和 64 位系统,如下所示:
通过这里可以看出:
32
位系统的内核空间占用1G
,位于最高处,剩下的3G
是用户空间;64
位系统的内核空间和用户空间都是128T
,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。
再来说说,内核空间与用户空间的区别:
- 进程在用户态时,只能访问用户空间内存;
- 只有进入内核态后,才可以访问内核空间的内存;
虽然每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。
接下来,进一步了解虚拟空间的划分情况,用户空间和内核空间划分的方式是不同的,内核空间的分布情况就不多说了。
我们看看用户空间分布的情况,以 32 位系统为例,我画了一张图来表示它们的关系:
通过这张图你可以看到,用户空间内存从低到高分别是 6 种不同的内存段:
- 程序文件段,包括二进制可执行代码;
- 已初始化数据段,包括静态常量;
- 未初始化数据段,包括未初始化的静态变量;
- 堆段,包括动态分配的内存,从低地址开始向上增长;
- 文件映射段,包括动态库、共享内存等,从低地址开始向上增长(跟硬件和内核版本有关 );
- 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是
8 MB
。当然系统也提供了参数,以便我们自定义大小;
在这 6 个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc()
或者 mmap()
,就可以分别在堆和文件映射段动态分配内存。
malloc 是如何分配内存的?
实际上,malloc() 并不是系统调用,而是 C 库里的函数,用于动态分配内存。
malloc 申请内存的时候,会有两种方式向操作系统申请堆内存。
- 方式一:通过 brk() 系统调用从堆分配内存
- 方式二:通过 mmap() 系统调用在文件映射区域分配内存;
方式一实现的方式很简单,就是通过 brk() 函数将「堆顶」指针向高地址移动,获得新的内存空间。如下图:
方式二通过 mmap() 系统调用中「私有匿名映射」的方式,在文件映射区分配一块内存,也就是从文件映射区“偷”了一块内存。如下图:
什么场景下 malloc() 会通过 brk() 分配内存?又是什么场景下通过 mmap() 分配内存?
malloc() 源码里默认定义了一个阈值:
- 如果用户分配的内存小于 128 KB,则通过 brk() 申请内存;
- 如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存;
1.malloc是如何分配内存的?
实际上,malloc()并不是系统调用,而是C库的函数,用于动态分配内存。
malloc申请内存的时候,会有两种方式向操作系统申请堆内存。
方式一:通过brk()系统调用从堆分配内存
方式二:通过mmap()系统调用在文件映射区域分配内存;
方式一实现的方式很简单,就是通过brk()函数将堆顶指针向高地址移动,获得新的内存空间。
方式二通过mmap()系统调用中私有匿名映射的方式,在文件映射区分配一块内存,,也就是从文件映射区投了一块内存。
什么场景malloc()会通过brk()分配内存?又是什么场景通过mmap()分配内存?
malloc()源码里默认定义了一个阈值:
如果用户分配的内存小于128KB,则通过brk()申请内存;
如果用户分配的内存大于128KB,则通过mmap()申请内存;
注意,不同的glibc版本定义的阈值也是不同的。
malloc()分配的是物理内存么?
不是的,malloc()分配的是虚拟内存。
如果分配后的虚拟内存没有被访问的话,是不会将虚拟内存映射到物理内存的,这样就不会占用物理内存了。
只有在访问已分配的虚拟地址空间的时候,操作系统通过查找页表,发现虚拟内存对应的页没有在物理内存中,就会触发中断,然后操作系统会建立虚拟内存和物理内存之间的映射关系。
malloc(1)会分配多大的虚拟内存?
malloc()在分配内存的时候,并不是老老实实按用户预期申请的字节数来分配内存空间大小,而是会预分配更大的空间作为内存池。
具体会预分配多大的空间,跟malloc使用的内存管理器有关系,我们就以malloc默认的内存管理器(Ptmalloc2)来分析。
通过实验,我们可以知道malloc(1)实际上预分配132K字节的内存。
free释放内存,会归还给操作系统么?
通过free释放内存后,堆内存还是存在的,并没有归还给操作系统。
这是因为预期把这1字节释放给操作系统,不如先缓存着放进malloc的内存池里,当进程再次申请1字节的内存时就可以直接复用,这样速度快了很多。
当然,当进程退出后,操作系统就会回收进程的所有资源。
上面说的free内存后堆内存还存在,是针对malloc通过brk()方式申请的内存的情况。
如果malloc通过mmap方式申请的内存,free释放内存后就会归还给操作系统。
为什么不全部使用mmap来分配内存?
因为向操作系统申请内存,是要通过系统调用的,执行系统调用是要进入内核态的,然后再回到用户态,运行态的切换会消耗不少时间。
所以,申请内存的操作应该避免频繁的系统调用,如果都用mmap来分配内存,等于每次都要执行系统调用。
另外,因为mmap分配的内存每次释放的时候,都会归还给操作系统,于是每次mmap分配的虚拟地址都是缺页状态的,然后再第一次访问该虚拟地址的时候,就会触发缺页中断。
也就是说,频繁通过mmap分配的内存,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致CPU消耗较大。
为了改进这两个为问题,malloc通过brk()系统调用在堆空间申请内存的时候,由于堆空间是连续的,所以直接预分配更大的内存来作为内存池,当内存释放的时候,就缓存在内存池中。
等下次在申请内存的时候,就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,这将大大降低CPU的消耗。
为什么不全部使用brk来分配?
前面我们提到通过brk从堆空间分配的内存,并不分钟归还给操作系统,那么我们考虑这样一个场景:如果我们连续申请了10k,20k,30k这三片内存,如果10k和20k这两片释放了,变为了空闲内存空间,如果下次申请的内存小于30k,那么就可以重用这个空闲的内存空间。
但是如果下次申请的内存大于30k,没有可用的空闲内存空间,必须向OS申请,实际使用内存继续增大。
因此,随着系统频繁地malloc和free,尤其对于小块内存,堆内将产生越来越多的不可用的碎片,导致“内存泄漏”。而这种“泄漏”现象使用valgrind是无法检测出来的。
所以,malloc实现中,充分考虑了brk和mmap行为上的差异及优缺点,默认分配大块内存(12KB)才使用mmap分配内存空间。
free()函数只传入一个内存地址,为什么能知道要释放多大的内存?
实际上,malloc返回给用户态的内存起始地址比进程的堆空间起始地址多了16字节.这个多出来的16字节就是保存了该内存块的描述信息,比如有该内存块的大小。
这样当执行free()函数时,free会对传进入的内存地址向左偏移16字节,然后从这个16字节的分析出当前的内存块的大小,自然就知道要释放多大的内存了。
原文链接:https://blog.youkuaiyun.com/qq_36638788/article/details/124593704