2内存管理
2.0绪论
程序被加载到物理内存,才能被CPU所执行。
- 页面换出
物理内存的空间有限,当难以满足使用需求时,会把一些长期闲置的代码和数据从物理内存缓存到磁盘上,这叫页面换出。
- 页面换入
一旦需要使用那些代码和数据,再把它们从磁盘上恢复到物理内存中,这叫页面的换入。
- 交换分区
磁盘上用来缓存物理内存数据的部分称为交换分区,磁盘的读写速率要远慢于物理内存,大量的数据换入和换出,会导致电脑变得很“卡”
- 虚拟地址与物理地址
我们在程序中所看到的或者使用的地址,并非是真实的物理内存的地址,而是经由系统内核的内存管理系统管理后所看到的虚拟地址。
- 虚拟地址和物理地址之间存在映射对应关系。
内存管理系统一方面保证物理内存的安全,避免物理内存被直接操作,同时也降低了程序员的编码难度。
CPU中包含一个名为MMU(Memory Management Unit)的内存管理单元,负责完成虚拟地址和物理地址之间的转换。
2.1虚拟地址空间
2.1.1 32位操作系统
对于32位操作系统而言,每个进程都有4G大小的虚拟地址空间。
所谓的虚拟地址空间本质就是一个地址范围,表示程序的寻址能力。我们所看到的虚拟地址,都是在这个4G的范围内。
对于32位操作系统而言,其虚拟地址空间范围从0x00000000到0xFFFFFFFF
其中03G-1的范围归用户所使用,称为**用户地址空间**,3G4G-1的范围归内核使用,称为内核地址空间。
2.1.2 64位操作系统
对于64位的操作系统而言,目前应用程序没有那么大的内存需求,所以不支持完全的64位虚拟地址。
64位系统上,其用户地址空间范围是:
0x0000 0000 0000 0000 ~ 0x0000 FFFF FFFF FFFF
64位系统上,其内核地址空间范围是:
0xFFFF 0000 0000 0000 ~ 0xFFFF FFFF FFFF FFFF
内核地址空间和用户地址空间之间是不规范地址空间,不允许使用。用户地址空间的代码不能直接访问内核空间的代码和数据,但可以通过系统调用进入内核态,间接与系统内核交互

2.2内存壁垒与段错误
2.2.1内存壁垒
每个进程的用户空间都是0~3G-1,但它们所对应的物理内存却是各自独立的,系统为每个进程的用户空间维护一张专属于该进程的内存映射表,记录虚拟内存到物理内存的对应关系,因此在不同进程之间交换虚拟内存地址是毫无意义的

