searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

详解处理器调度原理

2023-05-29 01:37:26
63
0

cpu 结构

cpu 在结构上包括:控制器、运算器、总线、高速缓存。

控制器

控制器根据用户程序,依次从内存读取指令,译码后,根据确定的时序,向相应部件发出微操作控制信号。

  • 程序计数器:简称PC,保存从内存提取的下一条指令的地址。
  • 指令寄存器:根据程序计数器里的地址取指令,将取到的指令放入指令寄存器,然后更新程序计数器。
  • 指令编译器:用于对指令进行译码。
  • 主存地址寄存器:保存cpu当前正要访问的内存地址,通过总线跟主存通信。
  • 通用寄存器:用于暂时存放数据或者指令。

运算器

接收控制器的指令,进行运算,将运算结果保存到通用寄存器,并写回主存。

  • ALU:算数运算单元,是运算器的核心,能完成常见的位运算(与、或、非、左移、右移)和算术运算(加、减、乘、除)。
  • 通用寄存器:用来保存参加运算的操作数和运算的结果。
  • 状态字寄存器:存放运算状态(条件码、进位、溢出)和运算控制信息。

总线

总线是cpu和外围设备(内存、IO设备)数据交换的通道,分为地址总线和数据总线。

高速缓存

高速缓存旨在解决cpu时钟周期和主存(RAM)访问速度不匹配的问题,cpu将频繁访问的内存数据放入高速缓存,可以提高访问速度。

缓存行

数据在缓存和主存之间,是按固定大小的块传输的,该块称为缓存行(cache line),一个 cache line 占 64 字节。

三级缓存

高速缓存分三级(L1、L2、L3),cpu访问数据顺序为:cpu -> 寄存器 -> L1缓存 -> L2缓存 -> L3缓存 -> 主存。

  • L1缓存:速度最快(1ns),容量最小(32K);对应每个物理线程(可以通过超线程技术,将1个核心模拟成多个物理线程)
  • L2缓存:速度其次(3ns);容量其次(256k);对应每个cpu核,同一个核下的多个物理线程共享一个L2缓存
  • L3缓存:速度最慢(12ns);容量最大(30M);对应每个cpu插槽;同一个物理cpu共享一个L3缓存。

缓存一致性

多个物理线程对同一块主存数据更改时,可能会产生数据不一致,而cpu提供了保证各个缓存一致性的机制。

写传播

某个cpu修改了缓存行后,会通过总线store到内存,因为总线是各个cpu共享,所以修改会被其他cpu监控到,并从内存更新各自对应的缓存行,保证了一致性。

cache

MESI 协议

为了提高写传播的效率,cpu 设计了 MESI 协议,每个缓存行都对应四种状态。

  • Modify(修改):表示缓存已经被修改,和主存不一致,等待同步到主存;当监听到其他cpu的读取操作时,写回主存,并设置为 Shared 状态、
  • Exclusive(独占):表示当前修改已经同步到主存;当监听到其他 cpu 的读取操作,将状态改为 Shared。
  • Shared(共享):表示当前状态和主存一致;监听其他 cpu 的写操作时,将状态标记为 Invalid 状态。
  • Invalid(失效):其他cpu修改了缓存行,导致本 cpu 中的缓存行失效。

cpu 管理

物理 cpu

主板上实际插入的 cpu ,查看物理 cpu 个数

cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
 

cpu 核心

cpu 的组成是由运算器和控制器组成,每个核心都有一组控制器和运算器,查看每个物理 cpu 的核心数

cat /proc/cpuinfo |grep "cores"|uniq
 

逻辑 cpu

top 命令中显示的 cpu,代表可独立执行的单元,逻辑 cpu 个数 = 物理 CPU 个数 * 每个物理 CPU 的核心数 * 超线程数量

cat /proc/cpuinfo| grep “processor”|wc -l
 

cpu 平均负载

通过 top 查看 load average,3个值分别代表1分钟,5分钟,15分钟的load情况,重点关注的是后两个。

load average 是一段时间内处于可运行状态和不可中断状态的线程平均数量,该值等于逻辑cpu个数时,说明各个核心刚好打满,也没有排队现象,超过逻辑cpu个数时说明负载变高,有进程排队等待。

cpu 利用率

cpu 利用率代表 cpu 时间的占用多少,top 命令查看时,如果是 4 个逻辑 cpu,则 cpu 利用率最高是 400%。

