第 1 章 操作系统概述

第 2 章 进程管理

线程与进程的区别⭐

  1. 定义
  • 进程是操作系统资源分配的基本单位,是一个独立运行的程序实例。每个进程有独立的内存空间、全局变量和系统资源。
  • 线程是进程中的一个执行单元,是CPU调度的基本单位。多个线程共享同一个进程的内存和资源。
  1. 调度。线程是独立调度的基本单位,进程是拥有资源的基本单位。在同一进程中,线程的切换不会引起进程切换。在不同进程中进行线程切换,如从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。
  2. 拥有资源。进程都是拥有资源的基本单位,而线程不拥有系统资源,但线程可以访问其所属进程的系统资源。
  3. 并发性。在引入线程的操作系统中,不仅进程之间可以并发执行,而且多个线程之间也可以并发执行。
  4. 系统开销。进程创建、撤销和切换,所付出的开销远大于线程。
  5. 地址空间。进程的地址空间之间互相独立,同一进程的各线程间共享进程的地址空间。
  6. 通信方面。进程间需要用PV操作、共享存储、消息传递和管道通信等方式进行通信,而线程间可以直接读/写进程数据段(如全局变量)来进行通信。

进程、线程和协程的区别⭐


1. 定义

  • 进程:操作系统资源分配的基本单位,是一个独立运行的程序实例。每个进程有独立的内存空间、全局变量和系统资源。
  • 线程:是进程中的一个执行单元,是CPU调度的基本单位。多个线程共享同一个进程的内存和资源。
  • 协程:是一种用户态的轻量级线程,由程序自身控制调度。协程可以在一个线程内实现多任务切换,而不依赖操作系统内核。

2. 资源分配

  • 进程:每个进程拥有独立的地址空间,系统为其分配资源(如内存、文件句柄)。进程之间资源完全隔离。
  • 线程:同一进程中的线程共享进程的地址空间和资源(如全局变量、文件描述符等)。
  • 协程:协程是基于线程或进程的用户态执行单元,不直接由操作系统分配资源,仅共享其所在线程/进程的资源。

3. 调度方式

  • 进程:进程的调度是由操作系统的内核完成的。操作系统根据一定的调度算法(如先来先服务、时间片轮转、优先级调度等)来分配 CPU 时间给不同的进程。
  • 线程:线程的调度同样主要由操作系统内核来完成。在内核的线程调度器的管理下,根据线程的优先级、等待状态等因素来分配 CPU 时间。
  • 协程:由程序自身调度(用户态切换),无需操作系统介入,调度开销最小。协程通过主动让出控制权(如yieldawait)实现切换。

4. 上下文切换开销

  • 进程:进程切换需要保存和恢复独立的内存空间和资源,开销最大。
  • 线程:线程切换比进程轻量,但需要保存和恢复线程的寄存器状态,仍需内核态支持,开销中等。
  • 协程:协程切换发生在用户态,不涉及内核调度,仅保存与恢复寄存器、堆栈等状态,开销最小。

5. 通信方式

  • 进程:进程之间是完全独立的,每个进程拥有自己的内存空间。需要通过操作系统内核来实现通信,开销较大,但安全性高。常见的进程通信方式有共享内存、消息队列和管道等。
  • 线程:线程共享同一进程的内存和资源,通信比进程间简单。由于共享资源,容易产生竞争,需借助锁和信号量等同步机制。
  • 协程:协程之间共享线程的资源,因此通信更高效,不涉及操作系统内核。通常使用队列、Channel、事件等方式进行通信。

6. 并发与并行

  • 进程:可实现真正的并行,多个进程可以运行在不同的CPU核心上。
  • 线程:可实现并行(在多核CPU上),但因为共享内存资源,容易出现竞争问题。
  • 协程:协程本质上是并发(通过时间片或事件循环模拟),不一定是真正的并行。需要配合多线程或多进程来利用多核CPU。

7. 使用场景

  • 进程:适用于需要高隔离性的任务(如多用户服务、分布式系统)。如:Web服务器中每个请求由一个独立的进程处理。
  • 线程:适用于需要高效并行处理的任务(如图像处理、并行计算)。如:多线程下载、并行文件处理。
  • 协程:适用于高并发但对CPU密集度要求低的任务(如I/O密集型任务)。如:网络爬虫、异步Web服务器(如Python的asyncio)。

僵尸进程和孤儿进程⭐

1. 僵尸进程(Zombie Process)

定义:
僵尸进程是指已经终止运行,但其退出状态尚未被父进程回收的进程。这类进程的进程描述符(PID)仍然保留在系统进程表中,但不再占用内存或CPU资源。

产生原因:
当子进程终止时,会向父进程发送 SIGCHLD 信号,并保留其退出状态(如返回值)。父进程需要通过 wait()waitpid() 系统调用获取该状态。若父进程未调用这些函数,子进程的退出信息会一直保留,导致其成为僵尸进程。

  • 潜在风险:大量僵尸进程可能耗尽 PID,导致无法创建新进程。
  • 解决方法: 父进程主动调用 wait()waitpid():回收子进程退出状态。

