Linux线程概念与控制

目录

分页式存储管理

物理内存的管理

OS管理物理内存时,将物理内存分为若干个4KB大小的页框,用struct page结构体描述每个页框并将其组织起来。

页表与虚拟地址

OS将虚拟内存下的逻辑地址空间分为若⼲⻚,将物理内存空间分为若⼲⻚框通过⻚表便能把连续的虚拟内存,映射到若⼲个不连续的物理内存⻚。

页表就是记录虚拟地址空间中的页与物理内存中的页的映射关系。页表只记录页对应的物理页的起始地址4GB/4KB*4Byte=4MB

两级页表

单级页表只记录页对应的物理页的起始地址4GB/4KB*4Byte=4MB,页表需要支持随机访问可以看作一个数组,但这个数组要求4MB的内存连续,且在稀疏地址空间下内存浪费严重。

两级页表:将页表分为页目录表(记录每个页表的地址,1024个页表4KB)和页表(只记录1024个页表项,4KB)。

核心优势在于降低对物理内存的连续空间需求(不需要一个数组来记录4MB大的页表),同时减少内存浪费(不需要分配所有页表项,只分配需要的页表)

两级页表的地址转换

  1. 在32位处理器中,采⽤4KB的⻚⼤⼩,则虚拟地址中低12位为⻚偏移,剩下⾼20位给⻚表,分成 两级,每个级别占10个bit(10+10)。
  2. CR3 寄存器 读取⻚⽬录起始地址,再根据⼀级⻚号查⻚⽬录表,找到下⼀级⻚表在物理内存中 存放位置。
  3. 根据⼆级⻚号查表,找到最终想要访问的物理页号(⼀个物理⻚的地址⼀定是 4KB 对⻬的(最后的 12 位全部为 0 ),所以其实只需要记录物理⻚地址的⾼20位即可。)。
  4. 结合⻚内偏移量得到物理地址。
    在 32 位系统的页表设计中,页表项(Page Table Entry, PTE)的大小通常是 32 位(4 字节),前20位记录物理页号,后12位是标志位

缺页异常

当 CPU 访问某个虚拟地址时,MMU(内存管理单元)在快表和页表中都没有找到对应物理地址时,CPU会触发缺页异常(中断),此时由内核接管处理。

内核根据不同缺页类型进行不同处理

  1. 主缺页错误:物理内存中无对应物理页,需要从磁盘中读入物理内存,再由MMU建立虚拟地址和物理地址映射
  2. 次要缺页错误:物理内存中已有对应物理页(可能由其它进程调入),此时只需要MMU建立地址映射
  3. 无效缺页错误:访问地址越界等非法地址,此时内核触发段错误segment fault,终止进程

快表

单级⻚表对连续内存要求⾼,于是引⼊了多级⻚表,但是多级⻚表也是⼀把双刃剑,在减少连续存储要求且减少存储空间的同时降低了查询效率。

快表(TLB,Translation Lookaside Buffer)是 CPU 内部的高速缓存(CPU的访问速度远快于物理内存),核心作用是缓存近期使用的虚拟页号到物理页号的映射关系,从而跳过多级页表的查表过程,大幅提升地址转换效率。

快表的工作流程

  1. 地址拆分:CPU 将虚拟地址拆分为 "虚拟页号(VPN)" 和 "页内偏移量"。
  2. TLB 查询:用虚拟页号作为索引,查询快表中的缓存项。
  3. 判断命中 / 未命中:
  • 命中:直接从快表项中取出对应的物理页号,与页内偏移量拼接成物理地址,完成转换(无需访问物理内存)。
  • 未命中:CPU 按二级页表流程,先查页目录表(物理内存),再查页表(物理内存),获取物理页号;同时将 "虚拟页号 - 物理页号" 的映射存入快表(若快表已满,会替换旧项),最后拼接成物理地址。

局部性原理

局部性原理:程序在执行过程中,对内存、数据或指令的访问往往集中在局部区域,而非随机分散在整个地址空间。

局部性原理的两大核心类型

