Non-preemptive multitasking 非抢占式多任务处理
非抢占式多任务处理将让操作系统运行其他任务的责任,例如响应来自鼠标的输入或运行后台任务,交给了程序员。通常,程序员可以将控制权交给操作系统。除了将巨大的责任推卸给为平台编写程序的每个程序员之外,这种方法自然容易出错。程序代码中的一个小错误可能会使整个系统停止或崩溃。我们称之为非抢占式多任务的另一个流行术语是协同多任务(cooperative multitasking)。Windows 3.1 使用协同多任务,并要求程序员通过使用特定的系统调用将控制权交给操作系统。一个行为不佳的应用程序就可能使整个系统瘫痪
Preemptive multitasking 抢占式多任务处理
解决方案是将在请求操作系统的程序(包括操作系统本身)之间调度 CPU 资源的责任置于操作系统的手中。操作系统可以停止一个进程的执行,做其他事情,然后切换回来。在这样的系统上,如果您在单核机器上编写并运行带有图形用户界面的程序,操作系统将停止您的程序以更新鼠标位置,然后再切换回您的程序继续。这种情况发生得如此频繁,以至于我们通常不会观察到 CPU 是否有大量工作或空闲的区别。操作系统负责调度任务,并通过在CPU上切换上下文来完成。这个过程每秒可以发生很多次,不仅是为了保持UI响应,也是为了给其他后台任务和IO事件一些时间。这是现在设计操作系统的主流方式
Hyper-threading 超线程技术
随着 CPU 的发展和增加更多的功能,如算术逻辑单元(ALU)和额外的逻辑单元,CPU 制造商意识到整个 CPU 没有得到充分利用。例如,当一个操作只需要 CPU 的某些部分时,一条指令可以同时在 ALU 上运行。这就是超线程的开端。例如,你今天的电脑可能有 6个核心 和 12个逻辑核心。这正是超线程的用武之地。它通过使用 CPU 的未使用部分来驱动 线程2 上的进程并同时在 线程1 上运行代码,从而在同一个内核上“模拟”两个内核。现在,使用超线程,我们实际上可以在一个线程上卸载一些工作,同时通过响应第二个线程中的事件保持 UI 交互,即使我们只有一个 CPU 核心,从而更好地利用我们的硬件
你可能会关心超线程技术的性能:事实证明,自上世纪 90 年代以来,超线程一直在不断改进。由于实际上并没有运行两个 cpu,因此会有一些操作需要等待对方完成。与单核的多任务处理相比,超线程的性能增益似乎接近 30%,但这主要取决于工作负载
Multicore processors 多核处理器
众所周知,处理器的时钟频率在很长一段时间内一直是平坦的。处理器通过改进缓存、分支预测和推测执行,以及处理器的处理管道(processing pipelines)来提高速度,但收益似乎正在减少。另一方面,新的处理器是如此之小,以至于我们可以在同一个芯片上安装许多处理器。现在,大多数 cpu 都有许多核心,而且大多数情况下,每个核心都有执行超线程的能力
Do you really write synchronous code?
从您的流程和您编写的代码的角度来看:一切通常都将按照您编写的顺序发生
从操作系统的角度来看:它可能会也可能不会中断您的代码,暂停它,并在恢复您的进程之前同时运行一些其他代码
从 CPU 的角度来看:它主要是一次执行一条指令。它不关心谁写的代码,所以当硬件中断发生时,它会立即停止并把控制权交给中断处理程序。这就是 CPU 处理并发的方式
然而,现代 cpu 也可以并行处理很多事情。大多数 cpu 都是流水线的,这意味着在当前指令执行时,下一条指令会被加载。它可能有一个分支预测器,试图找出下一步加载什么指令。处理器也可以通过使用乱序执行来重新排序指令(编译器对代码的优化也可能会出现汇编指令的重排),如果它认为这样做可以让事情更快,而不需要“询问”或“告诉”程序员或操作系统,所以你可能无法保证 A 在 B 之前发生。CPU 将一些工作卸载给独立的“协处理器”(coprocessors),比如 FPU 进行浮点计算,让主 CPU 准备好做其他任务等等。作为一个高级概述,将 CPU 建模为以**同步方式**操作是可以的,但是现在,让我们记住,这个模型带有一些注意事项,在讨论并行性、同步原语 (如互斥体和原子) 以及计算机和操作系统的安全性时,这些注意事项变得特别重要
Concurrency vs parallelism 并发与并行
Concurrency is about working smarter
Parallelism is a way of throwing more resources at the problem
考虑一下人类是如何有意识地运作的:我们不能并行地完成大多数任务,但我们可以同时做很多事情。例如,试着同时和两个或更多的人交谈,这比听起来难多了。你可以同时和很多人交谈,但你必须在他们之间切换语境,在从一个人切换到另一个人时暂停。人类在并发性方面做得相当好,但在并行性方面却很糟糕
- Resource: 这是我们需要能够推进任务的东西。我们的资源有限。这可能是CPU时间或内存
- Task: 这是一组需要某种资源才能进行的操作。一个任务必须由若干子操作组成
- Parallel: 这是在同一时间独立发生的事情
- Concurrent: 这些任务是并发进行的,但不一定并行进行
并发性及其与 I/O 的关系
当您需要聪明地优化使用资源时,编写异步代码大多是有意义的。现在,如果您编写了一个 working hard to solve a problem 的程序,那么并发性通常没有任何帮助。这就是并行性发挥作用的地方,因为如果您可以将问题分解为可以并行处理的部分,它可以为您提供一种将更多资源投入问题的方法。考虑以下两个不同的并发**场景**
- 当执行 I/O 并且需要等待一些外部事件发生时
- 是经典的 I/O 示例:在执行任务之前,必须等待网络调用、数据库查询或其他事情发生。但是,您有许多任务要做,而不是等待,您继续在其他地方工作,并且定期检查任务是否准备好进行,或者确保在任务准备好进行时通知您
- 当你需要分散注意力,防止一项任务等待太久时
- 是在 UI 中经常出现的情况。让我们假设你只有一个核心。如何防止整个 UI 在执行其他 cpu 密集型 任务时变得无响应。你可以每16 毫秒停止你正在做的任何任务,运行 update UI task,然后恢复你之后做的任何事情。这样,你将不得不每秒停止/恢复任务60次,但你也将拥有一个具有大约 60 Hz 刷新率 的 完全响应的 UI
操作系统提供的线程如何
当使用操作系统线程来理解并发性时,一个挑战是操作系统线程似乎被映射到内核。这并不一定是一个正确的心智模型,即使大多数操作系统会尝试将一个线程映射到一个内核,直到线程数量等于内核数量。一旦我们创建的线程数量超过了内核数量,操作系统就会在我们的线程之间切换,并使用调度程序并发地处理每个线程,从而给每个线程一些运行时间。还必须考虑这样一个事实,即您的程序不是系统上唯一运行的程序。其他程序也可能产生多个线程,这意味着线程数量将远远超过 CPU 上的内核数量。因此,线程可以是**并行执行任务的一种手段,但它们也可以是实现并发性**的一种手段
选择正确的参考系
当您编写的代码从您的角度来看是完全同步时,操作系统可能根本不会从头到尾运行您的代码。它可能会多次停止和恢复您的进程。当你认为 CPU 只专注于你的任务时,它可能会被中断并处理一些输入。当我们在**不提供任何其他上下文的情况下讨论并发性**时,我们使用的是 作为程序员的您 和 您的代码(您的进程) 作为 参考框架
Asynchronous vs concurrent 异步与并发
您可能会奇怪,为什么我们花这么多时间讨论多任务、并发性 和 并行性,而内容是关于 异步编程 的。主要原因是所有这些概念彼此密切相关,甚至可能具有相同(或重叠)的含义,这取决于它们所使用的上下文。为了学习的目的,将统一坚持这个定义:异步编程是编程语言或库对并发操作的抽象方式,以及我们作为语言或库的用户如何使用这种抽象来并发地执行任务。操作系统已经有一个现有的抽象来涵盖这一点,称为线程。使用**操作系统线程处理异步通常被称为**多线程编程。为了避免混淆,我们将不直接使用操作系统线程称为**异步编程,尽管它解决了同样的问题(注意,再说一次:不直接使用操作系统线程的处理方式下的情况,才叫异步编程**)
操作系统的角色
从操作系统的角度来看并发性
这与之前所说的需要在 参考框架(**作为程序员的您 和 您的代码(您的进程) **作为 参考框架)内讨论并发性的内容有关,并且解释了操作系统可能随时停止和启动您的进程。在大多数情况下,所说的同步代码**对我们程序员来说是同步的。操作系统和 CPU 都不是完全同步的。操作系统使用抢占式多任务处理,只要您正在运行的操作系统是抢占式调度进程**,您就不能保证您的代码一条指令一条指令地运行而不中断。操作系统将确保所有重要的进程从 CPU 获得一些时间来取得进展
当我们谈论具有 4、6、8 或 12个 物理内核的现代机器时,这就不那么简单了,因为如果系统负载很小(即压力不大,所以可能用一个核就能搞定的意思),您可能会在其中一个 cpu 上不间断地执行代码。这里的重要部分是,您不能确定,也不能保证您的代码将不间断地运行
与操作系统合作
当你发出网络请求时,你并 不是在要求 CPU 或 网卡 为你做什么,你是在要求操作系统为你与网卡对话。作为一名程序员,如果不发挥操作系统的优势,就不可能使系统达到最佳效率。你基本上不能直接访问硬件。您必须记住,操作系统是对硬件的抽象。这也意味着要从头开始理解一切,还需要知道操作系统如何处理这些任务。为了能够与操作系统一起工作,您需要知道如何与它通信
与操作系统通信
与操作系统的通信是通过我们所说的系统调用 (sycall) 进行的,我们需要知道如何进行系统调用,我们还需要了解我们每天使用的基本抽象是如何在幕后使用系统调用的。系统调用使用操作系统提供的公共 API,以便我们在“用户区 userland”编写的程序可以与操作系统通信。大多数情况下,作为程序员,这些调用被我们使用的语言或运行时**抽象掉了**
现在,系统调用是与您通信的内核所特有的一个例子,但是 UNIX 内核家族 有许多相似之处。UNIX 系统通过 libc 公开这一点。另一方面,Windows 使用自己的 API,通常称为 WinAPI,它的操作方式与基于 UNIX 的系统的操作方式截然不同。当我们深入研究 epoll, kqueue 和 IOCP 是如何工作的时候,它们在实现某个相同功能的系统调用方式上可能会有很大的不同。然而,系统调用并不是我们与操作系统交互的唯一方式,比如以下几种情况
- 图形用户界面(GUI):大多数现代操作系统都提供了图形用户界面,如 Windows 的桌面环境、macOS 的 Aqua 界面或 Linux 的各种桌面环境(如 GNOME、KDE 等)。用户可以通过点击、拖拽等直观操作与操作系统进行交互,而不需要直接进行系统调用
- 命令行界面(CLI):命令行界面允许用户通过输入命令来与操作系统交互。例如,在 Windows 中可以使用命令提示符(cmd)或 PowerShell,在 Linux 或 macOS 中可以使用终端。用户可以通过输入特定的命令来执行程序、管理系统资源等,而无需直接进行系统调用
- 应用程序编程接口(API):操作系统提供了各种 API,允许应用程序以更高级别的方式与操作系统进行交互。例如,Windows API(WinAPI)、POSIX API 等。应用程序可以使用这些 API 来执行文件操作、网络通信、图形渲染等任务,而无需直接进行系统调用
- 脚本语言:许多操作系统支持脚本语言(如Bash、Python、PowerShell等),允许用户编写脚本来自动化执行一系列任务。脚本语言通常提供了与操作系统交互的接口,使得用户可以在不直接进行系统调用的情况下执行复杂的操作
- 服务和管理工具:操作系统通常提供了各种服务和管理工具,允许用户以更高级别的方式管理系统资源和服务。例如,Windows 的任务管理器、服务管理器,Linux 的 systemd、top 命令等
系统调用与用户通过图形用户界面(GUI)、命令行界面(CLI)、应用程序编程接口(API)、脚本语言等方式与操作系统交互之间存在本质的区别,主要体现在以下几个方面
- 执行层级与权限
- 系统调用是操作系统提供给应用程序的接口,允许应用程序请求操作系统服务
- 系统调用通常发生在核心态(也称为内核态),是应用程序与操作系统内核之间的接口
- 通过系统调用,应用程序可以请求操作系统执行底层操作,如文件访问、进程控制、内存管理等
- GUI、CLI、API和脚本语言等方式通常运行在用户态。它们通过更高级别的抽象来简化与操作系统的交互,不需要直接访问操作系统的底层功能
- 功能与抽象级别
- 系统调用:提供操作系统最基础、最底层的服务。提供操作系统最基础、最底层的服务。应用程序通过系统调用来实现具体功能时,需要更详细地处理错误和异常情况
- 其他方式:提供更高级别的抽象和封装。例如,GUI 通过图形化界面简化操作,CLI 通过命令简化文本输入,API 通过预定义的函数接口简化编程,脚本语言通过脚本简化自动化任务
- 使用场景与目的
- 系统调用:主要用于实现操作系统级别的功能,如资源管理、进程调度等。开发者在编写需要直接与操作系统交互的应用程序时,会使用系统调用。例如,在 Linux 系统中,应用程序可以通过
<font style="color:rgb(5, 7, 59);">open</font>
、<font style="color:rgb(5, 7, 59);">read</font>
、<font style="color:rgb(5, 7, 59);">write</font>
、<font style="color:rgb(5, 7, 59);">close</font>
等系统调用来打开、读取、写入和关闭文件。这些系统调用直接与操作系统内核交互,处理文件的底层操作 - 其他方式:适用于更广泛的用户群体和应用程序场景。GUI 和 CLI 为普通用户提供直观的交互方式;API 为开发者提供编程接口;脚本语言为自动化任务提供灵活的执行方式
- 系统调用:主要用于实现操作系统级别的功能,如资源管理、进程调度等。开发者在编写需要直接与操作系统交互的应用程序时,会使用系统调用。例如,在 Linux 系统中,应用程序可以通过
CPU和操作系统合作
CPU 是否与操作系统配合?如果您认为 Rust 中的内联汇编看起来很陌生且令人困惑,请不要担心。我们将在稍后的部分对内联汇编进行适当的介绍。将确保遍历下面的每一行,直到您对语法更熟悉为止
use std::arch::asm;
fn main() {
let t = 100;
let t_ptr: *const usize = &t;
let x = dereference(t_ptr);
println!("{}", x);
}
fn dereference(ptr: *const usize) -> usize {
let mut res: usize;
unsafe {
asm!("mov {0}, [{1}]", out(reg) res, in(reg) ptr)
};
res
}
// 输出:100
不管怎样,我们只是在给 CPU 写指令。没有标准库,就没有系统调用;只是原始指令。OS 不可能参与到解参函数中,对吧?(慢慢引导)
Now, if you keep the dereference function but replace the main function with a function that creates a pointer to the 99999999999999 address, which we know is invalid, we get this function
use std::arch::asm;
fn main() {
let t_ptr: *const usize = 99999999999999 as *const usize;
let x = dereference(t_ptr);
println!("{}", x);
}
fn dereference(ptr: *const usize) -> usize {
let mut res: usize;
unsafe {
asm!("mov {0}, [{1}]", out(reg) res, in(reg) ptr)
};
res
}
我们得到一个 段错误 segmentation fault 。这并不奇怪,但你可能也注意到了,在不同平台上得到的错误是不同的。当然,操作系统在某种程度上是有影响的。来看看这里到底发生了什么
Down the rabbit hole(进入一个奇怪、神秘或令人困惑的境地或情境)
事实证明,在操作系统和 CPU 之间有大量的合作,但可能不是你天真地认为的方式。许多现代 cpu 提供了操作系统使用的一些基本基础设施。这种基础设施为我们提供了我们所期望的安全性和稳定性。实际上,大多数高级 CPU 提供的选项比 Linux、BSD 和 Windows 等操作系统实际使用的选项多得多。在这里特别想说两点
- CPU 如何阻止我们访问我们不应该访问的内存**(即硬件级别的保护:MMU 和页表等机制)**
- CPU 如何处理异步事件,如 I/O
CPU 是如何阻止我们访问我们不应该访问的内存的?
正如所提到的,现代 CPU 体系结构 通过设计定义了一些基本概念。这方面的一些例子如下
- Virtual memory 虚拟内存
- Page table 页表
- Page fault 缺页、页错误
- Exceptions 异常处理
- Privilege level 特权级别
具体的工作方式取决于特定的 CPU,所以在这里用一般的术语来对待它们(换句话说,这些方面可以决定了如何区别不同的 CPU )。大多数 现代 cpu 都有一个内存管理单元 (MMU)。这部分的 CPU 往往蚀刻在相同的染料(dye),甚至。MMU 的工作是将我们在程序中使用的虚拟地址转换为物理地址
- 当操作系统启动一个进程(比如我们的程序)时,它会为我们的进程设置一个页表,并确保 CPU上有一个特殊的寄存器指向这个页表
- 现在,当我们在前面的代码中尝试 解引用 t_ptr 时,该地址在某个时刻被发送给 MMU 进行转换,MMU 在页表中查找它,将其转换为内存中的物理地址,以便从中获取数据。在前面代码的情况下,t_ptr 将指向堆栈中保存值 100 的内存地址
- 当我们传入 9999999999999999 并要求 CPU 获取存储在该地址的内容(这就是解引用所做的)时,CPU 在页表中查找翻译,但找不到它,The CPU then treats this as a page fault
- 在启动(boot)时,操作系统向 CPU 提供一个 中断描述符表(interrupt descriptor table)。该表具有预定义的格式,其中操作系统为 CPU 可能遇到的预定义条件提供处理程序
- 由于操作系统提供了一个指向处理页面错误的函数的指针,当我们试图解引用 9999999999999999 时,CPU 跳转到该函数,从而将控制权交给操作系统
- 操作系统为我们打印一条很好的消息,让我们知道我们遇到了它所谓的 段错误(segmentation fault)。因此,该消息将根据运行代码的操作系统而有所不同
But can’t we just change the page table in the CPU?
这就是 Privilege level 特权级别 的用武之地。大多数现代操作系统都使用 two ring levels:ring 0 (内核空间) 和 ring 3 (用户空间)
- 大多数 cpu 都有一个比大多数现代操作系统使用的更多环的概念。这是有历史原因的,这也是为什么使用环0和环3(而不是环1和环2)的原因
- 页表中的每个条目都有关于它的附加信息。在这些信息中有关于它属于哪个 ring 的信息。此信息在操作系统启动时设置
- 在 ring 0 中执行的代码几乎可以不受限制地访问外部设备和内存,并且可以自由地更改在硬件级别提供安全性的寄存器
- 您在 ring 3 中编写的代码通常对 I/O 和某些 CPU 寄存器(和指令) 的访问非常有限。试图从 ring 3 发出指令或设置寄存器来更改页表将被 CPU 阻止。然后 CPU 将此视为异常,并跳转到操作系统提供的异常处理程序。这也是为什么除了与操作系统合作并通过系统调用处理 I/O 任务之外别无选择的原因。如果不是这样,系统就不会很安全
所以,总结一下:CPU 和操作系统合作得很好。大多数现代桌面 CPU 都是在考虑操作系统的情况下构建的,因此 CPU 提供了操作系统在启动时锁定的钩子(hooks)和 基础设施(infrastructure)。当操作系统生成一个进程时,操作系统还会设置其特权级别,确保正常进程保持在它为维护稳定性和安全性而定义的边界内
Interrupts, firmware, and I/O 中断、固件、输入输出
这一部分试图将这些东西(标题这些东西)联系在一起,并研究整个计算机如何作为一个系统来处理 I/O 和并发性。让我们看一下假设从网卡读取数据的一些步骤
记住这里我们化简了很多。这是一个相当复杂的操作,但我们将专注于我们最感兴趣的部分,并在此过程中跳过一些步骤
Step 1 – Our code
我们注册一个套接字。这是通过向操作系统发出系统调用来实现的。根据操作系统的不同,我们可以得到一个文件描述符(macOS/Linux)或一个套接字(Windows)。下一步是在套接字上注册我们对 Read 事件的兴趣
Step 2 – (重要)Registering(注册) events with the OS
向操作系统注册事件这有三种处理方式
- 我们告诉操作系统,我们对 Read 事件感兴趣,但我们希望通过将 线程控制权 交给(yielding control)操作系统来等待它发生。然后,操作系统通过存储寄存器状态(storing the register state)来 挂起(suspend)我们的线程,并 切换(switch)到其他线程
- 我们告诉操作系统,我们对 Read 事件感兴趣,但我们只想要一个任务的句柄,我们可以 轮询(poll)这个任务 来检查事件是否准备好了。操作系统不会挂起我们的线程,所以这 不会阻塞(因为我们的轮询在用户态,没进内核态) 我们的代码
- 我们告诉操作系统,我们可能会对许多事件感兴趣,但我们希望 订阅一个事件队列。当我们 轮询(poll)这个队列 时,它将阻塞(因为轮询队列,队列是系统调用,属于内核态,所以进入内核态,我们会被阻塞)我们的线程,直到一个或多个事件发生。这是现代异步框架处理并发性最常用的方法
Step 3 – The network card
我们在这里跳过了一些步骤,但我认为它们对我们的理解并不重要。在网卡上,有一个小型微控制器运行专门的固件。我们可以想象这个微控制器正在一个繁忙的循环中轮询,检查是否有数据传入。网卡处理其内部的确切方式与我在这里建议的略有不同,并且很可能因供应商而异。重要的部分是,在网卡上运行一个非常简单但特定的 CPU 来检查 **是否有传入事件(这个事件就是后面那句:固件注册了传入数据)。一旦固件注册了传入数据,它就会发出一个硬件中断**
Step 4 – Hardware interrupt
现代 CPU 有一组中断请求行 interrupt request line (IRQs),用于处理来自外部设备的事件。CPU有一组固定的中断行(interrupt lines)硬件中断是一种可以在任何时间发生的电信号。CPU 立即中断其正常工作流程,通过保存其寄存器的状态并查找中断处理程序来处理中断。中断处理程序在中断描述符表 interrupt descriptor table (IDT) 中定义
Step 5 – Interrupt handler
IDT(中断描述符) 是一个表,操作系统(或驱动程序)为可能发生的不同中断注册处理程序**(可以理解为:硬件中断是硬件电信号级别的,其中断的处理程序用软件实现,在操作系统实现,体现了 CPU 硬件级别和操作系统软件级别的合作)。每个入口指向一个特定中断的处理程序函数**。网卡的处理程序函数通常由该卡的驱动程序注册和处理
IDT 并不像图所示的那样存储在CPU上。它位于主存储器中一个固定且已知的位置。CPU 只在其中一个寄存器中保存一个指向表的指针
Step 6 – Writing the data
这个步骤可能会根据网卡上的 CPU 和固件而有很大的不同。如果网卡和 CPU 支持直接内存访问(DMA),这应该是当今所有现代系统的标准,那么网卡将直接将数据写入操作系统已经在主内存中设置的一组缓冲区(let mut buff = vec![ ])。在这样的系统中,当数据被写入内存时,网卡上的固件可能会发出中断(让 CPU 直接将数据写进内存)。DMA 非常高效,因为只有当数据已经在内存中时才会通知 CPU(呼应了前面那句:固件可能会向 CPU 发出中断)。在较旧的系统上,CPU 需要投入资源来处理来自网卡的数据传输
直接内存访问控制器**(DMAC)**被添加到图中,因为在这样的系统中,它将控制对内存的访问。它不是前面图中所示的 CPU 的一部分。我们现在在兔子洞里已经够深了,系统中不同部分的确切位置现在对我们来说并不重要,所以让我们继续
Step 7 – The driver
驱动程序通常会处理操作系统和网卡之间的通信。在某个时刻,缓冲区被填满,网卡发出中断。然后 CPU 跳转到该中断的处理程序。这种中断类型的中断处理程序是由驱动程序注册的,所以实际上是驱动程序处理这个事件,然后通知内核数据已经准备好可以读取
Step 8 – Reading the data
根据我们选择 方法 1(对一个事件感兴趣,交出控制权给操作系统,操作系统挂起我们的线程)、2(对一个事件感兴趣,要事件任务的句柄,我们线程自己轮询检查该任务句柄是否准备好,操作系统不阻塞我们)还是 3(可能对多个事件感兴趣,轮询内核态的事件队列,进入内核态功能,操作系统阻塞我们),操作系统 将执行如下操作(告诉我们程序,内存有数据可以去拿咯)
- Wake our thread 唤醒我们的线程
- Return Ready on the next poll
- Wake the thread and return a Read event for the handler we registered 唤醒线程并为我们注册的处理程序返回一个 Read 事件
知识拓展:中断和固件
Interrupts
- Hardware interrupts:硬件中断是通过 IRQ(中断请求) 发送电信号来创建的。这些硬件线路直接向 CPU 发出信号
- Software interrupts:这些中断是由软件而不是硬件发出的。与硬件中断的情况一样,CPU 跳转到 IDT 并为指定的中断运行处理程序
Firmware
固件并没有得到我们大多数人的太多关注;然而,它是我们生活的世界的重要组成部分。它可以在各种硬件上运行,并有各种奇怪的方式使我们编写程序的计算机工作。现在,固件需要一个微控制器才能工作。甚至CPU也有固件使其工作。这意味着在我们的系统上有更多的小“cpu”,而不是我们编程的核心。为什么这很重要?好吧,你记得并发是关于效率的,对吧?由于我们的系统上已经有许多 cpu/微控制器 在为我们工作,因此我们的关注点之一是在编写代码时不要复制或重复这些工作。如果网卡的固件不断检查是否有新数据到达,如果我们让CPU 不断检查是否有新数据到达,这是相当浪费的。如果我们每隔一段时间检查一次,或者在数据到达时得到通知,那就更好了