基本硬件结构:

基本结构:运算器、控制器(内存)、存储器、输入设备、输出设备

内存

线性储存,基本存储单位是byte,1byte为8字节,每个byte对应一个内存地址

CPU

  • 32位的CPU———4字节———4GB寻址空间
  • 64位的CPU———8字节———寻址空间极大
CPU寄存器:主要作用是存储计算时的数据
  • 通用寄存器:用于存放需要进行运算的数据
  • 程序计数器:存储CPU要执行下一条指令所在的内存地址
  • 指令寄存器:存放当前正在执行的指令本身
CPU的指令周期
  1. Fetch:CPU通过程序计数器获取对应内存地址
  1. Decode:CPU进行指令解码
  1. Execution:CPU执行指令
  1. Store:CPU将结果存回寄存器或者存入内存
notion image

总线

  • 地址总线:指定内存地址
  • 控制总线:控制读写命令
  • 数据总线:传输数据

指令的执行速度

notion image
CPU的时钟周期数=指令数×CPI(每条指令的平均时钟周期)
时钟周期时间=1/CPU主频(超频)

基本硬件问题:

64位相较于32位CPU的优势在哪?
硬件位数指得是CPU的位宽,运算超过32位的大数字时,64位的优势才会显现出来,并且32位的地址总线是32,64位的地址总线是48
软件32位和64位的区别?
软件位数指的是指令位宽,32位的寄存器存不下64位的指令

存储器的层级结构:

CPU 寄存器:最快,相当于大脑思考的东西
32位cpu存储4字节,64位cpu存储8字节。
2GHz主频的CPU,时钟周期是1/2G,也就是0.5ns。
CPU Cache(高速缓存):次快,相当于大脑储存的记忆,有电数据在,断电数据丢失
通常可以分为 L1、L2、L3 这样的三层高速缓存
notion image
L1高速缓存:访问速度几乎和寄存器一样快,速度在 2~4 个时钟周期
L2高速缓存:速度在 10~20 个时钟周期
L3高速缓存:速度在 20~60个时钟周期
内存:次慢,相当于桌面的资料
DRAM 的数据访问电路和刷新电路都比 SRAM 更复杂,所以访问的速度会更慢,速度大概在 200~300 个 时钟周期之间
SSD硬盘:最慢,相当于书架上的书
速度最慢,但是它相比其余的优点是断电后数据还是存在的
CPU Cache 的数据是从内存中读取过来的,它是以一小块一小块读取数据的,这种一小块一小块的数据,称为Cache Line。
Cache Line的大小也就是Cache的数据大小,其内部结构是:索引 + 有效位 + 组标记 + 数据块

CPU Cache问题:

如果内存中的数据已经在 CPU Cache 中了,那 CPU 访问一个内存地址的时候,会经历什么?
notion image
  1. 根据内存地址中索引信息,计算在 CPU Cache 中的索引
  1. 找到对应 CPU Cache Line 后,判断 CPU Cache Line 中的有效位,有效继续执行
  1. 对比内存地址中组标记和 CPU Cache Line 中的组标记,确认 CPU Cache Line 中的数据是我们要访问的内存数据
  1. 根据内存地址中偏移量信息,从 CPU Cache Line 的数据块中,读取对应的字
如何写出让CPU跑的更快的代码?
当 CPU 访问数据的时候,先是访问 CPU Cache,如果缓存命中的话,则直接返回数据,就不用每次都从内存读取数据了。
因此,缓存命中率越高,代码的性能越好,CPU L1 Cache 分为数据缓存和指令缓存,因而需要分别提高它们的缓存命中率。
  • 对于数据缓存,遍历数据的时候应该按照内存布局的顺序操作
  • 对于指令缓存,有规律的条件分支语句能够让CPU的分支预测器发挥作用,进一步提高执行效率
  • 多核CPU可以考虑把线程绑定到CPU某个核心

CPU缓存一致性:

CPU Cache的写回方法