局部性主要分为时间局部性和空间局部性,二者共同决定了程序的访问特征:

  1. 时间局部性
  • 定义:如果某个数据或指令在当前被访问,那么它在近期很可能会被再次访问。
  • 典型场景:
    • 循环结构:循环体中的指令会被重复执行,循环变量会被多次读写;
    • 频繁使用的变量:如函数中的全局变量、高频调用的临时变量。
  • 优化应用:CPU 缓存会将近期访问的数据 / 指令保留在高速缓存中,避免再次从低速内存读取,缩短访问延迟。
  1. 空间局部性
  • 定义:如果某个数据或指令被访问,那么它相邻地址(或连续地址区域)的数据 / 指令在近期很可能会被访问。
  • 典型场景:
    • 数组遍历:数组元素在内存中连续存储,遍历数组时会按顺序访问相邻元素;
    • 指令流执行:程序指令通常按顺序存放和执行,相邻指令大概率被依次调用。
  • 优化应用:缓存的 "预取机制"------ 当访问某个地址时,系统会自动将其相邻的若干字节数据一同载入缓存,提前准备后续可能的访问需求。
    通过利用这一原理减少低速设备的访问频率,最终实现系统性能的提升。

分页设计与快表对局部性原理的应用

分页设计是将4KB的连续内存划分到一个页中,利用了空间局部性原理。

快表内存储最近访问的页号,利用了时间局部性原理。

linux线程概念

什么是线程

进程是资源划分的基本单位,线程是资源调度的基本单位,线程是进程内部的一个执行路线,可以共享分配到进程的大部分资源。

  • 在⼀个程序⾥的⼀个执⾏路线就叫做线程(thread)。更准确的定义是:线程是"⼀个进程内部 的控制序列"
  • ⼀切进程⾄少都有⼀个执⾏线程
  • 线程在进程内部运⾏,本质是在进程地址空间内运⾏
  • 在Linux系统中,在CPU眼中,看到的PCB都要⽐传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的⼤部分资源,将进程资源合理分配给每个执⾏流,就形成了线程执⾏流

进程与线程对比

导致进程与线程特点鲜明对比的原因

进程和线程虽然都是操作系统调度的对象,但由于资源管理模型(独立与隔离 vs 共享与协作)存在本质差异,导致两者在特性上呈现出鲜明对比。

线程的优势

  1. 线程比进程轻量,核心因三点:
  • 创建快:仅需分配线程栈、TID和少量上下文数据等少量私有资源,无需复制进程地址空间和大量元数据;
  • 切换快:共享地址空间,无需刷新 TLB 和缓存,上下文数据量小(仅寄存器、栈指针等);
  • 省资源:共享进程内存(代码、数据、堆),仅线程栈私有,总内存占用远低于多进程。
    线程切换的成本更低的关键在于不用重置cache 和TLB等缓存机制,且上下文数据量更小。
    而进程切换因必须隔离地址空间和资源,导致 TLB 和 Cache 大量失效,上下文操作复杂,因此成本更高。
  1. 线程的CPU利用率更高:I/O密集型应⽤,为了提⾼性能,将I/O操作重叠。进程若因 IO 操作(如磁盘读写、网络请求)阻塞,整个进程会进入等待状态,无法利用 CPU,线程可以同时等待不同的I/O操作。
  2. 线程更适合多核并行
  • 多进程也可利用多核,但进程间资源隔离导致协作成本高,难以高效分配任务。
  • 多线程天然适合多核架构:同一进程的多个线程可被调度到不同 CPU 核心上并行执行,共享数据的同时充分利用多核算力,尤其适合计算密集型任务(如数据分析、图像处理)。
    线程的优势本质上是通过共享进程资源减少开销,通过轻量级设计提升调度和协作效率。

进程的优势

  1. 故障隔离与系统稳定性,进程崩溃,系统依然稳定,线程崩溃,整个进程终止
  2. 安全性与资源控制,进程通过独立的页表控制独立的权限和资源限制,而线程因为资源共享,可能导致不该共享的变量被共享,缺乏天然的隔离保护。
  3. 进程编程复杂性更低
    进程的优势本质上源于 "资源隔离" 带来的稳定性、安全性和可控性。

进程与线程的适合场景

线程的优势本质上是通过共享进程资源减少开销,通过轻量级设计提升调度和协作效率。当任务在同一资源容器内、需高频协作、或需细粒度并发时,线程更合适。

