文章目录
- 一、什么是线程
- 二、创建线程
一、什么是线程
线程在进程内部执行,是OS调度的基本单位。
在堆区上存在下面一种数据结构
struct vm_area_struct{
//用来记录这块空间的起始和终止。
unsigned long vm_start;
unsigned long vm_end;
//其实这是一个双向链表中的结点,用具记录前后的空间节点
struct vm_ares_struct *vm_next ,*vm_prev;
}
如果我们的堆区申请了比较多的空间,然后我们的vm_area_struct就是用来记录
我们每一小块的地址空间的起始和结束。
然后这些小的内存块就通过双向链表的形式串联起来。
所以说,OS是可以做到让进程进行细粒度的划分的
用户级页表+MMU(是集成在CPU当中的)
我们如何从虚拟地址映射到物理地址?
1.exe就是一个文件
2.我们的可执行程序本来就是按照地址空间方式进行编译的
3.可执行程序,其实按照区域也已经被划分成了以4kb为单位的空间。
我们如何管理这里的每一个4kb的空间呢?
我们需要先描述,再组织,也就是用struct page结构体来进行描述
struct page
{
int flag;
}
内核想要管理这么多物理内存,我们就需要创建一个数组struct page mem[100w+]
然后操作系统想要管理对应的物理内存的时候,就可以通过这一个数组进行管理。
所以操作系统对于物理内存的管理,就变成了对于对应的数据结构的管理。
磁盘中的可执行文件是按照4kb划分的,我们的物理内存也是按照4kb划分的,其中我们将磁盘当中以4kb为单位的,我们的代码的数据的内容,称之为页帧
我们物理内存这里的4kb大小称之为页框
IO的基本单位是4kb,IO就是将页帧装进页框里
缺页中断:如果我们的操作系统在寻值得时候,发现对应的数据不在我们的内存中,我们就需要去磁盘中读取对应的数据到我们的内存中,然后通过页表映射,获取到我们的数据。
我们的虚拟地址有232个(4GB,页表是保存在物理内存当中的),也就是说如果想要保存我们的一整张页表的话,需要的大小为页表的条目的大小×4GB,这样空间占用就会非常大。
但是我们可以按照下图建立一级页表和二级页表,来简化我们的索引。
如何理解线程:
通过我们创建了多个task_strcu纸箱同一个mm_struct,通过一定的技术手段,
将当前进程的“资源”,以一定的方式划分给不同的task_struct
也就是说我们再创建task_struct的时候,不再去开辟新的资源了。
我们就将这里的每一个task_struct就称为线程。
什么是线程在进程内部执行?
线程在进程的地址空间内进行运行。
为什么线程是0S调度的基本单位?
因为cpu并不关心执行流是线程还是进程,只关心pcb。
这只是Linux下的维护方案,没有为线程设计专门的数据结构。
但只要比进程更轻量,粒度更轻,就是线程。
windows有为线程设计专门的数据结构。
什么是进程(资源角度)
进程就是我们对应的内核数据结构,再加上该进程所对应的代码和数据。
一个进程可能会有多个PCB。
在内核的时间,进程是承担系统分配资源的基本实体。
所以我们创建线程的时候,只有第一个需要申请资源,也就是我们上面图中红框的那一个task_struct,也就是一个进程,后面所创建的线程不是想操作系统索要资源,而是向我们的进程共享了资源。
如何理解我们曾经我们所写的所有的代码?
内部只有一个执行流的进程。
我们现在就可以创建内部具有多个执行流的进程。
我们的task_struct仅仅是我们的进程内部的一个执行流。
在CPU的视角,CPU其实不怎么关心当前是进程还是线程这样的概念,只人stask_struct。
我们的CPU的调度其实调度的是stack_struct
在Linux下,PCB<=其他操作系统的PCB的
Linux下的进程:统一称之为轻量级进程。
当CPU拿到一个PCB的时候,可以是单执行流的进程的PCB,也可可能是多执行流的其中一个线程的PCB,所以比那些别的操作系统单独给线程和进程设计的数据结构更加轻量
所以Linux没有真正意义上的线程结构,Linux上是用进程PCB模拟线程的。
所以Linux并不能直接给我们提供线程相关的接口,只能提供轻量级进程的接口(在用户层实现了一套用户层多线程方案,以库的方式提供给其他用户进行使用,pthread线程库–原生线程库)。
线程如何看待进程内部的资源呢?
原则向线程能够看到进程的所有资源,在进程的上下文中进行操作。
进程 vs 线程
调度层面:上下文(调度一个线程的成本比调度进程的成本更低)
二、创建线程
我们还需要在我们的makefile中添加-lpthead选项
mythread:mythread.cc
g++ -o mythread mythread.cc -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f signal
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<unistd.h>
using namespace std;
void *threadRun(void *args)
{
const string name=(char*)args;
while(true)
{
//如果线程属于进程的话,我们这里获得的pid应该和我们的线程是同一个pid
cout<<name<<", pid: "<<getpid()<<endl;
cout<<endl;
sleep(1);
}
}
int main()
{
//无符号长整数类型
pthread_t tid[5];
char name[64];
//循环创建5个线程
for(int i=0;i<5;i++)
{
//格式化我们线程的名字
snprintf(name,sizeof name,"%s-%d","thread",i);
pthread_create(tid+i,nullptr,threadRun,(void *)name);
sleep(1);//缓解传参的bug
}
//我们的主线程在执行完上面的代码之后就会执行下面的代码。
while(true)
{
cout<<"main thread, pid: "<<getpid()<<endl;
sleep(3);
}
}
我们这里看我们的程序已经链接上了我们的pthread_create库
但是我们再这里只能查看到到一个进程,我们如何查看到这个进程里面的线程呢?
编写监控脚本
ps -aL |head |head -1 && ps -aL| grep mythread
我们Linux内部所看的一定是LWP,不是看的PID。
如果只是单线程的话,这个进程的PID和LWP是相同的。
我们这里只要将我们的进程终止了,我们所有的线程都会终止。
因为我们现成的所以资源都是来自于我们的进程的,没有了代码和数据,当然会退出。
线程的共享资源
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id
堆区可以被共享
共享区也是被所有线程共享的
栈区也是可以共享的,但我们一般不这么做。
线程的私有资源
线程ID
一组寄存器(线程的上下文)
栈
errno
信号屏蔽字
调度优先级
进程和线程切换,我们为什么说线程的切换成本更低?
如果我们调度的一个进程内的若干个线程,我们的地址空间不需要切换,页表也不需要切换
如果是进程切换的话,地址空间,页表等等都需要切换。
并且我们的CPU内是有硬件级别的缓存的(cache)(L1-L3)
我们只要将相关的数据load到我们CPU内部的缓存,对内存的代码和数据,根据局部性原理
(一条指令如果被使用了,它附近的代码也有很大的可能被使用),预读取到我们的CPU的缓存中,
这样我们的CPU就不需要访问内存,直接到缓存中访问就可以了。
但是如果进程切换,那么我们的cache立即失效,新进程过来的时侯,只能重新缓存。
所以我们的线程切换比我们的进程切换更加轻量化。