操作系统
操作系统用来保护系统资源和提高稳定性的重要机制
用户态和内核态
-
用户态
应用程序运行时所在的模式,权限受限,无法直接访问硬件或执行特权指令。
- 无法直接访问硬件(如磁盘、网卡)
- 只能访问分配给它的内存区域(用户空间)
- 不能执行特权指令(如开关中断、修改内存映射)
- 防止应用程序错误或恶意代码破坏系统或其他进程。
-
内核态
操作系统内核运行时所在的模式,拥有最高权限,可直接控制系统资源。
-
可访问所有内存(包括内核空间和用户空间)
-
能直接操作硬件设备
-
可执行所有CPU指令(包括特权指令)。
-
管理进程、内存、文件系统、设备驱动等核心功能。
-
为什么要区分状态?
- 安全性:如果没有权限区分,应用程序可能随意访问硬件或修改关键数据,导致系统崩溃或被恶意利用
- 稳定性:通过限制用户程序的权限,防止一个程序的错误影响整个系统
- **隔离性:**操作系统内核与用户程序之间有了明确的边界,有利于系统的模块化和维护。
进程管理
进程,线程
-
进程(Process)
进程是操作系统资源分配(虚拟内存、文件句柄、信号量等资源)的基本单位,拥有独立的地址空间、资源(如内存、文件句柄)和系统状态。
- 独立性:进程间资源隔离,每个进程都有自己独立的内存空间,一个进程崩溃,其内存空间会被操作系统回收,不会影响其他进程的内存空间。
- 并发性:多个进程可通过分时或并行在CPU上交替执行。
- 生命周期:创建(分配资源、建立 PCB)、就绪、运行、阻塞、终止(回收资源、撤销 PCB)。
-
线程(Thread)
线程是操作系统任务调度和执行的基本单位,是进程内的执行单元,共享同一进程的内存和资源(如文件描述符),但拥有独立的栈和寄存器。
- 轻量级:创建和切换开销远小于进程。
- 共享性:线程直接访问进程的全局变量和堆内存。
- 并发粒度:多线程可并行执行(需多核CPU支持)。
二者区别
维度 进程 线程 资源占用 独立内存空间,开销大 共享进程资源,开销小 崩溃影响 不影响其他进程 导致整个进程终止 切换开销 高(涉及内存映射、PCB保存) 低(仅切换栈和寄存器) 并发性 多进程并行(依赖多核) 多线程并行(更细粒度) 举个形象一点的例子:
进程— 一个独立的“公司”
- 比喻:想象进程是一家独立的公司。
- 独立办公室:公司有自己独立的办公空间(内存空间)。
- 独立资源:公司有自己的打印机、会议室(CPU、文件、网络等资源)。
- 员工协作:公司内部可能有多个员工(线程)一起工作。
- 关键特点:
进程之间完全隔离——一家公司倒闭了(崩溃),不会直接影响其他公司。
线程— 公司里的“员工”
- 比喻:线程就像公司里的员工。
- 共享办公室:所有员工共享公司的办公空间(共享进程的内存和资源)。
- 分工协作:员工可以同时做不同任务(并行处理任务)。
- 沟通成本低:员工之间可以直接说话(直接共享数据),无需走复杂流程。
- 关键特点:
线程之间高度共享——一个员工搞砸了(崩溃),可能让整个公司瘫痪!
协程:
是一种用户态轻量级线程,由程序自身控制调度(而非操作系统内核)。
协程拥有自己的寄存器上下文和栈,但与其他协程共享堆内存。
多线程
多线程:
- 运行效率高
- 充分利用多核处理器资源
- 同时处理多个任务,执行速度快
- 数据竞争,需要加锁保证线程安全,增加锁开销和产生死锁风险等
- 线程的创建和切换,消耗更多系统资源(每个线程都需要占用一定内存和处理时间)
进程/线程切换
操作系统将 CPU 从当前执行的任务(进程或线程)切换到另一个任务的过程。
切换时需要保存当前任务的状态,并恢复下一个任务的状态。
-
CPU上下文切换(含进程/线程/中断上下文切换):
CPU 寄存器和程序计数器是 CPU 在运行任何任务前,所必须依赖的环境,这些环境就叫做 CPU 上下文。
CPU 寄存器:CPU 内部一个容量小,但是速度极快的内存(缓存)。类似衣服口袋,而内存像书包,硬盘则是家里的柜子。
**程序计数器:**存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。
CPU上下文切换:保存前一个任务的CPU上下文,加载新任务的上下文,然后跳转程序计数器所指的新位置,运行新任务。
进程切换:
-
触发切换:
- 时间片用完(分时调度)。
- 进程主动阻塞(如等待 I/O)。
- 高优先级进程抢占。
-
保存当前进程上下文:
- 进程控制块(PCB):保存寄存器状态、程序计数器、内存页表、文件描述符等。
- 内存隔离:切换时需更新内存管理单元的页表基址寄存器,保证进程地址空间独立。
-
切换内核栈:
- 每个进程有独立的内核栈,用于执行系统调用时的内核代码。
-
调度新进程:
- 从就绪队列中选择下一个进程,加载其 PCB 中的上下文。
-
恢复新进程上下文:
- 恢复寄存器、内存映射、权限状态等。
- 更新 CPU 的当前进程指针。
线程切换:
线程切换在同一进程内进行,共享内存和资源,因此开销显著低于进程切换。
- 触发切换:
- 时间片用完或线程主动让出。
- 保存当前线程上下文:
- 线程控制块(TCB):保存寄存器状态、线程栈指针等。
- 无需切换内存映射:线程共享进程的地址空间, 页表无需更新。
- 调度新线程:
- 从进程的线程就绪队列中选择下一个线程。
- 恢复新线程上下文:
- 恢复寄存器、栈指针等。
- 无需切换内存或文件资源。
进程的5种状态
进程通信
操作系统提供的机制,允许不同进程之间交换数据或协调工作(终止/挂起)或资源共享(临界资源)。
由于进程间内存相互隔离,必须通过内核或共享资源实现通信。
常见的进程通信方式:
1.管道
创建一块内核缓冲区,连接两个进程,实现单向数据(无格式的流并且大小受限的数据)传输(读端和写端分离)。
-
匿名管道
-
匿名管道是特殊文件只存在于内存,没有存在于文件系统中
-
仅用于 父子进程或兄弟进程(有血缘关系的进程)。
-
通信方式单向,数据只能在一个方向上流动,如果要双向通信,要创建两个管道。
-
特点:临时存在,进程结束后销毁。
-
-
命名管道
- 基于文件系统,可用于 无血缘关系的进程。
- 特点:长期存在(需手动删除)。
2.消息队列
克服了管道通信的数据是无格式的字节流的问题。
内核中维护一个消息链表,进程可通过消息类型发送 / 接收格式化数据块。
消息队列的消息体是用户自定义的数据类型,发送和接收的数据类型保持一致。
-
每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。
用户进程运行在用户态,无法直接访问内核空间的数据。当进程想要发送消息时,它需要将数据从用户空间(用户态内存)复制到内核空间(内核态内存),这个过程需要切换到内核态,由操作系统内核来完成。同样,接收消息时,数据需要从内核空间复制回用户空间,这同样需要内核态的权限。
3.共享内存
解决了消息队列的每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程带来的开销问题。
允许多个进程直接访问同一块物理内存区域,是速度最快的IPC方式。
多个进程直接操作同一内存区域,竞争同个共享资源会造成数据错乱。一般使用信号量,互斥锁,文件锁等同步机制防止竞态条件。
信号量:资源个数,通过P(申请),V(释放)操作确保任何时刻只能有一个进程访问共享资源,进行互斥访问。
4.信号
异步事件通知机制,允许内核或进程通知目标进程某个事件的发生。
信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)。
进程对信号的处理可分为三种:
- 默认行为:由操作系统预定义(如终止、忽略、暂停进程等)。
- 忽略信号:通过
signal(SIGXXX, SIG_IGN)
显式忽略。 - 自定义处理函数:注册用户定义的信号处理函数。
但,有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SIGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。
与前面通信方式不同,前面都是工作于同一主机,那么如果是不同主机,就需要用到:Socket。
Socket:
也为套接字,是一种网络通信的抽象接口,用于实现不同设备(或同一设备)上的进程间通信(IPC)。
允许不同主机(或同一主机)上的进程通过网络协议(如 TCP/UDP/IP)交换数据。
线程通讯
多个线程访问共享资源时,必须通过同步机制避免数据竞争
1.互斥锁
在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁,确保同一时间只有一个线程访问共享资源。
2.条件变量
一般结合互斥锁使用,让线程在特定条件满足前挂起等待,条件满足后被唤醒,典型应用:生产者-消费者模型。
4.自旋锁
自旋锁是一种基于忙等待(Busy-Waiting)的同步机制。
通过 CPU 提供的 CAS 函数(Compare And Swap),当线程尝试获取锁失败时,它会循环检查锁的状态(“自旋”),直到它拿到锁。
等待时间较短的情况下效率较高,因为避免了线程上下文切换的开销。但长时间等待会导致CPU资源的浪费。
适用于多核系统,且临界区代码执行时间非常短的场景。
5.读写锁
读写锁区分读操作和写操作,允许多个读线程并发访问共享资源,但写线程必须独占访问。
(读者写者问题:可以多个读者同时都读,但是如果有写者在写,则其他写者不能写,读者也不能同时读)
- 读锁:多个线程可同时获取读锁,共享资源。
- 写锁:独占锁,获取时需确保无读锁或写锁存在。
适用读多写少的场景。
4.信号量(同上)
进程调度算法
1.先来先服务FCFS
先来后到,每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。
利于长作业,不利于短作业(等待时间长,但是运行时间短)。想象排队付钱。
2.短作业优先SJF
每次从当前已到达且未执行的进程中选择运行时间最短的那个来执行
利于短作业,不利于长作业(长期不会被调度)
3.高相应比优先HRRN
综合FCFS和SJF,响应比Rp=1+等待时间/服务时间。Rp越高越先被调度。
从公式来看,当等待时间相同时,服务时间越短的,Rp越大(SJF)
当服务时间相同时,等待时间越长,Rp越大(FCFS)。
4.时间片轮转RR
每个进程被分配一个时间段(时间片),该进程只能在该段时间内运行。
时间片用完,CPU直接分配给其他进程。
时间片没用完但进程结束了,CPU也立即切换。
5.最高优先级
优先级分为
静态优先级:创建进程时,就确定了的,不再变化
动态优先级:根据进程动态变化调整优先级。进程运行时间增加(降低其优先级);就绪队列进程等待时间增加(增加队列优先级)。
处理优先级分为:
非抢占式:如果就绪队列有优先级更高的,也得先等将当前进程运行完
抢占式:如果就绪队列有优先级更高的,则当前进程挂起,运行更高进程。
6.多级反馈队列
综合先来先服务,时间片轮转和最高优先级。
多级:设置多个队列,每个队列优先级从高到低,同时优先级越高时间片越短
反馈:如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列。
防止饥饿:周期性的优先级调整,将低优先级队列中等待时间过长的进程重新提到高优先级队列,避免它们被饿死。
来看具体调度过程:
当一个新进程到达时,它会被放入最高优先级的队列。如果它在该队列的时间片内没有完成,就会被移到下一个较低优先级的队列。
当较高优先级的队列为空,调度较低优先级队列,如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,将其移入原队列末尾,转而去运行优先级高的队列。
感谢大家的观看,刚刚考完操作系统,结合小林Coding,总结了一些知识点>W<