进程的优势本质上源于 "资源隔离" 带来的稳定性、安全性和可控性。当任务需要独立运行、避免相互干扰、支持分布式部署或需要精细资源控制时,进程是更优选择。

创建进程与线程的目的对比

创建进程和线程都是为了并发执行,但由于资源管理模型(独立与隔离 vs 共享与协作)存在本质差异,决定其不同的适用情况。

创建进程的目的:

  1. 并行:利用多进程并发能力提升任务处理效率;(继续执行当前文件的代码)
  2. "隔离":通过独立资源空间保护父进程和系统,同时支持动态执行新程序。(进行替换,执行新的程序)
    创建线程的目的:
    创建线程的目的只有一个,就是并发执行原进程代码。

多进程与多线程

多进程(基于 fork)和多线程的核心目标都是实现并发执行(让程序能同时处理多个任务),但两者因底层设计的差异(资源共享、隔离性、开销等),适用场景有显著区别。这种区别本质上是 "并发效率" 与 "隔离安全性" 的权衡

  1. 多进程(fork):用 "隔离" 换 "安全稳定"
  2. 多线程:用 "共享" 换 "高效协作"
    多进程(fork 创建)与多线程的核心特点可从 资源模型、开销、安全性、协作效率 四个维度简要分析:
  3. 资源模型
  • 多进程:进程间资源严格隔离(独立地址空间、页表、文件描述符表等),默认不共享数据,需通过 IPC 机制(如共享内存、管道)显式共享。
  • 多线程:线程共享进程的大部分资源(代码、数据、文件等),仅私有线程栈、寄存器等,天然支持数据共享。
  1. 开销成本
  • 多进程:创建 / 切换成本高(需复制页表、进程控制块,切换时刷新 TLB 和缓存)。
  • 多线程:创建 / 切换成本低(仅需分配私有资源,共享地址空间,TLB 和缓存有效率高)。
  1. 安全性与容错性
  • 多进程:隔离性强,单个进程崩溃不影响其他进程,适合运行不可信任务(如用户代码)。
  • 多线程:共享资源导致容错性弱,一个线程的内存错误可能破坏整个进程(如越界修改共享内存)。
  1. 协作效率
  • 多进程:通信依赖 IPC,机制复杂、开销高(如数据需拷贝),适合低频率交互场景。
  • 多线程:通过全局变量直接共享数据,配合锁 / 条件变量即可同步,通信效率极高,适合高频协作(如生产 - 消费者模型)。
    简言之,多进程是 "隔离优先" 的重量级并发,多线程是 "效率优先" 的轻量级并发,核心差异源于资源共享与否。
    多线程虽然可以使用锁/条件变量等同步互斥机制使协作效率高,但容易引发竞争问题和产生死锁

线程的私有数据

线程共享进程数据,但也拥有⾃⼰的⼀部分数据: ◦ 线程ID ◦ ⼀组寄存器,线程的上下文数据(反应线程是被独立调度) ◦ 线程栈 ◦ errno ◦ 信号屏蔽字 ◦ 调度优先级

线程私有资源的选择,始终围绕一个核心原则:"与线程独立执行流强绑定、共享后会导致逻辑混乱或行为冲突的资源,必须私有"。

进程的多个线程的共享数据

因为是在同一个地址空间,因此所谓的代码段(Text Segment)、数据段(Data Segment)都是共享的:

如果定义一个函数,在各线程中都可以调用。

如果定义一个全局变量,在各线程中都可以访问到。

