进程与线程

进程与线程的概念

  • 进程的概念:由 CPU 执行的运行中的程序,被称为进程 (Process)。
  • 并行:两个任务同时进行。
  • 并发:两个任务在一段时间中交替进行。

进程的状态

进程的状态

  • 创建
  • 就绪
  • 就绪挂起
  • 运行
  • 阻塞
  • 阻塞挂起
  • 结束

进程的控制结构(PCB)

  • PCB 是进程存在的唯一标识:
    • 进程描述信息
      • 进程标识符
      • 用户标识符
    • 进程控制和管理信息
      • 进程当前状态(new、ready、running、waiting、blocked)
      • 进程优先级:进程抢占 CPU 时的优先级
      • 资源分配清单:
        • 包括内存地址空间和虚拟地址空间的信息,所打开的文件列表和所使用的 I/O 设备信息。
    • CPU 相关信息
      • CPU 中各个寄存器的值,当进程被切换时,CPU 的状态信息都会保存在相应的 PCB 中。

进程的组织方式

  • 链表:通过链表,把具有相同状态的 PCB 连接在一起,形成各种状态队列。
  • 索引:把同一状态的进程组织在一个索引表中,索引表项指向对应的 PCB,不同状态对应不同的索引。

选择链表的原因

  • 链表可以更灵活地进行插入和删除,适应进程的创建、销毁等调度需求。

进程的控制

  • 创建
    • 为新进程分配一个唯一的标识号,并申请空白的 PCB,PCB 有限,若申请失败则创建失败。
    • 分配资源,若资源不足则进入等待状态。
    • 初始化 PCB。
    • 将进程插入到就绪队列,等待被调度。
  • 终止
    • 正常结束
    • 异常结束
    • 外界干扰(如 kill 命令)
  • 阻塞
    • 当进程需要等待某一事件完成时,使用阻塞语句阻塞自己,一旦被阻塞等待,只能被另一个进程唤醒。
  • 唤醒
    • 当阻塞条件解除时,唤醒被阻塞的进程。

上下文切换

  • 上下文切换:一个进程切换到另一个进程运行。
  • CPU 上下文:程序在运行期间需要的程序计数器和寄存器信息。
  • 进程上下文切换
    • 线程上下文切换
    • 中断上下文切换
  • 进程是由内核管理和调度的,进程的切换只能发生在内核态。
  • 上下文切换包括虚拟内存、栈、全局变量等用户空间的资源,还包括内核堆栈、寄存器等内核空间资源。

上下文切换的场景

  • 时间片耗尽
  • 进程需要的系统资源不足
  • 进程通过睡眠函数 sleep
  • 存在更高优先级的进程运行
  • 发生硬件中断

线程的概念

  • 线程:线程是进程中的一条执行流程。同一进程中的线程可以共享代码段、数据段、打开的文件资源,但每个线程都有一套独立的寄存器和栈,保证线程的控制流相对独立。

线程的优缺点

  • 优点
    • 一个进程中可以存在多个线程。
    • 各个线程之间可以并发执行。
    • 各个线程可以共享地址空间和文件等资源。
  • 缺点
    • 当进程中的一个线程崩溃时,会导致其所属进程的所有线程崩溃。

线程与进程的比较

  • 进程是资源(包括内存、打开的文件等)分配的单位,线程是 CPU 调度的单位。
  • 进程拥有一个完整的资源平台,线程只独享必不可少的资源,如寄存器和栈。
  • 线程同样具有就绪、阻塞、执行三种基本状态,并具备状态之间的转换关系。
  • 线程能减少并发执行的时间和空间开销:
    • 线程的创建比进程快。
    • 线程的销毁比进程快。
    • 同一进程内的线程切换比进程切换快,因为线程具备相同的地址空间,在切换时不需要切换页表。
    • 线程之间共享内存和文件资源,传递数据不需要经过内核审核,线程之间的数据交互效率更高。

线程的上下文切换

  • 当两个线程不属于同一进程,切换的过程和进程的上下文切换一样。
  • 当属于同一进程,虚拟内存等共享资源不需要改变,只切换线程的寄存器、程序计数器等私有数据。

线程的实现

  • 用户线程:在用户空间实现的线程,不由内核管理,由用户态的线程库来完成线程的管理。
  • 内核线程:在内核中实现的线程,由内核管理。
  • 轻量级进程:在内核中来支持用户的线程。

轻量级进程(Lightweight Process, LWP)和内核线程(Kernel Thread)是操作系统中两种用于处理并发执行的实体,它们在实现、管理方式和与操作系统内核的交互上存在显著区别。以下是它们的主要区别:

1. 定义与本质

  • 轻量级进程 (LWP):
    • 轻量级进程是用户级线程与内核线程之间的一种抽象层。它提供了用户级线程的接口和内核线程的执行能力的结合。LWP在用户空间管理,但最终通过内核线程在内核空间中执行。
    • LWP提供了线程的概念,使得多线程应用程序可以在单个进程中实现并发执行。
  • 内核线程 (Kernel Thread):
    • 内核线程是在内核空间中管理的线程,由操作系统内核直接控制和调度。每个内核线程都有自己的线程控制块(TCB),由内核负责线程的创建、调度和管理。
    • 内核线程执行系统级任务或用户级进程的部分操作。它们与硬件密切相关,可以直接调用系统资源。

2. 管理与调度

  • 轻量级进程 (LWP):
    • LWP由用户级线程库管理,调度和切换可以在用户空间完成,减少了上下文切换的开销。多个LWP可能会映射到一个或多个内核线程上。
    • 操作系统通常提供与LWP对应的轻量级管理工具,但主要的调度和切换仍然依赖于内核线程。
  • 内核线程 (Kernel Thread):
    • 内核线程完全由内核管理。操作系统的调度程序会调度内核线程,而无需考虑用户空间的线程库。
    • 内核线程的调度依赖于内核的调度器,它们的上下文切换开销相对较高,因为涉及到内核态和用户态的切换。

3. 与内核的交互

  • 轻量级进程 (LWP):
    • LWP通过内核线程执行真正的工作,但LWP本身不直接与硬件或内核资源交互。它们需要通过内核线程与内核交互。
    • 如果一个LWP发生阻塞,通常整个用户级线程组都可能被阻塞,除非操作系统支持多对多的线程模型。
  • 内核线程 (Kernel Thread):
    • 内核线程直接与内核交互,可以直接访问系统资源,如CPU、内存、设备等。它们是操作系统中的基本执行单位。
    • 当内核线程阻塞时,内核可以通过调度其他内核线程来继续处理任务,因此不会影响系统的整体并发性。

4. 性能与开销

  • 轻量级进程 (LWP):
    • LWP的调度和切换开销较低,因为大多数操作可以在用户空间完成,不需要频繁的系统调用。
    • 但是,由于LWP依赖内核线程执行,因此LWP的性能在很大程度上取决于内核线程的管理和调度效率。
  • 内核线程 (Kernel Thread):
    • 内核线程的开销较高,因为涉及到内核态和用户态之间的切换以及复杂的内核调度机制。
    • 然而,内核线程的执行更为直接和高效,因为它们直接由内核管理,不需要用户空间的中介。

5. 适用场景

  • 轻量级进程 (LWP):
    • 适用于需要用户级线程灵活性,同时又需要利用内核线程多处理器并发能力的场景。典型应用包括大多数用户空间的多线程应用程序。
  • 内核线程 (Kernel Thread):
    • 适用于需要直接与硬件交互、执行系统级任务,或需要高可靠性调度的场景。典型应用包括操作系统内核本身和驱动程序。

总结来说,轻量级进程(LWP)提供了用户级线程的灵活性,同时利用内核线程进行实际的并发执行,而内核线程是操作系统内核直接管理的调度单元,负责执行系统级任务和用户级进程的部分工作。两者在实现方式、管理机制、性能和适用场景上都有显著的差异。

用户线程和内核线程的关系

  • 多对一
  • 一对一
  • 多对多

轻量级进程(Lightweight Process, LWP)和用户线程(User Thread)在操作系统的并发执行模型中有密切的关系,但它们处于不同的抽象层次,各自承担着不同的角色。以下是它们的关系与区别:

1. 定义与本质

  • 轻量级进程 (LWP):
    • 轻量级进程是介于用户线程和内核线程之间的一层抽象,它为用户线程提供了执行的上下文,并且与内核线程进行绑定或映射。
    • 每个LWP对应一个内核线程或与多个内核线程共享一个内核线程,并负责实际的调度和执行。
    • LWP在用户空间中暴露为用户线程的执行载体,使得用户线程能够在多处理器系统中并行执行。
  • 用户线程 (User Thread):
    • 用户线程是完全在用户空间中管理和调度的线程。它们由用户级线程库(如Pthreads、Java线程等)实现,不直接与操作系统内核交互。
    • 用户线程的创建、销毁、同步和调度都由用户级线程库管理,不涉及系统调用,因此切换速度非常快。

