当前位置: 首页 > news >正文

深入了解linux系统—— 线程概念

线程

什么是线程呢?线程和之前学习到的进程又有什么关系呢?

从概念的角度来说

进程 = 内核数据结构 + 代码和数据(执行流)
线程 = 进程内部的一个执行分支(执行流)

在内核与资源的角度

进程:承担分配系统资源的基本实体;
线程:CPU调度的基本单位;

那线程到底是什么呢?在Linux操作系统中线程具体是什么呢?

首先,对于进程我们知道,有独立的内核数据结构(task_struct)、进程地址空间(mm_struct)、页表映射和物理内存中的代码和数据。

在这里插入图片描述

在我们之前的认知中,一个进程有一个task_struct,和进程地址空间mm_struct;也就是说这个task_struct是独享这个进程地址空间的。

而线程是进程内部的一个执行分支,那是不是就可以为线程创建task_struct,并且多个task_struct共享进程地址空间mm_struct;这样每一个task_struct执行自己的那部分代码,每一个task_struct就是一个执行流。

这里进程访问资源,大部分都是通过进程地址空间来访问的;进程地址空间就犹如一个窗口,进程通过窗口来访问资源。

所以,创建多个进程(task_struct),共享同一个窗口(进程地址空间),将资源分配给不同的task_struct,这样就可以使用进程来模拟线程的。

  • Linux操作系统中,线程就是用进程模拟实现的。
  • 对于资源的分配,本质就是对进程地址空间的虚拟地址的划分。(虚拟地址就是资源)
  • 对于代码区的划分,函数也是虚拟地址的集合;让线程执行不同的函数。

轻量级进程

说了这么多,在Linux操作系统中其实是没有线程这一概念的,线程是用进程模拟实现的。

那在Linux中线程和进程如何区分呢?

进程:内核数据结构 + 代码和数据;而在Linux是没有线程的概念的;

对于一个进程,其中有一个或者多个内核数据结构;而其中的一个task_struct又被称为 轻量级进程

所以说,在Linux内核中,task_struct结构体对象又被称为轻量级进程;而进程是内核数据结构对象 + 代码和数据。

在一个进程中,可能存在多个task_struct结构体对象;之前所了解的只是单执行流的进程。

在这里插入图片描述

所以,在Linux操作系统下,并没有线程这一概念,只有轻量级进程(task_struct);

Linux系统的线程是使用进程来模拟实现的:

应该轻量级进程task_struct就相当于进程中的一个线程;

线程是进程的一个执行流,而一个进程可能存在多个task_struct,分别执行不同的代码,也就是多个执行流。

分页式存储

在深入理解线程之前,先来简单了解一下分页式存储:

1. 虚拟地址和页表

如果没有虚拟地址和页表,为了能够保证在物理内存上找到对应的代码和数据,每一个程序在物理内存上所对应的空间就必须是连续的;

而代码和数据的长度是不一致的,这样去映射,物理内存就势必会以很多碎片的形式存在,这样内存利用率非常低。

在这里插入图片描述

而想要操作系统提供给用户的空间是连续的,而对应的物理内存尽量不要连续;就有了虚拟地址和页表分页:

在这里插入图片描述

将物理内存按照固定的长度的页框进行分割,也称为物理页;每一个页框包含一个物理页,一个页的大小等于页框的大小。

大部分32位体系结构支持4KB 的页,64位体系结构一般会支持8KB的页。

  • 页框是一个存储区域
  • 页是一个数据块,可以存放在任何页框或磁盘中。

有了虚拟地址和页表分页机制,CPU就能够通过虚拟地址空间来间接访问物理地址,而不是直接访问物理地址。

虚拟地址空间:就是操作系统在位每一个正在执行的进程分配的一个物理地址,在32位下,范围是[0,4]GB

操作系统将虚拟地址空间和物理地址之间建立映射关系,就是页表,页表中记录了每一对页和页框的映射关系;CPU能够根据虚拟地址,通过页表映射访问物理内存地址。

简单来说就是:

将虚拟内存下的逻辑地址空间分为若干页,将物理内存空间分为若干个页框,通过页表将连续的虚拟内存映射到若干个不连续的物理内存页

2. 物理内存

