在linux中,我们常常说的内存为虚拟内存,而在存储器件上的为物理内存,两者之间可以进行相互转换。
其中,虚拟内存对应的为地址空间,他是虚拟的,在用户空间是可以把握住的,是为了应用程序访问的。而物理内存是存储空间,其为实际的存储区域,只有内核层才能进行访问。物理内存包括半导体内存和换页文件两部分。
当半导体内存不够用时,可以把一些长期闲置的代码和数据从半导体内存中缓存到换页文件中,这叫页面换出,一旦需要使用被换出的代码和数据,再把它们从换页文件恢复到半导体内存中,这叫页面换入。因此,系统中的虚拟内存比半导体内存大得多。
而每个进程都有独立的4G字节的虚拟内存,分别映射到不同的物理内存区域,而映射或者前文提到的内存换页的基本单位皆为1页,1页 = 4096字节。4g内存中高地址的1g内存被映射到内核区域的代码和数据中,这1g在各个进程中分享,所以说,用户只能访问3g的虚拟内存,3g的内存成为用户空间,而前文说的1g为内核空间,用户空间的代码只能访问用户空间的的数据,要想访问内核空间代码,需要特定的函数。
而3g的虚拟内存可以进一步划分为:
高地址————————————
命令行参数和环境变量
————————————————
栈(向下生长):非静态局部变量
堆(向上生长):动态函数族(malloc)
————————————————
bss区:没有初始值的全局变量和静态的局部变量
————————————————
数据区:非const型的有初值的全局和静态局部变量
—————————————————
代码段:只读常量(字面值常量,const有初值的全局变量和静态的局部变量),可执行的指令。
简而言之,栈保存的大多数为函数内的局部变量,堆保存的为malloc函数(c)、new(c++)等可以动态分配函数的变量,bss段保存一直存在的,且无初值的变量(static修饰的局部变量和全局变量在进程运行时一直存在)。而数据区保存一直存在的且有初值的变量。代码段保存的为字面值常量,即为一些不变量。
那么,如何完成对内存的分配与释放呢?
首先,分配的大致概念为:映射+占有,其中映射为地址空间(虚拟内存)和存储空间(物理内存)之间建立映射关系,说白了就是把他俩通过某条线连一块,从虚拟内存能够找到物理内存。而占有则为指定内存的归属,是指内存归属于哪一个文件或者进程。释放则相反,为放弃占有内存和解除映射。
在unix c中,我们常用的函数为void *sbrk(intptr_t increment); 其包含在<unistd.h>。其作用是移动堆顶指针,并且返回调用该函数之前的堆顶指针,例如:sbrk(10),则将指针向上移动10个字节(相当于扩大十个字节),并且返回一下移动之前的指针,如果失败则返回-1。对于其传入参数intptr_t型的increment为绝对地址,如果大于0,则将堆顶指针向上移动,增大内存空间,分配虚拟内存。而小于0时,则将指针向下移动,缩小内存空间,释放内存。如果等于0,则不分配也不释放内存空间,仅仅返回当前的堆顶指针。实例代码如下:
#include <stdio.h>
#include <unistd.h>
int main(){
setbuf(stdout,NULL);
int *p1 = (int*)sbrk(sizeof(int));
if(p1 == (int*)-1){
perror("sbrk");
return -1;
}
*p1 = 100;
printf("p1 value is %d, address is %p\n", *p1, p1);
double *p2 = (double*)sbrk(sizeof(double));
if(p2 == (double*)-1){
perror("sbrk");
return -1;
}
*p2 = 1.23;
printf("p2 value is %f, address is %p\n", *p2, p2);
if(sbrk(-(sizeof(double) + sizeof(int))) == (void*)-1){
perror("sbrk");
return -1;
}
return 0;
}
程序大致意思为:首先通过(int*)sbrk(sizeof(int))来分配一个int型大小的内存,并且将其未分配之前的首地址传入给p1,后将*p1传入一个值100,。之后用同样的方法分配一个double型大小的内存,并且传入数值为1.23。将其打印后,内存释放,通过sbrk(-(sizeof(double) + sizeof(int))实现。注意,语句中类似于p1 == (int*)-1,皆是强制类型转换,将-1转换为int*型。
运行结果如下:
pzpz@pzpz-VirtualBox:~/uc/day3$ ./sbrk
p1 value is 100, address is 0x55dce3029000
p2 value is 1.230000, address is 0x55dce3029004
上文中的sbrk传入参数为绝对地址,而int brk(void* end_data_segment)中参数为相对地址,其成功返回0,失败返回-1。使用方法与sbrk相似,具体实现代码如下:
#include <stdio.h>
#include <unistd.h>
int main(){
setbuf(stdout,NULL);
int *p1 = (int*)sbrk(sizeof(int));
if(p1 == (int*)-1){
perror("sbrk");
return -1;
}
*p1 = 100;
printf("p1 value is %d, address is %p\n", *p1, p1);
double *p2 = (double*)sbrk(sizeof(double));
if(p2 == (double*)-1){
perror("sbrk");
return -1;
}
*p2 = 1.23;
printf("p2 value is %f, address is %p\n", *p2, p2);
if(brk(p1) == -1){
perror("brk");
return -1;
}
return 0;
}
代码和上文代码基本一致,唯独在内存释放的时候有所改变,其改为了brk(p1),即直接将堆顶指针移到p1地址处,直接释放。
运行结果与上文类似,便不再赘述。
同样的,我们可以使用mmap函数来实现物理内存到虚拟内存的映射。
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset), 包含在<sys/mman.h>文件中。
其成功返回映射区虚拟内存的起始地址,失败返回MAP_FAILED
各个参数如下:
start : 映射区虚拟内存的起始地址,NULL为自动选择。
length:表示映射字节数。自动按页取整(上文有写到:1页 = 4096字节。
port :访问权限,可以取以下值:
PROT_READ - 可读
PROT_WRITE - 可写
PROT_EXEC - 可执行
PROT_NONE - 不可访问
flags : 映射标志,可以取以下值:
MAP_ANONYMOUS - 匿名映射,将虚拟内存映射到物理内存,函数的最后两个参数fd和offset被忽略
MAP_PRIVATE - 私有映射,将虚拟内存映射到文件的内存缓冲区中而非磁盘文件
MAP_SHARED - 共享映射,将虚拟内存映射到磁盘文件中
MAP_DENYWRITE - 拒写映射,文件中被映射区域不能存在其它写入操作
MAP_FIXED - 固定映射,若在start上无法创建映射,则失败(无此标志系统会自动调整)
MAP_LOCKED - 锁定映射,禁止被换出到换页文件
fd : 文件描述符。
offset :文件偏移量,自动按页对齐。
解除虚拟内存到物理内存或文件映射:用int munmap(void* start, size_t length);
成功返回0,失败返回-1。
一个简单的例子如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
int main(void){
char *p = mmap(NULL, 8192, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if(p == MAP_FAILED){
perror("mmap");
return -1;
}
sprintf(p, "first page");
sprintf(p + 4096, "second page");
printf("%s\n", p);
printf("%s\n", p + 4096);
if(munmap(p, 8192) == -1){
perror("munmap");
return -1;
}
return 0;
}
其输出结果较为简单:仅为打印
first page
second page