操作系统的理解性总结

5 阅读18分钟

一、先从整体看:操作系统到底在解决什么问题

如果把计算机看成一套完整系统,那么最底层是硬件,比如:

  • CPU
  • 内存
  • 磁盘
  • 网卡
  • 键盘鼠标等输入输出设备

但硬件本身不会主动“优雅地服务程序”,所以如果没有一层统一管理,程序想跑起来会非常麻烦。

如果没有操作系统,应用程序想做任何事都要自己直接面对硬件:

  • 自己抢 CPU
  • 自己分配内存
  • 自己决定文件怎么存到磁盘
  • 自己操作网卡收发数据
  • 自己和别的程序协调资源

所以操作系统存在的根本意义是:操作系统是程序和硬件之间的管理层。

它一方面管理底层硬件资源,另一方面为上层程序提供统一、方便、安全的使用方式。

操作系统主要在解决三类问题:

1. 资源管理问题

硬件资源是有限的,CPU、内存、磁盘、网络都要统一调度,不能让程序随便乱抢。

2. 隔离与安全问题

程序之间不能互相乱改数据,普通程序也不能直接无限制操作硬件,不然系统会非常危险。

3. 抽象与简化问题

操作系统把复杂的底层细节封装起来,让应用程序不需要直接面对硬件,而是通过更统一、更稳定的方式使用机器能力。

二、操作系统主要在管什么

如果把操作系统的工作拆开来看,它最核心就是在管理四类东西。

1. 管 CPU

CPU 是真正干计算活的地方。
操作系统要决定:

  • 当前哪个任务先运行
  • 哪个任务后运行
  • 每个任务能运行多久
  • 多个任务之间如何切换

这背后就会引出:

  • 进程
  • 线程
  • 调度
  • 上下文切换

也就是说,CPU 管理本质上是在回答:谁现在能上场干活。

2. 管内存

程序运行起来之后,必须占用内存。
操作系统要管:

  • 哪个程序能用哪些内存
  • 程序之间怎么互相隔离
  • 内存不够怎么办
  • 为什么程序感觉自己有一整块连续空间

这背后会引出:

  • 地址空间
  • 内存隔离
  • 虚拟内存

本质上是在回答:程序运行时用的空间怎么分,怎么保护。

3. 管磁盘和文件系统

程序不可能所有数据都只放内存,很多信息必须落到磁盘上长期保存。
操作系统要负责:

  • 文件怎么组织
  • 文件怎么读写
  • 数据如何长期保存
  • 目录结构怎么维护

4. 管输入输出设备

包括:

  • 磁盘 IO
  • 网络 IO
  • 键盘鼠标输入
  • 网卡数据收发

尤其对后端开发来说,网络 IO 特别关键,因为大量程序其实都不是纯计算型,而是处在“等待 IO”的状态。

这部分最后就会自然连接到:

  • 阻塞 / 非阻塞
  • 同步 / 异步
  • IO 多路复用
  • select / poll / epoll

三、程序、进程、线程:操作系统是怎么组织运行任务的

1. 程序是什么

程序是静态的。

比如:

  • 一个 Python 文件
  • 一个 Java jar 包
  • 一个可执行文件
  • 一个安装好的应用程序

它们本质上都是放在磁盘上的代码和数据,还没有真正运行起来。

所以程序可以理解成:一份静态的代码蓝图。

2. 进程是什么

当程序真正被执行时,事情就变了。

操作系统会:

  • 为它分配内存
  • 为它建立运行环境
  • 跟踪它的状态
  • 给它安排 CPU 运行

这时候,程序就不再只是静态代码,而变成了:进程。

所以进程实际上是程序的一次运行实例。同一个程序可以运行多次,每运行一次,都可以对应一个独立进程。比如你开两个浏览器窗口,如果底层实现是两个独立运行实例,那它们就可能是两个不同进程。

进程之所以重要,不只是因为“程序跑起来了”,而是因为操作系统需要有一个单位来承载:

  • 当前执行状态
  • 占用的资源
  • 已打开的文件
  • 使用的内存空间
  • 正在运行的执行流

所以进程本质上更像:操作系统管理运行任务和分配资源的重要单位。

进程的核心价值是:

  • 独立
  • 隔离
  • 可管理

也就是说,操作系统用进程把“一个程序的运行现场”包起来,方便调度和保护。

3. 为什么要有进程

因为没有进程的话,多个程序一旦同时运行,很容易混在一起:

  • 内存互相覆盖
  • 资源互相抢占
  • 一个程序崩溃影响整个系统
  • 无法清晰区分“谁在干什么”