写直达
把数据同时写入内存和 Cache 中,这种方法称为写直达
写回
当发生写操作时,新的数据仅仅被写入 Cache Block 里,只有当修改过的 Cache Block「被替换」时才需要写到内存中
即缓存命中,Cache Block标为脏标记(表示已被修改但尚未写回)
缓存不命中,缓存行被替换出缓存时,将数据写入内存

CPU多核的缓存一致性(因为写回的策略导致内存和Cache可能不同步)

  • 写传播:某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache
总线嗅探(Bus SnoopingCPU 需要每时每刻监听总线上的一切活动
  • 事务的串行化:某个 CPU 核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的(加锁)
MESI协议(基于总线嗅探机制实现了事务的串行化)
  • Modified,已修改:脏标记
  • Exclusive,独占:这个时候 Cache Block 里的数据和内存里面的数据是一致性的。
  • Shared,共享:干净数据这个时候 Cache Block 里的数据和内存里面的数据是一致性的
  • Invalidated,已失效:表示的是这个 Cache Block 里的数据已经失效了,不可读取该状态的数据
「共享」状态代表着相同的数据在多个 CPU 核心的 Cache 里都有,所以当我们要更新 Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态,然后再更新当前 Cache 里面的数据。

什么是伪共享,如何解决?

这种因为多个线程同时读写同一个 Cache Line 的不同变量时,而导致 CPU Cache 失效的现象称为伪共享(False Sharing
解决办法:空间换时间,将变量不设置在同一个Cache Line中即可

什么是软中断?

Linux系统为了解决中断处理程序执行过长和中断丢失的问题,将中断过程分成了两个阶段,分别是「上半部和下半部分」
  • 上半部用来快速处理中断,直接处理硬件请求,也就是硬中断,主要是负责耗时短的工作,特点是快速执行。
  • 下半部用来延迟处理上半部未完成的工作,下半部是由内核触发,也就说软中断,通常都是耗时比较长的事情,特点是延迟执行。

内核态/用户态:两种不同的CPU执行模式

  1. 用户态(User Mode):
      • 在用户态下,程序的执行权限受到限制,它们无法直接操作底层资源,只能通过操作系统提供的调用来请求服务。
  1. 内核态(Kernel Mode):
      • 内核态最高的访问权限和控制权。在内核态下,操作系统内核可以直接访问系统硬件和底层资源。
      • 负责管理系统资源、处理中断、调度进程、分配内存等关键任务
3.系统态和内核态的交互方式
  • 系统调用:通过API接口使应用程序转入内核态
  • 管道,消息队列,套接字,信号,共享内存
  • I/O控制,可以从用户态转为内核态
  • 内核态和用户态的代码也可以通过文件系统进行通信

内存管理:

虚拟内存是什么?

操作系统为每个进程都分配独立的一套虚拟地址,操作系统会提供一种机制:将不同进程的虚拟地址和不同内存的物理地址映射起来。
  • 我们程序所使用的内存地址叫做虚拟内存地址Virtual Memory Address
  • 实际存在硬件里面的空间地址叫物理内存地址Physical Memory Address
notion image
 

管理虚拟地址与物理地址的关系

内存分段:程序是由若干个逻辑分段组成的,不同的段有不同的属性的,所以就用分段的形式把这些段分离出来。
分段机制下的虚拟地址由两部分组成:段选择因子,段内偏移量
notion image
段选择因子保存在段寄存器里,段选择因子里最重要的是段号,用作段表的索引
段表里面保存的是这个段的基地址,段的界限和特权等级
段内偏移量应该位于0和段界限之间,如果段内偏移量合法,那就将段基地址+段内偏移量得到物理地址
内存分段的不足:外部内存碎片
内存碎片:1G的物理内存,游戏占用了512MB内存,浏览器占用了128MB内存,音乐占用了256MB内存
这时候我们关闭浏览器,无法再打开一个200kb的程序,因为是两段128kb
处理方法:内存交换,将音乐的256写入硬盘,关闭浏览器后再装载回来紧跟着512MB即可
内存分段的不足:交换效率低
因为要把数据写到硬盘上,如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿
内存分页整个虚拟和物理内存空间切成一段段固定尺寸的大小
虚拟地址与物理地址之间通过页表来映射,如下图:
notion image
当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行
页表的内存交换的效率就相对比较高,因为换出时会把「最近没被使用」的内存页面写在硬盘上,需要的时候,再加载进来。所以,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间。此外,更进一步的只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去
分页机制下的虚拟地址由两部分组成:页号页内偏移
notion image
页号作为页表的索引
页表包含物理页每页所在物理内存的基地址
页内偏移合法,那就将页基地址+页内偏移得到物理地址
页表的不足:100 个进程的话,就需要 400MB 的内存来存储页表,这是非常大的内存了,占用内存大
多级页表:应用局部性原理将页表分为一级页表和二级页表,64位需要4级页表
如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表
从页表的性质来看,保存在内存中的页表承担的职责是将虚拟地址翻译成物理地址。假如虚拟地址在页表中找不到对应的页表项,计算机系统就不能工作了。所以页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有 100 多万个页表项来映射,而二级分页则只需要 1024 个页表项(此时一级页表覆盖到了全部虚拟地址空间,二级页表在需要时创建)。
TLB:一个存放程序最常访问的页表项的缓存
有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。
TLB 的命中率其实是很高的,因为程序最常访问的页就那么几个。
段页式内存管理:内存分页和内存分段组合起来,提高了内存空间的利用率
  • 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;
  • 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页
地址结构就由段号、段内页号和页内位移组成
  • 第一次访问段表,得到页表起始地址;
  • 第二次访问页表,得到物理页号;
  • 第三次将物理页号与页内位移组合,得到物理地址。
notion image
 

为什么要有虚拟内存和内存管理?

1.虚拟内存可以使运行内存超过物理内存大小,没有被经常使用到的内存,我们可以把它换出到物理内存之外。
2.每个进程的虚拟内存空间就是相互独立的,解决了多进程之间地址冲突的问题。
3.页表中存在一些标记属性的比特,在内存访问方面,提供了更好的安全性。

Malloc分配内存机制:分配的是虚拟内存

  • 方式一:通过 brk() 系统调用从堆分配内存,内存小于 128 KB
malloc 通过 brk() 方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用
  • 方式二:通过 mmap() 系统调用在文件映射区域分配内存,内存大于 128 KB
malloc 通过 mmap() 方式申请的内存,free 释放内存的时候,会把内存归还给操作系统,内存得到真正的释放

线程/进程管理

进程的状态

多进程实际上是并发,即快速处理多个任务而不是同时处理多个任务
进程的活动期间遵守原则:运行前一定要就绪
notion image
运行状态:该时刻进程占用CPU
就绪状态:只缺少CPU调度
阻塞状态:进程在等待某一事件发生而暂时停止运行
创建状态:被创建时的状态
结束状态:进程从系统消失的状态

进程的控制结构

操作系统中,是用进程控制块(PCB)数据结构来描述进程的,PCB 是进程存在的唯一标识
PCB里面具体包含:
  • 进程描述信息:进程标识符,用户标识符
  • 进程控制信息:进程当前状态,进程优先级
  • 资源分配清单:有关虚拟地址的信息,I/O设备信息
  • CPU相关信息
PCB通过链表的方式进行组织,把相同状态的进程链在一起,组成各种队列:如就绪队列和阻塞队列

进程的上下文切换

一个进程切换到另一个进程运行,称为进程的上下文切换
notion image
进程的上下文切换不仅包含了虚拟内存,栈,全局变量等用户空间的资源,还包括了内核堆栈,寄存器等内核空间的资源
应用场景:
  • 进程通过睡眠函数sleep的方法将自己主动挂起时,会重新调度
  • 进程系统资源不足的被迫挂起,去运行其他进程时会发生上下文切换
  • 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序

线程的上下文切换

线程是进程当中的一条执行流程
对于线程和进程,我们可以这么理解:
  • 当进程只有一个线程时,可以认为进程就等于线程;
  • 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下文切换时是不需要修改的;
线程的上下文切换看线程是不是属于同一个进程:
  • 当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样;
  • 当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据

线程/进程的实现

  • fork()来创建一个新的进程,在父进程中fork()返回子进程PID,子进程fork()成功返回0,失败返回-1
  • pthread_create()创建新的线程,成功返回0,失败返回错误号
1.用户线程:在用户空间实现的线程
2.内核线程:在内核中实现的线程
3.轻量级进程:在内核中来支持用户线程

线程与进程的区别?

  • 进程则是系统资源分配的独立单位,线程是CPU调度的最小单位,协程是一种轻量级的线程
  • 进程内的多个线程可以并发,多进程可以并发
  • 进程有独立的虚拟空间,多线程共享虚拟空间
  • 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈
  • 进程崩溃一般不会影响其他进程,线程崩溃可能会影响同一进程的其他线程
  • 协程是一种轻量级的线程,它的执行完全由用户代码来控制,而非操作系统,因此切换的代价更低
线程的优点:
  • 一个进程中可以同时存在多个线程;
  • 各个线程之间可以并发执行;
  • 各个线程之间可以共享地址空间和文件等资源;
线程的缺点:
在C++语言中,当进程中的一个线程崩溃时,会导致其所属进程的所有线程崩溃

进程间的通信方式有哪些?

1.管道:匿名管道(存在父子关系的进程)和命名管道(不相关的进程间也可以相互通信),效率低,不适合频繁交换
2.消息队列:两个进程之间的通信就像平时发邮件一样,你来一封,我回一封,但是通信不够及时
3.共享内存:拿出一块虚拟地址空间来,进程都能访问
4.信号量(PV操作):多进程修改一个共享内存容易冲突,因此信号量使得共享的资源,在任意时刻只能被一个进程访问
  • P操作会把信号量-1,相减后若信号量<0表明资源已经被占用,若≥0,表明还有资源可以使用
  • V操作会把信号量+1,相加后若≤0,表明当前有阻塞中的进程,若>0,表明当前没有阻塞的进程
  • PV成对出现,P用于进入共享资源之前,V用于离开共享资源之后,信号初始化为1代表互斥信号量,初始化为0代表多进程同步信号量
5.信号:信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,可以进行捕捉和忽略信号
6.Socket:跨网络与不同主机上的进程之间通信(TCP,UDP,本地通信)
6.1TcpSocket
notion image
是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket
6.2 UDPSocket(不需要connect)
notion image
6.3 本地通信不需要bind IP地址和端口,而是绑定一个本地文件
 

避免多线程资源竞争的的互斥丶同步方法

互斥的概念临界区时访问共享资源的代码片段,其必须保持互斥,保证一个线程在临界区执行时,其他线程应该被阻止进入临界区
同步的概念:所谓同步,就是并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步
为了实现进程/线程间正确协作其主要方法:锁(加锁,解锁)/信号量(PV)/条件变量/同步原语/设计模式(生产-消费)
锁:进入临界区的线程,必须先执行加锁操作。若加锁通过,则线程可进入临界区;在完成对临界资源的访问后再执行解锁操作,以释放该临界资源。
根据锁的实现不同,可以分为「忙等待锁」和「无忙等待锁」
忙等待锁:获取不到锁的时候,线程一致while循环,不做其他任何事情,也被称为自旋锁
无忙等待锁:获取不到锁的时候,不用自旋,如互斥锁,尝试获取锁失败时,它会将线程或进程置于睡眠状态
读写锁:读共享,写独占

生产者-消费者问题

notion image
  • 生产者在生成数据后,放在一个缓冲区中;
  • 消费者从缓冲区取出数据处理;
  • 任何时刻,只能有一个生产者或消费者可以访问缓冲区;
我们对问题分析可以得出:
  • 任何时刻只能有一个线程操作缓冲区,说明操作缓冲区是临界代码,需要互斥
  • 缓冲区空时,消费者必须等待生产者生成数据;缓冲区满时,生产者必须等待消费者取出数据。说明生产者和消费者需要同步
那么我们需要三个信号量,分别是:
互斥信号量 mutex:用于互斥访问缓冲区,初始化值为 1
资源信号量 fullBuffers:用于消费者询问缓冲区是否有数据,初始化为0(表明缓冲区一开始是0)
资源信号量 emptyBuffers:用于生产者询问缓冲区是否有空位,初始化为N(缓冲区大小)
 

哲学家就餐问题

读者-写者问题

 
 

死锁

两个线程都在等待对方释放锁,没有外力的情况下,这些线程就会一直相互等待无法继续运行
死锁只有同时满足以下四个条件才会发生:
  • 互斥条件:多个线程不能同时使用同一个资源
  • 持有并等待条件:线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1
  • 不可剥夺条件:资源一旦被占用,除非该资源自愿释放,否则该资源不能被其他进程或线程抢占。
  • 环路等待条件:两个线程获取资源的顺序构成了环形链
解决死锁问题常常从环路等待条件出发,使用资源有序分配法,来破环环路等待条件

各种锁的种类以及定义

  • 自旋锁加锁失败后,线程会忙等待,直到它拿到锁;
  • 互斥锁加锁失败后,线程会使当前线程休眠,释放 CPU给其他线程(两次线程上下文切换的成本)
  • 读写锁由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁,使用于能够明确区分读写操作的场景
写锁是独占锁,读锁是共享锁,读写锁在读多写少的场景,能发挥出优势
  • 悲观锁认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
  • 乐观锁先修改完共享资源,再验证这段时间内有没有发生冲突,若没冲突操作完成,若有其他线程已经修改过这个资源,就放弃本次操作(版本号,时间戳,CAS)

一个进程可以申请多少线程

在32位系统中,一个进程的虚拟空间是4G,内核分走1G,留给用户的是3G,一个线程假设需要10M,那最多可以创建300线程

调度算法:

进程间的调度算法有哪些?

  • 非抢占式调度算法:挑选一个进程,然后让该进程运行直到被阻塞,或者直到该进程退出,才会调用另外一个进程
  • 抢占式调度算法:挑选一个进程,然后让该进程只运行某段时间
调度算法的原则:高CPU利用率,高系统吞吐率,低响应时间和等待时间
1.CPU利用率:调度程序应确保 CPU 是始终匆忙的状态
2.系统吞吐率:吞吐量表示的是单位时间内 CPU 完成进程的数量
3.等待时间:进程处于就绪队列的时间
4.响应时间:用户提交请求到系统第一次产生响应所花费的时间
5.周转时间:周转时间是进程运行+阻塞时间+等待时间的总和
  • 先来先服务(FCFS):每次从就绪队列选择最先进入队列的进程
  • 最短作业优先调度算法(SJF):优先选择运行时间最短的进程来运行
  • 高响应比优先调度算法(等➕服/服):计算「响应比优先级」划分VIP,然后把「响应比优先级」最高的进程投入运行
  • 时间片轮转调度算法:每个进程被分配一个时间段,称为时间片(Quantum),即允许该进程在该时间段中运行
  • 最高优先级调度算法:就绪队列中选择最高优先级的进程进行运行【分为静态(创建时就知道)和动态(依据时间)
  • 多级反馈队列调度算法:是「时间片轮转算法」和「最高优先级算法」的综合
1)设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短;
2)如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;

