线程在进程内部执行,是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。
什么是线程
通过一定的技术手段,将当前进程的“资源”,以一定的方式划分给不同的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提供的原生线程库
线程的优点
线程的缺点
性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的
同步和调度开销,而可用的资源不变。
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
线程异常
线程用途
Linux进程VS线程
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
见一见线程
参数解释:
thread
:输出型参数返回线程id
attr
:
start_routine
:函数指针,线程要执行的回调函数
arg
:回调函数的参数
代码:
运行结果:
我们看到确实2个线程是同一个pid
那么我们如何查看线程呢?
打开连个窗口,一个窗口运行代码,一个窗口观察线程
窗口2:运行代码
窗口一查看进程和线程
先查看进程:
看到确实只有一个进程
再查看线程:
我们看到确实有两个线程。
ps -aL
: 查看轻量级进程
LWP
:轻量级进程ID
CPU调度时看的是LWP.
kill -9 进程id,进程被kill线程也被kill
演示:
创建多个线程:
代码:
#include
#include
#include
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就立即失效的话,新过来的进程只能重新缓存