本章重点
1.了解线程概念,理解线程与进程的区别与联系
2.学会现充控制,线程创建,线程终止,线程等待
3.了解现场分离与线程安全
4.学会线程同步
5.学会使用互斥量,条件变量,posix信号量,以及读写锁
6.理解基于读写锁的读者写者问题
目录
1.linux中什么是线程
2.重新定义线程
3.线程的管理
4.重谈地址空间
5.怎么让线程执行不同的代码
6.线程比进程更轻量化
7.线程优点
8.线程缺点
9.线程异常
10.线程用途
11.进程vs线程
1. linux中什么是线程
基本概念
线程是进程内的一个执行分支,线程的执行粒度比进程要细
上面是进程的内存结构,地址空间是进程的资源窗口。当我们创建一个进程后,会拥有自己的pcb,地址空间和页表等,父子进程互相独立。如果这时我们创建一个进程,它拥有自己的pcb结构,代码和数据共用父进程的一部分,这样页表也可以用父进程的映射,不需要创建多余的内容就可以执行,把这种结构成为线程
linux实现方案
1.线程实在进程“内部”执行的,线程在进程的地址空间运行,任何执行流要执行就要有资源,地址空间是进程的资源窗口,线程的资源比如代码和数据就是来自于它的进程
2.线程的执行粒度要比进程更细,线程执行的是进程代码的一部分。cpu只有调度执行流的概念,不需要关心是进程还是线程
2. 重新定义概念
进程是承担分配系统资源的基本实体,线程是系统调度的基本单位
以前说过,进程=内核数据结构(task struct)+代码和数据,这种说法不冲突。创建进程就是创建一个pcb,执行流,创建页表的地址空间的映射,在物理内存开辟一个空间。这些东西都是要在内存中占空间的,所以说进程是分配资源的基本实体。线程是进程内部的一个执行流,执行流也是一种资源,os的调度在进程间不断切换,线程是进程中的一部分,也是os的基本单位
linux的执行流线程,是轻量级进程
如何理解以前的进程
os以进程为单位,给我们分配资源,我们以前的进程是只有一个执行流的情况,这种属于特殊情况,一般的进程都会有很多执行流
3. 线程的管理
进程中有很多线程,是1:n的关系。线程需要cpu调度运行,线程有自己的资源那么就必须管理起来。有的会给线程用一个tcb的结构来管理,在linux中,考虑到线程和进程的结构并没有差太多,所以复用了进程的pcb结构模拟线程。这样更方便也更容易管理,由于pcb的基础上描述现成,所以也更稳定
4. 重谈地址空间
进程是承担资源分配的基本单位,线程又是进程的一部分内容。具体虚拟地址到物理地址是怎么转换的?
物理内存是分为很多块的,以4kb为cpu访问的基本单位,整体用一个数组来管理。32位的机器一个地址是32位的,页表保存的是虚拟地址到物理的映射关系,虚拟地址空间是4个G,也就是232,如果将页表看做一行有虚拟地址4字节,物理地址4字节,权限位2字节,看为10字节,页表被写满的情况就需要232 * 10的大小才能存储,这个结果是很大的,更不用说有很多进程,由其他内容,所以不可能这样存储
首先,将一个32位的地址分为10+10+12位三部分
页表分为两级页表,首先将地址的第一个10位转换为十进制,在第一个页表中索引下标,一级页表总共存1024个地址,对应了二级目录。由一级页表索引到二级页表的地址,然后将第二个10位转换为十进制,在二级页表索引下标,就能找到页框的起始地址,二级目录还包括权限之类的字段。最后一个12位刚好是4k的大小,转换为十进制,作为页框起始的地址的偏移量,就对应了物理内存中的地址。偏移量不会超过页框的大小
二级页表一个地址4字节,总共有1024个地址,就是4kb,一级页表可以存1024个二级页表的地址,也就是4*1024=4mb。用户空间只有3G,而且实际上一个进程基本不会占完所有大小,甚至只占用一小部分。二级页表大部分是不全的
有的页框的大小是4mb,最终的页表会更小,大页式内存
地址偏移
最后索引后只拿到了一个地址,内置类型取地址都是最低的地址,根据偏移读值。cpu怎么知道读取几个字节?类型的本质,是给cpu看的,cpu内置是能识别知道需要读几个字节的,比如有些命令是word,dword等。类的本质也是一堆内置类型的集合,空类也是有大小,才能找到它。所以起始地址+类型=起始地址+偏移量,x86的特点。
页表访问如果越界了,CR2寄存器保存的越界等其他原因造成的异常或缺页中断的地址。这样缺页中断加载后,就能把这个地址拿出来访问。
线程分配资源,本质就是分配地址空间范围
5. 怎么让线程执行不同的代码
函数地址是天然不一样,没交集的,只需要将不同函数地址交给不同的线程运行
6. 线程比进程更轻量化
从整个生命周期看
a.创建和释放更轻量化(生死)
b.切换更轻量化(运行)
线程在切换的时候,一定有自己的上下文数据要切换,页表和地址空间不需要切换,所以更少一点。cpu保存和恢复的数据更少,这里只是几个寄存器的多少。cpu里还有一个硬件级catch缓存,占的比较大。会把接下来要读取的部分代码都加载到里面,这样可以考虑到让运行更快,这部分数据叫做热数据。切换线程的时候,这部分是可以继续用的,线程在运行的时候,进程的时间片也在走,当进程切换的时候,catch数据需要重新缓存,由冷变热
一个进程的时间片是平均划分给线程,并不能创建一个线程就会增加时间片。怎么知道切换的是进程和线程,pcb里是需要标识的。将刚启动的线程叫父进程,其他线程叫新线程。os可以知道是进程还是线程的时间片用完了,主次线程也可以区分
7. 线程的优点
创建一个新线程的代价要比创建一个新线程小的多
与进程之间的切换相比,线程之间的切换需要操作系统做的工作少得多
线程占用的资源要比进程少得多
能充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。并不是越多越好,由多少个cpu创建多少个线程最好
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
8. 线程的缺点
- 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变 - 健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的 - 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些os函数会对整个进程造成影响 - 编程难度提高
编写与调试一个多线程程序比单线程程序困难
9. 线程异常
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
10. 线程用途
合理的使用多线程,能提高cpu密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如一边写代码一边下载开发工具,就是多线程)
11. 进程vs线程
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
进程的多个线程共享统一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个去哪聚变量,在个线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
线程哪部分独占,独立的上下文和独立的栈结构,前者保证线程独立调度,后者保证多个执行流不会错乱
线程和线程的关系如下图: