(。・∀・)ノ゙嗨!你好这里是ky233的主页:这里是ky233的主页,欢迎光临~
https://blog.csdn.net/ky233?type=blog
点个关注不迷路⌯'▾'⌯
目录
一、线程概念
1.vm_area_struct
2.页帧与页框
3.重新理解页表
4.如何理解线程
编辑5.重新理解进程
1.在用户和内核视角
2.理解之前写的代码
3.CPU视角
6.所以Linux下有线程吗?
二、pthread.h
1.pthread_create
三、线程如何看待进程内部的资源呢?
四、在调度层面上进程和线程的区别
五、线程的优缺点、异常与用途
1.优点
2.缺点
3.异常
4.用途
一、线程概念
1.vm_area_struct
每一个进程都需要有自己的task_struct和虚拟地址空间然后由页表映射物理内存中的
我们的堆空间看似是一个整体,但是我们每次申请都是一小块一小块的申请的,我们也并没有告诉我们的申请什么时候结束,所以我们还需要有一个叫做vm_area_struct的结构体,用来将我们的堆区开辟的空间串联起来,以便可以细致到找到我们每个申请的区域!我们的OS是可以做到让我们的进程进行资源的细粒度划分的!
2.页帧与页框
首先我们要知道,我们磁盘当中的可执行程序也是文件,本来就是按照地址空间方式进行编译的,并且可执行程序也已经被划分成为了以4KB为单位的一个个小文件!
我们把磁盘当中的,以4KB为单位的内容称之为页帧
同样物理内存也被划分为了以4KB为单位的块,那么如此多的内存块要怎么管理呢?OS是怎么直到已经用了多少的呢?
OS是通过数组的方式进行管理的,我们的每个页也就是内存块,就可以当作数组的下标,所以我们对物理内存的管理就变成了对特定数据结构的管理,我们把物理内存对应的4KB大小称之为页框
所以一般我们IO的基本单位是4KB,所以我们的IO的基本过程就是把页帧装进页框里!
缺页中断:页表当中还包含了一些标志性字段,用来代表我们所使用的代码和数据是否在我们内存当中,如果没在,就在我们的内存的page中,申请对应的大小,再根据页表中所对应的磁盘中的可执行程序的内容,将内容通过文件系统加载到物理内存中,然后将所对应的物理地址,填到页表中!
说白了分为这几步:
- 当操作系统再通过页表进行寻址时,发现所对应的可执行程序不在内存中
- 所以第一步,先在物理空间中申请我们对应的page
- 在磁盘找到我们对应的地址。
- 把目标可执行程序加载到我们的指定内存地址,并重现填写页表
- 返回到用户,让用户继续进行访问
这个过程用户是0感知的,用户不知道这件事情的发生
3.重新理解页表
因为我们的虚拟地址是32位的系统,所以分为3个部分
第一部分前10个比特位所对应的是:一级页表,用这些比特位去索引二级页表
第二部分中10个比特位所对应的是:在二级页表中的key值,然后所对应的val值就是所对应的页框的起始地址
第三部分后12个比特所所对应的是:用来定位具体的地址,可以理解为偏移量
4.如何理解线程
父子进程是子进程复制父进程的task_struct、虚拟地址空间、页表来映射到地址空间,那么线程呢?
如果我们通过一定的技术手段,将当前进程的“资源”,以一定的方式划分给不同的task_struct,这里每一个task_struct都可以叫做线程
线程在进程的地址空间内运行,是OS调度的基本单位,CPU不关心这个是进程还是线程,只关心task_struct,这是Linux特有的方案
5.重新理解进程
1.在用户和内核视角
进程在用户视角:内核数据结构(所有task_struct、虚拟地址空间、页表)+该进程所对应的代码和数据
进程在内核视角:承担分配系统资源的基本实体!(在创建我们的进程的时候,OS给进程分配资源,是以整个进程为单位分配的,并不是按照task_struct的个数来要的,而其中的线程不再向OS要资源,而是直接像进程去要资源,所以在OS看来,这就是承担分配系统资源的基本实体)
所以现在我们理解进程应该是,进程==一批内核数据结构(一个task_struct一个虚拟地址空间一组页表)+该进程所对应的代码和数据+然后还承担分配系统资源,分门别类地交给每一个线程
2.理解之前写的代码
之前我们都是以单进程的方式来写的,内部只有一个执行流,而今天的内部有多个执行流。
所以什么是进程呢?
进程就是一大批的执行流(至少得有一个)+虚拟地址空间+页表等内核数据结构+该进程所对应的代码和数据,整体以基本单位的方式向OS申请相对应的资源。
所以在Linux的视角下:task_struct就是进程内部的一个执行流
3.CPU视角
CPU根本不怎么关心,当前是进程还是线程,只认task_struct,调度的时候直接执行所调度的task_struct的代码和数据,至于代码是否是和别人共享的或者访问哪个地址空间,CPU根本不在乎,只要是能执行就可以。
所以CPU调度的是task_struct结构体,而不是某个进程,CPU的基本调度单位是“线程”!
在Linux下,task_struct<=其他OS内的task_struct的!Linux下的进程可以称之为轻量级进程,因为CPU拿到了有可能是单个执行流的进程也有可能是多个执行流中的一个线程!
6.所以Linux下有线程吗?
Linux下没有正真意义下的线程结构!因为没有对线程设计专门的数据据结构,但是Linux使用进程PCB来模拟线程的!
所以Linux并不能直接给我们提供线程相关的接口,只能提供轻量级进程的接口,所以,Linux给我们提供了一条完整的线程接口,在用户层实现了一套多线程方案,以库的方式提供给用户进行使用:pthread线程库---原生线程库!所以我们只需要调用提供的接口,就可以创建一个线程了!
二、pthread.h
1.pthread_create
用来创建线程的接口
pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*strat_routine)(void *),void *arg)
- 参数一:线程id
- 参数二:线程属性,默认就可以
- 参数三:返回值和参数为void* 的函数指针,传递的这个进程一部分的入口函数
- 参数四:传递给函数指针的参数
- 返回值:成功返回0,失败返回错误码和线程id
在编译的时候注意要引入线程库
g++ -o $@ $^ -lpthread -std=c++11
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
using namespace std;
void *threadRun(void *args)
{
string name = (char *)args;
while (1)
{
cout << name << " pid: " << getpid() << '\n' << endl;
sleep(1);
}
}
int main()
{
pthread_t tid[5];
char name[64];
for (int i = 0; i < 5; i++)
{
snprintf(name, sizeof(name), "%s-%d", "thread",i);
pthread_create(tid + i, nullptr, threadRun, (void *)name);
// 缓解传参bug
sleep(1);
}
while (1)
{
cout << "我是主线程" << getpid() << endl;
sleep(3);
}
return 0;
}
我们可以发现,他们的pid都一样,所以证明了线程在进程中运行!
同时,我们可以看到只有一个进程,但是却有6个执行流
我们也可以发现对应的LWP就是轻量级执行流的意思,所对应的编号也是不一样的,第一个线程的LWP和PID是一样的,所以它叫做主线程,操作系统调度的是LWP!
所以kill如果直接杀掉主线程,这个进程也就直接回收了,所以这个线程的所有子线程也全部回收了!
三、线程如何看待进程内部的资源呢?
进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
在我们的虚拟地址空间中栈结构只有一个,而栈又是每个进程私有的,那该怎么分呢?
这是因为在我们所调用pthread线程库中也要给我们提供一个用户层的栈结构!
四、在调度层面上进程和线程的区别
为什么线程切换的成本更低呢?
这是因为地址空间和页表不需要切换,而切换进程需要切换
CPU中是由一个了L1-L3的cache缓存的,就是CPU在进行寻址的时候把这条指令Load到内存里,这样每Load一次就要去访问物理空间一次,这样会大大拖慢CPU的速度,所以一次性会Load多条指令缓存CPU内部,说白了就是会预读一些代码!
可是如果切换进程,cache就直接失效了,只能来重新缓存!
五、线程的优缺点、异常与用途
1.优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
但是并不是我们创建线程越多越好,如果有太多的线程,反而变成了切换的成本在进行计算密集型的时候,会频繁的切换,或者IO密集型也会有相同的问题,注意这里是不要过多!
2.缺点
- 性能损失
- 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型 线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低
- 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 缺乏访问控制
- 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编程难度提高
- 编写与调试一个多线程程序比单线程程序困难得多
3.异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该 进程内的所有线程也就随即退出
4.用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)