一、硬件结构
-
冯诺依曼模型:中央处理器(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。
二、操作系统结构
- SMP 的意思是对称多处理,代表着每个 CPU 的地位是相等的,对资源的使⽤权限也是相同的,多个 CPU 共享同⼀个内存,每个 CPU 都可以访问完整的内存和硬件资源。
- Window的内核设计是混合型内核,Linux的内核设计是宏内核,华为的鸿蒙操作系统的内核架构是微内核。
三、内存管理
-
单⽚机的 CPU 是直接操作内存的「物理地址」。
-
内存分段和内存分⻚:
-
内存分段:
- 分段机制下的虚拟地址由两部分组成,段选择⼦和段内偏移量。
- 段选择⼦就保存在段寄存器⾥⾯。段选择⼦⾥⾯最重要的是段号,⽤作段表的索引。段表⾥⾯保存的是这个段的基地址、段的界限和特权等级等。
- 不足:内存碎片(外部碎片)、内存交换的效率低(linuxswap)。
-
内存分页:
-
分⻚是把整个虚拟和物理内存空间切成⼀段段固定尺⼨的⼤⼩。
-
当进程访问的虚拟地址在⻚表中查不到时,系统会产⽣⼀个缺⻚异常,进⼊系统内核空间分配物理内存(struct page)、更新进程⻚表,最后再返回⽤户空间,恢复进程的运⾏。
-
只有在程序运⾏中,需要⽤到对应虚拟内存⻚⾥⾯的指令和数据时,再加载到物理内存⾥⾯去。
-
在分⻚机制下,虚拟地址分为两部分,⻚号和⻚内偏移。⻚号作为⻚表的索引,⻚表包含物理⻚每⻚所在物理内存的基地址。
-
多级页表:
如果某个⼀级⻚表的⻚表项没有被⽤到,也就不需要创建这个⻚表项对应的⼆级⻚表了,即可以在需要时才创建⼆级⻚表。
-
-
段⻚式内存管理(内存分段 + 内存分页):
- 先将程序划分为多个有逻辑意义的段,也就是前⾯提到的分段机制;接着再把每个段划分为多个⻚,也就是对分段划分出来的连续空间,再划分固定⼤⼩的⻚。
- 地址结构就由段号、段内⻚号和⻚内位移三部分组成。
-
-
TLB:
TLB(Translation Lookaside Buffer) ,通常称为⻚表缓存、转址旁路缓存、快表等。
-
文件映射段(堆、栈之间):
包括动态库、共享内存等,从低地址开始向上增长。
mmap可以在文件映射段动态分配内存。
四、进程与线程
-
挂起状态:
-
描述进程没有占⽤实际的物理内存空间的情况(物理内存空间换出到硬盘),这个状态就是挂起状态。这跟阻塞状态是不⼀样,阻塞状态是等待某个事件的返回(占用物理内存空间)。
-
挂起状态可以分为两种:
阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;
就绪 挂起状态:进程在外存(硬盘),但只要进⼊内存,即刻⽴刻运⾏。
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K6kBU1R3-1621864827924)(C:\Users\NiGo\AppData\Roaming\Typora\typora-user-images\image-20210522213942175.png)]
-
PCB:
- 在操作系统中,是⽤进程控制块(process control block,PCB)数据结构来描述进程的。
- 通常是通过链表的⽅式进⾏组织,把具有相同状态的进程链在⼀起,组成各种队列。如就绪队列、阻塞队列。
-
CPU 上下⽂切换:
- CPU 上下⽂切换分成:进程上下⽂切换、线程上下⽂切换和中断上下⽂切换。
- 进程的上下⽂切换不仅包含了虚拟内存、栈、全局变量等⽤户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
- 当两个线程不是属于同⼀个进程,则线程上下文切换的过程就跟进程上下⽂切换⼀样。
-
线程是进程当中的⼀条执⾏流程。
-
用户线程:
- ⽤户线程的优点:
- 每个进程都需要有它私有的线程控制块(TCB)列表,⽤来跟踪记录它各个线程状态信息(PC、栈指针、寄存器),TCB 由⽤户级线程库函数来维护,可⽤于不⽀持线程技术的操作系统;
- ⽤户线程的切换也是由线程库函数来完成的,⽆需⽤户态与内核态的切换,所以速度特别快。
- ⽤户线程的缺点:
- 由于操作系统不参与线程的调度,如果⼀个线程发起了系统调⽤⽽阻塞,那进程所包含的⽤户线程都不能执⾏。
- 当⼀个线程开始运⾏后,除⾮它主动地交出 CPU 的使⽤权,否则它所在的进程当中的其他线程⽆法运⾏,因为⽤户态的线程没法打断当前运⾏中的线程,它没有这个特权,只有操作系统才有,但是⽤户线程不是由操作系统管理的。
- 由于时间⽚分配给进程,故与其他进程⽐,在多线程执⾏时,每个线程得到的时间⽚较少,执⾏会⽐较慢。
- ⽤户线程的优点:
-
内核线程:
- 内核线程的优点:
- 在⼀个进程当中,如果某个内核线程发起系统调⽤⽽被阻塞,并不会影响其他内核线程的运⾏;
- 分配给线程,多线程的进程获得更多的 CPU 运⾏时间;
- 内核线程的缺点:
- 在⽀持内核线程的操作系统中,由内核来维护进程和线程的上下⽂信息,如 PCB 和TCB;
- 线程的创建、终⽌和切换都是通过系统调⽤的⽅式来进⾏,因此对于系统来说,系统开销⽐较⼤;
- 内核线程的优点:
-
LWP:
- 轻量级进程 (LWP, light weight process) 是一种由内核支持的用户线程。它是基于内核线程的高级抽象,因此只有先支持内核线程,才能有 LWP 。每一个LWP可以支持一个或多个用户线程,每个 LWP 由一个内核线程支持。内核线程与LWP之间的模型实际上就是《操作系统概念》上所提到的一对一线程模型。在这种实现的操作系统中, LWP 相当于用户线程。 由于每个 LWP 都与一个特定的内核线程关联,因此每个 LWP 都是一个独立的线程调度单元。即使有一个 LWP 在系统调用中阻塞,也不会影响整个进程的执行。
- Linux下使用进程模拟的线程(难怪gettid返回值类型是pid_t)。
-
pid、tid:
- tid实际上是内核(线程)中可调度对象的标识符,而pid是共享内存和fds(进程)的可调度对象组的标识符。
- 当一个进程只有一个线程时,pid和tid总是相同的。
-
五种调度原则:
- CPU 利⽤率:调度程序应确保 CPU 是始终匆忙的状态,这可提⾼ CPU 的利⽤率;
- 系统吞吐量:吞吐量表示的是单位时间内 CPU 完成进程的数量,⻓作业的进程会占⽤较⻓的 CPU 资源,因此会降低吞吐量,相反,短作业的进程会提升系统吞吐量;
- 周转时间:周转时间是进程运⾏和阻塞时间总和,⼀个进程的周转时间越⼩越好;
- 等待时间:这个等待时间不是阻塞状态的时间,⽽是进程处于就绪队列的时间,等待的时间越⻓,⽤户越不满意;
- 响应时间:⽤户提交请求到系统第⼀次产⽣响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。
-
⾼响应⽐优先调度算法:
每次进⾏进程调度时,先计算「响应⽐优先级」,然后把「响应⽐优先级」最⾼的进程投⼊运⾏,「响应⽐优先级」的计算公式:优先级 = (等待时间 + 要求服务时间) / 要求服务时间。
-
匿名管道是特殊的⽂件,只存在于内存,不存于⽂件系统中。
-
消息队列:
- 消息队列是保存在内核中的消息链表,在发送数据时,会分成⼀个⼀个独⽴的数据单元,也就是消息体(数据块)。
- 消息体是⽤户⾃定义的数据类型,消息的发送⽅和接收⽅要约定好消息体的数据类型,所以每个消息体都是固定⼤⼩的存储块,不像管道是⽆格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
- 消息队列⽣命周期随内核,匿名管道随进程。
- 不足:
- 消息队列不适合⽐较⼤数据的传输,在 Linux 内核中,会有两个宏定义MSGMAX 和 MSGMNB ,它们以字节为单位,分别定义了⼀条消息的最⼤⻓度和⼀个队列的最⼤⻓度。
- 消息队列通信过程中,存在⽤户态与内核态之间的数据拷⻉开销。
-
共享内存:
共享内存的机制,就是拿出⼀块虚拟地址空间来,映射到相同的物理内存中。
-
i + 1:
从内存取出i值后,放入到寄存器;对寄存器中的i值+1;把寄存器中的i值放回内存。
-
哲学家就餐:
-
方案一:信号量
可能死锁,所有哲学家同时拿左手的刀叉。
-
方案二:信号量 + 互斥锁
效率低。
-
方案三:信号量 + 偶数先拿左、奇数先拿右
避免死锁。
-
方案四:信号量 + 互斥锁 + state数组
⼀个哲学家只有在两个邻居都没有进餐时,才可以进⼊进餐状态。
-
五、调度算法
-
LRU:
在每次访问内存时都必须要更新「整个链表」(主要在寻找已分配的页),开销大。
-
时钟⻚⾯置换算法:
把所有的⻚⾯都保存在⼀个类似钟⾯的「环形链表」中,⼀个表针指向最⽼的⻚⾯。
-
磁盘调度算法:
-
寻道的时间是磁盘访问最耗时的部分,如果请求顺序优化的得当,必然可以节省⼀些不必要的寻道时间,从⽽提⾼磁盘的访问性能。
-
最短寻道时间优先:
可能存在某些请求的饥饿。
-
扫描算法:
-
磁头在⼀个⽅向上移动,访问所有未完成的请求,直到磁头到达该⽅向上的最后的磁道,才调换⽅向,这就是扫描(Scan)算法。
-
不足:
中间部分的磁道会⽐较占便宜,中间部分相⽐其他部分响应的频率会⽐较多,也就是说每个磁道的响应频率存在差异。
-
-
循环扫描算法:
循环扫描算法相⽐于扫描算法,对于各个位置磁道响应频率相对⽐较平均。
-
LOOK 与 C-LOOK算法:
磁头在移动到「最远的请求」位置,然后⽴即反向移动。
-
六、文件系统
-
操作系统在打开⽂件表中维护着打开⽂件的状态和信息:
- ⽂件指针:系统跟踪上次读写位置作为当前⽂件位置指针,这种指针对打开⽂件的某个进程来说是唯⼀的;
- ⽂件打开计数器:⽂件关闭时,操作系统必须重⽤其打开⽂件表条⽬,否则表内空间不够⽤。因为多个进程可能打开同⼀个⽂件,所以系统在删除打开⽂件条⽬之前,必须等待最后⼀个进程关闭⽂件,该计数器跟踪打开和关闭的数量,当该计数为 0 时,系统关闭⽂件,删除该条⽬;
- ⽂件磁盘位置:绝⼤多数⽂件操作都要求系统修改⽂件数据,该信息保存在内存中,以免每个操作都从磁盘中读取;
- 访问权限:每个进程打开⽂件都需要有⼀个访问模式(创建、只读、读写、添加等),该信息保存在进程的打开⽂件表中,以便操作系统能允许或拒绝之后的 I/O 请求。
-
文件的存储:
-
连续空间存放⽅式:
- 不足:连续分配的磁盘碎⽚和⽂件动态扩展。
-
⾮连续空间存放⽅式:
-
链表方式:
-
隐式链表:
- ⽂件要以「隐式链表」的⽅式存放的话,实现的⽅式是⽂件头要包含「第⼀块」和「最后⼀块」的位置,并且每个数据块⾥⾯留出⼀个指针空间,⽤来存放下⼀个数据块的位置。
- 不足:⽆法直接访问数据块,只能通过指针顺序访问⽂件,以及数据块指针消耗了⼀定的存储空间。隐式链接分配的稳定性较差,系统在运⾏过程中由于软件或者硬件错误导致链表中的指针丢失或损坏,会导致⽂件数据的丢失。
-
显式链表:
- 指把⽤于链接⽂件各数据块的指针,显式地存放在内存的⼀张链接表中,该表在整个磁盘仅设置⼀张,每个表项中存放链接指针,指向下⼀个数据块号。
- 内存中的这样⼀个表格称为⽂件分配表(File Allocation Table,FAT)。
- 由于查找记录的过程是在内存中进⾏的,因⽽不仅显著地提⾼了检索速度,⽽且⼤⼤减少了访问磁盘的次数。但也正是整个表都存放在内存中的关系,它的主要的缺点是不适⽤于⼤磁盘。
-
-
索引方式:
- ⽂件头需要包含指向「索引数据块」的指针,这样就可以通过⽂件头知道索引数据块的位置,再通过索引数据块⾥的索引信息找到对应的数据块。
- 不足:存储索引带来的开销。
-
-
EXT2:
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6FTCePIz-1621864827926)(C:\Users\NiGo\AppData\Roaming\Typora\typora-user-images\image-20210523152321633.png)]
- 块组的内容如下:
- 超级块,包含的是⽂件系统的重要信息,⽐如 inode 总个数、块总个数、每个块组的inode 个数、每个块组的块个数等等。
- 块组描述符表,包含⽂件系统中各个块组的状态,⽐如块组中空闲块和 inode 的数⽬等,每个块组都包含了⽂件系统中「所有块组的组描述符信息」。
- 数据位图和 inode 位图, ⽤于表示对应的数据块或 inode 是空闲的,还是被使⽤中。
- inode 列表,包含了块组中所有的 inode,inode ⽤于保存⽂件系统中与各个⽂件和⽬录相关的所有元数据。
- 数据块,包含⽂件的有⽤数据。
- 超级块和块组描述符表是全局信息的原因:
- 如果系统崩溃破坏了超级块或块组描述符,有关⽂件系统结构和内容的所有信息都会丢失。如果有冗余的副本,该信息是可能恢复的。
- 通过使⽂件和管理数据尽可能接近,减少了磁头寻道和旋转,这可以提⾼⽂件系统的性能。
-
-
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 之后,就⽴即返回,内核⾃动将数据从内核空间拷⻉到应⽤程序空间,这个拷⻉过程同样是异步的,内核⾃动完成的,和前⾯的同步操作不⼀样,应⽤程序并不需要主动发起拷⻉动作。
-
七、设备管理
-
设备控制器:
- 为了屏蔽设备之间的差异,每个设备都有⼀个叫设备控制器(Device Control) 的组件。
- 设备控制器⾥有芯⽚,它可执⾏⾃⼰的逻辑,也有⾃⼰的寄存器,⽤来与 CPU 进⾏通信,相当于一个小CPU。
- 控制器是有三类寄存器,它们分别是状态寄存器(Status Register)、 命令寄存器(Command Register)以及数据寄存器(Data Register)。
-
CPU与设备通信:
-
端⼝ I/O:
每个控制寄存器被分配⼀个 I/O 端⼝,可以通过特殊的汇编指令操作这些寄存器,⽐如 in/out 类似的指令。
-
内存映射 I/O:
将所有控制寄存器映射到内存空间中,这样就可以像读写内存⼀样读写数据缓冲区。
-
-
设备驱动程序:
- 虽然设备控制器屏蔽了设备的众多细节,但每种设备的控制器的寄存器、缓冲区等使⽤模式都是不同的,所以为了屏蔽「设备控制器」的差异,引⼊了设备驱动程序。
- 设备控制器不属于操作系统范畴,它是属于硬件,⽽设备驱动程序属于操作系统的⼀部分。
- 通常,设备驱动程序初始化的时候,要先注册⼀个该设备的中断处理函数。
-
通用块层:
- 通⽤块层是处于⽂件系统和磁盘驱动中间的⼀个块设备抽象层,它主要有两个功能:
- 第⼀个功能,向上为⽂件系统和应⽤程序,提供访问块设备的标准接⼝,向下把各种不同的磁盘设备抽象为统⼀的块设备,并在内核层⾯,提供⼀个框架来管理这些设备的驱动程序;
- 第⼆功能,通⽤层还会给⽂件系统和应⽤程序发来的 I/O 请求排队,接着会对队列重新排序、请求合并等⽅式,也就是 I/O 调度,主要⽬的是为了提⾼磁盘读写的效率。
- 通⽤块层是处于⽂件系统和磁盘驱动中间的⼀个块设备抽象层,它主要有两个功能:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0XM0mg1e-1621864827927)(file:///C:\Users\NiGo\AppData\Roaming\Tencent\Users\1275810355\QQ\WinTemp\RichOle\VVPSGVXTDW2MNKA8Y09_DU0.png)]
- 为了提⾼⽂件访问的效率,会使⽤⻚缓存、索引节点缓存、⽬录项缓存等多种缓存机制,⽬的是为了减少对块设备的直接调⽤。
八、网络系统
-
用户态到内核态的开销:
- 当程序中有系统调用语句,程序执行到系统调用时,首先使用类似int 80H的软中断指令,保存现场,去系统调用,在内核态执行,然后恢复现场,每个进程都会有两个栈,一个内核态栈和一个用户态栈。当int中断执行时就会由用户态栈转向内核态栈。系统调用时需要进行栈的切换。而且内核代码对用户不信任,需要进行额外的检查。系统调用的返回过程有很多额外工作,比如检查是否需要调度等。
- 系统调用一般都需要保存用户程序得上下文(context), 在进入内核的时候需要保存用户态的寄存器,在内核态返回用户态的时候会恢复这些寄存器的内容。这是一个开销的地方。 如果需要在不同用户程序间切换的话,那么还要更新cr3寄存器,这样会更换每个程序的虚拟内存到物理内存映射表的地址,也是一个比较高负担的操作。
-
task_struct用通用的方式来描述进程, thread_info保存了特定体系结构的汇编代码段需要访问的那部分进程的数据。
-
thread_info中嵌入指向task_struct的指针。
-
零拷贝技术:
- 传统:4次。
- mmap + write(CPU参与,3次):
- mmap() 系统调⽤函数会直接把内核缓冲区⾥的数据「映射」到⽤户空间,这样,操作系统内核与⽤户空间就不需要再进⾏任何的数据拷⻉操作。
- 减少⼀次数据拷⻉的过程。
- sendfile(CPU参与,3次):
- 可以减少⼀次系统调⽤,也就减少了 2 次上下⽂切换的开销。
- SG-DMA(CPU不参与,2次):
- 对于⽀持⽹卡⽀持 SG-DMA 技术的情况下,sendfile具体过程:
- 第⼀步,通过 DMA 将磁盘上的数据拷⻉到内核缓冲区⾥;
- 第⼆步,缓冲区描述符和数据⻓度传到 socket 缓冲区,这样⽹卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷⻉到⽹卡的缓冲区⾥,此过程不需要将数据从操作系统内核缓冲区拷⻉到 socket 缓冲区中,这样就减少了⼀次数据拷⻉。
- 这就是所谓的零拷⻉(Zero-copy)技术,因为我们没有在内存层⾯去拷⻉数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进⾏传输的。
- 对于⽀持⽹卡⽀持 SG-DMA 技术的情况下,sendfile具体过程:
-
PageCache(一种磁盘高速缓存):
- PageCache 的优点主要是两个:缓存最近被访问的数据、预读功能。
- 在传输⼤⽂件(GB 级别的⽂件)的时候,PageCache 会不起作⽤,那就⽩⽩浪费DMA 多做的⼀次数据拷⻉,造成性能的降低,即使使⽤了 PageCache 的零拷⻉也会损失性能。
- 原因:
- PageCache 由于⻓时间被⼤⽂件占据,其他「热点」的⼩⽂件可能就⽆法充分使⽤到PageCache,于是这样磁盘读写的性能就会下降了;
- PageCache 中的⼤⽂件数据,由于没有享受到缓存带来的好处,但却耗费 DMA 多拷⻉到 PageCache ⼀次(局部性差)。
-
异步I/O机制:
-
POSIX AIO:
-
通过使用pthread库创建用户态多线程的方式实现异步IO的接口。
-
使用多线程实现异步IO的效率和可扩展性太差。
-
-
Linux AIO:
- 最大的局限性是只支持direct IO,这不仅导致程序无法使用cache,更让程序无法使用普通的malloc/new等方式分配的内存用于IO,而必须使用mmap方式直接分配4K对齐的页。
-
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:
- select 实现多路复⽤的⽅式是,将已连接的 Socket 都放到⼀个⽂件描述符集合,然后调⽤select 函数将⽂件描述符集合拷⻉到内核⾥,让内核来检查是否有⽹络事件产⽣,检查的⽅式很粗暴,就是通过遍历⽂件描述符集合的⽅式,当检查到有事件产⽣后,将此 Socket 标记为可读或可写, 接着再把整个⽂件描述符集合拷⻉回⽤户态⾥,然后⽤户态还需要再通过遍 历的⽅法找到可读或可写的 Socket,然后再对其处理。
- 对于 select 这种⽅式,需要进⾏ 2 次「遍历」⽂件描述符集合,⼀次是在内核态⾥,⼀个次是在⽤户态⾥ ,⽽且还会发⽣ 2 次「拷⻉」⽂件描述符集合,先从⽤户空间传⼊内核空间,由内核修改后,再传出到⽤户空间中。
- select 使⽤固定⻓度的 BitsMap,表示⽂件描述符集合,⽽且所⽀持的⽂件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE(fd_set) 限制, 默认最⼤值为 1024 ,只能监听 0~1023 的⽂件描述符。
-
poll:
- poll 不再⽤ BitsMap 来存储所关注的⽂件描述符,取⽽代之⽤动态数组,以链表形式来组织,突破了 select 的⽂件描述符个数限制,当然还会受到系统⽂件描述符限制。
-
epoll:
-
epoll 在内核⾥使⽤红⿊树来跟踪进程所有待检测的⽂件描述字,减少了内核和⽤户空间⼤量的数据拷⻉和内存分配。
-
epoll 使⽤事件驱动的机制,内核⾥维护了⼀个链表来记录就绪事件,当某个socket 有事件发⽣时,通过回调函数内核会将其加⼊到这个就绪事件列表中。
-
ET模式和⾮阻塞 I/O:
如果⽂件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那⾥,程序就没办法继续往下执⾏。所以,程序会⼀直执⾏ I/O 操作,直到系统调⽤(如 read 和 write )返回错误,错误类型为EAGAIN 或 EWOULDBLOCK 。
-
-
边缘触发的效率⽐⽔平触发的效率要⾼,因为边缘触发可以减少 epoll_wait 的系统调⽤次数(上下文切换)。
-
在Linux下,select() 可能会将⼀个 socket ⽂件描述符报告为 “准备读取”,⽽后续的读取块却没有。例如,当数据已经到达,但经检查后发现有错误的校验和⽽被丢弃时,就会发⽣这种情况。也有可能在其他情况下,⽂件描述符被错误地报告为就绪。因此,在不应该阻塞的 socket 上使⽤ O_NONBLOCK 可能更安全。
-
-
Reactor 是⾮阻塞同步⽹络模式,感知的是就绪可读写事件;Proactor 是异步⽹络模式, 感知的是已完成的读写事件。
九、Linux命令
-
网络性能指标:
- 带宽,表示链路的最⼤传输速率,单位是 b/s (⽐特 / 秒),带宽越⼤,其传输能⼒就越强。
- 延时,表示请求数据包发送后,收到对端响应,所需要的时间延迟。不同的场景有着不同的含义,⽐如可以表示建⽴ TCP 连接所需的时间延迟,或⼀个数据包往返所需的时间延迟。
- 吞吐率,表示单位时间内成功传输的数据量,单位是 b/s(⽐特 / 秒)或者 B/s(字节 /秒),吞吐率受带宽限制,带宽越⼤,吞吐率的上限才可能越⾼。
- PPS,全称是 Packet Per Second(包 / 秒),表示以⽹络包为单位的传输速率,⼀般⽤来评估系统对于⽹络的转发能⼒。
- ⽹络的可⽤性,表示⽹络能否正常通信;
- 并发连接数,表示 TCP 连接数量;
- 丢包率,表示所丢失数据包数量占所发送数据组的⽐率;
- 重传率,表示重传⽹络包的⽐例;
-
socket信息:推荐使⽤性能更好的 ss 命令,而不是netstat命令。
协议栈:ss/netstat -s。
网络吞吐率和PPS:sar。
带宽:ethtool。