将物理内存空间分为若干个页框;比如4GB物理内存,每个页框的大小4KB,那就存在``1048576`个页框;这么多物理页,操作系统肯定要将这些物理页管理起来。

Linux内核中,struct page结构表示每个系统中的物理页,在struct page中,存在非常多的联合体union(节省空间)

struct page
{/* 原⼦标志,有些情况下会异步更新 */unsigned long flags;union{struct{/* 换出⻚列表,例如由zone->lru_lock保护的active_list */struct list_head lru;/* 如果最低为为0,则指向inode * address_space,或为NULL* 如果⻚映射为匿名内存,最低为置位 * ⽽且该指针指向anon_vma对象 */struct address_space *mapping;/* 在映射内的偏移量 */pgoff_t index;/* * 由映射私有,不透明数据 * 如果设置了PagePrivate,通常⽤于buffer_heads* 如果设置了PageSwapCache,则⽤于swp_entry_t * 如果设置了PG_buddy,则⽤于表⽰伙伴系统中的阶 */unsigned long private;};struct{ /* slab, slob and slub */union{struct list_head slab_list; /* uses lru */struct{ /* Partial pages */struct page *next;
#ifdef CONFIG_64BITint pages;    /* Nr of pages left */int pobjects; /* Approximate count */
#elseshort int pages;short int pobjects;
#endif};};struct kmem_cache *slab_cache; /* not slob *//* Double-word boundary */void *freelist; /* first free object */union{void *s_mem;            /* slab: first object */unsigned long counters; /* SLUB */struct{                        /* SLUB */unsigned inuse : 16; /* ⽤于SLUB分配器:对象的数⽬ */unsigned objects : 15;unsigned frozen : 1;};};};...};union{/* 内存管理⼦系统中映射的⻚表项计数,⽤于表⽰⻚是否已经映射,还⽤于限制逆向映射搜索*/atomic_t _mapcount;unsigned int page_type;unsigned int active; /* SLAB */int units;           /* SLOB */};...
#if defined(WANT_PAGE_VIRTUAL)/* 内核虚拟地址(如果没有映射则为NULL,即⾼端内存) */void *virtual;
#endif /* WANT_PAGE_VIRTUAL */...
}

其中存在flags_mapcountvirtual等标志位

  1. falgs:存放页的状态,包括页是否是脏的,是否被锁定在内存中。falgs的每一个比特位单独表示一种状态,至少可以同时表示32种的状态。其中PG_locked表示指定页是否被锁定、PG_uptodate表示页的数据结已经存块设备读取且没有错误。
  2. _mapcount:表示页表中有多少项指向该页,也就是该页被引用了多少次;当计数值为-1时,表示内核并没有引用这一页,就在新分配中使用它。
  3. virtual:页的虚拟地址,一般情况下它就是页中虚拟内存中的地址。(一些内存并不永久映射到内核地址空间中,此时该值为NULL,在需要的时候动态映射这些页。

3. 页表

在上面描述中,通过页表将连续的虚拟地址映射到了不连续的物理地址中;那页表 是什么呢?

页表的每一个表项都指向一个物理页,在32位系统中,虚拟地址的最大空间是4GB,每一个用户程序都存在虚拟地址空间;让4GB空间全部可用,在页表中就要表示出来这4GB空间,也就需要4GB/4KB,也就是1048576个表项。

如果页表中直接存储物理地址,在32位系统下,地址的大小是4KB,这样页表就要占用1048576*4也就是4MB的空间,一个页表就要占用1024个物理页;在内核中存在非常多的进程,每一个进程都有自己的页表,那是不是就占用非常多的内存资源了。

要解决大容量页表,就要把页表看做普通文件,对它进程离散分离,对页表再分页,形成多级页表

在这里插入图片描述

简单了解了页表,那虚拟地址又是什么呢?CPU是如何通过虚拟地址获取物理地址的呢?

32位系统为例,地址大小为4字节,也就是32bit位;

32bit位分为三部分:10位,表示页目录表中页表对应的下标、之后的10位,表示页表中物理页对应的下标;后12位表示页内偏移

在这里插入图片描述

  • 页表的物理地址被页目录表项指向;
  • 页目录的物理地址被CR3寄存器指向;CR3寄存器保存了当前正在执行任务的页目录地址。

所以说,操作系统在加载用户程序时,不仅需要为程序内容分配物理地址,还要为页目录和页表分配物理内存。

在这里插入图片描述

4. 缺页中断

缺页中断简单来说:
缺页中断是当程序试图访问一个当前不在物理内存(RAM)中,而是存放在硬盘(如交换空间或页面文件)中的内存页面时,由硬件(通常是内存管理单元 MMU)触发的一种特殊中断(或异常)。

为什么需要它?
缺页中断是现代操作系统实现虚拟内存管理的关键机制。虚拟内存让程序以为自己拥有连续的、比实际物理内存大得多的地址空间。操作系统负责在物理内存和硬盘之间按需交换数据块(称为“页”)。

发生缺页中断的基本流程:

  1. 程序访问内存: CPU 执行一条指令,需要读取或写入某个内存地址。
  2. MMU 检查页表: 内存管理单元根据该地址查找页表(操作系统维护的数据结构,记录虚拟页到物理页框的映射关系,以及页的状态)。
  3. 发现“缺页”: MMU 发现该地址对应的页表项表明:
    • 无效: 该虚拟地址尚未分配或无效(这会导致更严重的错误,如段错误)。
    • 有效但不在内存中: 该虚拟页是合法的(已分配),但它的内容当前不在物理内存中(页表项中的“存在/有效”位被清除)。这就是“缺页”情况。
  4. 触发中断: MMU 检测到缺页条件,向 CPU 发出一个缺页中断信号。
  5. 操作系统接管: CPU 暂停当前程序的执行,保存其上下文(寄存器状态等),并切换到内核态,执行操作系统预先设置好的缺页中断处理程序
  6. 中断处理程序工作:
    • 查找页面位置: 根据触发中断的虚拟地址,在页表中找到对应的项,确定该页当前存放在硬盘上的具体位置(如交换空间)。
    • 分配物理页框: 在物理内存中找到一个空闲的页框(物理内存块)。如果没有空闲页框,则需要使用页面置换算法(如 LRU)选择一个“牺牲”页框,将其内容写回硬盘(如果它是脏的/被修改过)。
    • 调入页面: 发出 I/O 请求,将需要的页面从硬盘读入到分配好的物理页框中。这是一个相对慢速的操作(磁盘 I/O)。
    • 更新页表: 修改页表项,将该虚拟页映射到新的物理页框,并将状态标记为“在内存中”(设置“存在/有效”位)。
    • 可能调整: 如果需要置换页面,还需更新被换出页面的页表项(标记为不在内存)。
  7. 恢复执行: 中断处理程序完成后:
    • 恢复被中断程序的上下文。
    • 重新执行那条导致缺页中断的指令。
    • 这次 MMU 再查页表时,就能找到有效的物理地址映射,指令得以正常执行。

简单来说,缺页中断就是操作系统用来“现场搬救兵”的机制:当程序要用到硬盘上的数据时,硬件喊停,操作系统赶紧去硬盘把数据搬到内存,然后让程序接着运行。这是现代操作系统内存管理高效运作的核心机制之一。

线程与进程

1. 线程的优点

  • 创建与切换成本低
    • 创建代价:线程共享进程资源(内存、文件描述符等),无需复制完整地址空间。
    • 切换代价低
      • 保留虚拟内存:线程切换不刷新TLB(快表)和硬件Cache,避免内存访问效率下降。
      • 寄存器切换为主:仅需保存/恢复线程私有数据(寄存器、栈指针),无需切换页表等内核资源。
  • 资源占用少
    • 共享进程的代码段、数据段、堆、打开文件等,仅需独立维护线程私有栈、寄存器等少量资源。
  • 提升并行能力
    • 多线程可绑定到不同CPU核心,充分利用多处理器并行执行(尤其适合计算密集型任务)。
  • I/O效率优化
    • 线程可并发等待多个I/O操作(如一个线程处理用户输入,另一个线程下载文件),避免阻塞主程序。

2. 线程的缺点

  1. 性能损失风险
    • 计算密集型场景:若线程数 > CPU核心数,频繁切换导致调度开销增大,实际吞吐量反而下降。
    • 同步开销:锁竞争、信号量管理等额外操作消耗CPU时间。
  2. 健壮性降低
    • 共享数据风险:一个线程修改共享变量可能导致其他线程逻辑错误(需严格同步)。
    • 单点崩溃:单个线程的野指针/除零错误会触发信号机制(如SIGSEGV),终止整个进程
  3. 访问控制缺失
    • 进程是资源分配单位,线程无权限制其他线程访问共享资源(如全局变量、文件句柄)。
  4. 开发复杂度高
    • 需处理竞态条件、死锁、优先级反转等问题,调试难度显著增加。

3. 线程异常

线程是进程的一部分,某个线程如果出现除零、野指针等问题导致线程崩溃,进程也会崩溃。

线程是进程的执行分支,线程如果出现异常,就等同于进程出异常,从而触发信号机制,终止进程进程终止,进程中的所有线程也就随即退出。

4. 线程适用场景

场景类型应用示例线程作用
CPU密集型视频编码/科学计算分解任务到多核并行,缩短计算时间。
I/O密集型Web服务器/文件下载工具重叠I/O等待与计算,提升响应速度。
交互式应用图形界面程序后台任务不阻塞用户操作(如边渲染边响应用户输入)。

Linux进程与线程

  • 进程是资源分配的基本单位,线程是调度的基本单位

  • 线程共享进程数据,但同时也有自己的一部分数据(线程ID、一组寄存器、、调度优先级等)

    线程共享进程地址空间

Linux操作系统中,多个轻量级进程公用一个进程地址空间,在同一个进程地址空间内,代码和数据都是共享的;

如果定义一个函数,每一个线程都可以调用这一个函数;定义一个变量,每一个线程都可以访问这个变量;

除此之外,像文件描述符表、每种信号处理方式、当前工作目录、用户id和组id等等都是共享的。

在这里插入图片描述

在之前学习进程的过程中,都是单线程进程;也就是只具有一个执行流的进程。

到这里本篇文章内容就结束了,感谢支持

http://www.xdnf.cn/news/1297567.html

相关文章:

  • ZigBee入门与提高(3)—— ZigBee协议初识
  • Visual Studio2019/2022离线安装完整教程(含闪退解决方法)
  • Windows bypassUAC 提权技法详解(一)
  • 基于FPGA的8PSK+卷积编码Viterbi译码通信系统,包含帧同步,信道,误码统计,可设置SNR
  • Python之Django使用技巧(附视频教程)
  • HTML <link rel=“preload“>:提前加载关键资源的性能优化利器
  • 企业智脑正在构建企业第二大脑,四大场景引擎驱动数字化转型新范式
  • C++入门自学Day11-- List类型的自实现
  • 手写MyBatis第16弹:泛型魔法应用:MyBatis如何破解List的运行时类型
  • 一种适用于 3D 低剂量和少视角心脏单光子发射计算机断层成像(SPECT)的可泛化扩散框架|文献速递-深度学习人工智能医疗图像
  • OpenCV 高斯模糊降噪
  • Spring Boot + Redis + 布隆过滤器防止缓存穿透
  • 带root权限_贝尔RG020ET-CA融合终端S905L处理器当贝纯净版刷机教程
  • 分布式系统架构设计模式:从微服务到云原生
  • pycharm远程连接服务器跑实验详细操作
  • Go语言实战案例:简易图像验证码生成
  • Java 设计模式-组合模式
  • Vscode的wsl环境开发ESP32S3的一些问题总结
  • 在 Windows 系统中解决 Git 推送时出现的 Permission denied (publickey) 错误,请按照以下详细步骤操作:
  • 宋红康 JVM 笔记 Day01|JVM介绍
  • [工具]vscode 使用AI 优化代码
  • 使用EvalScope对GPT-OSS-20B进行推理性能压测实战
  • 【完整源码+数据集+部署教程】肾脏病变实例分割系统源码和数据集:改进yolo11-CARAFE
  • 自动化运维实验(二)---自动识别设备,并导出配置
  • AM32电调学习-使用Keil编译uboot
  • 搭建局域网yum源仓库全流程
  • 华为实验 链路聚合
  • GoLand 项目从 0 到 1:第八天 ——GORM 命名策略陷阱与 Go 项目启动慢问题攻坚
  • 更新pip及Python软件包的完整指南
  • STM32HAL 快速入门(七):GPIO 输入之光敏传感器控制蜂鸣器