线程(上)【Linux操作系统】
文章目录
- 线程概念及其相关知识
- 线程的概念及一些重要认识
- 重要认识
- 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_struct
,struct 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表和信号处理方法表是共享的) -
⑥每个线程有自己的调度优先级
相关文章:

线程(上)【Linux操作系统】
文章目录 线程概念及其相关知识线程的概念及一些重要认识重要认识Linux中线程的实现Linux中的被调度的执行流是被task_struct描述的 线程是如何瓜分进程的代码和数据的?对于数据:对于代码: 线程的优点线程的缺点线程调度细节调度:…...
FPGA中的“BPI“指什么
在FPGA(现场可编程门阵列)中,BPI 的全称是 “Byte Peripheral Interface” 或 “Bank Parallel Interface”,具体指一种 并行NOR闪存配置接口,主要用于FPGA的配置(Configuration)过程。以下是BP…...
Splunk Validated Architecture (SVA):构建企业级可观测性与安全的基石
Splunk Validated Architecture (SVA) 是 Splunk 官方提供的一套经过严格测试、性能验证和最佳实践指导的参考架构蓝图。它并非单一固定方案,而是根据企业数据规模、性能需求、高可用性目标和合规要求,提供一系列可落地的部署模型。SVA 的核心价值在于为…...
Python爬虫(40)基于Selenium与ScrapyRT构建高并发动态网页爬虫架构:原理、实现与性能优化
目录 一、引言二、技术背景1. 动态页面处理痛点2. 架构设计目标 三、核心组件详解1. Selenium Grid集群部署2. ScrapyRT服务化改造3. 智能等待策略 四、系统架构图五、性能优化实践1. 资源隔离策略2. 并发控制算法3. 监控体系 六、总结与展望🌈Python爬虫相关文章&a…...
深入解析 Python 字典:从基础到高级应用
文章大纲 引言:什么是字典? 在 Python 编程中,字典(Dictionary)是一种极其重要的数据结构,它以键值对(key-value pair)的形式存储数据,能够高效地进行数据的查找和操作。相比于列表(List)这种依赖整数索引的序列类型,字典通过自定义的键来访问对应的值,提供了更…...

