1.理解地址空间和页表
1.地址空间是进程能够看到的资源窗口
2.页表决定进程真正拥有的资源情况
3.合理的对地址空间和页表进行资源划分就可以对一个进程的所有资源进行划分:过地址空间分为栈区、堆区…通过页表映射到不同的物理内存。
在32位平台下,一共有2^32个地址,也就意味着有2^32个地址需要被映射。地址空间一共有2的32次方个地址,每个地址单位都是1字节,OS是如何做到从虚拟地址到物理地址做转换的呢?
其中页目录项是一级页表,页表项是二级页表。虚拟地址转成物理地址:32位的虚拟地址以10,10,12的二进制构成,页表不止一张,页目录:页目录中存放的是页表的地址,根据虚拟地址的前十位确定所需页表的起始地址,页表:每一个页表的条目项为2的10次方个,页表中存的是页框的起始物理地址,根据中间的十位确定页框的起始位置,剩下的12位虚拟地址是偏移量,根据页框的起始位置开始往下偏移这个偏移量就找到了对应的物理内存。OS中把物理内存一块块的数据框称为页框,磁盘上编译形成可执行程序的时候,也被划分成一个个4KB的区域称为页帧。当内存和磁盘进行数据交换时也就是以4KB大小为单位进行加载和保存的,这个偏移量刚好与页框的大小是等价的(4KB等于2的12次方字节)。
2.线程概念
在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列 ”。一切进程至少都有一个执行线程;线程在进程内部运行,本质是在进程地址空间内运行在Linux系统中CPU看到的PCB都要比传统的进程更加轻量化。透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
1.线程在进程的内部运行线程在进程的地址空间内部运行,拥有该进程的一部分资源,我们可以通过地址空间+页表的方式对进程进行资源划分。 2.站在CPU的角度,每一个PCB都可以叫做轻量级进程3.Linux线程是CPU调度的基本单位,进程是承担系统资源分配的基本单位,进程是申请资源,线程是向进程要资源4.Linux中没有真正意义上的线程,是由PCB模拟实现的。OS无法直接创建线程,而是只提供创建轻量级进程的窗口。5.PCB模拟线程,为PCB编写的结构与算法都能进行复用,不用单独为线程创建调度算法,这样做的好处是维护成本低,可靠性高。
3.创建线程
因为OS没有创建线程的接口,只能通过第三方的库<pthread.h>提供创建建轻量级进程的接口
因为只用的是第三方的库,所以在编译的时候要指明链接的库
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
using namespace std;
void* start_routine(void* args)
{
string name=static_cast<const char*>(args); //安全的类型转换
while(true)
{
cout<<"i am new thread, name: "<<name<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,start_routine,(void*)"new thread");
//线程id 新线程属性 线程执行的函数 传给线程执行函数的参数
while(true)
{
cout<<"我是主线程......"<<endl;
sleep(1);
}
return 0;
}
结果可以看到主线程和新线程是交替执行的,CPU是以什么为标识符来调度这两个执行流的呢?
ps -aL //查看轻量级进程
PID为9764的两个轻量级进程是进程的ID 与进程ID相同的LWP是主线程的ID,LWP为9765的轻量级进程是我们创建出来的新线程的ID。----->CPU调度时是以LWP为标识符的!
一个进程创建了线程,几乎所有的资源都是线程共享的,但是线程也是有自己的私有内部属性
什么资源是线程私有的呢?
1.PCB的属性是私有的
2.私有的上下文结构 //每个线程都是要被调度和切换的,如果时间片内线程的代码没有跑完,此时线程的上下文就需要被保存
3.每个线程都有自己独立的栈结构 //线程内部的局部变量需要被保存,保存在每个线程独立的栈结构里
总结之前的知识点:
1.线程是进程内部的执行流
2.进程承担分配系统的资源,创建线程知识创建了PCB
3.一个进程内部至少有一个执行流
4.CPU看到的PCB都是轻量级进程
4.线程的优缺点
优点
1.创建一个新线程的代价要比创建一个新进程小得多
2.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多(进程间切换,需要切换页表、虚拟空间、切换PCB、切换上下文,而线程间切换,页表和虚拟地址空间就不需要切换了,只需要切换PCB和上下文,成本较低)
3.线程占用的资源要比进程少很多
4.能充分利用多处理器的可并行数量
5.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6.计算密集型应用(CPU,加密,解密,算法等),为了能在多处理器系统上运行,将计算分解到多个线程中实现
7.I/O密集型应用(外设,访问磁盘,显示器,网络),为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
缺点
性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高:编写与调试一个多线程程序比单线程程序困难得多
5.线程终止
1.线程调用的函数return了,这个线程就终止了
2.在线程的内部调用pthread_exit(nullptr),线程就终止了。
3.exit(0)不能终止线程是用来终止进程的,任何一个执行流调用exit(0)这个进程就终止了,进程终止了线程也就都退出了。
6.线程等待
线程也是需要被等待的,如果不等待就会造成类似僵尸进程的问题----内存泄漏
等待是为了:1.回收新线程对应的PCB等内核资源,防止内存泄漏2.回收线程对应的退出信息
void* start_routine(void* args)
{
string name=static_cast<const char*>(args);
int cnt=3;
while(cnt--)
{
cout<<"i am new thread, name: "<<name<<endl;
sleep(1);
}
return (void*)10086;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,start_routine,(void*)"new thread");
int c=4;
while(c--)
{
cout<<"我是主线程......"<<endl;
sleep(1);
}
void* ret=nullptr;
pthread_join(tid,&ret);
cout<<"main join success exit number:"<<(long long)ret<<endl;
return 0;
}
void** retval:输出型参数,主要用来获取线程函数结束时返回的退出结果。之所以是void**,是因为如果想作为输出型结果返回,因为线程函数的返回结果是void*,而要把结果带出去就必须是void**
没有看到线程退出时对应的退出信号:这是因为线程出异常收到信号,整个进程都会退出,所以退出信号要由进程来关心,所以pthread_join默认会认为函数会调用成功,不考虑异常问题,异常问题是进程该考虑的问题
7.线程分离
线程是可以等待的,等待的时候,是join的等待的,阻塞式等待。而如果线程我们不想等待:不要等待,该去进行分离线程处理。
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成内存泄漏
而如果我们不关心线程的返回值,join是一种负担,这个时候我们可以告诉OS,当线程退出时,自动释放线程资源,这种策略就是线程分离。
int pthread_detach(pthread_t thread);
线程分离函数可以由新线称和主线程来调用,但是这里推荐分离操作由主线程来完成因为创建线程后主线程和新线程不确定谁先运行,所以可能会有这样的场景,当我们创建主线程之后,还没有执行新线程的pthread_detach,而主线程直接去等待了,也就是新线程还没来得及分离自己,也就是分离的太慢了,最后主线程直接去等待了。
void* start_routine(void* args)
{
//pthread_detach(pthread_self()); 线程得到自己的tid然后做分离
string name=static_cast<const char*>(args);
int cnt=3;
while(cnt--)
{
cout<<"i am new thread, name: "<<name<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,start_routine,(void*)"new thread");
// sleep(2);
pthread_detach(tid);
int c=4;
while(c--)
{
cout<<"我是主线程......"<<endl;
sleep(1);
}
return 0;
}
8.理解线程id和地址空间的关系
pthread_create函数会产生一个线程id,存放在第一个参数指向的地址中
<pthread.h>库帮助我们建立线程,这个库可以叫做原生线程库,当我们创建一个新的线程后,在pthread库中就创建了一个关于该线程属性集合的对象,同时创建一个轻量级进程。pthread_t类型的线程ID,本质 就是一个进程地址空间上的一个地址。通过这个地址就可以访问到对应轻量级进程的结构体,结构体中包含线程的一些私有属性。
Linux用户级线程:内核级轻量级进程 = 1:1