内存页面置换算法有哪些?

当 CPU 访问的页面不在物理内存时,便会产生一个缺页中断,请求操作系统将所缺页调入到物理内存
页面置换算法的功能是,当出现缺页异常,需调入新页面而内存已满时,选择被置换的物理页面
调度算法的原则:尽可能的减少页面换入换出的次数
  • 最佳页面置换算法(OPT):置换在未来最长时间不访问的页面,即计算每个逻辑页面下一次访问的时间,然后选择【未来】最长时间不访问的页面
  • 先进先出页面置换算法(FIFO):选择在内存驻留时间很长的页面进行中置换
  • 最近最久未使用的置换算法(LRU):发生缺页时,选择最长时间没有被访问的页面进行置换【历史】
  • 时钟页面置换算法:把所有的页面都保存在一个类似钟面的「环形链表」中,一个表针指向最老的页面,指0直接淘汰,指1变0继续找
  • 最不常用算法(LFU):而是当发生缺页中断时,选择「访问次数」最少的那个页面,并将其淘汰

磁盘调度算法有哪些(53号是起始磁头)

1.先来先服务算法(FCS)
2.最短寻道时间优先(SSF):优先选择从当前磁头位置所需寻道时间最短的请求
3.扫描算法(Scan):磁头在一个方向上移动,访问所有未完成的请求,直到磁头到达该方向上最后的磁道,才调换方向
4.循环扫描算法(CSCAN):先处理一个方向的请求,处理完毕后直接快速返回至复位磁头,返回途中不处理请求
5.LOOK算法—-Scan算法优化:磁头在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,而不需要移动到磁盘的最末端,反向移动的途中会响应请求
6.C-LOOK算法—CSCAN算法优化:头在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,而不需要移动到磁盘的最始端,反向移动的途中不会响应请求

