主存中的操作系统内核和进程

Operating system kernel and processes in main memory

继续我在 OS 发展研究方面的努力,我在脑海中构建了一个几乎完整的图景。一件事我还是没明白。

下面是基本的启动过程,据我了解:

1) BIOS/Bootloader 执行必要的检查,初始化所有内容。

2) 内核加载到内存中。

3) 内核执行初始化并开始调度任务。

4) 当一个任务被加载时,它被赋予一个虚拟地址 space 它驻留在其中。包括.text、.data、.bss、heap和stack。这个任务"maintains"它自己的堆栈指针,指向它自己的"virtual"堆栈。

5) 上下文切换只是将寄存器文件(所有 CPU 寄存器)、堆栈指针和程序计数器推入某个内核数据结构并加载属于另一个进程的另一组。

在此抽象中,内核是一个 "mother" 进程,其中托管所有其他进程。我试图在下图中表达我的最佳理解:

问题是,首先这个简单的模型是否正确?

其次,可执行程序是如何知道它的虚拟堆栈的?是不是OS计算虚拟栈指针,放到相关的CPU寄存器中? CPU pop 和 push 命令完成剩余的堆栈记账吗?

内核本身是否有自己的主栈和堆?

谢谢。

您忘记了重要的一点:Virtual memory 由硬件强制实施,通常称为 MMU(内存管理单元)。就是MMU把虚拟地址转换成物理地址。

内核通常会将特定进程的页面table的基址加载到MMU 中的寄存器中。这就是任务将虚拟内存 space 从一个进程切换到另一个进程的原因。在 x86 上,这个寄存器是 CR3.

虚拟内存保护进程的内存不受彼此影响。进程 A 的 RAM 根本没有映射到进程 B。(除了例如 shared libraries,其中相同的代码内存被映射到多个进程,以节省内存)。

虚拟内存还可以保护内核内存 space 免受用户模式进程的影响。覆盖内核地址 space 的页面上的属性被设置为,当处理器处于用户模式 ​​运行ning 时,不允许在那里执行。

请注意,虽然内核可能有自己的线程,运行 完全在内核 space 中,但实际上不应将内核视为 "a mother process" 运行s 独立于您的用户模式程序。内核基本上 "the other half"你的用户态程序!每当您发出 system call 时,CPU 会自动转换到内核模式,并开始在内核指定的预定义位置执行。内核系统调用处理程序然后代表您执行,您的进程的内核模式上下文中。内核处理您的请求所花费的时间是占,和 "charged to" 你的过程。

Question is, first is this simple model correct?

您的模型非常简化但基本上是正确的 - 请注意,您模型的最后两部分并未真正被视为引导过程的一部分,内核也不是一个过程。将其可视化为一个可能很有用,但它不符合流程的定义,而且它的行为也不像一个。

Second, how is the executable program made aware of its virtual stack? Is it the OS job to calculate the virtual stack pointer and place it in the relevant CPU register? Is the rest of the stack bookkeeping done by CPU pop and push commands?

可执行的 C 程序不一定是 "aware of its virtual stack." 当 C 程序编译成可执行文件时,局部变量通常是相对于堆栈指针引用的 - 例如,[ebp - 4] .

当Linux加载一个新程序执行时,它使用start_thread macro (which is called from load_elf_binary)初始化CPU的寄存器。该宏包含以下行:

regs->esp = new_esp;   

这会将 CPU 的堆栈指针寄存器初始化为 虚拟 地址,OS 已分配给线程的堆栈。

正如你所说,一旦堆栈指针被加载,poppush等汇编命令将改变它的值。操作系统负责确保有对应于虚拟堆栈地址的物理页面——在使用大量堆栈内存的程序中,物理页面的数量将随着程序的继续执行而增长。您可以使用 ulimit -a 命令找到每个进程的限制(在我的机器上,最大堆栈大小为 8MB,或 2KB 页)。

Does the kernel itself have its own main stack and heap?

这就是将内核可视化为一个进程会变得混乱的地方。首先,Linux中的线程有一个用户栈和一个内核栈。它们本质上是相同的,仅在保护和位置上有所不同(在内核模式下执行时使用内核堆栈,在用户模式下执行时使用用户堆栈)。

内核本身没有自己的堆栈。内核代码总是在某个线程的上下文中执行,每个线程都有自己的固定大小(通常为 8KB)的内核堆栈。当线程从用户模式移动到内核模式时,CPU 的堆栈指针会相应更新。因此,当内核代码使用局部变量时,它们存储在它们正在执行的线程的内核堆栈中。

在系统启动期间,start_kernel函数初始化内核init线程,然后该线程将创建其他内核线程并开始初始化用户程序。所以系统启动后CPU的栈指针会被初始化指向init的内核栈。

就堆而言,您可以使用 kmalloc 在内核中动态分配内存,它将尝试在内存中找到空闲页面 - 其内部实现使用 get_zeroed_page

在与进程和线程的关系上下文中思考内核的有用方法

