并发编程基础——操作系统

1,232 阅读44分钟

计算机组成原理

冯·诺依曼计算机的特点

  1. 计算机由运算器、存储器、控制器、输入设备和输出设备五大部件组成
  2. 程序存储原理:计算机的指令和数据以同等地位存储在内存中,这使得计算机能够自动地从存储器取出指令并执行,实现了自动计算的能力。
  3. 指令(程序)和数据以二进制不加区别地存储在存储器中
  4. 指令(程序)和数据均用二进制码表示。
  5. 指令和数据以同等地位存储于储存器内,并可按照地址寻访。
  6. 机器以运算器为中心,输入输出设备与存储器间的数据传输通过运算器完成。

输入设备

输入设备,是指将外部信息以计算机能读懂的方式输入进来,如键盘,鼠标等

输出设备

输出设备,就是将计算机处理的信息以人所能接受的方式输出出来,比如显示屏,打印机。

存储器

存储器的主要功能是存储程序和各种数据,并且能够在计算机运行过程高速、自动地完成程序或者数据的存储,存储器是有记忆的设备,而且采用俩种稳定状态的物理器件来记录存储信息,所以计算机中的程序和数据都要转换为二进制代码才可以存储和操作。

存储器的基本功能概括如下:

  1. 数据存储:存储器用来保存程序代码、用户数据、中间计算结果以及最终输出等所有形式的信息。
  2. 高速访问:为了匹配CPU的处理速度,主存储器(如RAM)设计为能够快速读取和写入数据。
  3. 分类多样:存储器可以根据存储介质、存取方式、在计算机中的作用等不同标准进行分类。主要分为主存储器(内存)和辅助存储器(外存)。主存储器如RAM(随机存取存储器)提供快速访问但易失性存储,而辅助存储器如硬盘、SSD提供持久化存储但访问速度较慢。
  4. 物理基础:存储器通常基于半导体材料制造,如DRAM和SRAM,但也可能采用磁性材料(如硬盘)或光学材料。存储信息的基本单位是比特,通过具有两种稳定状态的物理器件(如双稳态半导体电路、CMOS晶体管或磁性存储元)来表示“0”和“1”。
  5. 存取机制:存储器按照特定的地址结构组织,CPU通过地址总线指定位置来存取数据。此外,DMA(直接存储器访问)等机制允许存储器与I/O设备直接交换数据,减少CPU的干预。
  6. 重要性:存储器对计算机性能有着直接的影响,其容量、速度和成本是计算机设计中的重要因素。随着技术进步,存储器的容量不断增大,访问速度也在不断提升,同时力求降低成本和能耗。
主存储器

主存储器被称为内部存储器、内存、主存,是用来存放将要被执行的程序和数据。

在计算机内部,程序和数据都是以二进制代码的形式存储的,它们均以字节为单位(8位)存储在存储器中,一个字节占用一个存储单元,并且每个存储单元都有唯一的地址号。

CPU可以直接使用指令对内部存储器按照地址进行读写俩种操作,

  • 读:将内存中某个存储单元的内容读出,送入CPU的某个寄存器中;
  • 写:在控制器的控制下,将CPU中某寄存器内容传到某个存储单元中。

在计算机内存中,数据和地址均以二进制形式表示,二者虽同为二进制数,但所代表的意义截然不同。地址指的是存储单元的位置标识符,每一个地址唯一对应内存中的一个存储位置。数据则是存储单元里实际存放的内容,它可以是程序的机器指令(操作码)、需要处理的数据本身,或者是指向其他数据位置的指针(即另一个地址)。

内存地址的位数(地址码长度)由内存容量决定,确切地说,是由内存中存储单元的数量来确定。例如,若一个系统能访问的存储单元总数为 个,那么就需要 n 位二进制数来唯一标识这些存储单元,这里的 n 就是地址码的长度。因此,随着内存容量的增长,地址码的长度也会相应增加,以满足标识更多存储位置的需求。

内存的访问速度对计算机的整体运算性能有着直接的影响。由于CPU的工作频率远高于内存,这种速度不匹配可能导致CPU等待数据准备的时间过长,从而降低了效率。为了解决这一问题,在CPU与主内存之间设置了高速缓存(Cache)。Cache是一种高速、小容量的存储器,用于暂存CPU即将使用或近期使用过的指令及数据,其访问速度能与CPU的处理速度相匹配,通常采用静态RAM(SRAM)技术实现,因为SRAM比常用的动态RAM(DRAM)更快。

内存根据工作特性可分为两大类:

  • 随机存取存储器(RAM):允许CPU随机读取和写入信息,主要用于存储正在执行的程序、计算过程中产生的数据。其特点是断电后存储的信息会丢失,因此被称为易失性存储器。

  • 只读存储器(ROM):与RAM不同,ROM的内容在正常情况下是固定不变的,CPU可以从中读取数据但不能直接修改。ROM用于存储系统启动所需的固件、基本输入输出系统(BIOS)等关键程序,这些信息即使在系统断电后也能保留,展现出非易失性特点。随着技术发展,出现了可编程和可擦写的ROM变体,如EPROM、EEPROM和Flash ROM,它们提供了不同程度的可写能力。

运算器

