0%

CPU是怎么执行任务的?

CPU是如何读写数据的?

先简单看下CPU的架构图:

CPU架构

一个CPU通常有多个CPU核心,每个核心又有自己L1 L2 CPU Cache,多个核心之间共享L3 CPU Cache,当缓存未命中后再去内存寻找,还没有则接着再去Disk层寻找,由上到下读写读写速度依次减慢。

更多关于CPU与内存交互的文章:

什么是CPU伪共享

伪共享的出现与CPU Cache的缓存机制脱不开关系,我们先看下CPU Cache是如何缓存数据的。
当CPU想要从内存读某个数据时,会先去CPU Cache寻找,若没有,CPU Cache则会把对应的内存数据缓存,而这里不会只缓存CPU需要的那一个数据,他会将相连范围的一小块数据都缓存下来。(为什么这样做呢?我们认为,与CPU刚刚使用过的数据所相邻的数据,很有可能会被CPU调用。缓存一整块数据可以减少CPU与内存直接交互的频率。)

那我们来看下伪共享是什么,举个例子。

假设有两个核心CPU在运行两个不同的线程,CPU A用到变量a,CPU B用到变量b,a和b在内存地址上相邻。

  1. A只修改a,B只修改b。
  2. A读取a,由于ab相邻,他俩都被缓存到了一个Cache Line中,A Cache Line此时状态为Exclusive。
  3. B读取b,由于ab相邻,也被缓存到了B的Cache Line中,此时A Cache LineB Cache Line变为Shared。
  4. A修改a,此时A Cache Line变为Modified,而B Cache Line变为Invalidate。
  5. B修改b,因为B为Invalidate,则先通知A将数据写回到内存中,并变为Invalidate,B从内存中重新读取,并对b做修改,变为Modified。

若上述4-5步骤交替重复的话,CPU Cache的机制便失效了,称其为伪共享。

怎么避免伪共享呢?

其实很简单,对于可能位于多线程并被高频率修改的数据尽量使其独占一个Cache Line就可以了,即空间换时间的做法。

Java Disruptor 的 RingBuffer也是这么做的,具体细节可自查。

CPU是如何选择线程的?

在Linux内核中,线程与进程都是用task_strcut结构体表示的,所以CPU是如何执行任务的,任务其实就是指就是task_strcut。在Linux中,线程共享了进程已经创建的资源,包括内存地址空间、代码段、文件描述符,等等。所以Linux的线程又被称为轻量级进程。

在Linux系统中,根据任务的优先级以及相应要求,分为以下两种:

  • 实时任务,优先级0~99,优先级很高。
  • 普通任务,优先级100~139,对响应的要求没那么高。

调度类

为了保障优先级高的任务尽可能的被执行,于是分了以下几种调度类:
调度类

DeadlineRealtime都用于实时任务,对任务执行有以下三种策略:

  • SCHED_DEADLINE:距离deadline越近的任务越先执行。
  • SCHED_FIFO:对于相同优先级的任务,按照先来先服务的原则,对应高优先级的任务,可以插队。
  • SCHED_RR:对于相同优先级的任务,轮流着运行,每个任务都有一定的时间片,用完时间片后就被放到尾部,对于高优先级的任务,同理可插队。

而 Fair 调度类是应用于普通任务,都是由 CFS 调度器管理的,分为两种调度策略:

  • SCHED_NORMAL:普通任务使用的调度策略;
  • SCHED_BATCH:后台任务的调度策略,不和终端进行交互,因此在不影响其他需要交互的任务,可以适当降低它的优先级。

CFS调度器(完全公平调度)

对于普通任务来说,只有公平才是最重要的。Linux中,实现了一个基于CFS(Completely Fair Scheduling)的调度算法,也就是完全公平调度。
该算法的理念是让分配给每个任务的CPU时间相同,它为每个任务定义了一个虚拟运行时间,vruntime,如果一个任务在运行,且运行时间越久,该任务的vruntime自然越大,而没有被运行的任务,vruntime是不会变化的。
那么,在CFS算法调度中,会优先选择vruntime少的任务,以保证每个任务的公平性。
上面的选择方式保证了每个任务的公平性,但是没有考虑优先级的问题,这里又引入了权重值的概念,用来调整vruntime的大小。权重值越大,vruntime就会被调整的越小。这里有一条计算公式。

vruntime(虚拟运行时间) = delta_exec(实际运行时间)* NICE_0_LOAD/权重
权重值是根据优先级转换得到的。

CPU运行队列

一个系统通常都会运行着很多任务,任务数量基本都远超核心数量,因此就需要排队。
事实上,每个CPU都有自己的运行队列(Run Queue,RQ),用于描述在此CPU上所运行的所有进程,其队列包含三个运行队列,Deadline(dl_rq)、Runtime(rt_rq)、CFS(cfs_rq)。其中cfs_
rq是用红黑树来描述的,按照vruntime大小排序,最左侧的叶子节点,就是下次会被调度的任务。

CPU队列

这三个队列的调度也是有优先级的,dl_rq > rt_rq > cfs_rq,实时任务总要比普通任务优先被执行。

调整优先级

如果我们启动一个任务的时候没有指定优先级,默认被认为是普通任务,放在cfs_rq中,有CFS调度器来进行管理。

如果想要某个普通任务有更多的执行时间,可以调整任务的nice值,从而让优先级高的任务拥有更多的执行时间。nice值得设置范围为-20 ~ 19,值越低则优先级越高。

nice值与优先级的映射

上图可以看到普通任务的优先级范围为100139,nice值映射为-2019。
在启动任务时,可以指定nice值

linux
1
nice -n -3 /usr/sbin/mysqld

也可以使用renice重新设置

linux
1
renice -n -10 /usr/sbin/mysqld

当你启动一个普通任务时,优先级的值可以认为固定为120,你可以使用nice来调整优先级

优先级(新)= nice + 优先级(old)

还记得前面的vruntime计算公式吗。

vruntime(虚拟运行时间) = delta_exec(实际运行时间)* NICE_0_LOAD/权重
权重与nice值之间是有转换表的

权重nice转换表

weight = 1024 / 1.25nice

每降低1nice值,将多获得10%CPU时间。

-------- 本文结束 感谢阅读 --------