一、内存管理的层次关系
用户层 | ||
---|---|---|
STL | 自动分配、自动释放 | 调用C++ |
C++ | new/delete、构造/析构 | 调用C |
C | malloc\calloc\realloc\free | 调用POSIX\Linux |
POSIX | sbrk\brk | 调用Kernal |
Linux | mmap\munmap | 调用Kernal |
系统层 | ||
Kernal | kmalloc /vmalloc | 调用驱动Driver |
Driver | get_free_page | ... |
二、进程映像
程序与进程:
-
程序是存储在磁盘上的可执行文件,当程序被运行时,系统会把程序从磁盘加载到内存中运行,正在运行中的程序称为进程,一个程序可以同时被加载多次,形成多个进程,每个进程相互独立,由操作系统管理
什么是进程映像:
-
进程在内存空间中的分布使用情况就称为进程映像,从低地址到高地址分别是:
各个内存段的存储什么数据:
-
代码段(text)
-
存储二进制指令、字面值常量、被const修饰过的原data段的数据
-
权限r-- 或者 r-x 权限只读
-
-
数据段(data)
-
存储初始化过的全局变量和静态变量
-
-
静态数据段(BSS)
-
存储未初始化过的全局变量和静态变量
-
进程一旦加载,此内存段会被操作系统自动清零,默认值是0
-
如果初始化的值给0,依然还在BSS
-
-
堆区(heap)
-
要程序员动态分配、动态释放,从低地址向高地址扩招
-
使用malloc系列函数进行内存管理
-
而malloc系列函数底层调用操作系统的API(brk\sbrk\mmap\munmap)
-
-
栈区(stack)
-
存储非静态局部变量、块变量,包括函数的参数(除了main函数的参数)、返回值
-
内存扩展从高地址向低地址扩展
-
栈区与堆区中间有一段预留空间,一个作用为了预留,第二个是让共享库的内存以及共享内存使用此段内存
-
-
命令行参数与环境变量表
-
里面存储环境变量表以及命令行传给main的参数内容
// 打印出每个内存段中各种数据的地址 // 通过ps -aux 查看出进程id // /proc/进程号/maps 该文件能查看该进程的内存分布情况 // 通过打印的内存地址,分析maps文件的内存段分布情况 #include <stdio.h> #include <stdlib.h> const int const_global = 10; // 常全局变量 int init_global = 10; // 初始化全局变量 int uninit_global; // 未初始化全局变量 int main(int argc,const char* argv[]) { const static int const_static = 10; // 常静态局部变量 static int init_static = 10;// 初始化静态局部变量 static int uninit_static; // 未初始化静态局部变量 const int const_local; // 常局部变量 int prev_local; // 前局部变量 int next_local; // 后局部变量 int arr[10]; int* prev_heap = malloc(sizeof(int)); // 前堆变量 int* next_heap = malloc(sizeof(int)); // 后堆变量 const char* literal = "literal"; // 字面值常量 extern char** environ; // 环境变量 printf("-----命令行参数与环境变量-----<高>\n"); printf("环境变量:%p\n",environ); printf("命令行参数:%p\n",argv); printf("-------------栈-------------------\n"); printf("常局部变量:%p\n",&const_local); printf("前局部变量:%p\n",&prev_local); printf("后局部变量:%p\n",&next_local); printf("arr[0]:%p arr[1]:%p arr[2]:%p\n",&arr[0],&arr[1],&arr[2]); printf("-------------堆-------------------\n"); printf("后堆变量:%p\n",next_heap); printf("前堆变量:%p\n",prev_heap); printf("-----------BSS--------------------\n"); printf("未初始化全局变量:%p\n",&uninit_global); printf("未初始化静态变量:%p\n",&uninit_static); printf("----------data--------------------\n"); printf("初始化全局变量:%p\n",&init_global); printf("初始化静态变量:%p\n",&init_static); printf("---------代码段-------------------\n"); printf("常静态局部变量:%p\n",&const_static); printf("常全局变量:%p\n",&const_global); printf("字面值常量:%p\n",literal); printf("二进制指令:%p\n",main); printf("------------------------------<低>\n"); getchar(); }
-
三、虚拟内存
-
-
首先要理解操作系统不能把真实的物理内存直接分配给进程使用,如果真的这样做,那么会面临严重的安全问题,进程可以根据或得到的真实物理内存地址,通过指针访问其它进程的内存进行破坏,甚至会影响操作系统的安全,所以才会引入虚拟内存的概念
-
什么是虚拟内存:
-
虚拟内存是操作系统对进行内存空间地址进行管理的一套精心设计的逻辑层面的内存空间概念
-
在32位系统下,操作系统规定给每个启动的进程拥有4G大小虚拟内存,但是这4G的虚拟内存不能直接使用的,其实就是系统给进程画的饼,当进程真正要存储数据需要使用内存时,系统会把一部分虚拟内存与物理内存进行映射,必须进行映射后的虚拟内存才能正常使用
-
如果非要使用没有映射过的虚拟内存,操作系统一定会在运行时产生段错误
-
用户空间与内核空间:
-
每个进程的4G虚拟内存中,根据使用者不同分为两个部分:
-
[0x00000000~0xC0000000]部分,有3GB大小,在经过系统的映射后,在应用程序中使用,但是程序不能直接访问内核空间的代码和数据,但是可以通过系统调用(API)让当前进程从用户态进入内核态,间接地与系统内核进行数据交互
-
内核空间:
-
[0xC0000000~0xFFFFFFFF]部分,有1GB大小,只有操作系统才能使用,里面存储的是操作系统为服务进程与该进程进行交互所必需的相关数据,内核空间又操作系统来管理,内核空间不会随着进程的切换而改变
-
但是用户空间对应不同的进程,一旦进程切换,用户空间使用的物理内存也会被内核随之切换,这样就确保了不同进程的用户空间是完全独立的。
-
因此不同进程之间使用虚拟内存进行交互数据是毫无意义的,所以下面两个程序直接通过虚拟内存地址交互数据是毫无意义的
-
用户空间:
-
// 程序A
#include <stdio.h>
int main(int argc,const char* argv[])
{
int num = 123456;
printf("%lu\n",(unsigned long)&num);
getchar();
}
// 程序B
#include <stdio.h>
int main(int argc,const char* argv[])
{
int* p = NULL;
unsigned long num = 0;
scanf("%lu",&num);
p = (int*)num;
printf("%d\n",*p); // 无法访问程序A的num内存
}
-
在内核态下,进程运行在3~4GB空间范围内,此时CPU可执行任何指令,代码不受任何的限制,可以自由地访问任何有效地址,甚至可以直接访问端口
-
在用户态,进程运行在0~3GB空间范围内,此时执行的代码要受到一定的限制,CPU会做很多的检查,例如:检查进程只能使用映射过的虚拟地址等
进程使用虚拟内存的好处有哪些?
-
操作系统给每个进程分配4GB的虚拟内存,可以让进程之间进行隔离,避免进程之间相互影响、破坏
-
操作系统把4GB的虚拟内存划分成用户空间(0~3GB)和内核空间(3~4GB),可以让进程与操作系统之间进行隔离,避免进程去破坏操作系统的正常运行
-
通过给进程使用虚拟内存,避免暴露真实的物理内存地址,也是对操作系统的保护
-
操作系统还可以让磁盘上的文件与虚拟内存进行映射,当物理内存不够用时,还可以通过映射磁盘来替代,虽然速度慢一些,相当于能让用户使用比真实物理内存更大的空间
四、内存映射
自动映射
-
当程序执行时,操作系统会把它加载到内存形成进程后,会自动地给text、data、bss、stack、命令行参数、环境变量表进行自动映射
手动映射
-
在程序首次使用malloc申请内存时,此时malloc手中没有堆内存可以分配,也就是说没有任何映射过的内存可以供分配使用,malloc会找系统申请映射33页虚拟内存,这33页映射后交由malloc管理分配,之后再找malloc申请内存时,malloc会从这33页中直接分配给调用者,如果33页分配完,继续重复以上操作,这个过程malloc底层调用了操作系统的API接口函数完成映射的操作
-
如果程序需要的堆内存并不多,距离33页差距很大,那么直接使用malloc是方便,但是也造成了很大的内存映射浪费,所以通过学习直接调用系统的API接口函数,来更精确地映射内存
-
关于malloc获取虚拟内存空间的实现过程,与内核版本有关,大体逻辑:
-
如果分配的内存小于128Kb,底层调用sbrk\brk
-
如果分配的内存大于128Kb,底层调用mmap\munmap
-
以上只是简单情况,具体实际可能会更复杂
-