一、硬件结构

CPU执行程序步骤

图灵机:纸带(由连续格子组成,格子可以写入字符),读写头(读写纸带),读写头上有一些部件(存储单元、控制单元、运算单元)

冯诺依曼模型

中央处理器(CPU)、内存、输⼊设备、输出设备、总线

​ 内存:数据存储单位二进制(bit)最小存储单位字节(byte),1byte = 8 bit。地址从0开始编号(像数组)

​ CPU(Central Processing Unit):32位一次可以计算四个字节(64位计算8个),此处位称为CPU的位宽。CPU内部还有一些组件如寄存器(多种类)、控制单元(控制CPU工作)和逻辑运算单元(计算),CPU里的内存可以提高运算速度。

​ 寄存器:通用(存放要运算的数据),程序计数器(存储下一条指令的内存地址),指令寄存器(存放指令本身)

​ 总线:用于设备间通信。分为多种:地址(CPU要操作的内存地址),数据总线(读写内存),控制总线(发送接收信号如中断复位等)

​ 输入输出设备:如鼠标键盘

线路位宽与 CPU 位宽

​ 一位一位传输称为串行,增加线路一次多传些数据,则称并行。需要一次操作4G的内存则需要32条地址总线,因为2^32=4G(有那么多的地址)(字节是最小存储单位),一条线路可以处理两个地址(0和1)。如果是64位数字用32位CPU计算,要分成低位32和高位32(最好不要这样)

程序执行流程

CPU指令周期(直到程序结束)

  1. CPU读【程序计数器】(存储指令内存地址),【控制单元】操作【地址总线】访问地址,【数据总线】传数据,放入【指令寄存器】
  2. CPU分析指令进行相应操作(计算=>逻辑运算单元,存储=>控制单元)
  3. 【程序计数器】自增(32位自增4字节)

​ 程序 => 汇编语言 => 机器语言(计算机指令)

​ 程序运行时专门区域存放数据(数据段),指令和数据分开区域存放(正文段)

指令

(P15图)

​ 以MIPS为例:32位,高6位代表操作码,剩下26表示内容,分为R、I和J

​ R:算术逻辑操作,读写数据寄存器地址,后面可能有位移量功能码

​ I:数据传输、条件分支等。

​ J:用在跳转

​ 构造指令称为指令的编码,执行的时候解析指令成为指令的解码

