目录
📚Linux线程
📕什么是线程
在一个程序里
的一个执行路线
就叫做线程(thread)。更准确的定义是:线程
是“一个进程内部
的执行分支”,是CPU调度的基本单位。
进程
是加载到内存中的程序,进程=内核数据结构+进程代码和数据
。
一切进程至少都有一个执行线程。
线程在进程内部运行,本质是在进程地址空间内运行
。
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
一个进程的代码是由无数个函数构成,或则说是由我无数个代码块构成,每一个代码块都有对应的入口地址,这些代码块在一个进程中都是串行调用的
!
可以使用多进程去并发的执行一个进程的代码,那为什么要由线程呢?
- 在Linux系统中,为了让一个进程的代码能够并行去执行,可以使用多进程,但是创建一个新的进程,会创建进程PCB,进程地址的空间,页表,文件描述符表,加载程序,构建映射等等,这个成本是很高的,总:
进程创建成本是较高的(
时间和空间成)。 - 我们的目的是多执行流并发执行一个进程的代码,提高效率,那有没有可以不用使用多进程来实现这一目的呢?这就要引出
Linux线程
了。
线程的原理的简单介绍
- 线程和进程类似,只不过不需要像创建进程那样创建全部的内核数据结构,线程的创建只需要创建一个
task_struct
,并且该task_struct指向该进程的进程地址空间。这样地址空间以及地址空间上的资源都是多个线程共享的,将进程的代码划分为多个区域,那么就可以创建多个线程去并发的执行这几个区域的代码。
- 这就可以理解线程进程内部的一个执行分支的概念了,内部:线程是在进程的地址空间中运行的,线程是CPU调度的基本单位,CPU在调度的时候,不用区分看到的task_struct是一个进程还是一个进程中的一个执行流。
Linux为什么要这么设计线程?
- 因为如果我们要设计线程,OS也会对线程做管理,所以也得设计线程的结构体(
线程控制块struct TCB
)进行描述,再利用数据结构进行组织起来,并且需要自己的调度算法等等,这样设计非常复杂,并且可以发现 线程和进程 的很多字段很类似,所以,Linux的设计者
认为,进程和线程
都是执行流,具有极度地相似性
,没必要单独设计数据结构和算法,直接使用进程模拟线程。 - 但是Windows系统中,是对于进程和线程是分开设计的,都有自己独特的一套数据结构和算法。
在Linux中,没有实际上的线程,因为是使用进程模拟的线程,所以再Linux中,线程
是叫轻量级进程
。
Linux中,所有的调度执行流都叫做:轻量级进程
。
如果一个进程执行的代码是一个操作系统,就是一个内核级虚拟机的技术。
⚡再谈进程地址空间
(页表,虚拟地址和物理地址)
多个执行流是如何进行代码划分的?
操作系统
也是需要管理内存
的,实际上,内存是被划分为很多个以4KB为单位的内存块
,一个可执行程序,是以平坦模式进行整体编址的,不仅要编址,并且按照地址也被划分为一个一个的4KB的数据块(写入文件系统就是一个一个的4Kb数据块了,文件按系统部分讲过,这里不赘述),所以内存与磁盘进行交互数据,都是以4KB数据块为单位进行交互的。其中,内存是空间,磁盘上是内容,所谓的加载,就是将内容放在空间中,在OS系统的术语,把4KB的空间和内容叫做页框或者页帧
。- OS要对划分的4KB内存块进行管理,用
struct page
的结构体进行描述(如下图),其中包含一个标记为,标记该内存块的状态(是否正在被使用,是属于用户级还是内核级等等),再利用一个struct page类型的数组
进行管理(大小就是该内存被划分为的4KB的内存块数目),下标就标识了一个唯一的内存块,所以对内存的管理就是对该数组的增删查改。
页表
虚拟到物理地址如何转换的,以及页表最终结构
OS会将虚拟地址看成 10 10 12
个比特位为大小的子区域(如下图):
10个比特位的取值范围:0~1023
12个比特位的取值范围为0~4096(4KB)
-
操作系统中有一张
页目录
,最多可以存放1024张页表(实际中不可能),页表里面存放的是页框的物理地址(内存被划分的每一个 内存块的起始地址)。 -
其中,根据虚拟地址的前10个比特位标识在哪一个页表中,中间10个比特位标识在哪一个页框中,最后12个比特位标识在该页框(内存块)中的偏移量(页内偏移)。
-
其中,页表后面还可以加上一些权限标志位,如内核态,用户态,读写权限等。
-
给不同的线程划分不同的代码执行的区域,本质就是让不同的线程,各自看到页表全部的子集。
🍑线程的优点
-
创建一个新线程的代价要比创建一个新进程小得多
-
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多(
面试题
) -
在CPU中会存在一个硬件,叫
cache
,在进行调度一个进程的时候,CPU中除了会有一组寄存器存放临时变量外,OS还会将当前执行的代码中附近的代码预先加载到cache中
,所以CPU在寻址访问代码的时候,不用从内存中读取,直接在cache中读取,从而提高CPU寻址的效率。 -
所以如果是
进程间切换
,不仅要保存各自的上下文数据
,切换CPU中寄存器的数据等等,在cache
中曾经缓存的数据全部失效了,另一个进程只能重新加载数据进cache,这个过程耗时,但是如果是线程切换,在cache中曾经缓存的数据就不需要丢弃,因为预加载的代码数据线程可能还会使用。 -
线程占用的资源要比进程少很多
-
能充分利用多处理器的可并行数量
-
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
🌳线程的缺点
- 性能损失
- 一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低
- 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 编程难度提高
- 编写与调试一个多线程程序比单线程程序困难得多。
🌲线程异常
- 单个线程如果出现
除零
,野指针问题
导致线程崩溃,进程
也会随着崩溃。 - 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
🐕线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
🍒Linux进程VS线程
🚲进程和线程
- 进程是资源分配的基本单位
- 线程是调度的基本单位
- 线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
一组寄存器(一组线程执行的上下文数据)
栈(存放执行的函数的临时变量)
- errno
- 信号屏蔽字
- 调度优先级
- 进程的多个线程共享 同一地址空间,因此
Text Segment
、Data Segment
都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
进程和线程的关系如下图: