Windows 编程/用户模式与内核模式
在Windows(以及大多数现代操作系统)中,运行在“用户模式”下的代码和运行在“内核模式”下的代码之间存在区别。本章将指明一些差异。首先,Intel CPU 具有被称为“环”的操作模式,它们指定了可供运行代码使用的指令和内存类型。总共有四个环
- 环 0(也称为内核模式)拥有对所有资源的完全访问权限。它是 Windows 内核运行的模式。
- 环 1 和 2 可以用访问级别进行自定义,但通常未使用,除非有虚拟机运行。
- 环 3(也称为用户模式)对资源的访问权限有限。
之所以如此设计,是因为如果所有程序都在内核模式下运行,它们将能够覆盖彼此的内存,并在崩溃时可能导致整个系统崩溃。
当程序启动(例如网页浏览器或文字处理器)时,它会在自己的进程中运行。进程包含它自己的“虚拟”内存空间和资源。它的内存是“虚拟的”,因为进程认为在地址0x12345678
处的内存实际上可能在物理内存中的地址0x65f7a678
处。类似地,两个不同的进程可能在(对于它们来说)0x00401000
处存储了不同的数据。这是通过将内存划分为称为页的块来实现的;在 x86 系统上,一页的大小为 4 KB。每个页面可以拥有自己的一组属性,例如只读/读写。CPU 拥有一个透明的机制,通过页表将虚拟地址转换为物理地址,而页表是由操作系统设置的。
虚拟内存有很多用处
- 进程无法访问其他进程的内存,
- 每个页面可以拥有不同的保护设置(只读或读写,仅内核模式),以及
- 进程的非活动内存区域可以“分页出”(存储)到页面文件,并在需要时由操作系统检索。当系统物理内存不足时,也会这样做。
Windows 启动的每个进程(除了系统“进程”)都在用户模式下运行。在这种模式下,程序无法直接修改分页,因此除了通过 API 函数外,无法访问其他程序的内存。用户模式下的程序也不能干扰中断和上下文切换。
当 Windows 首次加载时,Windows 内核会启动。它在内核模式下运行,并设置分页和虚拟内存。然后,它创建一些系统进程,并允许它们在用户模式下运行。那么,CPU 如何才能切换回内核模式呢?这不是由 CPU 自动完成的。CPU 经常受到某些事件(计时器、键盘、硬盘 I/O)的干扰,这些事件被称为中断。内核必须首先设置中断处理程序来处理这些事件。然后,每当中断发生时,CPU 就会停止执行当前运行的程序,立即切换到内核模式,并执行该事件的中断处理程序。处理程序会保存 CPU 的状态,执行与该事件相关的某些处理,并恢复 CPU 的状态(可能切换回用户模式),以便 CPU 可以恢复执行程序。
当程序想要调用 Windows API 函数1时,它会触发一个中断2,这会导致 CPU 切换到内核模式,并开始执行所需的 API 函数。当 API 函数完成处理后,它会切换回用户模式,并恢复执行程序。这是因为像ReadProcessMemory
这样的 API 函数无法在用户模式下工作;程序无法访问其他程序的内存。但在内核模式下,API 函数可以无限制地读取任何内存区域。
1. 实际上,Windows API 函数最终会调用另一个 API:本地 API。这是 Windows NT 内核系列使用的 API。此时CPU 会切换到内核模式。
2. 现代 CPU 拥有用于系统调用的特殊、更快的指令,例如 x86 上的sysenter
和sysexit
。这些指令会导致 CPU 切换到环 0,然后开始执行由操作系统设置的处理程序。
因此,程序运行并调用 API 函数。那么,其他程序如何获得运行的机会呢?大多数情况下,程序只是允许操作系统切换到另一个程序,因为它们正在等待某些东西(用户输入、硬盘)。这些程序被称为不可运行程序,由于它们调用内核来等待某些东西,因此内核知道要执行上下文切换以允许另一个程序运行。这是通过以下方式完成的
- 保存当前程序的状态(包括寄存器),
- 确定下一个要运行的程序,
- 并恢复另一个程序的状态。
如果一个程序(更准确地说是线程或进程)运行时间超过了一定时间(线程量子或进程的时间片),操作系统将上下文切换到另一个程序。这个概念被称为抢占。抢占是通过在处理器中设置一个定时中断来实现的,该中断将调用上下文切换。用于每个进程的时间片可能不同。