12.21【os复习】[day2]进程 线程 线程资源 辨析 线程池 PCB 进程资源 进程运行进程同步 常见场景

发布于:2024-12-23 ⋅ 阅读:(15) ⋅ 点赞:(0)

进程

线程

线程资源

  • 内核资源分配
    • 当创建一个新线程时,操作系统内核需要为其分配一定的资源。这包括为线程分配内核栈空间,用于存储线程在操作系统内核模式下运行时的函数调用信息、局部变量等。例如,在 Linux 系统中,内核栈大小通常是固定的(如 8KB 或 16KB),内核需要从系统的内存资源中划分出这部分空间给新线程。
    • 还需要为线程分配一个线程控制块(TCB,Thread Control Block),类似于进程控制块(PCB),TCB 用于存储线程的各种管理信息,如线程 ID、线程状态(就绪、运行、阻塞等)、线程优先级等。这些信息的存储和管理需要占用一定的内存空间,并且在创建过程中,操作系统需要对这些数据结构进行初始化操作,这涉及到一定的计算和内存读写开销。
  • 寄存器初始化开销
    • 新线程的寄存器也需要初始化。寄存器是处理器内部用于快速存储数据和指令的单元。在创建线程时,操作系统需要将一些初始值加载到寄存器中,如程序计数器(PC)需要被设置为线程开始执行的第一条指令的地址,通用寄存器可能需要根据线程的初始状态设置为默认值(如 0 或其他特定值)。这个初始化过程需要处理器进行多次数据传输和写入操作,会消耗一定的 CPU 时间。
  1. 线程调度开销

    • 上下文切换开销
      • 当操作系统从一个线程切换到另一个线程时,会发生上下文切换。这需要保存当前线程的上下文信息,包括寄存器内容、程序计数器(PC)的值等,并将这些信息存储到当前线程的 TCB 中。然后,操作系统从即将运行的线程的 TCB 中恢复其上下文信息,将保存的寄存器内容和 PC 值加载到处理器的寄存器中。这个过程涉及到多次内存读写操作和寄存器数据传输,会消耗一定的时间和 CPU 资源。例如,在一个频繁进行线程切换的多线程应用程序中,上下文切换开销可能会显著影响系统的性能。
      • 另外,在上下文切换过程中,操作系统还需要更新一些与线程调度相关的内核数据结构,如就绪队列和阻塞队列。如果采用优先级调度,还需要根据线程的优先级重新调整线程在就绪队列中的位置,这也会带来一定的计算和数据结构维护开销。
    • 调度算法执行开销
      • 操作系统需要根据一定的调度算法来选择下一个要运行的线程。不同的调度算法(如先来先服务、时间片轮转、优先级调度等)有不同的实现方式和计算复杂度。例如,优先级调度算法需要比较各个线程的优先级,这可能涉及到对线程优先级数据的读取和比较操作。时间片轮转调度算法需要维护每个线程的时间片信息,在每次调度时检查线程的时间片是否用完,并更新相应的时间片计数器。这些调度算法的执行都会消耗一定的 CPU 资源,尤其是在系统中有大量线程需要调度时,调度算法的开销可能会变得比较明显。
  2. 线程同步开销

    • 互斥锁开销
      • 当多个线程访问共享资源时,为了保证数据的一致性和正确性,通常需要使用互斥锁(Mutex)进行同步。获取和释放互斥锁会带来一定的开销。当一个线程试图获取一个已经被其他线程持有的互斥锁时,它会进入阻塞状态,等待锁被释放。这个等待过程会涉及到线程状态的转换(从运行状态变为阻塞状态),并且需要操作系统将该线程从就绪队列移动到阻塞队列,这会消耗一定的时间和资源。
      • 同时,互斥锁的实现通常依赖于原子操作(如原子的比较并交换操作)来保证锁的正确性。原子操作需要硬件的支持,并且在执行过程中会涉及到特殊的指令和内存屏障操作,这些操作的执行速度相对较慢,会增加线程同步的开销。
    • 信号量和条件变量开销
      • 信号量(Semaphore)用于控制对共享资源的访问数量,条件变量(Condition Variable)用于线程之间的等待和唤醒操作。与互斥锁类似,使用信号量和条件变量进行同步也会带来开销。当线程等待信号量或条件变量时,会进入阻塞状态,涉及到状态转换和队列操作。而且信号量和条件变量的操作通常需要在多个线程之间进行通信和协调,这可能会导致缓存一致性问题(因为不同线程可能在不同的 CPU 核心上运行,它们的缓存数据需要保持一致),进一步增加了开销。
  3. 线程终止开销

    • 当一个线程完成任务或因其他原因需要终止时,操作系统需要进行一些清理工作。这包括释放线程占用的内核栈空间和 TCB 等资源。释放内核栈空间需要操作系统将这部分内存标记为空闲,以便其他线程或进程可以使用。对于 TCB,需要将其从相关的管理数据结构(如就绪队列或阻塞队列)中移除,并释放其占用的内存。这些操作都需要一定的计算和内存管理开销。

  1. TCB(线程控制块)位于内核空间

    • 原因一:安全性和管理需求
      • 线程控制块包含了线程的关键管理信息,如线程状态(就绪、运行、阻塞等)、线程优先级、寄存器内容(当线程被暂停时保存)等。这些信息对于操作系统内核正确地调度和管理线程是至关重要的。如果 TCB 位于用户空间,恶意程序可能会篡改这些信息,导致系统对线程的管理失控,例如修改线程优先级来获取更多的 CPU 资源,或者破坏寄存器内容使得线程无法正确恢复执行。
    • 原因二:内核操作的便利性
      • 操作系统内核需要频繁地访问和修改 TCB 中的信息。例如,在进行线程调度时,内核需要根据线程的状态和优先级来决定下一个要运行的线程,这就需要读取和更新 TCB 中的状态和优先级信息。当线程进行上下文切换时,内核需要保存和恢复线程的寄存器内容,这些操作都要求 TCB 能够被内核快速、安全地访问。将 TCB 放置在内核空间可以方便内核直接进行这些操作,而不需要通过复杂的用户空间 - 内核空间切换机制来访问。
  1. TCB 不在进程的用户地址空间

    • 隔离性考虑
      • 进程的用户地址空间主要用于存储用户程序代码、数据(如全局变量、局部变量)、堆(用于动态内存分配)和栈(用于函数调用和局部变量存储)。如果 TCB 位于进程的用户地址空间,不同进程之间可能会因为误操作或者恶意行为而互相干扰对方的线程管理信息。例如,一个进程可能会错误地修改另一个进程的线程控制块,导致其他进程的线程出现异常,如无法正确地被调度或者恢复执行。
    • 系统级资源管理角度
      • TCB 属于系统级的资源管理数据结构,用于支持操作系统对线程的全局管理。它与进程的用户级资源(如用户程序中的变量、文件描述符等)在管理层次和功能上是不同的。将 TCB 放置在内核空间可以更好地从系统层面统筹管理线程,独立于各个进程的用户地址空间,确保线程管理的公正性和高效性。

