一、虚拟地址空间的再谈
物理内存和磁盘的关系
在文件系统中我们也是以一个个块写入或读出数据的,那这样的话看来,这种的设计是很方便我们做内存管理的
页表的进一步认识
如图这差不多是页表的完整面目
CPU和虚拟地址和页表和物理内存的关系
32 位二级页表:页目录占 4KB,最多可以指向 1024 个页表,每个页表 4KB。所有页表全部创建最多占用 4MB。
但 OS 采用懒分配,不会一次性分配全部页表,实际进程的页表占用远小于 4MB。
CPU 执行指令给出虚拟地址,交给硬件 MMU,MMU 通过 CR3 找到当前进程的页目录,查询页表完成虚拟→物理地址翻译。
若访问的虚拟页面还没有加载到物理内存,触发缺页异常,内核介入,把磁盘上的数据读入物理页,更新页表,之后程序继续运行。
二、线程的概念
有了上面的知识铺垫,线程的宏观实现,我们就能讲清楚了
在Linux中: 线程就是一个个PCB结构体,它们是共享进程的虚拟地址空间和页表
所以严格意义来说,线程在Linux上是叫做轻量级进程(LWP),它没有独立的TCB结构来单独描述线程,它是复用进程的相关代码,从而节省了需要重新设计调度算法、结构和属性等等
LInux 在内核只提供轻量级进程,为了给用户提供线程编程模型,在用户态封装了 pthread 库,底层通过clone系统调用创建 LWP。
补充:clone的 flags 参数,决定创建出来是普通进程还是轻量级进程。
-不共享地址空间 → 普通进程(fork 底层就是 clone)
-指定CLONE_VM等标记,共享虚拟地址空间 → 轻量级进程(线程)
所以我们现在重谈进程和线程的关系
我们以前所学的进程,其实是一个单线程进程,就是一个执行流,那现在多线程也就是有多个执行流,每个执行流使用对应一部分资源
• 进程是资源分配的基本单位
• 线程是调度的基本单位
•线程共享进程数据,但也拥有⾃⼰的⼀部分"私有"数据: 线程ID、 ⼀组寄存器、线程的上下⽂数据、栈。
三、线程控制
线程创建
我们使用的接口都是原生线程库,
pthread_create();
第一个:参数:类似于pid,(其实实际是线程控制块的起始地址,后面详细讲),无符号长整型
第二个: 设置线程的属性,一般默认就行
第三个:线程函数,也就是回调函数,将来线程执行任务的入口,
第四个:线程函数的参数,因为是void*,所以不要局限于传内置类型,类类型、函数指针等都可以
void*Work(void*arg){while(true){printf("我是new pthread pid:%d tid:0x%lx\n",getpid(),pthread_self());}returnnullptr;}intmain(){id=syscall(SYS_gettid);intthread_num=1;pthread_create(&tid,nullptr,Work,nullptr);}while(true){printf("我是main pthread pid:%d tid:0x%lx\n",getpid(),pthread_self());sleep(1);}return0;pthread_self()函数就是拿到线程的tid,运行就能发现两个打印消息
线程终止和线程等待
终止的方式:
1、return 结束
2、pthread_exit()结束
3、一个线程可以调⽤pthread_cancel终⽌同⼀进程中的另⼀个线程。
注意:主线程也有自己的tid,如果你强行把主线程退出的话可以,但是子线程不会退出,这样就会出现bug,所以这个函数只建议终止子线程
线程等待
等待原因跟进程等待的原因类似,你可以认为会产生类似于僵尸线程的情况(实际是用户层的线程库中的线程控制块需要回收)
函数:pthread_join
第一个参数:tid
第二个参数: 线程函数的返回值(实际线程函数的返回值已经在struct thread中),也就是退出信息
看了下面的线程ID和进程地址空间布局,再来重说等待的流程
首先当线程函数退出时,它会先把退出信息先传入到 该线程的struct thread结构体中先保存,然后再在里面触发进程等待wait()先回收LWP,然后如果用户自己写了pthread_join时,它会把struct thread的退出信息给retval参数,然后最后再回收线程库中的申请的线程控制块
线程分离
它的作用就是:如果不关心线程退出时的返回值,就可以使用它,系统会帮你回收资源
可以是线程组内其他线程对⽬标线程进⾏分离,也可以是线程⾃⼰分离:
线程ID和进程地址空间布局
pthread_t 到底是什么类型呢?pthread_t类型的线程ID,本质就是⼀个进程地址空间上的⼀个地址。
mmap区域就相当于共享区
我们通过动态库的知识能知道,pthread_create是在库中完成的也就是在库中它会帮你申请好struct pthread然后在里面的把struct pthread起始地址赋值给你传的tid,而且我们知道线程的栈也是私有的,所以就如图所示,它会在动态库中申请线程控制块线程局部存储:我们知道在全局域中的资源是共享的,一个线程改变了资源另外的线程使用资源是改变后的,我们如果要使这部分资源私有化(只限于内置整数类型,不包括字符串,类类型), 可以__thread 修饰全局变量,在各个线程内部使用它时,它会新建一份到各自控制块的局部存储区,也就是同名不同虚拟地址,比如 __thread int cnt = 0;,在各个线程改变它的值只会改变后的结果只在自己的区域生效
对于共享资源常见的坑点:
资源共享导致数据不一致
所以不管是线程任务的参数,一般都要在堆上申请,让每个线程的资源是独立的