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

12

在我继续进行操作系统开发研究的努力中,我已经在脑海中构建了一个几乎完整的图景。但有一件事仍让我困惑。

以下是基本的启动过程,据我所知:

1) BIOS/引导加载程序执行必要的检查,初始化所有内容。

2) 将内核加载到RAM中。

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

4) 当加载任务时,它被赋予一个虚拟地址空间,包括.text、.data、.bss、堆和栈。该任务“维护”其自己的堆栈指针,指向其自己的“虚拟”堆栈。

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

在这种抽象中,内核是一个“母”进程,在其中托管所有其他进程。我尝试在以下图表中传达我最好的理解:

enter image description here

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

其次,可执行程序如何知道它的虚拟堆栈呢?它是操作系统的工作来计算虚拟堆栈指针并将其放置在相关CPU寄存器中吗?剩余的堆栈簿记由CPU推和弹出命令完成吗?

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

谢谢。

3个回答

9
问题是,首先这个简单的模型是否正确?
你的模型非常简化但基本上是正确的-请注意,模型的最后两部分并不真正被视为引导过程的一部分,而内核也不是一个进程。将其视为一个进程可能是有用的,但它不符合进程的定义,也不像一个进程那样运行。
第二,可执行程序如何知道它的虚拟堆栈? 是操作系统的工作来计算虚拟堆栈指针并将其放置在相关的CPU寄存器中吗?剩余的堆栈簿记是由CPU弹出和推送命令完成的吗?
一个可执行的C程序不必“知道它的虚拟堆栈”。当一个C程序被编译成可执行文件时,局部变量通常是相对于堆栈指针引用的-例如,[ebp-4]。
当Linux加载一个新程序以执行时,它使用start_thread宏(从load_elf_binary调用)来初始化CPU的寄存器。该宏包含以下行:
regs->esp = new_esp;   

这将初始化CPU的堆栈指针寄存器为操作系统分配给线程堆栈的虚拟地址。

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

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

这就是将内核视为进程可能会让人感到困惑的地方。首先,在Linux中,线程具有用户堆栈和内核堆栈。它们基本上是相同的,只有保护和位置不同(内核堆栈用于在内核模式下执行时,用户堆栈用于在用户模式下执行时)。

内核本身没有自己的堆栈。内核代码总是在某个线程的上下文中执行,每个线程都有自己固定大小(通常为8KB)的内核堆栈。当一个线程从用户模式切换到内核模式时,CPU的堆栈指针会相应地更新。因此,当内核代码使用局部变量时,它们存储在执行它们的线程的内核堆栈上。
在系统启动期间,start_kernel函数初始化内核init线程,然后创建其他内核线程并开始初始化用户程序。因此,在系统启动后,CPU的堆栈指针将被初始化为指向init的内核堆栈。
至于堆,您可以在内核中使用kmalloc动态分配内存,它将尝试在内存中找到一个空闲页面 - 其内部实现使用get_zeroed_page

4
“内核本身没有自己的堆栈。”虽然不是错误的,但我认为我的回答中这一点可以澄清提问者的误解:“尽管内核可能有自己的线程,但内核不应该被视为在独立运行的‘母进程’……” 重要的提示是:线程(任务)有堆栈,用户模式进程和内核线程(在Linux中称为kthread)都是线程。此外,一个用户模式线程实际上会有多个堆栈:一个在用户模式下,另一个在内核中。 - Jonathon Reinhart

8
你忘记了一个重要的点: 虚拟内存 是由硬件强制执行的,通常称为 MMU (内存管理单元)。正是 MMU 将虚拟地址转换为物理地址。
内核通常将特定进程的页表基地址加载到 MMU 中的寄存器中。这就是从一个进程切换到另一个进程的虚拟内存空间的任务。在 x86 上,这个寄存器是 CR3
虚拟内存保护进程的内存不会相互干扰。A 进程的 RAM 不会映射到 B 进程中。(除了例如共享库,其中相同的代码内存映射到多个进程中,以节省内存)。
虚拟内存还可以保护内核内存空间免受用户模式进程的影响。覆盖内核地址空间的页面属性被设置为,在处理器运行在用户模式时,不允许在那里执行。
请注意,虽然内核可能有自己的线程,在内核空间完全运行,但内核不应该被视为独立于您的用户模式程序运行的“母进程”。内核基本上是您的用户模式程序的“另一半”!每当您发出系统调用时,CPU会自动转换到内核模式,并从由内核指定的预定义位置开始执行。然后,内核系统调用处理程序代表您在内核模式上执行,您的进程的内核模式上下文中。在内核处理您的请求所花费的时间将被记录,并“计入”您的进程。

4

在进程和线程之间建立关系时,有关内核的有用思考方式

