Linux多线程(上)
🌟🌟hello,各位读者大大们你们好呀🌟🌟
🚀🚀系列专栏:【Linux的学习】
📝📝本篇内容:多线程基本概念;页表;线程的优缺点;线程使用控制;理解线程库;命令
⬆⬆⬆⬆上一篇:Qt的入门
💖💖作者简介:轩情吖,请多多指教(>> •̀֊•́ ) ̖́-
1.基本概念
①线程是一个执行分支,执行力度比进程更细,调度成本更低
②线程是进程内部的一个执行流
③对于内核来说,线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体
④Linux内核中,是复用PCB(进程控制块)模拟线程的TCB
⑤Linux没有真正意义上的线程,而是用进程方案来模拟线程
⑥进程是包含内部所有的task_struct,虚拟地址空间,页表,以及内存中的代码数据
⑦对于CPU来讲,它并不能区分轻量级进程(线程在Linux下的叫法)还是单独的task_struct(传统的进程)
⑧对于OS来讲,它能通过pid来区别进程,用LWP来区分线程
⑨当CPU要执行线程时,不需要将进程的上下文以及虚拟地址空间、页表进行更换,并且有cache的局部性原理,不需要更换数据,我们的寄存器分为可见和不可见
⑩CPU的内部包含运算器,控制器,寄存器,MMU,硬件cacheL1,L2,L3
2.页表
虚拟地址的基本单元是字节,虚拟地址空间上有2^32个地址,就是4G
在物理内存和磁盘交涉时,如果有高频的IO工作,会变的非常慢
OS和磁盘这样的设备进行交互的时候,都是按照块为单位的
在我们的文件系统中,我们一下子读取4KB(8个扇区),也可以对OS修改
因此在我们的文件系统中,文件在磁盘上都是以块为单位的4KB
对操作系统和内存而言,内存实际在进行内存管理的时候,也是要以4KB为单位的
上图整个称之为页表
并且我们的操作系统也会维护一个结构体
struct page
{
int status;//可以设置很多状态
//属性非常少
};
struct pagemem[1048576];
我们的虚拟地址不是整体被使用的,而是通过10+10+12比特位来进行划分的
因此我们定位一个内存字节位置:页框+页内偏移;这其实和我们的指针找变量一样,变量的起始地址+类型(偏移量)→基地址+偏移量,况且我们的页表不会全部创建,这样就能保证不浪费空间
我们的IO基本单位(内核内存+文件系统)都要提供支持4KB,同时4KB也能提高局部性原理,预测未来的命中情况,提高效率
局部性原理的特性:现代计算机预加载的理论基础,允许我们提前加载正在访问数据的相邻或附近的数据,因此预先加载要访问数据的附近的数据来减少未来的IO次数,都加载进来的数据本质就是数据的预加载
在我们实际申请malloc内存的时候,OS只要给你在虚拟地址空间上申请就可以了,当你在真正访问时,OS才会给你申请或者填充页表+申请具体的物理内存(MMU触发页表中断,当进程试图访问一个尚未映射到物理内存中的虚拟内存页面时,MMU会检测到这种情况并触发缺页中断)
像我们的字符串常量是不允许被修改的,只允许被读取是因为字符指针里面保存的是指向字符的虚拟起始地址,当*寻址的时候,必定会伴随着虚拟地址到物理地址的转化,MMU+查页表,对你的操作进行权限审查,发现z虽然能够找到对应的数据,但是执行的操作是非法的,MMU因此会发生异常,此时OS会识别到异常,并且发送信号给目标进程,再从内核转换成用户态的时候,进行信号处理,终止进程
3.线程的优点
①创建一个线程的代表要比一个新的进程小的多
②与进程之间的切换相比,线程之间的切换需要OS做的工作要少很多
③线程占用的资源比进程少很多
④能充分利用多处理器的可并行数量
⑤在等待慢速IO操作结束的同时,程序可以执行其他的计算任务
⑥计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个进程中
⑦IO密集型应用,为了提高性能,将IO操作重叠,线程可以同时等待不同的IO操作
4.线程的缺点
性能损失:增加了额外的同步和调度开销
健壮性降低:线程间缺乏保护
缺乏访问控制
同时多线程程序中任何一个线程崩溃了,最后会导致进程崩溃
站在系统的角度上来说,线程是进程执行的分支,线程崩了,就是进程问题;站在信号的角度来说,页表在转换的时候,MMU识别写入权限,没有验证通过,导致MMU异常,OS识别,因此给进程发信号,信号是以进程为主的。因为执行流看到的资源是通过地址空间看到的,多个LWP看到的是同一个地址空间,所以所有的线程可能会共享进程的大部分资源
5.线程使用控制
我们的进程是资源分配的基本单位,而线程又是调度的基本单位,线程会共享进程的一部分数据,但也会有自己的一部分数据:线程ID,一组寄存器(保存上下文数据),栈,errno,信号屏蔽字,调度优先级
在我们的OS的视角下:Linux下没有真正意义的线程,而是用进程模拟的线程(LWP),所以Linux不会提供直接创建线程的系统调用,它会最多提供轻量级进程的创建接口
用户线程库:对下将Linux接口封装,对上给用户提供进行线程控制的接口pthread库,任何系统都要自带,原生线程库
5.1创建线程
参数:
thread:线程ID
attr:线程属性,attr为NULL,表示默认
start_routine:线程启动要执行的函数
arg:传给线程启动函数的参数
pthread函数出错时不会设置errno,而是将错误代码通过返回值返回
# makefile
main:main.cc
g++ -o main main.cc -lpthread
.PHONY:clean
clean:
rm -rf main
#include <pthread.h>
#include <iostream>
#include <functional>
#include <memory>
#include <unistd.h>
#include <string>
using namespace std;
using func_t=function<string(string)>;//typedef类型重定义
void* Func1(void* arg)
{
const char* str=static_cast<const char*>(arg);//强转一下arg
while(1)
{
cout<<str<<endl;
sleep(1);
}
}
void* Func2(void* arg)
{
while(1)
{
const char* str=static_cast<const char*>(arg);
cout<<str<<endl;
sleep(1);
}
}
int main()
{
char* str1="hello,I am p1";
char* str2="hello,I am p2";
pthread_t p1,p2;//线程名
pthread_create(&p1,nullptr,Func1,str1);//调用Func1
pthread_create(&p2,nullptr,Func2,str2);//调用Func2
while(1)
{
cout<<"I am a main thread"<<endl;//主线程依旧往后运行
sleep(1);
}
return 0;
}
运行结果:
5.2线程终止
线程终止可以通过函数return也可以调用上面的函数,pthread_exit和return返回的指针所指向的内存单元必须是全局或malloc分配的不能在线程函数的栈上分配,因为当其他线程拿到数据时,线程函数已经退出了
5.3 线程取消
取消一个执行中的线程;成功返回0,失败返回错误码
5.4线程等待
已经退出的线程,其空间并没有被释放,仍然在进程的地址空间内,创建新的线程不会复用刚才退出线程的地址空间
返回值:成功返回0,失败返回错误码
调用该函数的线程运行到此函数时将挂起等待,知道ID为thread的线程终止
①如果thread通过return返回,retval所指向的单元里存放的是thread线程的返回值;
②如果thread线程被别的线程调用pthread_cancel异常终止的,retval所指向的单元里存放的是PTHREAD_CANCELED;
③如果thread线程是自己调用pthread_exit终止的,retval所指向的单元内存放的是传给pthread_exit的参数
#include <pthread.h>
#include <iostream>
#include <functional>
#include <memory>
#include <unistd.h>
#include <string>
using namespace std;
using func_t=function<string(string)>;//typedef类型重定义
void* Func2(void* arg)
{
int count=5;
while(count--)
{
const char* str=static_cast<const char*>(arg);
cout<<str<<endl;
sleep(1);
}
}
int main()
{
char* str2="hello,I am p2";
pthread_t p1,p2;//线程名
pthread_create(&p2,nullptr,Func2,str2);
cout<<"It is the time to wait p2"<<endl;
pthread_join(p2,nullptr);//等待p2线程结束
cout<<"join end!!!"<<endl;//只有等等待完p2才会执行
return 0;
}
5.5获得线程的ID
#include <pthread.h>
#include <iostream>
#include <functional>
#include <memory>
#include <unistd.h>
#include <string>
using namespace std;
int main()
{
cout<<pthread_self()<<endl;
return 0;
}
5.6分离线程
默认情况下,新创建的线程是jionable,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成内存泄露;但是如果我们不关心线程的返回值,jion只是一种负担,这个时候我们可以告诉系统,当线程退出的时候,自动释放线程资源
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离;
pthread_detach(pthread_self());//线程自己分离
jionable和分离是冲突的,一个线程不能即是jionable又是分离的
6.理解线程库
因此我们进程中线程可以随时访问库中的代码和数据
在库中也会创建类似的管理线程的TCB,里面的操作函数也是调用底层的系统函数
对于pthread_t类型的线程ID本质就是一个进程地址空间上的一个地址
所有的线程都要有自己独立的栈空间
主线程用的是进程系统栈,新线程用的是库中提供的栈,可以通过更改ebp,esp就能线程的栈
7.命令
我们可以通过上面的命令来查看我们的线程
🌸🌸Liunx多线程(上)的知识大概就讲到这里啦,博主后续会继续更新更多Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