计算机的核心在于其数据处理能力,这便需要一个专门的处理单元来执行各类数学运算和逻辑判断,即算术逻辑单元(Arithmetic Logic Unit, ALU)。ALU的首要职责是在控制信号的指引下,高效完成包括加减乘除在内的算术运算,以及与、或、非、异或等逻辑运算,还包括移位和补码操作等。

在此基础上,运算器作为数据处理的中枢,其核心组件正是ALU。运算器处理的数据规模及其表示形式对其效能至关重要。普遍来看,多数通用计算机的运算器设计为一次性处理16位、32位或64位的数据宽度,这直接影响了其处理能力和效率。运算器能够对数据的所有位实施同时操作的,被称作并行运算器,而仅能逐一处理数据位的,则属于串行运算器。

运算器与计算机其他组件间的协同作用体现在:运算的具体内容与类型由控制器指令指定,所需数据从内存中提取,经过运算器处理后的结果再反馈回内存存储,或是暂存在运算器的内部寄存器中。这一系列数据的搬运操作同样遵循控制器的调度指令,确保了整个计算流程的有序进行。

控制器

控制器又称为控制单元(Control Unit),是计算机的神经中枢和指挥中心,只有在控制器的控制下,整个计算机才能够有条不紊地工作、自动执行程序。

控制器的工作流程为:从内存中取指令、翻译指令、分析指令,然后根据指令的内存向有关部件发送控制命令,控制相关部件执行指令所包含的操作。

控制器和运算器共同组成中央处理器(Central Processing Unit),CPU是一块超大规模集成电路,是计算机运算核心和控制核心,CPU的主要功能是解释计算机指令以及处理数据。

现代计算机硬件原理图

运算器和控制器封装到一起,加上寄存器组和cpu内部总线构成中央处理器(CPU)。cpu的根本任务,就是执行指令,对计算机来说,都是0,1组成的序列,cpu从逻辑上可以划分为3个模块:控制单元、运算单元和存储单元。这三个部分由cpu总线连接起来。

运算器与控制器被集成在一起,并且配以一套寄存器组以及内部通信线路,共同构成了中央处理器(CPU)的核心。CPU的基本职责在于解析并执行指令,这些指令实质上是由0和1构成的二进制序列。从逻辑架构上看,CPU可以细分为三个关键模块:控制单元、算术逻辑单元和寄存器组。这三个模块通过高效的CPU内部总线相互连接,协同工作,确保了指令的顺利执行和数据的快速传递。

CPU原理图

CPU运作的核心机制如下:在时钟信号的驱动下,控制单元促使指令计数器指向当前指令的内存地址,并通过地址总线将该地址发送出去。随后,CPU依据此地址从内存中获取指令,并将其载入指令寄存器进行解码。解码过程中,如果指令执行需要额外数据,控制单元会将数据所在内存地址同样经由地址总线传送,CPU随即读取这些数据到内部的寄存器中暂存。准备工作就绪后,运算单元接收到命令,开始对寄存器中暂存的数据进行必要的运算处理。这一系列步骤循环往复,CPU持续不断地读取指令、获取数据、执行运算,推动计算任务的连续进行。

CPU缓存架构

多处理机(multiprocessor)

  • 定义:多处理机(multiprocessor)系统指的是在一台计算机中安装了两个或更多的独立CPU。每个CPU都有自己独立的缓存、控制单元、运算单元和系统总线,可以独立执行任务。
  • 工作方式:这些CPU可以并发工作,各自处理不同的任务,或者通过操作系统协调,分配同一任务的不同部分给不同的CPU来并行处理,实现任务的加速完成。
  • 优点:显著提高了系统的并行处理能力和总体计算速度,适合于高负载服务器、大型数据库系统和高性能计算领域。
  • 缺点:成本较高,因为需要多个CPU插槽和对应的硬件支持;且多CPU之间的通信和数据同步相对复杂,可能影响效率。

CPU 长这个样子:

CPU 通过一个插槽安装在主板上,这个插槽也叫做 CPU Socket,它长这个样子:

多核(multi-core)

多核CPU设计中,整合于单一芯片上的是一款集成了多个核心的处理器。这意味着,尽管计算机系统内物理上只安装了一个处理器单元,该处理器却内部蕴含了多个独立的核心。这些核心能够并行运作,各自处理任务,共同提升了系统的多任务处理能力和整体性能。简而言之,一个物理处理器承载了多个逻辑处理核心,实现了资源的高效整合与利用。

多CPU&多核CPU比较

不同项目的处理速度不同
  • 多核(multi-core) :多核处理器执行单个程序的速度更快。
  • 多处理机(multiprocessor) :多处理器执行多个程序的速度更快。
CPU数量不同
  • 多核(multi-core):一个CPU或处理器有两个或多个独立的处理单元,称为核心,能够读取和执行程序指令。
  • 多处理机(multiprocessor):一个有两个或更多CPU的系统,可以同时处理程序。
资源利用率不同

对于多处理器系统而言,它们在执行命令的时候多个处理器之间的通信手段是电脑主板上的总线。而对于多核系统而言,多个核心处理器之间通信时通过CPU内部总线进行信息的交互的。对于执行效率而言,多核处理器要优于多个处理器。

价格不同
  • 多核(multi-core):多核处理器非常便宜(单个CPU,不需要多CPU支持系统)。
  • 多处理机(multiprocessor):与多核心相比,多处理器很昂贵(多个独立的CPU,需要支持多个处理器的系统)。
线程控制不同

计算机在启动之后,一个进程最少包含一个主线程,如果这个主线程结束了,那么这个进程也就终止执行了,主线程是以函数的形式提供给操作系统的。对于并行计算是在多处理器的情况下,操作系统把多个线程分配给响应的处理器,然后各自执行任务。

CPU寄存器

每个CPU都包含一系列的寄存器,它们是CPU内内存的基础。CPU在寄存器上执行操作的速度远大于在主存上执行的速度。这是因为CPU访问寄存器的速度远大于主存。

寄存器:

  • 作用:暂时存放参与运算的数据和运算结果
  • 位置:是cpu内部元件,其读写速度非常快

种类:

  • 通用寄存器:存储需要进行运算的数据(需要加减等运算的两个数据)
  • 程序计数器:存储cpu要执行下一条指令所在的内存地址
  • 指令寄存器:存储程序计数器指向的指令,指令被执行完之前都放在这里

CPU寄存器是中央处理器(CPU)内部的一组特殊存储单元,它们设计为高速存取数据,以支持CPU在执行指令过程中的即时需求。寄存器由于直接集成在CPU内部,因此相比内存等其他存储设备,其访问速度极快,这对于提升计算机整体性能至关重要。以下是CPU中常见的一些寄存器类型及其功能概述:

  1. 程序计数器(Program Counter, PC) :存储下一条待执行指令的内存地址。每当一条指令被执行完毕,程序计数器会自动递增,指向下一个指令的位置,或者根据跳转指令更新为新的地址。

  2. 指令寄存器(Instruction Register, IR) :用于存放当前正在执行的指令。CPU会从内存中取出指令放入指令寄存器,并对其进行解码以决定下一步操作。

  3. 算术逻辑单元(ALU)使用的寄存器

    • 累加器(Accumulator, AC) :常用于存储算术逻辑运算的操作数或结果。
    • 通用寄存器(General Purpose Registers, GPRs) :如R0至Rn,可用于存储数据或地址,支持多种运算和数据传递功能。
  4. 状态/标志寄存器(Status/Flags Register, PSW) :包含一系列标志位,用于反映CPU执行指令后得到的各种状态信息,如零标志、进位标志、溢出标志等,这些信息常用于条件分支指令的决策。

  5. 存储器地址寄存器(Memory Address Register, MAR) :当CPU需要从内存读取或写入数据时,用来存放目标内存地址。

  6. 存储器数据寄存器(Memory Data Register, MDR) :作为CPU与内存之间数据传输的中转站,存放从内存读取的数据或准备写入内存的数据。

  7. 索引寄存器(Index Registers)基址寄存器(Base Registers)变址寄存器(Pointer Registers) :这些寄存器常用于存储数据地址,特别是在进行间接寻址或数组、向量运算时,帮助快速定位内存位置。

  8. 堆栈指针寄存器(Stack Pointer, SP) :跟踪堆栈的顶部位置,即最近一次压栈操作的地址。

  9. 段寄存器(Segment Registers) :在某些架构中,用于存储内存段的基地址,与偏移地址结合形成物理地址。