辨析

    • 基本组成部分类似
      • 进程和线程的上下文都包含程序计数器(PC)。对于进程而言,PC 指向进程下一条要执行的指令的地址;对于线程也是如此,它指示线程即将执行的指令位置。例如,在一个多线程的程序中,无论是进程还是线程,当执行被中断时,都需要保存这个 PC 的值,以便之后能够从被中断的位置继续执行。
      • 寄存器内容也是两者上下文共有的部分。寄存器用于临时存储正在处理的数据和指令。当进程或线程被暂停时,如在进行进程 / 线程切换或者等待某个事件(I/O 操作完成、信号量等)时,处理器的寄存器内容会被保存到进程或线程的上下文中。这些寄存器包括通用寄存器(用于存储操作数和计算结果)和状态寄存器(包含进位标志、溢出标志等条件码信息)。例如,在一个算术运算过程中,线程被中断,其通用寄存器中的操作数和运算中间结果会被保存,等到线程恢复执行时,这些寄存器内容会被重新加载,使运算能够继续进行。
  1. 进程上下文与线程上下文的不同点

    • 资源范围差异
      • 进程上下文包含整个进程的资源信息,如进程的内存空间布局(包括代码段、数据段、堆和栈)。进程有自己独立的虚拟内存空间,这个空间中的所有信息都属于进程上下文的一部分。例如,当进行进程切换时,需要切换内存映射关系,因为不同进程的虚拟地址到物理地址的映射是不同的,这是通过内存管理单元(MMU)和页表(在分页存储管理中)来实现的。而线程是共享进程的内存空间的,线程上下文通常不涉及完整的内存空间切换。线程主要在进程已分配的内存空间内进行操作,多个线程可以访问进程的全局变量、堆和栈等资源,它们之间通过同步机制来避免对共享资源的冲突访问。
      • 进程上下文还包括文件描述符等资源信息。进程在打开文件、设备等资源时,会获得相应的文件描述符,这些文件描述符的信息(如文件的打开模式、当前读写位置等)属于进程上下文。当进程切换时,文件描述符等资源信息也会被保存和切换。线程共享进程的文件描述符,在大多数情况下,线程对文件的操作是基于进程已打开的文件描述符进行的,所以线程上下文通常不单独考虑文件描述符的切换问题,除非有特殊的线程 - 特定文件操作场景。

  1. 一般情况:PC 值相同

    • 原理:在大多数情况下,当程序被中断时,进程中正在运行的线程的程序计数器(PC)和进程的 PC 保存的值是一致的。这是因为进程的执行实际上是通过线程来进行的,在单线程进程中,进程的执行路径和线程的执行路径完全相同。在多线程进程中,当某个特定线程正在运行时,这个线程的执行指令序列就代表了进程在这个时刻的执行流程。例如,在一个多线程的服务器程序中,当一个线程正在处理客户端请求时,这个线程的 PC 指向当前处理请求的指令位置,而从进程角度看,这个指令位置也是进程当前执行的位置。
    • 举例:假设一个简单的多线程 C 程序,有两个线程分别执行不同的函数。当其中一个线程正在执行一个循环结构的函数,并且这个线程正在执行循环体中的某条指令时被中断,此时这个线程的 PC 指向循环体中的这条指令,而进程的 PC 同样指向这条指令,因为进程此时的执行内容就是由这个正在运行的线程所决定的。
  2. 特殊情况:PC 值可能不同

    • 信号处理导致不同
      • 原理:如果程序接收到一个信号,并且这个信号有对应的信号处理程序。当信号被触发时,进程可能会中断当前线程的执行,转而去执行信号处理程序。在这种情况下,进程的 PC 会指向信号处理程序的入口地址,而正在运行的线程的 PC 会被保存为它被中断时的指令位置。例如,在 Linux 系统中,当一个进程接收到SIGINT(通常由用户按下Ctrl + C产生)信号时,如果为这个信号注册了信号处理程序,进程会暂停当前线程的执行,将线程的 PC 保存起来,然后将进程的 PC 设置为信号处理程序的入口地址开始执行信号处理程序。
    • 内核调度干预导致不同
      • 原理:在某些复杂的内核调度场景下,虽然比较少见,但也可能导致进程和线程的 PC 值不同。例如,当内核检测到系统的某些紧急情况(如内存严重不足、硬件错误等),可能会强行中断进程的当前执行线程,并将进程切换到一个特殊的内核线程或者进行一些紧急的资源回收操作。在这种情况下,进程的 PC 可能会被内核修改为与正在运行的线程的 PC 不同的值,用于执行内核规定的紧急任务流程。

