内存管理
用户层
STL(*) | 自动分配/自动释放 | 调用C++库 |
---|---|---|
C++ | new/delete | 调用C |
C | malloc/free | 调用POSIX |
POSIX | brk/sbrk | 调用Linux |
Linux | mmap/munmap | 调用内核(kernal) |
系统层
kernal | kamlloc/vmalloc | 调用驱动 |
dirver | get_free_page | 内层汇编指令 |
进程映像
程序是存储在磁盘上的可执行文件,当程序运行时,系统就会将可执行程序加载到内存中,在内存中形成进程(一个程序可以同时加载多个进程)
进程的内存空间分布情况就是进程映像,从低到高地址依次分布为:
text | 代码段 | 二进制指令,常量(字符串字面值,被const修饰过的全原data段数据) |
---|---|---|
只读段 | ||
data | 数据段 | 初始化过的全局变量和静态变量 |
bss | 静态数据段 | 未初始化过的全局变量和静态变量 |
该段内存会在进程运行前清理为0 | ||
heap | 堆 | 体量比较大的数据,结构变量 |
手动管理,申请释放可控,空间大,使用时与指针配合,容易有内存碎片,内存泄漏 | ||
stack | 栈 | 局部变量,块变量 |
大小有限,自动申请释放内存,不会产生内存碎片、内存泄漏 | ||
environ | 环境变量表 | 环境变量,每个进程各一份,修改不会影响其他进程 |
argv | 命令行参数 | 程序执行时附加的参数 |
打印出每段内存段的数据的地址,与该进程的内存记录文件比一下
getpid();
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
int num = 100;
int bss;
int main(int argc,const char* argv[])
{
printf("text: %p\n",main);
printf("read: %p\n","hehe");
printf("data: %p\n",&num);
printf("bss: %p\n",&bss);
printf("heap: %p\n",malloc(4));
int num1=10;
printf("stack:%p\n",&num1);
extern char** environ;
printf("environ:%p\n",&environ);
printf("argv: %p\n",argv);
printf("vi /proc/%d/maps\n",getpid());
scanf("%*c");
return 0;
}
虚拟内存
1、系统会给每个进程分配4G(32位)虚拟内存空间
32个0 ~ 32个1 地址范围
2、用户只能使用虚拟内存地址,无法直接使用物理内存地址
3、虚拟地址与物理内存之间需要进行映射才能使用,(使用没有映射的内存)否则就是产生段错误
4、虚拟地址与物理内存的映射有操作系统动态维护
5、让用户使用虚拟地址一方面为了安全,另一方面操作系统可以让应用程序使用比实际物理内存更大的地址空间
6、4G的虚拟地址为两部分
[0~3) 用户空间 (大部分时间用的是这个),每个进程的用户空间是独立的
[3~4) 系统空间 (内核态、内核空间)
7、用户空间中的代码确实不能直接访问内核空间的代码和数据,可以通过系统调用和API来切换到内核态,间接的与内核交换数据
8、对虚拟内存越界访问(使用了没有映射的内存),将导致段错误
映射虚拟内存与物理内存的函数
#include <unistd.h>
int brk(void *addr);
功能:直接使用addr值来修改指针的位置
addr:
> 位置指针 映射内存
< 位置指针 取消映射
返回值:成功返回0,失败返回-1
注意:系统映射内存是以页(1页=4096字节)为单位
系统内存维护一个指针指向内存映射的最后一个字节的下一个位置
void *sbrk(intptr_t increment);
功能:通过增量参数来调整该指针的位置,既能映射也能取消映射
increment:增量
0 获取指针的位置
>0 映射内存
<0 取消映射
返回值:如果是映射内存,返回新映射内存的首地址()
注意:sbrk/brk都是POSIX标准的内存映射函数,都单独进行映射、取消映射的功能,但是配合使用最方便(sbrk映射、brk取消映射)
例:
#include<stdio.h>
#include<unistd.h>
int main(int argc,const char* argv[])
{
// 获取开始的指针位置
int* arr = sbrk(0);
for(int i=0;i<10;i++)
{
// 映射
sbrk(4);
arr[i] = i+1;
printf("%d ",arr[i]);
}
// 取消映射
brk(arr);
return 0;
}
计算出前1000个素数,储存到堆内存中,但是不能浪费内存
#include<stdio.h>
#include<unistd.h>
#include<stdbool.h>
// 1000个素数,边出边储存
bool is_leap(int n)
{
for(int i=2;i<n;i++)
{
if(0 == n%i)
{
return false;
}
}
return true;
}
int main(int argc,const char* argv[])
{
int* arr = sbrk(0);
int k=0;
for(int i=2; k<1000; i++)
{
if(is_leap(i))
{
sbrk(4);
arr[k++] = i;
}
}
for(int i=0;i<k;i++)
{
printf("%d ",arr[i]);
}
brk(arr);
return 0;
}
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
功能:映射虚拟内存与物理内存,sbrk/brk底层调用的就是它们
addr:映射内存区域的起始地址,可以自己自定,如果是NULL的话,则操作系统帮你指定
length:映射的字节长度
prot:映射的权限
PROT_EXEC 执行权限
PROT_READ 读权限
PROT_WRITE 写权限
PROT_NONE 没有权限
flags:映射标志
MAP_FIXED 如果提供的addr无法映射则函数映射,则失败,系统不会自动调整
MAP_ANONYMOUS 将虚拟内存映射到物理内存中,而不是文件,也就是忽略了fd、offset两个参数(也就是赋0)
MAP_SHARED 对映射区域可以共享,如果是写入操作那么直接反应到文件
MAP_PRIVATE 对映射区域是私有的,并且对映射区域的写入操作只会反映到文件的缓冲区,并不会写入到文件中时
fd:文件表述符
offset:文件偏移量
返回值:成功返回映射后的内存首地址,失败返回0xffffffff
int munmap(void *addr, size_t length);
功能:取消映射
addr:映射内存区域的起始地址
length:内存字节数
返回值:成功返回0,失败返回-1
内存管理总结
1、mmap/munmap,底层不维护任何东西,只返回一个映射后内存的首地址,所映射的内存位于堆中
2、brk/sbrk,底层维护一个指针,该指针记录了所有映射内存的结尾,所映射的内存也是位于堆中,底层调用mmap/munmap
3、malloc/free,底层维护一个双向链表和必要的控制信息,所映射的内存也位于堆中,底层调用brk/sbrk
4、每个进程都有4G的虚拟内存空间,虚拟内存空间只是一个数字,必须与物理内存建立映射关系才能使用
5、平时所说的内存的分配和释放有两层含义
1、权限的分配与释放
2、映射关系的建立与取消
6、重点是理解Linux系统的内存管理机制,而不是sbrk/brk/mmap/munmap的用法
课后练习题:
实现一个基于顺序表的栈,栈的储存空间可以根据数据元素的多少儿自动增减,要求使用sbrk/brk实现