文章目录
- 线程学习前的了解知识
- 理解线程
线程学习前的了解知识
线程在进程内部执行,是OS调度的基本单位
OS可以做到让进程对进程地址空间进行资源的细粒度划分
比如malloc一块内存空间,我们拿到的一般都是起始位置,但是最终位置我们一般都不知道。
进程内部有struct vm_area_struc
t结构体。
vm管理虚拟地址空间的起始地址和末尾地址。而struct vm_area_struct也要用链表结构管理起来。
我们知道虚拟地址的映射是通过页表的,那么如何从虚拟内存映射到物理内存?
1.exe就是一个文件
2.我们的可执行程序本来就是按照地址空间方式进行编译的。
3.可执行程序,其实按照区域也已经划分了以4KB为单位。
物理内存有4GB,划分为4KB的个数
OS要不要管理100W+个4KB空间呢?当然要,先描述再组织
我们如何判断某4KB空间有没有被使用?我们只要看flag标记位即可。
物理内存被划分为4KB,而I/O的基本单位也是4KB
页表映射过程:
首先虚拟内存是映射到,磁盘的可执行程序的位置。
而可执行程序被加载到物理内存后,就有了物理内存的地址。
然后断开磁盘的地址,将物理内存的地址填入映射表中。
这个过程也叫缺页中断
,这个过程用户是零感知的,也就是对用户透明。
我们知道页表是映射地址的,那么页表如何映射?
假设在32的平台下,虚拟内存有232次方个地址。我们简单计算一下页表的大小。
我们看到一行就需要9字节,而有232行,那么所需的空间都超了4GB,而页表也是属于物理内存的所以根本不可能存的下。
那么OS是怎么做的呢?
OS将页表分为一级页表
和二级页表
,一个地址有32个比特位,一级页表存前10个bit位,二级页表存中间10个bit位,最后12页bit位表示页内偏移,212刚好是4KB。
理解线程
什么是线程
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
通过一定的技术手段,将当前进程的“资源”,以一定的方式划分给不同的task_struct。
多个进程(task_struct)指向同一个mm_struct—>这里每一个task_struct,都可以称之为线程
---->Linux特有的实现线程的方案。
线程是在进程内部执行的(线程在进程放入地址空间内运行),是OS调度的基本单位。(CPU其实并不关心,执行流是进程还是线程,只关心task_struct)
所以什么是进程呢?
1.从资源角度
用户视角:
内核数据结构+该进程对应的代码和数据
内核视角:
进程:承担分配系统资源的基本实体。
进程是直接向OS要资源的,而线程是向进程要资源的。
2.如何理解曾今我们所写的代码?
以前:内部只有一个执行流的进程
现在:内部具有多个执行流的进程---->task_struct就是进程内部的一个执行流
在CPU视角
:CPU不怎么关系当前是进程还是线程的概念,只认task_struct。—>和之前的概念也不冲突---->CPU调度的基本单位“线程”
在Linux下 进程PCB <= 其它OS内的 进程PCB!----->所以Linux下的进程统一称为轻量级进程
所以Linux没有真正意义上的线程结构,Linux是用进程PCB(task_struct)模拟的线程!---->Linux并不能直接给我们提供线程相关的接口,只能提供轻量级进程接口---->但是使用者要使用线程的话还要理解什么是轻量级进程,很麻烦---->所以Linux也考虑了使用者的难处,所以在用户层实现了一套用户层多线程方案,以库的方案提供给用户进行使用。
Linux的pthread线程库是Linux提供的原生线程库
线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点
-
性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的
同步和调度开销,而可用的资源不变。 -
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。 -
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。 -
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
Linux进程VS线程
- 进程和线程
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
– 线程ID
– 一组寄存器
– 栈
– errno
– 信号屏蔽字
– 调度优先级
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
见一见线程
参数解释:
thread
:输出型参数返回线程id
attr
:
start_routine
:函数指针,线程要执行的回调函数
arg
:回调函数的参数
代码:
运行结果:
我们看到确实2个线程是同一个pid
那么我们如何查看线程呢?
打开连个窗口,一个窗口运行代码,一个窗口观察线程
窗口2:运行代码
窗口一查看进程和线程
先查看进程:
看到确实只有一个进程
再查看线程:
我们看到确实有两个线程。
ps -aL
: 查看轻量级进程
LWP
:轻量级进程ID
CPU调度时看的是LWP.
kill -9 进程id,进程被kill线程也被kill
演示:
创建多个线程:
代码:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
void *threadRoutine(void *arg)
{
char* name = (char*)arg;
while (true)
{
std::cout << name << " pid:" << getpid() << "\n" << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid[5];
char name[64];
for (int i = 0; i < 5; i++)
{
snprintf(name, sizeof(name), "%s %d", "thread", i + 1);
pthread_create(tid + i, nullptr, threadRoutine, (void*)name);
sleep(1);
}
while (true)
{
std::cout << "main thread pid:" << getpid() << std::endl;
sleep(3);
}
return 0;
}
运行结果:
CPU线程进行切换的成本低,为什么?
地址空间和页表不需要切换,因为都共用一个进程的地址空间和页表,CPU内部有L1~L3寄存器cache,对内存的代码和数据根据局部性原理,预读进CPU内部!
如果进程切换,cache就立即失效的话,新过来的进程只能重新缓存