文件系统:

文件系统的基本组成

linux会为每个文件分配两个数据结构:索引节点目录项
索引节点:是文件唯一的标识,索引节点同样占用磁盘空间。
目录项:用来记录文件的名字等,目录项是由内核维护的一个数据结构,不存放于磁盘,而是缓存在内存
磁盘读写的最小单位是扇区,文件系统把多个扇区组成了逻辑块
磁盘一般会被分为三个存储区域:超级块和索引节点区不可能全都加载至内存
磁盘存储的数据结构:B+树(所有的值都出现在叶子节点上,其余非叶子节点只存储指向其子节点的指针)logn
超级块:用来存储文件系统的详细信息
索引节点区:用来存储索引节点
数据块区:用来存储文件和目录数据

文件的使用与存储

读写文件的过程都需要依赖数据块,因此文件系统的基本操作单位是数据块
  • 当用户进程从文件读取 1 个字节大小的数据时,文件系统则需要获取字节所在的数据块,再返回数据块对应的用户进程所需的数据部分
  • 当用户进程把 1 个字节大小的数据写进文件时,文件系统则找到需要写入数据的数据块的位置,然后修改数据块中对应的部分,最后再把数据块写回磁盘

文件的存储:
  • 连续空间存放方式
连续空间存放方式顾名思义,文件存放在磁盘「连续的」物理空间中,一次磁盘寻道就可以读出整个文件
但是存在【磁盘空间碎片】以及【文件长度不易扩展】的缺陷
  • 非连续空间存放方式
