线程:是进程内的一个执行分支。线程的执行粒度比进程要细
什么是线程?
• 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程 是“一个进程内部的控制序列”
• 一切进程至少都有一个执行线程
• 线程在进程内部运行,本质是在进程地址空间内运行
• 在 Linux 系统中,在 CPU 眼中,看到的 PCB 都要比传统的进程更加轻量化
• 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给 每个执行流,就形成了线程执行流
1. Linux 中线程该如何理解
地址空间是进程访问的资源窗口,下图当中的绿色部分就是线程
Linux 实现方案:
- 在 LInux 中,线程在进程“内部”执行,线程在进程的地址空间内运行(为什么?任何执行流要执行,都要有资源!地址空间是进程的资源窗口)
- 线程的执行粒度比进程要细?线程执行进程代码的一部分
CPU 是否知道执行的是进程还是线程?不关心,CPU 只有调度执行流的概念
2. 重新定义线程 和 进程
- 什么叫做线程?我们认为,线程是操作系统调度的基本单位
V1 时:进程==内核数据结构(PCB)+代码和数据
进阶版图:三块(PCB + 地址空间 +页表)
- 重新理解进程?内核观点:进程是承担分配系统资源的基本实体
进程掰一块空间给线程,线程是我进程内部的执行流资源!
如何理解我们以前的进程??
操作系统以进程为单位,给我们分享资源,理解为我们当前的进程内部,只有一个执行流~(进程的特殊情况)
进程中还存在对 线程先描述在组织 的队列,进程中套线程,太复杂啦(例如某 windows)
Linux 设计者直接让线程复用进程数据结构和管理算法,(多的不调度就行), 维护成本大大降低,非常卓越!
struct task_struct--模拟线程,Linux 没有真正意义上的线程,是指没有为它创建真正的 pcb, 而是用“进程的内核数据结构”模拟的线程,进程是那三块,最左边的都叫执行流
CPU 中
线程<= 执行流<=进程 Linux 中的执行流,轻量级进程
例如:家庭等同于进程,每一个成员的任务叫做线程
3. 重谈地址空间--第四讲
深入理解页表
虚拟地址是如何转化为物理地址的?以 32 位虚拟地址为例
虚拟地址是多少位的?32 位,将地址进行了拆分
- 32=10+10+12 页表是拆开分级作用
- 虚拟地址,查一级,查二级,找到页框
- 二级页表大部分情况的使用是不全的,用不完的大富翁~
- 任何一个进程都必须要有页目录,二级页表可以残缺
⭕如果MMU发现请求的虚拟页面不在物理内存中,则触发缺页中断(页面加载到内存)。操作系统内核响应这个中断,将所需的页面从磁盘或其他存储设备加载到物理内存中,并更新相应的页面表条目。
- 例如:起始地址 +虚拟地址的最后 12 位(即页框中的偏移量)=物理地址
- 任何一个类型取地址,都只有一个地址,多字节就取开辟空间的第一个字节的地址
虽然努力轻量化了进程,但创建一个进程任然是一个很”重“的工作
寻址:起始地址+类型 =起始地址+偏移量(X86 的特点)
CR2:记录引起缺页中断或者异常的地址
如何理解资源分配给每个执行流?
线程目前分配资源,本质就是分配地址空间范围
怎么让每个线程执行不同的代码?代码有地址,代码的地址是虚拟地址
4.Linux 线程周边概念
线程 VS 进程 切换
线程比进程要更轻量化,为什么?(整个生命周期)
- 创建和释放更加轻量化(生死)
- 切换更加轻量化(运行),对应的页表,地址空间都不需要切换
调度线程那还要调度进程吗?
线程的执行本质上就是进程在执行,线程是进程的一个分支,线程在执行,线程内的切换不需要重新 cache 数据
线程完成之后要实现进程的切换,就需要 cache 缓存数据的切换
- 对于时间片在处理。主线程记录
线程的优点
• 创建一个新线程的代价要比创建一个新进程小得多
• 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
• 线程占用的资源要比进程少很多
• 能充分利用多处理器的可并行数量
• 在等待慢速 I/O 操作结束的同时,程序可执行其他的计算任务
• 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
• I/O 密集型应用,为了提高性能,将 I/O 操作重叠。线程可以同时等待不同的 I/O 操作。
线程的缺点
• 性能损失
- ○ 一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一 个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大 的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的 资源不变。
• 健壮性降低
- ○ 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配 上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大 的,换句话说线程之间是缺乏保护的。
• 缺乏访问控制
- ○ 进程是访问控制的基本粒度,在一个线程中调用某些 OS 函数会对整个进 程造成影响。
• 编程难度提高
- ○ 编写与调试一个多线程程序比单线程程序困难得多
线程异常
• 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
• 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机 制,终止进程,进程终止,该进程内的所有线程也就随即退出
线程用途
• 合理的使用多线程,能提高 CPU 密集型程序的执行效率
• 合理的使用多线程,能提高 IO 密集型程序的用户体验(例如视频可以边下边看)
2. Linux 进程 VS 线程
进程和线程
• 进程是资源分配的基本单位
• 线程是调度的基本单位
• 线程共享进程数据,但也拥有自己的一部分数据:
- ○ 线程 ID
- ○ 一组寄存器 (独立的上下文)
- ○ 栈 (执行流不会错乱)
- ○ errno
- ○ 信号屏蔽字
- ○ 调度优先级
线程上下文和栈确保了独立性
进程的多个线程共享
同一地址空间,因此 Text Segment、Data Segment 都是共享的,如果定义一个函数,在 各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线 程还共享以下进程资源和环境:
• 文件描述符表
• 每种信号的处理方式(SIG_ IGN、SIG_ DFL 或者自定义的信号处理函数)
• 当前工作目录
• 用户 id 和组 id
小测试
#include <iostream>
#include <pthread.h>
#include <unistd.h>
void *threadRun(void* args)
{
while(1)
{
std::cout << "new thread: " << getpid() << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, nullptr);
while(1)
{
std::cout << "main thread: " << getpid() << std::endl;
sleep(1);
}
}
打印分析:
- 同一 pid
- 两个函数同时进行
本篇文章主要讲解了理论,下篇文章将进行实操~