CPU缓存(Cache Memory

CPU缓存(Cache Memory)是位于CPU与主内存之间的临时存储器,它的主要作用是减少CPU访问内存所需的时间,从而提高计算机系统的整体性能。CPU缓存利用了程序的局部性原理,即在一段时间内,CPU较频繁地访问某些数据或指令,这些数据和指令往往聚集在一起。

CPU缓存按照接近CPU的距离从近到远可以分为几级,常见的有三级:

  1. 一级缓存(L1 Cache):这是距离CPU核心最近的一层缓存,速度最快,容量相对较小。它被进一步划分为指令缓存(I-Cache)和数据缓存(D-Cache),分别用于存储指令和数据。L1缓存的目的是为了尽可能快地向CPU提供最常用的数据和指令。
  2. 二级缓存(L2 Cache):相比L1,L2缓存容量较大,但访问速度稍慢。早期的CPU中,L2缓存可能还是位于主板上,但现在大多数CPU都将L2缓存集成到了CPU内部,以提高效率。L2缓存用于存储L1缓存未命中的数据或指令。
  3. 三级缓存(L3 Cache):在多核CPU中更为常见,L3缓存的容量更大,速度比L1和L2更慢,但仍然远快于主内存。L3缓存为所有CPU核心共享,用于减少多个核心间的数据传输延迟,并作为L2缓存和主内存之间的缓冲区。

CPU在执行任务时,会首先查看L1缓存,如果找不到所需数据,则继续查找L2,再到L3,最后如果所有缓存中都未找到,才会访问主内存。这个过程称为“缓存命中”(Cache Hit)与“缓存未命中”(Cache Miss)。增加缓存的容量和优化缓存策略可以提高命中率,从而提升系统性能。

CPU缓存和CPU寄存器之前的关系

CPU缓存和CPU寄存器都是为了提高CPU处理数据的效率而设计的,但它们在功能、位置、容量和速度上有所不同,共同构成了CPU数据访问的层级结构。

CPU寄存器:

  • 属于CPU内部结构的一部分,是CPU能够直接、快速访问的存储区域。
  • 寄存器的数量有限,每个寄存器都能在单个时钟周期内被访问,因此速度极快。
  • 它们主要用于存储CPU在执行指令过程中最常访问或当前正在处理的数据及指令,比如算术逻辑运算的操作数、指令指针等。
  • 由于靠近CPU核心且访问速度快,寄存器的容量很小,通常只有几千字节。

CPU缓存:

  • 虽然也集成在CPU内部或封装上,但相对于寄存器来说是独立的组件。
  • 缓存的作用是暂存从主内存中预取的数据和指令,以减少CPU直接访问较慢的主内存的次数。
  • CPU缓存分为多级(如L1、L2、L3缓存),每级缓存的容量依次增大,速度依次减慢,但都比访问主内存要快。
  • 当CPU需要数据时,首先会在L1缓存中查找,未找到则继续到L2,以此类推,直至主内存。这一过程是自动由硬件管理的。

两者之间的关系:

  • 层次结构:寄存器位于CPU内部,是最接近CPU计算核心的存储层级,其后是多级缓存,再之后才是主内存(RAM)。这种层级结构旨在平衡速度与容量,确保CPU能以最高效的方式获取数据。
  • 数据流动:数据通常先从主内存加载到L3缓存,再到L2、L1,最终可能被转移到寄存器中供CPU直接操作。处理完的数据可能会沿这条路径反向写回。
  • 协同工作:寄存器和缓存共同工作,确保CPU能够快速访问所需数据,减少等待时间,提高整体处理速度。寄存器存储CPU即时处理的信息,而缓存则提供一个更快的内存层次,以补充寄存器的有限容量。

CPU寄存器和缓存都是为了加速数据处理而设计,寄存器更靠近CPU核心,容量小但速度极快,用于存储最活跃的数据;而缓存则提供了不同级别的存储空间,容量更大,速度介于寄存器和主内存之间,用于缓解CPU与主内存间速度不匹配的问题。

多cpu和多核cpu架构图

CPU 如何读写主存

CPU 与内存之间的数据交互是通过总线(bus)完成的,而数据在总线上的传送是通过一系列的步骤完成的,这些步骤称为总线事务(bus transaction)。

其中数据从内存传送到 CPU 称之为读事务(read transaction),数据从 CPU 传送到内存称之为写事务(write transaction)。

总线上传输的信号包括:地址信号,数据信号,控制信号。其中控制总线上传输的控制信号可以同步事务,并能够标识出当前正在被执行的事务信息:

在 MESI 缓存一致性协议中当 CPU core0 修改字段 a 的值时,其他 CPU 核心会在总线上嗅探字段 a 的物理内存地址,如果嗅探到总线上出现字段 a 的物理内存地址,说明有人在修改字段 a,这样其他 CPU 核心就会失效字段 a 所在的 cache line 。

如上图所示,其中系统总线是连接 CPU 与 IO bridge 的,存储总线是来连接 IO bridge 和主存的。

IO bridge 负责将系统总线上的电子信号转换成存储总线上的电子信号。IO bridge 也会将系统总线和存储总线连接到IO总线(磁盘等IO设备)上。这里我们看到 IO bridge 其实起的作用就是转换不同总线上的电子信号。

假设 CPU 现在需要将物理内存地址为 A 的内容加载到寄存器中进行运算。

需要注意的是 CPU 只会访问虚拟内存,在操作总线之前,需要把虚拟内存地址转换为物理内存地址,总线上传输的都是物理内存地址。

首先 CPU 芯片中的总线接口会在总线上发起读事务(read transaction)。 该读事务分为以下步骤进行:

  1. CPU 将物理内存地址 A 放到系统总线上。随后 IO bridge 将信号传递到存储总线上。
  2. 主存感受到存储总线上的地址信号并通过存储控制器将存储总线上的物理内存地址 A 读取出来。
  3. 存储控制器通过物理内存地址 A 定位到具体的存储器模块,从 DRAM 芯片中取出物理内存地址 A 对应的数据 X。
  4. 存储控制器将读取到的数据 X 放到存储总线上,随后 IO bridge 将存储总线上的数据信号转换为系统总线上的数据信号,然后继续沿着系统总线传递。
  5. CPU 芯片感受到系统总线上的数据信号,将数据从系统总线上读取出来并拷贝到寄存器中。

以上就是 CPU 读取内存数据到寄存器中的完整过程。

缓存一致性问题

在多处理器架构中,尽管每个处理器配备的高速缓存极大地提升了数据访问速度,但这种设计也引发了缓存一致性挑战。具体而言,当不同的处理器并发处理涉及相同主内存地址的任务时,可能会出现各自缓存中数据不一致的状况。这就引出了一个关键问题:在数据最终同步回主内存时,应如何确定以哪个处理器缓存中的数据为基准进行更新?

为有效应对这一挑战,业界开发了一系列缓存一致性协议,旨在确保所有处理器上的缓存数据保持同步和一致。这些协议包括但不限于MSI(Modified, Shared, Invalid)、MESI(Modified, Exclusive, Shared, Invalid)、MOSI、Synapse、Firefly以及Dragon Protocol等。这些协议的核心目标在于规范处理器在缓存读写操作时的行为,通过定义缓存行的状态转换规则和消息传递机制来维护一致性。

以MESI协议为例,它通过以下四个状态来标记缓存行:

  • Modified (M): 数据被修改,与主存不一致,且仅存储在一个处理器的缓存中。
  • Exclusive (E): 数据未修改,仅存储在一个处理器的缓存中,与其他处理器无关。
  • Shared (S): 数据未修改,可能存在于多个处理器的缓存中,内容与主存一致。
  • Invalid (I): 缓存行无效,数据不可用,需要从主内存重新加载。

在数据更新至主内存的过程中,不是简单地选择某一方的缓存数据作为标准,而是通过协议规定的步骤确保一致性。例如,当一个处理器试图修改一个处于S状态(共享)的缓存行时,它会先将其标记为M状态,并通过总线监听或其他通信机制通知其他处理器,这些处理器会相应地将该缓存行标记为I状态(无效)。这样,后续对该数据的访问会促使相关处理器从拥有最新数据的处理器(M状态)那里获取,或直接从主内存读取最新的数据,从而确保了全局数据的一致性。

CPU调度

CPU调度,又称为处理器调度或任务调度,是操作系统内核中的一个核心功能,负责决定哪一个进程或线程在什么时候获得CPU的使用权来执行。它的主要目的是合理、高效地分配CPU资源,使得系统中的多个任务能够并发执行,提升系统的整体性能和响应速度,同时也保证各个任务之间的公平性。

  1. CPU上的队列:在Linux调度器的上下文中,每个CPU都有自己的调度队列来管理准备在该CPU上运行的进程。CFS是Linux内核中用于一般任务调度的主要算法,它维护了一个基于红黑树的调度队列来确保所有任务能够公平地共享CPU时间。
  2. 红黑树:CFS使用红黑树作为其核心数据结构来组织调度实体(sched_entity)。红黑树是一种自平衡二叉查找树,它提供了对树中元素进行快速插入、删除和查找操作的能力,同时保持树的平衡,从而确保操作的时间复杂度为O(log n),其中n是树中节点的数量。在CFS中,树中的每个节点代表一个可调度的任务(或称为进程)。
  3. sched_entity:这是CFS内部用来表示可调度实体的数据结构。每个sched_entity都关联到一个task_struct,即实际的进程描述符。sched_entity中记录了与调度相关的各种信息,如进程的虚拟运行时间(vruntime)、权重(用于实现优先级调度)等,这些信息帮助CFS决定下一个应该运行的进程。
  4. task_struct:这是Linux内核中表示进程的核心结构体,包含了进程的所有相关信息,比如PID、状态(就绪、运行、阻塞等)、内存空间信息、打开的文件描述符等。正如您所述,task_struct中也包含了一个指针,指向所属的调度类,这允许不同的任务根据其特性和需求被归类并采用不同的调度策略处理。

进程和线程

一旦“程序”被执行起来,它就从磁盘上的二进制文件,变成了计算机内存中的数据、寄存器里的值、堆栈中的指令、被打开的文件,以及各种设备的状态信息的一个集合。像这样一个程序运起来后的计算机执行环境的总和,就是进程。

进程是程序的一次执行,一个程序有至少一个进程,是资源分配的最小单位,资源分配包括cpu、内存、磁盘IO等。线程是程序执行的最小单位,CPU调度的基本单元,一个进程有至少一个线程。

  1. 进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元
  2. 同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器、堆栈、上下文),一个进程至少包括一个线程。
  3. 进程的创建调用fork或者vfork,而线程的创建调用pthread_create,进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束
  4. 线程是轻量级的进程,它的创建和销毁所需要的时间比进程小很多,所有操作系统中的执行功能都是创建线程去完成的
  5. 线程中执行时一般都要进行同步和互斥,因为他们共享同一进程的所有资源
  6. 线程有自己的私有属性线程控制块TCB,线程id,寄存器、上下文,而进程也有自己的私有属性进程控制块PCB,这些私有属性是不被共享的,用来标示一个进程或一个线程的标志

