1.线程概念
线程是进程内部的执行流,一个进程可以有多个线程
系统资源分配的基本实体是进程,操作系统调度的基本单位是线程
线程"独占"数据:
1.线程id
2.线程的上下文数据
3.栈
线程对应的描述结构体是pthread库提供的,就像FILE结构体是C语言库提供的一样
1.1地址空间深层理解
疑问1:页表的映射是按照地址大小(4字节)映射的吗?
其实并不是按照四字节映射的
图示:
页表也是需要加载到物理内存的,所以如果映射是按照4字节映射,最终需要的页表一项有8字节,一共有4*1024*1024*1024(4GB)项,总共的大小就是8*4*1024*1024*1024(字节),也就是32GB,这还只是一个进程的页表大小,所以这是不可能的
注意:
1.我们可以将物理内存和磁盘存储都看成4kb块的集合,因为4kb是io的最小单位,从磁盘加载数据到内存就是4kb为单位传输数据,这些4kb的空间也叫页框(固定4kb为一页的叫内存页)
图示:
os使用struct page来管理页框,每个page对应一个页框,然后会用一个struct page pages[]结构体数组来管理所有page对象,0下标的page对象就对应当前可访问的最低地址的物理地址,以此类推
我们不需要保存物理地址了,因为可以根据结构体数组下标来推断出实际的物理地址(每个内存页的大小固定4kb)
申请内存空间的底层:申请一个page对象
得到所有物理地址的方法:得知page数组的起始物理地址
3.页表也要加载到物理内存,那么页表的映射地址我们怎么知道?
在虚拟内存系统建立之初,就通过一个预先构建的、固定的线性映射,将包括页表在内的所有物理内存映射到内核虚拟空间。这个映射是系统赖以构建其他所有映射的基石。
4.虚拟地址如何转换成物理地址的?
图示:前置知识:32位计算机中,虚拟地址总共4字节,32个bit位,其中高10位是页目录表偏移项,次高10位是页表偏移项(页表只能定位到页框),低12位是页框内偏移量(最后定位到1字节级别)
**(1)页表目录查找:**CR3中存储了当前进程页表目录的物理地址,我们根据虚拟地址高10位查找该虚拟地址所属页表(物理地址 = 页表目录物理地址 + 高10位值*4),页表目录存储的是地址元素,4字节为单位,所以偏移项*4**(2)页表查找:**根据虚拟地址次高10位查找虚拟地址所属页框(物理地址 = 页表物理地址 + 次高10位值*4),页表存储的是地址元素,4字节为单位,所以偏移项*4
**(3)页框查找:**同理,根据低12位进行页框内查找(物理地址 = 页框物理地址 + 低12位偏移量),页框存储是以字节为单位的,所以不用对偏移量处理
细节注意:
1.页表查找过程使用的均为物理地址
2.页表目录,页表,页框都是4kb大小
3.查找页框物理地址方法:addr&1111 1111 1111 1111 1111 0000 0000 0000
根据页框地址查找page结构体的下标方法:页框地址/4kb
4.访问其他变量的时候也是使用起始地址+偏移量的方法访问,偏移量根据变量数据类型来决定,int是4,char是1....,cpu在识别的时候就会使用不同位宽的寄存器访问
5.页目录表中每一项存储的都是页表起始物理地址,而页表中每一项的高20位存储页框的物理地址,低12位存储对应访问权限属性
疑问:写实拷贝和缺页中断为什么是以4kb为单位进行的?
因为os申请空间都是以4kb为单位进行的,单独申请一块小空间违反了存储规则
疑问:为什么我们在c/c++申请空间的时候可以自由申请任意空间?
因为c/c++可能提前申请了足够的空间空闲下来,等到用户需要申请空间的时候再将空间交给用户使用
1.2线程相关接口
(1)创建新线程的接口:pthread_create
参数1:创建的线程id(tid用户级)的地址,其中tid类似进程的pid,输出型参数
参数2:线程属性(默认设为nullptr),输入型参数
参数3:函数指针,新线程需要执行的函数(返回值和参数都是void*类型)
参数4:参数3指向的函数的参数
返回值为0表示成功,不为0表示失败
注意:
1.创建之后,主线程和新线程没有明确的先后运行关系
2.内核级tid需要使用gettid获取,和lwtid是数值上一样的,而这里的tid是库维护的,一般进程内有效
(2)阻塞等待线程接口:pthread_join
参数1:需要等待的线程的id
参数2:返回信息,输出型参数,如果不需要就设为nullptr
返回值为0表示成功,其余返回值表示不同情况的错误值
注意:
之所以参数2是**类型,是因为他是从tcb结构体中获取void*ret的,要把他带出来就要用void**
(3)线程退出接口:pthread_exit
参数:线程的返回值
没有返回值
(4)获取线程id接口:pthread_self
没有参数,返回值为当前线程的id
(5)取消指定线程:pthread_cancel
参数:线程id
返回值:成功返回0,失败返回非0
(6)线程分离接口:pthread_detach
参数:线程id
返回值:为0表示成功,非0为失败
以上所有接口都是包含在pthread中的,且该pthread库是一个共享库,os中所有线程使用的都是这个库,而该库又负责管理线程,所以线程的管理是基于os而不是进程的
1.3使用接口
(1)使用接口创建新线程
cpp#include <iostream> #include <cstdio> #include <pthread.h> #include <unistd.h> void *thread_func(void *n) { while (true) // 新线程 { printf("我是新线程: %s\n",(char*)n); sleep(1); } } int main() { pthread_t tid; pthread_create(&tid, nullptr, thread_func, (void *)"thread1"); while (true) // 父线程 { std::cout << "我是父线程" << std::endl; sleep(1); } return 0; }结果演示:
父线程执行流:使用thread_func函数作为子线程后续执行流,创建子线程thread1,然后死循环输出"我是父线程",以此体现父线程执行流
子线程执行流:创建出来后执行thread_func函数
线程新理解:
此时,一个进程有两个执行流,不同的线程使用了不同的函数,本质上是不同的线程拥有不同区域的虚拟地址,使用了不同的资源,这就是通过函数编译的方式,实现进程内部资源划分
所以实际上在linux操作系统中线程的管理是复用了进程的管理相关代码的,而不是再重新开发一套线程管理代码**(windows中真的单独实现了线程管理)**
黄色的task_struct是进程管理的,后面蓝色的是线程管理的,他们都指向同一个进程的虚拟地址空间,然后通过分配不同的入口函数地址来达到执行流资源分配的目的
什么是进程?
进程就是由一个或多个task_struct管理的,拥有虚拟地址空间,页表映射的一个启动的程序
而之前学习的一个进程对应一个task_struct的进程是单线程进程,这次的进程才是普遍的进程,他有多个task_struct,包含了多个线程
什么是线程?
绿色部分的task_struct就是线程,后续线程的叫法是主线程和新线程,要和进程的区分开
在cpu角度:cpu不区分进程和线程,而是都叫做轻量级进程
在用户角度:学习操作系统的时候是区分进程和线程的,为了减轻用户学习成本,linux会封装一个pthread库,让用户可以在上层使用线程相关接口,而不需要知道轻量级进程的概念
重点:面试常考
为什么和进程相比,线程之间切换比需要操作系统做的工作要少很多?
(1)CR3和TLB寄存器在线程间切换时不用更新
因为CR3存储的是页表目录物理地址,不同线程处于同一进程,不需要更换页表
TLB缓存的是虚拟-物理地址映射,同理
(2)cache硬件线程切换时不用更新,因为cache缓存的是内存数据,同一个进程中内存数据是一样的
演示线程全局变量共享
cpp#include <iostream> #include <cstdio> #include <pthread.h> #include <unistd.h> volatile int n = 10; void *thread_func(void *s) { while (true) // 新线程 { printf("我是新线程: %s, n: %d\n",(char*)s,n); n++; sleep(1); } } int main() { pthread_t tid; pthread_create(&tid, nullptr, thread_func, (void *)"thread1"); while (true) // 父线程 { std::cout << "我是父线程" << "n: " << n << std::endl; sleep(1); } return 0; }定义全局变量n,在主线程和新线程中都进行变量值打印,但是只在新线程中进行变量++
我们看到主线程和新线程的n值都改变了,说明全局变量对线程是共享的
除此之外,函数,堆空间等等都是共享的
主线程退出进程结束,新线程退出主线程不受影响
代码部分:将主线程的死循环改为5次执行,新线程仍然为死循环
cpp#include <iostream> #include <cstdio> #include <pthread.h> #include <unistd.h> volatile int n = 10; void *thread_func(void *s) { while (true) // 新线程 { printf("我是新线程: %s, n: %d\n",(char*)s,n); n++; sleep(1); } } int main() { pthread_t tid; pthread_create(&tid, nullptr, thread_func, (void *)"thread1"); int cnt = 5; while (cnt--) // 父线程 { std::cout << "我是父线程" << "n: " << n << std::endl; sleep(1); } return 0; }主线程5秒后退出,虽然新线程理论上仍然需要执行,但是由于主线程退出意味着进程退出,资源被释放,新线程无法继续执行
注意:实际项目中,我们肯定希望所有线程都执行完,所以需要保证主线程是最后结束的,这时候就需要使用阻塞等待接口pthread_join
反向验证:将新线程设置为5秒退出,主线程设置为死循环
新线程结束后,主线程仍然在运行
(2)使用接口阻塞等待线程
cpp#include <iostream> #include <cstdio> #include <pthread.h> #include <unistd.h> #include <vector> // volatile int n = 10; void *thread_func(void *s) { int cnt = 5; while (cnt--) // 新线程 { printf("我是新线程: %s\n", (char *)s); sleep(1); } return nullptr; } int main() { std::vector<pthread_t> tidv; for (int i = 1; i <= 5; i++)//批量创建新线程 { pthread_t tid; pthread_create(&tid, nullptr, thread_func, (void *)"thread1"); tidv.push_back(tid); } for(auto &e :tidv) printf("new thread id: 0x%lx\n",e); for(auto &e :tidv) { pthread_join(e,nullptr); printf("退出的线程是:0x%lx\n",e); } std::cout << "主线程退出" << std::endl; return 0; }创建pthread_t类型数组,将新创建出来的线程tid都存储起来,然后将其依次打印以体现确实创建了对应线程,最后回收新线程后将回收成功的线程打印出来,所有线程回收完成,主线程退出
注意:
1.如果需要给不同线程执行函数传递不同的参数,最好使用独立的堆空间(每个线程都自己申请堆空间),否则可能会因为主线程执行速度过快,导致不同线程真正用到参数的时候参数值都更新成最后一个线程的参数值
2.多线程运行的时候健壮性会降低,因为一个线程出现异常,进程会崩溃,进程崩溃其他线程也会崩溃
(3)使用线程退出接口返回
cppvoid *thread_func(void *s) { int cnt = 5; while (cnt--) // 新线程 { printf("我是新线程: %s\n", (char *)s); sleep(1); } pthread_exit(nullptr); }直接替换掉return语句即可
(4)使用接口获取当前线程id
cppint main() { pthread_t tid; pthread_create(&tid, nullptr, thread_func, (void *)"thread1"); printf("新线程id: 0x%lx, 主线程id: 0x%lx\n", tid,pthread_self()); pthread_join(tid, nullptr); printf("退出的线程是:0x%lx\n", tid); std::cout << "主线程退出" << std::endl; return 0; }我们在主线程中使用pthread_self获取主线程id,然后利用pthread_create的第一个输出型参数获取新线程的id
1.4应用级理解
(1)线程创建的传参不一定是内置类型,更多时候是我们自定义的结构体变量,以此满足实际情况的开发
代码演示:
cpp#include <iostream> #include <unistd.h> #include <pthread.h> #include <string> class Task { public: Task() {} Task(int x, int y) : _x(x), _y(y), _result(0) { } ~Task(){} void add() { _result = _x + _y; } void print() { printf("result: %d\n",_result); } private: int _x; int _y; int _result; }; void *start_run(void * task) { Task* t = static_cast<Task*>(task); t->add(); return t; } int main() { pthread_t tid; Task* task = new Task(1,2); pthread_create(&tid, nullptr, start_run, (void *)task); // 创建线程 void *ret = nullptr; pthread_join(tid, &ret); // 线程等待 Task* result = (Task*)ret; std::cout << "线程等待成功" << std::endl << "等待返回值"; result->print(); return 0; }我们这里创建了一个Task类,也就是任务结构体,用来让线程执行,执行完成后,由于线程之间不具有独立性,可以将任务结果数据直接传回主线程,然后再主线程中输出结果
流程:创建结构体变量,作为参数传递给新线程,新线程执行任务并返回结构体,主线程打印结果
疑问1:void*为什么可以无缝转换为其他指针类型?
因为指针的占用字节数都是4/8,所以进行转换没有数据丢失风险
疑问2:新线程执行的函数中,t是局部变量,为什么返回t没问题?
因为我们实际上获取的是t指向的申请在堆中的task对象的地址,而在堆上的地址是不会在主动释放前丢失的
(2)线程分离
线程一般是joinable模式,也就是需要让主线程等待新线程退出才会销毁,而有的线程根据实际需求会设置成分离模式,他不再需要主线程等待,而是执行结束自动释放空间,一般是新线程不关注返回值的时候使用
底层是将flags标志位更改
2.线程封装
先描述:
我们需要利用pthread库封装线程接口,需要有线程创建,线程回收等待,线程执行等接口
再组织:
创建一个线程类,将线程相关的属性设置为类内私有属性,将线程创建,线程等待,线程执行的接口创建出来(1)Thread.hpp
cpp#ifndef __THREAD__ #define __THREAD__ #include <iostream> #include <string> #include<vector> #include <unistd.h> #include <pthread.h> #include <functional> #include<sys/syscall.h> #define getlwpid() syscall(SYS_gettid) using func_t = std::function<void()>; const std::string namedefault = "none_name"; class thread { public: thread(const std::string &name, func_t func) : _name(name), _func(func), _isruning(false) { } ~thread() {} static void *start_func(void *args) // 将类内函数的this用static去除 { thread *t = static_cast<thread *>(args); t->_isruning = true; t->_lwpid = getlwpid(); t->_func(); pthread_exit((void*)0); } void Start() { int n = pthread_create(&_tid, nullptr, start_func, this); // 保证运行函数可以直接使用类内属性 if (n == 0) { printf("创建新线程成功\n"); } } void Join() { if (_isruning != true) return; int n = pthread_join(_tid, nullptr); if (n == 0) { printf("回收线程成功\n"); } } private: pthread_t _tid; pid_t _lwpid; bool _isruning; // 标志线程是否已经启动 std::string _name; func_t _func; }; #endif类内属性:
线程id(用户级与内核级),线程运行状态,线程名,线程执行任务
类内接口:
1.线程创建:Start直接调用pthread库的线程创建接口即可
2.线程等待:Join
先判断线程是否处于运行状态,若处于就调用pthread库的join接口并等待线程
3.线程执行:start_func
该接口需要将线程属性更新好,且需要调用func执行给定任务
注意:
1.start_func函数一定要设置为static,否则会引发参数不匹配这是因为类内接口有个隐藏的第一参数,this指针,该指针指向结构体对象本身。
但是为了让该接口仍然可以直接使用类内变量,所以我们现将他设置为static,然后将this指针作为参数传递给他


















