💓博主CSDN主页:麻辣韭菜💓
⏩专栏分类:Linux知识分享⏪
🚚代码仓库:Linux代码练习🚚
🌹关注我🫵带你学习更多Linux知识
🔝
目录
前言
一段代码经过编译器编译,形成可执行二进制文件,二进制文件运行,形成进程。如果一个进程的模块很多。好几个模块同时运行又是什么?答案线程。如果一个进程只有一个执行流程,那这些模块要运行起来是不是效率就会很低?
1.Linux中线程该如何理解
教材:是进程内的一个执行分支,线程的执行粒度,要比进程更细
如何理解上面这段话?
要理解线程首先我们要先从进程开始,回顾我们以前最先开始学习进程的概念。 每个进程都会有自己的地址空间,PCB 。
通过页表映射到真实的物理内存中。
假设多个PCB同时指向同一个地址空间会是什么?看下图
这在以前进程学习中,绝对是超认知的。其实上面的整个图 在Linux中才是真正的进程,为什么会这么说?首先我们要知道CPU只有调度执行流的概念。也就是说你所谓的进程被调度其实是进程内的一部分代码被执行而已。是整个代码被执行了吗?并没有!!!就好比我们一个可知执行程序里面写了好几个函数。这些函数在main函数中依次被执行调用。请问这些函数能同时运行吗?答案是:不能同时执行。
那有人说想要同时执行,把这个代码拆分成好几个工程然后同时编译执行不就可以了吗?首先一个进程被创建要有什么?
地址空间 页表 pcb 就像上面的图。如果是个4个pcb那就会有4个页表4个地址空间。拜托OS也很忙的。这样频繁的调度就会显得效率低下。进程切换是要写时拷贝上下文的。走的时候拷贝,自己带走,再次被调度又要拷贝回寄存器。 那如果是上图的那种情况我们是不是就能大大的提供效率。
所以为了效率,就有了线程。那Linux中线程又是什么?
先说Linux的线程实现方案:
1.在Linux中,线程在进程"内部执行"执行,线程在进程的地址空间内运行。
为什么??
因为任何执行流要执行,都是要资源的,地址空间就是进程的资源窗口。
2.在Linux中,线程的执行粒度都要比进程要细!
Linux中是没有线程的概念,而是用进程模拟的线程。
为什么?
想一想进程就有一大坨的地址空间,页表,内核数据结构。那线程和进程又是惊人相似,为什么不能复用进程的代码?不复用,又搞一坨出来。增加OS的维护成本。所以说Linux是一款卓越的操作系统。 但是还真有其他系统是这么干的。windows
问题来了?
进程有内核数据结构,一个进程和线程的比例是1:n 那么操作系统要不要管理线程?
当然要管理 struct tcb // thread ctrl block
所以Linux中的执行流是,轻量级的进程
线程 <= 执行流 <= 进程
2.重新定义线程和进程
什么叫线程? 我们认为:线程操作系统调度的基本单位!
重新理解进程: 内核观点:进程是承担分配系统资源的基本实体。
所以可以得出一个结论:线程就是我进程内部执行流资源!
进程: 进程 = 内核数据结构 + 代码和数据 +执行流(线程)
3.再谈地址空间
既然进程是承担分配系统资源的基本实体,地址空间又是进程的资源窗口,那地址空间的资源又是如何分配给线程的?
线程资源的分配本质是分配地址空间范围
那如何分配地址空间的范围?
地址空间是虚拟地址,那虚拟地址是如何转化成物理地址?
下面我以32位虚拟地址为例
所以即使是每个 物理地址 都被寻址的的极端情况下,页表 总大小不过为:(2^10 + 2^10) * (2^10 + 2^20)
,大约也就需要 4Mb
大小
即可映射至每一个 物理内存,但实际上 物理内存 并不会被时刻占满,大多数情况下都是使用一部分,因此实际 页表 大小不过 几十字节
像这种 页框起始地址+偏移量 的方式称为 基地址+偏移量,是一种运用十分广泛的思想,比如所谓的 类型(int、double、char…)都是通过 类型的起始地址+类型的大小 来标识该变量大小的,也就是说我们只需要 获得变量的起始地址,即可自由进行偏移操作(如果偏移过度了,就是越界),这也就解释了为什么取地址只会取到 起始地址
4.线程周边知识
为什么是说线程比进程更轻量化?
因为线程内的切换不需要重新cache数据
而是缓存热数据
热数据是指那些在程序运行过程中被CPU频繁访问的数据。这些数据如果存储在缓存中,就可以快速被CPU访问,从而提高程序的执行速度。
那既然是轻量级进程概念,那就说明OS不会给用户提供直接的线程系统调用接口。只会给我们提供轻量级进程系统调用接口。
pthread库
轻量级进程接口进行封装,为用户提供直接线程接口。
函数原型
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
这个函数用于在调用进程中启动一个新的线程。它需要以下参数:
thread
:指向pthread_t
类型的指针,用于存储新创建线程的ID。attr
:指向pthread_attr_t
结构的指针,该结构定义了新线程的属性。如果为NULL
,则使用默认属性。start_routine
:线程启动后执行的函数,其原型为void *(*start_routine)(void *)
。arg
:传递给start_routine
函数的参数。
编译链接
使用 -pthread
选项编译和链接你的程序,以确保链接了线程库。
描述
pthread_create()
函数启动一个新线程,该线程通过调用 start_routine()
开始执行,并将 arg
作为 start_routine()
的唯一参数。
代码示例
#include <iostream>
#include <pthread.h>
#include <unistd.h>
void *threadRun(void* args)
{
while(1)
{
std::cout << "new thread: " << getpid() << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, nullptr);
while(1)
{
std::cout << "main thread: " << getpid() << std::endl;
sleep(1);
}
}
这以前是不可能同时运行两个循环体的。
我们可以看到两个线程的的pid都是同一个,说明它们属于同一个进程。LWP // light weight process
第一个LWP和pid一样这就是主线程。也就是我们之前的讲的进程。只有一个执行流
第二个9266 新线程
既然线程的概念是库提供的,线程库注定了要维护这些多个线程属性集合,那线程库要不要管理这些线程?
肯定要管理。 老规矩 先描述 再组织!!!
除了主线程,所有其他线程都有独立的栈,都在共享区,具体来讲是在pthread库中,tid指向的tcb中!
为什么这么说因为再调用pthread_create 这个函数还会调一个函数
那我们用的原生库需不需要加载到内存中?
当然要啊
这不就和动态库一个道理吗?