操作系统的相关知识点可分为CPU,内存管理,进程管理,文件系统,设备管理和网络系统六大模块。本文简要总结整理这些模块的相关知识点。
CPU
CPU执行指令的过程
- 取指令(CPU去程序计数器获取指令地址,并取出放到指令寄存器中)
- 指令解码
- 执行指令
- 访问内存(若需要读取或写入数据到内存)
- 写回结果(执行结果被写回寄存器或内存)
64位与32位系统
- 64位CPU一次可以计算超过32位的数字,而32位CPU需要分多次计算,所以运算大数字时64位CPU才能体现优势。且64位CPU的地址总线位数更大,即64位地址长度,可以寻址更大的物理内存空间
- 64位软件和32位软件实际上代表指令是64位还是32位的,32位软件可以兼容到64位的机器上运行,但是64位软件无法放到32位机器上运行。
中断
中断是系统用来响应硬件设备请求的机制,OS收到硬件中断请求,会打断正在执行的进程,调用中断处理程序来响应请求。
-
Linux将中断分为两个阶段,即上半部分和下半部分(也叫硬中断和软中断)。
- 硬中断用来快速响应中断请求,会打断CPU,暂时屏蔽中断请求并调用软中断
- 软中断用于真正处理中断请求,一般以内核线程方式运行
- 例如网卡接收数据包,网卡收到网络包后通过DMA将数据写入内存,然后硬中断通知CPU有数据到了,然后再软中断调用内核线程,完成从内存中找到数据并按照协议栈解析再将数据送给进程的过程
-
中断的处理过程/上下文切换
- 保护现场:将当前程序相关数据保存到寄存器
- 开中断:以便响应级别更高的中断
- 中断处理
- 关中断:避免恢复现场时被打扰
- 恢复现场:从寄存器中取出之前数据,恢复中断前的状态
-
中断还可以分为内外中断。
- 内中断指CPU执行指令时发送的中断,如trap(自愿中断),整数除0,地址越界
- 外中断指CPU执行指令以外的事,如IO中断
用户态与内核态
- 用户态和内核态是操作系统的两种不同状态:内核态拥有最高权限,访问资源不受限制,执行CPU指令不受限制,中断请求不受限制。但以上操作在用户态都会受限制(防止用户做出一些危险操作,如设置时钟或内存清理)
- 操作系统运行在内核态,应用程序运行在用户态。当进程需要执行特权操作时(如访问硬件设备),则需要从用户态切换到内核态。通过系统调用完成(trap指令)
- trap指令即trap中断,先给CPU发trap中断,CPU保存现场,处理中断并转化为内核态,完成后恢复现场转化回用户态
内存管理
存储器的层次结构:寄存器(最快,存放指令和操作数),CPU的L1,L2,L3三级缓存(使用SRAM),内存(使用DRAM),硬盘。
操作系统如何管理内存?
在内存中,地址可分为物理地址和虚拟地址。OS不会让进程直接使用物理地址,而是把物理地址映射到虚拟地址,并使用分页机制和分段机制管理物理地址和虚拟地址的映射关系。
分段
分段是对程序的不同部分进行划分(如代码分段),方便程序员写代码时的逻辑需求,每个段的大小不同。
- 虚拟地址由段号和段内偏移组成,OS通过段号去段表中找到段基址,拼接段内偏移找到物理地址。
- 分段不会产生内部内存碎片(因为是按需分配的),但会产生外部内存碎片(内存固定但段大小不一)。
- 解决外部碎片要使用内存交换/整理(把段写入磁盘再写入到内存),但效率低
分页
分页是将内存划分为大小相等的页(4kb),以页为单位进行内存分配。
- 分页主要是为了提高内存利用率,解决外部内存碎片和内存交换效率低的问题。
- 虚拟地址由页号和页内偏移组成,OS通过页号去页表中找到物理块号,拼接页内偏移找到物理地址。所以实际访问内存数据时,需要两次内存访问,先去内存访问页表得到物理地址,再根据物理地址访问数据。
- 分页不会有外部碎片,但是会有内部碎片。
缺页
当进程访问的页不在物理内存中(由于页面置换被放到磁盘中),此时产生缺页中断,需要将所缺页调入物理内存,更新页表。缺页也可能是在页表中找不到对应的页(未分配)。
虚拟内存与页面置换
当内存空间不足,OS会执行页面置换算法把指定页换出到磁盘,需要使用时再加载进来,这样进程可以使用的内存就比实际的内存要大,也即虚拟内存。 虚拟内存容量为实际物理内存容量加上磁盘交换区容量。与分段不同,在页面置换时,分页一次性写入磁盘的只有几页,效率较高。常见页面置换算法有:
- 先进先出:可能删除重要的页面
- 最近最久未使用LRU
- 最近最不经常使用LFU
- 时钟页面置换算法:将页面保存在环形链表中,指针指向最老的页面,如果访问位为1就清除访问位,并继续找下一个页,如果访问位为0就淘汰该页面,新页插入这个位置,指针后移。类似给FIFO两次机会
为什么虚拟地址空间(进程)切换比较耗时
每个进程都有自己的页表,所以它们的虚拟内存空间是独立的,实现了进程间的隔离。但是由于进程有自己页表(即虚拟地址空间),把虚拟地址空间转化为物理地址需要查找页表,通常使用TLB加速。但是进程切换后页表失效,TLB也失效,导致需要重新去查找页表。
多级页表
由于每个进程都要有自己的页表,当页表项的数量很多时,占用空间就很多,所以引入了多级页表,进程只需要保存高级页表,通过高级页表去索引低级页表。但是多级页表带来了地址多次转换的问题,基于程序的局部性原理,我们把最常访问的几个页表项存储到缓存中加快访问速度,即TLB缓存(快表)
进程管理
并发和并行
并发就是在一段时间间隔内,CPU轮流执行多个任务,但实际在某一时刻只有一个任务在执行,多个线程轮流获取CPU时间片,因为切换速度很快,所以宏观上看好像同一时间运行了多个程序。
并行是在某一时刻多核CPU同时执行多个任务,不同任务被放到不同处理器核上完成。
进程与线程的区别
- 进程是资源分配的基本单位,线程是CPU调度的基本单位
- 进程独立地拥有一些资源,多个线程共享进程的资源,而线程只独立拥有寄存器和栈
- 进程创建,上下文切换开销大,速度慢;线程创建,上下文切换开销小,速度快
- 同一个进程的各线程共享内存,所以线程间传递数据不需要经过内核,使得线程间数据交互效率更高。但是不同进程间的通信通常需要OS内核介入。
进程
CPU执行程序中的每一条指令,这个运行中的程序就被称为进程。
-
进程状态:创建,就绪,运行,阻塞,终止
- 创建到就绪:进程已准备好资源,等待CPU。
- 就绪到运行:获得了CPU时间片。
- 运行到阻塞:等待某个事件,如IO请求。
- 阻塞到就绪:等待事件到达,如IO请求完成
- 另外,进程在阻塞时会占用物理内存空间,所以OS会将阻塞进程的物理内存空间换到磁盘,此时进程为挂起状态
-
进程控制结构PCB
- PCB是进程存在的唯一标识,PCB包含
- 进程描述信息:进程标识符,用户标识符
- 进程控制和管理信息:进程当前状态,进程优先级
- 资源分配信息:虚拟地址空间信息,打开的文件信息,IO信息
- CPU信息:CPU中各个寄存器的值,进程被切换时CPU状态保存这里
- PCB是通过链表的方式组织的,把相同状态的进程链接在一起组成各种队列,如就绪队列和阻塞队列。因为链表可以灵活地插入和删除,对应进程的创建和销毁。
- 创建进程就是申请一个PCB,填充进程信息,再为进程分配资源,最后将PCB插入就绪队列。
- PCB是进程存在的唯一标识,PCB包含
-
进程上下文切换
- CPU的上下文切换:先把前一个任务的上下文(寄存器和程序计数器等)保存起来,加载新任务的上下文到寄存器和程序计数器中,跳转到新程序计数器所指位置运行指令。这里的任务主要包含进程、线程和中断。
- 进程的上下文主要包含虚拟内存(页表切换),栈,全局变量等用户空间的资源,还包含内核堆栈,寄存器等内核空间的资源。通常这些信息保存在PCB。
- 什么时候会切换进程?
- 当进程CPU时间片用完时会变为就绪态,此时会切换进程。
- 进程等待某个资源时被阻塞或挂起。
- 进程sleep。
- CPU中断时会暂停当前进程
-
进程间通信方式
- 管道:数据单向流动,适用于简单的进程通信。管道又分为命名管道和匿名管道(有父子关系的进程才能使用)。Linux命令中的 | 就是匿名管道,将前一个命令的输出作为后一个命令的输入。命名管道也叫fifo,需要先创建(mkfifo),再往管道写入和读取数据。但是管道通常是临时的,数据流没有边界,不保证消息的顺序
- 消息队列:数据双向流动,进程可以向消息队列中添加或读取消息,适用于较复杂的进程通信。消息队列可以持久化,数据以消息的格式发送,保证顺序
- 共享内存:即一块虚拟地址空间,映射到相同的物理内存。这样一个线程写入的数据,另一个线程马上就可以看见,不用多次拷贝,但是可能有线程安全问题
- 信号量:就是一个计数器,用于实现进程间的同步和互斥,控制多个进程对共享资源的访问。信号量表示资源数量,主要是PV这两个原子操作。当信号量为1时就是互斥信号量。当信号量初始化为0,就是同步信号量,可以控制进程A在进程B之前执行(生产者和消费者)
- 信号:用于在异常情况下通知进程。例如ctrl+c给进程发送终止信号;kill pid给对应进程发送结束信号
- Socket:以上都是同一个主机上的进程通信方式,两个主机上的进程进行通信要用socket。
1. 服务端和客户端初始化socket得到文件描述符
2. 服务端调用bind绑定监听的Ip地址和端口(这是监听的socket)
3. 服务端调用listen进行监听
4. 服务端调用accept等待客户端连接
5. 客户端调用connect向服务端的地址和端口发送连接请求
6. 服务端accept返回用于传输的socket的文件描述符(这是传输数据的socket)
7. 客户端调用write写入数据,服务端调用read读取数据
8. 客户端调用close断开连接,服务端read读取到EOF,处理完数据后调用close关闭连接
线程
线程是进程中的一条执行流程,同一个进程内的多个线程共享进程的资源,每个线程有自己独立的寄存器和栈。
-
线程分类
- 内核级线程:由内核管理的线程
- 用户级线程:存在于用户态,由线程库来创建和管理的线程。OS无法感知到用户级线程存在。
-
线程控制结构TCB
- 线程库实现了线程控制块TCB来控制线程,TCB记录了PC,栈指针,寄存器等信息
-
线程上下文切换
- 同一个进程内的线程切换时,不用切换虚拟内存空间,只需要切换线程私有数据,如寄存器,栈。所以线程切换比进程切换开销小很多
-
调度策略
- 先来先服务:对长作业有利,对短作业不利,对IO密集型线程也不利(因为IO后需要重新排队)
- 短作业优先:可能会导致长作业饿死
- 最高响应比优先:响应比=(等待时间+服务时间)/服务时间,也就是等待时间越久响应比越高,优先级越高(实际上我们无法预知进程要求服务的时间,所以无法实现)
- 时间片轮转:将CPU时间分为多个等长的时间片,轮流给不同的进程。但是时间片的长度不好确定,时间片太短切换开销大,时间片太长实时性得不到保证
- 优先级:设置不同进程的优先级,优先级高的先执行。
- 多级反馈队列:有多个队列,优先级越高的队列CPU时间片越短,只有优先级高的队列执行完才能执行优先级低的队列,且如果有新进程加入优先级高的队列时要停止当前进程去运行优先级高的队列
-
优先级反转问题
- 优先级调度策略存在优先级反转问题,即低优先级的任务持有高优先级的任务需要的资源导致高优先级任务阻塞,但是中优先级的任务不需要资源从而抢占了低优先级任务,也就是中优先级任务间接抢占了高优先级任务。
- 这会导致高优先任务无法先执行,系统实时性变差。
- 解决方案是获取到资源的低优先级的任务提高自己的优先级为所有申请资源的任务的最高优先级,从而避免中优先级任务抢占它,直到完成任务后恢复优先级。
-
线程间通信方式
- 共享内存:同一个进程内的线程通过共享内存通信
- 信号量:实现线程间同步
- 互斥锁:实现线程互斥访问临界区
- 条件变量:线程需要等待某些条件成立时才获取锁
- 事件机制:线程等待某个事件发生
线程互斥与同步
互斥:多个线程竞争操作共享变量时它们之间的关系即为互斥,例如同一时刻,临界区只能被一个线程访问,其他线程被阻止进入临界区。互斥一般使用锁来控制,也可以用信号量控制。两种基本的锁如下:
- 自旋锁/忙等待锁,当获取不到锁时会一直while循环,直到获取到锁,自旋锁不会放弃CPU
- 互斥锁/无等待锁,获取不到锁时将线程放入等待队列,让出CPU给其他线程执行,但是互斥锁需要切换线程,会有上下文切换开销,所以若锁住的代码执行时间很短应该使用自旋锁
同步则是不同线程需要互相等待与互通消息,它们之间的关系为先后关系。使用信号量来实现同步。
- 信号量表示资源数量,用PV操作来控制信号量,P将信号量减一,如果信号量小于0则线程阻塞;V将信号量加一,如果信号量大于等于0则会唤醒一个进程。进入临界区之前调用P操作,离开临界区调用V操作。将信号量设置为1可以实现进程间互斥访问临界区。将信号量设置为0可以实现进程间的事件同步
- 生产者消费者问题
- 生产者生成数据后放入缓冲区,消费者从缓冲区消费数据,缓冲区每次只能被一个人访问, 所以访问缓存区需要一个互斥信号量。生产者需要一个信号量判断缓冲区是否有空位,初始化为N。消费者需要一个信号量判断缓冲区是否有资源,初始化为0。
- 哲学家问题
- 5个哲学家围着一张圆桌吃饭,在它们之间共有5支筷子,哲学家需要拿到两支筷子才能吃饭。如果每个哲学家都拿起一支筷子会造成死锁
- 方法1:加互斥信号量,当一个哲学家拿筷子时,其他哲学家都阻塞,哲学家吃完放下两支筷子才释放信号量。只能一人进餐
- 方法2:偶数号哲学家先拿左边筷子再拿右边筷子,奇数相反。可以两人进餐
- 方法3:记录每个哲学家状态(进餐,思考,尝试拿筷子),只有当两个邻居都不在进餐状态时才可以去拿筷子
- 读者写者问题
- 允许多个读者一起读,但读写和写写互斥。 对读者计数,使用信号量控制对读者计数器的互斥修改,使用信号量控制写操作的互斥信号量。
- 写者进来,如果有写者或者读者,则阻塞
- 读者进来,如果写者则阻塞,如果自己是第一个读者,则加锁,计数器加一,如果不是第一个读者则计数器加一,读者退出则计数器减一,如果是最后一个读者则释放锁,唤醒写者
死锁
死锁即两个或多个线程都持有一部分资源而又互相等待其他线程释放它所需要的其他资源,导致多个线程阻塞无法向前推进。死锁的四个条件:互斥,请求并保持,不剥夺,循环等待
- 死锁预防:破坏死锁的四个条件之一。
- 破坏请求并保持:一次性分配线程需要的所有资源。
- 破坏不剥夺:允许线程强行夺取其他线程资源,或者说当一个线程持有部分资源而拿不到其他资源时,会自动释放掉已持有的资源。
- 破坏循环等待:按一定的顺序分配资源,给稀缺资源使用较大编号,先分配到小编号资源才能申请大编号资源。
- 死锁避免:通过动态检测资源分配情况,确保循环等待条件不成立,确保系统处于安全状态(系统能以某个顺序为每个进程分配资源),如银行家算法
- 死锁检测:通过资源分配图来检测是否存在环
- 死锁解除:终止进程打破循环等待。从死锁线程中抢占资源。鸵鸟策略(忽略死锁,超时自动终止线程)
协程与线程的区别
- 线程是CPU调度的基本单位,而协程是用户态的轻量级线程,通过用户程序自己调度,比线程更加高效。
- 线程是由操作系统管理,需要内核态切换,占用的系统资源更多。协程是由程序调度的,在用户态进行切换,占用资源少。
- 线程的执行是抢占式的,执行顺序由OS调度。协程是非抢占式的,执行顺序由程序逻辑控制,协程会主动让出控制权给其他协程。
什么时候使用协程
- I/O密集型任务,例如网络编程中的异步I/O处理。
- 需要大量并发但不需要多核心的运算的场景。
文件系统
Linux一切皆文件,Linux会为每个文件分配两个数据结构: 索引节点(index node)和目录项(directory entry)
- inode(索引节点):用来记录文件的元信息(编号,大小,访问权限,数据位置),inode是文件的唯一标识。索引节点和数据块都被存储在硬盘中。
- directory entry(目录项):用来记录文件名,inode指针,与其他目录项的关系。多个目录项关联起来形成目录结构。目录项会被缓存在内存中。
文件数据在磁盘中是以逻辑块(数据块)存储。查找文件时OS先去目录中根据文件名找到对应的目录项,然后通过目录项中的inode指针找到索引节点,最后访问到数据块
文件的使用
- 首先使用open系统调度打开文件返回文件描述符
- OS会跟踪进程打开的所有文件,即OS为每个进程维护一个打开文件表,文件表中保存文件描述符。所以文件描述符是打开文件的标识
- 调用write写数据
- 使用结束后,使用close系统调用关闭文件
文件的存储
- 连续空间存放方式
- 文件存放在磁盘连续的物理空间中,但是需要知道一个文件的大小才能分配连续的空间,所以文件头中需要指定起始块位置和长度。这种方式有磁盘空间碎片和文件长度不易扩展的问题。
- 非连续空间存放方式
- 链表方式:可以消除磁盘碎片,文件长度可动态扩展
- 隐式链表:文件头包含第一块和最后一块的位置,且每个数据块保留一个指针用来存放下一个数据块的位置。但是这样无法直接访问数据块而是只能通过指针顺序访问文件,且指针消耗一定空间。
- 显式链接:将数据块的指针都存放到内存的链接表中,每个表项存放链接指针并指向下一个数据块号。查找过程在内存中进行,显著地提高了检索速度。但是不适合大磁盘
- 索引方式
- 为每个文件创建一个索引数据块,存放指向文件数据块的指针列表,另外,文件头需要包含指向索引数据块的指针。这样可以通过文件头找到索引数据块的位置再通过索引数据块的索引信息找到对应的数据块
- 索引方式不会有碎片问题,支持顺序读写和随机读写,文件扩展很方便,但是额外存储所有数据
- 链表方式:可以消除磁盘碎片,文件长度可动态扩展
空闲空间管理
- 空闲表法:为所有空闲空间创建一张表,内容包括空闲区的第一个块号和该空闲区块数,这种方式是连续分配的。
- 空闲链表法:用链表的方式管理空闲空间,每个空闲块都有一个指针指向下一个空闲块。
- 位图法,利用二进制的每一位来表示磁盘中一个盘块的使用情况。
硬链接和软链接
- 硬链接是多个目录项中的索引指向同一个文件(inode),由于多个目录项都是指向同一个inode,所以只有删除文件的所有硬链接以及源文件时,系统才会彻底删除该文件。
- 软链接相当于重新创建一个文件(inode),但是文件内容是另一个文件的路径,所以访问软链接时相当于访问到另外一个文件,甚至目标文件被删除了,链接文件还存在,只不过指向的文件找不到了。
设备管理与IO
设备控制器:每个设备都有的组件,CPU通过设备控制器来控制设备。设备控制器有自己的寄存器用来和CPU通信。
- 通过写入寄存器,OS可以命令设备发送数据,接收数据,开启或关闭。
- 通过读取寄存器,OS可以了解设备状态。虽然不同设备控制器功能不同,但是设备驱动程序会提供统一的接口给OS
键盘敲入A,OS发生了什么? 设备控制器收到数据将其缓存在寄存器中,给CPU发中断,CPU处理中断,从设备控制器的寄存器读取字符到缓冲区,然后显示设备的驱动程序定时从缓冲区读取数据到自己的缓冲区,最后显示到屏幕上。
IO流程(以用户进程读取磁盘数据为例)
原始IO流程
- PageCache页缓存会保存最近访问的数据,避免每次读写时都要去磁盘中找数据,所以每次读写时实际上是对文件的页缓存进行读写。可以理解为内核缓冲区。
加入DMA
- CPU对DMA控制器发送指令,告诉DMA要读取多少数据,放到内存哪里,DMA控制器发起IO请求读取数据并拷贝到内核缓冲区,并给CPU发信号让CPU继续完成数据拷贝工作
- DMA技术就是避免CPU亲自去读取/拷贝数据导致被频繁中断
文件传输流程
原始传输过程
- 需要2次系统调用,4次用户态与内核态切换,2次DMA拷贝,2次CPU拷贝
- mmap技术可以直接把内核缓存区中的数据映射到用户空间,这样可以避免2,3CPU拷贝,而是直接从内核缓冲区CPU拷贝到socket缓存区,减少了一次数据拷贝,注意依然要调用write
- sendfile可以直接把内核缓存区的数据拷贝到socket缓存区,不用调用write,故只有2次切换,3次拷贝
零拷贝
- DMA将磁盘缓冲区的数据拷贝到内核缓冲区,缓冲区将描述符和数据长度传到socket缓冲区,此时网卡的SG-DMA控制器可以直接将内核缓冲区中的数据拷贝到网卡缓冲区中,从而减少了一次数据拷贝。
- 零拷贝需要2次切换,2次拷贝(不需要CPU拷贝)
- 零拷贝的应用:Java的IO流中有FileChannel.transferTo方法对应linux中的sendfile方法,Tomcat的文件拷贝会用到这个方法。Java中的NIO提供了MappedByteBuffer来支持mmap。Kafka在客户端与broker间传输数据时用到了零拷贝。