​ 指令四阶段:FetchDecodeExecuteStore。(指令周期Instrution Cycle

​ 指令在存储器,通过控制器取出指令,通过控制器译码,指令执行通过运算器,跳转则通过控制器

​ 类型:数据传输、运算类型、跳转类型、信号类型(如中断)、闲置类型(如空转)

​ 程序CPU执行时间 = CPU时钟周期数(CPU主频) * 时钟周期时间 = 指令数 * CPI * 时钟周期时间 (CPI:每条指令的平均时钟周期数)

64位相比32位的优势

​ 只有运算⼤数字的时候,64 位 CPU 的优势才能体现出来,否则和 32 位 CPU 的计算性能相差不大。64位可以寻址更大空间。

​ 硬件的 64 位和 32 位指的是 CPU 的位宽,软件的 64 位和 32 位指的是指令的位宽。

存储器

存储器层次架构

​ 寄存器:32位存4字节,64位存8字节。同样在CPU中

​ CPU Cache(高速缓存):使用SRAM(Static Random-Access Memory,静态随机存储器)。通常分为L1(2到4个时钟周期)、L2(10到20个时钟周期)、L3(20到60个时钟周期)三层(三层高速缓存),L1距离CPU近读写速度更快存储空间更小,通常分成指令缓存和数据缓存,可以通过指令来查看其大小(L2L3也一样),L1 Cache和L2 Cache是每个CPU核心独有的,而L3 Cache是多个CPU共享的。

​ 内存:使用DRAM(Dynamic Random Access Memory,动态随机存取存储器)的芯片,动态是因为电容材料会不断漏电,要不断刷新才能将数据存储起来。访问速度大概在200-300个时钟周期

​ SSD / HDD硬盘:固态硬盘,机械硬盘。

时钟周期是CPU主频的倒数

每个存储器只跟相邻的存储设备打交道。读取数据的时候逐层往下去找(如果在当前层次找不到的话)。存储层级结构也形成了缓存体系

存储器速度越快、能耗越高、材料越贵,越往存储金字塔上走越是这个特点。

CPU Cache

​ Cache的数据从内存中读取过来一块一块的,这种一块一块的数据成为Cache Line(缓存块)。大小可以通过查询参数coherency_line_size来获取。在内存中这样的数据称为内存块(Block)

​ 无论数据是否存放到 Cache 中,CPU 都是先访问 Cache, 只有当 Cache 中找不到数据时,才会去访问内存,并把内存中的数据读⼊到 Cache 中,CPU 再从 CPU Cache 读取数据。(后续的内存,硬盘都是这样的原理)

直接映射Cache(Direct Mapped Cache):把内存块的地址基于取模运算,映射在一个CPU Line中(如划分为32个内存块的内存和有8个CPU Line的CPU Cache,7号CPU Line可以映射7号、15号、23号和31号的内存卡,因为n % 8 = 7)。为了区别不同的内存块,在Line中还会存储一个组标记(Tag),记录当前存储数据对应的内存块

​ CPU Line储存的信息:组标记Tag,实际数据Data,有效位Valid bit(如果为0,无论CPU Line中是否有数据,CPU都会直接访问内存)

​ CPU读取CPU Cache中的数据片段(统称为字(Word))时,需要一个通过偏移量(Offset)来寻找

​ CPU Cache中的数据结构由索引 + 有效位 + 组标记 + 数据块 组成

​ 通过内存地址的索引计算CPU Cache中的索引(即CPU Line地址),判定有效位,对比组标记,根据偏移量获取信息。如果某个环节出问题则访问内存重新加载数据

提升CPU速度

​ 在CPU Cache中找到数据缓存命中,提高命中率可以有效让CPU跑的更快(代码性能更好)

​ 遍历 array[i][j]的速度比array[j][i]快,因为在内存中是横着存的。按照内存布局顺序访问数据可以提升代码性能

分支预测器:如果分⽀预测可以预测到接下来要执行 if ⾥的指令,还是 else 指令的话,就可以「提前」把这些指令放在指令缓存中,这样 CPU 可以直接从 Cache 读取到指令,于是执行速度就会很快。所以像C中提供了likely和unlikely的宏,可以用于if语句中(如果对数据情况有一定了解的话)

​ 把线程绑定在某一个CPU核心上可以防止因为切换核心而降低缓存命中率(Linux上的sched_setaffinity方法)

CPU缓存一致性

Cache数据写入

写直达:把数据同时写入内存和Cache中

写回:发送写操作时仅仅将新的数据写入Cache Block中,只有当Cache Block被替换时才写到内存中。数据如果在CPU Cache中改变则标记为脏(Dirty),此时无需写到内存;如果不在,则看当前的Block是不是脏的,是的话则写回内存否则直接执行下一步,接下来把当前要写的数据写入Block并标记为脏的

缓存一致性

​ 多核心带来的缓存一致性问题,即数据在A核心修改了但还未写入内存,B核心访问了内存中相应的值。

​ 解决方案:某个CPU核心的Cache数据更新时,要传播到其他核心的Cache,称为写传播。某个CPU核心对数据的操作顺序在其他核心看起来顺序需要相同,称为事务的串行化。实现事物串行化需做到两点:首先是操作要同步到其他CPU,其次是引入的概念

​ 具体实现

总线嗅探:把事件广播通知其他核心,CPU时刻监听总线上的一切活动。

MESI协议Modified(已修改),Exclusive(独占),Shared,Invalidate(失效),用这四个状态来标记Cache Line的态。

  • 已修改则为脏标记(M);
  • 独占和共享表示Cache Block中的数据是干净的(即和内存一致),独占(E)的时候不需要通知其他核心,当其他核心访问了这部分数据时会要求把这部分状态变成共享(S),并且在此时把数据写到内存中(让其他核心读到最新的数据);
  • 共享状态下修改数据前要先通过广播将其他核心的Cache中的Cache Line标记为无效(I),然后再更新。由此可以发现独占或已修改状态下不需要广播(其他已经是I了),有效减少了总线带宽压力。

CPU如何执行任务

CPU如何写数据

⼀个 CPU ⾥通常会有多个 CPU 核心,比如上图中的 1 号和 2 号 CPU 核心,并 且每个 CPU 核心都有⾃⼰的 L1 Cache 和 L2 Cache,而 L1 Cache 通常分为 dCache(数据缓存) 和 iCache(指令缓存),L3 Cache 则是多个核心共享的,这就是 CPU 典型的缓存层次

为什么需要CPU Cache:因为访问CPU Cache速度比访问内存速度快一百遍,可以将Cache作为CPU与内存之间的缓存层,以减少对内存的访问频率

CPU从内存读取数据到Cache不是按字节,而是按块读取,这些块成为CPU Line(从内存到Cache的单位),可以通过查看coherency_line_szie参数来查看Coche Line的大小,CPU Line是CPU从内存读取数据到Cache的单位

当从内存中取单元到cache中时,会一次取一个cacheline大小的内存区域到cache中,然后存进相应的cacheline中。

内存 <=> Cache <=> CPU

伪共享问题

问题描述:

  1. CPU核心1和核心2读入了包含变量A和B的Cache LIne,核心1只对A进行读写,核心2只对B进行读写
  2. 因为CPU从内存读取数据单位到Cache的单位是Cache Line,而A和B同属一个Cache Line,因此核心1加载A后,Cache Line会被标记为“独占”
  3. 2号核心读取B之后,Cache Line状态转化为“共享”
  4. 1号核心修改完A后,因为Cache Line处于共享状态,所以通过总线发消息给2号核心,通知2号核心把Cache中的Cache Line标记为已失效,1号核心把他对应的Cache Line变成“已修改”
  5. 2号核心要修改B时发现Cache Line是“已失效”,因此核心1要线把Cache Line写回内存,2号核心重新读取,然后再去修改B,并把状态标记为“已修改”,自然核心1的Cache Line此时被标记为“已失效”

因此伪共享的定义可以理解为,多个线程同时读写同⼀个 Cache Line 的不同变量时,而导致 CPU Cache 失效

避免方法:

在 Linux 内核中存在 __cacheline_aligned_in_smp 宏定义,是⽤于解决伪共享的问题。多核中该宏定义是Cache Line的大小,单核中该宏定义是空的。在前面的例子中为了防止伪共享问题,可以将变量B设置为Cache Line对齐地址,可以保证A和B不在一个Cache Line中(空间换时间的思路)

应用层面比如RingBufferPad设置7个long变量前置填充和7个long变量后置填充,因为一个Cache Line64字节,一个long变量8个字节,保证了RingBufferPad不会和其他变量去共享Cache Line,自然不存在伪共享的问题

CPU 如何选择线程的?

在 Linux 内核中,进程和线程都是⽤ tark_struct 结构体表示,区别在于线程的 tark_struct 结构体⾥部分资源是共享了进程已创建的资源,因此线程也称轻量级进程。

根据任务的优先级以及响应要求,主要分为两种,其中优先级的数值越小,优先级越高:实时任务(099),普通任务(100139)

调度类

DeadLine:用于实时任务;采用SCHED_DEADLINE策略,根据任务的优先级以及响应要求,主要分为两种,其中优先级的数值越 小,优先级越高

Realtime:用于实时任务;采用SCHED_FIFO策略,对于相同优先级的任务,按先来先服务的原则,但是优先级更高的任 务,可以抢占低优先级的任务,也就是优先级高的可以插队;也可采用SCHED_RR策略,对于相同优先级的任务,轮流着运⾏,每个任务都有⼀定的时间片,当用完时间片的任务会被放到队列尾部,以保证相同优先级任务的公平性,但是高优先级的任务依然可以抢占低优先级的任务

Fair:应用于普通任务;SCHED_NORMAL:普通任务使用的调度策略;SCHED_BATCH:后台任务的调度策略,不和终端进行交互,因此在不影响其他需要交互的任务,可以适当降低它的优先级

完全公平调度

为每个任务安排⼀个虚拟 运⾏时间 vruntime,如果⼀个任务在运⾏,其运⾏的越久,该任务的 vruntime ⾃然就会越大,⽽没有被运⾏的任务,vruntime 是不会变化的。CFS 算法调度的时候,会优先选择 vruntime 少的任务(可以理解为动态调整任务的权重)

在计算虚拟运行时间vruntime的时候还会考虑权重值,权重值不是优先级的值,内核中有个nice值和权重值的转化表,二者负相关。

虚拟运行时间vruntime += 实际运行时间delta_exec * NICE_0_LOAD / 权重

NICE_0_LOAD理解为一个常量,高权重的任务因此会被优先调度

CPU运行队列

如同上述的三个调度类中就有三个运行队列(dl_rq,rt_rq,csf_rq(根据vruntime排序的红黑树)),调度类优先级排序:Deadline > Realtime > Fair

调整优先级

默认情况下是普通任务,普通任务可以通过调整nice值(-20 ~ 19)来让高优先级的任务执行更多时间,值越低优先级越高

nice 值并不是表示优先级,⽽是表示优先级的修正数值,它与优先级(priority)的关系是这样的:priority(new) = priority(old) + nice

在启动任务时可以指定nice值,可以通过renice来调整正在运行任务的nice值。但是不管怎么缩小nice值,任务都是普通任务。要求实时性的话要改变任务的优先级以及调度策略。

软中断

中断

  • 系统用来响应硬件设备请求的⼀种机制,操作系统收到硬件的中断请求,会打断正在执行的进程,然后调用内核中的中断处理程序来响应请求。

  • 中断请求的响应程序,也就是中断处理程序,要尽快的执行完,这样可以减少对正常进程运⾏调度地影响。

  • 在响应中断时可能临时关闭中断,意味着必须先把当前中断处理程序执行完,中断可能会丢失。

Linux 系统为了解决中断处理程序执⾏过长和中断丢失的问题,将中断过程分成了两个阶段,分别是「上半部和下半部分」。

  • 上半部⽤来快速处理中断,⼀般会暂时关闭中断请求,主要负责处理跟硬件紧密相关或者 时间敏感的事情。直接处理硬件请求
  • 下半部⽤来延迟处理上半部未完成的工作,⼀般以「内核线程」的方式运行。由内核触发,完成上半部分未完成的部分,可以用ps查到

比如网卡收到网络包后会通过硬件中断通知内核,内核调用中断处理程序来响应。上部分把网卡数据读到内存,更新寄存器状态,下部分把网络包逐层解析传给应用层

可以通过查看 /proc/softirqs 的 内容来知晓「软中断」的运⾏情况,以及 /proc/interrupts 的 内容来知晓「硬中断」的运⾏情况

/proc/softirqs内容:

  • 第一列代表软中断类型
  • 同一种中断在不同CPU上累计次数差不多
  • 系统的中断次数变化速率才是要关注的,可以使⽤ watch -d cat /proc/softirqs 命令查看中断次数的变化速率

ps无法获取内核线程的命令行参数,因此⼀般来说名字在中括号⾥到,都可以认为是内核线程。

如何定位软中断 CPU 使⽤率过⾼的问题?

可以使用top命令查看当前系统的软中断情况,如果在 top 命令发现,CPU 在软中断上的使⽤率⽐较⾼,⽽且 CPU 使⽤率最⾼的进程也是 软中断 ksoftirqd 的时候,这种⼀般可以认为系统的开销被软中断占据了。

要知道是哪种软中断类型导致的,我们可以使⽤ watch -d cat /proc/softirqs 命令 查看每个软中断类型的中断次数的变化速率

如果发现 NET_RX ⽹络接收中断次数的变化速率过快,接下⾥就可以使⽤ sar -n DEV 查看 ⽹卡的⽹络包接收速率情况

在通过 tcpdump 抓包,分析这些包的来源,如果是非法的地址,可以考虑加防⽕墙, 如果是正常流量,则要考虑硬件升级等

1.7 为什么 0.1 + 0.2 不等于 0.3 ?

为什么负数要用补码

数字在计算机中是以「补码」表示的,负数的补码就是把正数的⼆ 进制全部取反再加 1

如果负数不是使用补码的⽅式表示,则在做基本对加减法运算的时候,还需要多⼀步操作来 判断是否为负数,而用了补码的表示方式,对于负数的加减法操作,实际上是和正数加减法操作⼀样的。

⼗进制⼩数与⼆进制的转换

⼩数部分的转 换不同于整数部分,它采⽤的是乘 2 取整法,将⼗进制中的⼩数部分乘以 2 作为⼆进制的⼀位,然后继续取小数部分乘以 2 作为下⼀位,直到不存在小数为止。

因此存在小数二进制表达无限循环的可能(比如0.1),由于计算机的资源是有限的,所以是没办法⽤⼆进制精确的表示 0.1,只能⽤「近似值」来表 示,就是在有限的精度情况下,最⼤化接近 0.1 的⼆进制数,于是就会造成精度缺失的情况。

计算机是怎么存小数的?

计算机存储⼩数的采⽤的是浮点数,名字⾥的「浮 点」表示⼩数点是可以浮动的,现在绝⼤多数计算机使⽤的浮点数,⼀般采⽤的是 IEEE 制定的国际标准,这种标准形式如下图

符号位 指数位 尾数
  • 符号位:表示数字是正数还是负数,为 0 表示正数,为 1 表示负数;

  • 指数位:指定了⼩数点在数据中的位置,指数可以是负数,也可以是正数,指数位的⻓度 越⻓则数值的表达范围就越⼤;

  • 尾数位:⼩数点右侧的数字,也就是⼩数部分,⽐如⼆进制 1.0011 x 2^(-2),尾数部分 就是 0011,⽽且尾数的⻓度决定了这个数的精度,因此如果要表示精度更高的小数,则就要提高尾数位的⻓度;

⽤ 32 位来表示的浮点数,则称为单精度浮点数,也就是我们编程语⾔中的 float 变量, 用64 位来表示的浮点数,称为双精度浮点数,也就是 double 变量

float

符号位1位 指数位8位 尾数位23位

double

符号位1位 指数位11位 尾数位52位

以10.625为例:

10.625 => 1010.101 => 1.010101(右移三位+3),小数点右侧的数字就是尾数位,后面有剩余则补0,float默认偏移量是127,加上偏移量3后130则为指数位的值.

IEEE规定单精度浮点指数取值范围是-126~127,偏移量保证了指数部份不会出现负数,在实际存储指数的时候需要把驶数转换成无符号整数

float 的⼆进制浮点数转换成十进制时转换公式如下:

(-1)^(符号位) * (1 + 尾数位) * 2 ^ (指数 - 127)

因为有的⼩数⽆法可以⽤「完整」的⼆进制来表示,所以计算机⾥只能采⽤近似数 的⽅式来保存,那两个近似数相加,得到的必然也是⼀个近似数。因此可能出现0.1+0.2 != 0.3的情况,要根据精度舍入

posted @   XCCX0824  阅读(113)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示