操作系统基础
一、进程管理
进程 Process
进程是操作系统中资源分配的基本单位,拥有独立的,互相隔离的地址空间。
进程通信 IPC
- 管道(Pipes):单向流动,同步操作
- 消息队列:操作系统提供的消息传递机制,读写操作涉及用户态-内核态切换开销
- 共享内存:多个虚拟内存空间映射到同一物理地址,实现共享,实现高效的通信,但需要考虑数据竞争
- 信号量:用于进程间的互斥与同步,是一个整型计数器
- 信号:一种异步通信机制,用于通知进程/线程某个正常或异常事件的发生,例如
Ctrl+C
、Ctrl+Z
- 套接字(Sockets):通过网络接口进行通信,支持 TCP/UDP/本地三种模式
进程上下文切换
- 保存当前进程上下文到 PCB(process control block)中,包括CPU 寄存器、程序计数器、内存管理信息(虚拟内存映射、页表)等
- 根据调度算法如 Linux 中的完全公平调度(CFS),选择下一个进程
- 加载新进程的上下文
线程 Thread
线程是调度的基本单位,线程之间共享进程的资源。
线程共享进程中的地址空间和全局变量,每个线程有自己独立的栈和寄存器。
线程的实现
-
用户态线程:在用户空间实现,生命周期由应用进程负责,系统无感知,上下文切换不涉及内核,因此很轻量;但无法利用多核 CPU;同时有 IO 阻塞问题
-
内核态线程:在内核中实现的线程,由内核负责其生命周期,上下文切换需要进入内核态,因此重量级;能利用多核CPU实现并行
线程模型
- 1:1 模型(Linux pthread):每个用户态线程对应一个内核态线程。这种模型可以充分利用多核优势,但资源开销较大
- N:1 模型:所有用户态线程映射到一个内核态线程上,简单但无法充分利用多核优势
- M:N 模型:多个用户态线程映射到多个内核态线程上,平衡了并行执行和资源开销问题,但实现较为复杂
线程通信
- 共享内存
- 管道
- 信号量:控制对共享资源的访问
- 信号:信号是一种异步通知机制,用于通知进程/线程某个事件的发生
- 消息队列
线程上下文切换
- 包括栈和寄存器
- 同一进程不同线程的切换,共享地址空间,只用切换线程上下文
- 若涉及不同进程间线程切换,需要切换进程和线程的上下文
陷入内核态的原因
- 用户态的线程发起系统调用
- 中断处理(硬件中断和软件中断)
多线程冲突
信号量:PV 操作,P 相当于加锁,V 解锁
哲学家就餐问题:
- 不引入第三方:餐具脏的和干净的,支持高并发
- 引入第三方:服务生(全局互斥锁),服务生可能会忙不过来,解决方案:分片
调度算法
-
时间片轮转(Round Robin):每个进程被分配一个时间片,允许该进程在该时间片内运行
-
最高优先级(HPF):优先选择优先级最高的进程执行
-
多级反馈队列(MFQ):
- 设置多个队列,每个队列优先级从高到低,优先级高的时间片短;优先级低的时间片长
- 未执行完的任务将从高优先级转向低优先级
- 新进程被放入第一级队列的末尾,按先来先服务调度
协程 Coroutine
协程是一种更轻量级的线程,它是用户级的调度单位,可以在用户空间内控制执行。
内存空间:协程在同一线程内运行,彼此之间共享线程的地址空间,但每个协程有自己的栈
开销:创建和切换协程的开销非常小,因为不涉及内核的调度,完全在用户空间内完成。
通信方式:协程之间的切换不涉及内核态,可以通过直接调用实现。
并发性:协程适用于高并发网络应用。
锁
- 互斥锁:阻塞,让出 CPU
- 自旋锁:忙等待获取锁,占用 CPU
- 读写锁:读多写少
- 悲观锁:互斥访问共享资源,冲突成本低
- 乐观锁:CAS,lock-free,先改,改完验证有没有冲突,冲突成本高
例子:在线文档多人编辑,乐观锁
中断
中断是系统响应硬件设备请求的一种机制。
中断处理程序分两部分:
- 上半部是由硬件触发,即硬中断
interrupt
,用来快速处理中断; - 下半部是由内核触发,即软中断
softirq
,用来异步处理上半部未完成的工作,通常耗时较长;
硬中断会让 CPU 停止正在执行的工作去立即响应,而软中断是以内核线程的方式执行的(内核线程命名为 ksoftirqd/CPU编号
)。
二、内存管理
虚拟内存技术:提供程序空间到物理内存的映射
CPU 中的内存管理单元 MMU:可以将虚拟地址转换为物理地址
操作系统是如何管理虚拟地址与物理地址之间的关系?
内存分段
将程序按逻辑段分割(如代码分段、数据分段、栈段、堆段)并使用 段表 管理映射关系。
外部内存碎片 和 内存交换效率低(需要 swap 分区整理内存空间)
内存分页
分页 是把虚拟内存和物理内存切成固定大小的空间(称为页,一页 4KB),并使用 页表 来管理映射关系。
缺页中断处理
- 当进程访问的虚拟地址在页表中查询不到,就触发 缺页中断
- 此时进程切换到内核态,执行缺页中断函数:
- 如果有空闲物理内存则直接分配,否则尝试内存回收,有两种方式:直接内存回收或者后台内存回收
- 页分为文件页和匿名页,干净的文件页可以直接回收,脏文件页需要先写回磁盘再回收;匿名页无实际载体,不能直接释放,需通过 Swap 机制暂时放到磁盘中。回收基于 LRU 算法
- 若内存回收还无法满足申请需要,系统触发 OOM (Out of Memory),杀死一个占用内存最高的进程
- 若申请成功,则更新进程页表,然后返回用户态
多级页表
利用局部性原理,将页表索引拆成多级,二级页表按需分配,可以节省内存空间
TLB
页表缓存,在 CPU 中
段页式内存
分段+分页的结合,先把程序按段拆分,再把每个段按页拆分
QA
虚拟内存有什么作用?
- 虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些不频繁使用的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
- 由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。
- 页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。
在 4GB 物理内存的机器上申请 8 GB,会发生什么?
-
在 32 位系统,因为进程最大能申请 3 GB 大小的虚拟内存,所以会申请失败。
-
在 64 位系统,因为进程理论上最大能申请 128 TB 大小的虚拟内存,所以申请 8G 内存也是没问题,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有 Swap 分区
-
如果没有 Swap 分区,因为物理空间不够,进程会被操作系统杀掉,原因是 OOM(内存溢出)
-
如果有 Swap 分区,即使物理内存只有 4GB,程序也能正常使用 8GB 的内存,进程可以正常运行
网络协议
网络编程
Linux TCP backlog
当应用程序调用 listen
系统调用让一个 socket 进入 LISTEN 状态时,需要指定一个参数: backlog
,这个参数经常被描述为:新连接队列的长度限制。
首先,TCP 连接需要进行三次握手,期间一个连接建立过程有两种状态:
- 收到 SYN 包,状态变为半连接
SYN RECEIVED
- 收到 ACK 包,状态变为完全连接
ESTABLISHED
其中 Linux 有两个队列分别维护这两种状态的连接:
- 半连接状态队列(syns queue),长度由系统统一指定
- 完全连接状态队列(accept queue),长度由应用程序单独指定(backlog)