进程和线程的区别

比较线程进程
调度CPU调度和分配的基本单位CPU资源分配的基本单位
并发性进程与进程可以并发执行,进程下线程与线程也可以并发执行进程与进程之间并发执行,若某进程服务被锁,便没有该进程的服务可提供服务了
系统开销开销小(线程切换只需要保存少量的寄存器和内容,不涉及存储器管理方面,同一进程中,若线程有相同的地址空间,同步和通信容易,有的操作系统,同步和通信不需要系统内核干预)开销大(IBCPU环境的保存和设置,新CPU环境的保存和设置)
拥有资源线程不拥有系统资源,但是可以访问其隶属的进程资源.(也就是同一进程的代码段、数据段、以及系统资源可供同一进程的其他线程共享)进程拥有系统资源的独立单位

并发和并行

目的都是最大化CPU的使用率

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在,并发能够在单处理器系统中存在是因为并发是并行的假象,并行要求程序能够同时执行多个操作,而并发只是要求程序假装同时执行多个操作(每个小时间片执行一个操作,多个操作快速切换执行)

线程上下文切换

线程上下文的切换巧妙的利用了时间片轮转的方式,CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一个任务;线程状态的保存及其再加载,就是线程的上下文切换。时间片轮询保证CPU的利用率。

  • 上下文:是指在某一时间CPU寄存器和程序计数器的内容;
  • 寄存器:是CPU内部数量少但是速度很快的内存。寄存器通常对常用值的快速访问来提高计算机程序运行的速度;
  • 程序计数器:是一个专门的寄存器,用于存放下一条指令所在单元的地址的地方。当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称为“取指令”

