Linux-线程概念与线程控制的常用操作

一.Linux线程概念

1-1.线程是什么

        在Linux中,线程是基于Linux原有的进程实现的。本质是轻量级进程(LWP)。在⼀个程序⾥的⼀个执⾏路线就叫做线程(thread)。更准确的定义是:线程是“⼀个进程内部的控制序列”。

        我们之前所学习的进程其实均是单线程的进程。⼀切进程⾄少都有⼀个执⾏线程(主线程)。其次,线程在进程中运行,一个进程中的线程共享该进程的地址空间。(所以线程没有自己独立的地址空间,其运行空间本质是进程地址空间的一部分,这也是Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化的原因)。

        细心的读者应该不难发现,线程间通信要比进程间通信容易的多,因为它们天然就能看到同一块资源。

1-2.分页式存储管理

1-2-1.页表的由来

        我们都知道,如果没有页表。让进程的数据直接对应物理内存而不通过虚拟地址进行转化。那么进程在物理地址上的映射就必须是连续的。因为每⼀个程序的代码、数据⻓度都是不⼀样的,按照这样的映射⽅式,物理内存将会被分割成各种离散的、大小不同的块。经过⼀段运⾏时间之后,有些程序会退出,那么它们占据的物理内存空间可以被回收,导致这些物理内存都是以很多碎片的形式存在。

        而我们希望操作系统提供给用户的空间必须是连续的,但是物理内存最好不要连续。此时虚拟内存和分页便出现了,如下图所示:

        把物理内存按照⼀个固定的长度的页框进⾏分割,有时叫做物理页。每个页框包含⼀个物理页(page)。⼀个页的大小等于⻚框的大小。大多数 32位 体系结构⽀持 4KB 的页,而 64位 体系结构⼀般会支持 8KB 的页。

         操作系统通过将虚拟地址空间和物理内存地址之间建页映射关系,也就是页表,这张表上记录了每⼀对页和页框的映射关系,能让CPU间接的访问物理内存地址。

        总结⼀下,其思想是将虚拟内存下的逻辑地址空间分为若干页,将物理内存空间分为若干页框,通过页表便能把连续的虚拟内存,映射到若⼲个不连续的物理内存⻚。这样就解决了使⽤连续的物理内存造成的碎片问题。

1-2-2.物理内存管理

        假设⼀个可⽤的物理内存有 4GB 的空间。按照⼀个页框的大小4KB 进行划分, 4GB 的空间就是4GB/4KB = 1048576 个页框。有这么多的物理页,操作系统肯定是要将其管理起来的,操作系统需要知道哪些页正在被使⽤,哪些页空闲等等。

        内核⽤ struct page 结构表示系统中的每个物理页,出于节省内存的考虑, struct page 中使⽤了⼤量的联合体union。

         要注意的是 struct page 与物理页相关,⽽并非与虚拟⻚相关。⽽系统中的每个物理⻚都要分配⼀个这样的结构体,让我们来算算对所有这些页都这么做,到底要消耗掉多少内存。

        算 struct page 占40个字节的内存吧,假定系统的物理⻚为 4KB ⼤⼩,系统有 4GB 物理内存。那么系统中共有⻚⾯ 1048576 个(1兆个),所以描述这么多⻚⾯的page结构体消耗的内存只不过40MB ,相对系统 4GB 内存⽽⾔,仅是很小的⼀部分罢了。因此,要管理系统中这么多物理⻚⾯,这个代价并不算太⼤。

        要知道的是,页的大小对于内存利⽤和系统开销来说⾮常重要,⻚太⼤,⻚必然会剩余较⼤不能利⽤的空间(页内碎⽚)。页太小,虽然可以减小内碎⽚的大小,但是页太多,会使得⻚表太长而占⽤内存,同时系统频繁地进行页转化,加重系统开销。因此,页的大小应该适中,通常为 512B - 8KB ,windows系统的⻚框大小为4KB。

1-2-3.页表 

        如果我们进程中的页表是物理地址与虚拟地址一一对应的单张表。假设一个地址的大小为4字节,那么要映射地址空间4GB至少需要4GB*8=32GB的空间。而一个进程可使用的空间一共也就只有4GB啊。显然太荒诞了。

        显然不能以这中方式进行映射。换种思路,由于我们的物理内存天然被分为了1047576,也就是1024*1024个4kb大小的页。那我们便可以只记录每个页的起始位置,然后当我们需要使用某一位置的物理内存时,便可以先通过其前20个字节(取值范围为0~1047576)找到对应的页,然后加上后12字节(0~4096)找到真实的物理地址。此时只需要1024*1024*4B = 4mb即可索引4GB大小的物理内存空间。

        上面的方法看似已经完美,但实际上还有问题。如果我们要存储上面的单级页表,本身就需要4mb/4kb=1024个连续的物理页。但我们说过,不希望其在物理内存上的存储时连续的。这不就与我们当初的想法所背道而驰了吗。因此实际上我们页表在32位系统下是二级的。虚拟地址是这样索引的

 0000000000        0000000000         000000000000
