拓展 | CO
逻辑运算相关
逻辑运算符
&
:按位与 |
:按位或 ^
:按位异或
&&
:逻辑与 ||
:逻辑或 !
:逻辑非
逻辑表达式

逻辑门电路符号

CPU 架构名词
x86
指 Intel 8086 处理器及其后续版本的架构系列。
最初是 16 位处理器,后来也包括了 32 位和 64 位版本。
x86 处理器均向下兼容。
Intel 为防止竞争对手的 x86 兼容处理器产品用相近的命名方式,并与它们作出区分,80486 以后不再采用 80( )86 的命名方式,本应被命名为 80586 或 i586 结构的处理器被命名为 Pentium,也被称为 P5 架构。
实际上在 80486 以后 Intel 推出的绝大数 CPU 也都是 x86 架构,并且使用 x86 架构的处理器制造商远非 Intel 一家,目前出售的的品牌或组装的 PC 很少不是 x86 CPU 的。可以说 x86 架构就是桌面级 CPU 的标准。不过虽说都是 x86 的,也只能说明使用的指令集是兼容 8086 的,除 8086 指令集之外的指令的支持情况就不一样,且指令的实现显然也是各不相同。
在许多情况下,特别是在对比其他架构时,可能会默认 “x86” 指的是 32 位的处理器架构,因为 32 位 x86 处理器曾经是个人电脑领域的主流。
x86-32 / IA-32 / i386
用来描述 32 位 x86 架构,包括了从 Intel 80386 处理器开始的 32 位版本。
80386 是 Intel 推出的第一款 32 位处理器。
x64 / x86-64 / AMD64
即 64 位 x86 架构。
AMD 率先发布 x86-64 架构,并正式命名为 AMD64。
IA-64
一种设计完全不同于 x86 架构的 64 位处理器架构,主要用于高性能计算和企业级服务器。
不兼容 x86-64,更别说 IA-32。被 AMD 抢先发布 x86-64 就是因为 Intel 当时重心放在对 64 位需求更加强劲的企业服务器和高性能计算机市场上。
IA-32、Linux 基础知识点补充
特权级的检查
访问门时的特权级检查
访问门时,CPL ≤ DPL[门] && CPL ≥ DPL[段]:
对于受访者为数据段
只有访问者的权限大于等于该数据段 DPL 表示的最低权限才能够继续访问(数值上 CPL ≤ 数据段的 DPL)。
对于受访者为非一致性代码段
只有访问者的权限等于该代码段 DPL 表示的最低权限才能够继续访问,即只能平级访问,任何权限大于或小于它的访问者都将被 CPU 拒之门外。
“受访者为代码段” 实际是指 CPU 从访问者所在的代码段转移到该代码段去执行,而 “访问” 非一致性代码段除了会转移去执行它外,会将 CPL 用该代码段的 DPL 替换。但是除了从中断处理过程返回外,任何时候 CPU 都不允许从高特权级转移到低特权级,因为 CPU 没有理由自降等级后再去做某事(为了助记,可以简单理解为低特权级能做的高特权级也能做,高特权级不需要找低特权级帮忙)。所以访问非一致性代码段只能平级访问。
对于受访者为一致性代码段
只有访问者的权限小于等于该 DPL 表示的最低权限才能够继续访问(数值上 CPL ≥ 一致性代码段的 DPL)。
一致性代码段又称为依从代码段,其一大特点就是转移后的特权级不会用自己的 DPL 替换,而是保持转移前的低特权级,这就是它称为 “一致、依从” 的原因。所以尽管看上去在低特权级可以访问高特权级有点倒反天罡,实际上并没有提升特权级,只是可以跑到特权级更高的代码段中去执行指令。
访问段时的特权级检查
访问段时,MAX(CPL, RPL) ≤ DPL[段]。访问者任何时候都不允许访问比自己特权更高的资源,无论受访资源时数据还是代码。

选择子中的 RPL 会不会被用户进程伪造?一般情况下,选择子都是由操作系统提供的。比如说在平坦模型下,整个 4GB 内存是一个段,操作系统为所有用户进程构建了两个用户级的 RPL 为 3 的选择子,分别指向 4GB 的用户数据段和 4GB 的用户代码段。因为用户程序在自己的虚拟地址空间中运行,各用户进程的虚拟地址不冲突,所以各用户进程共用这两个选择子就够了,也就是说各用户进程在申请系统服务时无需提供选择子。如果需要提交选择子作为参数,为安全起见,操作系统会把选择子中的 RPL 改为用户进程的 CPL,而用户进程的 CPL 不可能伪造,它起始是由操作系统在加载用户程序时赋予的,记录在 CS 段寄存器中的低 2 位,就是 RPL 的位置,而 CS 寄存器只能通过 call
、jmp
、ret
、int
、sysenter
等指令修改。
工作模式
IA-32 处理器主要有两种工作模式:实地址模式、保护模式。
实地址模式
实地址模式是为与 8086/8088 兼容而设置的,在加电或复位时处于这一模式。此模式下的存储管理、中断控制以及应用程序运行环境等都与 8086/8088 相同。
其最大寻址空间为 1MB,32 条地址线中的 $A_{31}$$A_{20}$ 不起作用。存储管理采用分段方式,每段的最大地址空间为 64KB,物理地址由段地址乘以 16 加上偏移地址构成,其中段地址位于段寄存器中,偏移地址用来指定段内一个存储单元。例如,当前指令地址为 (CS) << 4 + (IP),其中 CS (Code Segment) 为代码段寄存器,用于存放当前代码段地址,IP 寄存器中存放的是当前指令在代码段内的偏移地址。内存区 00000H003FFH 存放中断向量表,共存放 256 个中断向量,采用 8086/8088 的中断类型和中断处理方式。
保护模式
保护模式的引入是为了实现在多任务方式下对不同任务使用的虚拟存储空间进行完全的隔离,以保证不同任务之间不会相互破坏各自的代码和数据。保护模式是 80286 以上微处理器最常用的工作模式。系统启动后总是先进入实地址模式,对系统进行初始化,然后转入保护模式进行操作。在保护模式下,处理器采用虚拟存储器管理方式。
IA-32 采用段页式虚拟存储管理方式,CPU 首先通过分段方式得到线性地址 LA,再通过分页方式实现从线性地址到物理地址的转换。
其实保护模式下内存的管理模式分为两种:段模式和页模式。其中页模式也是基于段模式的。也就是说,保护模式的内存管理模式实际上是纯段模式和段页式两种。
Linux 中,无论是进程还是线程,到了内核里,统一都叫任务 Task。
地址转换相关
在保护模式下,IA-32 采用段页式虚拟存储管理方式,存储空间采用逻辑地址、线性地址和物理地址来进行描述。
IA-32 中的虚拟地址由 48 位组成,包含 16 位的段选择符和 32 位的段内偏移量(即有效地址)。为了便于多用户、多任务下的存储管理,IA-32 采用在分段基础上的分页机制。分段过程实现将逻辑地址转换为线性地址,分页过程再实现将线性地址转换为物理地址。
段选择符/段选择子

TI 表示段选择符选择哪一个段描述符表,TI = 0 表示选择全局描述符表 GDT,TI = 1 表示选择局部描述符表 LDT。
RPL 用来定义段选择符的特权等级,若 RPL = 00 则为第 0 级(内核态),若 RPL = 11 则为第 3 级(用户态)。
高 13 位的索引值用来确定当前使用的段描述符在描述符表中的位置,表示是其中的第几个段表项。
段描述符
段描述符是一种数据结构,实际上就是分段方式下的段表项。
根据段描述符的用途,可以将其分为两种类型:
- 一种是普通的代码段或数据段描述符,包括用户进程或内核的代码段和数据段描述符;
- 另一种是系统控制段描符。系统控制段描述符比较复杂,按照不同的用途又可将其分为两种类型:一种是特殊的系统控制段描述符,包括局部描述符表描述符和任务状态段描述符;另一种是控制转移描述符,可专称为门描述符,包括调用门描述符、任务门描述符、中断门描述符和陷阱门描述符。
一个段描述符占用 8 个字节,包括 32 位的基地址、20 位的限界限及一些特征位。段描述符的一般格式如下:

G 表示粒度大小,G = 1 说明段以页(4KB)为基本单位,G = 0 则段以字节为基本单位。
D = 0 表示地址和数据为 16 位宽,D = 1 表示地址和数据为 32 位宽。
P 说明段是否已在主存中。P = 1 表示存在,P = 0 表示不存在。Linux 总是把 P 置 1,因为它从来不会把一个段交换到磁盘上而是以页为单位进行交换。
DPL 代表访问段时对当前特权级的最低等级要求。因此,只有 CPL 为 0(内核态)时才可访问 DPL 为 0 的段,任何进程(CPL = 3 或 0)都可以访问 DPL 为 3 的段。
S = 0 表示是系统控制段描述符,S = 1 表示是普通的代码段或数据段描述符。
TYPE 指示段的访问权限或系统控制段描述符的类型。通常包含字段 A。
A 说明段是否已被访问过。A = 1 表示该段已被访问过,A = 0 表示未被访问过。
AVL 可以由操作系统定义使用。Linux 忽略该字段。
四种门描述符格式如下:

