UnixC内存管理那些事儿(上)

对于学习C语言的人来说,相信大家应该都能够明白指针和内存管理的重要性。在实际的编程过程中,总是会有许多的bug产生是因为对指针的运用不当和对内存管理的细节不清楚所导致的。今天我们就来看一看在Unix/Linux环境下的内存管理的那些事儿。

       让我们先来看一下下面的两幅图,相信你看完后应该可以对一些语言中的内存实现函数或运算符,以及进程内存空间的分布问题有一个初步的了解

不同语言之间对内存管理的相应函数及运算符:


进程内存空间分布:


从低地址到高地址的分布顺序依次是:

代码区:用来存放代码(函数)的只读区域,函数指针所指向的便是这个区中函数的地址

只读常量区:由于它和代码区离得非常近,而且同样为只读属性,所以在很多书中也将其和代码区归到一起,用来存放字符串的字面值如"abc"和const修饰的全局变量

全局区:也叫静态区,存放的是全局变量和使用static修饰的局部变量,是读写区域

bss段:用来存放未经初始化的全局变量。bss段和全局区在main函数执行之前被创建,不同的是bss段会在main函数执行之前自动清零

堆区:也叫自由区,该区内的内存由程序员自行管理,系统不会对该区内存做任何处理。malloc()函数操作的便是堆区。在i386体系中,堆区向高地址方向延伸

栈区:用来存放未经static修饰的局部变量,函数参数等。栈区的内存由系统自动管理,程序员可以通过特定的函数参与内存的管理,但一般不建议使用。栈区的地址大概在3G左右。在i386体系中,栈向低地址方向延伸。这与堆区相反,由于栈区和堆区之间相隔很远,并不需要担心他们会相交

       在Unix系统中,我们所接触到的内存地址其实都是虚拟地址,并不是真实的物理地址。一个虚拟地址其实就是一个整数,本质上并不能存储任何数据,只有映射到物理内存或硬盘文件才能实现存储数据的功能。在32位的操作系统中,每个进程在启动后就具有了0~4G的虚拟内存地址空间,分为用户空间和内核空间,其中0~3G的地址属于用户空间,供用户使用,而3~4G的地址属于内核空间,供系统内核调用。用户空间的程序不能直接访问内核空间,但是可以通过系统调用来访问内核空间。所谓的内存分配其实就是先分配未使用的虚拟内存地址,然后再将其映射到物理内存/硬盘文件上。进程之间即使内存地址相同,映射到的物理内存也是不同的。如果引用了未经映射的内存或是对内存进行了没有权限的操作,如修改只读区,都会报段错误。在Unix/Linux系统中,虚拟内存地址是以字节来管理的,但是在做内存映射的时候并不是以字节为单位进行映射的,而是以内存页为单位进行映射。在32位的操作系统中,一个内存页的大小为4096字节(4K)。使用getpagesize()函数可以获取内存页的大小。在Linux操作系统中,一切都可以以文件来表示,内存也不例外。在Linux系统中,可以查看/proc/pid/maps可以查看相应pid的进程内存映射的情况,其中pid就是进程编号。在程序中通过getpid()函数获取进程的pid,再到maps文件中对比查看相应进程的内存分配情况。

       好的,对于操作系统中内存的一些细节就先说到这里。接下来就来介绍一下在Unix/Linux系统中的一些常用的6个内存分配函数malloc()、free()、brk()、sbrk()、mmap()、munmap()

内存分配函数之malloc():

1.malloc()函数分配的堆空间内存是不连续的。

2.每次使用malloc()函数申请小块内存时会映射33个内存页,如果申请的是大块内存(31个内存页以上),则会映射比申请稍多一点的内存。虽然malloc函数会映射多个内存页,导致未分配的内存地址只要在这些内存页当中就可以使用。但是为了便于内存的管理(反复使用和控制),还是应当遵循先分配后使用的原则。

3.使用malloc函数申请的内存空间由三个部分组成:附加数据空间,数据空间,预留空间。其中附加数据空间位于分配内存首地址之前,其中存放着关于分配空间大小的信息,指向下一个块分配的内存空间的指针等。整个附加数据空间存放在一个由操作系统底层维护的双向链表中。数据空间呢,则是由用户自行申请出来的内存大小。预留空间则大小不确定,由操作系统安排,可大可小。

例子:

#include <stdio.h>
#include <stdlib.h>
int main(){
	int a,b,c;//栈
	printf("%p,%p,%p\n",&a,&b,&c);
	int *p1 = malloc(4); //分配+映射
	int *p2 = malloc(4); //分配
	int *p3 = malloc(4); //分配
	printf("%p,%p,%p\n",p1,p2,p3);
	int *pi = &a;
	*(pi + 100) = 100; //映射未分配的内存,能用
	*(p1 + 100) = 100; //已映射未分配的堆内存
	printf("%d,%d\n",*(pi+100),*(p1+100));
	//*(p1-1) = 0; //清除p1的附加数据,附加数据在p的前面
	free(p1); 
}

内存回收函数之free():

free函数用于释放申请的堆内存,如果申请的堆内存没有进行释放,那么就会导致内存泄露(MemoryLeak)的产生。当大型程序运行时,如果使用了malloc函数动态申请了内存却没有使用free函数进行回收,那么该进程所占用的内存空间就会持续增加,直至不再有空闲空间。此时,由于过度的换页开销,会造成程序的性能下降。

1.free函数一定会释放掉被占用的虚拟内存地址空间,但却不一定会解除内存映射。当你使用free函数去回收大块内存时,free函数在释放虚拟地址空间的同时也会解除内存映射。

2.对于使用malloc函数分配出来的内存而言,free函数不会解除最后33个内存页的映射。这33个内存页在进程结束后才会释放。

3.free函数会清空内存,但却不保证将其清除为0。

例子:

#include <stdio.h>
#include <stdlib.h>
int main(void){
	int *pi = malloc(40*4096);
	printf("%p\n",pi);
	printf("pid=%d\n",getpid());
	sleep(20);
	free(pi); //free()大块内存会解除映射
	printf("free all\n");
	return 0;
}

总结:

1.每次使用malloc()函数分配内存时都应当注意使用free()函数进行内存的回收,否则将会导致内存泄露

2.malloc()函数分配出来的内存空间是不连续的,但并不影响我们使用

3使用malloc()函数时一定要避免越界访问,否则会影响内存下一次的分配和回收


PS:为避免长篇大论,使人产生阅读疲劳,如果有兴趣了解可以看我的另一篇博文——Unix内存管理那些事儿(下)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值