除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表。(进程打开一个文件后,其他线程也能够看到)
  • 每种信号的处理方式。(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
  • 当前工作目录。(cwd)
  • 用户ID和组ID。

线程资源管理的本质

线程进行资源划分,本质是划分进程地址空间以获得合法虚拟地址范围。

线程资源共享,本质是对进程地址空间的共享落实到页表层面就是对页表条目的共享。

POSIX线程与LWP

  • linux内核中没有线程,只有LWP(轻量级线程)
  • LWP完全复用进程的调度机制
  • LWP创建时可以通过指定clone()系统调用的参数而选择资源共享范围
  • 如果选择共享父进程的所有资源(地址空间)只创建自己的私有栈就是内核级线程,如果不共享就是独立进程

为什么轻量级进程能复用进程机制

  • 轻量级进程之所以可以复用进程机制,本质是因为从创建进程和线程的目的而言都是为了被调度执行,
  • 进程与 LWP 的本质区别仅在于 "资源隔离程度"(独立与隔离,共享与协作),而 Linux 通过灵活的系统调用参数,让这种差异可动态设置,无需修改进程机制本身。
  • 所以LWP复用进程大部分机制,只需对资源管理进行设置

POSIX线程与LWP的关系

POSIX 线程是 LWP 的用户态抽象

pthread线程库是应用层的原生线程库

LWP 是内核中的可调度实体(对应task_struct),而 POSIX 线程(pthread)是用户态对 LWP 的封装与抽象,二者是 "内核实体" 与 "用户态接口" 的关系。

  • 内核层:LWP 是内核中实际执行的任务,由clone()创建,通过参数定义资源共享范围,每个 LWP 有唯一内核tid,内核通过tid管理其调度和生命周期。
  • 用户态库层:pthread 线程库封装clone()等系统调用,提供pthread_create等统一接口,屏蔽内核参数细节,确保创建的 LWP 符合线程语义。
  • 用户态标识:pthread_t是用户态线程句柄,内部指向struct pthread,该结构体存储与内核 LWP 关联的tid等信息,是对内核task_struct的用户态抽象,类似FILE通过fd关联内核struct file。
    LWP在创建时通过clone()的参数设置资源共享的访问就能成为内核级线程,但这里需要让用户控制实现所以语义是不能称为线程(用户态线程)。通过pthread对系统调用进行封装就可以称为线程,这里的pthread_t就是线程句柄,可以指向struct pthread(内部包含tid),其就是对内核中轻量级进程的task_struct一层抽象,这里和c语言的struct FILE
    包含fd对内核中struct file的封装类似

pthread库与进程地址空间

线程"私有"的数据通过mmap创建映射到共享区,其中包括struct pthread 、线程局部存储 、线程栈。

线程栈是mmap映射在共享区的一块内存区域,创建时设置大小不能动态增长

线程局部存储是每个线程对全局变量的副本,不同进程间不同

pthread库中的基础接口

pthread_create的原理

创建线程时传递给pthread_create的参数中设置了线程的属性,pthread_create先创建struct pthread并根据参数将其初始化(属性和创建绑定线程栈),然后根据struct pthread的属性控制clone的参数实现对创建的线程属性的控制,最终创建线程。

其它接口

错误机制

pthread函数一般成功返回0失败返回错误码errno,不会更新全局的errno,因为线程共享全局的errno可能造成线程间干扰,pthreads同样也提供了线程内的errno变量,以⽀持其它使⽤errno的代码。

封装线程

内部创建线程并对其操作函数进行封装,对外接收一个参数(线程的执行方法函数)

封装基础接口

Routine函数不能设置为普通成员函数因为参数中有this指针,与pthread_create参数中的Routine函数不匹配

相关推荐
wypywyp几秒前
8. ubuntu 虚拟机 linux 服务器 TCP/IP 概念辨析
linux·服务器·ubuntu
Doro再努力15 分钟前
【Linux操作系统10】Makefile深度解析:从依赖推导到有效编译
android·linux·运维·服务器·编辑器·vim
senijusene20 分钟前
Linux软件编程:IO编程,标准IO(1)
linux·运维·服务器
忧郁的橙子.27 分钟前
02-本地部署Ollama、Python
linux·运维·服务器
醇氧36 分钟前
【linux】查看发行版信息
linux·运维·服务器
No8g攻城狮1 小时前
【Linux】Windows11 安装 WSL2 并运行 Ubuntu 22.04 详细操作步骤
linux·运维·ubuntu
XiaoFan0122 小时前
免密批量抓取日志并集中输出
java·linux·服务器
souyuanzhanvip2 小时前
ServerBox v1.0.1316 跨平台 Linux 服务器管理工具
linux·运维·服务器
HalvmånEver3 小时前
Linux:线程互斥
java·linux·运维
番茄灭世神3 小时前
Linux应用编程介绍
linux·嵌入式