CPU如何读取数据的?
CPU访问L1 Cache速度比访问内存快100倍,有高速缓存的目的:把Cache作为CPU与内存之间的缓存层,减少对内存的访问频率
所有CPU Cache Line是CPU从内存读取数据到Cache的单位。 64字节
CPU加载数组里面连续的多个数据到Cache里,因此应该按照物理内存地址分布的顺序去访问元素,Cache命中率就会提高。在我们不使用数组,而是使用单独的变量的时候,会由Cache伪共享的问题,Cache伪共享问题上是一个性能杀手,我们应该规避。
伪共享是什么,如何避免?
双核心CPU,两个CPU核心并行运行两个不同线程,同时从内存读取两个不同数据,分别是类型为long的变量A和B,这个两个数据的地址在物理内存上是连续的,如果Cache Line 是64字节,并且变量A在Cache Line的开头,那么这两个数据位于同一个Cache Line 中,因为Cpu Cache Line是CPU从内存读取数据到Cache的单位,所以这两个数据被同时读入到两个CPU核心中各自Cache中。
分析伪共享的问题
分析MESI协议:
1.最开始变量A和B都还不在Cache里面,假设1号核心绑定了线程A,2号核心绑定线程B,线程A只会读写变量A,线程B只会读写变量B
2.1号核心读取变量A,由于CPU从内存读取数据到Cache的单位是Cache Line,也正好A和B同属一个Cache Line,所以A和B的数据都会加载到Cache,并标记Cache Line 为独占
3.2号核心开始从内存里读取变量B,同样读取Cache Line大小的数据到Cache,也包含了A和B,此时1和2号核心的Cache Line变为共享
4.1号核心需要修改变量A,发现是共享状态,广播给2号核心,通知2号核心把Cache中对应的Cache Line标记为[已失效],然后1号核心对应的Cache Line状态变成已修改,并修改变量A。
5.2号核心修改变量B,发现2号核心的Cache 对应的Cache Line已失效,另外1号核心也有相同数据,且为已修改,所以要先把1号核心的Cache 对应的Cache Line写回内存,然后2号核心再从内存读取Cache line大小的数据到Cache,最后把变量B修改到2号核心的Cache,并标记为已修改。
虽然变量 A 和 B 之间其实并没有任何的关系,但是因为同时归属于一个 Cache Line ,这个 Cache Line 中的任意数据被修改后,都会相互影响,从而出现 ④ 和 ⑤ 这两个步骤。
因此,这种因为多个线程同时读写同一个Cache Line的不同变量时,而导致CPU Cache失效的现象称为伪共享
避免伪共享的方法
对于经常修改的数据,避免放在同一个Cache Line中,否则就会伪共享
Linux中,__cacheline_aligned_in_smp宏定义,用于解决伪共享问题。
- 如果在多核系统中,该宏定义是__cacheline_aligned,也就是Cache Line的大小;
- 而在单核系统里,该宏定义是空的
举例:
struct test {
int a;
int b;
}
a,b物理地址连续,可能位于同意个Cache Line
为了防止伪共享
struct test {
int a;
int b __cacheline_aligned_in_smp;
}
这样就不会再同一个Cache line中了,消耗空间,提升性能
CPU是如何选择线程的?
Linux内核中,进程和线程都是用task_struct结构体表示的,区别在于线程的task_struct结构体里部分资源是共享了进程已创建的资源,如内存地址空间,代码段,文件描述符等。Linux的线程被称为轻量级进程。
没有创建线程的进程,只有单个执行流,被称为主线程。如果想让进程处理多个事情,可以创建多个线程分别去处理,但不管怎样,它们对应到内核里都是task_struct
所以,Linux内核里的 调度器,调度的对象就是task_struct,把这个数据结构称为任务。
根据任务优先级以及相应要求分为两种,优先级的数值越小,优先级越高
- 实时任务,对系统的响应时间要求很高,也就是要尽可能快的执行实时任务,优先级在0-99
- 普通任务,响应时间没有很高要求,优先级在100-139
调度类
分为了这几种调度类,如下图。
Deadline和Realtime这两个调度类,都是应用于实时任务的,这两个调度类的调度策略合起来由这三种,作用如下:
SCHED_DEADLINE:按照deadline进行调度,举例当前时间点最近的deadline的任务会优先调度
SCHED_FIFO:对于相同优先级的任务,按先来先服务,但是优先级更高的任务可以插队
SCHED_RR:对于相同优先级的任务,轮流的运行,每个任务有一定的时间片,当完成时间片的任务会被放到队列尾部,保证相同优先级任务的公平性,但是高优先级的任务依然可以插队。
Fair调度类是应用与普通任务,都是由CFS调度器管理的,分为两种调度策略:
SCHED_NORMAL:普通任务使用的调度策略
SCHED_BATCH:后台任务的调度策略,不和终端进行交互,因此在不影响其他需要交互的任务,可以适当降低优先级。
完全公平的调度
基于CFS的调度算法,完全公平调度(Completely Fair Scheduling)。
想让分配给每个任务的CPU时间是一样的,于是它为每个任务安排了一个虚拟运行时间vruntime,运行越久,该任务的vruntime会越大,而没有被运行的任务,Vruntim 是不会变化的。
在CFS算法调度的时候,会优先选择vruntime少的任务,同时计算虚拟运行时间还要考虑普通任务的权重值(nice级别越低,权重值越大)
CPU运行队列
多任务的数量基本都远超CPU核心数量,因此需要排队
事实上,每个CPU都有自己的运行队列(Run Queue,rq),用于描述在此CPU上所运行的所有进程,其队列包含三个运行队列,Deadline运行队列dl_rq,实时任务运行队列rt_rq和CFS运行队列cfs_rq,其中cfs_rq是用红黑树来描述的,按Vruntime大小来排序的,最左侧的叶子节点,就是下次会被调度的任务。
调整优先级
如果启动任务,默认都是普通任务,调度类是Fair,由CFS调度器管理:实现任务运行的公平性,保障每个任务的运行的时间差不多的。
总结
理解 CPU 是如何读写数据的前提,是要理解 CPU 的架构,CPU 内部的多个 Cache + 外部的内存和磁盘都就构成了金字塔的存储器结构,在这个金字塔中,越往下,存储器的容量就越大,但访问速度就会小。
CPU 读写数据的时候,并不是按一个一个字节为单位来进行读写,而是以 CPU Cache Line 大小为单位,CPU Cache Line 大小一般是 64 个字节,也就意味着 CPU 读写数据的时候,每一次都是以 64 字节大小为一块进行操作。
因此,如果我们操作的数据是数组,那么访问数组元素的时候,按内存分布的地址顺序进行访问,这样能充分利用到 Cache,程序的性能得到提升。但如果操作的数据不是数组,而是普通的变量,并在多核 CPU 的情况下,我们还需要避免 Cache Line 伪共享的问题。
所谓的 Cache Line 伪共享问题就是,多个线程同时读写同一个 Cache Line 的不同变量时,而导致 CPU Cache 失效的现象。那么对于多个线程共享的热点数据,即经常会修改的数据,应该避免这些数据刚好在同一个 Cache Line 中,避免的方式一般有 Cache Line 大小字节对齐,以及字节填充等方法。
系统中需要运行的多线程数一般都会大于 CPU 核心,这样就会导致线程排队等待 CPU,这可能会产生一定的延时,如果我们的任务对延时容忍度很低,则可以通过一些人为手段干预 Linux 的默认调度策略和优先级。