线程池

  • 降低线程创建和销毁的开销
    • 线程的创建和销毁是有成本的。当创建一个新线程时,操作系统需要为其分配内存空间(如线程控制块、栈空间等),并进行初始化操作,这个过程涉及到系统调用和资源分配。例如,在 Java 中,每次创建一个新线程,JVM(Java 虚拟机)需要为其分配一定的栈空间(默认为 1MB 左右),并且需要进行一系列的初始化工作,包括设置线程的优先级、上下文等。而线程销毁时,也需要释放这些资源。如果频繁地创建和销毁线程,这些开销会累积,影响系统性能。线程池通过预先创建好线程,避免了这种频繁的创建和销毁过程,降低了系统的开销。
  • 提高响应速度
    • 对于一些需要快速响应的任务,如服务器接收客户端请求,如果每次请求都要先创建一个新线程来处理,那么在创建线程的过程中会产生延迟。而线程池中的线程是预先创建好的,当有任务到来时,可以立即分配线程进行处理,从而提高了响应速度。例如,在一个 Web 服务器中,使用线程池可以让服务器更快地处理客户端的 HTTP 请求,减少用户等待时间。
  • 控制并发线程数量
    • 线程池可以对同时执行的线程数量进行限制。如果不加控制地创建线程来处理任务,可能会导致系统中线程数量过多,从而耗尽系统资源(如 CPU 时间片、内存等)。线程池可以通过设置最大线程数来避免这种情况。例如,在一个多线程的应用程序中,如果同时有大量任务需要处理,线程池可以根据系统资源和任务的优先级等因素,合理地分配线程,确保系统不会因为线程过多而崩溃。同时,通过调整线程池的大小,可以优化系统的性能,根据任务的负载情况来灵活地分配资源。
  • 便于线程管理和资源分配
    • 线程池提供了一种集中管理线程的方式。可以方便地对线程进行监控、调度和资源分配。例如,可以对线程池中的线程进行统一的优先级设置,使得高优先级的任务能够优先得到处理。还可以根据任务的类型,将任务分配到不同的线程池进行处理,实现资源的合理分配和系统的模块化管理。此外,通过线程池可以更好地实现线程的复用,提高线程的利用率,避免线程资源的浪费。

PCB

  1. 进程标识信息

    • 进程 ID(PID):这是操作系统为每个进程分配的唯一标识符。就像每个人都有一个身份证号码一样,PID 用于在系统范围内区分不同的进程。例如,在 Linux 系统中,通过ps命令查看进程列表时,每个进程都有对应的 PID。系统利用 PID 来跟踪和管理进程,如发送信号给特定进程、查询进程状态等操作都需要使用 PID。
    • 父进程 ID(PPID):它记录了创建该进程的父进程的 PID。这有助于构建进程家族树,了解进程之间的派生关系。例如,当一个 shell 脚本启动一个新的程序时,新程序进程的 PPID 就是启动它的 shell 进程的 PID。这种父子关系对于进程的资源继承(如文件描述符继承)和信号传递等方面具有重要意义。
  2. 进程状态信息

    • 运行状态(Running):表明进程正在 CPU 上执行指令。例如,在一个单 CPU 的系统中,只有一个进程可以处于运行状态。不过在多 CPU 或多核系统中,可以有多个进程同时处于运行状态,每个 CPU 核心可以运行一个进程。
    • 就绪状态(Ready):进程已经准备好运行,只要 CPU 空闲就可以马上执行。处于就绪状态的进程已经获得了除 CPU 之外的所有资源,等待 CPU 调度器分配 CPU 时间片给它。例如,在一个多任务操作系统中,有多个进程可能同时处于就绪状态,它们在等待操作系统根据调度算法(如先来先服务、优先级调度等)选择它们来运行。
    • 阻塞状态(Blocked):进程因为等待某个事件的发生而暂停执行,如等待 I/O 操作完成(例如读取磁盘文件、等待网络数据到达)或者等待某个信号量。当进程执行一个需要等待外部资源的操作时,它会进入阻塞状态,此时 CPU 可以切换去执行其他就绪进程。一旦等待的事件完成,进程会被唤醒并转换为就绪状态。
  3. 进程的调度信息

    • 优先级(Priority):用于确定进程在就绪队列中的执行顺序。优先级高的进程通常会比优先级低的进程先获得 CPU 资源。例如,在实时操作系统中,实时任务进程的优先级会高于普通的用户进程,以确保实时任务能够及时得到处理。优先级可以是静态分配的,也可以是动态调整的,操作系统会根据进程的行为和系统的负载等因素来动态改变进程的优先级。
    • 调度策略相关信息:不同的操作系统可能采用不同的调度策略,如先来先服务(FCFS)、短作业优先(SJF)、时间片轮转(RR)等。PCB 中可能包含与调度策略相关的信息,如时间片大小(对于时间片轮转调度)、进程的剩余执行时间(对于短作业优先调度的估计)等,这些信息帮助操作系统的调度器按照预定的策略来分配 CPU 资源给进程。
  4. 进程的资源信息

    • 内存资源信息:包括进程使用的内存区域,如代码段、数据段、堆和栈的地址范围。通过这些信息,操作系统可以管理进程的内存分配和回收,以及实现内存保护。例如,当进程访问一个内存地址时,操作系统可以根据 PCB 中的内存资源信息判断这个访问是否合法,防止进程访问不属于它的内存区域。
    • 文件描述符(File Descriptor):记录了进程打开的文件和设备的相关信息。每个文件描述符是一个整数,对应一个打开的文件或设备。例如,当进程打开一个文件进行读写操作时,操作系统会为该进程分配一个文件描述符,这个文件描述符的相关信息(如文件的打开模式、当前读写位置等)会存储在 PCB 中。进程可以通过文件描述符来操作文件,如读取文件内容、写入数据、移动读写位置等。
    • 信号量(Semaphore)和其他同步机制信息:如果进程使用了信号量或其他同步机制(如互斥锁)来协调与其他进程的并发操作,PCB 中会包含这些同步工具的相关信息,如信号量的值、进程对互斥锁的拥有情况等。这些信息对于维护系统的并发安全性和正确性非常重要。
  5. 进程的程序执行信息

    • 程序计数器(PC):指向进程下一条要执行的指令的地址。它在进程的执行过程中起着关键作用,随着指令的执行不断更新。例如,在一个顺序执行的程序中,PC 会按照指令的顺序依次指向下一条指令;当遇到跳转指令(如条件跳转或无条件跳转)时,PC 会被更新为跳转目标地址,从而改变程序的执行方向。
    • 寄存器内容:当进程被暂停(如在进程切换时),其处理器中的寄存器内容会被保存到 PCB 中。这些寄存器包括通用寄存器(用于存储操作数和计算结果)、状态寄存器(包含进位标志、溢出标志等条件码信息)等。保存寄存器内容可以确保进程在恢复执行时能够从之前中断的地方继续,并且能够使用之前存储在寄存器中的数据和状态。

