【问题标题】:Putting CPU and Memory Management model all together将 CPU 和内存管理模型放在一起
【发布时间】:2019-12-28 08:22:34
【问题描述】:

警告:这很长,但我希望它对将来像我这样的人有用。

我想我知道程序计数器是什么,惰性内存分配是如何工作的,MMU 是做什么的,虚拟内存地址如何映射到物理地址以及 L1、L2 缓存的用途。我真正遇到的问题是,当我们运行 C 代码时,它们如何在高层次上结合在一起。

假设我有这个 C 代码:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int* ptr;
    int n = 1000000, i = 0;

    // Dynamically allocate memory using malloc()
    ptr = (int*)malloc(n * sizeof(int));

    ptr[0] = 99;
    i += 100;
    printf("%d\n", ptr[0]);
    free(ptr);
    return 0;
}

所以这是我将所有内容放在一起的尝试:

  1. 在调用execve() 之后,部分可执行文件被加载到内存中,例如文本和数据段,但大部分代码不是——它们是按需加载的(按需分页)。

  2. 第一条指令的地址在进程表的程序计数器 (PC) 字段中以及物理上的 PC 寄存器中,可供使用。

  3. 随着 CPU 执行指令,PC 被更新(通常 +1,但跳转可以转到不同的地址)。

  4. 进入主函数:ptrni在栈中。

  5. 接下来,当我们调用malloc 时,C 库会要求操作系统(我认为是通过sbrk() sys 调用,还是mmap()?)在堆上分配一些内存。

  6. malloc 在这种情况下成功,返回一个虚拟内存地址 (VMA),但物理内存可能尚未分配。页表不包含VMA,所以当CPU试图访问这样的VMA时,会产生缺页。

  7. 在我们的例子中,当我们执行ptr[0] = 99 时,CPU 会引发页面错误。我不确定是分配了整个数组还是仅分配了第一页(4k 大小)。

但现在我不知道如何将缓存访问放入图片中。 i如何放入L1缓存?它与 VMA 有什么关系?

对不起,如果这令人困惑。我只是希望有人可以帮助我完成整个过程...

【问题讨论】:

  • 是的,您的这些步骤是正确的。操作系统不知道您的数组大小,因此页面错误只会带来该页面(尽管它可以通过预取进行优化)。缓存只是复制主内存内容以加快访问速度。它没有用户可见的地址。
  • 感谢您查看@Jester。您能否澄清“不知道数组大小,所以引入该页面”?具体是哪一页,抱歉。另外,我想我在试图弄清楚数据访问的顺序时迷失了方向。我有一个虚拟机地址,我想加载i 的值。 CPU怎么知道i"的值存储在这个地址的缓存中?L1缓存中也有TLB吗?
  • 缓存是一种优化。它们的管理可能会变得复杂,但以更简单的形式,L1 和 L2 只是“所有”内存访问“必须”通过的两个仓库。如果 CPU 在这样的仓库中找到它需要的东西,它就不会进入内存。缓存独立于分页,它们影响到内存的“一切”。当然,如果没有架构标签,就不可能完全正确(实际上我的陈述过于简单化了)。 CPU 设计人员在优化方面非常有创意,但他们不能总是负担得起缓存(端口压力、传输器......)
  • 在 linux 上,您可以使用 strace 来查看程序执行期间进行了哪些系统调用。
  • OT:关于:ptr = (int*)malloc(n * sizeof(int)); 1) malloc() 返回类型:void*,可以分配给任何指针。强制转换只会使代码混乱,使其更难以理解、调试等。 2) 参数应该具有类型:size_t 然而,变量n 具有类型int。编译器应该告诉您这种隐式转换的风险。 3) 始终检查 (!=NULL) 返回值以确保操作成功。如果不成功,请调用perror() 将您的错误消息和失败的文本原因输出到stderr

标签: c assembly memory operating-system malloc


【解决方案1】:

在程序运行之前,操作系统和 C 运行时会在 CPU 寄存器中设置必要的值。

正如您已经注意到的,预期的 PC 值由操作系统(例如由加载程序)设置,然后设置 CPU 的 PC(又名 IP)寄存器,可能使用“从中断返回”指令,两者切换到用户模式(激活该进程的虚拟内存映射),同时使用正确的 PC 值(虚拟地址)加载 CPU。

此外,SP 寄存器以某种方式设置:在某些系统中,这将类似于 PC 在“从中断返回”期间完成,但在其他(较旧的)系统中,用户代码将 SP 设置为预先安排的位置。在这两种情况下,SP 还拥有一个虚拟内存地址。

通常,在用户进程中运行的第一条指令位于名为crt0 的库中的一个传统上称为_start 的例程中(C RunTime 0(又名启动))。 _start 通常用汇编语言编写并处理从操作系统到用户模式的转换。根据需要_start 将建立调用C 代码所需的任何其他内容,然后调用main。如果main 返回到_start,它将执行exit 系统调用。

_start 的第一条指令获得控制权时,CPU 缓存(可能还有 TLB)会变冷。用户模式下的所有地址都是虚拟内存地址,它们在进程的(虚拟)地址空间内指定内存。处理器正在用户模式下运行。可能操作系统已经预加载了保存_start 的页面(或者至少是_start 的开头)。所以当处理器从_start 取指令时,它很可能会发生TLB 未命中,但不会出现页面错误,然后是缓存未命中。

TLB 是一组寄存器,在 CPU 中形成高速缓存,支持虚拟地址到物理地址的转换/映射。 TLB 未命中时,将从进程的虚拟内存映射中的结构(例如页表)中加载。由于第一个页面是预加载的,映射的尝试将成功,然后 TLB 将填充从虚拟 PC 页面到物理页面的正确映射。但是,L1/L2 等缓存也是冷的,因此接下来的访问会导致缓存未命中。内存系统将通过在每个级别填充高速缓存行来满足高速缓存未命中。最后一个指令字或一组字被提供给处理器,它开始执行指令。

如果 TLB 中不存在代码(通过 PC)或数据(通过某种取消引用)的虚拟地址,则处理器将查询页表,其中的未命中可能导致可恢复或不可恢复可恢复的页面错误。可恢复的页错误是页表中不存在的虚拟到物理映射,因为数据在磁盘上并且需要操作系统干预;而不可恢复的故障是对错误的虚拟内存的访问,即不允许,因为它们引用了未被操作系统分配/授权的虚拟内存。

变量imain 已知的堆栈相对位置。因此,当 main 想要写入 i 时,它将写入内存并从 SP 偏移,例如SP+8(i 也可以是寄存器变量,但我离题了)。由于 SP 是一个持有虚拟内存地址的指针,i 则有一个虚拟地址。该虚拟地址经过上述步骤:从虚拟页面到物理页面的 TLB 映射、可能的页面错误以及可能的缓存未命中。后续访问会产生TLB命中、缓存命中,从而全速运行。 (操作系统可能还会在运行进程之前预加载一些但不是所有的堆栈页面。)

malloc 操作将使用一些系统调用,最终导致向进程添加额外的虚拟内存。 (尽管您也注意到,malloc 对于当前请求来说已经绰绰有余,因此系统调用不会在每个malloc 中完成。)malloc 将返回一个虚拟内存地址,即用户模式虚拟地址中的指针空间。对于刚刚通过系统调用获得的内存,TLB 和缓存也很可能是代码,也可能页面还没有加载。在后一种情况下,将发生可恢复的页面错误,并且操作系统将分配一个物理页面以供使用。如果操作系统很智能,它将知道这是一个新的数据页,因此可以用零填充它,而不是从分页文件中加载它。然后它将为正确的映射设置页表条目,并恢复用户进程,这可能会导致 TLB 未命中,从页表中填充 TLB 条目,然后缓存未命中,并从物理页面中填充缓存行。

【讨论】:

  • 在进入用户空间之前,操作系统还会将数据放在用户空间堆栈(或进程虚拟地址空间中的某个已知位置)。例如,x86-64 System V ABI(Linux、MacOS 等)指定堆栈指针最初指向 argcargv[]envp[] 位于其上方。 (char* 的实际数组,而不仅仅是指向数组的指针。)我认为 Linux 在非 x86 ISA 上的工作方式通常相同。
  • (激活该进程的虚拟内存映射) 至少在 x86 上,iret 不会修改页表。操作系统使用与用户空间相同的页表,包括一些设置了位表示仅限内核的页面。 (在 x86 上,U/S = 用户/主管)。内核将这些相同的页面映射为每个用户空间页表的一部分。 (实际上可能只是将页目录指针指向相同的第 2/3/4 级页表。)还有一个位告诉 CPU 在更改顶级页指针时它可以将此条目保留在 TLB 中,即保证它的映射相同。
  • 有趣的事实:U/S 位是 Meltdown 绕过的(在 Intel 上),以及为什么解决方法实际上在返回给用户时留下映射的内核页面-space,除了 IDT 和内核入口点。但是内核通过 mov 到 CR3 来实现这一点; iret 不适合你。请参阅 wiki.osdev.org/Page_TablesWhy in 64bit the virtual address are 4 bits short (48bit long) compared with the physical address (52 bit long)? 了解页表嵌套图。
  • 有关页面遍历的更多详细信息,请参阅What happens after a L2 TLB miss?(对于当前的 x86 硬件与 MIPS 之类的东西,使用内核提供的处理程序进行软件 TLB 未命中处理)。
  • 访问i 的堆栈内存极不可能发生 TLB-miss,因为 _start 只是在调用 main 之前使用堆栈。 main 没有使用很多堆栈空间,所以它可能仍然是同一个页面,所以翻译在 TLB 中仍然很热。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-11-24
  • 1970-01-01
  • 1970-01-01
  • 2016-03-26
相关资源
最近更新 更多