冯诺依曼模型:中央处理器(CPU)、内存、输⼊设备、输出设备、总线。
那 CPU 执⾏程序的过程如下:
第⼀步,CPU 读取「程序计数器」的值,这个值是指令的内存地址,然后 CPU 的「控制单元」操作「地址总线」指定需要访问的内存地址,接着通知内存设备准备数据,数据准备好后通过「数据总线」将指令数据传给 CPU,CPU 收到内存传来的数据后,将这个
指令数据存⼊到「指令寄存器」。
第⼆步,CPU 分析「指令寄存器」中的指令,确定指令的类型和参数,如果是计算类型的指令,就把指令交给「逻辑运算单元」运算;如果是存储类型的指令,则交由「控制单元」执⾏;
第三步,CPU 执⾏完指令后,「程序计数器」的值⾃增,表示指向下⼀条指令。这个⾃增的⼤⼩,由 CPU 的位宽决定,⽐如 32 位的 CPU,指令是 4 个字节,需要 4 个内存地址存放,因此「程序计数器」的值会⾃增 4;
每⼀次脉冲信号⾼低电平的转换就是⼀个周期,称为时钟周期。
不同的指令需要的时钟周期是不同的。
对于程序的 CPU 执⾏时间,我们可以拆解成 CPU 时钟周期数(CPU Cycles)和时钟周期时间(Clock Cycle Time)的乘积。
时钟周期时间就是我们前⾯提及的 CPU 主频。
只有运算⼤数字的时候,64 位 CPU 的优势才能体现出来,否则和 32 位 CPU的计算性能相差不⼤。
64 位 CPU 可以寻址更⼤的内存空间。
操作系统会分成 32 位操作系统、64 位操作系统,其代表意义就是操作系统中程序的指令是多少位。
SRAM 之所以叫「静态」存储器,是因为只要有电,数据就可以保持存在。
DRAM 存储⼀个 bit 数据,只需要⼀个晶体管和⼀个电容就能存储,但是因为数据会被存储在
电容⾥,电容会不断漏电,所以需要「定时刷新」电容,才能保证数据不会被丢失,这就是
DRAM 之所以被称为「动态」存储器的原因,只有不断刷新,数据才能被存储起来。
L1 Cache 通常会分为「数据缓存」和「指令缓存」,一般为64字节。
L1 Cache 和L2 Cache 都是每个 CPU 核⼼独有的,⽽ L3 Cache 是多个 CPU 核⼼共享的。
⽐如,有⼀个 int array[100] 的数组,当载⼊ array[0] 时,由于这个数组元素的⼤⼩在内存只占 4 字节,不⾜ 64 字节,CPU 就会顺序加载数组元素到 array[15]。
⼀个内存的访问地址,包括组标记、CPU Line 索引、偏移量这三种信息。⽽对于 CPU Cache ⾥的数据结构,则是由索引 + 有效位 + 组标记 + 数据块组成。
CPU分支预测器:如果分⽀预测可以预测到接下来要执⾏ if ⾥的指令,还是 else指令的话,就可以「提前」把这些指令放在指令缓存中,这样 CPU可以直接从 Cache 读取到指令,于是执⾏速度就会很快。在 C/C++ 语⾔中编译器提供了 likely 和 unlikely 这两种宏进行分支预测。
在 Linux 上提供了 sched_setaffinity ⽅法,来实现将线程绑定到某个 CPU 核⼼这⼀功能。
保持内存与 Cache ⼀致性最简单的⽅式是,把数据同时写⼊内存和Cache中,这种⽅法称为写直达(Write Through)。
在写回机制中,当发⽣写操作时,新的数据仅仅被写⼊ Cache Block ⾥,只有当修改过的Cache Block「被替换」时才需要写到内存中,减少了数据写回内存的频率。只有在缓存不命中,同时数据对应的 Cache 中的 Cache Block 为脏标记的情况下,才会将数据写到内存中,⽽在缓存命中的情况下,则在写⼊后 Cache 后,只需把该数据对应的 Cache Block 标记为脏即可。
解决缓存一致性:写传播、事务的串行化。
总线嗅探:CPU 需要每时每刻监听总线上的⼀切活动,但是不管别的核⼼的 Cache 是否缓存相同的数据,都需要发出⼀个⼴播事件。
MESI(Modified、Exclusive、Shared、Invalidate)协议基于总线嗅探机制实现了事务串形化。
因为多个线程同时读写同⼀个 Cache Line 的不同变量时,⽽导致 CPU Cache 失效的现象称为伪共享(False Sharing)。解决:通过__cacheline_aligned_in_smp设置Cache Line对齐地址、内存填充。
Linux中任务优先级的数值越⼩,优先级越⾼。
在CFS算法调度的时候,会优先选择vruntime少的任务。在计算虚拟运⾏时间vruntime还要考虑普通任务的权重值。
nice级别越低,权重值就越⼤,vruntime权重越小,优先被调度。nice 值并不是表示优先级,⽽是表示优先级的修
正数值,priority(new) = priority(old) + nice。nice 调整的是普通任务的优先级,所以不管怎么缩⼩ nice 值,任务永远都是普通任务。
每个 CPU 都有⾃⼰的运⾏队列(Run Queue, rq),⽤于描述在此 CPU 上所运⾏的所有进程,其队列包含三个运⾏队列,Deadline 运⾏队列 dl_rq、实时任务运⾏队列 rt_rq和 CFS 运⾏队列 csf_rq,其中 csf_rq 是⽤红⿊树来描述的,按 vruntime ⼤⼩来排序的,最左侧的叶⼦节点,就是下次会被调度的任务。这⼏种调度类是有优先级的,优先级如下:Deadline > Realtime > Fair,因此实时任务总是会⽐普通任务优先被执⾏。普通任务的调度类是 Fail,由 CFS 调度器来进⾏管理。
中断请求的响应程序,也就是中断处理程序,要尽可能快的执⾏完,这样可以减少对正常进程运⾏调度地影响。所以Linux中中断处理分为上半部和下半部。软中断是以内核线程的⽅式执⾏的。每个 CPU 核⼼都对应着⼀个内核线程ksoftirqd。
单⽚机的 CPU 是直接操作内存的「物理地址」。
内存分段和内存分⻚:
内存分段:
内存分页:
分⻚是把整个虚拟和物理内存空间切成⼀段段固定尺⼨的⼤⼩。
当进程访问的虚拟地址在⻚表中查不到时,系统会产⽣⼀个缺⻚异常,进⼊系统内核空间分配物理内存(struct page)、更新进程⻚表,最后再返回⽤户空间,恢复进程的运⾏。
只有在程序运⾏中,需要⽤到对应虚拟内存⻚⾥⾯的指令和数据时,再加载到物理内存⾥⾯去。
在分⻚机制下,虚拟地址分为两部分,⻚号和⻚内偏移。⻚号作为⻚表的索引,⻚表包含物理⻚每⻚所在物理内存的基地址。
多级页表:
如果某个⼀级⻚表的⻚表项没有被⽤到,也就不需要创建这个⻚表项对应的⼆级⻚表了,即可以在需要时才创建⼆级⻚表。
段⻚式内存管理(内存分段 + 内存分页):
TLB:
TLB(Translation Lookaside Buffer) ,通常称为⻚表缓存、转址旁路缓存、快表等。
文件映射段(堆、栈之间):
包括动态库、共享内存等,从低地址开始向上增长。
mmap可以在文件映射段动态分配内存。
挂起状态:
描述进程没有占⽤实际的物理内存空间的情况(物理内存空间换出到硬盘),这个状态就是挂起状态。这跟阻塞状态是不⼀样,阻塞状态是等待某个事件的返回(占用物理内存空间)。
挂起状态可以分为两种:
阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;
就绪 挂起状态:进程在外存(硬盘),但只要进⼊内存,即刻⽴刻运⾏。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K6kBU1R3-1621864827924)(C:\Users\NiGo\AppData\Roaming\Typora\typora-user-images\image-20210522213942175.png)]
PCB:
CPU 上下⽂切换:
线程是进程当中的⼀条执⾏流程。
用户线程:
内核线程:
LWP:
pid、tid:
五种调度原则:
⾼响应⽐优先调度算法:
每次进⾏进程调度时,先计算「响应⽐优先级」,然后把「响应⽐优先级」最⾼的进程投⼊运⾏,「响应⽐优先级」的计算公式:优先级 = (等待时间 + 要求服务时间) / 要求服务时间。
匿名管道是特殊的⽂件,只存在于内存,不存于⽂件系统中。
消息队列:
共享内存:
共享内存的机制,就是拿出⼀块虚拟地址空间来,映射到相同的物理内存中。
i + 1:
从内存取出i值后,放入到寄存器;对寄存器中的i值+1;把寄存器中的i值放回内存。
哲学家就餐:
方案一:信号量
可能死锁,所有哲学家同时拿左手的刀叉。
方案二:信号量 + 互斥锁
效率低。
方案三:信号量 + 偶数先拿左、奇数先拿右
避免死锁。
方案四:信号量 + 互斥锁 + state数组
⼀个哲学家只有在两个邻居都没有进餐时,才可以进⼊进餐状态。
LRU:
在每次访问内存时都必须要更新「整个链表」(主要在寻找已分配的页),开销大。
时钟⻚⾯置换算法:
把所有的⻚⾯都保存在⼀个类似钟⾯的「环形链表」中,⼀个表针指向最⽼的⻚⾯。
磁盘调度算法:
寻道的时间是磁盘访问最耗时的部分,如果请求顺序优化的得当,必然可以节省⼀些不必要的寻道时间,从⽽提⾼磁盘的访问性能。
最短寻道时间优先:
可能存在某些请求的饥饿。
扫描算法:
磁头在⼀个⽅向上移动,访问所有未完成的请求,直到磁头到达该⽅向上的最后的磁道,才调换⽅向,这就是扫描(Scan)算法。
不足:
中间部分的磁道会⽐较占便宜,中间部分相⽐其他部分响应的频率会⽐较多,也就是说每个磁道的响应频率存在差异。
循环扫描算法:
循环扫描算法相⽐于扫描算法,对于各个位置磁道响应频率相对⽐较平均。
LOOK 与 C-LOOK算法:
磁头在移动到「最远的请求」位置,然后⽴即反向移动。
操作系统在打开⽂件表中维护着打开⽂件的状态和信息:
文件的存储:
连续空间存放⽅式:
⾮连续空间存放⽅式:
链表方式:
隐式链表:
显式链表:
索引方式:
EXT2:
Linux 系统的 ext ⽂件系统就是采⽤了哈希表,来保存⽬录的内容。
⽬录查询是通过在磁盘上反复搜索完成,需要不断地进⾏ I/O 操作,开销较⼤。所以,为了减少 I/O 操作,把当前使⽤的⽂件⽬录缓存在内存。
硬链接是不可⽤于跨⽂件系统的,软链接可以(文件的内容是另一个文件的路径)。
同步I/O、异步I/O:
同步I/O:阻塞I/O、非阻塞I/O、基于非阻塞I/O的多路复用;
异步I/O:异步I/O(aio_read)。
实际上,⽆论是阻塞 I/O、⾮阻塞 I/O,还是基于⾮阻塞 I/O 的多路复⽤都是同步调⽤。因为它们在 read 调⽤时,内核将数据从内核空间拷⻉到应⽤程序空间,过程都是需要等待的,也就是说这个过程是同步的。
真正的异步 I/O 是「内核数据准备好」和「数据从内核态拷⻉到⽤户态」这两个过程都不⽤等待。
当我们发起 aio_read 之后,就⽴即返回,内核⾃动将数据从内核空间拷⻉到应⽤程序空间,这个拷⻉过程同样是异步的,内核⾃动完成的,和前⾯的同步操作不⼀样,应⽤程序并不需要主动发起拷⻉动作。
设备控制器:
CPU与设备通信:
端⼝ I/O:
每个控制寄存器被分配⼀个 I/O 端⼝,可以通过特殊的汇编指令操作这些寄存器,⽐如 in/out 类似的指令。
内存映射 I/O:
将所有控制寄存器映射到内存空间中,这样就可以像读写内存⼀样读写数据缓冲区。
设备驱动程序:
通用块层:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0XM0mg1e-1621864827927)(file:///C:\Users\NiGo\AppData\Roaming\Tencent\Users\1275810355\QQ\WinTemp\RichOle\VVPSGVXTDW2MNKA8Y09_DU0.png)]
用户态到内核态的开销:
task_struct用通用的方式来描述进程, thread_info保存了特定体系结构的汇编代码段需要访问的那部分进程的数据。
thread_info中嵌入指向task_struct的指针。
零拷贝技术:
PageCache(一种磁盘高速缓存):
异步I/O机制:
POSIX AIO:
通过使用pthread库创建用户态多线程的方式实现异步IO的接口。
使用多线程实现异步IO的效率和可扩展性太差。
Linux AIO:
io_uring:
io_uring的原理是让用户态进程与内核通过一个共享内存的无锁环形队列进行高效交互。
共享内存:
为了最大程度的减少系统调用过程中的参数内存拷贝,io_uring采用了将内核态地址空间映射到用户态的方式。
无锁环形队列:
io_uring使用了单生产者单消费者的无锁队列来实现用户态程序与内核对共享内存的高效并发访问,生产者只修改队尾指针,消费者只修改队头指针,不会互相阻塞。
内存屏障:
CPU避免内存访问延迟最常见的技术是将指令管道化,然后尽量重排这些管道的执行以最大化利用缓存,从而把因为缓存未命中引起的延迟降到最小。(并且还有store buffer)
Store Barrier:
Store屏障,是x86的”sfence“指令,强制所有在store屏障指令之前的store指令,都在该store屏障指令执行之前被执行,并把store缓冲区(store buffer)的数据都刷到CPU缓存。这会使得程序状态对其它CPU可见,这样其它CPU可以根据需要介入。
Load Barrier:
Load屏障,是x86上的”ifence“指令,强制所有在load屏障指令之后的load指令,都在该load屏障指令执行之后被执行,并且一直等到load缓冲区被该CPU读完才能执行之后的load指令。这使得从其它CPU暴露出来的程序状态对该CPU可见,这之后CPU可以进行后续处理。
Full Barrier:
Full屏障,是x86上的”mfence“指令,复合了load和save屏障的功能。
Java内存模型:
Java内存模型中volatile变量在写操作之后会插入一个store屏障,在读操作之前会插入一个load屏障。一个类的final字段会在初始化后插入一个store屏障,来确保final字段在构造函数初始化完成并可被使用时可见。
I/O多路复用:
select:
poll:
epoll:
epoll 在内核⾥使⽤红⿊树来跟踪进程所有待检测的⽂件描述字,减少了内核和⽤户空间⼤量的数据拷⻉和内存分配。
epoll 使⽤事件驱动的机制,内核⾥维护了⼀个链表来记录就绪事件,当某个socket 有事件发⽣时,通过回调函数内核会将其加⼊到这个就绪事件列表中。
ET模式和⾮阻塞 I/O:
如果⽂件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那⾥,程序就没办法继续往下执⾏。所以,程序会⼀直执⾏ I/O 操作,直到系统调⽤(如 read 和 write )返回错误,错误类型为EAGAIN 或 EWOULDBLOCK 。
边缘触发的效率⽐⽔平触发的效率要⾼,因为边缘触发可以减少 epoll_wait 的系统调⽤次数(上下文切换)。
在Linux下,select() 可能会将⼀个 socket ⽂件描述符报告为 “准备读取”,⽽后续的读取块却没有。例如,当数据已经到达,但经检查后发现有错误的校验和⽽被丢弃时,就会发⽣这种情况。也有可能在其他情况下,⽂件描述符被错误地报告为就绪。因此,在不应该阻塞的 socket 上使⽤ O_NONBLOCK 可能更安全。
Reactor 是⾮阻塞同步⽹络模式,感知的是就绪可读写事件;Proactor 是异步⽹络模式, 感知的是已完成的读写事件。
网络性能指标:
socket信息:推荐使⽤性能更好的 ss 命令,而不是netstat命令。
协议栈:ss/netstat -s。
网络吞吐率和PPS:sar。
带宽:ethtool。