进程和线程

需熟练掌握进程和线程的概念,以及进程的状态转换和内存空间结构,是后续内容的基础,在选择题中会考察。也需了解进程间通信的方式、用户级线程和内核级线程的概念,可能在选择题中考察。

进程和线程

在多道程序环境下,允许多个程序并发执行,为此操作系统引入了进程(Process)的概念,以便更好地描述和控制程序的并发执行,实 现操作系统的并发性。

进程

进程是 系统资源分配的基本单位。 进程拥有独立的系统资源,包括内存空间、文件描述符、CPU 时间片等,这些资源的分配由操作系统负责。

进程和程序

程序时静态的,进程是动态的,进程可以理解成运行的程序,两者的具体区别如下:

  • 程序
    • 程序是一组指令的静态集合,它存储在磁盘等持久性存储介质中。
    • 程序是被动的,它本身并不执行任何操作。
  • 进程
    • 进程是程序在内存中执行的实例。
    • 当一个程序被加载到内存中并开始执行时,操作系统会创建一个进程。
    • 进程是动态的,它代表了程序的执行过程。
Disk
program
CPU
Memory
process
stack
code
static data
heap
Loading:

读取磁盘中的程序并将其加载到进程的地址空间中
code
static data

操作系统在创建进程时,会为进程分配必要的资源,例如内存空间、文件句柄等。 进程是操作系统进行资源分配和调度的基本单位。 一个程序可以多次运行,每次运行都会创建一个新的进程。

进程控制块

操作系统通过 进程控制块(PCB,Process Control Block) 对进程进行管理,进程控制块中包含一系列进程的元信息,这些元信息帮助操作系统对进程进行管理:

进程描述信息进程控制和管理信息资源分配清单处理机相关信息
进程标识符(PID)进程状态代码段指针通用寄存器值
用户标识符(UID)进程优先级数据段指针地址寄存器值
代码运行入口地址堆栈段指针控制寄存器值
程序的外存地址文件描述符标志寄存器值
等待时间
CPU 占用时间

上表给出了 PCB 的关键内容,其中进程描述信息用于唯一标识和管理进程,以及实现用户级别的权限控制;进程控制和管理信息用于管理进程的生命周期、调度和执行;资源控制清单记录进程使用的资源,以便进行有效的资源管理和分配;处理机相关信息保存与处理器相关的状态信息,以实现上下文切换。

进程控制块在操作系统进程管理的多个阶段都发挥重要作用:

  • 进程创建和终止:在进程创建时,操作系统为它新建一个 PCB(进程控制块)。该结构在进程存在期间常驻内存,可以随时存取,并在进程结束时删除。
  • 进程调度:当操作系统进行进程调度时,会根据 PCB 中存储的信息(如优先级、进程状态)来决定哪个进程应该获得 CPU 的控制权。

线程

线程是 系统调度的基本单位 。 多核处理器可以将系统中的不同线程调度到不同的 CPU 逻辑核心上,所以在同一个时刻,系统中的线程可能会在不同的逻辑核心上并行运行。

每个进程启动的时候,都包含一个线程(主线程),可以在主线程外创建更多的线程。

Code
Data
Files
Stack
Registers
Processes
Code
Data
Files
Stack
Multi-Threaded Process
Register
Stack
Register
Stack
Register
T1
T2
T3
T1
T2
T3

如上图所示,一个进程中的所有线程都共享虚拟地址空间和系统资源,但是每个线程都维护自己的堆栈、寄存器和一些额外信息。

进程的状态

进程状态是指在操作系统中,一个进程在执行过程中的不同阶段。它反映了进程当前正在做什么,以及是否可以被 CPU 执行。

状态种类

不同的操作系统对进程状态的划分可能不同,但常见的包若以下几种:

  1. 创建状态(New):当进程被创建但还未分配资源或执行时,它处于创建状态。
  2. 就绪状态(Ready):在就绪状态中,进程已准备好执行,但由于操作系统调度算法或其他原因,尚未获得 CPU 时间片。
  3. 运行状态(Running):在运行状态中,进程正在执行指令并占用 CPU。
  4. 阻塞状态(Blocked):当进程在等待某些事件发生时,如等待 I/O 操作完成或等待其他资源时,它会进入阻塞状态。在阻塞状态下,进程暂停执行,直到等待的事件发生。
  5. 终止状态(Terminated):当进程执行完毕或被操作系统终止时,它进入终止状态。