所以进程的意义是:给每个运行中的程序一个相对独立的资源空间和身份。

也就是常说的:进程更强调资源隔离。

4. 线程是什么

但进程还不是最细的“执行单位”,真正执行代码的,不是进程这个壳子,而是进程里的线程。

线程可以理解成:进程内部的执行流。

也就是说,一个进程不一定只有一条执行路径,它可以有多个线程同时在内部工作。

可以把关系理解成:

  • 进程像一个工作空间或容器
  • 线程像这个空间里真正干活的人

线程会共享所属进程的大部分资源,比如:

  • 地址空间
  • 已打开文件
  • 很多进程级数据

但线程自己也有独立的一部分状态,比如执行位置、栈等。

所以线程的关键词是:轻量执行单元。

5. 为什么有了进程还要有线程

如果没有线程,那么程序内部如果想并发做很多事,就只能不断新建进程。
但进程比较重,因为它自带一整套独立资源环境。

很多时候,我们只是希望在同一个程序内部:

  • 同时处理多个任务
  • 共享同一批数据
  • 不想每次都重新创建一整套进程资源

这时候线程就很合适。所以有了进程还要线程,是因为:进程解决资源隔离问题,线程解决进程内部更轻量的并发执行问题。

6. 进程和线程最核心的区别

进程更偏资源容器,线程更偏执行流。

再展开一点:

  • 进程隔离更强,资源更独立
  • 线程共享更多资源,创建和切换通常更轻
  • 不同进程之间通信更复杂
  • 同一进程内线程共享内存更方便,但也更容易出现竞争问题

所以线程不是“更高级的进程”,而是:在同一进程内部,用更低成本实现并发的一种机制。

四、上下文切换:为什么线程多了不一定更快

看完线程后可能会觉得:

  • 线程更轻
  • 那我开很多线程不就更快了吗?

现实里不一定,因为线程切换不是免费的。

1. 什么是上下文

上下文可以先理解成:一个任务想继续运行下去,所必须保存的现场信息。

比如一个线程执行到一半时,它当前的:

  • 执行位置
  • 寄存器状态
  • 栈信息
  • 某些运行环境信息

都要被保存起来。 不然等它下次再回来时,就不知道从哪里继续了。

2. 什么是上下文切换

CPU 不可能永远只运行一个任务,操作系统要不断在多个任务间切换。

当 CPU 从执行任务 A 转到执行任务 B 时,需要:

  1. 保存 A 的现场
  2. 恢复 B 之前的现场
  3. 让 B 从它上次停下的位置继续

这个过程就叫:上下文切换。

所以切换是一个保存与恢复现场的完整动作。

3. 为什么切换有开销

线程切换有开销,核心原因就在于:CPU 会花一部分时间在“切任务”,而不是在“真正干活”。

开销主要来自:

  • 保存当前线程的运行现场
  • 恢复目标线程的运行现场
  • 调度器本身的调度判断
  • 可能带来的缓存命中率下降
  • 线程之间同步与竞争的额外成本

所以线程虽然轻,但不是零成本。

4. 为什么线程多了不一定更快

线程多,意味着潜在并发能力变强,但同时也意味着:

  • 更多调度
  • 更多切换
  • 更多竞争
  • 更多同步
  • 更多资源占用

所以如果线程数远超合理范围,系统可能会把很多时间浪费在上下文切换上。

最终就会出现一种很典型的情况:看起来线程很多、大家都很忙,但 CPU 实际大量时间花在“换人”而不是“做事”。

这就是为什么后端程序不是线程越多越好,而需要根据:CPU 核数、任务类型、IO 比例、锁竞争情况来控制线程数量。

五、用户态、内核态、系统调用:程序为什么不能直接操作硬件

这部分是理解“程序如何使用操作系统能力”的关键。

1. 为什么程序不能直接操作硬件

如果普通程序都能直接无限制操作:

  • 内存
  • 磁盘
  • 网卡
  • CPU 控制指令

那系统会非常危险。

比如:

  • 一个程序写错了就可能破坏整个系统
  • 一个恶意程序可以直接控制关键资源
  • 程序之间的隔离和安全根本无法保证

所以操作系统必须做权限分层。

2. 什么是用户态和内核态

用户态和内核态,本质上是:CPU 执行代码时的两种不同权限级别。

用户态

普通应用程序大多数时候运行在用户态。
在这个状态下,程序权限较低,不能直接做很多敏感操作。

内核态

