目录

操作系统基础


一、进程管理

进程 Process

进程是操作系统中资源分配的基本单位,拥有独立的,互相隔离的地址空间。

进程通信 IPC

  1. 管道(Pipes):单向流动,同步操作
  2. 消息队列:操作系统提供的消息传递机制,读写操作涉及用户态-内核态切换开销
  3. 共享内存:多个虚拟内存空间映射到同一物理地址,实现共享,实现高效的通信,但需要考虑数据竞争
  4. 信号量:用于进程间的互斥与同步,是一个整型计数器
  5. 信号:一种异步通信机制,用于通知进程/线程某个正常或异常事件的发生,例如 Ctrl+CCtrl+Z
  6. 套接字(Sockets):通过网络接口进行通信,支持 TCP/UDP/本地三种模式

进程上下文切换

  1. 保存当前进程上下文到 PCB(process control block)中,包括CPU 寄存器、程序计数器、内存管理信息(虚拟内存映射、页表)等
  2. 根据调度算法如 Linux 中的完全公平调度(CFS),选择下一个进程
  3. 加载新进程的上下文

线程 Thread

线程是调度的基本单位,线程之间共享进程的资源。

线程共享进程中的地址空间和全局变量,每个线程有自己独立的栈和寄存器。

线程的实现

  1. 用户态线程:在用户空间实现,生命周期由应用进程负责,系统无感知,上下文切换不涉及内核,因此很轻量;但无法利用多核 CPU;同时有 IO 阻塞问题

  2. 内核态线程:在内核中实现的线程,由内核负责其生命周期,上下文切换需要进入内核态,因此重量级;能利用多核CPU实现并行

线程模型

  1. 1:1 模型(Linux pthread):每个用户态线程对应一个内核态线程。这种模型可以充分利用多核优势,但资源开销较大
  2. N:1 模型:所有用户态线程映射到一个内核态线程上,简单但无法充分利用多核优势
  3. M:N 模型:多个用户态线程映射到多个内核态线程上,平衡了并行执行和资源开销问题,但实现较为复杂

线程通信

  1. 共享内存
  2. 管道
  3. 信号量:控制对共享资源的访问
  4. 信号:信号是一种异步通知机制,用于通知进程/线程某个事件的发生
  5. 消息队列

线程上下文切换

  1. 包括栈和寄存器
  2. 同一进程不同线程的切换,共享地址空间,只用切换线程上下文
  3. 若涉及不同进程间线程切换,需要切换进程和线程的上下文

陷入内核态的原因

  1. 用户态的线程发起系统调用
  2. 中断处理(硬件中断和软件中断)

多线程冲突

信号量:PV 操作,P 相当于加锁,V 解锁

哲学家就餐问题:

  1. 不引入第三方:餐具脏的和干净的,支持高并发
  2. 引入第三方:服务生(全局互斥锁),服务生可能会忙不过来,解决方案:分片

哲学家就餐问题-wiki

调度算法

  • 时间片轮转(Round Robin):每个进程被分配一个时间片,允许该进程在该时间片内运行

  • 最高优先级(HPF):优先选择优先级最高的进程执行

  • 多级反馈队列(MFQ)

    1. 设置多个队列,每个队列优先级从高到低,优先级高的时间片短;优先级低的时间片长
    2. 未执行完的任务将从高优先级转向低优先级
    3. 新进程被放入第一级队列的末尾,按先来先服务调度

协程 Coroutine

协程是一种更轻量级的线程,它是用户级的调度单位,可以在用户空间内控制执行。

内存空间:协程在同一线程内运行,彼此之间共享线程的地址空间,但每个协程有自己的栈

开销:创建和切换协程的开销非常小,因为不涉及内核的调度,完全在用户空间内完成。

通信方式:协程之间的切换不涉及内核态,可以通过直接调用实现。

并发性:协程适用于高并发网络应用。

  • 互斥锁:阻塞,让出 CPU
  • 自旋锁:忙等待获取锁,占用 CPU
  • 读写锁:读多写少
  • 悲观锁:互斥访问共享资源,冲突成本低
  • 乐观锁:CAS,lock-free,先改,改完验证有没有冲突,冲突成本高