/一级页表(0~1024)  /二级页表(0~1024)  /(0~4096)
/又称页目录表

 

        这样,将1024*1024个地址分级管理。页目录表为 1024*4b = 4kb大小。页表项也是1024*4b=4kb大小。就可以避免物理内存中连续存储的问题了。

1-2-4两级页表的地址转换

 

        上⾯是以⼀个逻辑地址为例。将逻辑地址( 0000000000,0000000001,11111111111 )转换为物理地址的过程:

  1. 在32位处理器中,采⽤4KB的⻚⼤⼩,则虚拟地址中低12位为⻚偏移,剩下⾼20位给⻚表,分成两级,每个级别占10个bit(10+10)。
  2. CR3 寄存器 读取⻚⽬录起始地址,再根据⼀级⻚号查⻚⽬录表,找到下⼀级⻚表在物理内存中存放位置。
  3. 根据⼆级⻚号查表,找到最终想要访问的内存块号。
  4. 结合⻚内偏移量得到物理地址。
  5. 注:⼀个物理⻚的地址⼀定是 4KB 对⻬的(最后的 12 位全部为 0 ),所以其实只需要记录物理⻚地址的⾼ 20 位即可。
  6. 以上其实就是 MMU 的⼯作流程。MMU(Memory Manage Unit)是⼀种硬件电路,其速度很快,主要⼯作是进⾏内存管理,地址转换只是它承接的业务之⼀。

        到这⾥其实还有个问题,MMU要先进⾏两次⻚表查询确定物理地址,在确认了权限等问题后,MMU再将这个物理地址发送到总线,内存收到之后开始读取对应地址的数据并返回。那么当⻚表变为N级时,就变成了N次检索+1次读写。可⻅,⻚表级数越多查询的步骤越多,对于CPU来说等待时间越⻓,效率越低。

        有没有提升效率的办法呢?计算机科学中的所有问题,都可以通过添加⼀个中间层来解决。 MMU 引⼊了新武器,江湖⼈称快表的 TLB(其实,就是缓存)。

        当 CPU 给 MMU 传新虚拟地址之后, MMU 先去问 TLB 那边有没有,如果有就直接拿到物理地址发到总线给内存,⻬活。但 TLB 容量⽐较⼩,难免发⽣ Cache Miss ,这时候 MMU 还有保底的⽼武器 ⻚表,在⻚表中找到之后 MMU 除了把地址发到总线传给内存,还把这条映射关系给到TLB,让它记录⼀下刷新缓存。

1-2-5缺页异常

        设想,CPU 给 MMU 的虚拟地址,在 TLB 和⻚表都没有找到对应的物理⻚,该怎么办呢?其实这就是缺⻚异常 Page Fault ,它是⼀个由硬件中断触发的可以由软件逻辑纠正的错误。

        假如⽬标内存⻚在物理内存中没有对应的物理⻚或者存在但⽆对应权限,CPU 就⽆法获取数据,这种情况下CPU就会报告⼀个缺⻚错误。

        由于 CPU 没有数据就⽆法进⾏计算,CPU罢⼯了⽤⼾进程也就出现了缺⻚中断,进程会从⽤⼾态切换到内核态,并将缺⻚中断交给内核的 Page Fault Handler 处理。

 

1-3线程的优缺点 

二.Linux线程与进程 

 

三.Linux线程控制基本操作

3-1.POSIX线程库

  1. 与线程有关的函数构成了⼀个完整的系列,绝⼤多数函数的名字都是以“pthread_”打头的
  2. 要使⽤这些函数库,要通过引⼊头⽂ <pthread.h>
  3.  链接这些线程函数库时要使⽤编译器命令的“-lpthread”选项

3-2 线程控制的重要函数

3-2-1线程创建

功能:创建⼀个新的线程
原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);参数:
thread:返回线程ID
attr:设置线程的属性,attr为NULL表⽰使⽤默认属性
start_routine:是个函数地址,线程启动后要执⾏的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码

3-2-1线程终止