非连续空间存放方式分为「链表方式」和「索引方式」
显式链表:把用于链接文件各数据块的指针,显式地存放在内存的一张链接表中
隐式链表:实现的方式是文件头要包含「第一块」和「最后一块」的位置,并且每个数据块里面留出一个指针空间,用来存放下一个数据块的位置
索引:为每个文件创建一个「索引数据块」,里面存放的是指向文件数据块的指针列表,像是图书目录

目录的存储:
和普通文件不同的是,普通文件的块里面保存的是文件数据,而目录文件的块里面保存的是目录里面一项一项的文件信息。
一般使用哈希表来保存目录的内容

空闲空间管理

如果我要保存一个数据块,我应该放在硬盘上的哪个位置呢?难道需要将所有的块扫描一遍,找个空的地方随便放吗,效率太低,因此引入空闲空间管理
空闲表法:
空闲表法就是为所有空闲空间建立一张表,表内容包括空闲区的第一个块号和该空闲区的块个数,该方式是连续分配的
notion image
空闲链表法:
我们也可以使用「链表」的方式来管理空闲空间,每一个空闲块里有一个指针指向下一个空闲块
notion image
位图法(LINUX采用的方法):
位图是利用二进制的一位来表示磁盘中一个盘块的使用情况,磁盘上所有的盘块都有一个二进制位与之对应。
当值为 0 时,表示对应的盘块空闲,值为 1 时,表示对应的盘块已分配。它形式如下:
1111110011111110001110110111111100111 ...