上下文切换的活动:

  1. 挂起一个线程,将这个进程在CPU中的状态存储于内存中的某处;
  2. 在内存中检索下一个进程的上下文并将其CPU的寄存器恢复;
  3. 跳转到程序计数器所指定的位置;

Java线程的实现

从JDK 1.3起,“主流”平台上的“主流”商用Java虚拟机的线程模型普遍都被替换为基于操作系统原生线程模型来实现,即采用1:1的线程模型。

以HotSpot为例,它的每一个Java线程都是直接映射到一个操作系统原生线程来实现的,而且中间没有额外的间接结构,所以HotSpot自己是不会去干涉线程调度的(可以设置线程优先级给操作系统提供调度建议),全权交给底下的操作系统去处理,所以何时冻结或唤醒线程、该给线程分配多少处理器执行时间、该把线程安排给哪个处理器核心去执行等,都是由操作系统完成的,也都是由操作系统全权决定的。

操作系统支持怎样的线程模型,在很大程度上会影响上面的Java虚拟机的线程是怎样映射的,这一点在不同的平台上很难达成一致,因此《Java虚拟机规范》中才不去限定Java线程需要使用哪种线程模型来实现。线程模型只对线程的并发规模和操作成本产生影响,对Java程序的编码和运行过程来说,这些差异都是完全透明的。

Java线程

  1. 定义:在Java中,线程是执行任务的最小单位,它是轻量级的子进程,允许程序同时执行多个任务。Java线程是由Java语言层面提供的一个抽象概念,程序员可以通过继承Thread类或实现Runnable接口来创建线程。
  2. 管理:Java线程的创建、调度、同步、通信等操作都是在Java虚拟机(JVM)中管理的。JVM会将Java线程映射到宿主机操作系统的线程上,以便实际执行。
  3. 生命周期:Java线程也有自己的生命周期,包括新建(New)、可运行(Runnable)、运行(Running)、阻塞(Blocked)、等待(Waiting)、超时等待(Timed Waiting)和终止(Terminated)等状态。
  4. 上下文切换:虽然Java线程的上下文切换是由操作系统实现的,但Java提供了诸如synchronized关键字、volatile关键字、java.util.concurrent包下的工具类等机制来帮助开发者管理线程间的同步和通信,以减少上下文切换的开销和避免竞态条件。

操作系统线程

  1. 定义:操作系统线程是操作系统内核直接管理和调度的基本执行单元。它是进程内的一个执行流,共享同一地址空间和其他资源,但拥有独立的栈、程序计数器等。
  2. 管理:操作系统负责创建、调度、销毁线程,以及线程间的上下文切换。它提供线程间同步和通信的原语,如信号量、互斥锁、条件变量等。
  3. 硬件支持:现代操作系统通常利用硬件级别的特性(如多核处理器)来实现线程的并发执行,提高系统效率。
  4. 粒度:相比于进程,线程是更细的执行单元,因此在资源消耗和上下文切换开销方面通常更为高效。

联系

Java线程最终依赖于底层操作系统提供的线程机制来实现其功能。当在Java中创建一个新线程时,JVM会向操作系统请求创建一个相应的操作系统线程。Java线程的状态转换、调度策略等高级特性,都需要底层操作系统的支持和配合才能实现。

Java线程与操作系统线程之间存在紧密的关联与互动,这种关系体现在以下几个方面:

  1. 映射关系:Java线程并不是直接在硬件上执行的实体,而是建立在操作系统线程基础之上的抽象。当在Java程序中通过Thread类或Runnable接口创建一个线程并调用start()方法时,Java虚拟机(JVM)会请求操作系统创建一个相应的操作系统线程。这意味着,每一个活动的Java线程背后,通常都对应着一个操作系统线程。
  2. 生命周期映射:Java线程的状态(如New, Runnable, Blocked, Waiting, Timed Waiting, Terminated等)与操作系统线程的状态有着直接的关联。例如,Java线程的Runnable状态对应于操作系统线程的Ready/Running状态,而Blocked、Waiting或Timed Waiting状态则对应于操作系统的等待状态。
  3. 调度与上下文切换:尽管Java线程的调度策略(如时间片轮转、优先级调度)是由Java虚拟机实现的,但最终的线程调度和上下文切换操作是由操作系统完成的。JVM会根据其自身的线程调度策略,向操作系统发出调度请求,然后操作系统负责实际的执行切换。
  4. 资源管理与同步:尽管Java提供了高级的线程同步机制(如synchronized、Lock、Condition、CountDownLatch等),这些机制最终还是依赖于操作系统提供的底层同步原语(如互斥锁、信号量等)来确保线程安全和同步。
  5. 性能影响:Java线程的创建、销毁和上下文切换的开销,很大程度上取决于底层操作系统的线程实现。例如,在某些操作系统中,创建一个新线程可能比较昂贵,这会影响到Java应用程序的性能。
  6. 模型差异:不同的操作系统有不同的线程模型(如一对一模型、多对多模型),这会影响Java线程与操作系统线程之间的映射关系。例如,在一对一模型中,每个Java线程直接对应一个操作系统线程;而在多对多模型中,多个Java线程可能会映射到较少数量的操作系统线程上,通过用户空间线程库来管理额外的逻辑线程。

