Intel Core i7/Linux内存系统

Intel Core i7/Linux内存系统

  1. 处理器封装包括4个核、一个所有核共享的L3高速缓存,DDR3内存控制器
  2. 每个核包含一个层次结构的TLB、一个层次结构的数据和指令高速缓存、一组快速的点到点链路
  3. TLB是虚拟寻址的四条组相联的,L1、L2、L3是物理寻址的,块大小是64字节
  4. L1和L2是8路组相联的,L3是16路组相联的

Core i7地址翻译

第一级、第二级或第三级页表中条目的格式

第四级页表中条目的格式

  1. 采用四级页表层次
  2. 每个进程都有自己私有的页表层次结构,CR3控制寄存器指向第一级页表的起始位置,CR3的值是每个进程上下文的一部分
  3. 第一级、第二级或第三级页表中条目的格式,P=1时,地址字段包含一个40位物理页号PPN,指向页表的开始处,要求物理页4KB对齐
  4. 第四级页表中条目的格式,P=1时,地址字段包括一个40位的PPN,指向物理内存中某一页的基地址,要求物理页4KB对齐
  5. PTE有3个权限位,控制对页的访问
  • R/W确定页的读写权限
  • U/S确定能否在用户模式中访问
  • XD用来禁止从某些内存页取指令
  1. 每次访问一个页时,MMU都会设置A位,称为引用位;设置D位修改位,修改位告诉内核在复制替换页之前,是否要写会牺牲页。内核可以调用一条特殊的内核指令清除引用位和修改位

Linux虚拟内存系统

Linux虚拟内存

  1. Linux为每个进程维护了一个单独的虚拟地址空间,虚拟内存位于用户栈之上
  2. 内核虚拟内存包含内核中的代码和数据结构,Linux将一组连续的虚拟内存映射到相应的物理内存,内核的其他区域包含每个进程都不相同的数据
  3. Linux将虚拟内存组织成一些区域的集合(段集合),允许虚拟地址空间有间隙
  4. 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指向链表中的下一个区域结构
  1. Linux异常缺页处理:
  • 检查虚拟地址A是否合法
  • 检查内存访问是否合法
  • 前两项都是合法的,内核选择一个牺牲页面,如果牺牲页面已经被修改过了,将它交换出去,换入新的页面并更新页表。缺页程序返回时,CPU重新执行这条指令,不会再发生缺页中断了
  1. 虚拟内存区域可以映射两种类型的对象中的一种:
  • Linux文件系统中的普通文件
  • 匿名文件:匿名文件是由内核创建的,如果CPU第一次引用这样一个区域内的虚拟文件时,内核就在物理内存中找到一个合适的牺牲页表,如果牺牲页面已经被修改过了,用二进制零覆盖并更新页表

共享对象

  1. 进程对共享对象的修改对其他进程是可见的;进程对私有对象的修改对其他进程不可见。
  2. 映射到共享对象的虚拟内存区域称为共享区域,类似的,有私有区域
  3. 两个进程将一个私有对象映射到它们虚拟内存的不同区域,,共享同一个物理副本,私有区域的页表条目被标记为只读,区域结构被标记为私有的写时复制。
  4. 如果有进程试图写私有页面,会触发一个保护故障,故障处理程序在物理内存中创建这个页面的一个新的副本,更新页表条目指向这个新的副本,然后恢复这个页面的写权限

fork函数

为每个进程都保持了私有空间的抽象概念

execve函数,加载和执行程序

  1. 删除已存在的用户区域
  2. 映射色号私有区域
  3. 映射共享区域
  4. 设置程序计数器,指向代码区域的入口点

使用mmap函数的用户级内存映射

  1. Linux进程可以用mmap函数来创建新的虚拟内存区域,并把对象映射到这些区域中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <首次适配会在链表起始处留下小的碎片>
#include <sys/mman.h>

void *mmap(void *start , size_t length , int port , int flags , int fd , off_t offset);
//新建虚拟内存区域地址从start开始,将文件描述符fd指定的对象的第一个连续的片映射到这个新的区域
//连续的片的大小为length字节,从距文件开始处偏移量为offset字节的地方开始
//start的地址通常被定义为NULL
//参数prot包含描述新映射的虚拟内存区域的访问权限位...
//参数flag描述被映射对象类型的位组成

bufp=Mmap(NULL,size,PROT_READ,MAP_PRIVATE|MAP_ANON,0,0);
//内核创建一个包含size字节的只读/私有/请求二进制零的虚拟内存区域

//munmap函数删除虚拟内存的区域
#include <unistd.h>
#include <sys/mman.h>

int munmap(void *start,size_t length);