cpu 利用率和 cpu 负载没有直接关系,cpu 利用率反应的是 cpu 的占用时间,load 反应的是执行 cpu 运算的任务平均数。

  • load 高利用率低:使用cpu的线程数很多,但是大部分都在等待IO返回,对应IO密集型服务。
  • load 低利用率高:工作线程数很少,但是占用时间很长,对应计算密集型服务,或者出现了死循环。

排查 cpu 热点

找出占用率高的线程

top -H -p 进程id

查看系统调用堆栈,找到对应的线程,分析上下文问题
pstack 进程id

利用strace跟踪具体线程的系统调用
strace -p 线程id

查看系统调用和花费时间,找出最占用cpu的系统调用
strace -T -r -c -p 线程id
 

调度原理

进程

进程是资源分配的最小单位,资源包括虚拟内存空间(代码段,数据段,堆)、文件描述符、页表、信号等内核资源。

操作系统用 PCB(process control block)来表示进程,linux 中用 task_struct 结构体表示 PCB,内核调度描述的进程实际上是线程,进程id就是主线程的pid,多个 task_struct 共享虚拟内存等资源指针。

struct task_struct {
	long			  state;
	struct mm_struct          *mm;
	pid_t			  pid;
	struct task_struct __rcu  *parent;
	struct list_head	  children;
	struct files_struct	  *files;
};
 

子进程

父进程通过 fork() 创建子进程,子进程会继承父进程的资源:

  • 物理内存:当二者任意一个修改内存时,系统会复制一个新的物理内存分配给修改的进程,叫做写时复制。
  • 文件描述符:此时父子进程共享文件的当前偏移,父进程向文件写内容,子进程接着写会往后追加。

僵尸进程

子进程结束时,父进程没有调用 wait() 回收系统资源,并将其从进程表删除,则该子进程就会成为僵尸进程。

僵尸进程太多,会导致进程表中有限的进程号被占用,不能产生新的进程。

使用Top命令查找,当zombie前的数量不为0时,即系统内存在相应数量的僵尸进程。

# 定位僵尸进程
ps -A -ostat,ppid,pid,cmd |grep -e '^[Zz]'

# 杀死僵尸进程,往往通过Kill杀不掉,此时需要杀死它的父进程,僵尸进程会被1号进程接管回收,正常退出。
kill -HUP pid
 

线程

线程是系统调度执行的最小单位,线程在linux内核对应一个 task_struct 结构,也就是一个PCB,所以线程调度也叫做进程调度。

每一个进程都需要一个主线程才能运行,主线程退出,进程也退出。

cpu 亲和性

线程可以与某个逻辑 cpu 绑定,通过设置 cpu 亲和性(affinity)实现。

Linux 内核 2.6 版本之后,支持开发人员通过编程设置 CPU 的硬亲和性(affinity),可以显式地指定线程在哪些处理器上运行。

  • 单个线程绑定在一个核上,可以避免 cpu缓 存因切换导致失效,提高缓存命中率。
  • 两个数据相关的线程绑定到一个核上,可以共享 cpu 缓存,提高缓存命中率。
# 查看运行的线程
top -HP pid
 

进程调度

处理器的调度是以线程为单位的,但是内核中,线程是一个 task_struct 对象,笼统上也叫进程,所以线程调度也叫进程调度。

调度类型

调度类型包括主动调度和被动调度,但是满足以下共同点

  • 所有调度的发生都是出于内核态,中断也是出于内核态,不会有调度出现在用户态。
  • 所有调度的都在 schedule 函数中发生。

主动调度

进程主动触发以下情况,然后陷入内核态,最终调用 schedule 函数,进行调度

  • 当进程发生需要等待 IO 的系统调用,如read、write。
  • 进程主动调用 sleep 时。
  • 进程等待占用信用量或 mutex 时,注意 spin 锁不会触发调度,可能在空转。

被动调度

发生以下情况时会发生被动调度

  • cpu 时钟中断(一般是10ms一次,取决于cpu的主频),此时会通过 cfs 检查进程队列,如果当前占用 cpu 的进程的 vruntime 不是最小时,且超过 sched_min_granularity_ns(默认最小运行时间10ms,防止频繁切换),发生“被动调度”。
  • fork 出新进程时,此时会通过 cfs 算法检查进度队列,如果当前占用 cpu 的进程的 vruntime 不是最小时且超过 sched_min_granularity_ns(最小运行时间),发生“被动调度”。