2. 管理与调度

  • 轻量级进程 (LWP):
    • LWP在内核中创建,并由内核调度,LWP的调度和管理主要由操作系统内核负责。它是用户线程与内核资源交互的桥梁。
    • 每个LWP在内核中都有一个独立的内核线程来进行调度,用户线程通过LWP获得了内核级别的调度能力。
  • 用户线程 (User Thread):
    • 用户线程由用户级线程库管理,调度在用户空间完成。这使得用户线程的切换和调度开销很小,但由于没有内核的直接支持,用户线程的并发能力依赖于LWP。
    • 在没有LWP的情况下,用户线程在阻塞系统调用时会导致整个进程阻塞。通过LWP,用户线程可以利用内核线程进行实际的并发执行。

3. 关系与映射模型

  • 多对一模型:
    • 在这种模型中,多个用户线程映射到一个单独的LWP(或内核线程)上。这种模型的优点是开销小,但如果LWP阻塞,所有映射到它的用户线程都会阻塞。
  • 一对一模型:
    • 每个用户线程都对应一个LWP(或内核线程)。这种模型提供了更高的并发性,因为每个用户线程都有独立的LWP处理系统调用或阻塞事件。
  • 多对多模型:
    • 多个用户线程映射到多个LWP上,操作系统可以根据需要动态创建或销毁LWP。这种模型提供了灵活性,允许在用户线程和LWP之间进行高效的资源利用。

4. 性能与应用场景

  • 轻量级进程 (LWP):
    • LWP适用于需要在用户空间实现高效并发执行的场景,特别是多线程应用程序。LWP使得用户线程可以在多处理器系统上并发运行,提高了多线程应用的性能。
  • 用户线程 (User Thread):
    • 用户线程适用于需要在用户空间进行高效、快速的线程管理的应用程序。由于它们不依赖内核调度,切换速度快,但在多处理器系统上无法直接并行执行,需要依赖LWP来提供并发能力。

5. 典型的使用模型

  • 用户线程与LWP的结合:
    • 在实际应用中,用户线程往往通过LWP来获得内核调度的支持,从而在多处理器系统上实现并发执行。LWP为用户线程提供了对内核资源的访问,并允许多个用户线程映射到一个或多个LWP上进行调度。

总结

用户线程是完全在用户空间管理的线程,其调度和管理不依赖内核,但它不能利用多处理器系统的并发能力。轻量级进程(LWP)则是用户线程与内核线程之间的桥梁,它允许用户线程利用内核调度机制,进行并发执行。因此,用户线程通过LWP与内核交互,实现多线程应用的高效执行。

用户线程的优势和劣势

  • 用户线程是基于用户态的线程管理库实现,线程控制块(TCB)也是由库生成的,对于操作系统来说是透明的。
  • 用户线程的创建、终止、同步、调度等线程管理由用户级线程库提供直接支持。

优点

  • TCB 由线程库函数维护,可以用于不支持线程库技术的操作系统。
  • 线程的切换由线程库函数进行,无需用户态和内核态的切换。

缺点

  • 不支持操作系统的调度,一个线程阻塞,进程包含的其他线程都不能执行。
  • 一个线程开始运行后,除非主动交出 CPU 使用权,否则它所在进程中的其他线程无法运行。
  • 时间片分配的单位是进程,线程分配的时间较少,执行较慢。

用户线程的模型

  • 多对一模型。

内核线程的优势和劣势

  • 内核线程由操作系统管理,TCB 存放在操作系统中,线程的创建、终止、管理都由操作系统负责。

优势

  • 一个进程中,某个内核线程发起系统调用而被阻塞,不会影响其他内核线程运行。
  • 时间片分配给线程,多线程的进程获得更多的 CPU 运行时间。

缺点

  • 在线程支持的操作系统中,由内核来维护线程和进程的上下文信息。
  • 线程的创建、终止、和切换通过操作系统完成,开销较大。

内核线程的模型

  • 一对一模型。

轻量级进程

  • 轻量级进程(LWP)是由内核支持的用户级线程,一个进程可以有多个 LWP,每个 LWP 和内核线程是一对一的关系。
  • LWP 只能由内核管理,并像普通进程一样被阻塞。

LWP 与用户线程的关系

  • 1 : 1 模型:
    • 一个线程对应一个 LWP,再对应一个内核线程。
    • 优点:实现并行,一个 LWP 阻塞,不会影响其他的 LWP。
    • 缺点:每个用户线程会产生一个内核线程,创建线程的开销大。
  • N : 1 模型:
    • 多个用户线程对应一个 LWP,再对应一个内核线程,用户线程对内核不可见。
    • 优点:上下文切换发生在用户空间,效率高。
    • 缺点:一个用户线程阻塞,整个进程都要阻塞,在多核 CPU 中无法充分利用 CPU。
  • M : N 模型:
    • 多个用户线程对应到 LWP,LWP 再一一对应到内核线程。
    • 优点:大部分线程的上下文发生在用户空间,多个线程又可以充分利用多核 CPU 的资源。