进程资源

  1. 进程地址空间的划分基础
    • 进程的地址空间是一个虚拟的地址范围,操作系统将其划分为不同的段来更好地管理和组织进程的资源。这种划分主要基于程序的功能结构和数据存储需求。例如,为了防止程序的指令(代码)被意外修改,同时方便代码的共享和保护,将代码单独划分成一个段;为了区分程序运行过程中使用的静态数据(全局变量、静态变量)和动态数据(栈中的局部变量、堆中的动态分配数据),又分别划分出数据段、栈段和堆段。
  2. 代码段(Text Segment)
    • 定义和功能
      • 代码段是进程地址空间中存放可执行指令的部分。这些指令是程序的逻辑代码,例如函数的定义、循环语句、条件判断语句等。代码段通常是只读的,这是为了防止程序在运行过程中意外修改自己的代码,从而保证程序的稳定性和安全性。例如,在一个 C 语言程序中,所有的函数体(如int main() {...}中的代码)都存储在代码段。
    • 区分方式
      • 在内存管理系统中,通过段表(Segment Table)或页表(Page Table)中的相关标记来区分代码段。对于段式存储管理,段表中有一个条目对应代码段,该条目记录了代码段的起始地址、长度和访问权限(如只读)等信息。在分页存储管理结合虚拟内存的系统中,代码页在页表中的映射会被标记为只读,处理器在访问这些页面时,会根据页表中的标记来判断是否允许写操作,从而区分代码段和其他可写的段。
  3. 数据段(Data Segment)
    • 定义和功能
      • 数据段主要存放程序中已初始化的全局变量和静态变量。这些变量在程序的整个生命周期内都存在,并且在程序启动时就已经分配了内存空间并进行了初始化。例如,在 C 语言程序中,int global_variable = 10;这样的全局变量就存储在数据段。数据段是可读写的,因为程序在运行过程中可能会修改这些变量的值。
    • 区分方式
      • 同样通过段表或页表来区分。在段式存储管理中,数据段在段表中有自己的条目,记录其起始地址、长度和读写权限等信息。在分页系统中,数据页在页表中的映射会被标记为可读写,当处理器访问这些页面时,根据页表的标记可以识别出是数据段。此外,数据段的内存地址范围通常是在代码段之后(在虚拟地址空间中),通过地址范围也可以在一定程度上进行区分。
  4. 栈段(Stack Segment)
    • 定义和功能
      • 栈段用于存储函数调用的相关信息,包括函数的参数、局部变量、返回地址等。它是一种后进先出(LIFO)的数据结构。每当一个函数被调用时,相关的信息就会被压入栈中,函数返回时,这些信息会从栈中弹出。例如,在一个函数void func(int a) { int b = 5;... }中,参数a和局部变量b的存储位置就在栈段。
    • 区分方式
      • 栈段的管理主要由栈指针(Stack Pointer)来实现。栈指针指向栈顶元素,通过栈指针的移动来操作栈中的数据。在内存管理系统中,栈段在段表或页表中也有相应的记录。其地址范围通常是在数据段之后(在虚拟地址空间中),并且栈的大小在程序运行过程中可以动态变化。操作系统会为栈段设置一个最大大小限制,当栈的增长超过这个限制时,可能会导致栈溢出错误。通过栈指针的操作方式和其独特的地址范围可以区分栈段与其他段。
  5. 堆段(Heap Segment)
    • 定义和功能
      • 堆段是用于动态内存分配的区域。在程序运行过程中,当需要动态分配内存(如 C 语言中的malloc函数)时,就会从堆段中分配空间。堆段用于存储那些在程序运行过程中动态创建的数据结构,如链表节点、动态数组等。它是可读写的,并且其大小可以根据程序的动态内存分配请求而变化。
    • 区分方式
      • 堆段的内存分配和管理比较复杂。在操作系统层面,通过专门的内存分配器来管理堆段。在段表或页表中,堆段也有自己的记录,其地址范围通常是在栈段之后(在虚拟地址空间中)。与栈段不同的是,堆段的内存分配是由程序主动请求(通过如malloc等函数),而不是像栈段那样由函数调用自动操作。并且堆段的内存释放也是由程序显式地进行(如 C 语言中的free函数),通过这些内存分配和释放的操作特点以及其在地址空间中的位置可以区分堆段与其他段。

  1. 运行中的进程所需资源
    • CPU 时间:进程需要 CPU 来执行指令。例如,一个进行复杂数学计算的科学计算程序,会占用 CPU 时间来执行计算指令,像进行矩阵乘法运算时,CPU 会按照程序中的指令序列,从内存中读取数据,在寄存器中进行运算,这个过程需要分配 CPU 时间来完成。
    • 内存空间
      • 代码段:用于存储进程的可执行代码。例如,一个 C 语言编写的简单文件读写程序,其代码段包含了打开文件、读取或写入文件内容、关闭文件等操作对应的机器指令。
      • 数据段:存放全局变量和静态变量。比如一个记录用户登录信息的程序,其全局变量(如记录用户登录次数、用户名等)就存储在数据段。
      • :用于动态分配内存。以一个处理动态链表的程序为例,当程序需要添加一个新的链表节点时,会通过堆内存分配函数(如 C 语言中的malloc)在堆中申请一块内存来存储新节点的数据。
      • :主要用于存储局部变量和函数调用信息。例如,当一个函数被调用时,函数的参数、局部变量以及返回地址都会被压入栈中。像在一个递归函数(如计算斐波那契数列的递归函数)中,每次函数调用的参数和局部变量都会在栈中占用空间。
    • I/O 设备
      • 进程可能需要从外部设备读取数据或者向外部设备输出数据。例如,一个文件打印程序需要使用打印机(I/O 设备)输出文件内容,它会通过操作系统提供的 I/O 接口与打印机进行通信,将文件内容发送到打印机进行打印。一个数据库应用程序可能需要从硬盘(I/O 设备)读取数据文件来进行数据查询和处理。
  2. 物理内存空间的组织和分配方式
    • 连续分配方式
      • 单一连续分配:这种方式主要用于早期的单用户、单任务操作系统。系统将内存分为系统区和用户区,用户区内存是一个连续的空间,只分配给一个进程使用。例如,早期的 DOS 操作系统在运行一个简单的文字处理程序时,这个程序可能会占用用户区的大部分连续内存空间。
      • 固定分区分配:把内存划分为若干个固定大小的分区,每个分区可以装入一个进程。例如,假设有内存空间被划分为大小为 10MB、20MB 和 30MB 的三个分区,一个大小为 8MB 的进程就可以装入 10MB 的分区中。这种方式简单,但容易造成内存空间的浪费,因为一个分区只能装入一个进程,即使分区还有剩余空间也不能被其他进程利用。
    • 非连续分配方式
      • 分页存储管理:将内存空间和进程的逻辑空间都划分成固定大小的页。例如,物理内存被划分为每页 4KB 大小的页框,进程的逻辑空间也被划分为同样大小的页。在分配内存时,进程的各页可以分散地存储在物理内存的不同页框中。当进程访问某一页时,通过页表来实现从虚拟页到物理页框的映射。比如一个进程的第 3 页逻辑页面,通过页表查询后,可能映射到物理内存的第 10 页框中。
      • 分段存储管理:按程序的逻辑结构划分段,如代码段、数据段等,每个段有自己的段号和段内地址。在物理内存中,段可以是不连续的。例如,一个大型程序的代码段可能存储在物理内存的开头部分,数据段可能存储在中间部分,不同的段通过段表进行映射。当进程需要访问某一段内的地址时,通过段表找到对应的物理内存位置。
      • 段页式存储管理:这是分段和分页结合的方式。先将进程按逻辑结构分段,然后每一段再分页。它综合了分段和分页的优点,既方便程序的模块化设计,又能有效利用物理内存空间。例如,一个大型软件系统的开发工具,它的代码段可以根据功能模块分段,然后每一段再分页存储到物理内存中,通过段表和页表来实现复杂的地址映射。
  1. PCB(进程控制块)的位置

    • PCB 并不在进程的用户地址空间中,它是操作系统内核数据结构,存放在操作系统内核的内存区域。这是因为 PCB 包含了对进程进行管理和调度的关键信息,需要被操作系统内核访问和维护,并且要保证其安全性和独立性,避免被进程本身随意修改。例如,在 Linux 系统中,PCB(在 Linux 中称为task_struct)存储在系统内核的内存区域,这个区域是用户进程无法直接访问的,只有通过系统调用等内核提供的合法方式才能间接操作与 PCB 相关的信息。
  1. 逻辑视图
    • 按功能模块划分
      • 从程序的功能角度来看,进程地址空间可分为代码段、数据段、堆和栈。这种划分方式能够清晰地展现程序的不同功能部分所对应的内存区域。例如,代码段存储程序的可执行指令,体现了程序的逻辑功能实现;数据段存放全局和静态变量,用于在程序的不同部分之间共享数据;堆提供动态内存分配,以支持程序在运行时灵活地申请和释放内存,比如在创建复杂的数据结构如链表、树时就会用到堆;栈用于存储局部变量和函数调用信息,保证函数调用的正确执行和局部变量的存储。
    • 按编程语言的内存布局划分
      • 不同的编程语言对内存布局有自己的规范。以 C/C++ 语言为例,除了上述的代码段、数据段、堆和栈外,还可能涉及常量区(用于存储常量字符串等)。在 Java 中,虽然程序员一般不需要直接关注内存的分配细节,但从逻辑上看,其内存空间也有方法区(存储类的结构信息等)、堆(对象的存储)、栈(局部变量和方法调用帧)和本地方法栈(用于执行本地方法)等划分。这种划分方式有助于程序员根据语言的特点理解程序在内存中的布局,以及不同类型的数据和指令是如何存储和访问的。
  2. 物理视图
    • 基于内存管理单元(MMU)的分页视图
      • 在操作系统和硬件的协作下,进程地址空间可以看作是由一系列的页组成。MMU 将进程的虚拟地址空间和物理内存空间都划分为固定大小的页(例如,在许多系统中,页大小为 4KB)。从这个角度看,进程地址空间是一个页的集合,每个页通过页表映射到物理内存中的页框或者磁盘中的交换空间(如果该页暂时不在物理内存中)。这种划分方式有助于实现虚拟内存机制,使得进程可以使用比实际物理内存大得多的地址空间,并且通过页面置换算法等方式实现高效的内存管理。
    • 基于内存分配方式的连续 / 非连续视图
      • 从物理内存的分配角度,进程地址空间可以分为连续分配和非连续分配两种情况。在连续分配方式下,如早期的单一连续分配或固定分区分配,进程的地址空间在物理内存中是连续的一块区域。而在非连续分配方式下,像分页存储管理和分段存储管理,进程的地址空间可以分散在物理内存的不同位置。这种划分方式能够体现出内存管理策略的演变,从早期简单的连续分配到现在更灵活的非连续分配,以适应复杂多变的程序需求和提高内存利用率。
  3. 用户 / 内核视图
    • 用户空间视图
      • 对于用户程序来说,它主要关注的是自己能够直接访问的地址空间部分,即进程地址空间中的用户空间。这部分空间包括代码段、数据段、堆和栈等用户程序可以操作的区域。用户程序在这个空间内进行各种计算、数据存储和访问等操作,一般不会直接涉及内核空间的内容。这种视图体现了用户程序的 “活动范围”,有助于程序员编写和理解应用程序的功能实现,同时也保证了系统的安全性,因为用户程序不能随意访问内核空间。
    • 内核空间视图
      • 从操作系统内核的角度看,进程地址空间除了用户空间部分,还有内核空间部分。内核空间是操作系统内核运行的区域,它包含了内核代码和数据,以及用于管理进程的各种数据结构,如 PCB(进程控制块)。内核空间用于实现系统调用、进程调度、内存管理等核心功能。当用户进程通过系统调用请求内核服务时,会涉及到用户空间和内核空间之间的切换。这种视图强调了内核在管理进程和整个系统中的核心地位,以及内核与用户进程之间的交互方式。