休眠和唤醒

休眠(被阻塞)状态的进程处于不可执行的状态,进程休眠的原因有多种多样,但通常来说都是等待某一事件的发生,例如等待I/O, 等待设备输入等等。

内核对于休眠和唤醒的操作如下:

  • 休眠:进程首先把自己标记为休眠状态(TASK_INTERRUPTIBLE),然后从 cfs 调度中移除,并将进程放入等待队列。
  • 唤醒:进程被置为可执行状态(TASK_RUNNING),进程从等待队列移入 cfs 等待调度。

进程调度算法(CFS)

linux 内核 2.6 之后,普通进程的调度算法采用的是 CFS(Completely Fair Scheduler 完全公平调度算法)。

  • CFS 通过计算进程消耗的 cpu 时间(vruntime)来分配调度,vruntime = 实际运行时间 * 1024 / 进程权重。

  • linux 采用一颗红黑树,记录下每一个进程的 vruntime,需要调度时,从红黑树中取出一个 vruntime 最小的进程出来运行。

  • 进程权重由 nice 值决定,每个进程对应一个 nice 值,nice 值越大,权重越低。

  • 休眠进程被唤醒时,会重新初始化 vruntime,在 min_vruntime (初始时间)基础之上作出少量的补偿,所以 IO 密集型进程比计算密集型进程拥有更高的调度权。

上下文切换

进程发生调度时,系统需要保存恢复上下文信息,需要完成的工作有:

  • 切换页表全局目录
  • 切换内核态堆栈
  • 切换cpu上下文,包括程序计数器和通用寄存器
  • 刷新TLB(快表)

进程上下文切换会带来额外的开销,也会导致一些缓存失效,降低运行速度。

  • TLB失效:TLB被刷新后,cpu需要访问页表来实现虚拟地址到物理地址的转换,性能降低。
  • 缓存失效:进程切换导致 cpu 缓存失效,产生cache miss。
0条评论
作者已关闭评论
董明高
6文章数
2粉丝数
董明高
6 文章 | 2 粉丝
原创

详解处理器调度原理

2023-05-29 01:37:26
63
0

cpu 结构

cpu 在结构上包括:控制器、运算器、总线、高速缓存。

控制器

控制器根据用户程序,依次从内存读取指令,译码后,根据确定的时序,向相应部件发出微操作控制信号。

  • 程序计数器:简称PC,保存从内存提取的下一条指令的地址。
  • 指令寄存器:根据程序计数器里的地址取指令,将取到的指令放入指令寄存器,然后更新程序计数器。
  • 指令编译器:用于对指令进行译码。
  • 主存地址寄存器:保存cpu当前正要访问的内存地址,通过总线跟主存通信。
  • 通用寄存器:用于暂时存放数据或者指令。

运算器

接收控制器的指令,进行运算,将运算结果保存到通用寄存器,并写回主存。

  • ALU:算数运算单元,是运算器的核心,能完成常见的位运算(与、或、非、左移、右移)和算术运算(加、减、乘、除)。
  • 通用寄存器:用来保存参加运算的操作数和运算的结果。
  • 状态字寄存器:存放运算状态(条件码、进位、溢出)和运算控制信息。

总线

总线是cpu和外围设备(内存、IO设备)数据交换的通道,分为地址总线和数据总线。

高速缓存

高速缓存旨在解决cpu时钟周期和主存(RAM)访问速度不匹配的问题,cpu将频繁访问的内存数据放入高速缓存,可以提高访问速度。

缓存行

数据在缓存和主存之间,是按固定大小的块传输的,该块称为缓存行(cache line),一个 cache line 占 64 字节。

三级缓存

高速缓存分三级(L1、L2、L3),cpu访问数据顺序为:cpu -> 寄存器 -> L1缓存 -> L2缓存 -> L3缓存 -> 主存。

  • L1缓存:速度最快(1ns),容量最小(32K);对应每个物理线程(可以通过超线程技术,将1个核心模拟成多个物理线程)
  • L2缓存:速度其次(3ns);容量其次(256k);对应每个cpu核,同一个核下的多个物理线程共享一个L2缓存
  • L3缓存:速度最慢(12ns);容量最大(30M);对应每个cpu插槽;同一个物理cpu共享一个L3缓存。

