进程模型
进程模型
Process Elements

进程
Process Hierarchy
尽管操作系统可以同时运行多个进程,但实际上它只能直接启动一个称为
1 init-+-apmd
|-atd
|-cron
...
5 |-dhclient
|-firefox-bin-+-firefox-bin---2*[firefox-bin]
| |-java_vm---java_vm---13*[java_vm]
| `-swf_play
文件描述符(File Descriptors)
我们之前了解了
Registers
处理器实际上对寄存器中的值执行一般简单的操作。这些值被读取(或写入)到内存中,每个进程都分配有内核跟踪的内存。
因此,方程式的另一面是跟踪寄存器。当当前正在运行的进程放弃处理器以便其他进程可以运行时,它需要保存其当前状态。同样,当进程有更多时间在
内核状态(Kernel State)
在内部,内核需要跟踪每个进程的许多元素。操作系统要跟踪的另一个重要元素是进程状态。如果该进程当前正在运行,则使其处于运行状态是有意义的。但是,如果进程请求从磁盘读取文件,则从内存层次结构中我们知道这可能会花费大量时间。该进程应该放弃其当前执行以允许另一个进程运行,但是内核不必让该进程再次运行,直到磁盘中的数据在内存中可用为止。因此,它可以将进程标记为磁盘等待(或类似
一些过程比其他过程更重要,并且具有更高的优先级。请参阅下面有关调度程序的讨论。内核可以保留有关每个进程行为的统计信息,这有助于其决定进程的行为。例如,它主要是从磁盘读取还是主要是
内存(Memory)
所有程序代码以及变量和任何其他分配的存储都存储在该存储器中。内存的一部分可以在进程之间共享(称为共享内存read()
和 write()
之类的命令,文件看起来就像是其他任何类型的
一个过程可以进一步分为代码和数据部分。程序代码和数据应分开保存,因为它们需要与操作系统不同的权限,并且分开可以促进代码共享(如您稍后所见
The Stack
进程的另一个非常重要的部分是称为栈的内存区域。这可以视为流程数据部分的一部分,并且与任何程序的执行都密切相关。栈是通用的数据结构,其工作方式与一堆板块完全相同。您可以推动一个元素(将一个元素放在一叠元素的顶部
栈是函数调用的基础。每次调用一个函数都会得到一个新的栈框架。这是一个内存区域,通常至少包含完成后要返回的地址,函数的输入参数和局部变量的空间。按照惯例,栈通常会变小。这意味着栈从内存中的高地址开始,然后逐渐降低。

我们可以看到拥有栈如何带来函数的许多功能:
-
每个函数都有其输入参数的副本。这是因为为每个函数分配了一个新的栈帧,其参数位于新的内存区域中。
-
这就是为什么在函数内部定义的变量无法被其他函数看到的原因。全局变量(可以通过任何函数看到)都保存在数据存储器的单独区域中。
-
这有助于递归调用。这意味着一个函数可以自由地再次调用自身,因为将为其所有局部变量创建一个新的栈框架。
-
每个帧都包含要返回的地址。
C 仅允许从函数返回单个值,因此按照惯例,该值将在指定的寄存器中而不是栈中返回给调用函数。 -
由于每一帧都引用了它之前的那一帧,因此调试器可以向后“遍历”指针,跟随指针到达栈。由此可以生成栈跟踪,该跟踪向您显示调用该函数的所有函数。这对于调试非常有用。
-
您可以看到函数的工作方式完全适合栈的性质。任何函数都可以调用任何其他函数,然后成为其他函数(放在栈顶
) 。最终,该函数将返回到调用它的函数(将自身移出栈) 。 -
栈的确会使调用函数变慢,因为必须将值移出寄存器并移入内存。有些体系结构允许参数直接在寄存器中传递。但是,为了保持每个函数获得每个参数的唯一副本的语义,寄存器必须旋转。
-
您可能听说过术语栈溢出。这是通过传递伪造的值来入侵系统的一种常见方法。如果您是程序员,则可以接受对栈变量的任意输入(例如,从键盘或通过网络读取
) ,则需要明确说明数据的大小。允许不检查任何数量的数据将仅覆盖内存。通常,这会导致崩溃,但是有些人意识到,如果函数写满了足以在栈帧的返回地址部分中放置特定值的内存,则函数完成时将返回而不是返回正确的位置(从) ,他们可以使其返回到刚发送的数据中。如果该数据包含会破坏系统的二进制可执行代码(例如,以root 权限为用户启动终端) ,则说明您的计算机已受到威胁。发生这种情况是因为栈向下增长,但是数据是“向上”读取的(即从较低地址到较高地址) 。有几种解决方法:首先,作为程序员,您必须确保始终检查要接收到的数据量。操作系统可以通过确保栈被标记为不可执行来帮助程序员避免这种情况。也就是说,即使恶意用户试图将某些代码传递到您的程序中,处理器也不会运行任何代码。现代体系结构和操作系统支持此功能。 -
栈最终由编译器管理,因为它负责生成程序代码。对于操作系统来说,栈就像该进程的任何其他内存区域一样。
为了跟踪栈的当前增长,硬件将寄存器定义为栈指针。编译器(或编程器,在用汇编器编写时)使用该寄存器来跟踪栈的当前顶部。
1 $ cat sp.c
void function(void)
{
int i = 100;
5 int j = 200;
int k = 300;
}
$ gcc -fomit-frame-pointer -S sp.c
10
$ cat sp.s
.file "sp.c"
.text
.globl function
15 .type function, @function
function:
subl $16, %esp
movl $100, 4(%esp)
movl $200, 8(%esp)
20 movl $300, 12(%esp)
addl $16, %esp
ret
.size function, .-function
.ident "GCC: (GNU) 4.0.2 20050806 (prerelease) (Debian 4.0.1-4)"
25 .section .note.GNU-stack,"",@progbits
上面我们展示了一个在栈上分配三个变量的简单函数。该反汇编说明了在
然后,我们将这些值移动到栈存储器中(并在实函数中使用它们
The Heap
堆是由进程管理的用于动态分配内存的内存区域。这适用于在编译时不知道其内存要求的变量。堆的底部称为
堆通常由
内存布局

如我们所见,一个进程分配了较小的内存区域,每个区域都有特定的用途。上面给出了内核如何在内存中安排进程的示例。从顶部开始,内核会在进程的顶部为其自身保留一些内存(我们通过虚拟内存了解如何在所有进程之间实际共享此内存
进程上下文
进程是操作系统对一个正在运行的程序的一种抽象,在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。所谓的并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。无论是在单核还是多核系统中,可以通过处理器在进程间切换,来实现单个
操作系统保持跟踪进程运行所需的所有状态信息。这种状态,也就是上下文,它包括许多信息,例如

在《

-
程序代码和数据,对于所有的进程来说,代码是从同一固定地址开始,直接按照可执行目标文件的内容初始化。
-
堆,代码和数据区后紧随着的是运行时堆。代码和数据区是在进程一开始运行时就被规定了大小,与此不同,当调用如
malloc 和free 这样的C 标准库函数时,堆可以在运行时动态地扩展和收缩。 -
共享库:大约在地址空间的中间部分是一块用来存放像
C 标准库和数学库这样共享库的代码和数据的区域。 -
栈,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。
-
内核虚拟存储器:内核总是驻留在内存中,是操作系统的一部分。地址空间顶部的区域是为内核保留的,不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。