文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
线程
并发(Concurrent)
并发是一个CPU处理器同时处理多个线程任务。
宏观上是同时处理多个任务,微观上其实是CPU在多个线程之间快速的交替执行CPU把运行时间划分成若干个(微小)时间段,公平的分配给各个线程执行,在一个时间段的线程运行时,其他线程处于挂起状态,这种就称之为并发。
并行(Parallel)
并行是多个CPU处理器同时处理多个线程任务。
当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这就被称之为并行。
同步异步/阻塞非阻塞
当一个request发送出去以后,会得到一个response,这整个过程就是一个同步调用的过程。
- 哪怕response为空,或者response的返回特别快,但是针对这一次请求而言就是一个同步的调用。
当一个request发送出去以后,没有得到想要的response,而是通过后面的callback、状态或者通知的方式获得结果。
异步请求分两步:
- 调用方发送request没有返回对应的response(可能是一个空的response)
- 服务提供方将response处理完成以后通过callback的方式通知调用方
阻塞和非阻塞:
- 阻塞和非阻塞就看调用方在发送请求后是否block住了。
协程
协程是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。
这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
用户态和内核态
Kernel 运行在超级权限模式(Supervisor Mode)下,所以拥有很高的权限。
按照权限管理的原则,多数应用程序应该运行在最小权限下。
因此,很多操作系统,将内存分成了两个区域:
内核空间(Kernal Space),这个空间只有内核程序可以访问。
用户空间(User Space),这部分内存专门给应用程序使用。
用户空间中的代码被限制了只能使用一个局部的内存空间,我们说这些程序在用户态(User Mode) 执行。
内核空间中的代码可以访问所有内存,我们称这些程序在内核态(Kernal Mode) 执行。
什么情况会导致用户态到内核态切换:
系统调用:用户态进程主动切换到内核态的方式,用户态进程通过系统调用向操作系统申请资源完成工作,例如 fork()就是一个创建新进程的系统调用。
异常:当 CPU 在执行用户态的进程时,发生了一些没有预知的异常,这时当前运行进程会切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常。
中断:当 CPU 在执行用户态的进程时,外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执行下一条即将要执行的指令,转到与中断信号对应的处理程序去执行,也就是切换到了内核态。
- 如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等。
用户态和内核态切换的开销
保留用户态现场(上下文、寄存器、用户栈等)
复制用户态参数,用户栈切到内核栈,进入内核态
额外的检查(因为内核代码对用户不信任)
执行内核态代码
复制内核态代码执行结果,回到用户态
恢复用户态现场(上下文、寄存器、用户栈等)
异常和中断的区别
中断是由硬件设备产生的,而它们从物理上说就是电信号,之后它们通过中断控制器发送给CPU,接着CPU判断收到的中断来自于哪个硬件设备(这定义在内核中),最后,由CPU发送给内核,有内核处理中断。
异常是由CPU产生的,同时,它会发送给内核,要求内核处理这些异常。
系统调用过程:
如果用户态程序需要执行系统调用,就需要切换到内核态执行。
内核程序执行在内核态(Kernal Mode),用户程序执行在用户态(User Mode)。
当发生系统调用时,用户态的程序发起系统调用。
因为系统调用中牵扯特权指令,用户态程序权限不足,因此会中断执行,也就是 Trap(Trap 是一种中断)。
发生中断后,当前 CPU 执行的程序会中断,跳转到中断处理程序。
内核程序开始执行,也就是开始处理系统调用。
内核处理完成后,主动触发 Trap,这样会再次发生中断,切换回用户态工作。
线程调度算法
先到先服务(First Come First Service,FCFS)
就是先到的作业先被计算,后到的作业,排队进行。
最短作业优先
最短作业优先(Shortest Job First, SJF)调度算法通常会同时考虑到来顺序和作业预估时间的长短。
优先级队列(PriorityQueue)
优先级队列可以给队列中每个元素一个优先级,优先级越高的任务就会被先执行。
抢占(Preemption)
抢占就是把执行能力分时,分成时间片段。
让每个任务都执行一个时间片段。
如果在时间片段内,任务完成,那么就调度下一个任务,如果任务没有执行完成,则中断任务,让任务重新排队,调度下一个任务。
多级队列模型:多级队列,就是多个队列执行调度。
紧急任务仍然走高优队列,非抢占执行。
普通任务先放到优先级仅次于高优任务的队列中,并且只分配很小的时间片。
如果没有执行完成,说明任务不是很短,就将任务下调一层。
下面一层,最低优先级的队列中时间片很大,长任务就有更大的时间片可以用。
通过这种方式,短任务会在更高优先级的队列中执行完成,长任务优先级会下调,也就类似实现了最短作业优先的问题。
CPU
冯诺依曼模型中 CPU 负责控制和计算。
为了方便计算较大的数值,CPU 每次可以计算多个字节的数据。
如果 CPU 每次可以计算 4 个 byte,那么我们称作 32 位 CPU。
如果 CPU 每次可以计算 8 个 byte,那么我们称作 64 位 CPU。
这里的 32 和 64,称作 CPU 的位宽。
CPU指令集权限
Inter把
CPU指令集
操作的权限由高到低划为4级:
- ring 0
- ring 1
- ring 2
- ring 3
其中 ring 0 权限最高,可以使用所有
CPU 指令集
,ring 3 权限最低,仅能使用常规CPU 指令集
,不能使用操作硬件资源的CPU 指令集
,比如IO
读写、网卡访问、申请内存都不行,Linux系统仅采用ring 0 和 ring 3 这2个权限。ring 0被叫做内核态,完全在操作系统内核中运行
ring 3被叫做用户态,在应用程序中运行
寄存器
CPU 要进行计算,比如最简单的加和两个数字时,因为 CPU 离内存太远,所以需要一种离自己近的存储来存储将要被计算的数字。
这种存储就是寄存器。
寄存器就在 CPU 里,控制单元和逻辑运算单元非常近,因此速度很快。
寄存器中有一部分是可供用户编程用的,比如用来存加和指令的两个参数,是通用寄存器。
还有一部分寄存器有特殊的用途,叫作特殊寄存器。
比如程序指针,就是一个特殊寄存器。
它存储了 CPU 要执行的下一条指令所在的内存地址。
- 注意,程序指针不是存储了下一条要执行的指令,此时指令还在内存中,程序指针只是存储了下一条指令的地址。
下一条要执行的指令,会从内存读入到另一个特殊的寄存器中,这个寄存器叫作指令寄存器。
指令被执行完成之前,指令都存储在这里。
寄存器的数量通常在几十到几百之间,每个寄存器可以用来存储一定字节(byte)的数据。
32 位 CPU 中大多数寄存器可以存储 4 个字节;
64 位 CPU 中大多数寄存器可以存储 8 个字节。
总线
CPU 和内存以及其他设备之间,也需要通信,因此我们用一种特殊的设备进行控制,就是总线。
总线分成 3 种:
地址总线:专门用来指定 CPU 将要操作的内存地址。
数据总线:用来读写内存中的数据。
- 当 CPU 需要读写内存的时候,先要通过地址总线来指定内存地址,再通过数据总线来传输数据。
控制总线:用来发送和接收关键信号,比如中断信号,还有设备复位、就绪等信号,都是通过控制总线传输。
- 同样的,CPU 需要对这些信号进行响应,这也需要控制总线。
存储器分级
当 CPU 需要内存中某个数据的时候,如果寄存器中有这个数据,我们可以直接使用;
如果寄存器中没有这个数据,我们就要先查询 L1 缓存;L1 中没有,再查询 L2 缓存;
L2 中没有再查询 L3 缓存;L3 中没有,再去内存中拿。
L1-Cache:
- 在 CPU 中,相比寄存器,虽然它的位置距离 CPU 核心更远,但造价更低。
- 通常 L1-Cache 大小在几十 Kb 到几百 Kb 不等,读写速度在 2~4 个 CPU 时钟周期。
L2-Cache:
- 在 CPU 中,位置比 L1- 缓存距离 CPU 核心更远。
- 它的大小比 L1-Cache 更大,具体大小要看 CPU 型号,有 2M 的,也有更小或者更大的,速度在 10~20 个 CPU 周期。
L3-Cache:
在 CPU 中,位置比 L2- 缓存距离 CPU 核心更远。
大小通常比 L2-Cache 更大,读写速度在 20~60 个 CPU 周期。
L3 缓存大小也是看型号的,比如 i9 CPU 有 512KB L1 Cache;有 2MB L2 Cache; 有16MB L3 Cache。
程序的执行过程
首先,CPU 读取 PC 指针指向的指令,将它导入指令寄存器。
具体来说,完成读取指令这件事情有 3 个步骤:
CPU 的控制单元操作地址总线指定需要访问的内存地址(简单理解,就是把 PC 指针中的值拷贝到地址总线中)
CPU 通知内存设备准备数据(内存设备准备好了,就通过数据总线将数据传送给 CPU)。
CPU 收到内存传来的数据后,将这个数据存入指令寄存器。
然后,CPU 分析指令寄存器中的指令,确定指令的类型和参数。
如果是计算类型的指令,那么就交给逻辑运算单元计算;如果是存储类型的指令,那么由控制单元执行。
PC 指针自增,并准备获取下一条指令。
比如在 32 位的机器上,指令是 32 位 4 个字节,需要 4 个内存地址存储,因此 PC 指针会自增 4。
CPU100%
top –c
,显示进程运行信息列表,找到最耗CPU的进程。
按数字1,显示多核CPU信息。
键入P,进程按照CPU使用率排序。
按M按照内存占用进行排序。
top -Hp 【PID】
,显示一个进程的线程运行信息列表,找到最耗CPU的线程。
printf %x\n 【线程pid】
,转换多个线程数字为十六进制。
jstack 【进程PID】| grep 【线程转换后十六进制】-A10
, 使用JStack获取进程PID堆栈,利用Grep定位线程ID,打印后续10行信息。
如果
VM Thread os_prio=0 tid=0x00007f871806e000 nid=0xa runnable
,第一个是线程名,如果是VM Thread
就是虚拟机GC回收线程了。
jstack 【进程PID】> 【文件】
,将JStack堆栈信息存储到文件。
CPU负载情况
top
15:52:00 up 42:35, 1 user, load average: 0.15, 0.05, 0.01
5:52:00 指的是当前时间
up 42:35 指的是机器已经运行了多长时间
1 user 指的是当前机器有1个用户在使用
load average: 0.15, 0.05, 0.01 的是CPU在1分钟、5分钟、15分钟内的负载情况
假设是一个4核的CPU,此时如果你的CPU负载是0.15,这就说明,4核CPU中连一个核都没用满,4核CPU基本都很空闲。
如果CPU负载是1,那说明4核CPU中有一个核已经被使用的比较繁忙了,另外3个核还是比较空闲一些。
要是CPU负载是1.5,说明有一个核被使用繁忙,另外一个核也在使用,但是没那么繁忙,还有2个核可能还是空闲的。
如果你的CPU负载是4,那说明4核CPU都被跑满了,如果你的CPU负载是6,那说明4核CPU被繁忙的使用还不够处理当前的任务,很多进程可能一直在等待CPU去执行自己的任务。