从零起步学习计算机操作系统:进程篇(知识扩展提升)

0 阅读7分钟

Q1:进程间的通信方式有哪些? A1:

管道:分为有名管道和匿名管道

匿名管道 顾名思义,它没有名字标识,匿名管道是特殊文件只存在于内存,没有存在于文件系统中,shell命令中的 “ | ” 竖线就是匿名管道,通信的数据是无格式的流并且大小受限,通信的方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道,再来匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。

命名管道 突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管通的前提,需要在文件系终创建一个类型为p的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。另外,不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持lseek之类的文件定位操作。

消息队列:

消息队列克服了管道通信的数据是无格式的节流的问题,消息队列实际上是保存在内核的「消息链表」,消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据关型保持一致,这样才能保证读取的数据是正确的。消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之同的拷贝过程。

共享内存:

共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度,享有最快的进程间通信方式之名。但是便捷高效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据的错乱。

信号量:

信号量可以保护共享资源,以确保任何时刻只能有一个进程访问共享资源,这种方式就是互斥访问。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数
器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是P操作和V操作。

信号:

与信号量名字很相似的叫信号,它两名字虽然相似,但功能一点儿都不一样。信号是异步通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通如用户空间的进程发生了哪些系统事件,信号事件的来源主要有硬件来源(键盘Cltr+C)和软件来源(如 kill 命令),一旦有信号发生,进程有三种方式响应信号1.执行默认操作、2.捕捉信号、3.忽略信号。有两个信号是应用进程无法捕捉和忽略的,即SIGKILL和SIGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。

Q2:讲解一下互斥与同步

A2:

 互斥(Mutual Exclusion)

定义:在同一时刻,只允许一个线程/进程访问共享资源(临界区)。 核心目的:防止竞态条件(Race Condition) ,保证数据一致性。

🚽 厕所类比: 公共厕所只有一个坑位(共享资源)。 互斥就是门锁:一个人进去后锁门,其他人必须在外面等,直到里面的人出来。 目的:避免两个人同时进去的尴尬(数据错乱)。

 同步(Synchronization)

定义:多个线程/进程在执行次序上协调,相互等待以达到某种执行顺序核心目的:保证任务执行的时序性协作性

🏃 接力赛类比: 4 名运动员跑接力赛。 同步就是交接棒:第二个人必须等第一个人跑到交接区并递棒后才能跑。 目的:保证顺序正确(A 做完,B 才能做)。

Q3:讲解一下锁,包括互斥锁,自旋锁,读写锁,乐观锁与悲观锁

A3:

悲观锁 vs 乐观锁(设计理念)

这是最上层的并发控制策略,不仅适用于代码,也适用于数据库。

悲观锁 (Pessimistic Locking)

核心思想“总觉得别人会改我的数据,所以操作前先上锁。”

  • 行为:获取锁 → 执行操作 → 释放锁。
  • 适用场景:写操作多,冲突频繁。
  • Java 实现synchronized, ReentrantLock
  • 数据库实现SELECT ... FOR UPDATE

乐观锁 (Optimistic Locking)

核心思想“觉得别人不会改我的数据,所以先操作,提交时检查版本。”

  • 行为:读取数据(记版本号) → 执行操作 → 提交时对比版本号(CAS)。
  • 适用场景:读多写少,冲突少。
  • Java 实现AtomicInteger (CAS), LongAdder
  • 数据库实现version 字段(UPDATE table SET v=v+1 WHERE id=1 AND version=old_version)。

​编辑

互斥锁 vs 自旋锁(等待机制)

当锁被占用时,线程该怎么办?这是操作系统层面的核心区别。

1. 互斥锁 (Mutex Lock / Blocking Lock)

机制:如果锁被占用,线程放弃 CPU,进入阻塞状态(Blocked) ,由操作系统调度唤醒。

  • 优点:不浪费 CPU 资源,适合锁持有时间的场景。
  • 缺点:用户态→内核态切换,上下文切换开销大(微秒级)。
  • Java 对应synchronized (重量级), ReentrantLock (阻塞后)。
📉 流程:
申请锁 → 失败 → 挂起线程 (OS 介入) → 睡眠 → 被唤醒 → 重新竞争

2. 自旋锁 (Spin Lock)

机制:如果锁被占用,线程不放弃 CPU,在用户态循环检查(Busy Wait),直到锁释放。

  • 优点:无上下文切换,适合锁持有时间极短的场景。
  • 缺点:浪费 CPU 资源,若锁持有时间长,会导致 CPU 100%。
  • Java 对应Atomic 类(CAS 自旋),ReentrantLock (尝试获取阶段)
📈 流程:
申请锁 → 失败 → while(!tryLock()) { } (空循环) → 成功

 关键权衡:什么时候自旋?

Java 的 ReentrantLock 其实是 自旋 + 互斥 的组合:

  1. 先自旋尝试几次(避免轻微竞争就阻塞)。
  2. 如果自旋失败,再挂起线程(进入互斥阻塞)。

 警告:Java 没有直接暴露 SpinLock 类,因为单核 CPU 自旋是死循环(永远拿不到锁),多核 CPU 也需谨慎使用。

读写锁(访问模式)

针对读多写少的场景优化,提升并发度。

 核心规则

操作读锁 (Read Lock)写锁 (Write Lock)
读锁✅ 兼容 (共享)❌ 互斥
写锁❌ 互斥❌ 互斥 (独占)
  • 读 - 读:不互斥(多个线程可同时读)。
  • 读 - 写:互斥。
  • 写 - 写:互斥。

 潜在问题与优化

  • 写饥饿:如果读请求源源不断,写线程可能永远拿不到锁。
  • 锁降级:Java 支持 写锁→读锁 降级,不支持 读锁→写锁 升级(防死锁)。
  • 进阶StampedLock (Java 8+),性能更好,支持乐观读,但不可重入。