进程大小是如何确定的?

7

我对这些概念非常陌生,但我想问大家一个非常基本的问题,我认为这个问题很简单,但我有些困惑,所以我来问一下。 问题是... 操作系统如何确定进程的大小? 首先让我解释清楚,假设我编写了一个C程序,并且我想知道它将占用多少内存,我该如何确定?其次,我知道进程有许多部分,例如代码段、数据段和BSS。那么这些的大小是预定的吗?另外,堆栈的大小是如何确定的?在计算进程的总大小时,堆栈的大小是否也需要考虑在内。

我们再次说,当我们加载程序时,进程被分配一个地址空间(由基址和限制寄存器完成,并由MMU控制,我猜),当进程尝试访问不在其地址空间中的内存位置时,我们会收到“分段故障”。进程如何访问不在其地址空间中的内存位置呢?据我了解,当发生某些缓冲区溢出时,地址就会损坏。现在,当进程想要访问已损坏的位置时,我们就会收到"分段故障"。是否还有其他方式违反地址规则呢?

第三个问题是,为什么堆栈向下增长,而堆向上增长。这个过程在所有操作系统中都一样吗?它如何影响性能?为什么不能以其他方式实现呢?

如果我在任何陈述中有错误,请纠正我。

谢谢 苏拉布

4个回答

3
当进程启动时,它会获得自己的虚拟地址空间。虚拟地址空间的大小取决于您的操作系统。通常情况下,32位进程获得4 GiB(4吉比二进制)地址,64位进程获得18 EiB(18艾字节二进制)地址。
您无法以任何方式访问未映射到您的虚拟地址空间中的任何内容,因为根据定义,任何未映射到那里的内容都没有地址供您使用。您可以尝试访问当前未映射到任何内容的虚拟地址空间的区域,在这种情况下,您会收到一个段错误异常。
并非所有地址空间在任何给定时间都映射到某些内容。也不是所有内容都可能被映射(可以映射多少取决于处理器和操作系统)。在当前一代英特尔处理器上,最多可以映射256 TiB的地址空间。请注意,操作系统可以进一步限制其数量。例如,对于具有最多4 GiB地址的32位进程(默认情况下),Windows保留2 GiB用于系统和2 GiB用于应用程序(但有一种方法可以使其成为1 GiB用于系统和3 GiB用于应用程序)。
地址空间的使用情况和映射情况在应用程序运行时会发生变化。操作系统特定的工具将允许您监视正在运行的应用程序的当前分配内存和虚拟地址空间。
代码段、数据段、BSS等术语是指链接器创建的可执行文件的不同区域。通常情况下,代码与静态不可变数据是分开的,而静态分配但可变数据是分开的。堆栈与上述所有内容都是分开的。它们的大小由编译器和链接器计算。请注意,每个二进制文件都有自己的部分,因此任何动态链接库都将单独映射到地址空间中,每个库都有自己的部分映射到某个位置。然而,堆和栈不是二进制映像的一部分,通常每个进程只有一个堆栈和一个堆。
堆栈的大小(至少初始堆栈)通常是固定的。编译器和/或链接器通常有一些标志,您可以使用这些标志设置要在运行时使用的堆栈大小。堆栈通常“向后增长”,因为这是处理器堆栈指令的工作方式。使堆栈向一个方向增长,其他内容向另一个方向增长,可以更轻松地组织内存,在这种情况下,您希望两者都是无限制的,但不知道每个内容可以增长多少。
堆通常指进程启动时未预先分配的任何内容。在最低级别上,有几个逻辑操作与堆管理相关(并非所有操作系统都按照我在此处描述的方式实现)。
虽然地址空间是固定的,但某些操作系统会跟踪进程当前回收的哪些部分。即使情况不是这样,进程本身也需要跟踪它。因此,最低级别的操作是实际上决定要使用地址空间的某个区域。
第二个低级别操作是指示操作系统将该区域映射到某个内容。通常可以这样做。
  • 未能交换的某些内存

  • 可被交换且映射到系统交换文件的内存

  • 可被交换且映射到其他文件的内存

  • 可被交换且以只读模式映射到其他文件的内存

  • 同一映射,另一个虚拟地址区域也映射到该映射

  • 同一映射,但以只读模式映射到另一个虚拟地址区域

  • 同一映射,但以写时复制模式映射到默认交换文件中的已复制数据