如果需要只终⽌某个线程⽽不终⽌整个进程,可以有三种⽅法:

  1.  从线程函数return。这种⽅法对主线程不适⽤,从main函数return相当于调⽤exit。

  2. 线程可以调⽤pthread_ exit终⽌⾃⼰。

  3. ⼀个线程可以调⽤pthread_ cancel终⽌同⼀进程中的另⼀个线程。

pthread_exit函数

功能:线程终⽌
原型:void pthread_exit(void *value_ptr);
参数:value_ptr:value_ptr不要指向⼀个局部变量
返回值:⽆返回值,跟进程⼀样,线程结束的时候⽆法返回到它的调⽤者(⾃⾝)

        需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是⽤malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

pthread_cancel函数

功能:取消⼀个执⾏中的线程
原型:int pthread_cancel(pthread_t thread);
参数:thread:线程ID
返回值:成功返回0;失败返回错误码

3-2-3线程等待 

为什么需要线程等待?

  1. 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内
  2. 创建新的线程不会复⽤刚才退出线程的地址空间。 
功能:等待线程结束
原型int pthread_join(pthread_t thread, void **value_ptr);参数:
thread:线程ID
value_ptr:它指向⼀个指针,后者指向线程的返回值返回值:成功返回0;失败返回错误码

        调⽤该函数的线程将挂起等待,直到id为thread的线程终⽌。thread线程以不同的⽅法终⽌,通过pthread_join得到的终⽌状态是不同的,总结如下:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元⾥存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调⽤pthread_ cancel异常终掉,value_ ptr所指向的单元⾥存放的是常数PTHREAD_ CANCELED。
  3. 如果thread线程是⾃⼰调⽤pthread_exit终⽌的,value_ptr所指向的单元存放的是传给

    pthread_exit的参数。

  4. 如果对thread线程的终⽌状态不感兴趣,可以传NULL给value_ ptr参数。 

 3-2-5 分离线程

  1. 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进⾏pthread_join操作,否则⽆法释放资源,从⽽造成系统泄漏。
  2. 如果不关⼼线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,⾃动释放线程资源。
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对⽬标线程进⾏分离,也可以是线程⾃⼰分离: pthread_detach(pthread_self());
joinable和分离是冲突的,⼀个线程不能既是joinable⼜是分离的。 

 四.线程ID及进程地址空间布局

         LWP是线程对应的linux中实际上的轻量化进程的ID。第一个创建的线程与进程ID一致。之后创建的线程顺延。那我们获取的线程ID是什么呢。我们说过,所有线程都在进程地址空间上拥有着一块属于自己的空间,而我们但凡使用线程就需要链接线程库pthread.h。而其又是一个动态库。此时我们就会得到一个结论:Linux中所有线程共享进程地址空间,包括通过mmap映射的动态库(如pthread),但线程本身并不存在于这些映射上,而是由内核调度并在共享地址空间中运行。

        而pthread_t 到底是什么类型呢?取决于实现。对于Linux⽬前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是⼀个进程地址空间上的⼀个地址。

 五.对3-2部分介绍的函数简单封装一个线程类

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <functional>
#include <string>#ifndef __Thread_hpp__
#define __Thread_hpp__namespace My_Thread
{static uint32_t number = 1;class Thread{using fun_c = std::function<void()>;void EnableDetach(){std::cout << "线程已分离" << std::endl;_isdetach = true;}void EnableRunning(){_isrunning = true;}static void* Routinue(void* arg){Thread* self = static_cast<Thread*>(arg);self->EnableRunning();pthread_setname_np(self->_pt,self->_name.c_str());self->_fc();return nullptr;}public:Thread(fun_c fc): _isdetach(false),_fc(fc),_pt(0),res(nullptr),_isrunning(false){_name = "thread-" + std::to_string(number++);}bool Detach(){if (_isdetach)return false;if (_isrunning){int n = pthread_detach(_pt);if (n != 0){std::cout << "Detach error:" << strerror(n) << std::endl;return false;}EnableDetach();return true;}return false;}bool Start(){if (!_isrunning){int n = pthread_create(&_pt, nullptr, Routinue, this);if(n != 0){std::cout << "pthread create error:" << strerror(n) << std::endl;return false;}else{std::cout << "pthread create success" << std::endl;return true;}}return false;}bool Stop(){if(_isdetach){std::cout << "线程已分离,无法停止" << std::endl;return false;}if(_isrunning){int n = pthread_cancel(_pt);if(n != 0){std::cout << "pthread cancel error:" << strerror(n) << std::endl;return false;}_isrunning = false;}else std::cout << "线程未运行,无需停止" << std::endl;return true;}bool join(){if(_isdetach){std::cout << "线程已分离,无法等待" << std::endl;return false;}int n = pthread_join(_pt, &res);if(n != 0){std::cout << "pthread join error" << strerror(n) << std::endl;return false;}else{std::cout << "pthread join success" << std::endl;}return true;}private:bool _isrunning;bool _isdetach;std::string _name;pthread_t _pt;fun_c _fc;void* res;};
}
#endif // __Thread_hpp__

        当一个进程下有多个线程时,如果多个线程访问同一个位置的资源的时候,就会引发同步与互斥问题。就像我们在前面使用共享内存的时候,若两个进程同时向共享内存写入时,就必然会因为竞争互斥导致某一方的数据丢失。进程可以通过管道来解决这个问题,因为管道本身就有同步功能。而线程呢?我们下一篇文章再来介绍线程之间的同步与互斥问题。

 

 

 

 

         

         

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/75177.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