缓存一致性

多个物理线程对同一块主存数据更改时,可能会产生数据不一致,而cpu提供了保证各个缓存一致性的机制。

写传播

某个cpu修改了缓存行后,会通过总线store到内存,因为总线是各个cpu共享,所以修改会被其他cpu监控到,并从内存更新各自对应的缓存行,保证了一致性。

cache

MESI 协议

为了提高写传播的效率,cpu 设计了 MESI 协议,每个缓存行都对应四种状态。

  • Modify(修改):表示缓存已经被修改,和主存不一致,等待同步到主存;当监听到其他cpu的读取操作时,写回主存,并设置为 Shared 状态、
  • Exclusive(独占):表示当前修改已经同步到主存;当监听到其他 cpu 的读取操作,将状态改为 Shared。
  • Shared(共享):表示当前状态和主存一致;监听其他 cpu 的写操作时,将状态标记为 Invalid 状态。
  • Invalid(失效):其他cpu修改了缓存行,导致本 cpu 中的缓存行失效。

cpu 管理

物理 cpu

主板上实际插入的 cpu ,查看物理 cpu 个数

cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
 

cpu 核心

cpu 的组成是由运算器和控制器组成,每个核心都有一组控制器和运算器,查看每个物理 cpu 的核心数

cat /proc/cpuinfo |grep "cores"|uniq
 

逻辑 cpu

top 命令中显示的 cpu,代表可独立执行的单元,逻辑 cpu 个数 = 物理 CPU 个数 * 每个物理 CPU 的核心数 * 超线程数量

cat /proc/cpuinfo| grep “processor”|wc -l
 

cpu 平均负载

通过 top 查看 load average,3个值分别代表1分钟,5分钟,15分钟的load情况,重点关注的是后两个。

load average 是一段时间内处于可运行状态和不可中断状态的线程平均数量,该值等于逻辑cpu个数时,说明各个核心刚好打满,也没有排队现象,超过逻辑cpu个数时说明负载变高,有进程排队等待。

cpu 利用率

cpu 利用率代表 cpu 时间的占用多少,top 命令查看时,如果是 4 个逻辑 cpu,则 cpu 利用率最高是 400%。

cpu 利用率和 cpu 负载没有直接关系,cpu 利用率反应的是 cpu 的占用时间,load 反应的是执行 cpu 运算的任务平均数。

  • load 高利用率低:使用cpu的线程数很多,但是大部分都在等待IO返回,对应IO密集型服务。
  • load 低利用率高:工作线程数很少,但是占用时间很长,对应计算密集型服务,或者出现了死循环。

排查 cpu 热点

找出占用率高的线程

top -H -p 进程id

查看系统调用堆栈,找到对应的线程,分析上下文问题
pstack 进程id

利用strace跟踪具体线程的系统调用
strace -p 线程id

查看系统调用和花费时间,找出最占用cpu的系统调用
strace -T -r -c -p 线程id
 

调度原理

进程

进程是资源分配的最小单位,资源包括虚拟内存空间(代码段,数据段,堆)、文件描述符、页表、信号等内核资源。

操作系统用 PCB(process control block)来表示进程,linux 中用 task_struct 结构体表示 PCB,内核调度描述的进程实际上是线程,进程id就是主线程的pid,多个 task_struct 共享虚拟内存等资源指针。

struct task_struct {
	long			  state;
	struct mm_struct          *mm;
	pid_t			  pid;
	struct task_struct __rcu  *parent;
	struct list_head	  children;
	struct files_struct	  *files;
};
 

子进程

父进程通过 fork() 创建子进程,子进程会继承父进程的资源:

  • 物理内存:当二者任意一个修改内存时,系统会复制一个新的物理内存分配给修改的进程,叫做写时复制。
  • 文件描述符:此时父子进程共享文件的当前偏移,父进程向文件写内容,子进程接着写会往后追加。

僵尸进程

子进程结束时,父进程没有调用 wait() 回收系统资源,并将其从进程表删除,则该子进程就会成为僵尸进程。

僵尸进程太多,会导致进程表中有限的进程号被占用,不能产生新的进程。

使用Top命令查找,当zombie前的数量不为0时,即系统内存在相应数量的僵尸进程。

# 定位僵尸进程
ps -A -ostat,ppid,pid,cmd |grep -e '^[Zz]'