状态转化

新建
就绪
运行
终止
阻塞
创建
调度
时间到
事件等待
事件发生
退出
  1. 就绪态 → 运行态:
    • 调度:当操作系统的调度器选择一个就绪状态的进程分配给处理器时,该进程就会从就绪状态转换到运行状态。
  2. 运行态 → 就绪态:
    • 时间片用完:如果系统使用时间共享调度,当进程的时间片用完,它会被中断并放回就绪队列。
    • 优先级更高的进程就绪:在优先级调度算法中,如果一个优先级更高的进程变为就绪状态,当前运行的进程可能会被挂起。
    • 自愿放弃 CPU: 进程可能主动放弃 CPU,比如它发出了一个系统调用请求其他资源。
  3. 运行态 → 阻塞态:
    • I/O 请求:进程进行 I/O 操作,由于 I/O 设备比 CPU 慢得多,进程会被挂起直到 I/O 完成。
    • 等待资源:进程等待不可用的资源,如信号量、互斥锁等。
    • 等待事件:如等待其他进程的信号、消息或者某个条件的发生。
  4. 阻塞态 → 就绪态:
    • I/O 完成:当 I/O 操作完成,相应的进程会被移至就绪队列。
    • 资源获得:进程所等待的资源变得可用,如获得了互斥锁。
    • 事件发生:进程所等待的事件发生了,如接收到了另一个进程发出的信号。

进程内存空间

  • 用户空间(User Space):包含进程执行的用户程序代码和数据。在用户空间中,进程可以执行各种任务,如运行应用程序、访问文件系统等。用户空间对于应用程序是可见的,但对于操作系统中的核心功能是不可见的。
    1. 代码区(Text Segment):也称为"可执行代码区",存储了进程的可执行代码,包括程序的指令和只读数据。这个区域通常是只读的,因为程序的指令在运行时不应该被修改。
    2. 数据区(Data Segment), 数据区分为两个子区域:
      • 初始化数据区(Initialized Data Segment):存储全局和静态变量以及初始化的数据。这些变量在程序运行前就已经分配了内存并初始化。
      • 未初始化数据区(Uninitialized Data Segment):也称为"BSS"(Block Started by Symbol)段,存储全局和静态变量,但这些变量没有显式的初始化值。操作系统会在程序启动时自动将这个区域初始化为零。
    3. 堆区(Heap):堆区是动态分配内存的地方,用于存储程序运行时需要的变量和数据结构。在堆中分配的内存需要手动释放,以避免内存泄漏。
    4. 栈区(Stack):栈区用于存储函数调用和局部变量。每个函数调用都会在栈上创建一个栈帧,栈帧包含了函数的参数、局部变量以及函数返回地址。栈是一种后进先出(LIFO)的数据结构,它的大小通常有限,由操作系统或编程语言定义。
    5. 内存映射区域(Memory Mapped Region):这是一些操作系统或运行时库的扩展,用于存储动态链接库(DLL)和共享库的信息以及其他系统数据结构。
  • 内核空间(Kernel Space):内核空间包含了操作系统的核心代码和数据结构,如页表、调度程序和系统调用接口等。内核空间具有更高的特权级别,可以执行特权指令并且访问系统的各种资源,内核空间对于用户程序是不可见的。
Process-specific data
structures
(e.g. page tables, tasks and kernel stack)
Physical Memory
Kernel code and data
User stack
Memory-mapped region
for shared libraries
Run-time heap
Uninitialized data (.bss)
Initialized data (.data)
Code (.text)
Different for
each process
Identical for
each process
Kernel
virtual
memory
Process
virtual
memory
low
address
space
high
address
space

函数调用时内存结构

