持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 10 天,点击查看活动详情
一、基本构成
- 控制器、运算器、存储器、输入设备、输出设备,即冯诺依曼模型;
- 控制单元、逻辑运算单元、寄存器都位于中央控制单元,即cpu内,存储器一般指的内存;
1,寄存器
- 通用寄存器:存放需要进行运算的数据,比如需要进行相加运算的两个数据;
- 程序计数器:存储CPU要执行下一条指令「所在的内存地址」,而不是存储了下一条要执行的指令,此时指令还在内存中;
- 指令寄存器:存放程序计数器指向的指令,也就是指令本身,指令被执行完成之前,指令都存储在这里;
2,CPU位宽
- 32位cpu:一次可以计算32位的数据,即CPU的位宽;
- 64位cpu:一次可以计算64位的数据,可以计算更大的数据;
3,总线
- 总线是用于 CPU 和内存以及其他设备之间的通信,分为以下三种;
- 地址总线:指定CPU将要操作的内存地址;
- 数据总线:用于读写内存的数据;
- 控制总线:用于发送和接收信号,比如中断、设备复位等信号;
- CPU要读写内存数据的时候,先通过「地址总线」来指定内存的地址,然后通过「控制总线」控制是读或写命令,最后通过「数据总线」来传输数据;
4,线路位宽
- 一条地址总线一次只能传输一个位数据,0或1;
- 线路位宽指的是同时能够传输多少位的数据;
- 线路位宽一般需要小于或等于CPU位宽;
- 32位的CPU,32位的线宽,能够访问的最大内存地址为4G;
- 64位的CPU,32位的线宽,一次性能够访问的最大内存地址为4G,4G以上的地址需要多次传输才能访问;
5,CPU执行过程
- CPU读取「程序计数器」存储的内存地址,然后「控制单元」操作「地址总线」指定需要访问的内存地址;
- 通知内存设备准备数据,数据准备好后通过「数据总线」将指令数据传给 CPU;
- CPU收到内存传来的数据后,将这个指令数据存入到「指令寄存器」;
- CPU分析「指令寄存器」中的指令,确定指令的类型和参数;
- 如果是计算类型的指令,就把指令交给「逻辑运算单元」运算;如果是存储类型的指令,则交由「控制单元」执行;
- CPU执行完指令后,「程序计数器」的值自增,表示指向下一条指令;
- 自增的大小,由CPU的位宽决定,32 位CPU,指令是4个字节,需要4个内存地址存放,因此「程序计数器」的值会自增4;
6,指令的类型
- 数据传输类型、运算类型、跳转类型、信号类型、闲置类型;
7,时钟周期
- 脉冲信号转换时间即为一个时钟周期,也等于1/xGHz;
- 一个时钟周期内,CPU仅能完成一个最基本的动作,跟硬件相关,即基频越高,CPU运行越快;
- 程序的CPU执行时间 = CPU时钟周期数 x 时钟周期时间;
- CPU时钟周期数 =「指令数 x 每条指令的平均时钟周期数(Cycles Per Instruction,CPI)
- 指令数与编译器相关,CPI可以通过pipeline优化;
二、存储器
- 从上往下分为:寄存器、CPU缓存,内存,SSD/HDD;
1,寄存器
- 容量最小,读写速度最快,半个时钟周期;
- 主要分为:指令寄存器、程序计数器、通用寄存器;
2,CPU缓存
- 分为L1、L2、L3三级高速缓存,容量递增、速度递减;
- 其中L1、L2为每个CPU核心独有的,L3为不同CPU核心共享;
- L1通常分为数据缓存和指令缓存,通过以下方式查看大小,L2,L3类似,但只有数据缓存:
- 数据:cat /sys/devices/system/cpu/cpu0/cache/index0/size
- 指令:cat /sys/devices/system/cpu/cpu0/cache/index1/size
3,CPU缓存结构
- CPU Cache是由多个Cache Line组成的,Cache Line是CPU从内存读取数据的基本单位;
- Cache Line由有效位、组标记、数据块组成;
- 有效位:用来标记对应的 CPU Cache Line中的数据是否是有效的,如果有效位是0,无论CPU Cache Line 中是否有数据,CPU 都会直接访问内存,重新加载数据;
- 组标记:记录当前CPU Cache Line中存储的数据对应的内存块,可以用来区分不同的内存块;
- 数据块:存放具体的数据;
- 一个内存地址中存放了组标记、索引、偏移量(通过该地址访问数据):
- 组标记:用来对比Cache Line中的组标记,判断是否是我们需要访问的内存块
- 索引:计算对应的 CPU Cache Line 的地址;
- 偏移量:从CPU Cache Line 的数据块中,读取对应的字;
- 查看L1 Cache 一次载入数据的大小:
- cat /sys/devices/system/cpu/cpu0/index0/coherency_line_size (64字节);
- 即一次性载入连续的64字节数据;
- 提升数据缓存命中率:按照内存布局顺序访问,将可以有效的利用 CPU Cache 带来的好处;
- 提升指令缓存命中率:如果分支预测可以预测到接下来要执行的指令,就可以「提前」把这些指令放在指令缓存中,CPU可以直接从Cache读取到指令,执行速度就会很快;
- 多核CPU缓存命中率:L3 Cache是多核心之间共享的,但是L1和L2 Cache是每个核心独有的,如果一个线程在不同核心来回切换,各个核心的缓存命中率就会受到影响,可以把线程绑定在某一个CPU核心上,性能可以得到非常可观的提升;在 Linux 上提供了sched_setaffinity方法,来实现将线程绑定到某个CPU核心这一功能;
4,CPU缓存数据写入内存
- 写直达:
- 如果数据已经在缓存中,先将数据更新到缓存中,再写入到内存里面;
- 如果数据没有在缓存中,就直接把数据更新到内存里面;
- 写操作将会花费大量的时间,性能会受到很大的影响;
- 写回:
- 如果数据已经在缓存中,则把数据直接更新到缓存中,同时标记这个Cache Block为脏,此时不用把数据再写到内存里的;
- 如果数据所对应的Cache Block里存放的是「别的内存地址的数据」的话,就要检查这个缓存块里的数据有没有被标记为脏:
- 如果是脏的话,就要把这个Cache Block里的数据写回到内存,再把当前要写入的数据,先从内存读入到Cache Block里(因为一个数据块中的其他字节可能被其他核心的缓存更新过,先更新到缓存再修改能保证数据一致性),然后再把当前要写入的数据写入到Cache Block,最后也把它标记为脏;
- 如果没有被标记为脏,则就直接将数据写入到这个Cache Block里,然后再把这个Cache Block标记为脏;
- 当发生写操作时,新的数据仅仅被写入Cache Block里,只有当修改过的Cache Block「被替换」时才需要写到内存中,减少了数据写回内存的频率,这样便可以提高系统的性能;
5,缓存一致性问题
- 问题:由于 L1/L2 Cache 是多个核心各自独有的,会带来多核心的缓存一致性问题;
- 原因:采用写回的方式,某个核心对数据的修改可能只同步到L1/L2层,未同步到内存,导致其他核心拿到的数据不一致;
- 解决方法:
- 写传播:某个核心对数据的修改要传递到其他核心;
- 事务串行化:某个核心对数据的操作顺序,必须在其他核心看起来顺序是一样的;
6,缓存一致性问题具体解决方式:
- 1) 总线嗅探
- CPU每时每刻监听总线上的一切活动,不管别的核心的Cache是否缓存相同的数据,都需要发出一个广播事件,会加重总线的负载,并不能保证事务串行化;
- 2)MESI协议
- 由4个状态单词的开头字母缩写表示,分别是:Modified已修改、Exclusive独占、Shared共享、Invalidated已失效;
- 「已修改」:表示这个 Cache Block 里的数据已经被更新过,但是还没有写到内存里,即脏标记;
- 「已失效」:表示这个 Cache Block 里的数据已经失效了,不可以读取该状态的数据;
- 「独占」:表示这个 Cache Block 里的数据是干净的,和内存里面的数据是一致性的,数据只存储在一个 CPU 核心的 Cache 里,而其他 CPU 核心的 Cache 没有该数据,数据可以自由写入,不需要通知其他核心,当其他核心从内存取该数据时,会转换为共享态;
- 「共享」:表示这个 Cache Block 里的数据是干净的,和内存里面的数据是一致性的,修改数据时需要先向其他核心广播一个请求,将其他核心的Cache Line 标记为「无效」状态,再进行数据更新;
7,伪共享
- 问题:因为多个线程同时读写同一个 Cache Line 的不同变量时,本来应该是独占状态的Cache Line逐步转换为共享状态,最后转变为失效状态,而导致 CPU Cache 失效的现象;
- 解决方法:
- 对于多个线程共享的热点数据,即经常会修改的数据,应该避免这些数据刚好在同一个 Cache Line 中,否则就会出现为伪共享的问题;
- 在 Linux 内核中存在 __cacheline_aligned_in_smp 宏定义,是用于解决伪共享的问题,使变量强制Cache Line对齐;
- 也可以采用前置填充的方式使两个变量位于不同的Cache Line;
三、线程调度
- 1,任务优先级以及响应要求,主要分为两种,其中优先级的数值越小,优先级越高:
- 实时任务,对系统的响应时间要求很高,优先级在 0~99 范围内;
- 普通任务,响应时间没有很高的要求,优先级在 100~139 范围内;
- 调度类的优先级如下:Deadline > Realtime > Fair
- 2,实时任务调度策略:
- SCHED_DEADLINE:是按照 deadline 进行调度的,距离当前时间点最近的 deadline 的任务会被优先调度;
- SCHED_FIFO:对于相同优先级的任务,按先来先服务的原则,优先级高的可以「插队」,更早执行;
- SCHED_RR:对于相同优先级的任务,按照时间片轮流着运行,当用完时间片的任务会被放到队列尾部,但是高优先级的任务依然可以抢占低优先级的任务;
- 使用的是deadline调度器和rt调度器;
- 3,普通任务调度策略:
- SCHED_NORMAL:普通任务使用的调度策略;
- SCHED_BATCH:后台任务的调度策略,不和终端进行交互,在不影响其他需要交互的任务,可以适当降低它的优先级;
- 采用的是CFS完全公平调度,任务队列采用红黑树描述;
- 4,优先级调整
- nice 的值能设置的范围是-20~19,值越低表明优先级越高,因此 -20 是最高优先级,19则是最低优先级,默认优先级是0;
- 事实上,nice 值并不是表示优先级,而是表示优先级的修正数值,它与优先级(priority)的关系为:priority(new) = priority(old) + nice;
- nice -n -5 /usr/bin/python:启动任务时指定优先级;
- renice -5 -p pid:修改运行中的任务的优先级;
- nice 调整的是普通任务的优先级,所以不管怎么缩小 nice 值,任务永远都是普通任务;
四、软中断
- Linux 系统为了解决中断处理程序执行过长和中断丢失的问题,将中断过程分成了两个阶段,分别是「上半部和下半部分」:
- 上半部直接处理硬件请求,也就是硬中断,主要是负责耗时短的工作,特点是快速执行;
- 下半部是由内核触发,也就是软中断,主要是负责上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行;
- 硬中断是会打断 CPU 正在执行的任务,然后立即执行中断处理程序,而软中断是以内核线程的方式执行,并且每一个 CPU 都对应一个软中断内核线程,名字通常为「ksoftirqd/CPU 编号」,比如 0 号 CPU 对应的软中断内核线程的名字是 ksoftirqd/0;、
- 一些内核自定义事件也属于软中断,比如内核调度等、RCU 锁(内核里常用的一种锁)等;
- cat /proc/softirqs:查看系统软中断运行情况;
- cat /proc/interrupts:查看系统硬中断运行情况;
- NET_RX 表示网络接收中断,NET_TX 表示网络发送中断、TIMER 表示定时中断、RCU 表示 RCU 锁中断、SCHED 表示内核调度中断;
- watch -d cat /proc/softirqs 命令查看中断次数的变化速率;
五、二进制码
- 负数表示
- int类型是32位的,最高位是作为「符号标志位」,正数的符号位是 0,负数的符号位是 1,剩余的31位则表示二进制数据;
- 负数在计算机中是以「补码」表示的,补码就是把正数的二进制全部取反再加1;
- 如果负数不是使用补码的方式表示,在做基本加减法运算的时候,需要先判断是否为负数,而用了补码的表示方式,对于负数的加减法操作,实际上是和正数加减法操作一样的,可以直接运算;
- 小数的十进制与二进制转换
- 整数除二取余,小数乘二取整;
- 小数点后的指数幂为负数;
- 乘二取整会出现无限循环的情况,在有限的精度情况下,最大化接近原先值的二进制数,但会造成精度缺失的情况;
- 小数的存储
- 符号位:表示数字是正数还是负数,为 0 表示正数,为 1 表示负数;
- 指数位:指定了小数点在数据中的位置,指数位的长度越长则数值的表达范围就越大;
- 尾数位:小数点右侧的数字,也就是小数部分,尾数的长度决定了这个数的精度,如果要表示精度更高的小数,就要提高尾数位的长度;
- double 的尾数部分是 52 位,float 的尾数部分是 23 位,因此在十进制中double的有效数字是15
16 位,float的有效数字是 78 位,这些有效位是包含整数部分和小数部分; - double 的指数部分是 11 位,而 float 的指数位是 8 位,意味着 double 相比 float 能表示更大的数值范围;
- 指数可以是负数或正数,而有符号整数的计算是比无符号整数麻烦的,所以在实际存储指数的时候,需要把指数转换成无符号整数,float 的指数偏移量是 127;
六、内核
- 作为应用连接硬件设备的桥梁,让应用程序只需关心与内核交互,不用关心硬件的细节;
- 内核的4个基本功能:
- 管理进程、线程:决定哪个进程、线程使用 CPU,即进程调度;
- 管理内存:决定内存的分配和回收,即内存管理;
- 管理硬件设备:为进程与硬件设备之间提供通信能力,即硬件通信;
- 提供系统调用:如果应用程序要运行更高权限运行的服务,那么就需要有系统调用,它是用户程序与操作系统之间的接口;
- 内核具有很高的权限,可以控制 cpu、内存、硬盘等硬件,而应用程序具有的权限很小,因此大多数操作系统,把内存分成了两个区域:
- 内核空间:这个内存空间只有内核程序可以访问;
- 用户空间:这个内存空间专门给应用程序使用;
- 内核架构:
- 宏内核:宏内核的特征是系统内核的所有模块,比如进程调度、内存管理、文件系统、设备驱动等,都运行在内核态,Linux系统;
- 微内核:微内核架构的内核只保留最基本的能力,比如进程调度、虚拟机内存、中断等,把一些应用放到了用户空间,比如驱动程序、文件系统等,服务与服务之间是隔离的,提高了操作系统的稳定性和可靠性,但会带来频繁的内核态切换,鸿蒙系统;
- 混合内核:架构有点像微内核,内核里面会有一个最小版本的内核,其他模块会在这个基础上搭建,实现的时候会跟宏内核类似,也就是把整个内核做成一个完整的程序,大部分服务都在内核中,就像是宏内核的方式包裹着一个微内核,Windows;