# 杀死僵尸进程,往往通过Kill杀不掉,此时需要杀死它的父进程,僵尸进程会被1号进程接管回收,正常退出。
kill -HUP pid
 

线程

线程是系统调度执行的最小单位,线程在linux内核对应一个 task_struct 结构,也就是一个PCB,所以线程调度也叫做进程调度。

每一个进程都需要一个主线程才能运行,主线程退出,进程也退出。

cpu 亲和性

线程可以与某个逻辑 cpu 绑定,通过设置 cpu 亲和性(affinity)实现。

Linux 内核 2.6 版本之后,支持开发人员通过编程设置 CPU 的硬亲和性(affinity),可以显式地指定线程在哪些处理器上运行。

  • 单个线程绑定在一个核上,可以避免 cpu缓 存因切换导致失效,提高缓存命中率。
  • 两个数据相关的线程绑定到一个核上,可以共享 cpu 缓存,提高缓存命中率。
# 查看运行的线程
top -HP pid
 

进程调度

处理器的调度是以线程为单位的,但是内核中,线程是一个 task_struct 对象,笼统上也叫进程,所以线程调度也叫进程调度。

调度类型

调度类型包括主动调度和被动调度,但是满足以下共同点

  • 所有调度的发生都是出于内核态,中断也是出于内核态,不会有调度出现在用户态。
  • 所有调度的都在 schedule 函数中发生。

主动调度

进程主动触发以下情况,然后陷入内核态,最终调用 schedule 函数,进行调度

  • 当进程发生需要等待 IO 的系统调用,如read、write。
  • 进程主动调用 sleep 时。
  • 进程等待占用信用量或 mutex 时,注意 spin 锁不会触发调度,可能在空转。

被动调度

发生以下情况时会发生被动调度

  • cpu 时钟中断(一般是10ms一次,取决于cpu的主频),此时会通过 cfs 检查进程队列,如果当前占用 cpu 的进程的 vruntime 不是最小时,且超过 sched_min_granularity_ns(默认最小运行时间10ms,防止频繁切换),发生“被动调度”。
  • fork 出新进程时,此时会通过 cfs 算法检查进度队列,如果当前占用 cpu 的进程的 vruntime 不是最小时且超过 sched_min_granularity_ns(最小运行时间),发生“被动调度”。

休眠和唤醒

休眠(被阻塞)状态的进程处于不可执行的状态,进程休眠的原因有多种多样,但通常来说都是等待某一事件的发生,例如等待I/O, 等待设备输入等等。

内核对于休眠和唤醒的操作如下:

  • 休眠:进程首先把自己标记为休眠状态(TASK_INTERRUPTIBLE),然后从 cfs 调度中移除,并将进程放入等待队列。
  • 唤醒:进程被置为可执行状态(TASK_RUNNING),进程从等待队列移入 cfs 等待调度。

进程调度算法(CFS)

linux 内核 2.6 之后,普通进程的调度算法采用的是 CFS(Completely Fair Scheduler 完全公平调度算法)。

  • CFS 通过计算进程消耗的 cpu 时间(vruntime)来分配调度,vruntime = 实际运行时间 * 1024 / 进程权重。

  • linux 采用一颗红黑树,记录下每一个进程的 vruntime,需要调度时,从红黑树中取出一个 vruntime 最小的进程出来运行。

  • 进程权重由 nice 值决定,每个进程对应一个 nice 值,nice 值越大,权重越低。

  • 休眠进程被唤醒时,会重新初始化 vruntime,在 min_vruntime (初始时间)基础之上作出少量的补偿,所以 IO 密集型进程比计算密集型进程拥有更高的调度权。

上下文切换

进程发生调度时,系统需要保存恢复上下文信息,需要完成的工作有:

  • 切换页表全局目录
  • 切换内核态堆栈
  • 切换cpu上下文,包括程序计数器和通用寄存器
  • 刷新TLB(快表)

进程上下文切换会带来额外的开销,也会导致一些缓存失效,降低运行速度。

  • TLB失效:TLB被刷新后,cpu需要访问页表来实现虚拟地址到物理地址的转换,性能降低。
  • 缓存失效:进程切换导致 cpu 缓存失效,产生cache miss。
文章来自个人专栏
文章 | 订阅
0条评论
作者已关闭评论
作者已关闭评论
0
0