dfs记忆化搜索刷题 + 总结

文章目录 记忆化搜索 vs 动态规划斐波那契数题解代码 不同路径题解代码 最长递增子序列题解代码 猜数字大小II题解代码 矩阵中的最长递增路径题解代码 总结 记忆化搜索 vs 动态规划 1. 记忆化搜索&#xff1a;有完全相同的问题/数据保存起来&#xff0c;带有备忘录的递归 2.记忆…

【HTML】验证与调试工具

个人主页&#xff1a;Guiat 归属专栏&#xff1a;HTML CSS JavaScript 文章目录 1. HTML 验证工具概述1.1 验证的重要性1.2 常见 HTML 错误类型 2. W3C 验证服务2.1 W3C Markup Validation Service2.2 使用 W3C 验证器2.3 验证结果解读 3. 浏览器开发者工具3.1 Chrome DevTools…

认识rand, srand, time函数,生成随机数

要完成猜数字游戏&#xff0c;首先要生成随机数&#xff0c;那么该怎么生成随机数&#xff1f;、 1.rand函数 rand函数是库函数&#xff0c;使用的时候要使用头文件stdlib.h c语言中&#xff0c;提供了rand函数来生成随机数&#xff0c;来看一下函数使用&#xff1a; 但是r…

BKA-CNN-GRU、CNN-GRU、GRU、CNN四模型多变量时序预测(Matlab)

BKA-CNN-GRU、CNN-GRU、GRU、CNN四模型多变量时序预测&#xff08;Matlab&#xff09; 目录 BKA-CNN-GRU、CNN-GRU、GRU、CNN四模型多变量时序预测&#xff08;Matlab&#xff09;预测效果基本介绍程序设计参考资料 预测效果 基本介绍 BKA-CNN-GRU、CNN-GRU、GRU、CNN四模型多…

Go语言从零构建SQL数据库引擎(2)

SQL标准与数据库系统实现差异 在上一节中&#xff0c;我们了解了关系型数据库的基础概念。现在&#xff0c;让我们深入探讨SQL语言标准以及不同数据库系统之间的实现差异。 SQL语言的诞生与演进 想象你经营的咖啡店生意蒸蒸日上&#xff0c;需要一个更强大的系统来管理数据。…

智能导诊系统的技术体系组成

智能导诊系统的技术体系由基础支撑技术、核心交互技术、应用场景技术及安全保障技术构成&#xff0c;具体可归纳为以下六个维度&#xff1a; 一、基础支撑技术 1、AI大模型与深度学习 医疗大模型&#xff1a;如腾讯医疗AI、DeepSeek等&#xff0c;通过海量医学文献和病例训…

QML输入控件: TextField(文本框)的样式定制

目录 引言示例简介示例代码与关键点示例1&#xff1a;基础样式定制示例2&#xff1a;添加图标示例3&#xff1a;交互式元素&#xff08;清除按钮&#xff09; 实现要点总结完整工程下载 引言 在Qt Quick应用程序开发中&#xff0c;文本输入是最常见的用户交互方式之一。TextFi…

leetcode hot100 多维动态规划

1️⃣2️⃣ 多维动态规划&#xff08;区间 DP、状态机 DP&#xff09; 62. 不同路径 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图…

3.27学习总结 爬虫+二维数组+Object类常用方法

高精度&#xff1a; 一个很大的整数&#xff0c;以字符串的形式进行接收&#xff0c;并将每一位数存储在数组内&#xff0c;例如100&#xff0c;即存储为[1][0][0]。 p2437蜜蜂路线 每一个的路线数前两个数的路线数相加。 #include <stdio.h> int a[1005][1005]; int …

车载以太网网络测试-26【SOME/IP-通信方式-2】