用户态和内核态

Linux系统在设计之初就考虑到了资源管理与安全的重要性,引入了权限体系来限制不同程序对系统资源的访问能力。这一机制的核心在于区分了两种主要的运行状态:内核态(Ring 0)和用户态(Ring 3)。在x86架构中,虽然提供了四个特权级别(Ring 0至Ring 3),但在Linux系统中,主要使用的是 Ring 0(内核态)和 Ring 3(用户态)来进行资源控制,以简化管理和保证安全性。

  • 内核态(Ring 0) :这是权限最高的级别,只有操作系统内核本身和部分核心模块(如设备驱动)能在此级别运行。内核态可以不受限制地访问和控制所有硬件资源,修改内存、执行任何指令,以及管理所有系统进程。
  • 用户态(Ring 3) :大多数用户应用程序运行在这个级别上,权限较低,只能执行有限的操作,不能直接访问硬件资源或执行特权指令。应用程序必须通过系统调用来请求内核服务来间接完成一些受限操作,如文件I/O、网络通信等。
  • Linux中虽然理论上支持Ring 1和Ring 2,但实际上很少使用,一般情况下它们被视作额外的安全隔离层,或用于特殊驱动程序,但这样的设计在现代Linux系统中并不常见。
  • 权限约束原则:在多级权限体系中,高级别(低编号)的权限层可以访问低级别层的数据和资源,但反之不行。在权限约束上,使用的是高特权等级状态可以阅读低等级状态的数据,例如进程上下文、代码、数据等等,但是反之则不可。R0最高可以读取R0-3所有的内容,R1可以读R1-3的,R2以此类推,R3只能读自己的数据。

在Windows平台,采用了较为简单的两层模型,即内核态(Ring 0)和用户态(Ring 3),没有明确地使用中间的Ring 1和Ring 2级别,这也是出于系统设计和安全策略的考量。

应用程序一般会在以下几种情况下切换到内核态:

1. 系统调用。

2. 异常事件。当发生某些预先不可知的异常时,就会切换到内核态,以执行相关的异常事件。

3. 设备中断。在使用外围设备时,如外围设备完成了用户请求,就会向CPU发送一个中断信号,此时,CPU就会暂停执行原本的下一条指令,转去 处理中断事件。此时,如果原来在用户态,则自然就会切换到内核态。

用户线程和内核线程

用户线程与内核线程是操作系统中处理线程概念的两种主要方式,它们各自具备不同的特性和应用场景,下面是对两者更详细的对比和解释:

  • 用户线程:指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。另外,用户线程是由应用进程利用线程库创建和管理,不依赖于操作系统核心。不需要用户态/核心态切换,速度快。操作系统内核不知道多线程的存在,当线程进行系统调用或等待某些资源(如I/O操作)而阻塞时,由于内核只知道进程这个概念,它会视整个进程为一个单元进行处理。因此,即使只是进程中某个线程需要等待,操作系统也会将整个进程标记为阻塞状态。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。
  • 内核线程: 线程的所有管理操作都是由操作系统内核完成的。内核保存线程的状态和上下文信息,当一个线程执行了引起阻塞的系统调用时,内核可以调度该进程的其他线程执行。在多处理器系统上,内核可以分派属于同一进程的多个线程在多个处理器上运行,提高进程执行的并行度。由于需要内核完成线程的创建、调度和管理,所以和用户级线程相比这些操作要慢得多,但是仍然比进程的创建和管理操作要快。大多数市场上的操作系统,如Windows,Linux等都支持内核级线程。

选择用户线程还是内核线程取决于应用程序的具体需求和目标平台特性。用户线程适用于那些对响应速度敏感且线程间交互频繁,且不涉及大量系统调用的应用。而内核线程更适合需要充分利用多处理器并行处理能力和避免单一线程阻塞影响整体性能的场景。现代操作系统通常结合两者,如采用混合线程模型,即用户线程之上再叠加内核线程,既保留用户线程的轻量级快速,又利用内核线程实现并行和独立调度,达到更好的性能和灵活性平衡。

JVM线程调度

JVM线程调度:依赖JVM内部实现,主要是Native thread scheduling,是依赖操作系统的,所以java也不能完全是跨平台独立的,对线程调度处理非常敏感的业务开发必须关注底层操作系统的线程调度差异,所以理解线程的时候,一个线程是java线程对象,一个是调度器的线程(jvm)。

  • 用户级线程(User Level Thread,ULT):操作系统内核不知道应用线程的存在。
  • Native thread scheduling 或者 内核级线程(Kernel Level Thread ,KLT):它们是依赖于内核的,即无论是用户进程中的线程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。

