线程(上)【Linux操作系统】

article/2025/6/20 18:09:09

文章目录

  • 线程概念及其相关知识
    • 线程的概念及一些重要认识
      • 重要认识
      • Linux中线程的实现
      • Linux中的被调度的执行流是被task_struct描述的
    • 线程是如何瓜分进程的代码和数据的?
      • 对于数据:
      • 对于代码:
    • 线程的优点
    • 线程的缺点
    • 线程调度细节
      • 调度:
      • 这样调度线程的话,怎么保证线程的切换成本低呢?
    • 线程的异常和作用
    • 进程怎么知道哪些线程是自己的?
    • 线程的信号
    • 总结一下每个线程自己私有一份的东西

线程概念及其相关知识

线程的概念及一些重要认识

重要认识

基本认识:
进程=内核数据结构+代码和数据

线程是一个执行流,执行粒度比进程更细,是进程内部的一个执行流

进阶认识:

  • 进程是承担分配系统资源的基本实体

    • 操作系统中,分配资源(包括所有软硬件资源,例如页表,进程地址空间,用于存储代码/数据的物理内存块)是以进程为基本单位的,不是以线程为基本单位
      而是:
      进程申请资源,线程瓜分进程资源
  • 线程是操作系统调度的基本单位
    操作系统调度是以线程为单位的,不是以进程为单位
    因为CPU调度队列里存的是轻量级进程(LWP)


Linux中线程的实现

和进程类似,线程有自己的代码和数据,CPU还要调度线程,线程还有状态,有生命周期
而且线程是进程中的执行流,所以线程的个数>=进程的个数
所以操作系统一定要对线程进行管理

如果Linux也给线程先描述再组织,那么就要设计各种各样的(进程有多少个,线程就至少有多少个)结构体和数据结构,才能满足线程管理的需要
但是因为线程和进程不管是实现上还是概念上都高度相似,而且如果重新为线程设计一套数据结构,会搞的非常复杂
所以
Linux下的线程是使用进程模拟实现的

Linux操作系统直接用进程的数据结构和结构体,模拟实现线程
(这样做就可以复用所有进程实现调度,切换,状态管理等等结构体和数据结构)
具体的:

  • 进程的PCB(struct task_struct)=线程的TCB
    即使用struct task_struct描述进程和线程
    [其他操作系统不一定是这样干的]

反正又没有要求操作系统一定要怎样实现线程,只需要实现出来的东西满足操作系统理念中线程的定义就行了