PCB(进程控制块)的存储位置

  • 内核空间存储:PCB 通常存储在操作系统的内核空间。这是因为 PCB 包含了进程的关键管理信息,如进程状态(就绪、运行、阻塞等)、程序计数器(PC)、寄存器内容、进程优先级等。这些信息需要被操作系统内核访问和管理,以实现进程的调度、切换和资源分配等功能。
  • 不占用进程自身地址空间:它不属于进程本身的用户地址空间。因为如果 PCB 位于进程的用户地址空间,当进程出现异常(如访问非法内存导致段错误)或者被其他进程非法访问时,PCB 中的重要管理信息可能会被破坏,从而导致系统对进程的管理失控

进程在地址空间中的工作方式

  • 用户空间和内核空间划分
    • 用户空间:进程的用户地址空间是进程用于存储用户程序代码、数据(如全局变量、局部变量)、堆(用于动态内存分配)和栈(用于函数调用和局部变量存储)的区域。当进程运行用户程序时,它主要在这个空间内操作。例如,一个 C 语言程序中的函数定义、变量声明等对应的机器码和数据都存储在用户空间。用户空间的地址范围是由操作系统分配给进程的,不同进程的用户空间是相互独立的,这样可以防止一个进程非法访问另一个进程的用户空间数据。
    • 内核空间:这是操作系统内核代码和数据存储的区域。进程在执行一些系统调用(如文件读写、进程创建、网络通信等)时,会通过特定的机制(如陷入指令)进入内核空间,由操作系统内核来执行相应的操作。例如,当一个进程调用read系统调用读取文件内容时,它的执行流程会从用户空间切换到内核空间,在内核空间中,操作系统内核会根据文件系统的相关信息和请求,从磁盘读取数据,然后再将数据返回给用户空间的进程。
  • 虚拟地址到物理地址的转换
    • 内存管理单元(MMU)的作用:进程使用的是虚拟地址空间,这些虚拟地址需要通过 MMU 转换为物理地址才能真正访问物理内存中的数据。MMU 会维护一张页表,页表记录了虚拟地址和物理地址之间的映射关系。例如,当进程访问一个虚拟地址时,MMU 会查找页表,确定对应的物理地址,然后进行数据访问。这种虚拟地址机制有很多优点,如可以实现进程间的内存隔离、方便操作系统进行内存管理(如内存共享、内存交换等)。
    • 地址空间的动态扩展和收缩:进程的地址空间在运行过程中可以动态变化。例如,在堆空间中,当进程通过malloc(C 语言中的动态内存分配函数)等函数请求更多的内存时,操作系统可能会为进程扩展其堆空间的虚拟地址范围,并在物理内存中分配相应的空间(如果物理内存足够)。同样,当进程释放内存(如通过free函数)时,其地址空间中的相关区域可以被回收,虚拟地址范围也可能会收缩。

进程运行

  1. 内存数据的情况

    • 进程的独立内存空间
      • 每个进程都有自己独立的虚拟内存空间。操作系统通过内存管理单元(MMU)来实现进程间的内存隔离。在进程切换时,物理内存中的数据不会自动被替换,因为不同进程的虚拟内存空间到物理内存空间的映射是不同的。
    • 共享内存和数据共享情况
      • 有些情况下,进程之间可能会共享部分内存区域。例如,在多进程服务器应用程序中,多个进程可能会共享一块用于存储配置信息的内存区域。在这种情况下,当进程切换时,共享内存区域的数据不会被替换,并且新进程可以根据权限访问这些共享数据。但是,每个进程也有自己独立的栈空间和堆空间用于存储局部变量、函数调用信息和动态分配的数据等,这些区域在进程切换时不会被新进程的数据随意替换。

  1. 缓存数据的影响

    • 处理器缓存中的数据
      • 处理器的缓存(如一级缓存、二级缓存等)中可能存储着进程执行过程中的数据和指令。在进程切换时,这些缓存数据不会立即被清除或替换为新进程的数据。因为缓存数据的清理和更新是一个复杂的过程,并且可能会影响系统的性能。
    • 缓存一致性维护
      • 操作系统和硬件会通过缓存一致性协议来确保缓存数据的正确性。例如,在多核处理器系统中,如果一个进程在一个核心的缓存中有数据,当另一个进程在其他核心上运行时,系统会根据缓存一致性协议来处理缓存中的数据,可能会使某些缓存行无效或者更新,以保证新进程能够正确地访问数据。不过,缓存数据的更新是基于缓存命中情况和数据的共享情况等因素动态进行的,不是在进程切换时就全部替换为新进程的数据。

进程同步

