1 .线程
是进程内的一个执行分支,线程的执行粒度比进程细。
在进程内部创建一个tack_struct,把其中的一部分执行代码和数据(通过页表映射)单独分给一个线程来维护,这就是一个进程内的一个执行分支。
线程不需要维护所有的代码和数据,所以线程的执行粒度比进程细。
2.Linux的实现方案
线程在进程“内部“执行,线程在进程的地址空间内运行(为什么?)
任何执行流要执行都要有资源,地址空间是进程的资源窗口
线程的执行粒度比进程更细?
线程执行进程的一部分代码和数据。
在CPU中没有进程线程的概念,只有执行流的概念。一个线程或者一个进程就是一个执行流。
3.重新定义一下进程和线程
什么是线程?我们认为,线程是操作系统调度的基本单位。
重新理解进程?内核观点:进程是承担分配系统资源的基本实体。
那么线程在操作系统中应该如何描述组织呢?
大多数其他操作系统中有线程对应的调度,执行,描述,组织,但是Linux中是没有专门去对线程进行描述组织的,它复用了进程的数据结构和管理算法。
Linux没有真正意义上的线程体现在其没有为线程专门用tcb描述组织起来,但是它符合操作系统对线程的定义。
我们考虑一下,那CPU怎么区分线程和进程呢?
对于CPU而言,我不关心是进程还是线程,只存在执行流的概念。
线程 <= 执行流 <= 进程。
这里执行流的粒度比进程小,所以叫轻量级进程,也就是Linux下所谓的线程,如果一个进程只有一个执行流,那就是一个主线程。
4.重谈地址空间
如何理解资源分配给线程?
我们知道,进程地址空间是一个虚拟空间,每个进程都有一份,我们要找的实际上是对应的物理内存,那怎么通过虚拟地址转换到对应的物理地址呢?这就需要用到页表了
CPU将一个虚拟内存空间中的地址转换为物理地址,需要进行两步:首先将给定一个逻辑地址(其实是段内偏移量,这个一定要理解!!!),CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址,再利用其页式内存管理单元,转换为最终物理地址。
那怎么样用页表映射呢?
直接用一张这个页表映射,那么我们可以粗略计算一下,以32位机器为例。
如果一个地址一一映射,那么一个指针4个字节,俩者一映射就需要8字节,那么完成这4GB内存的映射就需要32GB。不可能这样做的,所以应该怎么设计页表映射呢?
我们以32位虚拟地址为例,把32位虚拟地址化成 32 = 10 + 10 + 12的形式,我们把页表分级,前边十 位的范围【0 - 1023】用来表示对应的一级页表下表,然后一级页表内放的是二级页表的地址,同样,二级页表也是十位,其中存放的是对应物理内存的页框(物理内存或者磁盘通常是把4kb做为一个块,一个页框也就是一个块,这样可以提升效率,比如读取或者加载,可以一次加载或读取一个块,而不用一个字节,一个字节的去读取或加载),剩下的12位呢,我们可以发现,2^12 = 4 kb,所以这12位就作为页框起始地址的偏移量让我们可以找到准确的物理地址。
那我们来算一下空间消耗吧!
一个一级页表 1024 * 4 = 4kb,二级页表 1024 * 4 * 1024 = 4 MB,这样一看,通过页表分级的形式,我们虚拟地址映射所用空间相对来说并不大,而且通常二级页表是不全的,因为一般不存在一个进程需要所有内存(这里是4GB),那可能会说,我一开始不知道我需要访问哪块空间,要是我访问的空间没有映射怎么办?
这人家早就想到解决方案了,先通过二级页表的一个字段判断有没有映射,如果没有,那就缺页中断,不断去填充。
这里说明一下,一个进程可以没有二级页表,但一级页表(页目录)是必须存在的,且CPU中有一个寄存器(CR3)存放它的地址。寄存器(CR2)存放引起缺页中断异常的虚拟地址,这是为了在缺页中断后我能找到上次的地址接着执行。
所以线程分配资源,本质就是分配地址空间范围。
什么是缺页中断呢?
这部分先留着。。。。。。。。。。
我们知道,一个地址是一个字节,那么对一个int类型变量&拿到几个地址呢?
任何一个变量(结构体,内置类型,自定义类型,数组)&都只拿到一个地址,那么定义的不同类型的变量是为什么呢?
CPU内有许多寄存器,我们定义不同的类型是给CPU的寄存器去看的,CPU读取指令是一条一条去读的(也就是一个地址一个地址去读),那到底读多少,这个CPU在读取之前就要知道的,所以类型就作为了一个限制,规定一次读多少。
谈一下我们的‘ * ’ 操作。
我们对不同类型的变量解引用,就要读取不同的大小空间,但&是取的首地址,还有我们要区分大小端。
这是因为每一位地址里边的值是一定的,但是读取多少位地址,起始地址从哪里开始,这都会影响最终结果。
起始地址+类型 = 起始地址 + 偏移量。 这是X86的特点。
6.Linux线程周边概念
线程比进程更加轻量化(为什么?)
创建和释放更加轻量化(线程生死)
切换更加轻量化(线程运行)
这两者就是线程的生命周期
进程的创建需要:PCB,页表,地址空间,文件描述符表,信号的三张表……
线程只需要创建对应的tcb。
同样,进程释放申请的资源都要释放,线程只需要释放他的tcb。
切换:线程只需要切换他的tcb,页表,地址空间都不需要切换(这些都是进程的资源,被线程共享,也就是说,线程创建好了就能看到进程的这些资源)。
有人会说,这些切换也不需要多少时间呀,怎么就说线程的切换更高呢?
CPU中除了寄存器,还有有一个缓存进程数据的硬件(cache),线程的执行其实本质就是进程的执行,因为线程其实就是进程内部的一个执行分支(一个部门,其中一个人在做一个工作,其实就是你这个部门在做工作,他只是做其中的一部分)。
我这个机器是2G2核的,机器CPU配置高,cache也大。他会提前加载进程的数据,这样是为了加快效率。
那样就来了,线程切换只需要切换上下文,而这个cache不需要切换,进程切换就需要直接丢弃这个cache,切换不只有寄存器的切换,所以我们说线程切换要轻量化。
当然,进程也是需要去切换的,时间片一到,就需要切换,线程的数量不会影响进程的时间片,进程通常会把时间片合理分配给其内部线程。
7. 线程优缺点
7.1线程的优点
7.2线程的缺点
一个线程出现问题,整个进程都会挂掉,比如除0错误,会发信号,整个进程都会执行对应方法,信号是独立于进程之外的执行流,它的信号是发送给进程而不是线程,所以整个进程会挂掉,一旦进程挂掉,那么线程赖以生存的资源就没有了,那么线程也就挂了。一个进程除0,不会所有进程都挂掉,进程独立性。
就是说大部分资源是共享的