Java线程与系统内核线程关系  :

Java语言层、JVM层(C++层)以及OS层(OSThread对象)之间关系的流程图。它展示了从Java语言层的Thread类开始,如何通过一系列步骤最终在操作系统层创建出一个线程的过程。

  1. Java语言层:首先,我们有一个Java语言层的Thread对象,它是通过调用Thread::start()方法启动的。这个方法是一个本地方法,意味着它会被映射到C++代码中去执行。
  2. JVM层:在JVM层,Thread::start0()方法被调用,然后通过JVM_StartThread接口调用到C++代码中去执行。在这个过程中,会创建一个新的JavaThread对象,并且调用OS::create_thread方法来创建一个操作系统级别的线程。
  3. OS层:在操作系统层,不同的平台有不同的实现方式。例如,在Linux上,会调用pthread_create函数来创建一个线程;在Windows上则会调用create_thread函数。
  4. 构建映射:在创建完操作系统级别的线程之后,会构建一个映射关系,即将JavaThread对象与操作系统级别的线程(即OSThread对象)关联起来。
  5. 线程执行:线程会在thread_ native_entry入口点处开始执行。此时,线程会调用JavaThread::run方法,进而执行Thread::run方法的内容。
  6. TLAB和栈:在JavaThread对象中,还包含了TLAB(Thread Local Allocation Buffer)和栈(Stack)。栈用于存储线程局部变量和方法调用信息,而TLAB则是用于内存分配的一种优化机制。

  • java.lang.Thread: 这个是Java语言里的线程类,由这个Java类创建的实例都会 1:1 映射到一个操作系统的OSThread
  • JavaThread: JVM中C++定义的类,一个JavaThread的实例代表了在JVM中的java.lang.Thread的实例, 它维护了线程的状态,并且维护一个指针指向java.lang.Thread创建的对象(oop)。它同时还维护了一个指针指向对应的OSThread,来获取底层操作系统创建的osthread的状态
  • OSThread: JVM中C++定义的类,代表了JVM中对底层操作系统的OSThread的抽象,它维护着实际操作系统创建的线程句柄handle,可以获取底层OSThread的状态
  • VMThread: JVM中C++定义的类,这个类和用户创建的线程无关,是JVM本身用来进行虚拟机操作的线程,比如GC。

image.png

线程的创建

JVM中创建线程有2种方式

  1. new java.lang.Thread().start()
  2. 使用JNI将一个native thread attach到JVM中

针对 new java.lang.Thread().start()这种方式,只有调用start()方法的时候,才会真正的在JVM中去创建线程,主要的生命周期步骤有:

  1. 创建对应的JavaThread的实例
  2. 创建对应的OSThread的实例
  3. 创建实际的底层操作系统的native thread
  4. 准备相应的JVM状态,比如ThreadLocal存储空间分配等
  5. 底层的native thread开始运行,调用java.lang.Thread生成的Object的run()方法
  6. 当java.lang.Thread生成的Object的run()方法执行完毕返回后,或者抛出异常终止后,
    终止native thread

针对JNI将一个native thread attach到JVM中,主要的步骤有:

  1. 通过JNI call AttachCurrentThread申请连接到执行的JVM实例
  2. JVM创建相应的JavaThread和OSThread对象
  3. 创建相应的java.lang.Thread的对象
  4. 一旦java.lang.Thread的Object创建之后,JNI就可以调用Java代码了
  5. 当通过JNI call DetachCurrentThread之后,JNI就从JVM实例中断开连接
  6. JVM清除相应的JavaThread, OSThread, java.lang.Thread对象

线程的状态

globalDefinitions.hpp

从JVM的角度来看待线程状态的状态有以下几种:

其中主要的状态是这5种:

  • _thread_new: 新创建的线程
  • _thread_in_Java: 在运行Java代码
  • _thread_in_vm: 在运行JVM本身的代码
  • _thread_in_native: 在运行native代码
  • _thread_blocked: 线程被阻塞了,包括等待一个锁,等待一个条件,sleep,执行一个阻塞的IO等
osThread.hpp

从OSThread的角度,JVM还定义了一些线程状态给外部使用,比如用jstack输出的线程堆栈信息中线程的状态:

比较常见有:

  • Runnable: 可以运行或者正在运行的
  • MONITOR_WAIT: 等待锁
  • OBJECT_WAIT: 执行了Object.wait()之后在条件队列中等待的
  • SLEEPING: 执行了Thread.sleep()的
jvm.h

从JavaThread的角度,JVM定义了一些针对Java Thread对象的状态,基本类似,多了一个TIMED_WAITING的状态,用来表示定时阻塞的状态

JVM内部的VM Threads,主要有几类:

  • VMThread: 执行JVM本身的操作
  • Periodic task thread: JVM内部执行定时任务的线程
  • GC threads: GC相关的线程,比如单线程/多线程的GC收集器使用的线程
  • Compiler threads: JIT用来动态编译的线程
  • Signal dispatcher thread: Java解释器Interceptor用来辅助safepoint操作的线程
java.lang.Thread.State
public enum State {
    
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
}