内存管理
从底层硬件到上层应用,各层都提供了各自的内存管理接口,身处不同的开发层次,会使用不同层次的功能函数
malloc free 底层调用sbrk和brk,sbrk和brk分配则调用mmap/munmap,再往下是kmalloc和vmalloc,然后调驱动程序,驱动程序再驱动硬件
虚拟内存的分配和释放
- sbrk
#include
void* sbrk(intptr_t increment);
功能:以相对方式分配和释放虚拟内存
相对于当前指针
参数:increment 堆内存的字节增量(以字节为单位)
>0 分配内存
<0释放内存
=0 当前堆尾
返回值:成功返回调用该函数前的堆尾指针,失败返回-1
这里如果要判断失败,应该是这样
if(p == (void*)-1);
分配是在物理内存上分配一页4096个字节,而不只分配1字节2字节,然后与一个页的虚拟地址完成映射,而在使用这4096个字节时不会出现段错误,sbrk则限制了可以使用的字节,当一直扩大使用的字节,到一页结束可以暂时分配新一页。
系统内部维护一个堆尾指针,指向当前堆尾,即堆区最后一个字节的下一个位置,sbrk函数根据增量参数调整该指针的位置,同时返回该指针在调整前的位置,其间若发现内存页耗尽空闲,则自动追加或取消内存页的映射。
例如:
p1 = sbrk(4); // BBBB ^--- ---- ---- -
p2 = sbrk(4); // BBBB BBBB ^--- ---- -
p3 = sbrk(4); // BBBB BBBB BBBB ^--- -
p4 = sbrk(4); // BBBB BBBB BBBB BBBB ^
p5 = sbrk(0); // BBBB BBBB BBBB BBBB ^
p6 = sbrk(-8); // BBBB BBBB ^--- ---- -
p7 = sbrk(-8); // ^--- ---- ---- ---- -
B:已分配字节
-:未分配的字节
_:函数返回指针
^:当前堆尾指针
连续分配的sbrk虚拟地址是肯定连续的,即扩大可用空间,但是物理地址不一定
//sbrk函数演示
#include<stdio.h>
#include<unistd.h>
int main(void){
printf("%p\n",sbrk(0));
int* p1 = sbrk(sizeof(int));
printf("p1 = %p\n",p1);
*p1 = 123;
double* p2 = sbrk(sizeof(double));
printf("p2 = %p\n",p2);
*p2 = 4.56;
printf("%d %lg\n",*p1,*p2);
sbrk(-(sizeof(int) + sizeof(double)));
printf("%p\n",sbrk(0));
return 0;
}
这里为什么第一个和第二个不一样呢?
因为这是操作系统搞的,是系统版本内部的操作,中间差33页,没必要深究
而如果填写了更大的释放值,倒也没有报错,因为中间确实右33页内存
2.brk
#include
int brk(void* end_data_segment);
功能:以绝对方式分配和释放虚拟内存
参数:end_data_segment 堆尾指针的目标位置
> 堆尾指针的原位置 -分配内存
< 堆尾指针的原位置 -释放内存
= 堆尾指针的原位置 -空操作
返回值:成功返回0,失败返回-1
以绝对的方式,给什么地址就定位到哪里
系统内部维护一个指针,指向当前堆尾,即堆区最后一个字节的下一个位置,brk函数根据指针参数设置该指针的位置,其间若发现内存页耗尽空闲,则自动追加或取消内存页的映射
例如:
char p1 = sbrk(0); // ^--- ---- ---- ---- -
brk(p2 = p1 + 4); // BBBB ^--- ---- ---- -
brk(p3 = p2 + 4); // BBBB BBBB ^--- ---- -
brk(p4 = p3 + 4); // BBBB BBBB BBBB ^--- -
brk(p5 = p4 + 4); // BBBB BBBB BBBB BBBB ^
brk(p3); // BBBB BBBB ^--- ---- -
brk(1); // ^--- ---- ---- ---- -
B:已分配字节
-:未分配的字节
_:函数返回指针
^:当前堆尾指针
^:当前堆尾指针
//brk函数演示
#include<stdio.h>
#include<unistd.h>
int main(void){
printf("%p\n",sbrk(0));
int* p1 = sbrk(0);
brk(p1 + 1);
printf("p1 = %p\n",p1);
*p1 = 123;
double* p2 = sbrk(0);
brk(p2 + 1);
printf("p2= %p\n",p2);
*p2 = 4.56;
printf("%d %lg\n",*p1,*p2);
brk(p1); //这就是释放了
printf("%p\n",sbrk(0));
return 0;
}
这里需要注意!!!
int *p1 =sbrk(0);
这里p1是int类型指针,那么,p1+1则是加一个int类型,是加了4个字节!,所以这里是p1+1而不是p1+4!!!!!!!!
某种程度上,像是数组指针,而不是p[1]=*(p+1)
又p1指向初始未进行分配时的地址,顾brk(p1)相当于释放
当想用6个字节,可以自定义数据类型,比如结构体
struct{
char[6];
}
sbrk和brk的关系
1.事实上,sbrk和brk不过是移动堆尾指针的两种不同方法,移动过程中还要兼顾虚拟内存和物理内存之间映射关系的建立和接触(以页为单位)
2.用sbrk分配内存比较方便,用多少内存就传多少增量参数,同时返回指向新分配内存区域的指针,但用sbrk做一次性内存释放比较麻烦,因为必须将所有的既往增量进行累加
3.用brk释放内存比较方便,只需将堆尾指针设回到一开始的位置即可一次性释放掉之前分多次分配的内存,但用brk分配内存比较麻烦,因为必须根据所需要的内存大小计算出堆尾指针的绝对位置
4.用sbrk分多次分配适量内存,最后用brk一次性整体释放
内存映射的建立与解除
brk sbrk底层调用的就是下列两个函数,mmap和munmap
内存映射的建立
#include
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
功能:建立虚拟内存到物理内存或磁盘文件的映射
参数:start :映射区虚拟内存的起始地址,NULL系统自动选定后返回,这里,毕竟我们不知道虚拟内存哪些被使用,所以一般传空让操作系统去指定空间
length:映射区字节数,自动按页圆整
prot:映射区操作权限,可以取下值:
PROT_READ -映射区可读
PROT_WRITE -映射区可写
PROT_EXEC -映射区可执行
PROT_NONE -映射区不可访问
想一起就用或||,映射区一般指虚拟内存
flags: 映射标志,可以取以下值:
MAP_ANONYMOUS -匿名映射,将虚拟内存映射到物理内存而非文件,忽略fd和offset参 数
MAP_PRIVATE:-对映射区的写操作只反映到缓冲区中并不会真正写入文件
MAP_SHARED: -对映射区的写操作直接反映到文件中
MAP_DENYWRITE: -拒绝其他对文件的写操作
MAP_FIXED: -若在start上无法创建映射,则失败(无此标记系统会自动调整)
MAP_PRIVATE和MAP_SHARED是二选一的
fd :文件描述符
offset:文件偏移量,自动按页(4k)对齐
返回值:成功返回映射区虚拟内存的起始地址,失败返回MAP_FAILED(-1)
上边这俩是文件映射时再去考虑,物理内存映射时给0就行
解除内存映射
#include
int munmap(void* start,size_t length);
功能:解除虚拟内存到物理内存或磁盘文件的映射
参数:
start:映射区虚拟内存的起始地址
length:映射区字节数,自动按页圆整
返回值:成功返回0,失败返回-1
munmap允许对映射区的一部分解映射,但必须按页处理
解除映射后数据有没有消失不好说,看操作系统,有的清零有的不清,但是一定可以覆盖重写了
//内存的映射
#include<stdio.h>
#include<string.h>
#include<sys/mman.h>
int main(void){
//建立两个页的映射
char* p1 = mmap(NULL,8192,PROT_READ | PROT_WRITE,MAP_ANONYMOUS | MAP_PRIVATE,0,0);
if(p1 == MAP_FAILED){
perror("mmap");
return -1;
}
strcpy(p1,"第一个内存页");
printf("%s\n",p1);
//解除映射,允许部分页解除映射,但单位是页
if(munmap(p1,4096) == -1){ //只解除一页
perror("munmap");
return -1;
}
//printf("%s\n",p1); //段错误,接触映射后,p1已经无法找到物理内存
char* p2 = p1 + 4096;
strcpy(p2,"第二个内存页");
printf("%s\n",p2);
if(munmap(p2,4096) == -1){
perror("munmap");
return -1;
}
//printf("%s\n",p2); //段错误
return 0;
}
内存映射的建立和解除关系
1.mmap/munmap底层不维护任何东西,只是返回一个首地址,所分配内存位于堆中
2.brk/sbrk底层维护一个指针,记录所分配的内存结尾,所分配内存位于堆中,底层调用mmap/munmap
3.malloc底层维护一个线性链表和必要的控制信息,不可越界访问,所分配内存位于堆中,底层调用brk/sbrk
4.每个进程都有4G的虚拟内存空间,虚拟内存地址只是一个数字,在于实际物理内存建立映射之前是不能访问的
5.所谓内存分配与释放,其本质就是建立或解除从虚拟内存到物理内存的映射,并在底层维护不同形式的数据结构,以把虚拟内存的占用与空闲情况记录下来
系统调用
从应用程序到操作系统内核需要经历如下调用链