[Linux]线程概念
文章目录
- [Linux]线程概念
- 什么是线程
- Linux系统下的线程实现
- 线程是CPU调度的基本单位
- 进程是系统分配资源的基本实体
- 二级页表
- 线程的优点
- 线程的缺点
- 线程异常
- 线程用途
- 线程资源
什么是线程
线程是进程内部的一个执行分支,执行粒度比进程更细,调度成本比进程更低。
线程是CPU调度的基本单位,进程是系统分配资源的基本实体。
Linux系统下的线程实现
在Linux系统中,一个进程的创建需要创建 task_struct、进程地址空间、页表,并且加载数据和代码到内存中,然后通过页表映射,将进程地址空间的虚拟地址转换成内存实际地址进行数据和代码的访存。
进程创建好后,操作系统就通过task_struct找到进程地址空间,利用页表映射完成代码的执行。
如果我们创建一些这样的“进程”呢:只是创建task_struct然后让其指向已有进程的进程地址空间。因为有地址空间,这些task_struct也可能像进程一样被操作系统正常调用。
实际上,这样只创建task_struct并让它们指向已有进程的地址空间就是Linux操作系统对线程的实现方式。每个task_struct对应的线程都只是执行进程代码中的一部分。
Linux系统下的线程,它具有资源开销小、通信方便、并发性高等特点,适合用于实现并发编程和提高系统性能,因此被称为轻量级进程(LWP)。
说明:
不同操作系统对线程的实现方式是不同的,比如Windows操作系统的实现就为了线程单独设计了管理的数据结构和调用方法,这也导致了线程管理块占用较大的内存、不同版本不兼容性、较为复杂的同步机制等问题。
- 线程是进程内部的一个执行分支: 进程的代码被分为一个个部分由不同的线程来执行,线程提供了该部分的执行入口,操作系统会并发的调度各个线程。
- **线程执行粒度比进程更细:**每个线程一般只是执行进程代码中的一部分。
- 线程的调度成本比进程更低: 线程切换时,不需要修改地址空间和页表,(根据局部性原理而相应加载的)cache缓存。
线程是CPU调度的基本单位
CPU作为硬件只是机械的执行操作系统传入的命令的,CPU不能区别进程和线程区别,只要操作系统将task_struct以及相关数据传入CPU,CPU就会根据task_struct执行相应的代码,无论进程内部只有一个执行流还是有多个执行流,CPU都是以task_struct为单位进行调度的,因此称线程是CPU调度的基本单位。
进程是系统分配资源的基本实体
进程是由一个或多个task_struct构成的”执行流“、进程地址空间、页表、代码和数据组成的。
只有在创建进程时,操作系统才会为其申请内存资源创建地址空间等结构和加载代码和数据,因此进程是系统分配资源的基本实体。因为操作系统会给进程分配系统资源,才会有内存空间用于task_struct的创建来实现线程。
二级页表
注:二级页表用于32位计算机,64位计算机需采用三级页表,二级页表与三级页表的使用原理相同。
为了提高磁盘和内存之间的IO效率,Linux操作系统下文件系统将磁盘分割成一个个数据块(一般大小为4KB,8个扇区),然后对数据块整体进行管理,文件存储时使用的磁盘空间也是以块为单位,并且操作系统和磁盘进行IO时,数据也是按数据块为单位进行存取的,同样的,内存为了按数据块读取磁盘数据需要将内存分割成一个个和磁盘数据块大小相同的部分,这一个个部分被称为页(page)/ 页框,内存和磁盘进行数据交换的本质就是对应的页和对应的磁盘数据块进行数据的交换:
内存管理的本质: 将磁盘中的特定的数据块(数据内容)存放到物理内存中哪个页框(数据加载的空间)。
将内存划分成一个个页框后,操作系统就可以使用数组结构描述物理内存,然后使用该结构对物理内存继续管理。值得注意的是,根据局部性原理,从磁盘中加载一个数据块到内存中,实际上就是一个预加载操作,能够减少IO的次数,提高整机效率。
32位计算机物理内存大小为4GB,物理内存的基本单位是字节,因此表示物理内存的地址需要32位比特位,在使用二级页表时将这32位的地址划分为10+10+12三个部分:
前10位地址作用于第一级页表(页目录),页目录中会存储前10位地址所组成的所有二进制地址的映射关系,映射到对应的第二级页表(页表项)。中间的10位地址作用于第二级页表(页表项),由于每个页表项是根据对应的页目录映射找到的,因此页表项中的10位地址实际建立的是前20位地址所组成的所有二进制地址的映射关系,映射到对应的页框。最后12位地址作用于页框中,当通过二级页表找到对应页框后,根据页框首地址偏移后12位地址所表示的大小找到对应数据的首地址。使用二级页表寻找对应数据的示意图如下:
这种采用二级页表映射找到页框首地址,再使用偏移量找到对应数据首地址的策略,使得页表在实现时,无需再建立第三级页表将内存中每个字节映射起来,大大节约了内存空间。
实际上,在页表的具体实现中,不止会记录地址的映射关系,还会记录操作权限:
要对数据进行操作时,首先要通过虚拟地址在页表中进行映射找到该数据的实际物理地址,然后通过页表中记录的权限判断该操作是否合法,如果不合法硬件MMU就会发生异常,然后被操作系统识别到,操作系统就会给造成异常的进程发送信号,使得进程终止。
线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点
- 性能损失 :
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的 同步和调度开销,而可用的资源不变。
- 健壮性降低:
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。(一个线程发生异常,操作系统检测到后就会给进程发送信号,然后整个进程终止。)
- 缺乏访问控制 :
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。 编程难度提高编写与调试一个多线程程序比单线程程序困难得多。
- 编程难度提高 :
编写与调试一个多线程程序比单线程程序困难得多。
线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率。
- 合理的使用多线程,能提高IO密集型程序的用户体验。(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
线程资源
线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器 (用于保存上下文,用于切换)
- 栈 (用于保存临时变量,从而更好的管理数据)
- errno
- 信号屏蔽字
- 调度优先级
进程的多线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
线程的执行流方式如下: