虚拟内存的混乱
Confusion of virtual memory
考虑下面的示例。
char* p = (char*)malloc(4096);
p[0] = 'a';
p[1] = 'b';
调用malloc()
分配4KB内存。 OS处理user-space中用户程序的内存请求。首先,OS请求内存分配给RAM,然后RAM给OS分配物理内存地址。一旦 OS 收到物理地址, OS 将物理地址映射到虚拟地址,然后 OS returns 将虚拟地址即 p
的地址映射到用户程序。
我在虚拟地址中写入了一些值(a 和 b),它们确实写入了主存(RAM)。我很困惑我在虚拟地址而不是物理地址中写了一些值,但它确实写入了主内存(RAM),即使我不关心它们。
后面发生了什么? OS对我有什么用?我在一些书上找不到相关资料(OS,系统编程)。
你能解释一下吗? (为了便于理解,请省略缓存内容)
你要明白虚拟内存是虚拟的,它可以比物理内存RAM更广泛,所以映射不同。虽然其实是一样的
您的程序使用虚拟内存地址,决定保存在 RAM 中的是您的 OS。如果它满了,那么它将使用硬盘驱动器上的一些 space 来继续工作。
但是硬盘驱动器比 RAM 慢,这就是为什么您的 OS 使用一种算法,可以是 Round-Robin,根据工作在硬盘驱动器和 RAM 之间交换内存页面完成后,确保最有可能使用的数据在快速内存中。来回交换页面,OS不需要修改虚拟内存地址。
总结忽略了很多东西
您想了解虚拟内存的工作原理。有很多关于此的在线资源,这是我发现的一个似乎在尝试解释它而不会在技术细节上过于疯狂,但也没有掩盖重要术语。
https://searchstorage.techtarget.com/definition/virtual-memory
对于 x86 平台上的 Linux,相当于请求内存的程序集基本上是使用 int 0x80
调用内核,并将调用的一些参数设置到某些寄存器中。中断由 OS 在启动时设置,以便能够响应请求。在IDT中设置。
32 位系统的 IDT 描述符如下所示:
struct IDTDescr {
uint16_t offset_1; // offset bits 0..15
uint16_t selector; // a code segment selector in GDT or LDT
uint8_t zero; // unused, set to 0
uint8_t type_attr; // type and attributes, see below
uint16_t offset_2; // offset bits 16..31
};
偏移量是该中断处理程序的入口点地址。所以中断 0x80 在 IDT 中有一个条目。此条目指向处理程序(也称为 ISR)的地址。当您调用 malloc() 时,编译器会将此代码编译为系统调用。系统调用 returns 在某些寄存器中分配了内存的地址。我也很确定这个系统调用实际上会使用 sysenter x86 指令切换到内核模式。该指令与 MSR 寄存器一起使用,以在 MSR(模型特定寄存器)中指定的地址处从用户模式安全地跳转到内核模式。
进入内核模式后,可以执行所有指令,并解锁对所有硬件的访问。为了提供请求,OS 不会“向 RAM 请求内存”。 RAM 不知道 OS 使用什么内存。 RAM 只是盲目地响应其 DIMM 上的断言引脚并存储信息。 OS 只是在启动时使用由 BIOS 构建的 ACPI table 进行检查,以确定有多少 RAM 以及连接到计算机的不同设备有哪些避免写入某些 MMIO(内存映射 IO)。一旦 OS 知道有多少 RAM 可用(以及哪些部分可用),它将使用算法来确定每个进程应该获得可用 RAM 的哪些部分。
当您编译 C 代码时,编译器(和链接器)将在编译时确定所有内容的地址。当您启动该 executable 时,OS 知道该进程将使用的所有内存。因此它将相应地为该进程设置页面 tables。当您使用 malloc() 动态请求内存时,OS 确定您的进程应该获取物理内存的哪一部分并相应地更改(在运行时)页面 tables。
关于分页本身,大家可以多看看文章。一个简短的版本是 32 位分页。在 32 位分页中,每个 CPU 内核都有一个 CR3 寄存器。该寄存器包含页面全局目录底部的物理地址。 PGD包含几个页表底部的物理地址,这些页表本身包含几个物理页(https://wiki.osdev.org/Paging)底部的物理地址。虚拟地址分为 3 个部分。右边的 12 位 (LSB) 是物理页中的偏移量。中间的 10 位是页面中的偏移量table,10 MSB 是 PGD 中的偏移量。
所以当你写
char* p = (char*)malloc(4096);
p[0] = 'a';
p[1] = 'b';
您创建了一个 char* 类型的指针并进行系统调用以请求 4096 字节的内存。 OS 将该内存块的首地址放入某个常规寄存器(这取决于系统和 OS)。你不应该忘记 C 语言只是一个约定。操作系统通过编写兼容的编译器来实现该约定。这意味着编译器知道要使用什么寄存器和什么中断号(用于系统调用),因为它是专门为此编写的 OS。因此,编译器将在运行时将存储在这个特定寄存器中的地址存储到这个 char* 类型的指针中。在第二行,您告诉编译器您想要获取第一个地址处的 char 并将其设为 'a'。在第三行,您将第二个字符设置为 'b'。最后,你可以写一个等价的:
char* p = (char*)malloc(4096);
*p = 'a';
*(p + 1) = 'b';
p 是一个包含地址的变量。指针上的 + 操作会根据该指针中存储的内容的大小递增此地址。在这种情况下,指针指向一个字符,因此 + 操作将指针递增一个字符(一个字节)。如果它指向一个 int 那么它将增加 4 个字节(32 位)。实际指针的大小取决于系统。如果你有一个 32 位系统,那么指针就是 32 位宽(因为它包含一个地址)。在 64 位系统上,指针为 64 位宽。相当于你所做的静态内存是
char p[4096];
p[0] = 'a';
p[1] = 'b';
现在编译器将在编译时知道这个 table 将获得什么内存。它是静态内存。即便如此,p 仍表示指向该数组第一个字符的指针。这意味着你可以写
char p[4096];
*p = 'a';
*(p + 1) = 'b';
结果是一样的。
First, OS requests memory allocation to RAM,…
OS 不必申请内存。它在启动时可以访问所有内存。它保留自己的数据库,记录内存的哪些部分用于什么目的。当它想要为用户进程提供内存时,它会使用自己的数据库来查找一些可用的内存(或者停止将内存用于其他目的然后使其可用)。选择要使用的内存后,它会更新其数据库以记录它正在使用中。
… then RAM gives physical memory address to OS.
RAM 不给 OS 地址,除了启动时,OS 可能必须询问硬件以查看系统中可用的物理内存。
Once OS receives physical address, OS maps the physical address to virtual address…
虚拟内存映射通常被描述为将虚拟地址映射到物理地址。 OS 有一个用户进程中的虚拟内存地址数据库,它有一个物理内存数据库。当它满足进程提供虚拟内存的请求并决定用物理内存支持该虚拟内存时,OS 将通知硬件它选择的映射。这取决于硬件,但典型的方法是 OS 更新一些页面 table 条目,描述哪些虚拟地址被转换为哪些物理地址。
I wrote some value(a and b) in virtual address and they are really written into main memory(RAM).
当你的进程写入映射到物理内存的虚拟内存时,处理器会获取虚拟内存地址,在页面table条目或其他数据库中查找映射信息,并替换虚拟内存内存地址与物理内存地址。然后它将数据写入该物理内存。
您的问题的详细答案会很长,而且 Whosebug 放不下。
这里是对你问题的一小部分的非常简单的回答。
你写:
I'm confusing that I wrote some value in virtual address, not physical address, but it is really written to main memory
看来你对这里有一个很根本的误解。
虚拟地址“后面”没有内存。每当您访问程序中的虚拟地址时,它都会自动转换为物理地址,然后使用物理地址在主内存中进行访问。
转换发生在硬件中,即在处理器内部称为“MMU - 内存管理单元”的块中(参见 https://en.wikipedia.org/wiki/Memory_management_unit)。
MMU 拥有一个小但非常快的查找 table 告诉如何将虚拟地址转换为物理地址。 OS 配置此 table 但之后,转换发生时不涉及任何软件,并且 - 只是重复 - 只要您访问虚拟内存地址就会发生。
MMU 还将某种进程 ID 作为输入以进行转换。这是必需的,因为两个不同的进程可能使用相同的虚拟地址,但它们需要转换为两个不同的物理地址。
如上所述,MMU 查找 table (TLB) 很小,因此 MMU 无法容纳完整系统的所有翻译。当 MMU 无法进行翻译时,它会产生某种异常,从而触发某些 OS 软件。然后 OS 将重新编程 MMU,以便丢失的翻译进入 MMU 并且进程执行可以继续。注意:某些处理器可以在硬件中执行此操作,即不涉及 OS.
考虑下面的示例。
char* p = (char*)malloc(4096);
p[0] = 'a';
p[1] = 'b';
调用malloc()
分配4KB内存。 OS处理user-space中用户程序的内存请求。首先,OS请求内存分配给RAM,然后RAM给OS分配物理内存地址。一旦 OS 收到物理地址, OS 将物理地址映射到虚拟地址,然后 OS returns 将虚拟地址即 p
的地址映射到用户程序。
我在虚拟地址中写入了一些值(a 和 b),它们确实写入了主存(RAM)。我很困惑我在虚拟地址而不是物理地址中写了一些值,但它确实写入了主内存(RAM),即使我不关心它们。
后面发生了什么? OS对我有什么用?我在一些书上找不到相关资料(OS,系统编程)。 你能解释一下吗? (为了便于理解,请省略缓存内容)
你要明白虚拟内存是虚拟的,它可以比物理内存RAM更广泛,所以映射不同。虽然其实是一样的
您的程序使用虚拟内存地址,决定保存在 RAM 中的是您的 OS。如果它满了,那么它将使用硬盘驱动器上的一些 space 来继续工作。
但是硬盘驱动器比 RAM 慢,这就是为什么您的 OS 使用一种算法,可以是 Round-Robin,根据工作在硬盘驱动器和 RAM 之间交换内存页面完成后,确保最有可能使用的数据在快速内存中。来回交换页面,OS不需要修改虚拟内存地址。
总结忽略了很多东西
您想了解虚拟内存的工作原理。有很多关于此的在线资源,这是我发现的一个似乎在尝试解释它而不会在技术细节上过于疯狂,但也没有掩盖重要术语。
https://searchstorage.techtarget.com/definition/virtual-memory
对于 x86 平台上的 Linux,相当于请求内存的程序集基本上是使用 int 0x80
调用内核,并将调用的一些参数设置到某些寄存器中。中断由 OS 在启动时设置,以便能够响应请求。在IDT中设置。
32 位系统的 IDT 描述符如下所示:
struct IDTDescr {
uint16_t offset_1; // offset bits 0..15
uint16_t selector; // a code segment selector in GDT or LDT
uint8_t zero; // unused, set to 0
uint8_t type_attr; // type and attributes, see below
uint16_t offset_2; // offset bits 16..31
};
偏移量是该中断处理程序的入口点地址。所以中断 0x80 在 IDT 中有一个条目。此条目指向处理程序(也称为 ISR)的地址。当您调用 malloc() 时,编译器会将此代码编译为系统调用。系统调用 returns 在某些寄存器中分配了内存的地址。我也很确定这个系统调用实际上会使用 sysenter x86 指令切换到内核模式。该指令与 MSR 寄存器一起使用,以在 MSR(模型特定寄存器)中指定的地址处从用户模式安全地跳转到内核模式。
进入内核模式后,可以执行所有指令,并解锁对所有硬件的访问。为了提供请求,OS 不会“向 RAM 请求内存”。 RAM 不知道 OS 使用什么内存。 RAM 只是盲目地响应其 DIMM 上的断言引脚并存储信息。 OS 只是在启动时使用由 BIOS 构建的 ACPI table 进行检查,以确定有多少 RAM 以及连接到计算机的不同设备有哪些避免写入某些 MMIO(内存映射 IO)。一旦 OS 知道有多少 RAM 可用(以及哪些部分可用),它将使用算法来确定每个进程应该获得可用 RAM 的哪些部分。
当您编译 C 代码时,编译器(和链接器)将在编译时确定所有内容的地址。当您启动该 executable 时,OS 知道该进程将使用的所有内存。因此它将相应地为该进程设置页面 tables。当您使用 malloc() 动态请求内存时,OS 确定您的进程应该获取物理内存的哪一部分并相应地更改(在运行时)页面 tables。
关于分页本身,大家可以多看看文章。一个简短的版本是 32 位分页。在 32 位分页中,每个 CPU 内核都有一个 CR3 寄存器。该寄存器包含页面全局目录底部的物理地址。 PGD包含几个页表底部的物理地址,这些页表本身包含几个物理页(https://wiki.osdev.org/Paging)底部的物理地址。虚拟地址分为 3 个部分。右边的 12 位 (LSB) 是物理页中的偏移量。中间的 10 位是页面中的偏移量table,10 MSB 是 PGD 中的偏移量。
所以当你写
char* p = (char*)malloc(4096);
p[0] = 'a';
p[1] = 'b';
您创建了一个 char* 类型的指针并进行系统调用以请求 4096 字节的内存。 OS 将该内存块的首地址放入某个常规寄存器(这取决于系统和 OS)。你不应该忘记 C 语言只是一个约定。操作系统通过编写兼容的编译器来实现该约定。这意味着编译器知道要使用什么寄存器和什么中断号(用于系统调用),因为它是专门为此编写的 OS。因此,编译器将在运行时将存储在这个特定寄存器中的地址存储到这个 char* 类型的指针中。在第二行,您告诉编译器您想要获取第一个地址处的 char 并将其设为 'a'。在第三行,您将第二个字符设置为 'b'。最后,你可以写一个等价的:
char* p = (char*)malloc(4096);
*p = 'a';
*(p + 1) = 'b';
p 是一个包含地址的变量。指针上的 + 操作会根据该指针中存储的内容的大小递增此地址。在这种情况下,指针指向一个字符,因此 + 操作将指针递增一个字符(一个字节)。如果它指向一个 int 那么它将增加 4 个字节(32 位)。实际指针的大小取决于系统。如果你有一个 32 位系统,那么指针就是 32 位宽(因为它包含一个地址)。在 64 位系统上,指针为 64 位宽。相当于你所做的静态内存是
char p[4096];
p[0] = 'a';
p[1] = 'b';
现在编译器将在编译时知道这个 table 将获得什么内存。它是静态内存。即便如此,p 仍表示指向该数组第一个字符的指针。这意味着你可以写
char p[4096];
*p = 'a';
*(p + 1) = 'b';
结果是一样的。
First, OS requests memory allocation to RAM,…
OS 不必申请内存。它在启动时可以访问所有内存。它保留自己的数据库,记录内存的哪些部分用于什么目的。当它想要为用户进程提供内存时,它会使用自己的数据库来查找一些可用的内存(或者停止将内存用于其他目的然后使其可用)。选择要使用的内存后,它会更新其数据库以记录它正在使用中。
… then RAM gives physical memory address to OS.
RAM 不给 OS 地址,除了启动时,OS 可能必须询问硬件以查看系统中可用的物理内存。
Once OS receives physical address, OS maps the physical address to virtual address…
虚拟内存映射通常被描述为将虚拟地址映射到物理地址。 OS 有一个用户进程中的虚拟内存地址数据库,它有一个物理内存数据库。当它满足进程提供虚拟内存的请求并决定用物理内存支持该虚拟内存时,OS 将通知硬件它选择的映射。这取决于硬件,但典型的方法是 OS 更新一些页面 table 条目,描述哪些虚拟地址被转换为哪些物理地址。
I wrote some value(a and b) in virtual address and they are really written into main memory(RAM).
当你的进程写入映射到物理内存的虚拟内存时,处理器会获取虚拟内存地址,在页面table条目或其他数据库中查找映射信息,并替换虚拟内存内存地址与物理内存地址。然后它将数据写入该物理内存。
您的问题的详细答案会很长,而且 Whosebug 放不下。
这里是对你问题的一小部分的非常简单的回答。
你写:
I'm confusing that I wrote some value in virtual address, not physical address, but it is really written to main memory
看来你对这里有一个很根本的误解。
虚拟地址“后面”没有内存。每当您访问程序中的虚拟地址时,它都会自动转换为物理地址,然后使用物理地址在主内存中进行访问。
转换发生在硬件中,即在处理器内部称为“MMU - 内存管理单元”的块中(参见 https://en.wikipedia.org/wiki/Memory_management_unit)。
MMU 拥有一个小但非常快的查找 table 告诉如何将虚拟地址转换为物理地址。 OS 配置此 table 但之后,转换发生时不涉及任何软件,并且 - 只是重复 - 只要您访问虚拟内存地址就会发生。
MMU 还将某种进程 ID 作为输入以进行转换。这是必需的,因为两个不同的进程可能使用相同的虚拟地址,但它们需要转换为两个不同的物理地址。
如上所述,MMU 查找 table (TLB) 很小,因此 MMU 无法容纳完整系统的所有翻译。当 MMU 无法进行翻译时,它会产生某种异常,从而触发某些 OS 软件。然后 OS 将重新编程 MMU,以便丢失的翻译进入 MMU 并且进程执行可以继续。注意:某些处理器可以在硬件中执行此操作,即不涉及 OS.