一 CPU是如何读写数据的?
1.1 CPU架构(组成)
当代CPU一般是多核心的,每个核心都有自己的一个L1和L2Cache,L3Cache是一个CPU所有核心共享的,一个CPU只有一个。L1Cache分为数据缓存和指令缓存。
CPU有三层高速缓存的目的就是将Cache作为CPU和内存间的缓存层,减少CPU访问内存的频率从而提高效率。 CPU从内存读取数据也是先读取到Cache,一次读取是读取一个Cache Line(缓存行)。所以说缓存行是CPU读取数据的最小单位。Cache Line的大小可以在Linux系统中查看和修改,默认是64byte。
所以说我们访问内存时候尽量按照物理内存地址数据的时候,尽量顺序访问,这样就可以命中率高,从而提高效率。
1.2 CPU伪共享问题
先介绍一下什么是伪共享问题?
多线程因为同时读写同一个CacheLine的不同变量时导致失效的现象。
假设现在有一个双核心CPU,这两个CPU核心并行运行两个不同的线程,同时从内存中读取两个不同的数据,分别是类型为 long 的变量 A 和 B,这个两个数据的地址在物理内存上是连续的,如果 Cahce Line 的大小是 64 字节,并且变量 A 在 Cahce Line 的开头位置,那么这两个数据是位于同一个 Cache Line 中,又因为 CPU Line 是 CPU 从内存读取数据到 Cache 的单位,所以这两个数据会被同时读入到了两个 CPU 核心中各自 Cache 中。 如果这两个不同核心的线程分别修改不同的数据,比如 1 号CPU 核心的线程只修改了 变量 A,或 2 号 CPU 核心的线程的线程只修改了变量 B,会发生什么呢? 结合保证多核缓存一致的 MESI 协议,来说明这一整个的过程:
●①.最开始变量 A 和 B 都还不在 Cache 里面,假设 1 号核心绑定了线程 A,2 号核心绑定了线程 B,线程 A 只会读写变量 A,线程 B 只会读写变量 B。
●②1 号核心读取变量 A,由于 CPU 从内存读取数据到 Cache 的单位是 Cache Line,也正好变量 A 和 变量 B 的数据归属于同一个 Cache Line,所以 A 和 B 的数据都会被加载到 Cache,并将此 Cache Line 标记为「独占」状态。
●③接着,2 号核心开始从内存里读取变量 B,同样的也是读取 Cache Line 大小的数据到 Cache 中,此 Cache Line 中的数据也包含了变量 A 和 变量 B,此时 1 号和 2 号核心的 Cache Line 状态变为「共享」状态。
●④1 号核心需要修改变量 A,发现此 Cache Line 的状态是「共享」状态,所以先需要通过总线广播发送消息给 2 号核心,通知 2 号核心把 Cache 中对应的 Cache Line 标记为「已失效」状态,然后 1 号核心对应的 Cache Line 状态变成「已修改」状态,并且修改变量 A。
●⑤之后,2 号核心需要修改变量 B,此时 2 号核心的 Cache 中对应的 Cache Line 是已失效状态,另外由于 1 号核心的 Cache 也有此相同的数据,且状态为「已修改」状态,所以要先把 1 号核心的 Cache 对应的 Cache Line 写回到内存,然后 2 号核心再从内存读取 Cache Line 大小的数据到 Cache 中,最后把变量 B 修改到 2 号核心的 Cache 中,并将状态标记为「已修改」状态。
所以,可以发现如果 1 号和 2 号 CPU 核心这样持续交替的分别修改变量 A 和 B,就会重复 ④ 和 ⑤ 这两个步骤,Cache 并没有起到缓存的效果,虽然变量 A 和 B 之间其实并没有任何的关系,但是因为同时归属于一个 Cache Line ,这个 Cache Line 中的任意数据被修改后,都会相互影响,从而出现 ④ 和 ⑤ 这两个步骤。
1.3 避免伪共享解决方案
所以为了避免出现伪共享的问题就得让数据避免出现在一个Cache Line。 Linux中为了防止出现这种问题是采用了宏定义的方案,定义一个宏来指定变量的对齐方式,确保变量被存储在不同的缓存行中,避免不必要的缓存行竞争。具体不赘述。
常用的解决伪共享问题的方案还有使用缓存行填充和缓存行隔离技术。
二 CPU如何选择线程?
2.1 进程和线程
进程是分配资源的基本单位,也是调度的基本单位。 进程有自己的PCB,线程有自己的TCB,TCB们用链表管理起来被置在PCB中。本质都是一个结构体,存储着一些变量。不过线程的资源都是用的把这个线程创建出来的进程的资源。所以线程又名轻量级进程。没有创建线程的进程,是只有单个执行流的,被称为主线程。主线程可以创建子线程(工作线程)。
2.2 普通任务和实时任务
Linux中,根据调度进程的优先级和响应要求,优先级数值越小,优先级越高。将调度进程分为两类:
●①实时任务:对系统的响应时间要求很高,要求尽快被执行,优先级在0~99。
●②对响应速度没太大的要求,优先级在100~139。
2.3 调度类
调度类通常是指负责任务调度和资源管理的一个类。调度类通常包含了各种方法和属性,用于实现任务的调度、资源的分配以及执行控制。
调度器是计算机系统中的一个重要组件,其作用是协调和管理系统资源的分配,以实现对任务和进程的调度和执行。
Linux为了保证高优先级的进程能够尽早的被执行,于是有了几种调度类:Deadline/Realtime/Fair Deadline和Realtime这两个调度类都是用于实时任务,这两个调度类的策略策略有三种。
Deadline的调度策略是sched_deadline。Deadline的调度器是Deadline调度器。
Realtime的调度策略是sched_fifo和sched_rr。Realtime的调度器是RT调度器。
Fair的调度策略是sched_normal和sched_batch。调度器是CFS调度器。
Deadline和Realtime是用于实时任务,Fair是用于普通任务。
2.4 完全公平调度
我们平日里遇到的基本都是普通任务,对于普通任务来说,公平性最重要,在 Linux 里面,实现了一个基于 CFS 的调度算法,也就是完全公平调度。
●这个算法的理念是想让分配给每个任务的 CPU 时间是一样,于是它为每个任务安排一个虚拟运行时间 vruntime,如果一个任务在运行,其运行的越久,该任务的 vruntime 自然就会越大,而没有被运行的任务,vruntime 是不会变化的。
●那么,在 CFS 算法调度的时候,会优先选择 vruntime 少的任务,以保证每个任务的公平性。
●这就好比,让你把一桶的奶茶平均分到 10杯奶茶杯里,你看着哪杯奶茶少,就多倒一些;哪个多了,就先不倒,这样经过多轮操作,虽然不能保证每杯奶茶完全一样多,但至少是公平的。
●当然,上面提到的例子没有考虑到优先级的问题,虽然是普通任务,但是普通任务之间还是有优先级区分的,所以在计算虚拟运行时间 vruntime 还要考虑普通任务的权重值,注意权重值并不是优先级的值,内核中会有一个 nice 级别与权重值的转换表,nice 级别越低的权重值就越大。所以虚拟运行时间(vruntime)+=实际运行时间*nice_0_load/权重,可以不用管 nice_0_load 是什么,就认为它是一个常量,那么在「同样的实际运行时间」里,高权重任务的 vruntime 比低权重任务的 vruntime 少,你可能会奇怪为什么是少的?你还记得 CFS 调度吗,它是会优先选择 vruntime 少的任务进行调度,所以高权重的任务就会被优先调度了,于是高权重的获得的实际运行时间自然就多了。
2.5 CPU运行队列
一个系统通常都会运行着很多任务(任务就是进程),多任务的数量基本都是远超 CPU 核心数量,因此这时候就需要排队。
事实上,每个 CPU 都有自己的运行队列,用于描述在此 CPU 上所运行的所有进程,其队列包含三个运行队列,Deadline 运行队列 、实时任务运行队列 和 CFS 运行队列 ,优先级Deadline > Realtime > Fair,这意味着 Linux 选择下一个任务执行的时候,会按照此优先级顺序进行选择,也就是说先从 Deadline运行队列里选择任务,然后从 实时任务运行队列里选择任务,最后从 CFS运行队列里选择任务。因此,实时任务总是会比普通任务优先被执行。
2.6 调度优先级
如果我们启动任务的时候,没有特意去指定优先级的话,默认情况下都是普通任务,在Linux系统中,默认情况下,如果没有特意指定任务的优先级,则新创建的任务通常会被视为普通任务。这些普通任务将按照默认的调度策略和调度参数来进行调度。普通任务的调度类是 Fail,由 CFS 调度器来进行管理。CFS 调度器的目的是实现任务运行的公平性,也就是保障每个任务的运行的时间是差不多的。
如果你想让某个普通任务有更多的执行时间,可以调整任务的 nice 值,从而让优先级高一些的任务执行更多时间。nice 的值能设置的范围是 -20~19, 值越低,表明优先级越高,因此 -20 是最高优先级,19 则是最低优先级,默认优先级是 0。nice 值并不是表示优先级,而是表示优先级的修正数值,它与优先级的关系是这样的:priority(new) = priority(old) + nice。内核中,priority 的范围是 0~139,越低,优先级越高,其中前面的 0~99 范围是提供给实时任务使用的,而 nice 值是映射到 100~139,这个范围是提供给普通任务用的,因此 nice 值调整的是普通任务的优先级。
在前面我们提到了,权重值与 nice 值的关系的,nice 值越低,权重值就越大,计算出来的 vruntime 就会越少,由于 CFS 算法调度的时候,就会优先选择 vruntime 少的任务进行执行,所以 nice 值越低,任务的优先级就越高。