基本硬件结构:
基本结构:运算器、控制器(内存)、存储器、输入设备、输出设备
内存
线性储存,基本存储单位是byte,1byte为8字节,每个byte对应一个内存地址
CPU
- 32位的CPU———4字节———4GB寻址空间
- 64位的CPU———8字节———寻址空间极大
CPU寄存器:主要作用是存储计算时的数据
- 通用寄存器:用于存放需要进行运算的数据
- 程序计数器:存储CPU要执行下一条指令所在的内存地址
- 指令寄存器:存放当前正在执行的指令本身
CPU的指令周期
- Fetch:CPU通过程序计数器获取对应内存地址
- Decode:CPU进行指令解码
- Execution:CPU执行指令
- Store:CPU将结果存回寄存器或者存入内存
总线
- 地址总线:指定内存地址
- 控制总线:控制读写命令
- 数据总线:传输数据
指令的执行速度
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 这样的三层高速缓存
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 访问一个内存地址的时候,会经历什么?
- 根据内存地址中索引信息,计算在 CPU Cache 中的索引
- 找到对应 CPU Cache Line 后,判断 CPU Cache Line 中的有效位,有效继续执行
- 对比内存地址中组标记和 CPU Cache Line 中的组标记,确认 CPU Cache Line 中的数据是我们要访问的内存数据
- 根据内存地址中偏移量信息,从 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 Snooping):CPU 需要每时每刻监听总线上的一切活动
- 事务的串行化:某个 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执行模式
- 用户态(User Mode):
- 在用户态下,程序的执行权限受到限制,它们无法直接操作底层资源,只能通过操作系统提供的调用来请求服务。
- 内核态(Kernel Mode):
- 内核态最高的访问权限和控制权。在内核态下,操作系统内核可以直接访问系统硬件和底层资源。
- 负责管理系统资源、处理中断、调度进程、分配内存等关键任务
3.系统态和内核态的交互方式
- 系统调用:通过API接口使应用程序转入内核态
- 管道,消息队列,套接字,信号,共享内存
- I/O控制,可以从用户态转为内核态
- 内核态和用户态的代码也可以通过文件系统进行通信
内存管理:
虚拟内存是什么?
操作系统为每个进程都分配独立的一套虚拟地址,操作系统会提供一种机制:将不同进程的虚拟地址和不同内存的物理地址映射起来。
- 我们程序所使用的内存地址叫做虚拟内存地址(Virtual Memory Address)
- 实际存在硬件里面的空间地址叫物理内存地址(Physical Memory Address)
管理虚拟地址与物理地址的关系
内存分段:程序是由若干个逻辑分段组成的,不同的段有不同的属性的,所以就用分段的形式把这些段分离出来。
分段机制下的虚拟地址由两部分组成:段选择因子,段内偏移量
段选择因子保存在段寄存器里,段选择因子里最重要的是段号,用作段表的索引
段表里面保存的是这个段的基地址,段的界限和特权等级
段内偏移量应该位于0和段界限之间,如果段内偏移量合法,那就将段基地址+段内偏移量得到物理地址
内存分段的不足:外部内存碎片
内存碎片:1G的物理内存,游戏占用了512MB内存,浏览器占用了128MB内存,音乐占用了256MB内存
这时候我们关闭浏览器,无法再打开一个200kb的程序,因为是两段128kb
处理方法:内存交换,将音乐的256写入硬盘,关闭浏览器后再装载回来紧跟着512MB即可
内存分段的不足:交换效率低
因为要把数据写到硬盘上,如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿
内存分页:整个虚拟和物理内存空间切成一段段固定尺寸的大小
虚拟地址与物理地址之间通过页表来映射,如下图:
当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行
页表的内存交换的效率就相对比较高,因为换出时会把「最近没被使用」的内存页面写在硬盘上,需要的时候,再加载进来。所以,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间。此外,更进一步的只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去
分页机制下的虚拟地址由两部分组成:页号和页内偏移
页号作为页表的索引
页表包含物理页每页所在物理内存的基地址
页内偏移合法,那就将页基地址+页内偏移得到物理地址
页表的不足:
100
个进程的话,就需要 400MB
的内存来存储页表,这是非常大的内存了,占用内存大多级页表:应用局部性原理将页表分为一级页表和二级页表,64位需要4级页表
如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表
从页表的性质来看,保存在内存中的页表承担的职责是将虚拟地址翻译成物理地址。假如虚拟地址在页表中找不到对应的页表项,计算机系统就不能工作了。所以页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有 100 多万个页表项来映射,而二级分页则只需要 1024 个页表项(此时一级页表覆盖到了全部虚拟地址空间,二级页表在需要时创建)。
TLB:一个存放程序最常访问的页表项的缓存
有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。
TLB 的命中率其实是很高的,因为程序最常访问的页就那么几个。
段页式内存管理:内存分页和内存分段组合起来,提高了内存空间的利用率
- 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;
- 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页
地址结构就由段号、段内页号和页内位移组成
- 第一次访问段表,得到页表起始地址;
- 第二次访问页表,得到物理页号;
- 第三次将物理页号与页内位移组合,得到物理地址。
为什么要有虚拟内存和内存管理?
1.虚拟内存可以使运行内存超过物理内存大小,没有被经常使用到的内存,我们可以把它换出到物理内存之外。
2.每个进程的虚拟内存空间就是相互独立的,解决了多进程之间地址冲突的问题。
3.页表中存在一些标记属性的比特,在内存访问方面,提供了更好的安全性。
Malloc分配内存机制:分配的是虚拟内存
- 方式一:通过 brk() 系统调用从堆分配内存,内存小于 128 KB
malloc 通过 brk() 方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用;
- 方式二:通过 mmap() 系统调用在文件映射区域分配内存,内存大于 128 KB
malloc 通过 mmap() 方式申请的内存,free 释放内存的时候,会把内存归还给操作系统,内存得到真正的释放
线程/进程管理
进程的状态
多进程实际上是并发,即快速处理多个任务而不是同时处理多个任务
进程的活动期间遵守原则:运行前一定要就绪
运行状态:该时刻进程占用CPU
就绪状态:只缺少CPU调度
阻塞状态:进程在等待某一事件发生而暂时停止运行
创建状态:被创建时的状态
结束状态:进程从系统消失的状态
进程的控制结构
操作系统中,是用进程控制块(PCB)数据结构来描述进程的,PCB 是进程存在的唯一标识
PCB里面具体包含:
- 进程描述信息:进程标识符,用户标识符
- 进程控制信息:进程当前状态,进程优先级
- 资源分配清单:有关虚拟地址的信息,I/O设备信息
- CPU相关信息
PCB通过链表的方式进行组织,把相同状态的进程链在一起,组成各种队列:如就绪队列和阻塞队列
进程的上下文切换
一个进程切换到另一个进程运行,称为进程的上下文切换
进程的上下文切换不仅包含了虚拟内存,栈,全局变量等用户空间的资源,还包括了内核堆栈,寄存器等内核空间的资源
应用场景:
- 进程通过睡眠函数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
是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。
6.2 UDPSocket(不需要connect)
6.3 本地通信不需要bind IP地址和端口,而是绑定一个本地文件
避免多线程资源竞争的的互斥丶同步方法
互斥的概念:临界区时访问共享资源的代码片段,其必须保持互斥,保证一个线程在临界区执行时,其他线程应该被阻止进入临界区
同步的概念:所谓同步,就是并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步
为了实现进程/线程间正确协作其主要方法:锁(加锁,解锁)/信号量(PV)/条件变量/同步原语/设计模式(生产-消费)
锁:进入临界区的线程,必须先执行加锁操作。若加锁通过,则线程可进入临界区;在完成对临界资源的访问后再执行解锁操作,以释放该临界资源。
根据锁的实现不同,可以分为「忙等待锁」和「无忙等待锁」
忙等待锁:获取不到锁的时候,线程一致while循环,不做其他任何事情,也被称为自旋锁
无忙等待锁:获取不到锁的时候,不用自旋,如互斥锁,尝试获取锁失败时,它会将线程或进程置于睡眠状态
读写锁:读共享,写独占
生产者-消费者问题
- 生产者在生成数据后,放在一个缓冲区中;
- 消费者从缓冲区取出数据处理;
- 任何时刻,只能有一个生产者或消费者可以访问缓冲区;
我们对问题分析可以得出:
- 任何时刻只能有一个线程操作缓冲区,说明操作缓冲区是临界代码,需要互斥;
- 缓冲区空时,消费者必须等待生产者生成数据;缓冲区满时,生产者必须等待消费者取出数据。说明生产者和消费者需要同步。
那么我们需要三个信号量,分别是:
互斥信号量
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 个字节大小的数据写进文件时,文件系统则找到需要写入数据的数据块的位置,然后修改数据块中对应的部分,最后再把数据块写回磁盘
文件的存储:
- 连续空间存放方式
连续空间存放方式顾名思义,文件存放在磁盘「连续的」物理空间中,一次磁盘寻道就可以读出整个文件
但是存在【磁盘空间碎片】以及【文件长度不易扩展】的缺陷
- 非连续空间存放方式
非连续空间存放方式分为「链表方式」和「索引方式」
显式链表:把用于链接文件各数据块的指针,显式地存放在内存的一张链接表中
隐式链表:实现的方式是文件头要包含「第一块」和「最后一块」的位置,并且每个数据块里面留出一个指针空间,用来存放下一个数据块的位置
索引:为每个文件创建一个「索引数据块」,里面存放的是指向文件数据块的指针列表,像是图书目录
目录的存储:
和普通文件不同的是,普通文件的块里面保存的是文件数据,而目录文件的块里面保存的是目录里面一项一项的文件信息。
一般使用哈希表来保存目录的内容
空闲空间管理
如果我要保存一个数据块,我应该放在硬盘上的哪个位置呢?难道需要将所有的块扫描一遍,找个空的地方随便放吗,效率太低,因此引入空闲空间管理
空闲表法:
空闲表法就是为所有空闲空间建立一张表,表内容包括空闲区的第一个块号和该空闲区的块个数,该方式是连续分配的
空闲链表法:
我们也可以使用「链表」的方式来管理空闲空间,每一个空闲块里有一个指针指向下一个空闲块
位图法(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)
期间发生了四次用户态和内核态的上下文切换,因为发生了两次系统调用,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事件分发到对应的处理器中。
- 主线程中的 MainReactor 对象通过 epoll 监控连接建立事件,收到事件后通过 Acceptor 对象中的 accept 获取连接,将新的连接分配给某个子线程;
- 子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入 epoll继续进行监听,并创建一个 Handler 用于处理连接的响应事件。
- 如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应。
- Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。