动态内存分配

  1. 动态内存分配器维护着一个进程的虚拟内存区域,成为堆heap
  2. 对于每个进程,内核维护着一个变量brk(break)指向堆的顶部
  3. 显式分配器:要求显式地释放任何已分配的块(new/delete)
  4. 显式分配器的约束:处理任意请求序列,但是没有假设分配和释放的顺序;立即响应请求;只使用堆;对齐块;不修改已经分配的块
  5. 隐式分配器:要求分配器检测一个已分配块何时不再被使用,释放这个块,也称为垃圾收集器。自动释放已分配的块的过程称为垃圾收集(智能指针)
  6. 垃圾收集器是一种动态内存分配器,自动释放程序不再需要的已分配块,自动回收堆存储的过程称为垃圾收集
  • 有向可达图,将节点分为根节点和堆节点,对接点对应已分配的块,根节点不在堆中,指向堆中的指针
  • 存在根节点到达块的路径,节点p可达,不可达节点对应于垃圾,不能被应用再次使用
  • 收集器代替应用调用free
  1. 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当前值
  1. free 简单的返回到调用函数

    1
    2
    3
    4
    #include <stdlib.h>
    void free(void *ptr);
    //ptr参数指向已分配块的起始位置,否则free就是未定义的
    //没有返回值,不会告诉应用哪里出错了
  2. 有未使用的内存但不能没能满足分配请求称为碎片,有内部碎片和外部碎片。内部碎片是有效载荷和已分配内存之差;外部碎片是必须要请求更大的虚拟内存分散映射多个块

  3. 放置已分配的块有三种
  • 首次适配会在链表起始处留下小的碎片
  • 下一次适配从上一次结束的地方开始搜索,比首次适配快,利用率低
  • 最佳适配慢,分离式空闲链表组织,接近于最佳适配策略,不需要进行彻底的堆搜索
  1. 分配器通过调用sbrk函数,向内核请求额外的堆内存,分配器将额外的内存转换为一个大的空闲块,将这个块插入空闲链表中,然后将被请求的块放置到这个新的空闲块
  2. 合并空闲块:分配器采用立即合并或者推迟合并
  3. 带标记的合并
  • 使用边界标记来合并前面的空闲块,在每个块的结尾处添加一个脚本是头部标记的一个副本,合并所有的前后空闲块情况的合并
  • 将前面的块的已分配/空闲位存放当前块中多出来的低位,则已分配的块就不需要脚部了

显式空闲链表

  1. 将空闲块组织成一个某种形式的显式数据结构,实现这个数据结构的指针可以存放在空闲块的主体中
  2. 使用首次分配的时间从块总数的时间减少到空闲块数量的线性时间
  3. 释放块的时间可以是线性的,也可以是常数,取决于排序策略
  4. 一种方法是使用后进后出的方法来维护链表,将新释放的块放在链表头部,在常数时间内完成首次适配放置和释放块,以及有边界标记的合并
  5. 另一种方法是按照地址顺序来维护链表,释放块需要线性时间来搜索定位,比后进后出排序的首次适配有更高的内存利用率
  6. 显式链表的缺点是空闲块必须足够大,包含所有指针,以及头部和可能的脚部,导致更大的最小块的大小,提高内部碎片的程度

分离的空闲链表

  1. 使用分离存储来减少分配时间,将所有可能的块划分为大小类,每个大小类都有一个空闲链表,按照大小升序排列
  2. 简单分离存储:每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小
  • 空闲块不分割不合并,不需要头部和脚部,分配和释放都在空闲链表的起始处,是单向的
  • 如果链表为非空,分配器向系统请求一个固定大小的额外内存片,将这个片分成大小相等的块,将块链接起来形成新的空闲链表
  • 释放一个块只要简单的将块插入相应的空闲链表的前部
  • 任何块都需要唯一字段是每个空闲块中的一个字的succ指针,最大块的大小是一个字
  1. 分离配适:每个链表包含潜在的大小不同的块,是大小类的成员
  • 分配块时,确定请求的大小类,对适当的空闲链表进行首次适配,查找合适的块,并可选地进行分割,将剩余部分插入适当的空闲链表中
  • 如果没有找到合适的块,向系统请求额外的堆内存,从新的堆内存中分离出一块,将剩余部分放置在适当的大小类中
  • 释放一个块执行合并,将结果存放到相应的空闲链表中
  • C标准库中的GNU malloc包就是采用的这种方法
  1. 伙伴系统:快速搜索和快速合并,缺点是可能导致显著的内部碎片
  2. 递归的二分割块,直到找到合适的块,将剩余的半块放置到合适的空闲链表中

C程序中常见的与内存有关的错误

  1. 间接引用坏指针
  2. 读未初始化的内存
  3. 允许栈缓冲区溢出:不检查输入串的大小写入栈中的目标缓冲器,这个程序就会有缓冲区溢出错误
  4. 假设指针和指向的对象是相同大小的
  5. 造成错位错误:常见的覆盖错误
  6. 引用指针而不是指针指向的对象
  7. 误解指针运算
  8. 引用不存在的变量
  9. 引用空闲堆块中的数据
  10. 引起内存泄露
WhitneyLu wechat
Contact me by scanning my public WeChat QR code
0%