在学习多线程之前,我们先来了解一些背景知识,我们需要这些背景知识来辅助我们理解多线程!
首先,物理内存并不是连续的一整块大空间,物理内存实际上是被划分为很多块(4KB空间),是一个大数组。操作系统进行内存管理不是以字节为单位,而是以内存块为单位,默认大小为4KB!之前文件系统中,系统与磁盘文件进行基础IO的基本单位也是4KB(8个扇区),这都啥经过精心设计的!
之前我们的可执行程序中会存在地址!可执行程序是储存在磁盘的文件,就会有对应inode
,天然的就按4KB
储存好了。所以未来进行内存与磁盘的IO交互时,就直接将磁盘的数据块加载到内存块中!
所以一切都是设计好的! 所以IO的基本单位是4KB,无论是磁盘到内存,还是内存到磁盘都是4KB!而内存中的内存块我们称之为" 页框 / 页帧 "
那操作系统对内存的管理工作就是对4KB空间的管理! 也就是说改变一个变量(假如是int类型 4字节)时,会会直接将整个页框进行写时拷贝,而不是单单一个int类型。这是因为OS系统认为当你修改一个变量时,其周围的变量会有很大概率也发生改变,所以将整个页框进行写时拷贝!从而避免多次写时拷贝(空间换时间)。
4GB的内存中共有 1024 *1024 = 1048576
个页框,那么操作系统然后管理这么多的页框呢???
其实每个页框在内核里都有一个struct page
结构体来管理,这里面会有该页框的对应属性。那么管理起来就可以通过一个大数组struct page memory[1048576]
来管理。所以物理内存也就是这个大数组了!!!
CPU可以通过虚拟地址转换为物理地址,这是怎么进行的。接下来我们就来谈谈页表,按我们之前的理解页表是虚拟地址映射物理地址的。那这样储存一个页表就需要2^32
个地址映射,这就以及32GB了,所以很显然,操作系统不会以这种方式来储存页表。接下来我们就来学习页表的底层是什么样子的!!!
物理内存中的每个物理地址一定是有对应的页的,也就是只要找到了对应页就能访问其物理地址。虚拟地址有2^32
个地址:每个地址都是这样的32位序列 0000 0101 0010 0000 0110 1001 1100 1000
。那么虚拟地址是如何转换为物理地址的呢???这个转换不是直接进行转换,而是按照一定规则进行划分查找:
虚拟地址共有32位比特位,分为三部分:A部分前10位 ,B部分中间十位,C部分最后12位。
一个地址分为了三部分,并且页表也不止有一张!前10位对应页目录的1024个元素,以A部分作为索引对应每个元素,而这个元素是指向另一张页表的指针。这个页表也有1024个元素,以B部分作为索引,而这个元素是内存中的页框的起始地址(大小为4KB,4096字节),而C部分恰好有4096种组合,作为索引对应每个内存块中的字节!!!C部分中的12位作为页内偏移,与页框的起始地址进行加和,就能找到对应字节!
这样就将虚拟地址转换为了物理地址!!!
也就看出来:页表的本质就是搜索页框!在通过最后12位来找到对应字节!这样算下来这个页表只花费了页目录 1024 * 4 + 页表 1024 *(1024 * 4) = 4 MB
这可比32GB小的太多太多了
但是这样只能找到一个字节啊!一个int都有4个字节,更别说更大的类对象了。这可怎么来找到对应的数据?我们还有“类型”这一关键一步,类型 + 起始地址
就能从内存中找到对应的数据!
CPU可以通过MMU帮助我们将虚拟地址转换为物理地址,页表是储存在CR3
寄存器中:
CR3寄存器通常被称为页目录基址寄存器(Page Directory Base Register)。 它存储了当前任务页目录表(Page Directory Table, PDT)的物理地址。页目录表是一个数据结构,用于在启用分页时转换虚拟地址到物理地址。
通过这个寄存器与MMU的硬件电路配合,就可以成功转换为物理地址!
地址空间的各个分区是通过限定一批虚拟地址空间的范围来实现分区。如果我们将代码区的代码拆分为20个函数,让我们的代码来并行运行,这样在技术上可行吗?首先函数也有地址,函数地址是代码的入口地址(函数第一行地址)函数内部每行代码都有地址。连续的代码块就是函数,那么一个函数就对应一批虚拟地址,如果要拆分函数,就只需要拆分页表就可以了!
所以说:虚拟地址本质是一种资源,可以进行分配!
只要将虚拟地址分配清楚,就可以将代码数据进行拆分!
先来看官方概念:线程:在进程内部运行,是CPU调度的基本单位。
进程我们很熟悉:是由PCB描述,通过地址空间与页表获取物理内存中的代码与数据。
今天如果我们想要创建一个进程,但不给它分配对应的地址空间,只创建一个task_struct
与先前的进程共享地址空间:
线程就是这许多的task_struct
,可是这样进程又是什么呢?之前我们学习的是进程 = 内核数据结构 + 进程代码与数据
但是今天,要进行重新矫正。这样多个task_struct
不叫进程,叫做进程的执行流!那什么是进程呢?
这一整套是进程!之前我们学习的就是只有单个task_struct
的特殊情况!!!
进程从内核来看,是承担分配系统资源的基本实体!
在我们这个社会中,家庭是经营的基本单位,家庭中每个人都有对应的责任:孩子好好学习健康生活,父母勤奋工作,爷爷奶奶安心养老。所有人都在执行自己的事情,但所有的人把自己的事情做好,就能产生将家庭过幸福的结果!
而家庭就是进程,家庭成员就是线程!这就是他们之间的关系!
刚才我们所说的是Linux内核下的线程,对于线程来说,也一定要和进程一样需要对应操作方法:新建,暂停 ,销毁,调度。那么线程会不会与进程产生关联呢? 接下来我们就来了解线程如何管理。
线程我们一般称为tcb
(进程是pcb
),那么该结构体struct tcb
中就需要:线程id ,优先级,状态,上下文,链接属性…
在Windows下,pcb
与tcb
是相对独立的,其通过数据结构来关联起来,是两套不同的控制体系!CPU在进行处理时,就要先选择一个进程再选取一个线程,这就需要两个不同的调度算法:
这样就使操作过程十分的复杂!
而Linux吸取Windows的经经验,发现tcb
与pcb
里面的属性是一致的,并且两个都是执行流,为什么不用一个模块来统一管理呢?!这样就不需要单独设计线程的模块了。
所以Linux是用进程模拟的线程!
我们再来从CPU的角度来看,CPU调用一个task_stuct
是小于等于 进程
的,进程里面有很多的task_struct
!那么CPU需不需要来区分task_stuct
是进程还是线程?当然不需要,执行进程和线程和CPU有什么关系?!你要执行什么就给我CPU什么!给CPU什么执行流(进程或线程),它就执行什么!可以说线程是CPU调度的基本单位。
我们在实践中见一见:
这是创建线程的系统调用,参数有四个: int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
#include <iostream>
#include <pthread.h>
#include <unistd.h>
// 新线程
void *threadStart(void *args)
{
while (true)
{
std::cout << "new thread running..." << std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadStart, (void *)"sthread-ew");
// 主线程
while (true)
{
sleep(1);
std::cout << "main thread running..." << std::endl;
}
return 0;
}
我们编译的时候会报一个这样的错误:
(.text+0x1b): undefined reference to main
线程未定义,之所以会出错是因为Linux下使用线程需要引用线程库:
这个库的详细信息我们后面再说。编译的时候链接上动态库pthread
就可以了
g++ -o testthread testthread.cc -std=c++11 -lpthread
这样主线程和新线程就可以同时跑了:
我们查看进程信息:
发现只有一个进程: 再来让这两个线程打印一下自己的pid:
会发现,他们两个虽然是两个不同的执行流,但是却是同一个进程!原因不就是这两个进程属于同一个进程内部!我们可以使用ps -aL
就可以查看不同线程
这个pid是对应进程的pid,这个LWP
其实就是这个线程的id!!!操作系统调度的时候,是通过LWP
调度的。CMD
是主线程。
有个问题:多进程调度和单进程调度相互影响吗? 进程调度时通过pid来,每个进程都不一样,都有自己的pid,所以并不影响。
下面我们来解决一下几个疑问:
cache
会储存热点数据(进程相关数据) ,要访问数据时,会先在cache
中寻找,如果命中直接访问,反之进行置换。 所以进程之间切换时,会将cache
的数据全部作废操,重新读取,切换线程就不需要进行切换。所以线程的调度成本更低!!!对于多线程和单线程来说,是要合适最好!对于一个2线程的CPU那么创建两个线程是最好的!
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程 中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境: