一、内存
1.内存:
虚拟内存:是地址空间,虚拟的存储区域,应用程序所访问的都是虚拟内存。
(我们在程序中所访问的内存基本都不是真实的,是虚拟的,实际上是存在物理内存中,通过内存映射,完成从虚拟到物理的存放。)
物理内存:存储空间,实际的存储区域,只有系统内核可以访问物理内存。
虚拟内存和物理内存之间存在对应关系,当应用程序访问虚拟内存时,系统内核会依据这种对应关系找到与之相应的物理内存。上述对应关系存储在内核中的内存映射表中。
物理内存包括半导体内存和换页文件两部分:
当半导体内存(相当于内存条)不够用时,可以把一些长期闲置的代码和数据从半导体内存中缓存到换页文件(相当于硬盘)中,这叫页面换出。一旦需要使用被换出的代码和数据,再把它们从换页文件恢复到半导体内存中,这叫页面换入。因此,系统中的虚拟内存比半导体内存大得多。
2.进程映射:
每个进程都拥有独立的4G字节的虚拟内存,分别被映射到不同的物理内存区域。
内存映射和换入换出都是以页为单位,1页=4096字节。
4G虚拟内存中:
高地址的1G被映射到内核的代码和数据区,这1个G在各个进程间共享。用户的应用程序只能直接访问低地址的3个G虚拟内存,因此该区域被称为用户空间,而高地址的1个G虚拟内存则被称为内核空间。
用户空间中的代码只能直接访问用户空间的数据,如果要想访问内核空间中的代码和数据必须借助专门的系统调用完成。
用户空间的3G虚拟内存可以进一步被划分为如下区域:
利用代码看一下实际的地址如何:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
const int const_global = 10; // 常全局变量
int init_global = 10; // 初始化全局变量
int uninit_global; // 未初始化全局变量
int main(int argc, char* argv[]) {
// 常静态变量
const static int const_static = 10;
// 初始化静态变量
static int init_static = 10;
// 未初始化静态变量
static int uninit_static;
// 常局部变量
const int const_local = 10;
int prev_local; // 前局部变量
int next_local; // 后局部变量
// 前堆变量
int* prev_heap = malloc(sizeof(int));
// 后堆变量
int* next_heap = malloc(sizeof(int));
// 字面值常量
char const* literal = "literal";
extern char** environ; // 环境变量
printf("---- 命令行参数与环境变量 ----\n");
printf(" 环境变量:%p\n", environ);
printf(" 命令行参数:%p\n", argv);
printf("-------------- 栈 ------------\n");
printf(" 后局部变量:%p\n",
&next_local);
printf(" 前局部变量:%p\n",
&prev_local);
printf(" 常局部变量:%p\n",
&const_local);
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("------------ 数据 ------------\n");
printf(" 初始化静态变量:%p\n",
&init_static);
printf(" 初始化全局变量:%p\n",
&init_global);
printf("------------ 代码 ------------\n");
printf(" 常静态变量:%p\n",
&const_static);
printf(" 字面值常量:%p\n", literal);
printf(" 常全局变量:%p\n",
&const_global);
printf(" 函数:%p\n",
main);
printf("------------------------------\n");
return 0;
结果如下:
也可以通过size命令查看一个可执行程序的代码区、数据区和BSS区的大小。
size maps
每个进程的用户空间都拥有独立的从虚拟内存到物理内存的映射,谓之进程间的内存壁垒。
即:我们同时开两个进程,前一个先往虚拟内存中写一个数,然后挂在一边,后一个进程也往同一块虚拟内存中写一个数,但是让前一个再去读这个内存的数时,还是之前的数,不会被后面的进程改变,存在内存壁垒,即使用同一块虚拟内存,也不在同一块物理内存中。
3.内存的分配和释放:
3.1以增量的方式分配或释放虚拟内存
分配:映射+占有
映射: 在地址空间(虚拟内存)和存储空间(物理内存)之间建立映射关系
占有:指定内存空间的归属性
释放:放弃占有+解除映射
放弃占有:解除对内存空间的归属约束
解除映射:消除地址空间(虚拟内存)和存储空间(物理内存)之间的映射关系
以增量的方式分配或释放虚拟内存:
#include <unistd.h> //需要使用函数的所在头文件
void* sbrk(intptr_t increment);
成功返回调用该函数之前的堆顶指针,失败返回-1。
increment参数说明:
>0 - 堆顶指针上移,增大堆空间,分配虚拟内存
<0 - 堆顶指针下移,缩小堆空间,释放虚拟内存
=0 - 不分配也不释放虚拟内存,仅仅返回当前堆顶指针
系统内核维护一个指针,指向堆内存的顶端,即有效堆内存中最后一个字节的下一个位置。sbrk函数根据增量参数increment调整该指针的位置,同时返回该指针原来的位置,期间若发生内存耗尽或空闲,则自动追加或取消相应内存页的映射。
3.2以绝对地址的方式分配或释放虚拟内存:
int brk(void* end_data_segment);
成功返回0,失败返回-1。
end_data_segment
>当前堆顶,分配虚拟内存
<当前堆顶,释放虚拟内存
=当前堆顶,空操作
系统内核维护一个指针,指向当前堆顶,brk函数根据指针参数end_data_segment设置堆顶的新位置,期间若发生内存耗尽或空闲,则自动追加或取消相应内存页的映射。
3.3建立虚拟内存到物理内存或文件的映射
#include <sys/mman.h>
void* mmap(void* start, size_t length, int prot,
int flags, int fd, off_t offset);
成功返回映射区虚拟内存的起始地址,失败返回MAP_FAILED(void*类型的-1)。
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_LOCKED - 锁定映射,禁止被换出到换页文件
fd - 文件描述符
offset - 文件偏移量,自动按页对齐
3.4解除虚拟内存到物理内存或文件的映射
int munmap(void* start, size_t length);
成功返回0,失败返回-1。
start - 映射区的起始地址
length - 映射区的字节数
相比于malloc的释放,这个更好,malloc被free时是全部被释放掉,不可以选则被释放的空间长度。
使用示例:
释放示例: