Lazy loaded imageOS02 抽象-线程与进程

type
status
date
slug
summary
tags
category
icon
password
我们将在以下几个章节学习操作系统提供的几个抽象。

抽象一:线程与进程

线程的动机

操作系统必须具备同时处理多件事情(MTAO)的能力,这种需求源于多种应用场景。
网络服务器需要同时处理多个客户端连接,如果一次只能处理一个连接,服务器的吞吐量将极其低下。并行程序为了充分利用多核处理器的计算能力,必须将任务分解成多个部分同时执行,以此提高整体效率。带有用户界面的程序也必须具备并发处理能力,这样在进行耗时计算时,界面仍然能够响应用户的输入操作,避免程序“卡死”。此外,网络和磁盘控制器也需要并发操作来隐藏I/O延迟,当一个I/O操作在进行时,CPU可以转去处理其他任务,而不是空闲等待。

关键概念辨析

在深入讨论之前,我们需要明确几个容易混淆的重要概念:
  1. 多处理:指在多个物理CPU或多核处理器上真正同时运行多个任务。
  1. 多道程序:指在单个CPU或核心上,通过快速切换执行多个任务,营造出它们同时运行的假象。
  1. 多线程:指在一个进程内创建多个执行流(线程),这些线程可以并发或并行执行。
  1. 并发:指系统具有处理多个任务的能力,这些任务的执行在时间上是重叠的。它强调通过交错执行来模拟“同时性”,核心问题在于如何管理共享资源(如使用互斥锁)。
  1. 并行:是并发的一种特例,指在多核或多CPU硬件上,任务真正地同时执行。它更侧重于如何将工作任务有效拆分,以便多个计算单元能够同时处理。

线程如何掩盖I/O延迟

线程通过状态转换来有效利用CPU时间,从而掩盖耗时的I/O操作带来的延迟。任何一个线程在其生命周期内都处于以下三种基本状态之一:
  • 运行中:线程正在CPU上执行。
  • 就绪:线程已准备好运行,但由于CPU正被其他线程占用而暂时等待。
  • 阻塞:线程因等待某个事件(如I/O操作完成)而无法继续执行。
操作系统根据既定规则在这些状态间进行切换: 当一个运行中的线程发起I/O请求时,它会被移入阻塞状态,让出CPU。当该线程等待的I/O操作完成时,它从阻塞状态变为就绪状态,等待被调度执行。在没有I/O事务干扰时,调度器主要在运行中和就绪状态的线程之间进行切换,决定哪个线程接下来使用CPU。

创建多线程

程序需要通过系统调用(syscall)来请求操作系统创建新线程。关键的一点是,同一进程内的所有线程共享相同的地址空间。
notion image
操作系统通常会提供线程库(如POSIX的pthread)来封装这些系统调用,为程序员提供方便的API。创建线程的核心函数是pthread_create,其参数包括:
  • thread:指向线程标识符的指针。
  • attr:用于设置线程属性(如栈大小、调度策略),通常可为NULL使用默认值。
  • start_routine:线程开始执行时的函数入口地址。
  • arg:传递给start_routine函数的参数。
在线程函数中,return语句会隐式地调用pthread_exit来终止线程。主线程可以使用pthread_join函数来等待一个指定的线程终止。调用pthread_join会使主线程阻塞,直到目标线程结束,并可以获取目标线程的返回值。

线程的共享与私有状态

理解线程哪些状态是共享的、哪些是私有的至关重要。
共享状态(同一进程内所有线程可见和可修改)包括:
  1. 整个进程的地址空间,如全局变量和堆上动态分配的内存。
  1. 进程的I/O状态,如打开的文件描述符。
私有状态(每个线程独有,保存在线程控制块TCB中)包括:
  1. 寄存器集合的状态(如程序计数器PC、栈指针SP)。
  1. 该线程的执行栈。执行栈用于存储函数调用时的返回地址、局部变量、临时变量等。虽然同一进程的所有线程的栈都位于进程的地址空间内(通常栈区向下增长),但每个线程拥有自己独立的栈空间。

线程调度与同步

操作系统为应用程序提供了看似无数的虚拟处理器(即线程),但物理处理器的数量是有限的。因此,操作系统调度器负责决定在任一时刻哪些线程可以占用物理处理器执行,这引入了非确定性。
非确定性指的是调度器可以以任何顺序、在任何时间点进行线程切换。这种不确定性是并发编程复杂性的根源,它会导致竞态条件等问题:程序的正确性依赖于线程执行的时间顺序,而这种顺序是不可预测的。
根据是否共享状态,线程可以分为:
  • 独立线程:不与其他线程共享任何状态。这类线程的行为是确定性的、可重现的。
  • 相关线程:与其他线程共享状态(如全局变量、堆内存)。这类线程需要同步机制来协调。