2. 孤儿进程(Orphan Process)

定义:
孤儿进程是指父进程已终止,但子进程仍在运行的进程。此时,孤儿进程会被 init 进程(PID 为 1)收养,并由 init 负责在其终止时回收资源。

产生原因:
父进程可能因崩溃、错误或主动退出而终止,导致子进程失去父进程。

特点:

  • 被 init 进程接管:PPID(父进程ID)变为 1,init 进程会正确回收孤儿进程的资源。
  • 常见场景:后台守护进程(daemon)常有意成为孤儿进程。

解决方法:
通常无需手动处理,init 进程会自动回收孤儿进程。但在编程中,可通过以下方式避免意外孤儿进程:

  1. 父进程等待子进程结束:使用 wait()waitpid()
  2. 使用双 fork 技术:创建守护进程时,通过两次 fork() 确保子进程脱离原会话。

进程间通信(IPC)的方式⭐

进程间通信(Inter-Process Communication,IPC)是指在不同进程之间传播或交换信息。

  1. 管道(Pipe)
  • 匿名管道:是一种半双工的通信方式,数据只能单向流动,通常用于具有亲缘关系的进程之间,如父子进程。它在内存中开辟一块缓冲区实现通信,由系统自动分配和管理,通信效率相对较低。
  • 命名管道:可以在不相关的进程之间进行通信,提供了一个路径名与之关联,以FIFO(First In First Out)的方式进行数据传输,有自己的文件系统节点,可以通过文件系统的接口进行访问。
  1. 信号(Signal)
    信号是一种比较简单的进程间通信机制,用于通知进程发生了某种特定事件,如进程终止、键盘中断等。信号可以由系统、其他进程或进程自身发送,进程接收到信号后,会根据信号的类型和进程的设置执行相应的操作,如执行默认动作、忽略信号或执行自定义的信号处理函数。

  2. 消息队列(Message Queue)
    消息队列是一种以消息为单位进行数据传输的通信方式,进程可以向消息队列中发送消息,也可以从消息队列中读取消息。消息队列提供了一种异步通信的机制,发送方和接收方不需要同时运行,消息会在队列中等待被接收。消息队列具有一定的存储容量,不同的消息可以根据类型或优先级进行区分。

  3. 共享内存(Shared Memory)
    共享内存允许不同进程访问同一块物理内存空间,是一种高效的进程间通信方式。多个进程可以直接读写共享内存中的数据,无需进行数据复制,从而提高了通信效率。但是,由于多个进程可能同时访问共享内存,因此需要使用信号量等同步机制来保证数据的一致性和完整性。

  4. 信号量(Semaphore)
    信号量主要用于实现进程之间的同步和互斥,它本质上是一个计数器,用于控制多个进程对共享资源的访问。信号量的值表示当前可用的共享资源数量,当进程需要访问共享资源时,会先检查信号量的值,如果大于0,则可以访问,并将信号量的值减1;如果等于0,则进程需要等待,直到信号量的值大于0。

  5. 套接字(Socket)
    套接字是一种网络通信机制,也可以用于本地进程间通信。它提供了一种通用的、面向连接或无连接的通信方式,允许不同主机或同一主机上的不同进程之间进行数据传输。

  6. 文件(File)
    同进程通过对同一文件进行读写操作来实现信息传递。一个进程将数据写入文件,其他进程从文件中读取数据,以此达到进程间通信的目的。

  7. 内存映射(Memory-Mapped File)
    将文件的内容映射到进程的虚拟地址空间中,使得进程可以像访问内存一样直接访问文件内容,而不需要通过传统的文件 I/O 系统调用。这样多个进程可以通过映射同一个文件到各自的地址空间,实现对同一块数据区域的共享访问,从而达到通信的目的。

  8. RPC(Remote Procedure Call,远程过程调用)
    它是一种通信协议,允许程序调用另一个地址空间的过程或函数,而不需要了解底层的网络细节和远程系统的具体实现。它将网络通信和远程调用进行了封装,使得开发者可以像调用本地函数一样调用远程服务,实现了进程间的跨网络通信。

线程间同步方式(线程安全的实现方式)⭐⭐

线程安全是指在多线程环境下,程序能够正确地执行,不会出现数据竞争、不一致等问题。
线程间的同步是确保多个线程在访问共享资源时能够正确协作、避免数据竞争和状态不一致的关键机制。

1. 互斥锁(Mutex)

  • 原理:通过锁的获取(Lock)与释放(Unlock)实现独占访问共享资源。同一时刻只有一个线程能持有锁。当一个线程获得互斥锁后,其他线程如果试图获取该锁,就会被阻塞,直到持有锁的线程释放锁。

2. 读写锁(Read-Write Lock)

  • 原理:读写锁允许多个线程同时进行读操作,但在写操作时会独占资源。当有线程进行写操作时,其他线程的读和写操作都会被阻塞;当只有读操作时,多个线程可以同时进行。这种锁适用于读多写少的场景。

