一、先从整体看:操作系统到底在解决什么问题
如果把计算机看成一套完整系统,那么最底层是硬件,比如:
- 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 时,需要:
- 保存 A 的现场
- 恢复 B 之前的现场
- 让 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 是后端程序最常见的状态之一
- 高并发不是多开线程就行,还要考虑等待模型
- 并发系统一旦资源竞争处理不好,就可能死锁
开始真正理解:程序在机器上是怎么被操作系统托起来运行的。