继续我在操作系统开发研究方面的努力,我已经构建了一个几乎完整的图片 . 还有一件事让我望而却步 .
根据我的理解,这是基本的启动过程:
1)BIOS / Bootloader执行必要的检查,初始化所有内容 .
2)内核加载到RAM中 .
3)内核执行其初始化并开始调度任务 .
4)加载任务时,会为其提供一个虚拟地址空间 . 包括.text,.data,.bss,堆和堆栈 . 此任务“维护”自己的堆栈指针,指向其自己的“虚拟”堆栈 .
5)上下文切换仅将寄存器文件(所有CPU寄存器),堆栈指针和程序计数器推入某个内核数据结构,并加载另一个属于另一个进程的集合 .
在这个抽象中,内核是一个“母”过程,其中所有其他进程都是托管的 . 我试图在下图中表达我最好的理解:
问题是,首先这个简单的模型是否正确?
其次,可执行程序如何识别其虚拟堆栈?是OS作业来计算虚拟堆栈指针并将其放在相关的CPU寄存器中吗?堆栈簿记的其余部分是由CPU弹出和推送命令完成的吗?
内核本身是否有自己的主堆栈和堆?
谢谢 .
3 回答
您的模型非常简化,但基本上是正确的 - 请注意,模型的最后两部分并不真正被认为是引导过程的一部分,而内核不是一个过程 . 将它可视化为一个可能很有用,但它不适合过程的定义,并且它不像一个过程 .
可执行的C程序不必是"aware of its virtual stack."当C程序被编译成可执行文件时,通常相对于堆栈指针引用局部变量 - 例如,
[ebp - 4]
.当Linux加载新程序以执行时,它使用start_thread宏(从load_elf_binary调用)来初始化CPU的寄存器 . 该宏包含以下行:
这会将CPU的堆栈指针寄存器初始化为操作系统分配给线程堆栈的 virtual 地址 .
如上所述,一旦加载了堆栈指针,诸如
pop
和push
之类的汇编命令将更改其值 . 操作系统负责确保存在与虚拟堆栈地址对应的物理页面 - 在使用大量堆栈内存的程序中,随着程序继续执行,物理页面的数量将增加 . 使用ulimit -a
命令可以找到每个进程的限制(在我的机器上,最大堆栈大小为8MB,或2KB页面) .这是将内核可视化为进程可能会变得混乱的地方 . 首先,Linux中的线程具有用户堆栈和内核堆栈 . 它们基本相同,仅在保护和位置上有所不同(在内核模式下执行时使用内核堆栈,在用户模式下执行时使用用户堆栈) .
内核本身没有自己的堆栈 . 内核代码总是在某个线程的上下文中执行,每个线程都有自己的固定大小(通常是8KB)内核堆栈 . 当线程从用户模式移动到内核模式时,CPU的堆栈指针会相应更新 . 因此,当内核代码使用局部变量时,它们存储在它们正在执行的线程的内核堆栈中 .
在系统启动期间,start_kernel函数初始化内核
init
线程,然后创建其他内核线程并开始初始化用户程序 . 因此,在系统启动后,CPU的堆栈指针将被初始化为指向init
的内核堆栈 .就堆而言,您可以使用
kmalloc
在内核中动态分配内存,这将尝试在内存中查找空闲页面 - 其内部实现使用get_zeroed_page .您忘记了一个重点:Virtual memory是 enforced by hardware ,通常称为MMU(内存管理单元) . MMU将虚拟地址转换为物理地址地址 .
内核通常将特定进程的页表的基地址加载到MMU中的寄存器中 . 这就是任务 - 将虚拟内存空间从一个进程切换到另一个进程 . 在x86上,该寄存器为CR3 .
虚拟内存可以保护进程的内存 . 进程A的RAM根本没有映射到进程B中 . (例如shared libraries,其中相同的代码存储器映射到多个进程,以节省内存) .
虚拟内存还可以保护内核内存空间免受用户模式进程的影响 . 设置覆盖内核地址空间的页面上的属性,以便当处理器在用户模式下运行时,不允许在那里执行 .
请注意,虽然内核可能有自己的线程(完全在内核空间中运行),但内核不应该被认为是独立于用户模式程序运行的"a mother process" . 内核基本上是你的用户模式程序的"the other half"!无论何时发出system call,CPU都会自动转换到内核模式,并开始在内核指定的预定义位置执行 . 然后,内核系统调用处理程序代表您在 your process 的内核模式上下文中执行 . 在内核中处理您的请求所花费的时间将被计入,并且"charged to"您的流程 .
The helpful ways of thinking about kernel in context of relationships with processes and threads
您提供的模型非常简化,但一般都是正确的 . 与此同时,关于内核的“母亲过程”的思考方式并不是最好的,但它仍然有一定道理 . 我想提出另外两个更好的模型 .
一个 . 内核是一个“库”,由系统中的每个进程共享 .
湾内核是一个“库”,它不仅共享代码段,还共享数据段 .
C . 内核是一个受到特别保护的“库” . 您的进程无法直接访问内核代码和数据 . 相反,它被迫通过特殊的“调用门”调用内核控制的方式 .
d . 在系统调用的情况下,您的应用程序将在几乎连续的堆栈上执行 . 但实际上,这个堆栈将由两个独立的部分组成 . 一部分用于用户模式,第二部分将在进入内核期间逻辑连接到用户模式堆栈的顶部,并在退出期间脱离 .
一个 . 服务器组装单个网络中的所有计算机 . 类似地,内核提供了进程间通信和同步的手段 . 内核作为中间的人工作,它调解整个通信过程(传输数据,路由消息和请求等) .
湾就像服务器为每台连接的计算机提供一些服务一样,内核为这些进程提供了一组服务 . 例如,像网络文件服务器允许的那样计算机读取和写入位于共享存储上的文件,您的内核允许进程执行相同的操作,但使用本地存储 .
请注意,在客户端 - 服务器通信范例之后,客户端(进程)是网络中唯一的活动参与者 . 他们向服务器和彼此之间发出请求 . 服务器又是系统的反应部分,它从不启动通信 . 相反,它只回复传入的请求 . 此模型反映了系统各部分之间的资源共享/隔离关系以及内核和进程之间通信的客户端 - 服务器性质 .
How stack management is performed, and what role plays kernel in that process
当新进程启动时,内核使用来自可执行映像的提示,决定虚拟地址空间将为进程的初始线程的用户模式堆栈保留的位置和数量 . 做出这个决定后,内核会设置处理器寄存器集的初始值,这些值将在执行开始后由进程的主线程使用 . 此设置包括设置堆栈指针的初始值 . 在实际开始执行流程之后,流程本身就成为堆栈指针的负责人 . 更有趣的是,进程负责初始化由它创建的每个新线程的堆栈指针 . 但请注意,内核内核负责为系统中的每个线程分配和管理内核模式堆栈 . 另请注意,内核对于堆栈的物理内存分配是可以承担的,并且通常使用页面错误作为提示,根据需要懒惰地执行此作业 . 运行线程的堆栈指针由线程本身管理 . 在大多数情况下,堆栈指针管理由编译器在构建可执行映像时执行 . 编译器通常通过添加和跟踪与堆栈相关的所有指令来跟踪堆栈指针值并保持其一致性 . 这些指令不仅仅受“推”和“流行”的限制 . 有许多CPU指令会影响堆栈,例如“call”和“ret”,“sub ESP”和“add ESP”等等 . 正如您所看到的,堆栈指针管理的实际策略大部分是静态的并且之前已知流程执行 . 有时,程序具有执行特殊堆栈管理的逻辑的特殊部分 . 例如,协同程序的实现或C中的长跳跃 . 事实上,如果你愿意,你可以用程序中的堆栈指针做任何你想做的事情 .
Kernel stack architectures
我知道这个问题的三种方法:
系统中每个线程分离内核堆栈 . 这是大多数知名操作系统采用的方法,基于单片内核,包括Windows,Linux,Unix,MacOS . 虽然这种方法导致内存方面的显着开销并且恶化缓存利用率,但它改善了内核的抢占,这对于具有长时间运行的系统调用的单片内核尤其在多处理器环境中是至关重要的 . 实际上,很久以前Linux只有一个共享内核堆栈,整个内核都被Big Kernel Lock所覆盖,它只能通过一个线程限制线程数量,这些线程可以同时执行系统调用 . 但是Linux内核开发人员已经很快意识到阻止执行一个想知道例如其PID的进程,因为另一个进程已经开始通过非常慢的网络发送大数据包是完全没有效率的 .
一个共享内核堆栈 . 微内核的权衡是非常不同的 . 具有短系统调用的小内核允许微内核设计者坚持使用单内核堆栈进行设计 . 在证明所有系统调用都非常短的情况下,它们可以从提高的缓存利用率和更小的内存开销中受益,但仍然可以保持良好的系统响应能力 .
系统中每个处理器的内核堆栈 . 即使在微内核操作系统中,一个共享内核堆栈也会严重影响多处理器环境中整个操作系统的可扩展性 . 因此,设计人员经常遵循上述两种方法之间的折衷方法,并在系统中为每个处理器(处理器内核)保留一个内核堆栈 . 在这种情况下,它们受益于良好的缓存利用率和较小的内存开销,这比每个线程的堆栈方法要好得多,并且比单个共享堆栈方法稍微恶化 . 同时,他们受益于系统良好的可扩展性和响应能力 .
谢谢 .