同步

同步是协调多个共享状态的线程之间执行顺序的机制,其核心概念包括:
  1. 互斥:保证在同一时间,只有一个线程能够执行特定的操作或访问特定的资源,具有排他性。
  1. 临界区:指一段需要互斥访问的代码区域。互斥机制保证了同一时间只有一个线程能够执行临界区内的代码。
  1. :是实现互斥的一种基本机制。它像一个令牌,同一时间只能被一个线程持有。锁提供两个原子操作:加锁(acquire,尝试获取锁的所有权)和解锁(release,释放锁的所有权)。
  1. 信号量:由Dijkstra提出,是锁的一种更通用的形式。它包含一个整数值,支持两个原子操作:P操作(可能阻塞,直到信号量值大于0然后将其减1)和V操作(将信号量值加1,可能唤醒等待的线程)。当信号量的最大值被限制为1时,它就成为了一个互斥锁。

进程:更重量级的抽象

进程是操作系统进行资源分配和调度的基本单位,可以定义为一个拥有受限权限的执行环境,是程序的一次运行实例
操作系统通过进程模型提供了强大的保护机制:确保进程之间、进程与操作系统内核之间的内存空间和资源相互隔离,一个进程的异常或恶意行为不会影响其他实体。
进程相关的核心API包括:
  • fork():创建一个新的进程,该进程是调用进程(父进程)的副本,拥有相同的内存映像、文件描述符等。
  • exec()系列函数:用一个新的程序覆盖当前进程的内存空间,并开始执行新程序。
  • wait()系列函数:使父进程等待子进程终止。
  • kill():向指定进程发送一个信号(如请求终止)。
  • exit():终止当前进程。
  • sigaction():允许进程为接收到的特定信号注册自定义处理函数。

系统调用的实现机制

系统调用是用户程序请求操作系统内核服务的接口,其实现基于精密的硬件和软件协作。
  1. 系统初始化:操作系统启动时,会预先设置好陷阱表。需要注意的是,陷阱表用于处理同步中断(也称为陷阱,trap),如系统调用和程序错误(除零、页错误等);而中断表用于处理异步中断(interrupt),如定时器中断、I/O设备中断。
  1. 启动用户程序:当操作系统要运行一个用户程序时,它首先在内核态下执行一系列准备工作:在进程表中创建新条目、为进程分配内存、将用户程序的代码段和数据段加载到内存、设置用户栈并放置命令行参数等。最后,操作系统执行一个特殊的指令,将CPU模式从内核态切换到用户态,并跳转到用户程序的入口点开始执行。
  1. 发起系统调用:当用户程序需要操作系统服务时(如调用read()),流程如下:
      • 程序实际上调用的是C标准库中封装好的函数,这些函数内部包含少量手工编写的汇编指令。
      • 这些汇编指令负责将系统调用所需的参数放入约定好的寄存器或栈中,并将代表特定系统调用的陷阱号放入一个指定寄存器。
      • 然后执行一条陷阱指令,该指令会触发一个软中断,使CPU从用户态陷入内核态。
      • 硬件自动完成以下工作:将当前的用户态寄存器值、程序状态字、程序计数器(PC)等关键状态保存到该进程的内核栈中,然后根据陷阱号查找陷阱表,跳转到对应的操作系统系统调用处理程序(handler)开始执行。
  1. 执行与返回:操作系统在内核态下执行系统调用的实际逻辑。执行完毕后,处理程序将返回值放入约定的寄存器或内存位置,然后执行一条从陷阱返回指令。硬件接着从内核栈中恢复之前保存的用户态上下文,将CPU模式切换回用户态,并跳转回用户程序中陷阱指令之后的下一条指令继续执行。
这套完整的协议被称为受限直接执行。其核心思想是:让程序大部分指令直接在CPU上运行(“直接执行”以获得高性能),但当需要执行特权操作或处理异常事件时,通过陷阱机制将控制权交还给操作系统进行监管和处理(“受限”以保证安全和控制)。
 
Prev
OS01 绪论
Next
OS03 抽象-文件与I/O
Loading...
Article List
SunVapor的小站
计算机系统导论
操作系统
文档