组合模式

  • 结合 1 : 1 和 M : N,开发人员可以根据不同的应用特点调节内核线程的数目,以达到物理并行性和逻辑并行性的最佳方案。

进程调度原则

  • 系统吞吐量:单位时间内 CPU 完成的进程数。
  • CPU 利用率:CPU 的使用效率。
  • 周转时间:进程的运行和阻塞的时间之和。
  • 等待时间:进程处于就绪队列的时间。
  • 响应时间:用户提交的请求第一次到系统消耗的时间。

进程调度算法

  • 先来先服务调度 (非抢占式)
  • 最短作业优先调度
  • 高响应比优先调度(等待时间 + 要求服务时间) / 要求服务时间
  • 时间片轮转调度算法
    • 太长:短作业的响应时间变长。
    • 太短:导致过多的上下文切换。
    • 时间片一般为 20 - 50ms。
  • 最高优先级调度算法
    • 静态优先级:优先级不变。
    • 动态优先级:等待时间越长,优先级越高。
    • 处理方法:
      • 抢占式:当就绪队列出现更高优先级的进程时,当前进程挂起。
      • 非抢占式:运行完当前进程后,再选择高优先级的进程。
  • 多级反馈队列调度算法
    • 兼顾了长短作业,同时有较好的响应时间。

进程间通信

  • 管道(传输的数据是无格式的流且大小受限):
    • 本质是内核中的一串缓存。
    • 匿名管道 (int pipe(int fd[2])):用于父子进程通信。
    • 命名管道 (mkfifo):可用于任意进程通信。
  • 消息队列(传输用户定好的消息格式,MSGMAXMSGMNB 消息最大长度/队列最大长度):
    • 本质上是保存在内核的消息链表。
    • 通信不及时。
    • 不适合大数据的传输。
    • 用户态和内核态存在数据拷贝开销。
  • 共享内存
    • 本质是拿出一块虚拟地址空间,映射到相同的物理内存中。
  • 信号量
    • 本质是一个整形计数器,实现进程的互斥和同步。
    • 互斥信号量,初始为 1。
    • 同步信号量,初始为 0。
  • 信号
    • 针对异常情况进程。
    • Ctrl + C 产生 SIGINT 表示终止。
    • Ctrl + Z 产生 SIGTSTP 表示停止该进程。
    • 通过 kill + 数字 + 进程 pid 发送信号。
    • 唯一一种异步通信方式。
      • 响应方式:
        • 默认操作。
        • 编程函数操作。
        • 忽略信号。
  • Socket
    • 跨网络与不同主机的进程通信。
    • 系统调用(int socket(int domain, int type, int protocal)):
      • Domain 协议族AF_INETAF_INET6AF_LOCAL/AF_UNIX
      • Type 通信特性SOCK_STREAMSOCK_DGRAMSOCK_RAW
    • 通信方式
      • 基于 TCP:
        TCP 通信
      • 基于 UDP:
        UDP 通信
      • 针对本地:
        本地通信

管道(Pipe)和消息队列(Message Queue)都是进程间通信(Inter-Process Communication, IPC)的机制,但它们在数据传输方式、使用场景、实现方式等方面存在明显的区别。以下是它们的主要区别:

1. 数据传输方式

  • 管道 (Pipe):
    • 管道是一种单向或双向的数据流通信机制。数据在管道中以字节流的形式传输,发送方将数据写入管道的一端,接收方从另一端读取数据。
    • 管道没有消息边界,接收方从管道中读取数据时,可能无法确定消息的开始和结束位置。
  • 消息队列 (Message Queue):
    • 消息队列是一种基于消息的通信机制。数据在消息队列中以独立的消息单元(带有消息边界)进行传输,每条消息是一个独立的实体。
    • 消息队列支持消息的优先级,可以根据消息的优先级进行排序,从而影响消息的传递顺序。

2. 数据持久性

  • 管道 (Pipe):
    • 管道是非持久化的,数据在管道中传递后,如果接收方没有及时读取,数据可能会被丢弃。管道的生命周期通常与进程的生命周期绑定,管道关闭后,数据即丢失。
    • 一般用于父子进程之间或相关进程之间的短期通信。
  • 消息队列 (Message Queue):
    • 消息队列是持久化的,即使发送方发送消息后退出,消息仍然保留在队列中,直到接收方读取。消息队列可以在进程间进行异步通信,允许发送方和接收方在不同的时间访问消息。
    • 消息队列的生命周期与系统内核关联,可以在不同进程之间长时间共享。

