Intel Core i7/Linux内存系统
- 处理器封装包括4个核、一个所有核共享的L3高速缓存,DDR3内存控制器
- 每个核包含一个层次结构的TLB、一个层次结构的数据和指令高速缓存、一组快速的点到点链路
- TLB是虚拟寻址的四条组相联的,L1、L2、L3是物理寻址的,块大小是64字节
- L1和L2是8路组相联的,L3是16路组相联的
Core i7地址翻译
第一级、第二级或第三级页表中条目的格式
第四级页表中条目的格式
- 采用四级页表层次
- 每个进程都有自己私有的页表层次结构,CR3控制寄存器指向第一级页表的起始位置,CR3的值是每个进程上下文的一部分
- 第一级、第二级或第三级页表中条目的格式,P=1时,地址字段包含一个40位物理页号PPN,指向页表的开始处,要求物理页4KB对齐
- 第四级页表中条目的格式,P=1时,地址字段包括一个40位的PPN,指向物理内存中某一页的基地址,要求物理页4KB对齐
- PTE有3个权限位,控制对页的访问
- R/W确定页的读写权限
- U/S确定能否在用户模式中访问
- XD用来禁止从某些内存页取指令
- 每次访问一个页时,MMU都会设置A位,称为引用位;设置D位修改位,修改位告诉内核在复制替换页之前,是否要写会牺牲页。内核可以调用一条特殊的内核指令清除引用位和修改位
Linux虚拟内存系统
Linux虚拟内存
- Linux为每个进程维护了一个单独的虚拟地址空间,虚拟内存位于用户栈之上
- 内核虚拟内存包含内核中的代码和数据结构,Linux将一组连续的虚拟内存映射到相应的物理内存,内核的其他区域包含每个进程都不相同的数据
- Linux将虚拟内存组织成一些区域的集合(段集合),允许虚拟地址空间有间隙
- Linux组织的虚拟内存的结构
- 任务结构中的条目mm指向mm_struct,描述虚拟内存的当前状态
- pgd指向第一级页表的基址;mmap指向一个vm_area_structs(区域结构)的链表
- 每个vm_area_structs都描述了当前虚拟空间的一个区域
- 内核运行进程时,将pgd存放在CR3控制寄存器中
- 具体区域的具体字段包含下面的字段:1)vm_start指向区域的起始处;2)vm_end指向区域的结束处;3)vm_port区域中所有页的读写许可权限;4)vm_flags描述区域内的页面是进程共享的还是私有的;4)vm_next指向链表中的下一个区域结构
- Linux异常缺页处理:
- 检查虚拟地址A是否合法
- 检查内存访问是否合法
- 前两项都是合法的,内核选择一个牺牲页面,如果牺牲页面已经被修改过了,将它交换出去,换入新的页面并更新页表。缺页程序返回时,CPU重新执行这条指令,不会再发生缺页中断了
- 虚拟内存区域可以映射两种类型的对象中的一种:
- Linux文件系统中的普通文件
- 匿名文件:匿名文件是由内核创建的,如果CPU第一次引用这样一个区域内的虚拟文件时,内核就在物理内存中找到一个合适的牺牲页表,如果牺牲页面已经被修改过了,用二进制零覆盖并更新页表
共享对象
- 进程对共享对象的修改对其他进程是可见的;进程对私有对象的修改对其他进程不可见。
- 映射到共享对象的虚拟内存区域称为共享区域,类似的,有私有区域
- 两个进程将一个私有对象映射到它们虚拟内存的不同区域,,共享同一个物理副本,私有区域的页表条目被标记为只读,区域结构被标记为私有的写时复制。
- 如果有进程试图写私有页面,会触发一个保护故障,故障处理程序在物理内存中创建这个页面的一个新的副本,更新页表条目指向这个新的副本,然后恢复这个页面的写权限
fork函数
为每个进程都保持了私有空间的抽象概念
execve函数,加载和执行程序
- 删除已存在的用户区域
- 映射色号私有区域
- 映射共享区域
- 设置程序计数器,指向代码区域的入口点
使用mmap函数的用户级内存映射
- Linux进程可以用mmap函数来创建新的虚拟内存区域,并把对象映射到这些区域中
1 | #include <首次适配会在链表起始处留下小的碎片> |
动态内存分配
- 动态内存分配器维护着一个进程的虚拟内存区域,成为堆heap
- 对于每个进程,内核维护着一个变量brk(break)指向堆的顶部
- 显式分配器:要求显式地释放任何已分配的块(new/delete)
- 显式分配器的约束:处理任意请求序列,但是没有假设分配和释放的顺序;立即响应请求;只使用堆;对齐块;不修改已经分配的块
- 隐式分配器:要求分配器检测一个已分配块何时不再被使用,释放这个块,也称为垃圾收集器。自动释放已分配的块的过程称为垃圾收集(智能指针)
- 垃圾收集器是一种动态内存分配器,自动释放程序不再需要的已分配块,自动回收堆存储的过程称为垃圾收集
- 有向可达图,将节点分为根节点和堆节点,对接点对应已分配的块,根节点不在堆中,指向堆中的指针
- 存在根节点到达块的路径,节点p可达,不可达节点对应于垃圾,不能被应用再次使用
- 收集器代替应用调用free
- Mark&Sweep垃圾收集器
- 由标记阶段和清除阶段组成,标记阶段调用mark函数,清除阶段调用sweep函数一次。
- 标记阶段标记处根节点所有的可达和已分配的后继,清除阶段清除所有未标记的已分配块,块头部中空闲的低位中的一位通常用来表示这个块是否被标记了
- ptr定义为typedef void *ptr
1
2
3
4
5
6
7
8
9
10
11
12#include <stdlib.h>
void *malloc(size_t size);
//返回一个指针指向大小至少位size字节的内存块
//如果程序要求的块比可用的虚拟内存大,返回NULL,设置errno
//malloc不初始化它返回的内存,需要初始化的动态内存的程序可以适应calloc,想要改变已分配的块的大小使用realloc
//malloc使用mmap和munmap显式地分配和释放堆内存
#include <unistd.h>
void *sbrk(inrptr_t incr);
//通过brk指针增加incr来扩展和收缩堆
//成功返回brk的旧值,失败返回-1,并将errno设为ENOMEN
//如果incr为0,函数返回brk当前值
free 简单的返回到调用函数
1
2
3
4#include <stdlib.h>
void free(void *ptr);
//ptr参数指向已分配块的起始位置,否则free就是未定义的
//没有返回值,不会告诉应用哪里出错了有未使用的内存但不能没能满足分配请求称为碎片,有内部碎片和外部碎片。内部碎片是有效载荷和已分配内存之差;外部碎片是必须要请求更大的虚拟内存分散映射多个块
- 放置已分配的块有三种
- 首次适配会在链表起始处留下小的碎片
- 下一次适配从上一次结束的地方开始搜索,比首次适配快,利用率低
- 最佳适配慢,分离式空闲链表组织,接近于最佳适配策略,不需要进行彻底的堆搜索
- 分配器通过调用sbrk函数,向内核请求额外的堆内存,分配器将额外的内存转换为一个大的空闲块,将这个块插入空闲链表中,然后将被请求的块放置到这个新的空闲块
- 合并空闲块:分配器采用立即合并或者推迟合并
- 带标记的合并
- 使用边界标记来合并前面的空闲块,在每个块的结尾处添加一个脚本是头部标记的一个副本,合并所有的前后空闲块情况的合并
- 将前面的块的已分配/空闲位存放当前块中多出来的低位,则已分配的块就不需要脚部了
显式空闲链表
- 将空闲块组织成一个某种形式的显式数据结构,实现这个数据结构的指针可以存放在空闲块的主体中
- 使用首次分配的时间从块总数的时间减少到空闲块数量的线性时间
- 释放块的时间可以是线性的,也可以是常数,取决于排序策略
- 一种方法是使用后进后出的方法来维护链表,将新释放的块放在链表头部,在常数时间内完成首次适配放置和释放块,以及有边界标记的合并
- 另一种方法是按照地址顺序来维护链表,释放块需要线性时间来搜索定位,比后进后出排序的首次适配有更高的内存利用率
- 显式链表的缺点是空闲块必须足够大,包含所有指针,以及头部和可能的脚部,导致更大的最小块的大小,提高内部碎片的程度
分离的空闲链表
- 使用分离存储来减少分配时间,将所有可能的块划分为大小类,每个大小类都有一个空闲链表,按照大小升序排列
- 简单分离存储:每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小
- 空闲块不分割不合并,不需要头部和脚部,分配和释放都在空闲链表的起始处,是单向的
- 如果链表为非空,分配器向系统请求一个固定大小的额外内存片,将这个片分成大小相等的块,将块链接起来形成新的空闲链表
- 释放一个块只要简单的将块插入相应的空闲链表的前部
- 任何块都需要唯一字段是每个空闲块中的一个字的succ指针,最大块的大小是一个字
- 分离配适:每个链表包含潜在的大小不同的块,是大小类的成员
- 分配块时,确定请求的大小类,对适当的空闲链表进行首次适配,查找合适的块,并可选地进行分割,将剩余部分插入适当的空闲链表中
- 如果没有找到合适的块,向系统请求额外的堆内存,从新的堆内存中分离出一块,将剩余部分放置在适当的大小类中
- 释放一个块执行合并,将结果存放到相应的空闲链表中
- C标准库中的GNU malloc包就是采用的这种方法
- 伙伴系统:快速搜索和快速合并,缺点是可能导致显著的内部碎片
- 递归的二分割块,直到找到合适的块,将剩余的半块放置到合适的空闲链表中
C程序中常见的与内存有关的错误
- 间接引用坏指针
- 读未初始化的内存
- 允许栈缓冲区溢出:不检查输入串的大小写入栈中的目标缓冲器,这个程序就会有缓冲区溢出错误
- 假设指针和指向的对象是相同大小的
- 造成错位错误:常见的覆盖错误
- 引用指针而不是指针指向的对象
- 误解指针运算
- 引用不存在的变量
- 引用空闲堆块中的数据
- 引起内存泄露