3. 信号量(Semaphore)

  • 原理:信号量维护一个计数器,表示可用资源数量。它有两个基本操作:P 操作(等待)和 V 操作(释放)。P 操作会将信号量的值减 1,如果信号量的值小于 0,则线程会被阻塞;V 操作会将信号量的值加 1,如果有线程在等待,则唤醒其中一个线程。

4. 条件变量(Condition Variables)​

  • 允许线程在特定条件下等待或唤醒其他线程,需与互斥锁配合使用。

5. 原子操作(Atomic Operations)

  • 原理:原子操作通过硬件指令保证单条操作的不可分割性,在多线程环境下不会被其他线程中断。

6. 无锁编程(Lock-Free)

  • 原理:通过CAS(Compare-And-Swap)等原子指令实现同步,无需传统锁。
  • 无锁数据结构是通过无锁编程实现的,不使用传统的锁机制,而是通过原子操作和内存屏障来实现线程安全的数据结构。
  • 面试题:使用C++实现无锁队列、无锁栈。

7. 屏障(Barrier)

  • 原理:多个线程在屏障处等待,直到所有线程到达后才继续执行。

8. 自旋锁(Spinlock)

  • 原理:线程在获取锁时循环检查锁状态(忙等待),而非阻塞休眠。
  • 使用场景:锁持有时间极短且线程不想让出 CPU(如内核中断处理)。

锁的使用场景⭐

  1. 共享数据的并发读写
    当多个线程/进程需要访问同一块内存或数据结构时,需通过锁保证操作的原子性。
  2. 线程间协作与条件同步
    当线程需要等待特定条件满足后再继续执行时,结合锁和条件变量实现同步,如:生产者-消费者队列。
  3. 资源池管理
    管理有限资源(如数据库连接池、线程池)时,需通过锁分配资源。
  4. 单例模式初始化
    确保全局单例对象在多线程环境下只初始化一次。
  5. 数据库事务隔离
    在数据库操作中,通过锁实现事务隔离级别(如行锁、表锁)。
  6. 文件或硬件设备访问
    多个进程/线程操作同一文件或硬件设备时,需通过文件锁协调访问。
  7. GUI主线程更新
    在图形界面编程中,UI控件通常只能在主线程修改,需通过锁同步后台线程的更新请求。
  8. 分布式系统协调
    在分布式系统中,通过分布式锁(如Redis、ZooKeeper)协调多个节点对共享资源的访问。

介绍死锁,如何解除⭐


1. 死锁的定义

死锁(Deadlock) 是多个进程(或线程)因竞争资源而陷入互相等待的状态,若无外力干预,这些进程将永久阻塞。


2. 死锁的四个必要条件

死锁发生需同时满足以下条件:

  1. 互斥(Mutual Exclusion):资源同一时间只能被一个进程占用。
  2. 请求并保持(Hold and Wait):进程持有资源的同时请求其他资源。
  3. 不可剥夺(No Preemption):资源只能由持有者主动释放,不可强制剥夺。
  4. 循环等待(Circular Wait):多个进程形成资源请求的环形依赖链。

3. 解除死锁的通用方法

(1) 死锁预防

通过破坏死锁的四个必要条件之一,避免死锁发生:

  • 破坏互斥:允许资源共享(如只读文件),但多数资源无法共享。
  • 破坏占有且等待:进程必须一次性申请所有所需资源(资源利用率低)。
  • 破坏不可抢占:强制剥夺资源(需设计资源恢复机制,复杂度高)。
  • 破坏循环等待:按固定顺序申请资源(如统一资源编号,按升序请求)。

(2) 死锁避免

在资源分配时动态判断是否安全,常用算法:

  • 银行家算法(Banker’s Algorithm):模拟分配资源后系统是否处于安全状态(所有进程能按顺序完成)。若不安全,则拒绝分配请求。

(3) 死锁检测与恢复

允许死锁发生,但定期检测并解除:

  • 检测方法:构建资源分配图,检测是否存在环路。
  • 恢复方法
    • 终止进程:强制终止一个或多个进程(如优先级最低或剩余时间最长的进程)。
    • 资源剥夺:从某些进程中剥夺资源分配给其他进程(需处理进程回滚)。
    • 进程回退法:让一个或多个死锁进程回退到足以避免死锁的地步。

第 3 章 内存管理

第 4 章 文件管理

第 5 章 I/O管理

第 6 章 网络系统

1. 零拷贝技术

2. I/O 多路复用:select/poll/epoll

3. 高性能网络模式:Reactor 和 Proactor

4. 一致性哈希

1.背景

在分布式系统中,服务器节点的动态增减是常见需求。传统哈希算法(如取模哈希 key % N,N为节点数)存在两个核心问题:

  1. 节点变动时数据迁移量大:当节点数N变化时,几乎所有键的映射关系都会改变,导致大量数据重新分布(如缓存失效)。
  2. 负载不均衡:节点直接按哈希值分配时,若数据分布不均,易出现“热点节点”。

一致性哈希的目标:在节点动态变化时,尽可能减少数据迁移,并实现负载均衡。
```接下来阅读:https://developer.aliyun.com/article/1082388``