操作系统:进程、分页和内存分配疑问

Posted

技术标签:

【中文标题】操作系统:进程、分页和内存分配疑问【英文标题】:Operating Systems: Processes, Pagination and Memory Allocation doubts 【发布时间】:2021-12-20 02:11:55 【问题描述】:

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

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

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

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

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

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

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

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

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

单独的内核,位于所有进程之下。 内核在进程内部运行(它们只改变模式),但有“进程切换功能”。 内核本身是基于进程的,但仍然一切都基于进程切换函数。

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

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

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

【问题讨论】:

请编辑问题以将其限制为具有足够详细信息的特定问题,以确定适当的答案。 【参考方案1】:

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

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

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

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

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

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

进程在虚拟内存中连续分配,但不在物理内存中。请在此处查看我的答案以获取更多信息:Each program allocates a fixed stack size? Who defines the amount of stack memory for each application running?。我认为堆栈溢出是用页面保护来检查的。堆栈有一个最大大小(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-i-o-operation/141918#141918)。它的工作原理类似于我为 USB 提供的答案(在我在此处提供的链接上)。 Round-robin 是一种调度程序优先级方案,通常被称为 naive,因为它只是给每个进程一个时间片并按顺序执行它们,这在 Linux 内核中目前没有使用(我认为)。

最后一点我没看懂。如何管理新内存的分配。

新内存的分配是通过系统调用完成的。请在此处查看我的答案以获取更多信息:Who sets the RIP register when you call the clone syscall?。

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

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

【讨论】:

好的,谢谢。我仍然有一些疑问:编译(和解释?)程序只能使用虚拟地址吗?那么PCB内部的指针也使用虚拟地址吗?而且由于每个进程的虚拟内存是连续的,内核可以立即识别堆栈溢出。 LTS 我的意思是长期调度程序,但是调度程序是从我所理解的(至少在现代系统中)通过定时器机制(如循环)调用的。这是正确的?我不明白最后一点。如何管理新内存的分配。谢谢你的时间,你帮了我很多! @Bart Doe 我为您的评论编辑了我的答案。如果您认为此答案或我链接的任何帖子对您有帮助,请随时投票。 或将其标记为已接受(因为您没有投票的声誉)。 '调度程序只能通过定时器中断来调用。'大错特错,我说不出话来……你不明白操作系统是如何管理 CPU 执行资源的:( 请注意,这里的其他内容很好,但是可以在 I/O 完成时输入调度程序(对于良好的 I/O 性能绝对重要),以及在线程间发出信号时。定时器中断对于抢占式调度器来说甚至不是绝对必要的,但它对于超时 sycall、睡眠以及如果系统 CPU 资源过载、在就绪线程之间共享 CPU 非常有用。

以上是关于操作系统:进程、分页和内存分配疑问的主要内容,如果未能解决你的问题,请参考以下文章

操作系统| 存储器管理(基本分页和基本分段的逻辑地址结构变换连续分配方式分区分配算法首次适应循环首次适应最佳适应分页和分段的比较程序装入方式和链接方式)

操作系统| 存储器管理(基本分页和基本分段的逻辑地址结构变换连续分配方式分区分配算法首次适应循环首次适应最佳适应分页和分段的比较程序装入方式和链接方式)

操作系统面试专题:分页和分段存储管理有何区别?

操作系统笔记内存管理之分页,分段和段页式

如何在 Linux 中分配满足分页和缓存要求的内存?

现代操作系统是不是使用分页和分段?