您提供的模型非常简单,但总体上是正确的。 同时,将内核视为 "mother process" 的方式并不是最好的,但它仍然有一定的意义。 我想再推荐两个更好的模型。

  1. 尝试将内核视为一种特殊的共享库。 就像一个共享库内核在不同进程之间共享。 系统调用的执行方式在概念上类似于来自共享库的例程调用。 在这两种情况下,调用后,您执行 "foreign" 代码,但在您的本机进程的上下文中。 在这两种情况下,您的代码都会继续执行基于堆栈的计算。 另请注意,在这两种情况下,对 "foreign" 代码的调用都会导致 "native" 代码的执行受阻。 在调用 return 之后,执行继续从相同的代码点开始,并使用执行调用的堆栈的相同状态。 但是为什么我们认为内核是一种 "special" 的共享库呢?因为:

    一个。内核是一个"library",由系统中的每个进程共享。

    b。内核是一个"library",不仅共享代码段,还共享数据段。

    c。内核受到特别保护"library"。您的进程无法直接访问内核代码和数据。相反,它被迫通过特殊的 "call gates".

    调用内核控制方式

    d.在系统调用的情况下,您的应用程序将在几乎连续的堆栈上执行。但实际上这个堆栈将由两个独立的部分组成。一部分在用户模式下使用,第二部分将在进入内核期间逻辑附加到用户模式堆栈的顶部,并在退出期间解除附加。

  2. 另一种思考计算机计算组织的有用方法是将其视为不支持虚拟内存的 "virtual" 计算机网络。 您可以将进程视为一台虚拟的多处理器计算机,它只执行一个可以访问所有内存的程序。 在此模型中,每个 "virtual" 处理器将由执行线程表示。 就像您可以拥有一台具有多个处理器(或多核处理器)的计算机一样,您的进程中可以有多个 oncurrent 运行 线程。 就像在您的计算机中,所有处理器都共享对物理内存池的访问,您进程的所有线程都共享对同一虚拟地址 space 的访问。 就像单独的计算机在物理上彼此隔离一样,您的进程也在逻辑上彼此隔离。 在此模型中,内核由服务器表示,该服务器直接连接到星形拓扑网络中的每台计算机。 类似于网络服务器,内核有两个主要目的:

    一个。服务器将所有计算机组装在一个网络中。 同样,内核提供了一种进程间通信和同步的方法。内核充当中间人,调解整个通信过程(传输数据、路由消息和请求等)。

    b。就像服务器为每台连接的计算机提供一组服务一样,内核为进程提供一组服务。例如,就像网络文件服务器允许计算机读取和写入位于共享存储上的文件一样,您的内核允许进程执行相同的操作但使用本地存储。

请注意,遵循客户端-服务器通信范例,客户端(进程)是网络中唯一的活跃参与者。他们向服务器和彼此之间发出请求。服务器本身是系统的反应部分,它从不发起通信。相反,它只回复传入的请求。 该模型反映了系统各部分之间的资源 sharing/isolation 关系以及内核和进程之间通信的客户端-服务器性质。

堆栈管理是如何执行的,内核在该过程中扮演什么角色

当新进程启动时,内核使用来自可执行映像的提示,决定在何处以及多少虚拟地址 space 将保留给进程初始线程的用户模式堆栈。 做出此决定后,内核会为处理器寄存器组设置初始值,这些寄存器将在执行开始后由进程的主线程使用。 此设置包括设置堆栈指针的初始值。 在实际开始执行流程后,流程本身负责堆栈指针。 更有趣的是,进程负责初始化它创建的每个新线程的堆栈指针。 但请注意,内核内核负责为系统中的每个线程分配和管理内核模式堆栈。 另请注意,内核负责为堆栈分配物理内存,并且通常使用页面错误作为提示,根据需要懒惰地执行此工作。 运行线程的栈指针由线程自己管理。在大多数情况下,堆栈指针管理由编译器在构建可执行映像时执行。编译器通常通过添加和跟踪与堆栈相关的所有指令来跟踪堆栈指针值并保持其一致性。 此类指令不仅限于"push"和"pop"。影响堆栈的CPU指令有很多,例如"call"和"ret"、"sub ESP"和"add ESP"等。 因此,正如您所见,堆栈指针管理的实际策略大部分是静态的,并且在进程执行之前已知。 有时程序有一个特殊的逻辑部分来执行特殊的堆栈管理。 例如 C 中协程或长跳转的实现。 事实上,如果你愿意,你可以在你的程序中使用堆栈指针做任何你想做的事情。

内核栈架构

我知道解决此问题的三种方法:

  1. 系统中每个线程的独立内核堆栈。这是大多数基于单片内核的知名操作系统所采用的方法,包括Windows、Linux、Unix、MacOS。 虽然这种方法会导致显着的内存开销并恶化缓存利用率,但它改进了内核的抢占,这对于具有 long-运行 系统调用的单片内核至关重要,尤其是在多处理器环境中。 实际上,很久以前Linux只有一个共享内核堆栈,整个内核被Big Kernel Lock覆盖,限制了线程数,只能由一个线程并发执行系统调用。 但是 linux 内核开发人员很快就认识到,阻塞一个想知道其 PID 的进程的执行是完全低效的,因为另一个进程已经开始通过非常慢的网络发送一个大数据包。

  2. 一个共享内核堆栈。 微内核的权衡非常不同。 带有短系统调用的小内核允许微内核设计者坚持使用单一内核堆栈的设计。 在所有系统调用都非常短的证据存在的情况下,它们可以受益于提高的缓存利用率和更小的内存开销,但仍将系统响应保持在良好的水平。

  3. 系统中每个处理器的内核堆栈。 即使在微内核操作系统中,一个共享内核堆栈也会严重影响整个操作系统在多处理器环境中的可扩展性。 由于这个原因,设计人员经常采用看起来像是上述两种方法之间的折衷的方法,并为系统中的每个处理器(处理器核心)保留一个内核堆栈。 在那种情况下,它们受益于良好的缓存利用率和较小的内存开销,这比每个线程堆栈方法好得多,但比单个共享堆栈方法稍差。 同时,他们还受益于系统良好的可扩展性和响应能力。

谢谢。