硬链接和软连接

有时候我们希望给某个文件取个别名,那么在 Linux 中可以通过硬链接(Hard Link 和软链接(Symbolic Link 的方式来实现
硬链接:指向的是同一个文件,因此不可用于跨系统文件,删除时必须删除文件的所有硬链接以及源文件才能彻底删除
软连接:是一个独立的文件,但文件内容是另一个文件的路径,因此可以跨文件系统,源文件被删除链接文件也还在,只是指向文件不在了

文件I/O

  • 缓冲与非缓冲 I/O(在用户空间和内核空间之间有一个中间缓冲区)
  • 直接与非直接 I/O
  • 阻塞与非阻塞 I/O VS 同步与异步 I/O

缓冲与非缓冲I/O:根据「是否利用标准库缓冲」,可以把文件I/O分为缓冲I/O和非缓冲I/O
  • 缓冲IO利用的是标准库的缓存实现文件的加速访问,标准库在通过系统调用访问文件
  • 非缓冲IO是通过系统直接调用访问文件,不经过标准库缓存

直接与非直接I/O:根据是「否利用操作系统的缓存」,可以把文件 I/O 分为直接 I/O 与非直接 I/O
  • 直接I/O,不会发生内核缓存和用户程序之间的数据复制,而是经过文件系统访问磁盘,不经过操作系统的缓冲区
  • 非直接I/O,读操作数据从内核缓存中拷贝给用户程序,写操作时,数据从用户程序拷贝给内核缓存

阻塞与非阻塞I/O VS 同步与异步I/O
阻塞I/O,非阻塞I/O,I/O多路复用都是同步I/O调用:因为它们在 read 调用时,内核将数据从内核空间拷贝到应用程序空间,过程都是需要等待的
  • 阻塞I/O是就是在I/O执行的两个阶段:数据准备数据拷贝阶段都被block(内核数据准备,数据从内核缓冲区拷贝到应用程序的缓冲区)
  • 非阻塞I/O是不断主动轮询内核直到数据准备好,内核将数据拷贝到应用程序缓冲区
异步 I/O 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待,对于磁盘,异步IO仅支持直接IO

进程写文件的时候,进程崩溃后已写入的数据会丢失吗?

不会,因为使用缓存IO(系统标准库缓冲)的时候,实际上是把文件数据写到了内核page cache上
即使进程崩溃了,文件数据还是保留在内核的 page cache,我们读数据的时候,也是从内核的 page cache 读取,因此还是依然读的进程崩溃前写入的数据。

当键盘敲入A字母,操作系统发生了什么:

通过总线向CPU发起中断请求,CPU收到中断请求然后保存被中断进程的CPU上下文,后调用键盘的中断处理函数,将字符显示到屏幕中,CPU恢复上下文

网络系统:

DMA技术

在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给DMA控制器,而CPU不再参与任何与数据搬运相关的事情
CPU 不再参与「将数据从磁盘控制器缓冲区搬运到内核空间」的工作

传统的文件传输(拷贝技术)

服务端提供文件传输的功能:从磁盘上把文件读取出来,然后通过网络协议发送给客户端(read,write)
notion image
期间发生了四次用户态和内核态的上下文切换,因为发生了两次系统调用,read()和write(),每次系统调用都得先从用户态切换到内核态,完成任务再切回来
其次还发生了四次数据拷贝,两次DMA拷贝,两次CPU拷贝
  • 第一次拷贝,磁盘到内核—DMA
  • 第二次拷贝,内核到用户缓冲区-CPU
  • 第三次拷贝,用户到内核Socket缓冲区-CPU
  • 第四次拷贝,内核的Socket数据拷贝到网卡缓冲区里-DMA
因此要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数

优化实现零拷贝:零拷贝技术是基于 PageCache 的,PageCache 会缓存最近访问的数据,提升了访问缓存数据的性能
1.mmap+write
mmap函数会把内核缓冲区里的数据映射到用户空间,不需要拷贝了,映射即可(四切换,三拷贝)
2.sendfile
它可以替代前面的 read() 和 write() 这两个系统调用,并且不需要拷贝到内核态(二切换,三拷贝)
3.启动SG-DMA
磁盘到内核(DMA),内核直接到网卡(SG-DMA),这个过程之中,只进行了 2 次数据拷贝(二切,二拷)

大文件的传输方式:直接IO+异步IO

小文件的传输方式:零拷贝技术

I/O多路复用

I/O 多路复用接口最大的优势在于,允一个线程内同时处理多个I/O请求,常用在Socket中

多进程模型

服务器的主进程负责监听客户的连接,一旦与客户端连接完成,accept() 函数就会返回一个「已连接 Socket」,这时就通过 fork() 函数创建一个子进程
正因为子进程会复制父进程的文件描述符,于是就可以直接使用「已连接 Socket 」和客户端通信了
父进程只需要关心【监听Socket】
子进程只需要关心【已连接Socket】

多线程模型

服务器与客户端 TCP 完成连接后,通过 pthread_create() 函数创建线程,然后将「已连接 Socket」的文件描述符传递给线程函数,接着在线程里和客户端进行通信,从而达到并发处理的目的。

使用线程池的方式来避免线程的频繁创建和销毁,所谓的线程池,就是提前创建若干个线程,这样当由新连接建立时,将这个已连接的 Socket 放入到一个队列里,然后线程池里的线程负责从队列中取出「已连接 Socket 」进行处理。

使用一个进程维护多个Socket——I/O复用

I/O多路复用可以使单个进程同时处理多个网络连接的IO请求,基本原理是select/poll的function会不断轮询负责的socket,当有socket到达就通知用户进程,但select/poll本身并不是实现事件驱动的最佳选择,因为其线性存储结构,每次传入或者查找时都需要传入整个集合,会消耗大量时间。
因此linux提供了epoll,BSD提供了kqueue来作为上位替代
  • Select/POLL
1.poll 和 select都是使用「线性结构」存储进程关注的Socket集合,每次操作时都传入整个 socket 集合给内核
而且都需要遍历文件描述符集合来找到可读或可写的Socket,时间复杂度为 O(n)
2.仅支持水平触发
  • epoll
1.epoll在内核里使用红黑树跟踪进程所有文件描述字(O(logn)),传入时只需要传输一个待检测的 socket
2.epoll 内核里还维护了一个链表来记录就绪事件,不需要扫描整个集合
3.epoll是支持边缘触发水平触发
【边缘触发】:服务器端只会从 epoll_wait 中苏醒一次(菜鸟驿站)
【水平触发】:当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束(人工快递)

高性能网络模式

Reactor:I/O 多路复用监听事件,收到事件后,根据事件类型分配(Dispatch)给某个进程/线程
  • 单 Reactor 单进程 / 线程;
  • 单 Reactor 多线程 / 进程;
  • 多 Reactor 多进程 / 线程;
nginx的多进程reactor模型:每个线程都有自己的reactor(epoll)
其核心思想是将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程/进程阻塞在多路复用器上,一旦有I/O事件到来或者准备就绪(文件描述符或Socket可读、写),多路复用器返回并将事先注册的相应I/O事件分发到对应的处理器中。
notion image
 
  • 主线程中的 MainReactor 对象通过 epoll 监控连接建立事件,收到事件后通过 Acceptor 对象中的 accept 获取连接,将新的连接分配给某个子线程;
  • 子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入 epoll继续进行监听,并创建一个 Handler 用于处理连接的响应事件。
  • 如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应。
  • Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
 
 
 
 
 
Camellia
Camellia
明天会更好吗?🍚
公告
type
status
date
slug
summary
tags
category
icon
password
这里是一个个人博客
用于记录和分享生活