目录 1 摘要2 Method &#xff08;FF/RR&#xff09;、Event、Filed介绍2.1. SOME/IP Method 接口2.1.1 **Fire & Forget (FF)** - 单向调用2.1.2 **Request/Response (RR)** - 请求/响应模式2.1.3 **车载ECU通信实现示例**:2.1.4 **通信序列示例**2.1.5 实现注意事项 2.2 …

把doi直接插入word中,然后直接生成参考文献

这段代码通过提取、查询、替换DOI&#xff0c;生成参考文献列表来处理Word文档&#xff0c;可按功能模块划分&#xff1a; 导入模块 import re from docx import Document from docx.oxml.ns import qn from habanero import Crossref导入正则表达式模块re用于文本模式匹配&a…

[C++] : C++11 右值引用的理解

&#xff08;一&#xff09;什么是左值和右值&#xff1f; 传统的C语法中就有引用的语法&#xff0c;而C11中新增了的右值引用语法特性&#xff0c;所以从现在开始我们 之前学习的引用就叫做左值引用。无论左值引用还是右值引用&#xff0c;都是给对象取别名。 1.左值 左值是一…

windows服务器切换到linux服务器踩坑点

单节点环境依赖性 单节点问题&#xff0c;影响业务可用性&#xff0c;windows影响后续自动化&#xff0c;健壮性的提升&#xff0c;需要进行linux化 每个服务至少是双节点&#xff0c;防止单点故障&#xff0c;提升系统的可用性&#xff0c;健壮性。linux化后可以进行docker化…

美颜SDK兼容性挑战:如何让美颜滤镜API适配iOS与安卓?

如何让美颜滤镜API同时适配iOS与Android&#xff0c;并确保性能流畅、效果一致&#xff0c;是开发者面临的一大挑战。今天&#xff0c;我将与大家一同深度剖析美颜SDK的跨平台兼容性问题&#xff0c;并分享优化适配方案。 一、美颜SDK兼容性面临的挑战 1.1不同平台的图像处理框…

Vue3 表单

Vue3 表单 随着前端技术的发展,Vue.js 作为一款流行的前端框架,不断更新迭代,以适应更高效、更便捷的开发需求。Vue3 作为 Vue.js 的第三个主要版本,引入了许多新特性和改进,其中包括对表单处理机制的优化。本文将深入探讨 Vue3 表单的使用方法、技巧以及注意事项。 1. …

笔记:代码随想录算法训练营day62:108.冗余连接、109.冗余连接II

学习资料&#xff1a;代码随想录 108. 冗余连接 卡码网题目链接&#xff08;ACM模式&#xff09; 判断是否有环的依据为&#xff0c;利用并查集&#xff0c;isSame函数&#xff0c;判断当下这条边的两个节点入集前是否为同根&#xff0c;如果是的话&#xff0c;该边就是会构…

RK3588,V4l2 读取Gmsl相机, Rga yuv422转换rgb (mmap)

RK3588, 使用V4l2 读取 gmsl 相机,获得yuv422格式图像, 使用 rga 转换 rgb 图像。减少cpu占用率. 内存管理方式采用 mmap… 查看相机信息 v4l2-ctl --all -d /dev/cam0 , 查看自己相机分辨率,输出格式等信息,对应修改后续代码测试… Driver Info:Driver name : rkcif…

Kubernetes》k8s》Containerd 、ctr 、cri、crictl

containerd ctr crictl ctr 是 containerd 的一个客户端工具。 crictl 是 CRI 兼容的容器运行时命令行接口&#xff0c;可以使用它来检查和调试 k8s 节点上的容器运行时和应用程序。 ctr -v 输出的是 containerd 的版本&#xff0c; crictl -v 输出的是当前 k8s 的版本&#x…

Vue 入门到实战 十一 Vuex

目录 11.1状态管理与应用场景 1&#xff09;state 2&#xff09;Getters 3&#xff09;Mutations 4&#xff09;Actions 5&#xff09;Module 11.2Vuex的安装与基本应用 11.3Vuex的核心概念 一句话解释vuex&#xff1a;就是单独成立一个组件&#xff0c;这个组件存储共…

【YOLOv11】目标检测任务-实操过程

目录 一、torch环境安装1.1 创建虚拟环境1.2 启动虚拟环境1.3 安装pytorch1.4 验证cuda是否可用 二、yolo模型推理2.1 下载yolo模型2.2 创建模型推理文件2.3 推理结果保存路径 三、labelimg数据标注3.1 安装labelimg3.2 解决浮点数报错3.3 labelimg UI界面介绍3.4 数据标注案例…