段描述符表
段描述符表实际上就是分段方式下的段表,由段描述符组成。
主要有三种类型:全局描述符表 GDT、局部描述符表 LDT、中断描述符表 IDT。
- GDT 只有一个,用来存放系统内每个任务都可能访问的描述符,例如内核代码段、内核数据段、用户代码段、用户数据段以及任务状态段 TSS 等都属于 GDT 中描述的段。
- LDT 用于存放某一个任务专用的段描述符。
- IDT 包含 256 个中断门、陷阱门和任务门描述符。
用户不可见寄存器
为了支持 IA-32 的分段机制,除了提供 6 个段寄存器外,还提供了多个用户进程不可直接访问的内部寄存器,包括描述符 cache、任务寄存器 TR、局部描述符表寄存器 LDTR、全局描述符表寄存器 GDTR 和中断描述符表寄存器 IDTR。
描述符 cache
一组用来存放当前段描述符信息的高速缓存,每当段寄存器装入新的段选择符时,处理器将段选择符指定的一个段描述符中部分信息装人相应的描述符 cache 中。这样,在进行逻辑地址到线性地址的转换过程中,MMU 就直接用对应描述符 cache 中保存的段地址来形成线性地址 LA,而不必每次都去主存访问段表,从而大大节省访问存储器的时间(类似 TLB)。
全局描述符表寄存器 GDTR(48 位)
高 32 位存放 GDT 首地址,低 16 位存放限界(最大字节数)。
中断描述符表寄存器 IDTR(48 位)
高 32 位存放 IDT 首地址,低 16 位存放限界(最大字节数)。
GDT 和 IDT 的最大长度可达 $2^{16}$B = 64KB,每个段描述符占 8B,因此 GDT 和 IDT 最多可有 $2^{13}$ 个表项,对应段选择符中高 13 位的索引值。
局部描述符表寄存器 LDTR(16 位)
存放局部描述符表 LDT 的段选择子(LDT 需要在 GDT 中注册)。通过该选择符可把 GDT 中的 LDT 描述符中的部分信息(包括 LDT 首地址、LDT 界限和访问权限等)装入 LDT 描述符 cache 中,从而使 CPU 可以快速访问 LDT。
选择子的高 13 位表示可索引的描述符范围,因此一个任务最多可定义 8192 个内存段,如果任务是用 LDT 来实现的话,最多可同时创建 8192 个任务。
任务寄存器 TR(16 位)
存放任务状态段 TSS 的段选择子。通过该选择符可把 GDT 中的 TSS 描述符中的部分信息(包括 TSS 首地址、TSS 界限和访问权限等)装入 TSS 描述符 cache 中,从而可以方便地对任务的状态信息进行访问。
指令地址转换大致过程(段页式)
第一阶段(分段机制:逻辑地址 → 线性地址)
CS + GDTR 可知当前任务代码段在内存中的段基址,代码段基址 + EIP 指针便形成了线性地址。
第二阶段(分页机制:线性地址 → 物理地址)
CR3 寄存器保存了当前任务顶级页表在内存中的物理地址。
Linux 中的 PCB
task_struct
进程控制块 PCB 有时也被称作进程描述符,Linux 内核中进程描述符对应的数据结构为
task_struct
。task_struct
成员很多,包括进程标识、进程状态、进程内核栈指针、进程调度信息、亲缘进程指针、文件描述符、内存描述符(表示进程的地址空间)、I/O 调度信息、进程通信机制等等,内容符合 PCB 的定义。thread_info
实际上
task_struct
存储的是通用的 PCB 信息。因为 Linux 内核是支持不同体系的,但是不同的体系结构可能进程需要存储的信息不尽相同,这就需要将体系结构相关的部分和无关的部分进行分离,Linux 中使用thread_info
存储体系架构有关的进程信息,可视为对task_struct
结构的补充。该结构的定义因不同的处理器而不同,大多数系统上该结构的内容类似于下列代码:注意里面的
thread_info.flags
与标志寄存器 flags 内容不同,进程的请求调度标志TIF_NEED_RESCHED
就在其中(表示该进程应该被抢占,需要调用 schedule 函数进行调度),而其他大部分标志是特定于硬件的,几乎不会使用;CPU
说明了正在执行该进程的 CPU 数目;preempt_count
指示进程当前是否可以被抢占;addr_limit
指定进程可以使用的虚拟地址的上限(对内核线程没用);restart_block
用于实现信号机制。task_struct、thread_info 和内核栈的布局
根据
CONFIG_THREAD_INFO_IN_TASK
的存在与否,task_struct
、thread_info
、内核栈三者在内核中存在两种布局:thread_info
结构在内核栈中(CONFIG_THREAD_INFO_IN_TASK
= N 时)thread_info
和栈stack
在一个联合体thread_union
内,共享一块内存:进程描述符
task_struct
中的成员void *stack
指向内核栈。不同的是,在 ARM 中,thread_info
结构体有成员struct task_struct *task
指向进程描述符task_struct
,而 x86 中没有(x86 早期内核 3.x 版本中有,后续版本被删除)。thread_info
结构在task_struct
中(CONFIG_THREAD_INFO_IN_TASK
= Y 时)此时
thread_info
是task_struct
的第一个成员。thread_union
中只有栈,即栈和thread_info
结构不再共享一块内存:应当注意,
task_struct.stack
指针,是指向栈区域内存基地址,即thread_union.stack
数组基地址,既不是栈顶也不是栈底,栈顶存在寄存器 esp 中,栈底是task_struct.stack
+THREAD_SIZE
。异常、中断、系统调用
IA-32 中异常和中断的处理
以下描述的情形是处理器在保护模式下运行的情况(操作系统内核已被初始化)。
异常和中断的检测
在每条指令执行过程会根据执行情况判定是否发生了某种内部异常事件,在每条指令执行结束时判定是否发生了内部中断。若检测到有异常或中断发生,则进入异常和中断响应阶段。
异常和中断的响应
在此期间 CPU 的控制逻辑完成以下工作:
确定检测到的异常/中断类型号 i,从 IDTR 指向的 IDT 中取出第 i 个表项 IDTi。
根据 IDTi 中的段选择子,从 GDTR 指向的 GDT 中取出相应的段描述符,得到对应异常处理程序或中断服务程序所在段的 DPL、基地址等信息。
Linux 下内核代码段的 DPL 为 0,基地址为 0。
Linux 为了使得它能够移植到绝大多数流行的处理器平台,简化了段页式虚拟存储管理。因为 RISC 体系结构对分段的支持非常有限,所以 Linux 仅使用了 IA-32 架构中的分页机制,而对于分段机制,则通过初始化时将所有段描述符的基地址来简化其功能。
将当前特权级 CPL(CS 寄存器最低两位)与段描述符中的 DPL 比较。若 CPL 小于 DPL,则产生 13 号异常 #GP(一般性保护错)。
Linux 中,内核代码段的 DPL 总是 0,因此不会发生 CPL 小于 DPL 的情况。
对于编程异常(陷阱指令引起),还需进一步做以下检查:若 IDTi 门描述符中的 DPL 小于 CPL,则产生 13 号异常。这个检查主要是为了防止恶意应用程序通过
int n
指令模拟非法异常和中断以进入内核态执行非法的破坏性操作。检查是否发生了特权级变化,即 CPL 是否与相应段描述符中的 DPL 不同。如果是的话,就需要从用户态切换至内核态,以使用内核对应的栈。通过以下步骤完成栈的切换:
① 读 TR 寄存器,以访问正在运行进程的 TSS 段;
② 将 TSS 段中保存的内核栈的段选择子
ss0
和栈指针esp0
分别装入寄存器 SS 和 ESP,然后在内核栈中保存原来的用户栈的 SS 和 ESP。显然用户栈的 SS 和 ESP 需要先临时保存在某个地方(Linux 存放在 per-CPU 变量中),以防被内核栈的段选择子和栈指针覆盖而找不到。
如果发生的是故障,则将发生故障的指令的逻辑地址写入 CS 和 EIP,以保证故障处理后能回到发生故障的指令执行。
在当前栈中保存 EFLAGS、CS 和 EIP 寄存器的内容。若是中断门,则将 EFLAGS 寄存器中的 IF 清 0(关中断)。
如果异常产生了一个硬件出错码,则将其保存在内核栈中。
将 IDTi 中的段选择符装入 CS,IDTi 中的偏移地址装入 EIP,它们是异常处理程序或中断服务程序第一条指令的逻辑地址。
这样,从下一个时钟开始,就执行异常处理程序或中断服务程序的第一条指令。
异常和中断的返回
在异常处理程序或中断服务程序中,处理完异常或中断后,通过执行最后一条指令 IRET(即中断返回指令)回到原被中断的进程继续执行。
CPU 在执行
IRET
指令的过程中完成以下工作:从内核栈中弹出 EIP、CS 和 EFLAGS,恢复断点和程序状态。
检查当前异常或中断处理程序的 CPL 是否等于 CS 中最低两位(即将此时 CS 选择子对应代码段的 DPL 和选择子中的 RPL 比较),若是,则说明异常或中断响应前后都处于一个特权级,
IRET
指令完成操作;否则,再继续完成下一步工作。从内核栈中弹出 SS 和 ESP,以恢复到异常或中断响应前的特权级进程所使用的栈。
检查 DS、ES、FS 和 GS 段寄存器的内容,若其中有某个寄存器的段选择子对应段的 DPL 小于 CPL,则将该寄存器清 0 这是为了防止恶意应用程序(CPL = 3)利用内核以前使用过的段寄存器(DPL = 0)来访问内核空间地址。
因为处理器的特权级检查只发生在往段寄存器中加载选择子的时候,检查通过后,再从该段中进行后续数据访问时就不需要再进行特权检查了(每次访问都检查太低效)。而 DS、ES、FS 和 GS 段寄存器之前的工作中不会被更新,故处理器在这里选择清 0(GDT 中第 0 个描述符不可用,称为哑描述符,为了防止选择子未初始化而设置。0 值选择子在 GDT 中检索到哑描述符时会引发处理器的异常)。
与 GDT 不同的是 LDT 中的第 0 个段描述符是可用的。因为要从 LDT 中选择段描述符时,选择子的 TI 位必须为 1,这就确保了选择子只有经过显式地初始化后才能从 LDT 中检索描述符,不存在忘记初始化的情况。
显然,执行完
IRET
指令后,CPU 自然回到原来发生异常或中断的进程继续执行。IA-32 的异常/中断类型
Linux 对异常和中断的处理
CPU 负责对异常和中断的检测与响应,操作系统负责初始化 IDT 以及编制好异常处理程序或中断服务程序。
IDT 的初始化
IA-32 提供了三种包含在 IDT 中的门描述符:中断门、陷阱门和任务门。Linux 运用 IA-32 提供的三种门描述符格式,构造了以下 5 种类型的门描述符:
中断门(DPL = 0,TYPE = 1110B)
所有 Linux 中断服务程序都通过中断门激活。
系统中断门(DPL = 3,TYPE = 1110B)
Linux 使用系统中断门激活 3 号中断(即断点)的异常处理程序,对应指令
int3
。因为 DPL 为 3,故任何情况下 CPL ≤ DPL,所以用户态下可以使用int3
指令。系统门(DPL = 3,TYPE = 1111B)
Linux 使用系统门激活三个陷阱型异常处理程序,它们的中断类型号是 4、5 和 128,分别对应指令
into
、bound
和int 0x80
。因为 DPL 为 3,故任何情况下 CPL ≤ DPL,所以在用户态下可以使用这三条指令。陷阱门(DPL = 0,TYPE = 1111B)
Linux 使用陷阱门阻止用户程序使用
int n
(n ≠ 128 或 3)指令模拟非法异常来陷入内核态运行。因为编程异常需要进一步检查门的 DPL 是否小于 CPL,若是的话,该指令将无法通过陷阱门,而出现 13 号异常#GP(通用保护错)。这里将 DPL 设为 0,那么在执行用户程序中的int n
(n ≠ 128 或 3)指令将会引发#GP 异常,从而阻止非法int n
指令的执行。任务门(DPL = 0,TYPE = 0101B)
Linux 中对 8 号中断(双重故障)用任务门激活。
系统初始化时,Linux 完成对 GDT、GDTR、IDT 和 IDTR 等的设置,这样以后一旦发生异常或中断,则 CPU 可以通过异常或中断响应机制调出异常或中断处理程序执行。
Linux 对异常和中断的处理有不同的考虑。
对异常的处理
对于 IA-32 产生的部分异常,Linux 都解释为一种出错条件。当硬件检测到异常发生后,硬件通过异常响应机制调出对应的异常处理程序(IA-32 对中断和异常的处理实际就是响应机制)。
异常处理程序的结构
所有异常处理程序的结构是一致的,都可以划分成以下三个部分:
准备阶段
在内核栈中保存各寄存器的内容(即现场信息),这部分大多用汇编语言程序实现(宏
SAVE_ALL
)。处理阶段
采用 C 函数(异常处理函数)进行具体的异常处理。
大部分异常处理函数会把硬件出错码和类型号保存在发生异常的当前进程的描述符中(PCB),然后向当前进程发送一个对应的信号。异常处理结束时,内核将检查是否发送过某种信号给当前进程。若没有发送,则继续第 3 步;若发送过信号,则强制当前进程接收信号,而异常处理结束。当前进程接收到一个信号后,若有对应的信号处理程序,则转到信号处理程序执行,执行结束后,返回到当前进程的逻辑控制流的断点处继续执行;若没有对应的信号处理程序,则调用内核的
abort
例程终止当前进程。程序员可自行定义信号处理函数来替换系统默认的信号处理程序。
恢复阶段
恢复保存在内核栈中的各个寄存器的内容,切换到用户态并返回到当前进程的逻辑控制流的断点处继续执行。
Linux 采用向发生异常的进程发送信号的机制实现异常处理,其主要出发点是尽量缩短在内核态的处理时间,尽可能把异常处理过程放在用户态下的信号处理程序中进行。用信号处理程序来处理异常,使得用户进程有机会捕捉并自定义异常处理方法。实际上,各种高级编程语言(如 C++)中的运行时环境的异常处理机制就是基于信号处理来实现的😮。如果异常全部由内核来处理,那么高级编程语言的异常处理机制就无法实现。
Linux 中异常对应的信号名和处理程序名
例如,如果某个进程执行了一条带有非法操作码的指令,CPU 就产生一个 6 号异常#UD,在对应的异常处理程序中,向当前进程发送一个 SIGILL 信号,以通知当前进程执行相应的信号处理程序或终止当前进程的运行。
所有对存储器的非法引用所对应的信号都是 SIGSEGV,所有与协处理器和浮点运算相关的异常,其对应的信号都是 SIGFPE,在 Linux 中,除法错(除数为 0 或带符号整数除法结果无法表示)也归类为浮点异常。1 号(单步跟踪)和 3 号(断点)的信号都是 SIGTRAP,因而都转到一个专门的用于程序调试的信号处理程序去执行。
并不是所有异常处理都只是发送一个信号到发送到异常的进程。例如,对于 14 号页故障异常#PF,在页故障处理程序中,需要判断是否是访问越级(如用户态下访问内核空间)、访问越权(如修改只读区的信息)或访问越界(如访问了无效存储区)等。如果发生了这些无法恢复的故障,页故障处理程序发送 SIGSEGV 信号给发生页故障异常的进程;如果没有发生无法恢复的故障而只是所需内容不在主存(缺页异常),则页故障处理程序负责把所缺失页面从硬盘装入主存,然后返回到发生缺页故障的指令继续执行。
对中断的处理
对于大部分异常,Linux 只是给引起异常的当前进程发送一个信号就结束异常处理,这种情况下,具体的异常处理要等到当前进程接收到信号并转到信号处理程序才能进行,而且大部分情况下,异常对应的信号处理结果就是显示异常信息并终止当前进程。
显然,这种方式不适合中断的处理。因为中断事件的发生与正在执行的当前进程很可能没有关系,因而将一个信号发给当前进程是没有意义的。
Linux 中处理的中断的类型
- I/O 中断:由 I/O 外设发出的中断请求。
- 时钟中断:由某个时钟产生的中断请求,告知一个固定的时间间隔到。
- 处理器中断:多处理器系统中其他处理器发出的中断请求。
后两种中断的情况超出袁书的范围。
I/O 中断与 PIC
对于 I/O 中断,每个能够发出中断请求的外部设备控制器都有一条 IRQ 线,所有外设的 IRQ 线都会连接到一个可编程中断控制器 PIC (Programmable Interrupt Controller) 中对应的 IRQ 引脚上。PIC 中每个 IRQ 引脚都有一个编号,如 IRQ0、IRQ1、…、IRQi,这里编号 i 就是 IRQ 的值。Intel 处理器共支持 256 个中断,而单个 PIC 引脚有限,如较流行的 PIC 芯片 Intel 8259A 芯片,单个 8259A 芯片只有 8 个中断信号请求线:IRQ0~IRQ7,这显然是不够的,所以通常采用级联方式串联多个 8259A 芯片。级联时只能有一片 8259A 芯片为主片 master,其余的均为从片 slave。来自从片的中断只能传递给主片,再由主片向上提供给 CPU,也就是说只有主片才会向 CPU 发送 INT 中断信号。$n$ 片 8259A 通过级联可支持 7n+1 个中断源(级联一片要占用一个 IRQ 接口),最多可级联 9 个,也就是最多支持 64 个中断。
PIC 需要对所有外设发来的 IRQ 请求按照有限级进行排队,如果至少有一个 IRQ 线有请求且未被屏蔽,则 PIC 向 CPU 的 INTR 引脚发中断请求。CPU 每执行完一条指令后都会查询 INTR 引脚,若发现有中断请求,则进入中断响应过程,调出中断服务程序执行。
中断服务程序的结构
所有中断服务程序的结构类似,都可划分为以下三个阶段:
准备阶段
在内核栈中保存各寄存器的内容以及所请求 IRQ 的值等(宏
SAVE_ALL
),并给发出中断请求信号的 PIC 回送应答信息,允许其发送新的中断请求信号。因为在响应中断的过程中,CPU 已经禁止了 PIC 发送中断请求的功能。处理阶段
执行 IRQ 对应的中断服务例程。
恢复阶段
恢复保存在内核栈中的各个寄存器的内容,切换到用户态并返回到当前进程的逻辑控制流的断点处继续执行。
IA-32 + Linux 的系统调用
系统调用是一种特殊的 “异常事件”,是操作系统为用户提供服务的一种手段。
Linux 系统调用的类型
Linux 提供了几百种系统调用,主要分为以下几类:进程控制、文件操作、文件系统操作、系统控制、内存管理、网络管理、用户管理和进程通信。
系统调用号用整数表示,它用来确定系统调用跳转表中的偏移量。跳转表中的每个表项给出相应系统调用对应的系统调用服务例程的首地址。
封装函数
内核实现的系统调用是以一个软中断的形式(即陷阱指令,如
int 0x80
)来提供的,如果高级语言编写的用户程序直接用陷阱指令来调用系统调用,则会很麻烦,因此需要将系统调用封装成用户程序能直接调用的函数,如exit()
、read()
和open()
,这些都是标准 C 库中系统调用对应的封装函数。在用 C 语言编写的用户程序中,只要用 #include 命令嵌入相应的头文件,就可以直接使用这些函数来调出操作系统内核中相应的系统调用服务例程,以完成与 I/O、文件操作以及进程管理等相关的操作。这里将系统调用及对应的封装函数称为系统级函数。从 C 语言编程者角度来看,系统级函数在形式上与普通的应用编程接口 API 以及普通的 C 语言函数没有差别。但实际上它们在机器级代码的具体实现上是不同的。例如,在 IA-32 + Linux 中,普通函数(包括 API)使用
CALL
指令来实现过程调用,而系统调用则使用陷阱指令(如int 0x80
或sysenter
)来实现。对于过程调用,执行CALL
指令前后,处理器一直在用户态下执行指令,所执行的指令是受限的,所能访问的存储空间也是受限的;而对于系统调用,一旦执行了发出系统调用的陷阱指令,处理器就从用户态转到内核态下运行,此时,CPU 可以执行特权指令并访问内核空间。实现普通的 API 以及普通的库函数可能会使用一个或多个系统调用服务功能,也可能不需要使用系统调用服务功能。
在 Linux 系统中,系统调用所用的参数通过寄存器传递,而不是像过程调用那样用栈来传递,因此在封装函数对应的机器级代码中,将使用传送指令把系统调用所需要的参数传送到相应的寄存器,按照惯例,系统调用号存放在 EAX 中,传递参数的寄存器顺序依次为:EAX(调用号)、EBX、ECX、EDX、ESI、EDI 和 EBP,除调用号外,最多 6 个参数。若参数个数超出寄存器个数,则将参数块所在存储区的首址放在寄存器中传递。
封装函数对应的机器级代码有一个统一的结构:总是若干条传送指令后跟上一条陷阱指令。传送指令用来传递系统调用所用的参数,陷阱指令(如
int 0x80
)用来陷入内核进行处理。例如,若用户程序希望将字符串 “hello, world!\n” 中的 14 个字符显示在标准输出设备文件 stdout 上,则可以调用系统调用 write(1, “hello, world!\n”, 14),它的封装函数用以下机器级代码(用汇编指令表示)实现:1
2
3
4
5movl $4, %eax #调用号为4,送EAX
movl $1, %ebx #标准输出设备stdout的文件描述符为1.送EBX
movl $string, %ecx #字符串"he11o, world!n"的首地址等于string的值,送ECX
movl $14, %edx #字符串的长度为14,送EDX
int $0x80 #系统调用系统调用处理程序 system_call
在 Linux 中,有一个系统调用的统一入口,即系统调用处理程序
system_call
的首地址,所以 CPU 执行指令int 0x80
后,便转到system_call
的第一条指令开始执行。system_call
的结构:与 Linux 其他异常处理程序和中断服务程序一样,首先使用宏
SAVE_ALL
在内核栈中保存各寄存器的内容。根据调用号跳转到当前系统调用对应的系统调用服务例程去执行。
恢复保存在内核栈中的各个寄存器的内容,切换到用户态,返回到
int 0x80
指令后面一条指令继续执行。返回值在 EAX 中,为整数值,若是正数或 0 表示成功,负数表示出错码。
快速系统调用指令
Intel 从 Pentium Ⅱ 处理器开始,引入了指令
sysenter
和sysexit
,分别用于进入系统调用和退出系统调用。在 Intel 文档中,sysenter
被称为快速系统调用指令,它提供了从用户态到内核态的快速切换方式。下面给出在 IA-32 + Linux 系统中进入和退出系统调用的大致过程。
通过软中断指令进入和退出系统调用
CPU 在用户空间执行软中断指令
int 0x80
的过程与异常和中断响应过程一样:- CPU 的运行状态从用户态切换为内核态;
- 并从任务状态段 TSS 中将内核态对应的栈段寄存器内容和栈指针装入 SS 和 ESP;
- 再依次将原先执行完软中断指令 int 0x80 时的栈段寄存器 SS、栈指针 ESP、标志寄存器 EFLAGS、代码段寄存器 CS、指令计数器 EIP 的内容(即返回地址或断点)保存到内核栈中,即当前 SS:ESP 所指之处;
- 然后从中断描述符表 IDT 中的第 128(80H)个表项中取出相应的门描述符 IDTi(i = 128),将其中的段选择符装入 CS,偏移地址装入 EIP,这里,CS:EIP 即是系统调用处理程序
system_call
的第一条指令的逻辑地址。
需要从系统调用返回时,则通过执行
iret
指令实现上述逆过程。通过快速系统调用指令进入和退出系统调用
因为系统调用属于陷阱类异常,所以通过软中断指令
int n
进入和退出系统调用的处理过程(即异常和中断的响应过程),需要进行一连串的一致性和安全性检查,因而速度较慢。快速系统调用指令
sysenter
主要用于从用户态到内核态的快速切换。为了实现快速系统调用,Intel 在 Pentium Ⅱ 以后的处理器增加了以下三个特殊的 MSR 寄存器:- SYSTEM_CS_MSR:存放内核代码段的段选择符。
- SYSTEM_EIP_MSR:存放内核中系统调用处理程序的起始地址。
- SYSTEM_ESP_MSR:存放内核栈的栈指针。
执行
sysenter
指令时,CPU 将 SYSTEM_CS_MSR、SYSTEM_EIP_MSR 和 SYSTEM_ESP_MSR 的内容分别复制到 CS、EIP 和 ESP,同时将 SYSTEM_CS_MSR 的内容加 8 的值设定到 SS。因此,CPU 执行完sysenter
指令,即可切换到内核态,并开始执行系统调用处理程序的第一条指令。MSR 寄存器的内容只能通过特权指令
rdmsr
和wrmsr
进行读写。汤子瀛《操作系统》第 4 版中对系统调用处理步骤的描述
首先,将处理机状态由用户态转为系统态;之后,由硬件和内核程序进行系统调用的一般性处理,即首先保护被中断进程的 CPU 环境,将处理机状态字 PSW、程序计数器 PC、通用寄存器、用户栈指针、系统调用号等压入内核栈;然后,将用户定义的参数传送到指定的地址并保存起来。
其次,分析系统调用类型,利用系统调用号查找系统调用入口表,找到相应的系统调用处理子程序的入口地址而转去执行它。
最后,在系统调用处理子程序执行完后,应恢复被中断的或设置新进程的 CPU 现场,然后返回被中断进程或新进程,继续往下执行。
总结
任务切换
x86 原生支持的任务切换方式
任务状态段 TSS
TSS 是 x86 的特性,原本是用来配合处理器在硬件上原生支持多任务的数据结构。Intel 建议给每个任务 “关联” 一个 TSS,用其保存任务的最新状态。
TSS 由程序员 “提供”,由 CPU 来 “维护”。“提供” 就是指 TSS 是程序员为任务单独定义的一个结构体变量,“维护” 是指 CPU 自动用此结构体变量保存任务的状态(处理器上下文)和自动从此结构体变量中载入任务的状态。
TSS 记录了三种特权级栈的段选择子和偏移量/栈顶指针。由于正常情况下,特权级由低向高转移在先,由高到低返回在后,当处理器由低到高特权级转移时,它会自动把当时低特权级的栈地址(SS 和 ESP)压入转移后的高特权级所在的栈中,所以 TSS 就没有记录最低特权级 3 的栈地址。对于 Linux,只使用了特权级 0 和特权级 3 作为系统态和用户态,因此在模式转换过程中也就只使用了 TSS 中的 ss0 和 esp0(内核栈指针)。
TSS 描述符和任务寄存器 TR
TSS 和其他段一样,本质上是一片存储数据的内存区域,因此也需要用描述符结构来 “描述” 它,即 TSS 描述符。TSS 描述符也要在 GDT 中注册,这样才能 “找到它”。TSS 描述符结构如下:
寄存器 TR (Task Register) 保存当前运行任务的 TSS 描述符选择子。
任务门描述符
任务门属于系统段,任务门描述符位于 IDT 中,包含 TSS 选择子。
在该方式下,任务切换中保存和恢复上下文的动作由 CPU 通过 TSS 完成。但其效率不高,所以现在大多数操作系统(Linux 2.2.0 以后)已经不用 TSS 做硬切换了,而是通过软件指令进行软切换。
CPU 原生支持的任务切换方式
共有两种方式:
通过 “中断 + 任务门” 进行任务切换
当中断发生时,处理器通过中断向量号在 IDT 中找到描述符后,通过分析描述符中字段 S 和字段 TYPE 的组合判断描述符的类型:若发现中断对应的描述符是中断门描述符,则转而去执行中断门描述符中指定的中断服务程序。在中断服务程序的最后,通过
iretd
指令回到被中断任务的中断前的代码处;若发现中断对应的描述符是任务门描述符,则从该任务门描述符中取出任务的 TSS 选择子,开始进行任务切换。步骤如下,比较繁琐:
可以发现,
iretd
指令(32位下,iretd
等价于iretd
)除了可以从中断返回当前任务的中断代码处之外,若当前任务是被嵌套调用时,它会调用自己 TSS 中 “上一个任务的 TSS 指针” 的任务,也就是返回到上一个任务。call
/jmp
指令 + TSS 选择子或任务门选择子任务门描述符除了可以在 IDT 中注册,还可以在 GDT 和 LDT 中注册。
只要包括 TSS 选择子的对象都可以作为任务切换的操作数。
call
是有去有回的指令,jmp
是一去不回的指令,它们在调用新任务时的区别也在于此。任务切换步骤依然繁琐:
TSS 使用现状
首先,基于 TSS 进行任务切换过程繁琐,在每一次任务切换中,CPU 除了需要做特权级检查外,还要在 TSS 的加载、保存、设置 B 位,以及设置标志寄存器 eflags 和NT 位诸多方面消耗很多精力,导致此种切换方式效能很低;其次,x86 指令集属于 CISC,虽然提供了
call
和jmp
指令实现任务切换,但这两个指令所消耗的时钟周期也是可观的(上百个);最后,一个任务需要单独关联一个 TSS,TSS 需要再 GDT 中注册,GDT 最多支持 8192 个描述符(选择子索引位数决定),为了支持更多的任务,随着任务的删减,要及时修改 GDT,在其中增减 TSS 描述符,修改过后还要重新加载 GDT。这种频繁修改描述符表的操作还是很消耗 CPU 资源的。以上是效率方面的原因,除了效率以外,还有便携性和灵活性等原因,不仅 LIinux 未采用这种原生的任务切换方法,而且几乎所有 x86 操作系统都未采用。
CPU 要求使用 TSS 这是是硬指标,Linux 也得遵守,不过为了 “应付” 这一指标, Linux 只为每个 CPU 创建一个 TSS,在各个 CPU 上的所有任务共用共享同一个 TSS,各 CPU 的 TR 寄存器保存各 CPU 上的 TSS,在用
ltr
指令加载 TSS 选择子到 TR 寄存器后,该 TR 寄存器永远指向同一个 TSS,之后再也不会重新加载 TSS。在进程切换时,只需要把 TSS 中的ss0
及esp0
更新为新任务的内核栈的段地址及栈指针。Linux 在 TSS 中只初始化了
ss0
、esp0
和 I/O 位图字段,除此之外 TSS 便没用了,不再做保存和恢复任务状态之用。目前,我们使用 TSS 唯一的理由是为 0 特权级的任务提供栈(即内核栈),使用内核栈来保存和恢复任务状态。另外,Linux 中任务切换也不使用
call
和jmp
指令,这也避免了任务切换的低效。IA-32 + Linux 的任务切换
由时间片到引发的任务调度与切换
一、被切换任务的执行流
发生时钟中断,进入中断响应阶段
用户态切换为内核态 → 用户栈切换为内核栈 → 将用户栈段选择子、用户栈指针、断点和程序状态字压栈 → EFLAGS 寄存器中的 IF 清 0(关中断)→ 将 IDT 中第 32(20H)个表项的段选择子和偏移地址分别装入 CS 和 EIP,开始时钟中断处理程序的执行。
执行时钟中断处理程序
首先保存现场以及所请求 IRQ 的值(IRQ0),再转去执行 IRQ0 对应的时钟中断服务例程。
执行时钟中断服务例程
包括但不限于:
① 更新此任务占用的 CPU 时间(+1);
② 更新系统运行时间(+1);
③ 若当前任务的时间片用完,则调用
schedule
函数,否则将当前任务的时间片 -1。返回时钟中断处理程序
恢复保存在内核栈中的各个寄存器的内容,切换到用户态并返回到当前任务的逻辑控制流的断点处继续执行。
二、
schedule
函数执行流- 将当前任务换下处理器,并在就绪队列中找出下个可运行的程序;
- 调用
context_switch
函数进行任务切换。
schedule
函数主要内容就是读写就绪队列,因此它不需要参数。另外schedule
函数依旧属于时钟中断处理程序(同一过程,同一层执行流),没有发生过程调用,因此也不需要保存上下文。三、
context_switch
函数执行流切换任务虚拟地址空间(用户空间部分),即重新加载页表 PGD(修改 cr3 寄存器)。
调用
switch_to
宏切换进程的内核栈。需要三个参数:旧任务 PCB 指针
prev
、新任务 PCB 指针next
、上一个任务 PCB 指针last
。形式为
switch_to(prev, next, last)
,其中prev
和next
为输入参数,last
为输出参数。后面会解释为什么需要三个参数。调用
finish_task_switch
函数完成一些清理工作。
四、
switch_to
宏执行流保存被调用者保存寄存器 EBX、ESI、EDI 到当前内核栈(旧任务内核栈)。
switch_to
宏是不同于时钟中断处理程序的另一过程(不同层的执行流),因此根据 ABI 调用约定需要保存上述寄存器。保存 EFLAGS 到当前内核栈(旧任务内核栈)。
保存 EBP 到当前内核栈(旧任务内核栈)。
将使用栈从当前任务的内核栈切换到新任务的内核栈。
先将当前任务内核栈栈指针(即当前 ESP 寄存器内容)保存到旧任务 PCB(
prev->thread.esp
)中,再将新任务内核栈指针从新任务 PCB 中(next->thread.esp
)取出装入 ESP 寄存器。把标号为 1 的指令地址保存到旧任务 PCB(
prev->thread.eip
)中,当此任务下次被switch_to
回来时,会从这条指令开始执行(即手工设置返回地址)。将新任务 PCB 中保存的 IP 值(
next->thread.eip
)压入当前内核栈(新任务内核栈)。这里如果新任务之前也被
switch_to
出去过,那么此时栈顶就是标号 1 的指令地址;如果新任务之前尚未执行过(只是被创建),那么此时栈顶的值是函数ret_ftom_fork
的地址。jmp
到__switch_to
函数__switch_to
函数主要工作有:① 从新任务 PCB(
next->thread
)中取出内核栈指针esp0
和 I/O 操作权限位图io_bitmap
去更新新任务 TSS 对应字段;② 保存恢复部分寄存器上下文。恢复内容的寄存器有:浮点寄存器、调试寄存器(挂起前有使用过的话)以及两个段寄存器 FS 和 GS,内容均来自新任务的 PCB(
next->thread
)。其中 FS 和 GS 需要先保存到旧任务 PCB(prev->thread.fs
和prev->thread.gs
)中。③ 根据此时栈顶的返回地址继续执行。如果新任务是新创建的还没执行过的任务,则不会再回到
__switch_to
函数,而是转去执行ret_ftom_fork
函数,该函数是新任务在其初始地址空间中第一个执行的函数;如果新任务是运行过的,但是执行了__switch_to
函数被切换了的,则回到标号 1 的指令地址去执行,即jmp __switch_to;
的下一个指令,刚好续上了新任务当初被切换出去的执行流。两种情况对应两种返回地址,这就是为什么不用call
而是用jmp
调用__switch_to
的原因。__switch_to
函数的调用不同于一般的函数调用,它从寄存器 EAX 和 EDX 取参数prev
和next
,而不像大多数函数一样从栈中取参数。从新任务内核栈中恢复 EBP、EFLAGS、EBX、ESI、EDI、EFLAGS。
这些寄存器映像也是新任务在之前某次执行
switch_to
时,在被换下处理器前保存的。
🍘为什么
switch_to
需要三个参数?对于同一个
switch_to
宏的一次执行过程而言,可视为由两个任务共同完成的一个完整的执行流,即switch_to
宏是由旧任务调用,在新任务结束,这样看只涉及两个任务。但对于某个任务 A 对switch_to
宏的一次调用过程而言,实际上是不同时间的两个执行流:前半部分是 A 作为旧任务切换出去的执行流,后半部分是 A 作为新任务切换回来的执行流。为方便讨论,现设定调度顺序为 A → B → … → C → A,前半部分对应 A → B,后半部分对应 C → A。所以涉及到三个任务 A、B、C。两部分实际上是switch_to
宏的两次执行过程执行流,对应两次任务切换。__switch_to
函数的返回是前半部分和后半部分的分界。__switch_to
函数的返回值是prev
指针,保存在 EAX 寄存器中。当 A 再次被调度执行(C → A)时,它想回到自己上次被切换出去(A → B)的断点处,继续执行__switch_to
函数的后半部分(即从标号 1 的指令开始),恢复完寄存器上下文后,再调用finish_task_switch
函数完成一些清理工作。清理工作需要用到这次任务切换(C → A)中的旧任务即 C 的 PCB 指针,切换前局部变量prev
是保存的 C 的 PCB 指针没错,但任务切换已经把内核栈和寄存器上下文都切换了,而局部变量是根据 EBP 寄存器和内核栈去索引的,所以切换后的局部变量prev
实际是 A 自己的 PCB 指针。所以 Linux 在切换前(switch_to
宏前半部分的最后,即__switch_to
函数的返回),将prev
作为__switch_to
函数的返回值放入 EAX,这样 C 的 PCB 指针就会在随着寄存器上下文的保存与恢复而传递过来。由于__switch_to
函数是由switch_to
调用的,而 EAX 是switch_to
值是用来保存输出参数last
,所以switch_to(prev, next, last)
等价于prev = last = switch_to(prev, next)
。🌱上述流程中上下文的保护工作分为两部分:
- 第一部分是 CPU 和中断处理程序保存断点、程序状态字和用户态现场信息,用于恢复中断前的状态;
- 第二部分是
switch_to
函数在被调用时保存的 EFLAGS、EBP、EBX、ESI、EDI 信息,用于任务切换后恢复执行中断处理程序中的后续代码。
这两次上下文的保护也正对应整个执行流的两次改变。为了将来能够恢复到本层执行流继续执行,必须在改变发生前将本层执行流的上下文保护好,因此执行流改变了几层就要做几次上下文保护。
在抢占式内核中,利用中断来实现上下文切换是一个非常理想的机制。中断发生时,中断会强制 CPU 把控制权交给操作系统,也就相当于一次上下文切换。这样不仅可以减少程序出错的后果,而且提高切换的效率。
其他情况下的任务调度与切换
从
schedule
函数开始的执行顺序是一样的,即schedule()
→context_switch()
→switch_to
宏 →__switch_to()
。任务调度和切换都是通过执行
schedule
函数实现的。所以所谓的 “其他情况下的任务调度与切换” 关键在于何时/何种条件下会被何种程序/函数调用schedule
函数(由时间片到引发的任务调度与切换是在时钟中断触发的时钟中断服务例程中更新当前任务的时间片时发现时间片用尽后调用了schedule
函数)。首先可以确认一点,schedule
函数的调用和执行,或者说任务切换,只会发生在内核态,因此在即将进行任务切换前(即将调用schedule
函数时),用户态进程使用的所有寄存器内容都已保存在内核栈/中断栈上。处理器总处于以下三种状态之一:
- 内核态,运行于进程上下文,使用内核栈,内核代表进程运行于内核空间;
- 内核态,运行于中断上下文,使用内核栈/中断栈,内核代表硬件运行于内核空间;
- 用户态,运行于用户空间,使用用户栈。
调度器 schedule 并不仅由时间中断处理程序来调用,它还有被其他函数调用的情况,比如函数 thread_block。
其他必定会调用
schedule
函数的情况:当前任务状态由运行态变为非可执行状态
主动调用相关库函数将自己终止、睡眠、阻塞时,这些库函数会去调用相关系统调用进行处理,而这些相关的系统调用服务例程内部一定会去调用
schedule
函数。进程从中断/异常/系统调用处理程序返回到用户态前,检查当前任务是否置上了请求调度标志(
task_struct.thread_info.flags
中的TIF_NEED_RESCHED
),被置上了就转去调用schedule
函数而非回到当前进程的用户态继续执行。因为模式切换有开销,既然还在内核态,就该把在内核态该处理的事处理完。……
锁存器、触发器和寄存器
锁存器
对脉冲电平敏感,在时钟脉冲的电平作用下改变状态。
锁存器是电平触发的存储单元,数据存储的动作取决于输入时钟(或者使能)信号的电平值,当锁存器处于使能状态时,输出才会随着数据输入发生变化。
触发器 FF (Flip-Flop)
对脉冲边沿敏感,其状态只在时钟脉冲的上升沿或下降沿的瞬间改变。
也叫双稳态门,又称双稳态触发器。是一种可以在两种状态下运行的数字逻辑电路。触发器一直保持它们的状态,直到它们收到输入脉冲,又称为触发。当收到输入脉冲时,触发器输出就会根据规则改变状态,然后保持这种状态直到收到另一个触发。
锁存器 vs 触发器
锁存器同其所有的输入信号相关,是电平触发,当输入信号变化时锁存器就变化,没有时钟端,属于异步电路设计,时序分析困难且浪费大量芯片资源。
触发器受时钟控制的边沿触发,只有在时钟触发时才采样当前的输入产生输出,当然因为锁存器和触发器二者都是时序逻辑,所以输出不但同当前的输入相关,还同上一时间的输出相关。
锁存器和触发器都是具有存储功能的逻辑电路,是构成时序电路的基本逻辑单元,每个锁存器或触发器都能存储 1 位二进制信息。
钟控 D 触发器其实就是 D锁存器,边沿 D 触发器才是真正的 D 触发器。
寄存器
来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。
其实寄存器就是一种常用的时序逻辑电路,但这种时序逻辑电路只包含存储电路。
由锁存器或触发器构成,由 N 个锁存器或触发器可以构成 N 位寄存器。
寄存器的应用:
可以完成数据的并串、串并转换。
可以用做显示数据锁存器。
许多设备需要显示计数器的记数值,以8421BCD码记数,以七段显示器显示,如果记数速度较高,人眼则无法辨认迅速变化的显示字符。在计数器和译码器之间加入一个锁存器,控制数据的显示时间是常用的方法。
用作缓冲器。
组成计数器。
移位寄存器可以组成移位型计数器,如环形或扭环形计数器。
Linux 内存管理
基础知识
页表寄存器 CR3 中保存的是进程顶级页表的物理地址,
task_struct.pgdir
是顶级页表的虚拟地址,在调度切换到某进程时,会将该进程的task_struct.pgdir
转换成物理地址再加载到 CR3 中。页表相关数据结构
- PGD (Page Global Directory):页全局目录表,即顶级页表,每个进程有且仅有一个,PCB 保存其虚拟地址,页表寄存器保存其物理地址。
- PMD (Page Middle Directory):页中间目录表,即二级页表
- PDE (Page Directory Entry):页目录表项。
- PTE (Page Table Entry):页表项,代表了一页的映射关系。
逻辑地址、有效地址、线性地址、虚拟地址、物理地址
有效地址(段内偏移)(实模式/保护模式)
无论在实模式下或是保护模式下,段内偏移地址又称为有效地址,是程序员可见的地址。
逻辑地址(段选择子 : 段内偏移)(保护模式)
段选择子位于段寄存器,段内偏移/有效地址位于 EIP。
线性地址(段基址 : 段内偏移)(保护模式)
保护模式下,段基址需要根据段选择子和段描述符表得到。
若没有开启分页功能,线性地址就被当作物理地址使用,可直接访问内存。
实模式下,段基址 << 4 : 段内偏移 即是物理地址。
虚拟地址(页目录索引 : 页表索引 : 页内偏移)(保护模式)
若开启了分页功能,线性地址又可叫做虚拟地址,即虚拟地址和线性地址在分页机制下是一回事。
页目录索引 : 页表索引 : 页内偏移 对应二级页表机制,一级或其他多级页表机制有不同的划分方式。
物理地址(页框号 : 页内偏移)
即真实的内存地址。
内核地址空间
在 32 位的系统上,内核占有从第 3GB ~ 第 4GB 的线性地址空间,共 1GB 大小。
如果物理内存小于 1G 的空间,通常内核把物理内存与其地址空间做了线性映射,也就是一一映射,这样可以提高访问速度。但是当物理内存超过 1G 时,线性访问机制就不够用了,因为只能有 1G 的内存可以被映射,剩余的物理内存无法被内核管理(用户进程最多只可以访问 3G 物理内存,而内核进程可以访问所有物理内存),所以为了解决这一问题,Linux 把内核地址分为线性区和非线性区两部分,线性区规定最大为 896M,剩下的 128M 为非线性区。从而,线性区映射的物理内存称为低端内存,剩下的物理内存称为高端内存。与线性区不同,非线性区不会提前进行内存映射,而是在使用时动态映射。
直接映射区
这块 896M 大小的虚拟内存会直接映射到 0~896M 这块 896M 大小的物理内存上,即这块区域中的虚拟内存地址直接减去 0xC000 0000 (3G) 就得到了物理内存地址。
在这段 896M 大小的物理内存中,前 1M 已经在系统启动的时候被系统占用,1M 之后的物理内存存放的是内核代码段、数据段、BSS 段(这些信息起初存放在 ELF 格式的二进制文件中,在系统启动的时候被加载进内存)。
当我们使用 fork 系统调用创建进程的时候,内核会创建一系列进程相关的描述符,比如进程的核心数据结构
task_struct
、进程的内存空间描述符mm_struct
,以及虚拟内存区域描述符vm_area_struct
等。这些进程相关的数据结构会存放在物理内存前 896M 的这段区域中,当然也会被直接映射至内核态虚拟内存空间中的 3G~3G+896m 这段直接映射区域中。内核栈也是分配在直接映射区。当进程被创建完毕之后,在内核运行的过程中,会涉及内核栈的分配,内核会为每个进程分配一个固定大小的内核栈(一般是两个页大小,依赖具体的体系结构),每个进程的整个调用链必须放在自己的内核栈中。
直接映射区的前 16M 专门让内核用来为 DMA 分配内存,其映射的 16M 大小的物理内存区域称为 ZONE_DMA;直接映射区中剩下的部分(16M~895M)映射的物理内存称为 ZONE_NORMAL,即这块区域包含的是正常的页框,使用没有任何限制;物理内存 896M 以上的区域被内核划分为 ZONE_HIGHMEM 区域,称为高端内存。注意这里的 ZONE_DMA、ZONE_NORMAL 和 ZONE_HIGHMEM 是内核针对物理内存区域的划分。
高端内存的映射
IA-32 的内核虚拟内存空间除去直接映射区只剩下 128M,而物理内存中大小以 G 为单位的 ZONE_HIGHMEM 区域就只能采用动态映射的方式映射到 128M 大小的内核虚拟内存空间中,也就是说只能动态的一部分一部分的分批映射,先映射正在使用的这部分,使用完毕解除映射,接着映射其他部分。
vmalloc 动态映射区
和用户态进程使用 malloc 申请内存一样,在这块动态映射区内核是使用
vmalloc
进行内存分配。由于动态映射的原因,vmalloc 分配的内存在虚拟内存上是连续的,但物理内存是不连续的。通过页表来建立物理内存与虚拟内存之间的映射关系,从而可以将不连续的物理内存映射到连续的虚拟内存上。永久映射区
在内核的这段虚拟地址空间中允许建立与物理高端内存的长期映射关系。比如内核通过
alloc_pages
函数在物理内存的高端内存中申请获取到的物理内存页,这些物理内存页可以通过调用 kmap 映射到永久映射区中。固定映射区
这块区域中的每个地址项都服务于特定的用途。 比如:在内核的启动过程中,有些模块需要使用虚拟内存并映射到指定的物理地址上,而且这些模块也没有办法等待完整的内存管理模块初始化之后再进行地址映射。因此,内核固定分配了一些虚拟地址,这些地址有固定的用途,使用该地址的模块在初始化的时候,将这些固定分配的虚拟地址映射到指定的物理地址上去。
临时映射区
在内核有的场景里,需要内核临时映射物理区域,以便可以访问这些物理区域,待访问完毕之后再将解除临时映射。临时映射的存在不仅解决了需要临时访问某些硬件资源的场景,而且可以节省内存开销。
中断系统中的触发器
中断请求触发器 INTR
用来登记中断源发出的随机性中断请求信号,以便为 CPU 查询中断及中断排队判优线路提供稳定的中断请求信号
允许中断触发器 EINT
CPU 中的中断总开关。当 EINT = 1时,表示允许中断(开中断),当 EINT = 0 时,表示禁止中断(关中断)。其状态可由开中断指令、关中断指令、中断隐指令等设置
中断标记触发器 INT
控制器时序系统中周期状态分配电路的一部分,表示中断周期标记。当 INT = 1 时,进入中断周期,执行中断隐指令的操作
中断屏蔽触发器 IM
是 CPU 是否受理中断或批准中断的标志。IM 标志为“0”时,CPU 可以受理外界的中断请求,反之,IM 标志为“1”时,CPU 不受理外界的中断
指令系统的一种分类
根据指令使用数据的方式,指令系统可分为堆栈型、累加器型和寄存器型。寄存器型又可以进一步分为寄存器-寄存器型和寄存器-存储器型
堆栈型
堆栈型指令又称零地址指令,其操作数都在栈顶,在运算指令中不需要指定操作数,默认对栈顶数据进行运算并将结果压回栈顶
累加器型
累加器型指令又称单地址指令,包含一个隐含操作数——累加器,另一个操作数在指令中指定,结果写回累加器中
寄存器-存储器型
在这种类型的指令系统中,每个操作数都由指令显式指定,操作数为寄存器和内存单元
寄存器-寄存器型
在这种类型的指令系统中,每个操作数也由指令显式指定,但除了访存指令外的其他指令的操作数都只能是寄存器
下表给出了四种类型的指令系统中执行 C = A + B 的指令序列,其中 A、B、C 为不同的内存地址,R1、R2 等为通用寄存器
北桥与南桥
历史
曾经,北桥芯片和南桥芯片组成一个 “芯片组”,是计算机中各个组成部分相互连接和通信的枢纽。主板上所有的存储器控制功能和 I/O 控制功能几乎都集成在芯片组内,它既实现了总线的功能,又提供了各种 I/O 接口及相关的控制功能。
北桥
负责与 CPU 通信,并且连接高速设备(内存/显卡)以及与南桥通信。
北桥是一个主存控制器集线器(Memory Controller Hub, MCH)芯片,本质上是一个 DMA 控制器,因此可通过 MCH 芯片,直接访问主存和显卡中的显存。
南桥
负责与低速设备(硬盘/USB 设备)通信,时钟/BIOS/系统管理/旧式设备控制,以及与北桥通信。
南桥是一个 I/O 控制器集线器 ICH (I/O Controller Hub)芯片,其中可以继承 USB 控制器、磁盘控制器、以太网控制器等各种外设控制器,也可以通过南桥芯片引出若干主板扩展槽,用以接插一些 I/O 控制卡。
现状
计算机中的频率
CPU 主频
即 CPU 时钟频率。
CPU 外频
通常指系统总线的工作频率。
CPU 倍频
USB
通用串行总线 USB (Universal Serial Bus),是连接计算机与设备的一种序列总线标准,也是一种输入输出(I/O)连接端口的技术规范。
产生背景
多媒体电脑刚问世时,外接式设备的传输接口各不相同,如打印机只能接并行端口、调制解调器只能接 RS-232、鼠标键盘只能接 PS/2 等。繁杂的接口系统,加上必须安装驱动程序并重启才能使用的限制,都会造成用户的困扰。因此,创造出一个统一且支持易插拔的外接式传输接口,便成为无可避免的趋势,USB应运而生。
相关概念
USB-C
即 USB Type-C。
USB-IF
Thunderbolt
Thunderbolt 又称“雷电技术”,苹果中国译为“雷雳”,是由英特尔和苹果联合推出的一种高速数据传输和视频协议,目的在于当作电脑与其他设备之间的通用总线。
详见 Thunderbolt 一节。
USB 版本
USB 1.0
1996 年 1 月发布,数据传输速率为 1.5Mbit/s (Low-Speed) 和 12Mbit/s (Full-Speed)。
无预测及通过检测功能,Full-Speed 也难以达成,仅极少数出现在市场上。
USB 1.1
1998 年 9 月发布,修正 1.0 版已发现的问题,主要是关于 USB Hubs 及 Full-Speed,最早被采用的修订版。
USB 2.0
2000 年 4 月发布,数据传输速率为 480Mbit/s (Hi-Speed),但受限于 BOT 传输协议和 NRZI 编码方式,实际最高传输速度只有 35MByte/s 左右。
USB 3.0 / USB 3.1 Gen 1 / USB 3.2 Gen 1×1 / USB 5Gbps
2008 年 11 月发布,数据传输速率为 5Gbps,引入了全双工传输。
USB 3.1 / USB 3.1 Gen 2 / USB 3.2 Gen 2×1 / USB 10Gbps
2013年 7 月 31 日发布,数据传输速率为 10Gbps。
USB 3.2 / USB 3.2 Gen 2x2 / USB 20Gbps
USB 3.1 Gen 2 的双通道模式,数据传输速率可达 20Gbps(每条通道 10Gbps)。
只在 USB Type-C 接口上实现双通道,USB Type-A 和 Type-B 不支持双通道。
从 USB 3.2 开始,Type-C 是唯一推荐的接口方案。
USB 3.2 Gen 1x2
USB 3.0 的双通道模式,数据传输速率可达 10Gbps(每条通道 5Gbps)。
必须使用 USB Type-C 接口才能达到 10Gbps 的速度。
USB4 / USB 40Gbps
2019 年 9 月 3 日发布,支持 40Gbps 的传输速度,但达到 40Gbps 的速度要求 USB 资料线、产品均支持USB4。
集成 Thunderbolt 3 协议。
USB-IF 是 intel 和微软等公司发起的组织,雷电/雷雳协议 Thunderbolt 是 intel 和苹果一起制定的协议。
Thunderbolt 1 和 Thunderbolt 2 使用 Mini DP 接口,Thunderbolt 3 开始使用 Type-C 接口。
USB4 V2.0
2022 年 9 月发布,传输速率可达 80 Gbps。
Thunderbolt
Thunderbolt 又称“雷电技术”,苹果中国译为“雷雳”(可能是为了避免与 Lightning 闪电接口弄混),是由英特尔和苹果联合推出的一种高速数据传输和视频协议,目的在于当作电脑与其他设备之间的通用总线。
Thunderbolt 版本
第一版 Thunderbolt
2011 年发表,数据传输速率达 10Gbps,使用 mini-DP 接口。
采用两种通信协议 ,包括用在资料传输的 PCI Express ,以及用在显示的 DisplayPort。
只有当时的苹果笔记本和极小部分主板使用。
第二版 Thunderbolt 2
2013 年发表,数据传输速率达 20Gbps,使用 mini-DP 接口。
Thunderbolt 1 和 2 在当时的主要用途是外接高分辨率显示器(从采用 mini DP 接口就能看出)
CPU 命名
Intel
以 intel Core i7-13700K 为例,intel 为品牌名,Core 为产品线名,i7 为型号名,13 为代数,700 为子型号,K 为尾缀。
产品线名
赛扬 Celeron:低端
奔腾 Pentium:入门
酷睿 Core:主流
志强 Xeon:服务器
型号名
i3:低端产品
i5:中端产品
i7:高端产品
i9:旗舰产品
前缀 i 指 Intel。
子型号
子型号一般为数字序列的末三位。
i3、i5、i7、i9 都会有细分的子型号,一般情况下,同代子型号越高性能越好。
子型号的第二位数有时也会再划分,一般也是数字越大,性能越好,比如 i5-13490F 就比 i5-13400F 在频率上要略高一点,缓存也要略大一些。
尾缀
无尾缀:自带核显,锁倍频(超频时不能超倍频)。
以无尾缀为基准——
K:最大睿频更高,不锁倍频(i5-12600K 为例外,它还多了 4 个小核)。
F:无核显。
K 和 F 可以组合出现。如果已经搭配了高性能独显以及用不上核显的视频加速和辅助直播推流功能,可以直接买 F 尾缀的 CPU 省点米。
T:低 TDP。
S:早期代表低 TDP,现在表示特别版,有更高频率。
X:高频率,不锁倍频,无核显,高性能。
XE:代表至尊性能,X 的强化语义,无核显。
尾缀-移动端
按 TDP 从低到高划分为:Y、U、P、H、HX 五个尾缀。
Y:超低功耗。
U:更低功耗。
P:低功耗。
H:标准功耗,游戏本最常见 CPU 尾缀,一般被叫做标压处理器。
HX:桌面端移植的核心。
其他尾缀——
G:Graphic,集成 VEGA 显卡。G + 数字的组合中,数字越大,核显性能越强。
Q:Quad,4 颗物理核心。
M:PGA 封装,可更换的移动版 CPU。
AMD
只介绍 ZEN 架构的消费级 Ryzen 系列的命名。
以 AMD Ryzen7 7700 X 为例,AMD 为品牌名,Ryzen7 为产品线及定位,7 为代数,700 为子型号,X 为尾缀。
产品线及定位(型号)
Ryzen3(入门)、Ryzen5(中端)、Ryzen7(高端)、Ryzen9(旗舰),通常也会被简写为 R3、R5、R7、R9。
代数
锐龙台式机 CPU 目前只有 1000、2000、3000、5000、7000 这五代。
更新的代数对应更新的架构以及更先进的制程,也就意味着更强的性能和更高的能耗比。
子型号
数值越高定位越高,第二位数同样也有更细微的子型号划分。
通常型号和子型号的对应关系为:
尾缀
AMD 台式机 CPU 全系都是不锁倍频的设计。
X:频率更高。
X3D:内置 3D V-Cache,三级缓存更大。
XT:官方特挑提频。
G:内置高性能核显。
尾缀-移动端
按 TDP 从低到高划分为:E、C、U、HS、H、HX 六个尾缀。
E:超低功耗。
C:更低功耗(针对 ChromeBook)。
U:更低功耗。
HS:标准功耗。
H:标准功耗。
HX:桌面端移植的核心。
在 7 代之前,锐龙实际命名相当混乱
DDR
DDR SDRAM 芯片技术
DDR (Double Data Rate) 核心技术点就在于双沿传输和预取 Prefetch。
双沿传输即利用存储器总线上时钟信号的上升沿和下降沿进行两次传送,以实现一个时钟内传送两次数据的功能。
预取,顾名思义就是预先存取数据,也就是说在 I/O 控制器发出请求之前,存储单元已经事先准备好了 2/4/8 位数据。简单来说这就是把并行传输的数据转换为串行数据流。
DDR1 是两位预取(有公司更贴切地称之为 2n Prefetch,n 代表芯片位宽,即两倍位宽预取),在一个时钟周期中,同时将相邻列地址的存储单元的数据一起取出来放入芯片内部 I/O 缓冲。如此,再结合双沿传输,就可在存储阵列频率不变的情况下,数据传输率达到了 SDRAM 的两倍。
通过上表就能非常直观的看出,近年来内存的频率虽然在成倍增长,可实际上真正存储单元的频率一直在 133MHz - 200MHz 之间徘徊,这是因为电容的刷新频率受制于制造工艺而很难取得突破。而每一代 DDR 的推出,都能够以较低的存储单元频率,实现更大的带宽,并且为将来频率和带宽的提升留下了一定的空间。
虽然存储单元的频率一直都没变,但内存颗粒的 I/O 频率却一直在增长(配合预取位数的增长),再加上 DDR 均采用双沿传输,因此内存的数据传输率可以达到核心频率的 8 倍之多。
DDR2 SDRAM 芯片技术
DDR2 在 DDR1 的基础上,数据预取位数从 2bit 扩充至 4bit,此时双沿传输已经无法充分利用 4bit 的预取,因此 I/O 控制器频率(存储器总线上的时钟频率)必须加倍,如此等效频率达到核心频率的 4 倍。
DDR3 SDRAM 芯片技术
DDR3 芯片内部 I/O 缓冲可以进行 8 位预取,同理,I/O 频率需要在 DDR2 的基础上再次翻倍,如此等效频率达到核心频率的 8 倍。
综上可以看出,DDR1/2/3 的发展是围绕着数据预取而进行的,同时也给 I/O 控制器造成了不小的压力,虽然存储单元的工作频率保持不变,但 I/O 频率以级数增长,我们可以看到 DDR3 的 I/O 频率已逼近 1GHz 大关,此时 I/O 频率成为了新的瓶颈。
PCIe
分辨率相关
像素
影像显示的基本单位。
译自英文 “pixel”,pix 是英语单词 picture 的常用简写,加上英语单词 “元素” element,就得到 pixel,故“像素”表示“画像元素”之意,有时亦被称为 pel(picture element)。
一个像素通常被视为影像的最小的完整取样。每个像素不是一个点或者一个方块,而是一个抽象的取样。仔细处理的话,一幅影像中的像素可以在任何尺度上看起来都不像分离的点或者方块;但是在很多情况下,它们采用点或者方块显示。
像素可以是长方形的或者方形的。电脑屏幕上的像素通常是方的。
分辨率
分辨率泛指量测或显示系统对细节的分辨能力。
日常生活常用的分辨率为影像分辨率和显示分辨率。它们经常被表示成每一个方向上的像素数量,比如 640×480 等,而现在更为常见的方式是只使用一个方向上的像素数,如 720P、1080P 表示垂直像素数为 720、1080,2K、4K 表示水平像素数达到 2000 和 4000 级别,而大多数的高清显示屏的纵横比都是16 : 9,故另一方向的像素数也能确定,所以 720P、1080P、2K、4K 的像素数量通常为 1280×720、1920x1080、2560×1440、3840 x 2160。
实际上 2K、4K 中的 “K” 在不同领域(如手机/桌面显示器/数字电视/电影等)、不同规范下都有着不同的定义,连不同厂商也都有自己的定义方式,但大致差不多。
720P、1080P 等的 “P” 代表 progressive scan,意思为逐行扫描,与之对应的 720i、1080i 等的 “i” 代表 interlaced scan,意为隔行扫描。逐行扫描通常从上到下地扫描每帧图像;隔行扫描则是交换扫描偶数行和奇数行(先奇后偶),由于视觉暂留效应,人眼不会注意到。
日常使用的分辨率说明了像素数量,只能反映影像或显示的精细程度,为了评价影像或显示的直观效果,需要分辨率和尺寸这两个参数,就像我们平时挑选显示器时一样。
DPI
每英寸点数 DPI (Dots Per Inch),用于点阵数字图像,指一英寸跨度内可以放置在一条线上的单个点的数量。
Windows 系统中的 DPI 代表软件的每英寸像素,其一般默认以 96 dpi 作为 100% 的缩放比率。
将 Windows 系统的 DPI 调高之后,字体、图标等的显示反而变大了。这是因为 Windows 系统的 DPI 实际上是逻辑 DPI。首先 Window 系统认定同一字体、图像的尺寸是不会改变的,例如某个格式的字体的尺寸为 0.1 英寸,当使用默认的系统 DPI 时,系统计算认为该字体需要 96 × 0.1 = 9.6 个像素来显示,当系统 DPI 调高至 120 时(缩放 125%),系统就会计算认为该字体需要 120 × 0.1 = 12 个像素来显示。这里的像素指显示器的像素,而显示器的尺寸和分辨率是不会变的,即显示器的 PPI 或者说像素的大小是不会变的,因而系统 DPI 越高,显示的图像越大。
PPI
每英寸像素 PPI (Pixels Per Inch),又称为像素密度,指打印图像或显示器在一英寸跨度内的像素数量(并非单位面积的像素量)。一般用来计量电脑显示器,电视机和手持电子设备屏幕的精细程度。通常情况下,每英寸像素值越高,屏幕能显示的图像也越精细。
PPI 和 DPI 经常都会出现混用现象,但它们所用的领域也存在区别。从技术角度说,“像素” 只存在于电脑显示领域,而 “点” 只出现于打印或印刷领域。
Windows 系统的 DPI 为什么默认为 96 ?
早期电脑屏幕的 PPI 与当时流行的点阵打印机的 DPI 相同,都为 72,也就是说,图片在屏幕上显示的尺寸与其打印出来的尺寸相同,所见即所得。但在实际生活中,人眼到屏幕的距离通常要比纸张更远,所以同样的图像或字体会显得更小,为了解决这个问题,微软非常机智地把系统默认的 DPI 提高到了 96,从而让图像和文本显示得更大,提升了可读性,后续的 Windows 软件也都以这个标准进行设计。影像在不同 PPI 显示器屏幕上的显示问题
在系统 DPI 一致的情况下,显示器屏幕的 PPI 越高,同一图片的显示尺寸显然就越小。如果想要图片的尺寸恢复到原本的尺寸,就需要放大该图片(废话),也就是提高其分辨率,即占用更多的像素点。根据缩放的比例不同,常用的缩放算法有两种:双线性插值(非整数缩放)和邻近像素采样(整数缩放)。
- 双线性插值的基本思想是先在一个方向上进行线性插值,然后在另一个方向上再进行一次线性插值,从而得到未知点的值。而线性插值的做法是,采集相邻的两个像素点的颜色,通过复杂的公式计算出中间的过渡色填充进去。双线性插值可以用于图像处理中的图像缩放、旋转、畸变等操作,可以保持图像的平滑性和连续性,但也会造成一定的模糊和失真。
- 临近像素采样选择离新位置最近的现有像素的颜色作为新像素的颜色值,这种方法的优点是计算速度快,不会有模糊的效果,但可能会导致图像边缘锐利的锯齿。
Windows 使用 DPI 96 为 100% 基准,定义了不同 DPI 对应不同的缩放比例:DPI 96 (100%)、DPI 120 (125%)、DPI 144 (150%)、DPI 192 (200%)。有些软件(比如微信)对于不同的 DPI 都做了适配,在所有的缩放比例下都能保持清晰,但有些软件只适配了默认的 96 DPI(比如傻鸟百度网盘个人版,纯纯恶心个人用户,23 年 5 月个人版才适配高 DPI 屏),此时桌面窗口管理器就会先使用 96 DPI 渲染窗口,再使用算法进行放大。非整数放大使用插值算法,整数放大则使用临近像素采样算法。不过可以在应用属性中来指定该应用的缩放策略:
选择“系统”意味着使用前面说的算法;选择“应用程序”意味着让软件自行选择缩放模式,例如原神启动器会直接关闭缩放,用原始的 96 DPI 点对点渲染在屏幕上,这样虽然保留了原始的清晰度,但在高 PPI 显示器屏幕上的 UI 就会非常小;选择“系统(增强)”,系统就会采用 GDI 缩放策略,GDI 策略的做法是先用整数倍来渲染 UI 中的矢量文本和图片,然后再压缩到实际的缩放比例,以减小矢量文本和图像的失真,但不是所有的元素都适用 GDI 策略缩放,有时会出现错位或清晰度不变的情况。
挑选电脑显示器屏幕时,能避免缩放的发生最好,即屏幕 PPI 接近 96。如果想要追求高分辨率同时显示器尺寸不能太大,那就尽量避免非整数倍缩放(避免模糊)。
由于人眼的分辨率是有极限的,在特定距离下,屏幕 PPI 超过一个值时视觉的差别就不再明显了,此时屏幕已具备足够高的像素密度而使得肉眼无法分辨其中单独的像素点。这就是苹果提出的 Retina 视网膜屏幕概念。根据苹果给出的视网膜设计标准的公式,可以计算出不同使用距离对应的视网膜 PPI,例如 60cm 对应的视网膜 PPI 为 146。当然 PPI 越高带来的视觉体验也会越好。iPhone 15 全系的屏幕 PPI 为 460,Studio Display 的 PPI 为 218,M3 MBP 显示屏 PPI 为 254。
基于总线的互连结构
下图给出了一个传统的基于总线互连的计算机系统结构示意图,在其互连结构中,除了 CPU、主存储器以及各种接插在主板扩展槽上的 I/O 控制卡(如声卡、视频卡)外,还有北桥芯片和南桥芯片。这两块超大规模集成电路芯片组成一个 “芯片组”,是计算机中各个组成部分相互连接和通信的枢纽。主板上所有的存储器控制功能和 I/O 控制功能几乎都集成在芯片组内,它既实现了总线的功能,又提供了各种 I/O 接口及相关的控制功能。其中,北桥是一个主存控制器集线器 MCH (Memory Controller Hub) 芯片,本质上是一个 DMA 控制器,因此,可通过 MCH 芯片,直接访问主存和显卡中的显存。南桥是一个 I/O 控制器集线器 ICH (I/O Controller Hub) 芯片,其中可以集成 USB 控制器、磁盘控制器、以太网络控制器等各种外设控制器,也可以通过南桥芯片引出若干主板扩展槽,用以接插一些 I/O 控制卡。
如图所示,CPU 与主存之间由处理器总线(也称为前端总线)和存储器总线相连,各类 I/O 设备通过相应的设备控制器(例如,USB 控制器、显示适配卡、磁盘控制器)连接到 I/O 总线上,而 I/O 总线通过芯片组与主存和 CPU 连接。
传统上,总线分为处理器-存储器总线和 I/O 总线。处理器-存储器总线比较短,通常是高速总线。有的系统将处理器总线和存储器总线分开,中间通过北桥芯片(桥接器)连接,CPU 芯片通过 CPU 插座插在处理器总线上,内存条通过内存条插槽插在存储器总线上。
下面对处理器总线、存储器总线和 I/O 总线进行简单说明。
处理器总线
国内教材中系统总线通常指连接 CPU、存储器和各种 I/O 模块等主要部件的总线统称,而 Intel 公司推出的芯片组中,对系统总线赋予了特定的含义,特指 CPU 连接到北桥芯片的总线,也称为处理器总线或前端总线 FSB (Front Side Bus)。
FSB 总线
早期的 Intel 微处理器的处理器总线称为前端总线 FSB (FrontSide Bus),它是主板上最快的总线,主要用于处理器与北桥芯片进行信息交换。
FSB 的传输速率单位实际上是 MT/s,表示每秒传输多少兆次。通常所说的总线传输速率单位 MHz 是习惯上的说法,实质是时钟频率单位。早期的 FSB 每个时钟周期传送一次数据,因此时钟频率与数据传输速率一致。但是,从 PentiumPro 开始,FSB 采用 quad pumped 技术在每个总线时钟周期内传 4 次数据,也就是说总线的数据传输速率等于总线时钟频率的 4 倍,若时钟频率为 333MHz,则数据传输速率为 1333MT/s,即 1.333CT/s,但习惯上称 1333MHz。若前端总线的工作频率为 1333MHz(实际时钟频率为 333MHz),总线宽度为 64 位,则总线带宽为 10.66GB/s。对于多 CPU 芯片的多处理器系统则多个 CPU 芯片通过一个 FSB 进行互连,也即多个处理器共享一个 FSB。
QPI 总线
Intel 推出 Core i7 时,北桥芯片的功能被集成到了 CPU 芯片内,CPU 通过存储器总线(即内存条插槽)直接和内存条相连,而在 CPU 芯片内部的核与核之间、CPU 芯片与其他 CPU 芯片之间,以及 CPU 芯片与 IOH (Input/Output Hub) 芯片之间,则通过 QPI (Quick Path Inter-connect) 总线相连。
IOH 也是从传统意义上的北桥剥离出来,作为 CPU(MCH)与 ICH 连接起来的桥梁,通过 QPI 总线与 CPU 相连,下方使用 DMI 总线连接南桥 ICH 芯片。
上图给出了 Intel Core i7 中核与核之间、核与主存控制器之间以及各级 cache 之间的互连结构。从图中可以看出,一个 Core i7 处理器中有 4 个 CPU 核(core),每两个核之间都用 QPI 总线互连,并且每个核还有一条 QPI 总线可以与 IOH 芯片互连。处理器支持三通道 DDR3 SDRAM 内存条插槽,因此,处理器中包含 3 个内存控制器,并有 3 个并行传输的存储器总线,也意味着有 3 组内存条插槽。
QPI 总线是一种基于包传输的串行高速点对点连接协议,采用差分信号与专门的时钟信号进行传输。QPI 总线有 20 条数据线,发送方(TX)和接收方(RX)有各自的时钟信号,每个时钟周期传输两次。一个 QPI 数据包包含 80 位,需要两个时钟周期即 4 次传输,才能完成整个数据包的传送。在每次传输的 20 位数据中,有 16 位是有效数据,其余 4 位用于循环冗余校验,以提高系统的可靠性。由于 QPI 是双向的,在发送的同时也可以接收另一端传输来的数据,这样,每个 QPI 总线的带宽计算公式为:每秒传输次数 x 每次传输的有效数据 x 2。
QPI 总线的速度单位通常为 GT/s,若 QPI 的时钟频率为 2.4GHz,则速度为 4.8GT/s,表示每秒传输 4.8G 次数据,并称该 QPI 频率为 4.8GT/S。因此,QPI 频率为 4.8GT/s 的总带宽为 4.8GT/s × 2B × 2 = 19.2GB/s。QPI 频率为 6.4GT/s 的总带宽为 6.4GT/s × 2B × 2 = 25.6GB/s。
存储器总线
早期的存储器总线由北桥芯片控制,处理器通过北桥芯片和主存储器、图形卡(显卡)以及南桥芯片进行互连。Core i7 以后的处理器芯片中集成了内存控制器,因而,存储器总线直接连接到处理器。
根据芯片组设计时确定的所能处理的主存类型的不同,存储器总线有不同的运行速度。如上图所示的计算机中,存储器总线宽度为 64 位,每秒传输 1333M 次,总线带宽为 1333M x 64/8 = 10.66 (GB/s),因而 3 个通道的总带宽为 32GB/s,与此配套的内存条型号为 DDR3-1333。
I/O 总线
I/O 总线用于为系统中的各种 I/O 设备提供输入/输出通路,在物理上通常是主板上的一些 I/O 扩展槽。早期的第一代 I/O 总线有 XT 总线、ISA 总线、EISA 总线、VESA 总线,这些 I/O 总线早已经被淘汰;第二代 I/O 总线包括 PCI、AGP、PCI-X;第三代 I/O 总线包括 PCI-Express。
将北桥芯片功能集成到 CPU 芯片后,主板上的芯片组不再是传统的三芯片结构(CPU + 北桥 + 南桥)。根据不同的组合有多种主板芯片组结构,有的是双芯片结构(CPU + PCH),有的是三芯片结构(CPU + IOH + ICH)。其中,双芯片结构中的 PCH (PlatformController Hub) 芯片除了包含原来南桥(ICH)的 I/O 控制器集线器的功能外,还集成了以前北桥中的图形显示控制单元管理引擎 ME (Management Engine) 单元,另外还包括 NVRAM (Non-Vola-tile Random Access Memory) 控制单元等。也就是说,PCH 比以前南桥的功能要复杂得多。
示例:CPU+IOH+ICH 三芯片结构
下图给出了一个基于 Intel Core i7 系列三芯片结构的单处理器计算机系统互连示意图。图中 Core i7 处理器芯片直接与三通道 DDR3 SDRAM 主存储器连接,并提供一个带宽为 25.6GB/s 的 QPI 总线,与基于 X58 芯片组的 IOH 芯片相连。图中每个通道的存储器总线带宽为 64/8 × 533 × 2 = 8.5 (GB/s),所配内存条速度为 533MHz × 2 = 1066MT/s。
图中,IOH 的重要功能是提供对 PCI-Express 2.0 的支持,最多可支持 36 条 PCI-Express 2.0 通路,可以配置为一个或两个 PCI-Express2.0 ×16 的链路,或者 4 个 PCI-Express 2.0 ×8 的链路,或者其他的组合,如 8 个 PCI-Express 2.0 ×4 的链路等。这些 PCI-Express 链路可以支持多个图形显示卡。
IOH 与 ICH 芯片(ICH10 或 ICH10R)通过 DMI (Direct Media Interface) 总线连接。DMI 采用点对点的连接方式,时钟频率为100MHz,因为上行与下行各有 1GB/s 的数据传输率,因比总带宽达到 2GB/s。ICH 芯片中集成了相对慢速的外设 I/O 接口。若采用 ICH10R 芯片,则还支持 RAID 功能,即 ICH10R 芯片中还包含 RAID 控 制器,所支持的 RAID 等级有 SATA RAID 0、RAID 1、RAID 5、RAID 10 等。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 ZERO!