操作系统核心代码运行在内核态。 在这个状态下,系统拥有更高权限,可以真正管理硬件和底层资源。

可以理解成:

  • 用户态:普通程序活动区
  • 内核态:操作系统核心管理区

这种划分的目的就是:

  • 安全
  • 稳定
  • 可控

3. 什么是系统调用

既然用户态程序权限有限,那它想做底层操作时怎么办?

通过系统调用向操作系统申请服务。

系统调用可以理解成:用户态程序请求内核帮忙完成某项操作的标准方式。

比如:

  • 读文件
  • 写文件
  • 发网络请求
  • 创建线程
  • 创建进程
  • 申请内存

这些很多都不是程序自己直接做,而是通过系统调用让操作系统代为完成。

所以系统调用本质上是:用户态 -> 内核态 -> 用户态 的一个过程。

也就是说,程序发起请求,切到内核态,由操作系统处理,处理完再切回用户态。

4. 为什么系统调用有成本

因为这不是普通函数调用,而是一次权限级别切换。

它通常涉及:

  • 状态切换
  • 现场保存与恢复
  • 内核执行逻辑
  • 再返回用户态

这也是为什么高性能程序很关注:

  • 系统调用频率
  • 减少不必要的内核切换
  • 高效的 IO 模型

5. 这和后端有什么关系

后端程序平时做的很多事,其实都离不开系统调用,比如:

  • 文件读写
  • 网络收发
  • 线程创建
  • 进程管理
  • 内存使用

六、阻塞 / 非阻塞、同步 / 异步:程序等待结果时到底在怎么等

最核心的区分方式一定要先记住:阻塞 / 非阻塞,关注的是线程会不会被卡住;同步 / 异步,关注的是结果回来和任务完成的方式。

这两组概念不是一个维度。

1. 阻塞 / 非阻塞

阻塞

阻塞的意思是:线程发起一个操作后,如果结果还没准备好,它就只能停在那里等。

比如线程去读网络数据,如果数据没到,而调用方式又是阻塞的,那这个线程就得卡在这儿,不能继续做别的事。

非阻塞

非阻塞的意思是:线程发起操作后,如果结果还没准备好,它不会一直卡住,而是先返回,线程还能继续做别的事。

所以阻塞和非阻塞的核心问题是:当前线程会不会被等待动作卡住。

2. 同步 / 异步

同步

同步的核心是:结果需要调用方自己主动等待、主动确认、主动获取。

也就是说:事情是我发起的,结果也得我自己盯着拿。

异步

异步的核心是:任务完成后,不需要调用方一直自己盯着,系统会通过通知、回调、事件等方式告诉你,或者替你推进后续逻辑。

所以同步和异步关注的是:结果是怎么回来的。

3. 为什么大家总把它们混在一起

因为很多常见场景里,它们经常同时出现。比如最传统的读文件、发请求代码,经常既是:

  • 阻塞的:线程被卡住等结果
  • 同步的:结果还得自己等回来

所以很多人就误以为阻塞 = 同步。

但其实是两个维度的问题:

  • 阻塞 / 非阻塞:线程状态维度
  • 同步 / 异步:结果返回方式维度

4. 这对后端有什么意义

因为后端程序很多时候都在等:

  • 网络数据
  • 磁盘数据
  • 数据库结果
  • 远程服务响应
  • 锁释放

怎么等,会直接影响:

  • 线程利用率
  • 服务吞吐
  • 并发能力
  • 性能表现

所以这四个概念,是理解高并发和 IO 模型的基础。

七、IO 多路复用:为什么高并发服务不能一个连接一个线程傻等

1. 问题背景

服务器里有很多网络连接,但不是每个连接都时时刻刻有数据。
如果给每个连接都分一个线程,让每个线程一直阻塞等数据,就会有几个问题:

  • 线程太多,资源占用高
  • 大量线程其实都在空等
  • 上下文切换成本很高

所以问题来了:能不能少用一些线程,同时等很多个连接,谁准备好了就处理谁?

这就是 IO 多路复用要解决的问题。

2. 什么是 IO 多路复用

IO 多路复用就是用一个或少量线程,同时监视很多个 IO,谁就绪了就处理谁。也就是说,它不是为每一路连接都配一个线程,而是把“等待很多连接”的动作集中处理。

它优化的重点不是“让某一个连接更快”,而是:让大量连接的等待方式更高效。

3. select、poll、epoll

它们本质上都在做同一件事:帮助程序同时关注很多个 IO,看哪些已经准备好了。

select

