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在内存地址上相邻。
- A只修改a,B只修改b。
- A读取a,由于ab相邻,他俩都被缓存到了一个Cache Line中,A Cache Line此时状态为Exclusive。
- B读取b,由于ab相邻,也被缓存到了B的Cache Line中,此时A Cache Line与B Cache Line变为Shared。
- A修改a,此时A Cache Line变为Modified,而B Cache Line变为Invalidate。
- 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,对响应的要求没那么高。
调度类
为了保障优先级高的任务尽可能的被执行,于是分了以下几种调度类:
Deadline
与Realtime
都用于实时任务,对任务执行有以下三种策略:
- 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大小排序,最左侧的叶子节点,就是下次会被调度的任务。
这三个队列的调度也是有优先级的,dl_rq > rt_rq > cfs_rq,实时任务总要比普通任务优先被执行。
调整优先级
如果我们启动一个任务的时候没有指定优先级,默认被认为是普通任务,放在cfs_rq中,有CFS调度器来进行管理。
如果想要某个普通任务有更多的执行时间,可以调整任务的nice
值,从而让优先级高的任务拥有更多的执行时间。nice
值得设置范围为-20 ~ 19
,值越低则优先级越高。
上图可以看到普通任务的优先级范围为100139,nice值映射为-2019。
在启动任务时,可以指定nice值
1 | nice -n -3 /usr/sbin/mysqld |
也可以使用renice重新设置
1 | renice -n -10 /usr/sbin/mysqld |
当你启动一个普通任务时,优先级的值可以认为固定为120,你可以使用nice
来调整优先级
优先级(新)= nice + 优先级(old)
还记得前面的vruntime计算公式吗。
vruntime(虚拟运行时间) = delta_exec(实际运行时间)* NICE_0_LOAD/权重
权重与nice值之间是有转换表的
weight = 1024 / 1.25nice
每降低1nice值,将多获得10%CPU时间。