进程和线程

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

进程和线程

两者的对比

进程主要特点如下:

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

线程主要特点如下:

  • 系统调度的基本单位: 多核处理器可以将系统中的不同线程调度到不同的 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 的栈帧也会被恢复。

进程间通信

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

用户级线程和内核级线程

  • 用户级线程(user level thread):用户级线程是在用户空间中创建和管理的线程,不依赖于操作系统的内核支持。这些线程由用户程序或用户级线程库管理,而不需要操作系统内核的介入。
  • 内核级线程(kernel level thread):内核级线程是由操作系统内核管理和调度的线程。每个内核级线程都有自己的内核数据结构(例如,进程控制块,PCB),由操作系统内核负责创建、销毁和调度。

这个东西其实不太好理解,以真实的应用场景为例,当我们使用 linux 的 pthread library 创建线程时,这个线程其实是由操作系统进行管理和调度的,并不需要用户程序进行管理,所以当我们使用系统调用创建线程时,可以理解为创建的实际是内核级线程。

那么用户级线程在实际应用中到底是怎样的呢,根据其定义,用户级线程由程序或用户级线程库管理,这里举实际的例子的话,就是 go 语言中的 goroutine 或者 python 异步库 asyncio 中的的 coroutine,这两者都是协程模型的实现,协程可以理解为更轻量的线程,协程库在用户空间内实现了一个调度机制,可以在系统执行一个线程的时间片内,创建多个协程并且在这个时间片内调度多个协程执行。由于协程的切换是在用户空间内实现的,不需要系统调用,所以效率更高,占用的资源更少。

用户级线程和内核级线程的映射方式(这里了解即可)包含多对一、一对一、多对多模型:

多对一模型
一对一模型
多对多模型
用户线程
内核线程

视频讲解