例子:在线文档多人编辑,乐观锁

中断

中断是系统响应硬件设备请求的一种机制。

中断处理程序分两部分:

  • 上半部是由硬件触发,即硬中断 interrupt,用来快速处理中断;
  • 下半部是由内核触发,即软中断 softirq,用来异步处理上半部未完成的工作,通常耗时较长;

硬中断会让 CPU 停止正在执行的工作去立即响应,而软中断是以内核线程的方式执行的(内核线程命名为 ksoftirqd/CPU编号)。

二、内存管理

虚拟内存技术:提供程序空间到物理内存的映射

CPU 中的内存管理单元 MMU:可以将虚拟地址转换为物理地址

操作系统是如何管理虚拟地址与物理地址之间的关系?

内存分段

将程序按逻辑段分割(如代码分段、数据分段、栈段、堆段)并使用 段表 管理映射关系。

https://cdn.nlark.com/yuque/0/2024/png/23073858/1719133378086-944e7b27-f344-49c1-972d-69301d53aeac.png

外部内存碎片内存交换效率低(需要 swap 分区整理内存空间)

内存分页

分页 是把虚拟内存和物理内存切成固定大小的空间(称为页,一页 4KB),并使用 页表 来管理映射关系。

https://cdn.nlark.com/yuque/0/2024/png/23073858/1719133985675-1dc99aa6-7fc3-4dc7-8f9f-ea32c67474a6.png

缺页中断处理

  1. 当进程访问的虚拟地址在页表中查询不到,就触发 缺页中断
  2. 此时进程切换到内核态,执行缺页中断函数:
  3. 如果有空闲物理内存则直接分配,否则尝试内存回收,有两种方式:直接内存回收或者后台内存回收
  4. 页分为文件页和匿名页,干净的文件页可以直接回收,脏文件页需要先写回磁盘再回收;匿名页无实际载体,不能直接释放,需通过 Swap 机制暂时放到磁盘中。回收基于 LRU 算法
  5. 若内存回收还无法满足申请需要,系统触发 OOM (Out of Memory),杀死一个占用内存最高的进程
  6. 若申请成功,则更新进程页表,然后返回用户态

多级页表

利用局部性原理,将页表索引拆成多级,二级页表按需分配,可以节省内存空间

TLB

页表缓存,在 CPU 中

https://cdn.nlark.com/yuque/0/2024/png/23073858/1719134902460-ba06ee04-4e5a-4e56-bc9b-f73b84a38bb4.png

段页式内存

分段+分页的结合,先把程序按段拆分,再把每个段按页拆分

QA

虚拟内存有什么作用?

  1. 虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些不频繁使用的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
  2. 由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。
  3. 页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。

在 4GB 物理内存的机器上申请 8 GB,会发生什么?

  • 在 32 位系统,因为进程最大能申请 3 GB 大小的虚拟内存,所以会申请失败。

  • 在 64 位系统,因为进程理论上最大能申请 128 TB 大小的虚拟内存,所以申请 8G 内存也是没问题,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有 Swap 分区

  • 如果没有 Swap 分区,因为物理空间不够,进程会被操作系统杀掉,原因是 OOM(内存溢出)

  • 如果有 Swap 分区,即使物理内存只有 4GB,程序也能正常使用 8GB 的内存,进程可以正常运行

网络协议

网络编程

Linux TCP backlog

当应用程序调用 listen 系统调用让一个 socket 进入 LISTEN 状态时,需要指定一个参数: backlog ,这个参数经常被描述为:新连接队列的长度限制。

首先,TCP 连接需要进行三次握手,期间一个连接建立过程有两种状态:

  1. 收到 SYN 包,状态变为半连接 SYN RECEIVED
  2. 收到 ACK 包,状态变为完全连接 ESTABLISHED

其中 Linux 有两个队列分别维护这两种状态的连接:

  • 半连接状态队列(syns queue),长度由系统统一指定
  • 完全连接状态队列(accept queue),长度由应用程序单独指定(backlog)

深入理解Linux TCP backlog