比较早期的方案。
核心问题是:

  • 监听数量有限制
  • 每次都要重新提交关注集合
  • 返回后还要自己遍历全集

poll

和 select 思路类似,只是管理方式更灵活一些。
但本质问题没变:

  • 仍然要遍历关注集合
  • 连接多时仍然会有明显扫描成本

epoll

Linux 下非常经典的高并发方案。
核心思路是:

  • 先注册好感兴趣的连接
  • 等谁就绪了,系统更高效地返回就绪结果
  • 不用像 select / poll 那样每次都低效扫描全集

所以 epoll 为什么经典?
因为在“大量连接、少量活跃”的高并发场景下,它更高效。

4. IO 多路复用和后端的关系

这部分对后端特别重要,因为很多高并发网络服务本质上都在做同一件事:用尽量少的线程,高效管理大量连接。

所以后面:

  • Nginx
  • Redis
  • Netty
  • 高并发网关
  • 事件驱动模型

都会不断看到 IO 多路复用的影子。

八、死锁:为什么并发系统有时会彻底卡死

1. 什么是死锁

死锁指的是:多个线程或进程因为争夺资源而互相等待,导致谁都无法继续执行。

注意,它不是普通“慢一点”,而是如果没有外力干预,就可能一直僵在那里。其实就是:你等我,我等你,最后谁也走不动。

2. 死锁为什么会发生

最典型的情况是:

  • 每个人都先拿到一部分资源
  • 但不释放已经拿到的资源
  • 同时又继续等待别人手里的资源

一旦这种等待关系形成闭环,就可能死锁。

比如最经典的例子:

  • 线程 1 先拿锁 A,再等锁 B
  • 线程 2 先拿锁 B,再等锁 A

于是两边互相等待,谁都走不下去。

3. 死锁的四个必要条件

这部分是面试重点。

死锁成立通常要同时满足四个条件:

1.互斥:

资源同一时刻只能给一个执行单元使用。

2.请求并保持:

已经拿到一部分资源,还继续申请新资源,而且不释放旧资源。

3.不可剥夺:

已占有的资源不能被别人强行夺走,只能等持有者自己释放。

4.循环等待:

多个线程 / 进程之间形成首尾相接的等待环。

这四个条件缺一不可。

4. 怎么避免死锁

思路就是:破坏四个必要条件中的至少一个。

工程里最常见、最实用的方法是:固定加锁顺序

比如所有线程都规定:

  • 先拿锁 A
  • 再拿锁 B

这样就不容易形成循环等待。

另外还有一些常见思路:

  • 一次性申请所需资源
  • 获取失败就释放重试
  • 减少锁持有时间
  • 不要在持锁时做耗时操作

死锁这部分要建立一种并发资源竞争意识:只要有多线程、多资源、锁嵌套,就要警惕有没有形成等待环。

九、总结

以上操作系统内容是在回答同一个大问题:程序到了机器上以后,操作系统是怎么让它安全、有序、高效地跑起来的?

第一步:操作系统先作为管理层存在

它夹在程序和硬件之间,负责:

  • 管 CPU
  • 管内存
  • 管文件和磁盘
  • 管各种 IO 设备

第二步:程序运行后变成进程

进程是程序的一次运行实例,是资源管理的重要单位。

第三步:进程里有线程真正执行代码

线程是进程里的执行流,负责具体干活。

第四步:操作系统不断调度线程 / 进程

不同任务之间来回切换,这就会涉及上下文切换。

第五步:程序权限受限,不能直接碰硬件

所以需要区分用户态和内核态。

第六步:程序想用底层资源时,要通过系统调用申请

读文件、发网络、创建线程等都离不开这一层。

第七步:程序很多时间都在等 IO

所以会涉及阻塞 / 非阻塞、同步 / 异步这些等待模型。

第八步:高并发场景下,要高效地等待很多连接

这就引出了 IO 多路复用,以及 select / poll / epoll。

第九步:并发资源竞争时,还可能出现死锁

所以必须考虑锁顺序、资源申请方式和等待关系。

后端开发角度

这部分操作系统知识最重要的价值是开始有一种真实的机器视角。慢慢意识到:

  • 后端服务不是飘在空中,它最终是进程
  • 服务处理请求,最终靠线程执行
  • 线程太多会有切换开销
  • 程序很多能力要通过系统调用获得
  • 等 IO 是后端程序最常见的状态之一
  • 高并发不是多开线程就行,还要考虑等待模型
  • 并发系统一旦资源竞争处理不好,就可能死锁

开始真正理解:程序在机器上是怎么被操作系统托起来运行的。