可能还有我忘记的其他组合,但这些是主要的。

当然,实际使用的总空间取决于如何定义它。当前使用的RAM不同于当前映射的地址空间。但正如我上面所写的,操作系统相关工具应该让您找出当前正在发生的情况。


2
这些部分由可执行文件预先确定。除了一个文件外,可能还有任何动态链接库的文件。虽然DLL的代码和常量数据应该在使用它的多个进程之间共享,但其特定于进程的非常量数据应在每个进程中计算。
此外,进程中可能有动态分配的内存。
如果进程中有多个线程,则每个线程都会有自己的堆栈。
此外,进程本身和内核代表其拥有每个线程、每个进程和每个库的数据结构(线程局部存储、命令行参数、各种资源的句柄、这些资源的结构等等)。
不知道所有实现细节的情况下,很难精确地计算出完整的进程大小。但你可以得到一个合理的估计。
关于“据我所知,当一些缓冲区溢出发生时,地址就会被破坏。”这并不一定是真的。首先,地址指的是什么?这取决于缓冲区附近的内存中有什么。如果有一个地址,那么在缓冲区溢出期间它可能会被覆盖。但如果附近有另一个缓冲区,其中包含你的图片,那么图片的像素可能会被覆盖。
当尝试访问没有必要权限的内存(例如映射或以其他方式出现在进程地址空间中的内核部分)时,你可能会遇到段错误或页面错误。或者它可能是只读位置。或该位置可能没有映射到物理内存。
不知道我们所讨论的性能如何受到堆栈和堆布局的影响,因此很难进行推断。你可以做出猜测,但这些猜测可能是错误的。
顺便说一下,你应该真正考虑为不同的问题单独提问。

谢谢...我会记住的下次...:-)...感谢解释。 - Sohrab Ahmad

1

一个进程如何访问不在其地址空间中的内存?

在内存保护的情况下,这是不可能的。但是它可能会被尝试。考虑随机指针或超出缓冲区的访问。如果您增加任何指针的长度足够长,它几乎肯定会漫游到未映射的地址范围内。以下是一个简单的例子:

 char *p = "some string";

 while (*p++ != 256)  /* Always true. Keeps incrementing p until segfault. */
     ;

这样的简单错误并不罕见,说得轻描淡写。


谢谢!我并不完全讨厌学习这种术语 :-) - Jens

0

我可以回答问题2和问题3。

回答问题2

在C语言中使用指针时,实际上是使用一个被解释为内存地址的数值(在现代操作系统中是逻辑地址,请参见脚注)。您可以随意修改这个地址。如果该值指向的地址不在您的地址空间中,则会出现段错误。

例如,考虑以下情况:您的操作系统为您的进程提供了从0x01000到0x09000的地址范围。然后

int * ptr = 0x01000;
printf("%d", ptr[0]); // * prints 4 bytes (sizeof(int) bytes) of your address space
int * ptr = 0x09100;
printf("%d", ptr[0]); // * You are accessing out of your space: segfault

大多数情况下,如你所指出的,segfault的原因是使用指向NULL(通常为0x00地址,但实现有关)或已损坏地址的指针。

请注意,在linux i386上,基址和限制寄存器并不像你想象的那样被使用。它们不是每个进程的限制,而是指向两种类型的段:用户空间或内核空间。

答案#3

堆栈增长是硬件相关的,而不是操作系统相关的。在i386汇编指令中,例如push和pop使堆栈相对于与堆栈相关的寄存器向下增长。例如,当您执行push时,堆栈指针会自动减少,而当您执行pop时,堆栈指针会增加。操作系统无法处理它。

脚注

在现代操作系统中,进程使用所谓的逻辑地址。该地址由操作系统映射到物理地址。要了解这一点,请编译此简单程序:

#include <stdio.h>

int main()
{
    int a = 10;
    printf("%p\n", &a);
    return 0;
}

如果您多次运行此程序(甚至同时运行),您会看到即使对于不同的实例,也会打印出相同的地址。当然,这不是真正的内存地址,而是一个逻辑地址,在需要时将映射到物理地址。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接