首页 > 解决方案 > 操作系统:进程、分页和内存分配疑问

问题描述

我对进程和内存管理有几个疑问。列出主要的。我正在慢慢尝试自己解决这些问题,但我仍然希望得到各位专家的帮助 =)。

我理解与进程相关的数据结构或多或少是这些:文本、数据、堆栈、内核堆栈、堆、PCB。

如果进程已创建但 LTS 决定将其发送到辅助存储器,是否所有数据结构都复制到 SSD 上,或者可能只是文本和数据(以及内核空间中的 PCB)?

分页允许您以非连续方式分配进程:

  1. 内核如何知道进程是否试图访问非法内存区域?在页表上找不到索引后,内核是否意识到它甚至不在虚拟内存(辅助内存)中?如果是这样,是否会引发中断(或异常)?是立即处理还是稍后处理(可能有进程切换)?

  2. 如果进程是非连续分配的,内核如何意识到堆栈溢出,因为堆栈通常会向下增长并且堆会向上?也许内核使用 PCB 中的虚拟地址作为每个进程连续的内存指针,因此在每个函数调用时,它都会检查指向堆栈顶部的虚拟指针是否已触及堆?

  3. 程序如何生成它们的内部地址?例如,在虚拟内存的情况下,每个人都假设从地址 0x0000 ... 到地址 0xffffff ... 然后由内核进行映射?

  4. 这些过程是如何结束的?系统调用退出是否在正常终止(完成最后一条指令)和终止(由父进程、内核等)的情况下调用?进程本身是否进入内核模式并释放其相关内存?

  5. 内核调度程序(LTS、MTS、STS)何时被调用?据我了解,内核分为三种类型:

  1. 我猜分配文本和数据的页数取决于代码的“长度”和“全局”数据。另一方面,每个进程的每个堆和堆栈变量分配的页数是多少?例如,我记得 JVM 允许您更改堆栈的大小。

  2. 当一个正在运行的进程想要在内存中写入 n 个字节时,内核是否会尝试填充一个已经专用于它的页面并为剩余的字节创建一个新的页面(因此页表被加长)?

我真的很感谢那些会帮助我的人。祝你有美好的一天!

标签: memoryprocessoperating-systemheap-memorystack-memory

解决方案


我觉得你有很多误解。让我们尝试清除其中的一些。

如果进程已创建但 LTS 决定将其发送到辅助存储器,是否所有数据结构都复制到 SSD 上,或者可能只是文本和数据(以及内核空间中的 PCB)?

我不知道你说的 LTS 是什么意思。内核可以决定将一些页面发送到辅助内存,但只能在页面粒度上。这意味着它不会将整个文本段或完整的数据段发送到硬盘,而只会发送一个页面或一些页面。是的,PCB 存储在内核空间中并且从未被换出(请参阅此处:内核页面是否被换出?)。

内核如何知道进程是否试图访问非法内存区域?在页表上找不到索引后,内核是否意识到它甚至不在虚拟内存(辅助内存)中?如果是这样,是否会引发中断(或异常)?是立即处理还是稍后处理(可能有进程切换)?

在 x86-64 上,每个页表条目都有 12 位保留用于标志。第一个(最右边的位)是present位。在访问此条目所引用的页面时,它会告诉处理器是否应该引发页面错误。如果当前位为 0,则处理器引发页面错误并调用操作系统在 IDT 中定义的处理程序(中断 14)。虚拟内存不是辅助内存。这是不一样的。虚拟内存没有物理介质来支持它。这是一个概念,是的,是在硬件中实现的,但使用逻辑而不是物理介质。内核保存了 PCB 中进程的内存映射。在页面错误时,如果访问不在此内存映射中,它将终止该进程。

如果进程是非连续分配的,内核如何意识到堆栈溢出,因为堆栈通常会向下增长并且堆会向上?也许内核使用 PCB 中的虚拟地址作为每个进程连续的内存指针,因此在每个函数调用时,它都会检查指向堆栈顶部的虚拟指针是否已触及堆?

进程在虚拟内存中连续分配,但不在物理内存中。有关更多信息,请参阅我的答案:每个程序分配一个固定的堆栈大小?谁定义了每个运行的应用程序的堆栈内存量?. 我认为堆栈溢出是用页面保护来检查的。堆栈有一个最大大小(8MB),并在下面留下一个标记为不存在的页面,以确保如果访问此页面,内核会通过页面错误通知它应该终止进程。就其本身而言,用户模式下不会存在堆栈溢出攻击,因为分页机制已经通过页表隔离了不同的进程。堆保留了一部分虚拟内存,而且非常大。因此,堆可以根据您实际需要多少物理空间来支持它而增长。那是交换文件 + RAM 的大小。

