HuSharp | Blog - Topic - Résumé - Collections | RSS |

内存管理(三)虚拟内存与物理内存的映射关系 & pagefault

# Linux, 2020-12-05

内存映射

详解缺页中断—–缺页中断处理(内核、用户)

我们既看了虚拟内存空间如何组织的,也看了物理页面如何管理的。现在我们需要一些数据结构,将二者关联起来。

用户态内存映射

无论是内核线程还是用户进程,对于内核来说,无非都是 task_struct 这个数据结构的一个实例而已,task_struct 被称为进程描述符(process descriptor),因为它记录了这个进程所有的context。其中有一个被称为 ‘内存描述符’ (memory descriptor)的数据结构 mm_struct,抽象并描述了Linux视角下管理进程地址空间的所有信息。 每一个进程都有一个列表 vm_area_struct,指向虚拟地址空间的不同的内存块,这个变量的名字叫mmap

struct mm_struct {
	struct vm_area_struct *mmap;		/* list of VMAs */
......

}

image-20201207172335213

1、brk 是将数据段(.data)的最高地址指针_edata往高地址推;

2、mmap 是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

​ 这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

内存管理并不直接分配物理内存,因为物理内存相对于虚拟地址空间太宝贵了,只有等你真正用的那一刻才会开始分配。

一旦开始访问虚拟内存的某个地址,如果我们发现,并没有对应的物理页,那就触发缺页中断,调用 do_page_fault。

1)通过mm是否存在判断是否是内核线程,对于内核线程,进程描述符的mm总为NULL,一旦成立,说明是在内核态中发生的异常,跳到no_context

用户态缺页异常

           if (in_atomic() || !mm)
                goto no_context;

在 __do_page_fault 里面,先要判断缺页中断是否发生在内核。如果发生在内核则调用 vmalloc_fault,这就和咱们前面学过的虚拟内存的布局对应上了。在内核里面,vmalloc 区域需要内核页表映射到物理页。咱们这里把内核的这部分放放,接着看用户空间的部分。

接下来在用户空间里面,找到你访问的那个地址所在的区域 vm_area_struct,然后调用 handle_mm_fault 来映射这个区域。

当一个进程发生缺页中断的时候,进程会陷入内核态,执行以下操作:

  1. 检查要访问的虚拟地址是否合法
  2. 查找/分配一个物理页
  3. 填充物理页内容(读取磁盘,或者直接置0,或者啥也不干)
  4. 建立映射关系(虚拟地址到物理地址) 重新执行发生缺页中断的那条指令 如果第3步,需要读取磁盘,那么这次缺页中断就是majflt,否则就是minflt。

每个进程都有独立的地址空间,为了这个进程独立完成映射,每个进程都有独立的进程页表,32 位 就位于 cr3

具体过程如下

首先从CPU的控制寄存器CR2中读出出错的地址address,然后调用find_vma(),在进程的虚拟地址空间中找出结束地址大于address的第一个区间,如果找不到的话,则说明中断是由地址越界引起的,转到bad_area执行相关错误处理;

确定并非地址越界后,控制转向标号good_area。在这里,代码首先对页面进行例行权限检查,比如当前的操作是否违反该页面的Read,Write,Exec权限等。如果通过检查,则进入虚拟管理例程handle_mm_fault().否则,将与地址越界一样,转到bad_area继续处理。

handle_mm_fault()用于实现页面分配与交换,它分为两个步骤:首先,如果页表不存在或被交换出,则要首先分配页面给页表;然后才真正实施页面的分配,并在页表上做记录。具体如何分配这个页框是通过调用handle_pte_fault()完成的。

handle_pte_fault()函数根据页表项pte所描述的物理页框是否在物理内存中,分为两大类:

(1)请求调页:被访问的页框不在主存中,那么此时必须分配一个页框,分为线性映射、非线性映射、swap情况下映射

(2)写时复制:被访问的页存在,但是该页是只读的,内核需要对该页进行写操作,此时内核将这个已存在的只读页中的数据复制到一个新的页框中