所以
Linux中,在进程创建一个线程就是:

  • ①创建一个独立的PCB(task_struct

  • ②让这个PCB和所有的PCB一起使用同一份进程地址空间,页表等结构体和数据结构
    即:
    Linux中创建线程,就是创建一个独立的PCB,然后瓜分一份进程的资源(代码和数据)

在这里插入图片描述

所以:
Linux中线程由3部分共同组成:

  • ①一个独立的PCB

  • ②从进程地址空间里分到的属于线程自己的代码和数据

  • ③所有线程共享的,进程相关的所有数据结构和结构体(mm_structstruct file页表等等)


瓜分同一个进程的资源的所有的线程组合在一起就是进程
所以我们之前学的进程确实也是进程,只不过它内部只有一个执行流,即只有一个线程,这一个线程自己和自己“组合”形成进程


线程是独立的个体,进程是线程的组合
所以:
一个struct task_struct结构体对象≤一个进程


Linux中的被调度的执行流是被task_struct描述的

因为
一个struct task_struct结构体对象≤一个进程
所以
被称为:struct task_struc也被称为轻量级进程(LWP)
其实:
Linux中物理上不存在线程(因为没有给它单独先描述再组织)
只是使用PCB模拟线程实现线程
但是我们逻辑上就认为LWP,就是线程就行

因为线程是在进程内运行的,所以同一个进程中的不同线程,使用getpid()时获得的pid都是一样的,都是进程的pid
但是CPU调度队列里面的是LWP啊!
那如何标识LWP(或者说线程)的唯一性呢?
使用LWP(整型封装类型)标识
主线程的LWP与进程的pid相等


PCB中除了会存储pid,还会存储LWP
在这里插入图片描述



线程是如何瓜分进程的代码和数据的?

对于数据:

因为数据都可以通过进程地址空间访问
进程地址空间是所有线程共享的
所以本质上线程数据(除了栈区数据)都是所以线程共享的

对于代码:

从线程相关的系统调用,我们就可以进行划分了

因为创建线程的时候就会给它一个函数,所以这个函数就归这个线程使用了
而主线程就至少占据main函数了
这个本质上就是在划分进程的代码了
因为:
对应函数的起始虚拟地址就被存储到对应线程的PCB中,而且每个函数的代码的虚拟地址都不一样
CPU执行切换LWP时,只要有一个函数的虚拟起始地址给EIP寄存器,就可以执行完这个函数所有的代码了
这不就可以调度/切换线程了吗?



线程的优点

前3点是线程比进程的优势

  • Linux中创建一个线程的代价比创建一个进程小得多
    因为除了创建主线程以外,创建一个线程只需要创建并初始化一个PCB,再瓜分一下资源就行了,其他的结构体对象全部用进程创建的

  • 线程占用的资源比进程少
    因为可以多个线程使用一个进程的资源

  • 线程切换的代价比进程切换低得多
    上下文切换上(进程切换时):

    • 1.切换存储PCB地址的寄存器中的值就可以把所有的进程相关的数据结构和结构体这些内核数据结构切换掉
      因为所有内核数据结构都是,通过PCB里面的指针找到的

    • 2.切换CR3寄存器中的值,就可以把页表切换掉,代码和数据就切换了
      因为代码和数据都是通过页表建立虚拟→物理的映射关系的

    • 3.切换其他的各种存放进程临时上下文(计算结果,要执行的代码等等)的寄存器

  • 因为所有同一个进程中的线程共享进程地址空间,页表,fd表等结构体
    所以:
    线程切换比起进程切换,可以不用切换1和2,只需要切换3(存储函数运行时,产生的各种临时数据的寄存器)

  • 重点在于缓存机制上:
    缓存机制上(切换进程时):
    切换进程一定会导致CPU中的

    • 1.TLB中缓存的虚拟地址→物理地址的映射关系失效,需要花时间重新缓存
      所以切换进程之后虚拟→物理的转换效率短时间内会显著降低

    • 2.cache中从物理内存那里,缓存来的进程高频使用的代码和数据以及CPU上正在运行的代码附近的代码和数据,也会全部失效也需要重新花时间缓存
      在缓存好之前,CPU访问进程的代码和数据的效率会显著降低


  • 线程切换则不会让TLB和cache硬件中的缓存失效
    因为同一个进程中的所有线程共用进程的代码和数据


后四点可以说是线程和进程共有的优点,只不过线程通信比进程简单
所以我们其实写多线程的代码比写多进程的代码的频率高得多

  • 线程的执行粒度比进程更细,如果都采用多线程的方式构成一个进程,那么多核CPU时,可以更好地利用上并行执行

  • 利用线程可以很轻松地实现多执行流,就让一些线程去专门执行IO/系统调用等慢速操作,等待它们完成的同时,其他线程还能执行其他任务

线程在计算密集型任务(算法,加密等对数据进行运算)中比进程更有优势
因为计算密集区型任务为了快速计算,一般会在多个CPU/多个核心中并行执行
线程越多越好吗?
肯定不是,一般有几个核心就搞几个线程,这样直接并行执行就行
因为再更多线程的话,最多同时运算的线程就CPU运算核心那么多个,反而会增加切换成本
又没有像IO密集型任务中的阻塞的线性,不占用CPU运算核心


IO密集型任务(比如下载,传输等)为了提高性能,也可以采用多线程
此时线程的个数可以>CPU的运算核心个数
因为IO是大部分时候都是阻塞状态,阻塞状态的线程不占用CPU的运算核心



线程的缺点

  • ​​性能损失​​
    一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。
    如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

  • ​​健壮性降低​​
    编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

  • ​​缺乏访问控制​​
    线程共享资源导致 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

  • ​​编程难度提高​​
    编写与调试一个多线程程序比单线程程序困难得多(这个因人而异把



线程调度细节

调度:

线程是CPU调度的基本单位

不是先调度进程,把进程“放在”CPU上之后,再让CPU核心执行进程的线程
仔细想想就知道不行,因为进程中线程的个数是不固定的,而CPU的核心个数是固定的,所以分配很可能不合理,CPU核心利用率可能不足

操作系统调度线程的方式是怎样的?
其实和我们之前讲的“进程调度"是一模一样的
我们之前说的进程调度其实是一种特殊情况:进程中只有一个主线程

因为:
Linux内核通过任务(Task)作为调度的基本单位,每个任务对应一个 task_struct 结构体
而进程和线程都是用task_struct描述的

  • 进程:被视为资源容器(如地址空间、文件描述符表等),包含一个或多个线程
  • 线程:共享进程的资源(如内存、文件fd),但拥有独立的执行上下文(如寄存器、栈等)
    调度器直接选择线程(任务)作为调度单位,而非进程

所以:
调度一个进程,就是把这个进程分割成n个线程,然后单独调度分割出来的线程
(n的大小当然由程序员自己决定)

换句话说:
CPU核心调度进程分割出来的线程时,就是在调度进程

例如,一个多线程进程(包含3个线程)在内核中会被视为3个独立的调度实体

调度过程示例
假设系统中有以下任务:
进程A:包含线程A1、A2(共享地址空间)
进程B:包含线程B1(单线程)

调度器会直接选择可运行的线程(如A1、A2、B1)分配CPU时间,而非先选择进程A/B,再选择其内部的线程
例如:
CPU核心1运行线程A1
CPU核心2运行线程B1
线程A2因等待IO阻塞或者时间片耗尽,调度器从就绪队列中选择另一个线程(如A1或B1)运行


所以根据上面讨论的,一个多核CPU的不同核心可以运行不同进程的线程
是不是就是说:一个多核CPU可以并行运行多个进程?
没座!!!
因为线程的代码就是瓜分进程代码来的,所以执行线程代码就是在执行进程代码



这样调度线程的话,怎么保证线程的切换成本低呢?

如果这样调度线程的话,为什么还说线程的切换成本比进程低呢?
如果进程A有2个线程a1和a2,进程B有两个线程b1和b2
如果CPU核心1,运行线程a1之后,运行的是a2那切换成本就很低
但是如果再运行b1或者b2那就要进行上下文切换和清空CPU缓存和TLB呀,就是进程切换了呀!

这个问题Linux操作系统也考虑到了
毫无疑问的在同一个CPU核心上调度同一个进程的多个线程,尽可能让同一个进程的线程之间互相切换,这样才能保证把线程切换成本低的优势发挥出来

怎么做到这一点呢?

  • CPU亲和性的自动维护
    Linux调度器(如CFS)会尽量保持同一个进程的线程在同一个CPU核心上运行,以减少因跨核心迁移导致的缓存失效和TLB刷新。
    具体机制包括:

    • 缓存局部性优化:同一进程的线程共享地址空间(通过CLONE_VM标志),调度器倾向于将同一进程的线程调度到最近使用的核心,以利用缓存中的热数据。
    • 调度域=策略:在多核系统中,调度器将CPU核心划分为多个调度域。在负载均衡时,优先在同一调度域内迁移线程,减少跨域迁移的概率。
  • 负载均衡与迁移抑制

    • 负载感知的迁移决策:
      当系统负载较低时,调度器倾向于将同一进程的线程集中在少数核心上运行,避免不必要的迁移。当负载较高时,才会分散到不同核心以均衡负载
    • 唤醒亲和性:
      当一个线程被唤醒时(如从睡眠状态恢复),调度器会优先选择该线程之前运行的CPU核心,以减少缓存失效的开销


线程的异常和作用

我们之前说过进程异常的概念,进程产生异常的本质就是CPU在执行进程的某一代码时,出现了错误

而进程是由线程组成的,进程的代码全部被它的线程瓜分了,所以触发异常的那一行代码也被分给了一个线程
所以线程触发了异常,就是进程触发了异常
所以进程就会挂掉,进程的其他所有线程也会挂掉
这也就是为什么说多线程代码健壮性不够好的原因了



进程怎么知道哪些线程是自己的?

其实PCB里面还有一个链接字段:组
一个进程中的所有线程都会被放进同一个组中
这样操作系统就可以通过组,对所有线程进行统一操作


线程的信号

本质上并没有线程信号这个东西,只有进程信号

一个进程的所有线程共享进程的信号处理历程
即线程的信号相关的2张表(pending表,信号处理方法表)是共享的
(注意:block表不是共享的


总结一下每个线程自己私有一份的东西

一个进程中的所有线程会共享进程的资源
但是
每个线程也有自己也有自己私有一份(即其他线程看不见)的资源

  • ①线程的id(LWP)

  • ②CPU切换线程时,线程会保存自己的上下文

  • 线程虽然共享地址空间,但是每个线程都有自己独立的栈
    因为每个线程都可以调用函数,就要申请栈桢,如果共享同一个栈,操作系统太不好管理了
    而且每个线程有自己独立的栈,就可以做到局部性的线程之间互不影响

  • ④每个线程都有自己的errno(错误码)
    为了方便形成各自的日志,方便debug
    因为C语言的erron是全局变量,如果不每个线程一份的话,很容易出现线程安全问题

  • ⑤每个线程都有自己的信号屏蔽字[block]
    但是pending表和信号处理方法表是共享的

  • ⑥每个线程有自己的调度优先级


http://www.hkcw.cn/article/oGtPbvvELL.shtml

相关文章

定制开发开源AI智能名片S2B2C商城小程序:数字营销时代的话语权重构

摘要:在数据驱动的数字营销时代,企业营销话语权正从传统媒体向掌握用户数据与技术的平台转移。本文基于“数据即权力”的核心逻辑,分析定制开发开源AI智能名片S2B2C商城小程序如何通过技术赋能、场景重构与生态协同,帮助企业重构营…

【笔记】Windows 成功部署 Suna 开源的通用人工智能代理项目部署日志

#工作记录 本地部署运行截图 kortix-ai/suna: Suna - 开源通用 AI 代理 项目概述 Suna 是一个完全开源的 AI 助手,通过自然对话帮助用户轻松完成研究、数据分析等日常任务。它结合了强大的功能和直观的界面,能够理解用户需求并提供结果。其强…

哪些工作最容易被AI取代?

在 AI 技术狂飙突进的今天,一场 “职场大地震” 正在悄然酝酿。当 ChatGPT 能妙笔生花,当智能机器人开始站岗执勤,在这个 AI 飞速发展的时代,“饭碗危机” 已悄然降临。你是否想过,自己的工作是否也处在被 AI 取代的高…

二叉搜索树——AVL

AVL AVL定义AVL树出现的原因AVL的插入平衡因子的更新旋转左单旋右单旋左右双旋右左双旋 杂谈完整代码 AVL定义 AVL树是最先发明的⾃平衡⼆叉查找树,AVL是⼀颗空树,或者具备下列性质的⼆叉搜索树:它的左右⼦树都是AVL树,且左右⼦树…

Deepin 20.9社区版安装Docker

个人博客地址:Deepin 20.9社区版安装Docker | 一张假钞的真实世界 注意事项 Deepin 20.9 社区版安装 Docker 需要注意两点: 因为某些原因,Docker 官方源基本不可用,所以需要使用镜像源进行安装。当然也可以用安装包直接安装&am…

(7)-Fiddler抓包-Fiddler状态面板-QuickExec命令行

1.简介 Fiddler成了网页调试必备的工具,抓包看数据。Fiddler自带命令行控制,并提供以下用法。Fiddler的快捷命令框让你快速的输入脚本命令。 除了输入默认命令,也可以自定义命令,你可以通过编辑 FiddlerScript 来增加新命令&…

Linux --UDP套接字实现简单的网络聊天室

一、Server端的实现 1.1、服务端的初始化 ①、创建套接字&#xff1a; 创建套接字接口&#xff1a; #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); //1. 这是一个创建套接字的接…

OpenHarmony标准系统-HDF框架之音频驱动开发

文章目录 引言OpenHarmony音频概述OpenHarmony音频框图HDF音频驱动框架概述HDF音频驱动框图HDF音频驱动框架分析之音频设备驱动HDF音频驱动框架分析之supportlibs实现HDF音频驱动框架分析之hdi-passthrough实现HDF音频驱动框架分析之hdi-bindev实现HDF音频驱动加载过程HDF音频驱…

C#WinForm程序时方法很多时Form.cs文件会很长,如何分别写入多个文件,partial class的作用体现出来了。

右键->添加->类 类文件名称为 FormButtonClick.cs 双击button3&#xff0c;将Form1里button3的Click事件处理方法拷贝到FormButtonClick.cs里面。

关于win10系统中环境变量path变成一行显示的问题

怎么把环境变量从一行显示恢复成列表显示(原文链接在最下面&#xff0c;感谢) 一行显示&#xff08;调整了环境变量把C:\Windows\System64开头的挪到了后面或者删了就会这样&#xff09;&#xff1a; 只需在开头加上 C:\Windows\System64; 重新打开 就恢复成列表显示了 关于wi…

NW969NW978美光闪存颗粒NW980NW984

NW969NW978美光闪存颗粒NW980NW984 技术解析&#xff1a;NW969、NW978、NW980与NW984的架构创新 美光&#xff08;Micron&#xff09;的闪存颗粒系列&#xff0c;尤其是NW969、NW978、NW980和NW984&#xff0c;代表了存储技术的前沿突破。这些产品均采用第九代3D TLC&#xf…

python打卡训练营打卡记录day41

知识回顾 数据增强卷积神经网络定义的写法batch归一化&#xff1a;调整一个批次的分布&#xff0c;常用与图像数据特征图&#xff1a;只有卷积操作输出的才叫特征图调度器&#xff1a;直接修改基础学习率 卷积操作常见流程如下&#xff1a; 1. 输入 → 卷积层 → Batch归一化层…

某航参数逆向及设备指纹分析

文章目录 1. 写在前面2. 接口分析3. 加密分析4. 算法还原5. 设备指纹风控分析与绕过 【&#x1f3e0;作者主页】&#xff1a;吴秋霖 【&#x1f4bc;作者介绍】&#xff1a;擅长爬虫与JS加密逆向分析&#xff01;Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享…

电子电器架构 --- OTA测试用例分析(上)

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 钝感力的“钝”,不是木讷、迟钝,而是直面困境的韧劲和耐力,是面对外界噪音的通透淡然。 生活中有两种人,一种人格外在意别人的眼光;另一种人无论…

方案精读:42页华为企业组织活力设计方案【附全文阅读】

该文档聚焦华为企业组织活力设计,核心内容为:在非线性时代,方向未必完全正确时,组织活力是企业成功关键,可通过创新与柔性激发。以熵增、熵减为理论基础,华为活力引擎模型包含宏观(厚积薄发、开放合作对抗熵增)与微观(人力资源管理对抗个人惰怠)。 实践上从三层面激活…

双目相机深度的误差分析(基线长度和相机焦距的选择)

全文基于针孔模型和基线水平放置来讨论 影响双目计算深度的因素&#xff1a; 1、基线长度&#xff1a;两台相机光心之间距离2、相机焦距&#xff08;像素&#xff09;&#xff1a; f x f_x fx​&#xff08;或 f y f_y fy​&#xff09;为焦距 f f f和一个缩放比例的乘积。在…

Namespace 命名空间的使用

名字空间&#xff1a;划分更多的逻辑空间&#xff0c;有效避免名字冲突的问题 1.什么是命名空间 名字命名空间 namespace 名字空间名 {...} // 名字空间 n1 域 namespace n1 {// 全局变量int g_money 0;void save(int money){g_money money;}void pay(int money){g_money - m…

力扣热题100之翻转二叉树

题目 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 代码 方法一&#xff1a;递归 # Definition for a binary tree node. # class TreeNode: # def __init__(self, val0, leftNone, rightNone): # self.val val # …

simulink mask的使用技巧

1.mask界面布局 1.1如何调整控件的位置和控件大小&#xff1f; 反正2020a是调不了&#xff0c; 找了好久&#xff0c;只能是调布局&#xff0c;例如你要调成下面这样&#xff1a; 第一个控件的iTem location属性选择New row 后面跟着的几个和第一个同一行的空间属性选择Cu…

第12讲、Odoo 18 权限控制机制详解

目录 引言权限机制概述权限组&#xff08;Groups&#xff09;访问控制列表&#xff08;ACL&#xff09;记录规则&#xff08;Record Rules&#xff09;字段级权限控制按钮级权限控制菜单级权限控制综合案例&#xff1a;多层级权限控制最佳实践与注意事项总结 引言 Odoo 18 提…