进程同步:生产者-消费者 题目
正确答案: 问题类型: 经典生产者 - 消费者问题 同时涉及同步和互斥。 同步:生产者与消费者通过信号量协调生产 / 消费节奏(如缓冲区满时生产者等待,空时消费者等待)。互斥:对共享缓冲区的访问需…...
Linux轻量级文件传输——tftp命令
摘要 TFTP是基于UDP/69端口的轻量文件传输协议。本文整理tftp命令参数/交互命令,提供示例,涵盖文件上传下载、模式设置等核心操作,帮助快速掌握基础文件传输。 一、TFTP核心特性 tftp(Trivial File Transfer Protocol࿰…...
JavaSwing之--为组件添加背景
JavaSwing之–为组件添加背景 从实践角度,可以把Java Swing中的组件分为容器组件和普通组件,容器组件是为了更好的按照某种布局摆放各种组件,形成功能强大且友好的界面。 Swing中组件的背景可以分为两种类型,一种是背景色&#…...
MySQL项目实战演练:搭建用户管理系统的完整数据库结构【MySQL系列】
本项目适用于后台管理系统、电商用户中心、SaaS 用户模块等场景,特别适合开发者进行实战演练与面试准备。 一、项目背景与需求概述 我们将构建一个基础版的用户管理系统,具备以下业务功能: 用户注册与登录用户角色与权限分配日志记录与用户…...

展会聚焦丨漫途科技亮相2025西北水务博览会!
2025第三届西北水务数字化发展论坛暨供排水节水灌溉新技术设备博览会在兰州甘肃国际会展中心圆满落幕。本届展会以“科技赋能水资源,数智引领新动能”为主题,活动汇集水务集团、科研院所、技术供应商等全产业链参与者,旨在通过前沿技术展示与…...

【数据结构初阶】顺序表的应用
文章目录 顺序表的应用基于动态顺序表实现通讯录前言1.定义联系人数据2.给顺序表改名3.通讯录的初始化4.通讯录的销毁5.通讯录添加数据6.通讯录删除数据7.通讯录修改数据8.通讯录查找数据9.展示通讯录数据10.通讯录的最终实现 顺序表的应用 基于动态顺序表实现通讯录 前言 功…...
抽象工厂模式与策略模式结合使用小案例
目录 1.前言 1.前言 上一篇章就通过简单的案例来了解抽象工厂模式和策略模式的使用,现在就用个支付场景的小案例来演示两者设计模式的联合使用;...

C#数字图像处理(一)
文章目录 1.C#图像处理基础1.1 Bitmap类1.2 Bitmapdata类1.3 Graphics类1.4 Image类 2.彩色图像灰度化1.提取像素法2.内存法3.指针法三种方法的比较4.灰度图像二值化: 3.相关链接 Bitmap类、 Bitmapdata类和 Graphics类是C#图像处理中最重要的3个类,如果要用C# 进行…...

麻省理工新突破:家庭场景下机器人实现精准控制,real-to-sim-to-real学习助力
麻省理工学院电气工程与计算机科学系Pulkit Agrawal教授,介绍了一种新方法,可以让机器人在扫描的家庭环境模拟中接受训练,为任何人都可以实现定制的家庭自动化铺平了道路。 本文将探讨通过Franka机器人在虚拟环境中训练的特点,研…...

从零实现本地语音识别(FunASR)
FunASR 是达摩院开源的综合性语音处理工具包,提供语音识别(ASR)、语音活动检测(VAD)、标点恢复(PUNC)等全流程功能,支持多种主流模型(如 Paraformer、Whisper、SenseVoic…...
Vue 项目中 Sass 与 Less 的对比
文章目录 一、核心特性对比二、Vue 项目集成方案三、性能关键指标四、选型决策矩阵五、Vue 3 最佳实践六、构建优化建议最终建议一、核心特性对比 特性Sass/SCSSLess语法扩展.scss(类CSS语法)类似CSS,更接近原生变量系统$variable@variable嵌套规则支持(含属性嵌套)支持Mixi…...
Python爬虫实战:研究CherryPy库相关技术
1. 引言 1.1 研究背景与意义 随着互联网信息的爆炸式增长,如何高效地获取、组织和利用网络信息成为重要研究方向。网络爬虫作为自动采集网页内容的关键技术,被广泛应用于搜索引擎构建、市场调研、数据挖掘等领域。同时,将采集到的数据以 Web 服务的形式提供,能够为用户提…...

已解决:.NetCore控制台程序(WebAPI)假死,程序挂起接口不通
本问题已得到解决,请看以下小结: 关于《.NetCore控制台程序(WebAPI)假死,程序暂停接口不通》的解决方案 记录备注报错时间2025年报错版本VS2022 WINDOWS10报错复现鼠标点一下控制台,会卡死报错描述——报错截图——报错原因 控制台启用了“快…...

Excel如何分开查看工作表方便数据撰写
首先我这里有2class和3class两个工作表 接下来我们点击视图 按照顺序分别点击新建窗口和全部重排 ### 然后就是这样 接下来就OK了...

微软技术赋能:解锁开发、交互与数据潜力,共探未来创新路
在微软 Build 2025 大会以及创想未来峰会上,微软展示的一系列前沿技术与创新应用,不仅展现了其在科技领域的深厚底蕴与前瞻视野,更为开发者和企业带来了前所未有的机遇与变革动力。 领驭科技作为微软中国南区核心合作伙伴及 HKCSP 1T 首批授…...

VR看房系统,新生代看房新体验
VR看房系统的概念 虚拟现实(VirtualReality,VR)看房系统,是近年来随着科技进步在房地产行业中兴起的一种创新看房方式。看房系统利用先进的计算机技术模拟出一个三维环境,使用户能够身临其境地浏览和体验房源,无需亲自…...

【Linux笔记】Shell-脚本(下)|(常用命令详细版)
在(上)篇,我们详细的讲解了Shell脚本的基础知识和些许命令与实验,这次的的(下)篇,我们会详细讲解Shell脚本的常用命令 关于脚本的基础知识请各位移步到(上)篇啦~ Shell…...

钉钉热点实时推送助理-思路篇
以下是针对热点实时推送助理的功能描述,结合机器学习技术栈与用户场景的通俗化解释: 快速体验的话直接用钉钉扫描下方二维码体验 1. 核心功能 (1)热点抓取引擎 类比:像蜘蛛爬取全网信息(网络爬虫信息抽取…...
RuoYi前后端分离框架实现前后端数据传输加密(一)之后端篇
一、背景 项目采用RuoYi前后端分离框架搭建,版本为3.8.9。为确保数据传输安全性,提高爬虫获取数据的门槛,领导要求系统指定的字段在API通信过程中要实现加密传输,但未对算法类型做具体要求,本人基于目前的新创的大环境考虑,采用了SM4对称加密算法对系统指定字段进行加密…...
第七十篇 从餐厅后厨到电影院选座:生活场景拆解Java并发编程核心
目录 一、并发基础:餐厅后厨的协作艺术1.1 厨师与线程(Thread)1.2 共享资源竞争:唯一的炒锅1.3 线程状态转换:厨师工作流 二、线程同步:电影院选座中的锁机制2.1 同步锁(synchronized࿰…...
深入理解设计模式之代理模式
深入理解设计模式之:代理模式 一、什么是代理模式? 代理模式(Proxy Pattern)是一种结构型设计模式。它为其他对象提供一种代理以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介作用,可以在不改变目标…...
8位单通道数据保存为JPG
如何将单通道8位灰度数据(0黑~255白)直接保存为JPG文件? 这里提供两种最实用方案:轻量级STB库(推荐)和OpenCV方案(已有环境适用) STB方案 - 推荐 //https://github.com/nothings/…...
【Java实战】低侵入的线程池值传递
欢迎来到啾啾的博客🐱。 记录学习点滴。分享工作思考和实用技巧,偶尔也分享一些杂谈💬。 有很多很多不足的地方,欢迎评论交流,感谢您的阅读和评论😄。 目录 引言InheritableThreadLocalAlibaba Transmittab…...

实验设计与分析(第6版,Montgomery)第5章析因设计引导5.7节思考题5.11 R语言解题
本文是实验设计与分析(第6版,Montgomery著,傅珏生译) 第5章析因设计引导5.7节思考题5.11 R语言解题。主要涉及方差分析,正态假设检验,残差分析,交互作用图。 dataframe<-data.frame( densityc(570,565,…...
c++复习_第一天(引用+小众考点)
https://en.cppreference.com/w/cpp/io/manip 参考一下,这一部分比较基础,所以就一遍过 eg1:转16进制 #include<iostream> #include<iomanip> using namespace std;int main() {int n;cout << "请输入一个整数:";cin >> n;cou…...