3. 数据同步性

  • 管道 (Pipe):
    • 管道通常是同步通信机制,即在写操作时,如果管道已满,写操作会被阻塞;在读操作时,如果管道为空,读操作也会被阻塞。管道在读写之间具有一定的同步特性。
    • 通过命名管道(FIFO),可以在不相关的进程之间实现通信,但仍然受限于同步的特性。
  • 消息队列 (Message Queue):
    • 消息队列支持异步通信,发送方可以将消息发送到队列中而不需要等待接收方立即处理,接收方可以在之后的任意时间读取消息。
    • 这种异步特性使得消息队列在需要解耦发送和接收时间的场景下非常有用。

4. 命名与访问

  • 管道 (Pipe):
    • 管道分为匿名管道和命名管道。匿名管道通常只能在有亲缘关系的进程(如父子进程)之间使用,无法在不相关的进程之间使用。
    • 命名管道(FIFO)是通过文件系统命名的,可以在不相关的进程之间进行通信,但仍然需要在同一台机器上运行。
  • 消息队列 (Message Queue):
    • 消息队列通常通过系统V IPC或POSIX消息队列实现,具有唯一的标识符(如消息队列ID或名字),进程可以通过这个标识符访问消息队列。
    • 消息队列可以在系统内的任意进程之间共享,并且在同一台机器上跨进程使用非常方便。

5. 复杂性与功能

  • 管道 (Pipe):
    • 管道的实现和使用相对简单,适用于传输简单的字节流数据。由于缺乏消息边界和优先级功能,管道适合简单的、顺序的数据传输。
    • 管道主要用于轻量级的通信需求,不适合复杂的消息处理逻辑。
  • 消息队列 (Message Queue):
    • 消息队列的实现和使用更为复杂,支持消息的优先级、消息的长度和类型、异步通信等高级特性。
    • 消息队列适用于需要复杂消息管理和高效数据处理的场景,例如任务队列、事件通知等。

总结

  • 管道适合简单、顺序的字节流传输,通常用于父子进程之间或相关进程之间的短期通信。它的使用较为简单,但缺乏消息边界和持久化特性。
  • 消息队列适合需要持久化、异步通信和复杂消息管理的场景,适用于不同进程之间共享消息队列进行数据传输。它的使用较为复杂,但功能强大。

多线程同步

  • 互斥:保证临界区只有一个线程执行。
  • 同步:并发的进程/线程在一些关键点上需要互相等待并互通消息,相互制约的等待与互通为同步。

互斥与同步的实现

    • 忙等待锁/自旋锁(原子操作:Test-and-Set
    • 无等待锁:获取不到锁就加入等待队列。
  • 信号量
    • P 操作。
    • V 操作。
    • 实现互斥:先 PV,信号量初始值为 1。
      信号量实现
    • 实现同步:先 VP,信号量初始值为 0。

生产者-消费者问题

生产者-消费者问题

经典同步问题

  • 哲学家就餐

    • 互斥,一次只允许一人就餐。
    • 奇数哲学家先拿左边筷子,偶数哲学家先拿右边筷子。
    • 只有哲学家两边的哲学家都没有进食时,该哲学家才可以进食。
  • 读者-写者
    读者-写者问题

    读者-写者问题示意图1

    读者-写者问题示意图2

死锁问题

死锁的条件

  • 资源互斥:多个线程不能同时使用同一个资源。
  • 持有并等待:线程 A 在等待资源 B 时不会释放自己的资源 A。
  • 不可剥夺:当线程持有了资源,在执行完成之前不能被其他线程获取。
  • 环路等待:两个线程获取资源的顺序构成了环形链。

避免方式

  • 预防、避免、检测、解除。

排查方式

  • Javajstack 排查 Java 堆栈。
  • Linuxpstack + gdb
    • gdb
      gdb 排查

  • 互斥锁和自旋锁

    • 如果程序运行时间很短,就应该使用自旋锁,否则使用互斥锁。
    • 自旋锁是通过 CAS 来实现的。
    • 自旋锁

    自旋锁与互斥锁

  • 读写锁

    • 读写锁在读多写少的场景中更能发挥优势。
    • 读优先锁
    • 写优先锁
    • 公平读写锁
      公平读写锁
  • 乐观锁和悲观锁
    乐观锁和悲观锁

    锁的操作

    锁的实现