所有进程的内核空间都是3G~4G-1,它们所对应的物理内存只有一份,系统为所有进程的内核空间维护一张内存映射表init_mm.pgd,记虚拟内存到物理内存的对应关系,因此不同进程通过系统调用所访问的内核代码和数据是同一份
2.2.2段错误
一切对虚拟内存的越权访问,都会导致段错误
-
试图访问没有映射到物理内存的虚拟内存
-
试图以非法方式访问虚拟内存,如对只读内存做写操作等。
2.3内存映射的建立与解除
2.3.1建立内存映射 mmap()
#include <sys/mman.h>
void* mmap(void* start, size_t length, int prot, int flags,int fd, off_t offset);
/*功能:建立虚拟内存到物理内存或磁盘文件的映射:
参数:start:映射区虚拟内存的起始地址,NULL系统自动选定后返回。
length:映射区字节数,自动按页(4096byte)取整。
prot:映射区操作权限,可取以下值:
PROT_READ - 映射区可读
PROT_WRITE - 映射区可写
PROT_EXEC - 映射区可执行
PROT_NONE - 映射区不可访问
flags:映射标志,可取以下值:
MAP_ANONYMOUS- 匿名映射,将虚拟内存映射到物理存而非文件,忽略fd和offset参数
MAP_PRIVATE - 对映射区的写操作只反映到缓冲区中并不会真正写入文件
MAP_SHARED - 对映射区的写操作直接反映到文件中
MAP_DENYWRITE - 拒绝其它对文件的写操作
MAP_FIXED - 若在start上无法创建映射,则失败(无此标志系统会自动调整)
fd:文件描述符
offset:文件偏移量,自动按页(4K)对齐
返回值:成功返回映射区虚拟内存的起始地址,失败返回MAP_FAILED(-1)。
2.3.2解除内存映射 munmap()
#include <sys/mman.h>
int munmap(void* start, size_t length);
/*功能:解除虚拟内存到物理内存或磁盘文件的映射:
参数:start:映射区虚拟内存的起始地址。
length:映射区字节数,自动按页圆整。
返回值:成功返回0,失败返回-1。
lmunmap允许对映射区的一部分解映射,但必须按页处理
2.3.3实例
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
int main(void){
//建立内存映射
char* start = mmap(NULL,4096,PROT_READ | PROT_WRITE,MAP_ANONYMOUS | MAP_PRIVATE,0,0);
if(start == MAP_FAILED){
perror("mmap");
return -1;
}
//读写内存
strcpy(start,"UC Funny!");
printf("%s",start);
//解除映射
if(munmap(start,4096) == -1){
perror("munmap");
return -1;
}
return 0;
}
2.4 虚拟内存的分配和释放
系统内部维护一个指针,指向当前堆尾,即堆区最后一个字节的下一个位置,
sbrk与brl函数根据增量参数或绝对参数调整该指针的位置,同时返回该指针在调整前的位置,其间若发现内存页耗尽或空闲,则自动追加或取消内存页的映射

2.4.1 sbrk()
#include <unistd.h>
void* sbrk(intptr_t increment);
/*功能:以相对方式分配和释放虚拟内存
参数:increment
堆内存的字节增量(以字节为单位)
>0 - 分配内存
<0 - 释放内存
=0 - 当前堆尾
返回值:成功返回调用该函数前的堆尾指针,失败返回-1。
标准库函数 fopen fclose fread fwrite fseek
系统调用函数 open fclose read write lseek
2.4.2 brk()
#include <unistd.h>
int brk(void* end_data_segment);
/*功能:以绝对方式分配和释放虚拟内存
参数:end_data_segment
堆尾指针的目标位置
> 堆尾指针的原位置 - 分配内存
< 堆尾指针的原位置 - 释放内存
= 堆尾指针的原位置 - 空操作
返回值:成功返回0,失败返回-1。
2.4.3实例
//
#include <stdio.h>
#include <unistd.h>
int main(){
printf("当前堆尾:%p\n",sbrk(0)); //sbrk(0)返回当前堆尾地址
int* p1 = sbrk(0);
brk(p1 + 1); //int型指针+1就是+4byte
printf("&p1 = %p\t",p1);
*p1 = 10;
printf("p1 = %d\n",*p1);
double* p2 = sbrk(8); //由当前堆尾向后分配4byte空间,并返回当前堆尾地址
*p2 = 3.14;
printf("&p2 = %p\t",p2);
printf("p2 = %lg\n",*p2);
//释放内存
brk(p1);//等价于sbrk(-12)
return 0;
}
2.4.4比较sbrk()与brk()
事实上,sbrk()和brk()不过是移动堆尾指针的两种不同方法,移动过程中还要兼顾虚拟内存和物理内存之间映射关系的建立和解除(以页为单位)
用sbrk分配内存比较方便,用多少内存就传多少增量参数,同时返回指向新分配内存区域的指针,但用sbrk做一次性内存释放比较麻烦,因为必须将所有的既往增量进行累加
用brk释放内存比较方便,只需将堆尾指针设回到一开始的位置即可一次性释放掉之前分多次分配的内存,但用brk分配内存比较麻烦,因为必须根据所需要的内存大小计算出堆尾指针的绝对位置
一般用sbrk分多次分配适量内存,最后用brk一次性整体释放

1万+

被折叠的 条评论
为什么被折叠?