信号量是一种用于进程或线程同步的机制,它本质上是一个非负整数变量。这个变量用于控制对共享资源的访问,其值表示可用资源的数量。例如,在一个多线程的文件读取系统中,如果有 3 个线程同时想要读取文件,但是系统规定同时最多只能有 2 个线程进行文件读取操作,那么就可以使用信号量来管理这个共享资源(文件读取权限),初始信号量的值可以设为 2

  • P 操作(等待操作)
    • 也称为 “down” 操作或者 “wait” 操作。当一个进程或线程想要访问共享资源时,它会执行 P 操作。P 操作会将信号量的值减 1。如果信号量的值大于等于 1,那么进程或线程可以继续执行并访问共享资源;如果信号量的值小于 1(即已经没有可用资源),那么执行 P 操作的进程或线程会被阻塞,进入等待状态,直到信号量的值大于等于 1(即有其他进程或线程释放了资源)。例如,在上述文件读取的例子中,当 3 个线程都尝试执行 P 操作来获取文件读取权限时,前两个线程可以成功执行 P 操作(信号量的值从 2 变为 1,再变为 0)并开始读取文件,而第三个线程会因为信号量的值变为 0 而被阻塞。
  • V 操作(信号操作)
    • 也称为 “up” 操作或者 “signal” 操作。当一个进程或线程使用完共享资源后,它会执行 V 操作。V 操作会将信号量的值加 1。如果有其他进程或线程因为之前执行 P 操作而被阻塞等待这个信号量,那么这些被阻塞的进程或线程中的一个(通常是按照等待的先后顺序)会被唤醒,然后可以重新尝试执行 P 操作来获取资源。例如,在文件读取的例子中,当一个线程读完文件后执行 V 操作,信号量的值从 0 变为 1,此时被阻塞的第三个线程就可以被唤醒,然后尝试执行 P 操作来获取文件读取权限。

  • 互斥访问控制
    • 信号量可以用于实现互斥访问。例如,在一个多线程的数据库系统中,多个线程可能会同时访问和修改数据库中的某个记录。为了保证数据的完整性和一致性,需要确保同一时刻只有一个线程能够修改这个记录。可以使用一个信号量(初始值为 1)来控制对这个记录的访问。当一个线程想要修改记录时,它执行 P 操作获取信号量(信号量值从 1 变为 0),此时其他线程想要修改记录时执行 P 操作会被阻塞。当修改记录的线程完成操作后,执行 V 操作释放信号量(信号量值从 0 变为 1),这样就实现了对数据库记录的互斥访问。
  • 资源分配管理
    • 如前面提到的文件读取例子,信号量可以用于管理有限的资源。在操作系统中,对于像打印机、磁盘 I/O 通道等物理资源,或者像数据库连接、网络套接字等软件资源,都可以使用信号量来分配和控制。通过合理设置信号量的初始值,可以根据实际资源的数量来限制同时访问这些资源的进程或线程的数量。
  • 生产者 - 消费者问题解决
    • 这是一个经典的并发编程问题。在一个有生产者线程和消费者线程的系统中,生产者生产数据并将其放入一个缓冲区,消费者从缓冲区中取出数据进行消费。可以使用两个信号量来解决这个问题。一个信号量用于控制生产者对缓冲区的写入操作(初始值为缓冲区的空闲空间数量),另一个信号量用于控制消费者对缓冲区的读取操作(初始值为缓冲区中初始的产品数量)。生产者在写入数据前执行 P 操作来获取空闲空间信号量,写入后执行 V 操作释放产品数量信号量;消费者在读取数据前执行 P 操作来获取产品数量信号量,读取后执行 V 操作释放空闲空间信号量,这样就可以协调生产者和消费者之间的操作,避免缓冲区的溢出和空读问题。
  • 临界区是指在多线程或多进程环境下,访问共享资源(如共享变量、共享文件、共享设备等)的一段代码区域。在这段代码中,多个线程或进程对共享资源的访问必须进行同步控制,以避免出现数据不一致、竞争条件等问题。例如,假设有两个线程同时对一个共享变量进行读写操作,如果没有适当的同步机制,就可能导致数据错误。这个共享变量的读写操作代码部分就是临界区。

    • int oldval;:在函数内部声明了一个整型变量oldval,用于存储后续从指针word所指向的内存位置读取出来的值,也就是当前的旧值。
  1. 读取旧值
    • oldval=*word;:通过解引用指针word,将其所指向内存位置存储的整数值赋给oldval。例如,如果word指向的内存地址中存储的是整数5,那么执行完这行代码后,oldval的值就会变为5
  2. 比较并更新值(条件判断与赋值操作)
    • if(oldval==testval):这里将刚刚获取到的旧值oldval与传入函数的参数testval(期望的值)进行比较。如果两者相等,说明满足更新的条件。
    • *word=newval;:当oldvaltestval相等时,就通过解引用指针word,将其指向的内存位置的值更新为newval。例如,testval5newval10,且oldval也为5时,执行完这行代码后,word所指向的内存位置的值就会从5变为10
  3. 返回旧值
    • return oldval;:无论是否进行了值的更新操作,函数最后都会返回最开始获取到的那个内存位置的旧值oldval。这个返回值可以被调用者用于后续的判断等操作,比如判断是否成功进行了期望的更新操作等。

总体而言,这个函数提供了一种原子性操作(虽然代码本身不是真正原子操作,只是实现类似逻辑,真正原子操作依赖硬件或特定库支持)的功能雏形,常用于多线程环境下对共享变量的操作,通过比较当前值和期望的值来决定是否更新,以此来避免多个线程同时修改同一变量时可能出现的数据不一致问题,但实际使用中如果要保证原子性等特性,可能需要结合硬件支持(如现代 CPU 的原子指令)或合适的同步库来完善它的功能。

  • while(compare_and_swapWait(lock, 0, 1) == 1);
    • 这里调用了compare_and_swapWait函数,它的功能在上一个代码片段分析中已经说明,大致是比较lock所指向内存位置的值(当前值)和期望的值(这里期望是0),如果相等就将其更新为1,并返回旧值。这个内层循环的目的是不断地尝试进行这样的比较并交换操作,直到返回值不等于1为止。
    • 当返回值为1时,意味着当前lock所指向内存位置的值已经是1了,很可能表示已经有其他线程或进程获取了对临界区的访问权限(使用1来表示占用,只是一种简单的示意,具体含义取决于整体代码设定),所以当前的这个线程或进程需要继续等待,不断循环尝试,直到compare_and_swapWait函数返回的值不为1,也就是lock的值变为0,意味着可以获取访问临界区的权限了。这个过程就像是在一个门口不断查看是否有空位(权限)可以进入一样。

semaphore

semaphore

semaphore

semaphore

code

  1. mutex信号量

    • 含义mutex(互斥锁)用于实现对共享资源(在这里是缓冲区Buffer)的互斥访问。它确保在同一时刻只有一个生产者或消费者能够访问缓冲区,防止数据竞争和不一致性。
    • 代码中的体现
      • 在生产者函数producer()中,wait(mutex)signal(mutex)用于获取和释放互斥锁。这确保了当生产者往缓冲区Buffer中放入数据时,不会有其他生产者或消费者同时访问缓冲区。
      • 在消费者函数consumer()中,同样有wait(mutex)signal(mutex)操作,用于确保消费者从缓冲区Buffer中取出数据时的互斥访问。
  1. empty信号量

    • 含义empty信号量用于表示缓冲区中剩余的空闲位置数量。它初始化为缓冲区的大小n,每当生产者往缓冲区中放入一个数据项,empty的值就会减 1;每当消费者从缓冲区中取出一个数据项,empty的值就会加 1。
    • 代码中的体现
      • 在生产者函数producer()中,wait(empty)操作在生产者往缓冲区放入数据前进行。如果empty的值为 0(即缓冲区已满),生产者将被阻塞,直到有消费者从缓冲区中取出数据,使empty的值大于 0。
      • 在消费者函数consumer()中,消费者从缓冲区取出数据后,会执行signal(empty)操作,这会增加empty的值,表示缓冲区中多了一个空闲位置。

常见场景

读者写者

semaphore mutex_write;

semaphore read_count=0;

semaphore mutex_read;

void read(){

        wait(mutex_read);

        signal(read_count);

        if(read_count==1){

                wait(mutex_write);        

        }

        read

        if(read_count==1){

                signal(mutex_write);

        }

        wait(read_count);

}

void write(){

        wait(mutex_write);

        write;

        signal(mutex_write);

}

void read(){

        while(true){

                wait(read_count);

                count++;

                if(count==1){

                        wait(mutex_write);

                }

                signal(read_count);

                read;

                wait(read_count);

                count--;

                if(count==0){

                       signal(mutex_write);

                }

                signal(read_count);

        }

}

生产者消费者

semaphore

semaphore

semaphore

semaphore

semahore

semahore

semahore empty1=m

semahore empty2=n

semahore full1=0

semahore full2=0

semahore mutex1=1

semahore mutex2=1

void put(){

        wait(empty1)

        wait(mutex1)

        signal(full1)

        signa(muext1)l

}

void move(){

        wait(full1)

        wait(empty2)

        wait(mutex1)

        wait(mutex2)

        signal(empty1)

        signal(full2)

        signal(mutex1)

        signal(mutex2)

}

void get(){

        wait(full2)

        wait(mutex2)

        signal(empty2)

        signal(mutex2)

}

内存管理

  • 内存(Memory)
    • 内存是计算机存储系统中的主要存储部件,用于存储正在运行的程序和数据。它的容量相对较大,通常以 GB(吉字节)为单位,在现代计算机系统中可以达到数 GB 甚至数 TB。例如,一台普通的个人计算机可能配备 8GB 或 16GB 的内存,服务器则可能有更高的内存容量。内存主要用于存储操作系统、应用程序的代码和数据,以及程序运行过程中产生的中间结果等大量信息。
    • 内存的存储速度相对较慢,它与处理器之间的数据传输速度比寄存器慢很多。这是因为内存的物理结构和工作原理决定了其数据访问需要经过地址译码、数据读取 / 写入等一系列相对复杂的过程。
  • 寄存器(Register)
    • 寄存器是位于处理器内部的高速存储单元,用于临时存储处理器正在处理的数据和指令。寄存器的数量相对较少,容量也较小,通常以字节或字(如 32 位或 64 位)为单位。例如,一个典型的处理器可能有十几个通用寄存器,每个寄存器可以存储 32 位或 64 位的数据。
    • 寄存器的主要作用是为处理器提供快速的数据存储和访问,以支持高效的指令执行。由于寄存器位于处理器内部,与处理器的运算单元和控制单元紧密相连,所以数据在寄存器之间以及寄存器与处理器内部其他部件之间的传输速度非常快,通常在一个时钟周期内就可以完成数据的读取或写入操作。

MMU

地址空间

  1. 程序地址空间和 CPU 结构的联系

    • 协同工作关系
      • 程序地址空间和 CPU 结构紧密配合,使得程序能够正常运行。当程序执行时,CPU 的指令寄存器从程序地址空间的代码段读取指令,然后根据指令的类型和操作数,从数据段、堆或栈中读取数据到数据寄存器进行运算,或者将数据寄存器中的结果写回到数据段、堆或栈中的相应位置。例如,在一个简单的加法运算中,指令寄存器从代码段获取加法指令,数据寄存器从数据段获取两个操作数,进行加法运算后,再将结果写回数据段中的目标变量位置。
    • 地址映射和数据传输机制
      • CPU 通过地址总线与程序地址空间进行交互。CPU 发出的地址信号用于定位程序地址空间中的代码段、数据段、堆或栈中的具体位置。同时,数据总线用于在 CPU 的数据寄存器和程序地址空间之间传输数据。这种机制使得 CPU 能够访问程序地址空间中的各种数据和指令,实现程序的功能。例如,当一个函数调用另一个函数时,CPU 通过地址总线定位栈空间,将调用函数的返回地址、局部变量等信息压入栈中;当函数返回时,再通过数据总线从栈中读取返回地址,恢复程序的执行流程。

  1. 共享内存区
    • 定义与用途:共享内存是多个进程可以共享的一块内存区域。它允许不同进程将同一段物理内存映射到它们各自的虚拟地址空间中。例如,在一个数据库管理系统中,多个进程(如查询进程、更新进程等)可能需要同时访问数据库缓存。通过共享内存区,这些进程可以高效地共享和交换数据,避免了数据的多次复制,提高了系统的性能。
    • 实现方式:在操作系统的支持下,通过系统调用(如shmgetshmat等函数,在 Linux 系统中)来创建和管理共享内存段。进程可以将共享内存段附加到自己的地址空间中,像访问普通内存一样访问共享内存中的数据。不过,为了确保数据的一致性,通常需要配合使用信号量等同步机制,防止多个进程同时对共享内存进行冲突的读写操作。
  2. 映射文件区
    • 定义与用途:这是将磁盘文件的一部分或全部映射到进程地址空间的区域。当一个进程需要处理文件内容时,使用映射文件区可以简化文件的读写操作。例如,在一个大型文件处理程序中,将文件映射到进程地址空间后,程序可以像访问内存数组一样访问文件内容,而不需要频繁地使用文件 I/O 系统调用(如readwrite)。这种方式在处理大型文件(如数据库文件、多媒体文件)时,能够提高文件访问的效率。
    • 实现方式:通过系统调用(如mmap函数,在 Linux 系统中)将文件的内容映射到进程的虚拟地址空间。操作系统会根据文件的大小和内存管理策略,将文件的相应部分加载到物理内存中,并建立虚拟地址和文件内容之间的映射关系。当进程访问映射文件区中的地址时,操作系统会自动根据映射关系从文件中读取数据或者将数据写入文件。
  3. 动态链接库(DLL)空间(在相关系统中)
    • 定义与用途:动态链接库是包含可被多个程序共享的代码和数据的库。当一个进程使用动态链接库时,库中的代码和数据会被加载到进程地址空间的特定区域。这使得多个应用程序可以共享同一段库代码,减少了内存的占用。例如,在许多图形用户界面(GUI)应用程序中,都会使用相同的图形库(如 Windows 系统中的 GDI 库),这些应用程序将图形库的 DLL 加载到自己的地址空间中,通过共享库代码来实现图形绘制等功能。
    • 实现方式:在程序运行时,操作系统根据程序对动态链接库的引用,将相应的 DLL 文件加载到进程地址空间。这个加载过程可以是在程序启动时自动进行,也可以是在程序需要使用 DLL 中的特定功能时才加载。加载后的 DLL 代码和数据在进程地址空间中有自己的存储区域,并且可以通过一定的机制(如函数指针等)被进程调用。