上一个栈帧
上一个栈帧的 EBP
局部变量
callee 的参数列表 n ~ 1
返回地址
caller 栈帧的 EBP
局部变量
下一个函数的参数列表 n ~ 1
返回地址
下一个栈帧
调用函数(caller)的栈帧
被调用函数(callee)的栈帧
高地址,栈底
低地址,栈顶
caller 的 EBP
callee 的 EBP

EBP (base pointer) 指向函数栈(栈帧)的底部(高地址),函数执行过程中在栈帧中分配局部变量,栈帧由高地址向低地址增长,ESP(stack pointer)一直指向栈顶。

每个函数的栈帧中包含如下内容:

  • 上一个函数的 EBP
  • 该函数的局部变量
  • 如果函数内有 call 指令的话,还需要保存额外信息:
    • 下一个函数的参数:依次存储从第 n 个到第 1 个,从高地址到低地址
    • 返回地址:当前 PC 指向的位置,即 call 指令的下一条指令的地址

在函数的调用过程中,调用函数叫做 caller,被调用函数叫做 callee。当我们从 caller 中调用 callee 时,callee 的 EBP 指向的物理地址的上下存储单元分别包含 caller 的返回地址以及 caller 所在栈帧的 EBP,当我们在 callee 中执行 ret 指令时,计算机可以跳转到 caller 中 call 指令的下一条并开始执行,同时 caller 的栈帧也会被恢复。

main
main
f1
main
f1
f2
main
f1
main
高地址
低地址
时间
函数的栈帧在内存中的结构变化
main( f1( f2 ))

进程间通信

Process  A
Shared Memory
Process B
Kernel
M
Process A
M
Process B
M
Kernel
1
2
1
2
共享内存
消息传递
  • 共享内存(Shared Memory):共享内存允许多个进程访问相同的物理内存区域,这样它们可以直接共享数据,而不需要复制数据。这是一种高效的通信方式,但需要谨慎管理共享数据以避免竞态条件。
  • 管道(Pipes):管道是一种单向通信方式,通常用于父子进程之间或兄弟进程之间的通信。有命名管道和匿名管道两种,匿名管道只能在有亲缘关系的进程之间使用。
  • 消息队列(Message Queues):消息队列是一种进程间通信的机制,允许进程通过消息来进行异步通信。消息队列通常由操作系统维护,可以支持多个读者和写者。
  • 信号(Signals):信号是一种轻量级的通信机制,用于通知进程发生了某些事件。进程可以发送信号给其他进程,比如终止信号、挂起信号等。
  • 套接字(Sockets):套接字是一种通用的进程间通信方式,通常用于不同计算机之间的网络通信。它支持多种协议,如 TCP 和 UDP,可以实现客户端-服务器通信。
  • 信号量(Semaphores):信号量是一种计数器,用于控制多个进程对共享资源的访问。信号量可以用于解决竞争条件和进程同步的问题。

用户级线程和内核级线程

  • 用户级线程(ULT,User Level Thread)
    • 用户级线程是由应用程序通过线程库来实现的,操作系统内核并不直接感知到这些线程的存在。
    • 线程的创建、调度和管理都由应用程序在用户空间完成。
  • 内核级线程(KLT,Kernel Level Thread)
    • 内核级线程是由操作系统内核直接支持的线程。
    • 线程的创建、调度和管理都由操作系统内核完成。

线程模型

操作系统中的线程实现方式称为线程模型,不同的线程模型在用户级线程(ULT)和内核级线程(KLT)的设计上各有不同。

系统中的线程模型可以分为如下三种:

  • 纯用户态:所有的线程操作都在用户空间中进行,内核对线程的存在一无所知。
  • 纯系统态:线程由操作系统内核直接支持和管理,内核负责线程的创建、调度和管理。
  • 混合方案:应用程序可以在用户空间管理多个用户级线程,这些线程映射到较少数目的内核级线程。
Thread
Library
P
User
Space
Kernel
Space
P
User
Space
Kernel
Space
P
User
Space
Kernel
Space
P
Thread
Library
a) Pure user-level
b) Pure kernel-level
c) combined
user-level thread
kernel-level thread
P
Process

混合方案中的映射关系

如果使用混合方案的线程模型的话,用户级线程和内核级线程的映射方式也可以分为如下几种:

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