您提供的模型非常简化,但总体上是正确的。同时,将内核视为“母进程”的思考方式并不是最好的,但它仍然有一定意义。我想提出另外两个更好的模型。

  1. 将内核视为一种特殊的共享库。与共享库类似,内核在不同进程之间共享。系统调用的执行方式在概念上类似于从共享库中调用例程。在两种情况下,在调用后,您会执行“外部”代码,但是在本地进程的上下文中。并且在两种情况下,您的代码都将基于堆栈继续执行计算。还要注意,在两种情况下,对“外部”代码的调用会导致阻塞您的“本地”代码的执行。在调用返回后,执行将继续从相同的代码点开始,并且具有从执行调用时的堆栈状态相同的状态。但是为什么我们认为内核是一种“特殊”的共享库呢?因为:

    a. 内核是每个系统进程共享的“库”。

    b. 内核是共享代码部分和数据部分的“库”。

    c. 内核是一种特别受保护的“库”。您的进程无法直接访问内核代码和数据。相反,它被迫通过特殊的“调用门”以内核控制的方式调用内核。

    d. 在系统调用的情况下,您的应用程序将在虚拟连续堆栈上执行。但实际上,此堆栈将由两个分离的部分组成。一个部分在用户模式下使用,第二部分将在进入内核时逻辑上附加到您的用户模式堆栈顶部,并在退出时取消附加。

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

    a. 服务器将所有计算机组装成单个网络。类似地,内核提供了一种进程间通信和同步的手段。内核作为中间人来调解整个通信过程(传输数据,路由消息和请求等)。

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

请注意,遵循客户端-服务器通信模式,客户端(进程)是网络中唯一的主动执行者。它们向服务器和彼此之间发出请求。服务器则是系统的反应部分,从不主动发起通信。相反,它仅回复传入的请求。 这些模型反映了系统各部分之间的资源共享/隔离关系以及内核和进程之间通信的客户端-服务器本质。
新进程启动时,内核使用可执行映像的提示来决定为进程的初始线程的用户模式堆栈保留多少虚拟地址空间以及如何保留。 有了这个决定后,内核设置用于进程主线程在执行开始后立即使用的一组处理器寄存器的初始值。这个设定包括堆栈指针的初始值设置。 在实际进程执行开始后,进程本身成为堆栈指针的责任方。 更有趣的事实是,进程对由它创建的每个新线程的堆栈指针进行初始化的责任也由进程本身承担。 但请注意,内核对系统中每个线程的内核模式堆栈的分配和管理负责。 还请注意,内核负责为堆栈分配物理内存,并通常使用页故障作为提示按需惰性完成此工作。 运行线程的堆栈指针由线程本身管理。在大多数情况下,编译器在构建可执行映像时执行堆栈指针管理。编译器通常跟踪堆栈指针的值并通过添加和跟踪与堆栈相关的所有指令来维护其一致性。 这些指令不仅限于“push”和“pop”。有许多CPU指令会影响堆栈,例如“call”和“ret”,“sub ESP”和“add ESP”等等。 因此,实际的堆栈指针管理策略大多是静态的,并在进程执行之前就已知。 有时,程序具有专门处理特殊堆栈管理的逻辑部分。例如,在C语言中实现协程或长跳转。 事实上,如果您愿意的话,可以在程序中随意处理堆栈指针。
我了解到三种内核堆栈架构方法:
  1. 系统中每个线程都有单独的内核栈。这是大多数基于单体内核的著名操作系统(包括Windows、Linux、Unix和MacOS)采用的方法。 虽然这种方法在内存方面会带来显着的开销并恶化缓存利用率,但它提高了内核的抢占能力,这对于长时间运行系统调用的单体内核尤其重要,特别是在多处理器环境下。 实际上,很久以前,Linux只有一个共享的内核栈,并且整个内核都被大内核锁所覆盖,这限制了可以同时执行系统调用的线程数量,仅为一个线程。 但是,Linux内核开发人员很快意识到,阻塞想要知道其PID的一个进程的执行,因为另一个进程已经开始通过非常慢的网络发送一个大数据包,这是完全低效的。

  2. 一个共享的内核栈。 对于微内核来说,权衡是非常不同的。 小内核加上短系统调用允许微内核设计者坚持使用单个内核栈的设计。 在证明所有系统调用都非常短的情况下,他们可以从改进的缓存利用率和较小的内存开销中受益,但仍然保持系统的响应性水平。

  3. 系统中每个处理器都有一个内核栈。 即使在微内核操作系统中,一个共享的内核栈也会严重影响整个操作系统在多处理器环境下的可扩展性。 因此,设计者经常采用上述两种方法之间的妥协方案,并为系统中的每个处理器(处理器核心)保留一个内核栈。 在这种情况下,他们可以从良好的缓存利用率和小的内存开销中受益,这比线程级别的栈方法要好得多,并略差于单个共享栈方法。 同时,他们还可以从系统的良好可扩展性和响应性中受益。

谢谢。


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