线程概念
- 进程是资源分配的基本单元,而线程是系统调度的基本单元,一个进程包含若干个线程(同一份资源所有线程共同构成了进程,而每个线程都是进程的一个执行流,负责执行一部分代码)。每个线程都可以都叫做LWP(轻量级进程)。
- 属于一个进程的多个线程共享一套资源,比如他们的虚拟地址空间就是同一个。
- 线程在linux和window上都有,但是linux没有真正为线程创建一套新的内核结构和调度体系,而是直接复用了进程的内核结构(PCB),实现方式要比windows简单得多。尽管这样,linux在概念上也是有线程的,只不过采用了一种简单的"实现方式"
线程对于进程的优缺点
优点:
- 一个新线程的代价比一个新进程的代价要小,只需要创建一个LWP,而不需要做构建虚拟地址空间,映射页表等其他工作。
- 线程占用资源较少
- 线程切换比进程损耗小得多,进程切换会把CPU中TLB和CACHE中的数据刷新,下一次执行这个进程的时候还要重新加载,而线程切换则会保留缓存,因为毕竟,组成同一个进程的线程使用相同的资源(包括虚拟地址空间)。线程也切换更少的上下文数据(比如不用切换CR寄存器中的页表地址,这也是因为线程之间共享资源导致的)。
缺点:
- 缺乏访问控制(这是多线程共享虚拟地址空间的必然结果,一个线程可以访问到另一个线程的数据)
- 健壮性低,一个线程崩,所有线程都崩(比如,当一个线程执行非法操作,系统的kill信号不是单发给某个线程,而是发送给整个进程,再比如,一个线程没释放锁,其余线程都卡住)。
题外话------TLB和CACHE是什么

程序加载的时候,将程序开始位置的虚拟地址加载到EIP(PC)中,EIP将虚拟地址交给MMU。
MMU先看TLB中有没有缓存下这个映射,有的话直接读取,没有的话就通过CR3中的页表的物理地址查询页表,将虚拟地址转换为物理地址,并把这次的转换记录到TLB中,未来再想转换这个虚拟地址不用再去查页表,直接就能读取TLB缓存。一句话,TLB用来缓存虚拟地址和真实地址的映射关系,也就是页表的条目。
之后CPU先查看cache里面是否有这个地址上的指令,有就直接读取,如果没有就将指令的物理地址以及in操作通过地址总线写到内存的两个寄存器上,内存按照寄存器上的指示读取这个地址上的指令以及它附近的指令,并把这些指令缓存在cache寄存器上,并将将该指令输入到IR上,CPU执行这条指令并且更新EIP的值。这样整个程序就转起来了。一句话,cache缓存未来可能会执行的指令。
cache中预加载当前指令附近的指令,目的就是为了有很大概率能更快的读取下一条指令而不用去内存上找,这利用了局部性原理。TLB也是这个道理,比如循环时会访问同一个地址**。**
linux线程控制
上面我们说过,linux没有专门的线程机制,所以也就没有用于创建,管理线程的接口,但是linux之前用来对进程控制的系统调用具有较好的扩展性,所以可以使用这些系统调用模拟出线程体系,线程库就是做这个的,所以下面介绍的函数都不是系统调用,而是由我们链接的用户级线程库提供的
- 线程创建:****

- 获取本线程的线程id:

- 等待回收线程:

主线程总是需要对子线程进行回收,并选择性的获取子线程的返回值 - 线程销毁:
- return直接返回
cpp
void pthread_exit(void *retval);
要销毁的线程主动调用该函数
将要返回给主线程的返回值作为参数传入
cpp
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),都要调用系统级接口clone:****
**fn就是要执行代码块的地址,stack就是栈的地址,arg是要执行函数的函数参数,flag是要选择哪个创建模式,因此,pthread_create只要创建了PCB,然后调用clone把执行地址,在哪儿个栈执行,参数等都传进去,调用的时候就知道该线程执行哪儿块代码了。
如何线程切换?:
- 每一次时钟中断后或者系统调用结束从内核态返回用户态后,会对时间片检查,如果时间片到了,就会切换线程调度。