OS中的线程
目录线程概念线程对于进程的优缺点题外话——TLB和CACHE是什么linux线程控制用户级线程库线程概念进程是资源分配的基本单元而线程是系统调度的基本单元一个进程包含若干个线程同一份资源所有线程共同构成了进程而每个线程都是进程的一个执行流负责执行一部分代码。每个线程都可以都叫做LWP轻量级进程。属于一个进程的多个线程共享一套资源比如他们的虚拟地址空间就是同一个。线程在linux和window上都有但是linux没有真正为线程创建一套新的内核结构和调度体系而是直接复用了进程的内核结构PCB实现方式要比windows简单得多。尽管这样linux在概念上也是有线程的只不过采用了一种简单的“实现方式”线程对于进程的优缺点优点一个新线程的代价比一个新进程的代价要小只需要创建一个LWP而不需要做构建虚拟地址空间映射页表等其他工作。线程占用资源较少线程切换比进程损耗小得多进程切换会把CPU中TLB和CACHE中的数据刷新下一次执行这个进程的时候还要重新加载而线程切换则会保留缓存因为毕竟组成同一个进程的线程使用相同的资源(包括虚拟地址空间)。线程也切换更少的上下文数据比如不用切换CR寄存器中的页表地址这也是因为线程之间共享资源导致的。缺点缺乏访问控制这是多线程共享虚拟地址空间的必然结果一个线程可以访问到另一个线程的数据健壮性低一个线程崩所有线程都崩比如当一个线程执行非法操作系统的kill信号不是单发给某个线程而是发送给整个进程再比如一个线程没释放锁其余线程都卡住。题外话——TLB和CACHE是什么程序加载的时候将程序开始位置的虚拟地址加载到EIPPC中,EIP将虚拟地址交给MMU。MMU先看TLB中有没有缓存下这个映射有的话直接读取没有的话就通过CR3中的页表的物理地址查询页表将虚拟地址转换为物理地址并把这次的转换记录到TLB中未来再想转换这个虚拟地址不用再去查页表直接就能读取TLB缓存。一句话TLB用来缓存虚拟地址和真实地址的映射关系也就是页表的条目。之后CPU先查看cache里面是否有这个地址上的指令有就直接读取如果没有就将指令的物理地址以及in操作通过地址总线写到内存的两个寄存器上内存按照寄存器上的指示读取这个地址上的指令以及它附近的指令并把这些指令缓存在cache寄存器上并将将该指令输入到IR上CPU执行这条指令并且更新EIP的值。这样整个程序就转起来了。一句话cache缓存未来可能会执行的指令。cache中预加载当前指令附近的指令目的就是为了有很大概率能更快的读取下一条指令而不用去内存上找这利用了局部性原理。TLB也是这个道理比如循环时会访问同一个地址。linux线程控制上面我们说过linux没有专门的线程机制所以也就没有用于创建管理线程的接口但是linux之前用来对进程控制的系统调用具有较好的扩展性所以可以使用这些系统调用模拟出线程体系线程库就是做这个的所以下面介绍的函数都不是系统调用而是由我们链接的用户级线程库提供的线程创建获取本线程的线程id等待回收线程主线程总是需要对子线程进行回收并选择性的获取子线程的返回值线程销毁return直接返回void pthread_exit(void *retval); 要销毁的线程主动调用该函数 将要返回给主线程的返回值作为参数传入int pthread_cancel(pthread_t thread); 主线程调用该函数销毁子线程 thread参数是要销毁的子线程的线程id 注意就算主线程主动销毁子线程也要调用pthread_join对子线程 进行回收只不过获取的子线程返回值总是-1线程分离主线程可以调用分离函数分离后线程不用被回收。总的来说主线程做的步骤是创建子线程等待回收子线程/分离子线程用户级线程库linux中线程是用户级的概念线程相关函数都只在线程库存在但是用户级线程库只是提供一些函数吗不是的。实际上用户级线程库还承担着管理所有线程的责任那读者就要问了库怎么管理线程具体来说体现在下面几个方面每个线程的LWP结构体线程执行所需要的环境每个线程都要有单独的函数栈线程自己独有的一些数据只要库能管理上述结构库就相当于管理了线程话不多说我们直接看图库内部会维护每个线程的struct_pthread存放LWP结构体的内容栈空间以及线程的私有数据过程如下在pthread_create中我们会申请一块实际的内存空间K然后调用相应函数将K映射到进程的虚拟地址空间上的共享区的用户级线程库内部这是关键因此库不就管理起来这块空间了吗然后我们将新线程LWP的内容拷贝到K的前部分也就是struct_pthread结构体的部分并将这个结构体的地址注册给系统注册之后如果线程运行过程中属性有了变化系统会自动修改struct_pthread结构体的内容进行同步因此库不就管理起来了线程的属性LWP结构体了吗每个线程都要有自己的栈空间在我们调用系统接口创建LWP的时候把K中的线程栈区域的首地址传给系统接口那么新线程以后就会把线程栈区域当作栈来使用。因此库不就管理起来每个线程的执行环境了吗只要一个变量被__thread关键字修饰那么每新建一个线程线程都会将这个变量存在自己的局部存储空间中。因此库不就管理起来这个线程的独有数据了吗再来捋顺一下思路线程库中维护了每个线程的管理结构。当我们像获取线程信息比如获取线程ID的时候pthread_self访问的是struct pthread而不是系统的LWP主线程回收子线程时得到的返回值是从线程库中拿到的struct pthread里面包含一个线程的返回值。这样的设计让线程库和OS解耦这也是用户级线程库的特点。每个线程都会有线程局部存储的空间用以存放__thread关键字修饰的全局变量__thread只能修饰内置类型的变量比如errno就是每个线程有一份。被__thread修饰的内置类型全局变量在每个线程内部都会维护自己的一份互不干扰。这个线程局部存储的一个用处是缓存数据提高访问效率比如说要获取一个线程的id正常来说每次都要调用pthread_self,这个函数就会去对应的地址下找TCB然后找id但是有了局部存储只需要创建一个__thread修饰的全局变量然后用id初始化它这样每个线程都有自己的存放id的全局变量互不影响用的时候直接去全局变量取不用调用函数。计算机内有很多进程所有进程引用同一个线程库进程由很多线程组成因此线程库管理着所有线程!clone函数无论是创建子进程fork还是创建线程vfork,pthread_create调用vfork都要调用系统级接口clonefn就是要执行代码块的地址stack就是栈的地址arg是要执行函数的函数参数flag是要选择哪个创建模式因此pthread_create只要创建了PCB然后调用clone把执行地址在哪儿个栈执行参数等都传进去调用的时候就知道该线程执行哪儿块代码了。实际上因为无论主线程还是子线程都用pid来唯一的标识自己我们调用getid其实获取到的是pid而调用getpid获取到的其实是tgid同一个进程下的所有线程的tgid都是相同的。如何线程切换每一次时钟中断后或者系统调用结束从内核态返回用户态后会对时间片检查如果时间片到了就会切换线程调度。