【linux线程(一)】线程概念、线程控制详细剖析
个人主页HABuo个人专栏《C系列》《Linux系列》《数据结构》《C语言系列》《Python系列》《YOLO系列》⛰️如果再也不能见到你祝你早安午安晚安目录一、页表二、线程2.1 线程概念2.2 线程和进程的关联2.3 线程控制三、pthread线程库讲解四、总结前言本篇博客我们将进入线程篇章的学习线程与进程类似是我们要学习的一个大章节在过去的学习中我常听说线程进程的却也从未认真的了解过他们今天就让我们走进它相信通过我的讲解你一定会受益匪浅本章重点页表结构、线程概念、线程与进程的关系、线程控制、Linux下线程库一、页表关于页表的认识前期我们仅知道它就是从虚拟地址空间映射到物理内存的中间体事实上页表的结构还是比较复杂的下面我们来认识一下32位虚拟地址的分割32位虚拟地址被划分为三个部分高10位页目录索引、中间10位页表索引、低12位页内偏移。10位可表示0~1023共1024个索引。12位可表示0~4095对应4KB页内偏移因此页框大小为4KB。页目录与页表结构CR3寄存器保存页目录的物理起始地址。页目录包含1024个页目录项每个4字节每个目录项指向一个二级页表的物理地址。二级页表同样包含1024个页表项每个4字节每个页表项指向一个物理页框的起始地址。页目录必须存在但二级页表可以按需分配部分存在或不存在以节省内存。地址转换过程从CR3取得页目录基址用虚拟地址的高10位作为索引找到对应的页目录项获取二级页表的物理地址。用中间10位作为索引在二级页表中找到对应的页表项获取物理页框的起始地址。将低12位偏移量加上页框起始地址得到最终的物理地址。数据类型与访问范围地址转换只定位到起始物理地址要访问多少个字节由指令中的数据类型如int、char决定。类型信息在编译时确定并体现在机器指令中如操作数大小CPU执行指令时据此访问相应字节数。因此变量地址通常只给出起始地址结合类型即可确定完整访问范围。缺页中断处理当页表项的存在位为0未建立映射时访问该虚拟地址会触发缺页异常。CPU将引起异常的虚拟地址保存在CR2寄存器中然后操作系统缺页处理程序从磁盘或交换区加载数据到物理内存并更新页表项填写物理地址和权限位。缺页也可能因二级页表本身不存在而触发此时需先分配二级页表。页表内存占用估算每个页表项4字节一个二级页表大小为4KB1024项。若所有二级页表全存在共1024个则页表总大小约为4MB页目录4KB 1024×4KB 4MB 4KB ≈ 4MB。实际系统中二级页表通常不会全部创建从而大幅减少内存占用但进程创建仍需一定开销。总结成一段话就是分页机制是将虚拟地址划分为10位页目录索引、10位页表索引和12位页内偏移通过CR3定位页目录逐级查找页表项获得物理页框基址加上偏移得到物理地址并由指令类型决定访问字节数缺页时硬件保存地址至CR2由操作系统处理且二级页表可部分存在以节省内存。二、线程2.1 线程概念我们常听说线程线程的还有什么多线程但我们好像真心没了解过它和进程又有啥区别呢其实线程概念很简单如下但是理解需要下点功夫让我们慢慢来线程概念程序中的一个执行流就叫做线程一个进程至少要有一个执行线程单个进程本身就是一个执行流所以单个进程某种意义上也是一个线程(是主线程)。线程在进程内部运行本质是在进程地址空间内运行。在Linux系统中在CPU眼中看到的PCB都要比传统的进程更加轻量化所谓更轻量化就是说我们在进程那里创建子进程本质是将PCB、进程地址空间、页表等等内容全都拷贝一份这也就是为什么进程具有独立性但是线程不一样线程仅仅是个执行流是在进程内部进行执行的因此它和进程共享地址空间和页表。聪明的你一定想到了我没有强调PCB没错之前我们在讲解进程在CPU中运行时本质就是将PCB放入CPU的运行队列里那么线程运行呢也是这样不过他不叫做PCB而是TCB因此今天我们再说进程就不再仅仅是只有一个PCB而是和多个TCB在一起的整体进程就是承担分配系统资源的一个基本实体。但是我又要告诉你Linux下没有TCB因为单纯从线程调度角度线程和进程有很多的地方是重叠的所以Linux工程师不想给Linux“线程“专门设计对应的数据结构而是直接复用PCB用PCB用来表示linux内部的“线程”Windows下是有独立的TCB总结以下几点Linux内核中有没有真正意义的线程呢没有的。Linux是用进程PCB来模拟线程的是一种完全属于自己的一套线程方案站在CPU的视角每一个PCB都可以叫做轻量级进程Linux线程是CPU调度的基本单位而进程是承担分配系统资源的基本单位进程用来整体申请资源线程用来伸手向进程要资源。Linux中没有真正意义的线程好处是什么简单维护成本大大降低-可靠高效综上所述,线程就是一个没有独立的地址空间的PCB结构,线程的资源是从最开始的主线程,也就是进程来的.而站在CPU的视角,CPU调度的是PCB结构,CPU只认PCB,它并不关心此PCB是进程的还是线程的,所以线程被称为系统调度的基本单位.而在Linux操作系统下,线程就是轻量化的进程2.2 线程和进程的关联通过上面对线程的描述,我们可以窥探到:线程是担任系统调度的基本实体进程是担任系统资源分配的基本实体虽然线程共享进程数据,但也拥有自己的数据:线程ID栈区资源信号屏蔽字调度优先级使用指令: ps -al查看线程ID主线程的PID和LWP相同,CPU调度时是在看LWP,而不是PID,线程的PID和主线程相同,自己独有LWP几个小问题线程一旦被创建几乎所有的资源都是被所有线程共享的线程也一定要有自己私有的资源什么资源应该是线程私有的呢1. PCB属性私有2.要有一定私有上下文结构3.每一个线程都要有自己独立的栈结构与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多1.进程切换页表虚拟地址空间切换PCB上下文切换2.线程切换PCB上下文切换3.线程切换cache不用太更新但是进程切换全部更新2.3 线程控制创建线程我们已经说了Linux下没有线程这个概念而只是轻量化进程但是它又是线程那一套只不过换个名字而已因此Linux下就封装了第三方库来供我们使用库中提供了操作线程的一些接口由于pthred是第三方库所以编译时要加上-lpthread的字段第一个参数需要传入一个pthread_t类型的变量这个变量我们需要先定义.所谓的pthread_t,实际上本质就是unsigned long int类型第二个参数我们一般设置为空。第三个参数要传入线程启动后要执行的函数的地址这个函数的返回值和参数都是void*最后一个参数是传入给函数形参的,要强转为void*示例如下void* thread_routine(void* args) { const char* name (const char*)args; while (true) { cout 我是新线程, 我正在运行! name: name endl; } } int main() { // typedef unsigned long int pthread_t; pthread_t tid; int n pthread_create(tid, nullptr, thread_routine, (void*)thread one); assert(0 n); (void)n; // 主线程 while (true) { char tidbuffer[64]; snprintf(tidbuffer, sizeof(tidbuffer), 0x%lx, tid); cout 我是主线程, 我正在运行!, 我创建出来的线程的tid tidbuffer endl; sleep(1); } return 0; }如何终止线程pthread_exit((void*)7)//方法2参数是设置的类似退出码的标识 pthread_cancel(tid)//方法3参数是线程的tid一般使用return或者pthread_exit来终止线程如何进行线程等待如果不等待新线程那么就会造成内存泄漏那么等待新线程的目的就是如下几点回收新线程PCB等内存资源防止内存泄漏如果需要的话获取新线程执行任务的返回值确认新线程是否执行完任务void* threadRoutine(void* args) { pthread_exit((void*)7); } int main() { pthread_t tid; pthread_create(tid, nullptr, threadRoutine, (void*)Thread 1); void* retval; pthread_join(tid, retval);//设置retval就是获取新线程的退出信息不获取就设置nullptr仅回收线程的内存资源即可 cout main thread quit..., ret: (long long int)retval endl; return 0; }获取线程的tidchar hex[64]; snprintf(hex, sizeof(hex), %p, pthread_self());线程分离int pthread_detach(pthread_t thread);可以是线程组内其他线程对目标线程进行分离也可以是线程自己分离pthread_detach(pthread_self());一般编码格式:void* thread_run(void* arg) { pthread_detach(pthread_self()); printf(%s\n, (char*)arg); return NULL; } int main(void) { pthread_t tid; if (pthread_create(tid, NULL, thread_run, thread1 run...) ! 0) { printf(create thread error\n); return 1; } int ret 0; sleep(1);//很重要要让线程先分离再等待 因为不确定是新线程先分离还是主线程先等待 //如果主线程先等待那么新线程分离后主线程一直阻塞在那里 //所以一般分离时是在主线程处进行分离 if (pthread_join(tid, NULL) 0) { printf(pthread wait success\n); ret 0; } else { printf(pthread wait failed\n); ret 1; } return ret; }三、pthread线程库讲解首先,pthread_create的返回值是线程ID。那么线程ID的本质是什么呢?线程ID的本质是一个地址,pthread库是一个动态库,是第三方库,这个库会被映射到进程的地址空间的共享区中,而线程ID所指的地址则是线程在线程库级别的tcb的起始地址线程的函数想要对线程进行操作那么只需要拿着线程的tid找到线程控制块tcb就可以进行访问并且操作刚才我们说线程有自己的栈区,也就是说线程要维护自己的栈区,那么可以联想一下,谁帮线程维护这个栈区呢?答案是pthread库维护的栈区,也就是说其实线程的栈区也是被映射到共享区的,由pthread第三方库维护补充知识局部存储在多线程编程中我们常遇到两类变量全局变量所有线程共享一个线程修改其他线程立即可见。这容易引发竞态条件通常需要用锁来保护。局部变量位于函数栈上仅在该函数执行期间有效无法跨函数调用共享。线程局部存储正好填补了这两者之间的空白它既有全局的“生命周期”跟随线程的创建和销毁又有线程私有的“可见性”仅在本线程内可见。使用方式_thread int g_val 1000四、总结今天这篇博客我们介绍了页表的具体结构、线程概念、线程控制、线程库等内容小结一下页表分页机制是将虚拟地址划分为10位页目录索引、10位页表索引和12位页内偏移通过CR3定位页目录逐级查找页表项获得物理页框基址加上偏移得到物理地址并由指令类型决定访问字节数线程概念Linux下线程就是轻量级进程他没有按照windows下TCB的具体概念而是复用了进程的PCB也就是说他们共用了一套进程地址空间之前我们所说的进程就是单线成流。所以现在进程的概念就是担任系统资源分配的基本实体。而线程就是CPU进行调度的基本实体。每一个线程有一个PCB。CPU调度的时候就是拿着PCB进行调度。线程控制pthread_creat、pthread_exit、pthread_join、pthread_self、pthread_detach线程库pthread库是一个动态库,是第三方库,这个库会被映射到进程的地址空间的共享区中,而线程ID所指的地址则是线程在线程库级别的tcb的起始地址线程的函数想要对线程进行操作那么只需要拿着线程的tid找到线程控制块tcb就可以进行访问并且操作线程的栈帧结构以及局部存储的的变量都在库中定义