程序如何生成它们的内部地址?例如,在虚拟内存的情况下,每个人都假设从地址 0x0000 ... 到地址 0xffffff ... 然后由内核进行映射?

这些程序假定可执行文件的基地址(通常为 0x400000)。今天,您还拥有 ASLR,其中所有符号都保存在可执行文件中,并在加载可执行文件时确定。实际上,这并没有做太多(但得到支持)。

这些过程是如何结束的?系统调用退出是否在正常终止(完成最后一条指令)和终止(由父进程、内核等)的情况下调用?进程本身是否进入内核模式并释放其相关内存?

内核对每个进程都有一个内存映射。当进程因异常终止而死时,内存映射将被交叉并清除该进程的使用。

内核调度程序(LTS、MTS、STS)何时被调用?

你所有的假设都是错误的。调度程序只能通过定时器中断来调用。内核不是一个进程。可以有内核线程,但它们主要是通过中断创建的。内核在启动时启动一个定时器,当有定时器中断时,内核调用调度程序。

我猜分配文本和数据的页数取决于代码的“长度”和“全局”数据。另一方面,每个进程的每个堆和堆栈变量分配的页数是多少?例如,我记得 JVM 允许您更改堆栈的大小。

堆和堆栈具有为它们保留的部分虚拟内存。文本/数据段从 0x400000 开始,在需要的地方结束。为他们保留的空间在虚拟内存中非常大。因此,它们受到可用于支持它们的物理内存量的限制。JVM 是另一回事。JVM 中的栈并不是真正的栈。JVM 中的堆栈可能是堆,因为 JVM 为所有程序的需要分配堆。

当一个正在运行的进程想要在内存中写入 n 个字节时,内核是否会尝试填充一个已经专用于它的页面并为剩余的字节创建一个新的页面(因此页表被加长)?

内核不这样做。在 Linux 上,libstdc++/libc C++/C 实现会这样做。当您动态分配内存时,C++/C 实现会跟踪分配的空间,这样它就不会为少量分配请求新页面。

编辑

编译(和解释?)程序是否仅适用于虚拟地址?

是的,他们这样做。一旦启用分页,一切都是虚拟地址。启用分页是通过内核在引导时设置的控制寄存器完成的。处理器的 MMU 将自动读取页表(其中一些被缓存)并将这些虚拟地址转换为物理地址。

那么PCB内部的指针也使用虚拟地址吗?

是的。比如Linux上的PCB就是task_struct。它包含一个名为 pgd 的字段,它是一个无符号长 *。它将保存一个虚拟地址,并且在取消引用时,它将返回 x86-64 上 PML4 的第一个条目。

而且由于每个进程的虚拟内存是连续的,内核可以立即识别堆栈溢出。

内核无法识别堆栈溢出。它不会为堆栈分配更多的页面,然后是堆栈的最大大小,堆栈的最大大小是 Linux 内核中的一个简单的全局变量。堆栈与推送弹出一起使用。它不能推送超过 8 个字节,因此只需为其保留一个页面保护,以便在访问时创建页面错误。

然而,调度程序是从我所理解的(至少在现代系统中)使用定时器机制(如循环)调用的。这是正确的?

循环不是计时器机制。定时器使用内存映射寄存器进行交互。在启动时使用 ACPI 表检测这些寄存器(请参阅我的答案:https ://cs.stackexchange.com/questions/141870/when-are-a-controllers-registers-loaded-and-ready-to-inform- an-io-操作/141918#141918)。它的工作原理类似于我为 USB 提供的答案(在我在此处提供的链接上)。Round-robin 是一种调度程序优先级方案,通常被称为 naive,因为它只是给每个进程一个时间片并按顺序执行它们,这在 Linux 内核中目前没有使用(我认为)。

我不明白最后一点。如何管理新内存的分配。

新内存的分配是通过系统调用完成的。请在此处查看我的答案以获取更多信息:当您调用克隆系统调用时,谁设置了 RIP 寄存器?.

用户模式进程通过调用程序集跳转到系统调用的处理syscall程序。它跳转到内核在启动时在 LSTAR64 寄存器中指定的地址。然后内核从汇编跳转到一个函数。此函数将执行用户模式进程所需的操作并返回到用户模式进程。这通常不是由程序员完成,而是由 C++/C 实现(通常称为标准库)完成,它是一个动态链接的用户模式库。

C++/C 标准库将跟踪它自己分配的内存,分配一些内存并保存记录。然后,如果您要求进行少量分配,它将使用它已经分配的页面,而不是使用 mmap(在 Linux 上)请求新的页面。


推荐阅读