handle_pte_fault()调用pte_non()检查表项是否为空,即全为0;如果为空就说明映射尚未建立,此时调用do_no_page()来建立内存页面与交换文件的映射;反之,如果表项非空,说明页面已经映射,只要调用do_swap_page()将其换入内存即可;

如何查看进程发生缺页中断的次数?

用ps -o majflt,minflt -C program命令查看。
majflt 代表 major fault,中文名叫大错误,minflt 代表 minor fault,中文名叫小错误。

这两个数值表示一个进程自启动以来所发生的缺页中断的次数。

发生缺页中断后,执行了那些操作?

当一个进程发生缺页中断的时候,进程会陷入内核态,执行以下操作:

  1. 检查要访问的虚拟地址是否合法
  2. 查找/分配一个物理页
  3. 填充物理页内容(读取磁盘,或者直接置0,或者啥也不干)
  4. 建立映射关系(虚拟地址到物理地址) 重新执行发生缺页中断的那条指令 如果第3步,需要读取磁盘,那么这次缺页中断就是 majflt,否则就是 minflt。

TLB

为了提高映射速度,我们引入了TLB(Translation Lookaside Buffer),我们经常称为快表,专门用来做地址映射的硬件设备。它不在内存中,可存储的数据比较少,但是比内存要快。所以,我们可以想象,TLB 就是页表的 Cache,其中存储了当前最可能被访问到的页表项,其内容是部分页表项的一个副本。

有了 TLB 之后,地址映射的过程就像图中画的。我们先查块表,块表中有映射关系,然后直接转换为物理地址。如果在 TLB 查不到映射关系时,才会到内存中查询页表。

内核态内存映射

在系统初始化的时候,我们就创建内核页表

image-20201207182152880

内核态缺页异常

1)通过mm是否存在判断是否是内核线程,对于内核线程,进程描述符的mm总为NULL,一旦成立,说明是在内核态中发生的异常,跳到no_context

           if (in_atomic() || !mm)
                goto no_context;

如果当前执行流程在内核态,不论是在临界区还是内核进程本身(内核的mm为NULL),说明在内核态出了问题,跳到标号no_context进入内核态异常处理,由函数_do_kernel_fault完成;

2)_do_kernel_fault 这个函数首先尽可能的设法解决这个异常,通过查找异常表中和目前的异常对应的解决办法并调用执行;如果无法通过异常表解决,那么内核就要在打印其页表等内容后退出;

内存管理总结

物理内存根据 NUMA 架构分节点。每个节点里面再分区域。每个区域里面再分页。

物理页面通过伙伴系统进行分配。分配的物理页面要变成虚拟地址让上层可以访问,kswapd 可以根据物理页面的使用情况对页面进行换入换出。

对于内存的分配需求,可能来自内核态,也可能来自用户态。

image-20201207174552644

物理地址/线性地址/虚拟地址/逻辑地址

1)实模式下,”段基址+段内偏移地址”经过段部件的处理,直接输出的就是物理地址,CPU可以直接用此地址访问内存。

2)保护模式下,”段基址+段内偏移地址”经段部件处理后为线性地址。(但此处的段基址不再是真正的地址,而是一个选择子,本质上是个索引,类似于数组下标,通过这个索引便能在GDT中找到相应的段描述符。段描述符记录了该段的起始、大小等信息,这样便得到了段基址。)若没有开启地址分页功能,此线性地址就被当作物理地址来用,可直接访问内存。

3)保护模式+分页机制,若开启了分页功能,线性地址则称为虚拟地址(虚拟地址、线性地址在分页机制下都是一回事)。虚拟地址要经过CPU页部件转换成具体的物理地址,这样CPU才能将其送上地址总线取访问内存。

逻辑地址,无论是在实模式或保护模式下,段内偏移地址又称为有效地址,也称为逻辑地址,这是程序员可见的地址。最终的地址是由段基址和段内偏移地址组合而成。实模式下,段基址在对应的段寄存器中(cs ds es fs gs);保护模式下,段基址在段选择子寄存器指向的段描述符中。所以,只要给出段内偏移地址就行了,再加上对应的段基址即可。