学习操作系统往往需要先学习CPU相关知识,然后再学习操作系统的结构,主要是因为操作系统是运行在 CPU 上的核心软件,它通过与 CPU 的交互来管理计算机的硬件资源,执行各种系统服务,并为用户和应用程序提供接口和功能。
目录
- 1. 硬件结构
- 1.1 CPU定义、作用及结构
- 1.1.1 软件层面
- 1.1.2 硬件层面
- 1.1.3 多CPU、多核CPU、逻辑CPU
- 补充:一个多核 CPU 共享内核态(kernel mode),可以访问同一个内核
- 1.1.4 CPU Cache和其他存储器
- 2. 硬件层面的硬件机制和协议
- 2.1 CPU Cache的缓存命中率——为数据读操作服务
- 2.1.1 提升数据缓存的命中率
- 2.1.2 提升指令缓存的命中率
- 2.1.3 提升多核 CPU 的缓存命中率
- 2.2 缓存一致性——确定共享数据什么时候写入
- 2.2.1 基于总线嗅探机制的 MESI 协议
- 3. 硬件层面和软件层面的协作
- 3.1 CPU 是如何执行任务的
- 3.1.1 CPU 是怎么读写数据的?
- 3.1.1.1 由于块读取导致的伪共享问题
- 3.1.1.2 伪共享问题的解决方法
- 3.1.2 CPU 是如何选择线程调度任务的?
- 2.3.2.1 如何保障优先级高的任务先执行
- 3.2 CPU中断机制
- 3.2.1 软中断
- 3.2.2 硬中断
- 4. CPU的加法操作
- 4.1 加减法的统一借由补码实现
1. 硬件结构
在冯诺依曼计算机模型中,将计算机分为5个基础结构:
- 运算器
- 控制器:CPU(中央处理单元)
- 存储器:内存等
- 输入设备
- 输出设备
CPU是控制器,但不仅仅是控制器,准确来说,运算器、控制器都是在中央处理器之中。关系如下图:
1.1 CPU定义、作用及结构
CPU架构既涉及硬件层面,也涉及软件层面。硬件层面,CPU架构指的是中央处理单元的设计和组织方式,包括其内部的处理单元、寄存器、缓存、总线等组件的结构和连接方式。软件层面,CPU架构指的是为特定CPU设计的指令集。
1.1.1 软件层面
指令集:x86,x64,x86-64,amd64(这些是和Intel公司的相关的),arm指令集(这个是arm公司的) 架构之间的关系。
- 指令集是操作系统和硬件的对接接口。通过操作系统调度,操作系统然后让硬件去计算。
- x86 架构源起于1978 年的 Intel 8086,也称为x86-32。该系列的处理器名称是以数字来表示,因此其架构被称为 x86。
- x86-64也称为x64,是为了解决32位x86架构所面临的内存寻址限制和性能瓶颈而引入的。它使得计算机可以支持更大的内存空间,并且在处理大型数据和运行复杂应用程序时性能得到显著提升。Intel将x86的专利授权AMD后,AMD公开 64 位集以扩展 x86,后续形成了AMD64。后来英特尔也推出了与之兼容的处理器,并命名Intel 64。两者一般被统称为 x86-64 或 x64,开创了 x86 的 64 位时代。
- arm架构:该架构 CPU 主要有高通、三星、苹果、华为海思、联发科等公司。这种 CPU 常用在手机上,包括安卓和苹果。
硬件架构决定操作系统版本。对于Windows操作系统,会在下载页上看到不同的安装包版本,如x86(32位)和x64(64位)版本。操作系统版本决定软件安装时的版本,比如scrcpy:在windows操作系统下也需要选择win64或者win32。
软件可能也有通用安装包,即所有版本的二进制文件都包含在其中,几乎支持所有处理器架构的机器,但相应的安装包比较大。
在linux系统上,可以用cat /proc/version
来插查询操作系统版本得到:Linux version 3.10.0-1160.24.1.el7.x86_64 (mockbuild@kbuilder.bsys.centos.org) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC) ) #1 SMP Thu Apr 8 19:51:47 UTC 2021
,这里就能看到CPU架构是intel x86_64。
- Linux version 3.10.0-1160.24.1.el7.x86_64:这是Linux内核的版本号。版本号的组成通常包括主版本号、次版本号和修订版本号。在这个例子中,主版本号为3,次版本号为10,修订版本号为1160.24.1。后面的".el7"表示该内核是基于RHEL 7或CentOS 7的版本。
- (mockbuild@kbuilder.bsys.centos.org): 这部分是构建内核时的信息,指示构建内核的主机名称和用户
- (gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC))是GCC编译器的版本信息,指示了在构建内核时使用的GCC版本。
- 为什么会提到两个系统(CentOS和Red Hat)呢?CentOS和Red Hat Enterprise Linux (RHEL) 实际上是非常相关的发行版。CentOS 是 RHEL 的社区衍生版本,几乎与 RHEL 完全兼容,并在 RHEL 的源代码发布后通过重新编译来创建。因此,CentOS 和 RHEL 有很多共同的特性和软件包。由于 CentOS 是从 RHEL 代码重新编译而来,因此内核版本信息中可能会同时提到 CentOS 和 Red Hat,以表明该内核适用于两者。在上述版本信息中,“.el7” 部分指示该内核是适用于 CentOS 7 和 RHEL 7 的版本。
x86_64的含义体现在getconf LONG_BIT
和getconf WORD_BIT
。64位系统中应该分别得到64和32。32位系统中应该分别得到32和32。32位的系统中int类型和long类型一般都是4字节,64位的系统中int类型还是4字节的,但是long已变成了8字节。64位系统和32位系统区别如下:
- 寻址能力:
32位系统:每个内存地址由32位二进制数表示,因此最大寻址能力为2^32(约为4GB)。这意味着在32位系统下,一个进程最多只能访问4GB的内存空间。
64位系统:每个内存地址由64位二进制数表示,最大寻址能力为2^64,这是一个非常大的数值,远远超过目前实际使用的内存容量。64位系统可以支持更大的内存,因此可以同时处理更大的数据集。- 数据处理能力:
32位系统:每次处理的数据块大小是32位,即4个字节。这意味着在32位系统中,CPU一次可以处理32位的数据。
64位系统:每次处理的数据块大小是64位,即8个字节。这意味着在64位系统中,CPU一次可以处理64位的数据,相对于32位系统,64位系统在处理大型数据集时具有更高的性能。- 运行兼容性:
32位系统:32位应用程序只能在32位系统上运行,不能在64位系统上运行。但在一些64位操作系统上可能提供一些兼容层,使得一些32位应用程序可以在64位系统上运行。
64位系统:64位应用程序可以在64位系统上运行,并且在性能上通常比在32位系统上运行更好。同时,64位系统也可以运行32位应用程序,但性能可能略有降低。- 安全性:
64位系统:由于寻址空间更大,64位系统在内存地址随机化(ASLR)等安全机制方面更加有效。这使得在64位系统上执行攻击更加困难,增强了系统的安全性。
1.1.2 硬件层面
- 控制单元负责控制 CPU 工作
- 逻辑运算单元负责计算
- 寄存器可以分为多种类,每种寄存器的功能又不尽相同:
- 通用寄存器,用来存放需要进行运算的数据,比如需要进行加和运算的两个数据。
- 程序计数器,用来存储 CPU 要执行下一条指令「所在的内存地址」。
- 指令寄存器,用来存放当前正在执行的指令,也就是指令本身,指令被执行完成之前,指令都存储在这里。
当 CPU 要读写内存数据的时候,一般需要通过下面这三个总线:
- 首先要通过「地址总线」来指定内存的地址;
- 然后通过「控制总线」控制是读或写命令;
- 最后通过「数据总线」来传输数据;
CPU 执行程序的过程如下:
- 第一步,CPU 读取「程序计数器」的值,这个值是指令的内存地址,然后 CPU 的「控制单元」操作「地址总线」指定需要访问的内存地址,接着通知内存设备准备数据,数据准备好后通过「数据总线」将指令数据传给 CPU,CPU 收到内存传来的数据后,将这个指令数据存入到「指令寄存器」。
- 第二步,「程序计数器」的值自增,表示指向下一条指令。这个自增的大小,由 CPU 的位宽决定,比如 32 位的 CPU,指令是 4 个字节,需要 4 个内存地址存放,因此「程序计数器」的值会自增 4;
- 第三步,CPU 分析「指令寄存器」中的指令,确定指令的类型和参数,如果是计算类型的指令,就把指令交给「逻辑运算单元」运算;如果是存储类型的指令,则交由「控制单元」执行;
a = 1 + 2 在 32 位 CPU 的执行过程:程序编译过程中,编译器通过分析代码,发现 1 和 2 是数据,于是程序运行时,内存会有个专门的区域来存放这些数据,这个区域就是「数据段」,同时把从从数据段读入数据、+、向数据段写入数据存放在指令区域「正文段」。然后依次执行这 4 条指令。
1.1.3 多CPU、多核CPU、逻辑CPU
- 多个CPU: 在计算机系统中使用了多个独立的物理处理器(CPU)。每个CPU都是一个完整的处理器,拥有自己的执行单元、寄存器、缓存等,它们可以独立地执行指令和处理任务。多个CPU可以并行地处理多个任务,提高系统的整体性能。
- 多核CPU:在单个物理芯片上集成了多个处理核心(Core)的CPU。每个核心都是一个完整的处理器,可以执行独立的指令和任务。多核CPU利用在一个物理芯片上集成多个核心的方式来提高计算性能。处理器之间核心寄存器私有,但能共享公共缓存。
- 逻辑CPU:在多核CPU或多个物理CPU的基础上,通过超线程(Hyper-Threading)技术将每个物理核心模拟成多个逻辑处理器。在逻辑CPU的概念中,一个物理核心被虚拟化为多个逻辑处理单元,使得每个物理核心可以同时执行多个线程。
多核适合多线程,多个可以多任务。如果一个系统有两个物理CPU,每个CPU有256个核心,并且每个核心都支持超线程技术,那么逻辑CPU的个数可能是256 * 2 * 2 = 1024(假设每个物理核心可以模拟出两个逻辑CPU)。在一些没有启用超线程的系统中,逻辑CPU的个数和物理CPU核数可能是相同的。这种情况下,每个物理核心只能同时执行一个线程,因此逻辑CPU的个数与核数相同。
总核数 = 物理CPU个数 X 每颗物理CPU的核数
总逻辑CPU数 = 总核数 X 线程数
查看CPU型号
cat /proc/cpuinfo | grep name | sort | uniq
查看物理CPU个数
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
查看每个物理CPU中core的个数(即核数)
cat /proc/cpuinfo| grep "core id"| uniq|wc -l
查看逻辑CPU的个数
cat /proc/cpuinfo| grep "processor"|uniq| wc -l
- cat /proc/cpuinfo:这部分指令使用cat命令来显示CPU信息。在Linux系统中,/proc/cpuinfo是一个特殊的文件,它包含有关系统CPU的详细信息,如CPU型号、频率、核心数等。
- grep name:这部分指令使用grep命令来筛选含"name"关键字的行,从而过滤出包含CPU型号信息的行。
- sort:这部分指令使用sort命令对CPU型号信息进行排序,将相同的CPU型号放在一起。
- uniq:这部分指令使用uniq命令去除重复的行,以便只显示唯一的CPU型号。
- wc:是Linux/Unix系统中的一个计数命令,用于统计文件或输入流的行数、单词数、字符数等。
- -l:是wc命令的一个选项,表示只统计行数。
或者直接lscpu
:
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian 表示计算机系统采用小端字节序(Little Endian)。
CPU(s): 8
On-line CPU(s) list: 0-7
Thread(s) per core: 2
Core(s) per socket: 4 每个物理CPU插槽(socket)上有4个核心。
Socket(s): 1 计算机中的物理CPU插槽数量。
NUMA node(s): 1 (Non-Uniform Memory Access)节点数量。NUMA是一种多处理器系统架构,在这种架构中,每个处理器都有自己的本地内存,同时可以访问其他处理器的内存。
Vendor ID: GenuineIntel CPU的制造商。
CPU family: 6 CPU的系列为6,这代表是Intel的第6代CPU。
Model: 158 CPU的型号为158。
Model name: Intel® Core™ i7-7700HQ CPU @ 2.80GHz CPU的名称@主频。
Stepping: 9 CPU的步进版本。步进版本用于标识CPU的特定版本和修订。
CPU MHz: 899.998 CPU的运行频率。
BogoMIPS: 5616.00 这个字段表示BogoMIPS值,是一个估计值,用于衡量CPU的处理能力。
Virtualization: VT-x CPU支持的虚拟化技术。
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 6144K
NUMA node0 CPU(s): 0-7
在这个例子中,lscpu命令显示了一个具有8个CPU核心(系统中的逻辑CPU数量)。Thread(s) per core:这个字段显示的是每个物理核心模拟出的逻辑线程数。如果该值大于1,表示系统支持超线程技术。
补充:一个多核 CPU 共享内核态(kernel mode),可以访问同一个内核
内核态是一种为了获取更高权限和执行特权操作而设计的状态,一个多核CPU的所有内核都可以进入这个状态。
1.1.4 CPU Cache和其他存储器
CPU Cache 用 SRAM(Static Random-Access Memory,静态随机存储器) 芯片,只要有电,数据就可以保持存在,而一旦断电,数据就会丢失了。
现代的一台计算机,都用上了 CPU Cahce、内存、到 SSD 或 HDD 硬盘这些存储器设备了。存储层次结构形成了缓存的体系。当 CPU 需要访问内存中某个数据的时候,如果寄存器有这个数据,CPU 就直接从寄存器取数据即可,如果寄存器没有这个数据,CPU 就会查询 L1 高速缓存,如果 L1 没有,则查询 L2 高速缓存,L2 还是没有的话就查询 L3 高速缓存,L3 依然没有的话,才去内存中取数据。
L1 Cache 通常会分为「数据缓存」和「指令缓存」:
- L1d cache: 32K
- L1i cache: 32K
L1 Cache 和 L2 Cache 都是每个 CPU 核心独有的。
- L2 cache: 512K
L3 Cache 是多个 CPU 核心共享的。
L3 cache: 16384K
2. 硬件层面的硬件机制和协议
2.1 CPU Cache的缓存命中率——为数据读操作服务
对于使用缓存的存储结构(如CPU缓存、硬盘缓存等),缓存命中率指在特定时间内成功从缓存中获取数据的次数与总的数据访问次数之间的比率。读取数据时如果缓存中存在该数据(命中),计算机可以直接从缓存中获取,速度非常快,这样就避免了从较慢的主存或磁盘中读取数据的开销。如果缓存中不存在该数据(未命中),计算机就需要从主存或磁盘中读取数据,速度较慢。高缓存命中率意味着大部分数据都能从缓存中快速获取,系统性能较好。、
如果数据在该CPU核心的缓存中存在,则称为缓存命中(Cache Hit),CPU可以直接从缓存中读取或写入数据,速度很快。但是,如果数据在该CPU核心的缓存中不存在,则称为缓存失效(Cache Miss)。
2.1.1 提升数据缓存的命中率
基于缓存机制,写出让CPU跑得更快的代码(以二维数组为例):
形式一用 array[i][j] 访问数组元素的顺序,正是和内存中数组元素存放的顺序一致。当 CPU 访问 array[0][0] 时,由于该数据不在 Cache 中,于是会「顺序」把跟随其后的 3 个元素从内存中加载到 CPU Cache,这样当 CPU 访问后面的 3 个数组元素时,就能在 CPU Cache 中成功地找到数据,这意味着缓存命中率很高,缓存命中的数据不需要访问内存,这便大大提高了代码的性能。形式二的 array[j][i] 访问的方式跳跃式的,而不是顺序的,那么如果 N 的数值很大,那么操作 array[j][i] 时,是没办法把 array[j+1][i] 也读入到 CPU Cache 中的,既然 array[j+1][i] 没有读取到 CPU Cache,那么就需要从内存读取该数据元素了。
因此,按照内存布局顺序访问,将可以有效的利用 CPU Cache 带来的好处,代码的性能就会得到提升。
2.1.2 提升指令缓存的命中率
基于CPU 的分支预测器,写出跑的更快的代码:
CPU的分支预测器(Branch Predictor)是计算机处理器中的一种硬件机制,用于预测分支指令(如条件跳转语句)的执行路径。分支预测器的作用就是在分支指令未执行完之前尽可能准确地预测分支的执行路径,从而提前预取正确的分支指令,提高指令执行效率。分支预测器通常是一种硬件模块,内部有一个预测表(prediction table),用来记录历史分支的执行情况和模式,以便根据历史信息做出预测。
比如有一个元素为 0 到 100 之间随机数字组成的一维数组,对这个数组做两个操作:
- 第一个操作,循环遍历数组,把小于 50 的数组元素置为 0;
- 第二个操作,将数组排序;
先排序再遍历速度会更快,这是因为排序之后,数字是从小到大的,那么前几次循环命中 if < 50 的次数会比较多,于是分支预测就会缓存 if 里的 array[i] = 0 指令到 Cache 中,后续 CPU 执行该指令就只需要从 Cache 读取就好了。
如果肯定代码中的 if 中的表达式判断为 true 的概率比较高,可以使用显示分支预测工具,比如在 C/C++ 语言中编译器提供了 likely 和 unlikely 这两种宏,如果 if 条件为 ture 的概率大,则可以用 likely 宏把 if 里的表达式包裹起来,反之用 unlikely 宏。
2.1.3 提升多核 CPU 的缓存命中率
线程可能在不同 CPU 核心来回切换执行,这对 CPU Cache 不是有利的,虽然 L3 Cache 是多核心之间共享的,但是 L1 和 L2 Cache 都是每个核心独有的,如果一个线程在不同核心来回切换,各个核心的缓存命中率就会受到影响,相反如果线程都在同一个核心上执行,那么其数据的 L1 和 L2 Cache 的缓存命中率可以得到有效提高,缓存命中率高就意味着 CPU 可以减少访问内存的频率。
当有多个同时执行「计算密集型」的线程,为了防止因为切换到不同的核心,而导致缓存命中率下降的问题,我们可以把线程绑定在某一个 CPU 核心上,这样性能可以得到非常可观的提升。
在大部分操作系统中,可以使用特定的系统调用或库函数将线程绑定到某个CPU核心上。这个过程通常称为CPU亲和性(CPU affinity)。通过将线程绑定到特定的CPU核心,可以控制线程在特定的处理器上执行,从而优化性能并避免频繁的核心切换。
- 在 Linux 上提供了 sched_setaffinity 方法:使用sched_setaffinity函数将线程绑定到指定的CPU核心上。该函数允许设置线程的CPU亲和性掩码,将线程限制在指定的CPU核心上执行。
sched_setaffinity是一个Linux系统调用,用于设置线程的CPU亲和性,是C/C++编程中可以使用的系统调用函数。它不是特定于C++的,而是属于Linux操作系统的特性。在C/C++编程中,可以通过包含<sched.h>头文件来调用sched_setaffinity函数。
- 在Linux系统中,taskset命令用于将进程或线程绑定到指定的CPU核心上,以实现CPU亲和性。它可以通过终端或shell来执行。
2.2 缓存一致性——确定共享数据什么时候写入
补充一下怎么写入数据,写入到Cache还是内存:
- 写直达(Write-Through):在写直达方式下,当CPU写入数据时,数据会同时写入缓存和主内存。这意味着数据在写入缓存后还会立即写入主内存,确保缓存和主内存中的数据始终保持一致。写直达能够确保数据的一致性,但写入的开销较大,因为每次写入都需要同时写入两个存储层。
假设有两个CPU(CPU1和CPU2)共享同一块内存区域,并且使用写直达方式。CPU1写入数据X到内存地址A,数据同时被写入CPU1的缓存和主内存。CPU2在之后读取内存地址A的数据,由于CPU2的缓存与主内存保持一致,它从自己的缓存中读取数据X,而不需要再访问主内存。
- 写回(Write-Back):在写回方式下,当CPU写入数据时,数据只会写入缓存,而不会立即写入主内存。只有当缓存中的数据被替换出去或者所在的缓存行失效时,才会将数据写回主内存。写回方式减少了写入主内存的次数,从而减少了写入的开销,提高了写入的效率。但同时,写回方式可能导致缓存和主内存中的数据不一致,因为数据在缓存中被修改后,并没有立即同步到主内存。
假设有两个CPU(CPU1和CPU2)共享同一块内存区域,并且使用写回方式。CPU1写入数据X到内存地址A,数据被写入CPU1的缓存,但不会立即写入主内存。CPU2在之后读取内存地址A的数据,如果数据在CPU2的缓存中不存在或者失效,CPU2需要从主内存中读取数据X,然后将其缓存在自己的缓存中。
- 写分配(Write-Allocate):写分配是一种特殊的写入策略,用于处理写回方式下的缓存失效情况。当CPU写入数据到一个缓存行,但该缓存行已经失效(不包含要写入的数据)时,写分配会先将该缓存行的数据从主内存读取到缓存,然后再进行写操作。这样做可以确保写入操作在缓存中完成,从而减少了对主内存的访问。
假设有两个CPU(CPU1和CPU2)共享同一块内存区域,并且使用写回方式。CPU1写入数据X到内存地址A,数据被写入CPU1的缓存,但不会立即写入主内存。之后,CPU2也要写入数据Y到内存地址A,但该地址在CPU2的缓存中失效。在写分配的策略下,CPU2首先会从主内存中读取数据X,并将其缓存在自己的缓存中,然后再将数据Y写入自己的缓存。
概念:CPU缓存一致性(Cache Coherence)指的是在多个CPU(或处理器)共享同一块内存区域时,保证多个CPU对该内存区域的缓存数据是一致的。当多个CPU共享同一块内存区域时,如果一个CPU修改了该内存区域的数据,并将其写回到内存,其他CPU的缓存中存储的数据就会变得不一致。这就是缓存一致性问题。
场景:多核处理器或分布式系统,采用写回方式
目的:保证多个处理器之间的数据一致性,避免数据冲突和错误。
解决方法:用协议定义CPU之间如何进行缓存数据的共享和更新,以确保缓存一致性。
- MESI协议(Modified, Exclusive, Shared, Invalid)
- MOESI协议(Modified, Owner, Exclusive, Shared, Invalid)
- 某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache,这个称为写传播(Write Propagation);
- 第二点,某几个CPU 核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的,这个称为事务的串行化(Transaction Serialization)。
2.2.1 基于总线嗅探机制的 MESI 协议
MESI 协议,是已修改、独占、共享、已失效这四个状态的英文缩写的组合。整个 MSI 状态的变更,则是根据来自本地 CPU 核心的请求,或者来自其他 CPU 核心通过总线传输过来的请求,从而构成一个流动的状态机。另外,对于在「已修改」或者「独占」状态的 Cache Line,修改更新其数据不需要发送广播给其他 CPU 核心。
在MESI协议中,当一个CPU核心(称为"核心A")修改了某个缓存行中的数据时,它会将该缓存行的状态设置为"Modified",表示该缓存行的数据已被修改,并且数据只存在于该核心的缓存中,而不是与主内存一致。
在此情况下,核心A不需要广播告知其他CPU可以更新缓存的原因是:核心A已经将数据标记为"Modified"状态,此时其他CPU的缓存中可能仍然包含着旧的缓存行数据(可能是"Shared"或"Exclusive"状态),而且这些缓存行数据可能与主内存中的数据不一致。
如果核心A要广播告知其他CPU可以更新缓存,其他CPU仍然需要从主内存中读取最新的数据来更新自己的缓存,这样反而增加了总线通信的开销和延迟。相反,核心A标记为"Modified"状态后,其他CPU如果要读取相同的内存地址,将会发生缓存失效,而后再从核心A的缓存中获取最新的数据,从而保证数据的一致性。
使用MESI协议的目标是确保多个CPU之间对共享数据的一致性,并在数据被修改时最大程度地减少对总线的广播通信。通过在写入数据时将缓存行状态设置为"Modified",核心A表明它独占了该数据,并且其他CPU需要在读取该数据时更新自己的缓存。这样可以减少总线通信和内存访问,提高缓存访问的效率和性能。
3. 硬件层面和软件层面的协作
3.1 CPU 是如何执行任务的
3.1.1 CPU 是怎么读写数据的?
CPU 从内存中读取数据到 Cache 的时候,并不是一个字节一个字节读取,而是一块一块的方式来读取数据的,这一块一块的数据被称为 CPU Cache Line(缓存块),所以 CPU Cache Line 是 CPU 从内存读取数据到 Cache 的单位。
对数组的加载, CPU 就会加载数组里面连续的多个数据到 Cache 里,这也是为什么前面提到应该按照物理内存地址分布的顺序去访问元素,这样访问数组元素的时候,Cache 命中率就会很高,于是就能减少从内存读取数据的频率, 从而可提高程序的性能。
3.1.1.1 由于块读取导致的伪共享问题
CPU伪共享(False Sharing)是由于多个CPU核心同时访问同一缓存行中的不相关数据而引起的性能问题。它是由于缓存行对齐和缓存一致性协议的特性造成的。
当多个CPU核心同时访问同一缓存行中的不相关数据时,由于缓存一致性协议的限制,每个CPU核心必须将整个缓存行都标记为"Invalid"状态,然后再从主内存中获取最新的数据。这样,当其他CPU核心要访问相同的缓存行时,它们也必须从主内存中获取最新的数据,导致缓存失效和频繁的主内存访问,从而降低了性能。
3.1.1.2 伪共享问题的解决方法
-
缓存行填充:在多个变量之间添加填充数据,使它们不再位于同一缓存行,从而避免伪共享。
-
使用无锁数据结构:采用无锁数据结构可以避免多个CPU核心对同一缓存行进行写操作,从而减少伪共享问题。
-
使用缓存对齐:将相关的数据放在不同的缓存行中,确保它们在不同的CPU核心之间不会产生伪共享。
在 Linux 内核中存在 __cacheline_aligned_in_smp 宏定义,是用于解决伪共享的问题。SMP是多核共享内存
3.1.2 CPU 是如何选择线程调度任务的?
在 Linux 内核中,进程和线程都是用 task_struct 结构体表示的,区别在于线程的 task_struct 结构体里部分资源是共享了进程已创建的资源,比如内存地址空间、代码段、文件描述符等,所以 Linux 中的线程也被称为轻量级进程,因为线程的 task_struct 相比进程的 task_struct 承载的 资源比较少,因此以「轻」得名。
Linux 内核里的调度器,调度的对象就是 task_struct,接下来把这个数据结构统称为任务。在 Linux 系统中,根据任务的优先级以及响应要求,主要分为两种,其中优先级的数值越小,优先级越高:
- 实时任务,对系统的响应时间要求很高,也就是要尽可能快的执行实时任务,优先级在 0~99 范围内的就算实时任务;
- 普通任务,响应时间没有很高的要求,优先级在 100~139 范围内都是普通任务级别;
2.3.2.1 如何保障优先级高的任务先执行
由于任务有优先级之分,Linux 系统为了保障高优先级的任务能够尽可能早的被执行,于是分为了这几种调度类,如下图:
每个 CPU 都有自己的运行队列(Run Queue, rq),用于描述在此 CPU 上所运行的所有进程,其队列包含三个运行队列,Deadline 运行队列 dl_rq、实时任务运行队列 rt_rq 和 CFS 运行队列 cfs_rq,其中 cfs_rq 是用红黑树来描述的,按 vruntime 大小来排序的,最左侧的叶子节点,就是下次会被调度的任务。
如果我们启动任务的时候,没有特意去指定优先级的话,默认情况下都是普通任务,普通任务的调度类是 Fair,由 CFS 调度器来进行管理。CFS 调度器的目的是实现任务运行的公平性,也就是保障每个任务的运行的时间是差不多的。如果你想让某个普通任务有更多的执行时间,可以调整任务的 nice 值,从而让优先级高一些的任务执行更多时间。nice 的值能设置的范围是 -20~19, 值越低,表明优先级越高,因此 -20 是最高优先级,19 则是最低优先级,默认优先级是 0。
nice -n 10 ./my_program # 启动一个新进程 my_program,并将其优先级调整为10(较低的优先级)
renice -n -5 12345 # 将进程ID为12345的进程优先级调整为-5(较高的优先级)
3.2 CPU中断机制
CPU中断是计算机系统中的一种机制,用于处理来自外部设备或其他核心的请求。当发生中断时,CPU会暂停当前正在执行的任务,并转而执行中断处理程序来响应中断事件。中断可以是硬件中断,如设备发出的中断请求;也可以是软件中断,如程序中使用软件指令触发的中断。
-
硬件层面:CPU中断机制是由硬件设计来实现的。CPU内部包含中断控制器(Interrupt Controller),它负责接收来自外部设备或其他核心的中断信号,并根据优先级和类型来处理这些中断请求。当有中断请求时,CPU会立即停止当前正在执行的任务,保存当前的上下文信息,并跳转到相应的中断处理程序来处理中断事件。
-
软件层面:系统软件(如操作系统)负责管理和处理中断。当硬件中断控制器接收到中断信号后,它会通知操作系统,并将中断信息传递给操作系统的中断处理程序。操作系统中的中断处理程序负责处理中断事件,根据中断类型来执行相应的操作,比如处理输入输出、处理时钟中断、处理异常等。
中断丢失是指当系统在处理一个中断时,又接收到了一个同样或更高优先级的中断请求,而当前正在处理的中断还未完成,导致新的中断请求被丢失。这种情况可能会导致系统不能及时响应高优先级的中断请求,从而可能导致数据丢失或系统性能下降。
3.2.1 软中断
软中断的执行过程是由操作系统内核(当前运行的程序)自主触发的,而不是由外部设备发出的中断请求。因此,软中断在执行过程中一般不会发生中断丢失的情况。中断处理程序的上部分和下半部可以理解为:
- 上半部直接处理硬件请求,也就是硬中断,主要是负责耗时短的工作,特点是快速执行;
- 下半部是由内核触发,也就说软中断,主要是负责上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行;
典型的例子是系统调用(system call)。系统调用允许用户程序向操作系统请求服务,如文件操作、内存分配等。当用户程序执行系统调用时,会切换到特权模式(例如在 x86 架构中切换到内核态),从而允许操作系统执行相应的服务,并在服务完成后再切换回用户程序继续执行。
软中断的执行是通过在用户态(User Mode)切换到内核态(Kernel Mode)来实现的。当用户程序发起一个系统调用或其他特殊操作时,操作系统会通过软中断来处理该请求。在处理软中断时,操作系统会保存当前用户程序的上下文,切换到内核态执行软中断处理程序,完成相应的操作后再切换回用户态继续执行用户程序。由于软中断的触发和处理都是在内核内部进行的,不存在与硬件中断类似的中断丢失问题。
然而,软中断的执行仍然需要消耗一定的CPU时间和资源,因此操作系统需要合理地设计软中断的处理程序,确保软中断的执行效率和稳定性。在一些特定情况下,如果软中断处理程序执行时间过长或者频繁触发软中断,可能会导致系统性能下降。因此,在设计和实现软中断时,需要注意避免可能的性能问题,以确保系统的稳定性和高效性。
当网络数据包到达网卡,网卡通过硬件中断,然后将其传递给内核,内核通过软中断来处理这些数据包。比如将数据包交给应用程序进行解析或处理。
在高流量的网络环境下,软中断可能会频繁发生,尤其是在多核处理器上,不同的 CPU 核心可能会处理不同的软中断。这可以允许系统更好地利用多核处理器的优势,从而提高网络处理能力。
在某些情况下,频繁的软中断可能会导致系统负载过高,进而影响系统的性能。为了解决这个问题,可以通过优化网络配置、调整中断处理的优先级,或者使用更高性能的网卡以及调整网络应用程序的处理逻辑等措施来改善软中断的情况。
3.2.2 硬中断
由外部设备或硬件组件触发的中断。它通常用于处理异步事件,比如来自硬件设备的信号,如定时器中断、键盘输入、网络数据到达等。当硬件触发中断时,CPU会暂停正在执行的指令流,并保存当前执行上下文(通常是保存寄存器状态和程序计数器值),然后跳转到预定义的中断处理程序(Interrupt Service Routine,ISR)来处理该中断。处理完中断后,CPU再恢复之前保存的执行上下文,继续执行被打断的程序。
4. CPU的加法操作
先来看个题:
float32数据类型可以精确的表示下列哪个小数() PS:选项{0.1,0.3,0.8,0.5}
而0.1,0.2,0.3用binaryconcert这个工具都能看到,计算机表示这三个数都只能近似表示。所以在计算机底层0.1 + 0.2 并不等于完整的 0.3
4.1 加减法的统一借由补码实现
计算机中使用补码来表示负数是为了统一加法和减法的操作,使得加减法可以使用相同的硬件电路来进行处理,节约硬件实现成本。
在没有补码的情况下,要计算减法,我们需要执行减法的规则:减去另一个数相当于加上其相反数。所以,我们要计算 9 - 5,我们需要先计算 5 的相反数(取负数)为 -5,然后执行加法:9 + (-5)。
使用补码的情况下,负数的补码是对正数的补码取反再加1。对于上面的例子,我们可以将 5 转换为补码:
- 正数 5 的二进制表示:0101(正数的补码就是它本身)
- 负数 -5 的补码表示:1011(对5的补码取反再加1)
9-5本质就是9和-5的补码相加直接使用补码就省去了转化过程,所以说计算机中的负数存储形式就是补码形式。