CPU多核心及缓存

cpu多核真相

物理CPU:主板上真正安装的CPU的个数

物理核:一个CPU会集成多个物理核心

逻辑核:超线程技术可以把一个物理核虚拟出来多个逻辑核

超线程里的2个逻辑核实际上是在一个物理核上运行的,模拟双核心运作,共享该物理核的L1和L2缓存。物理计算能力并没有增加,超线程技术只有在多任务的时候才能提升机器核整体的吞吐量。而且据Intel官方介绍,相比实核,平均性能提升只有20-30%30%左右。

通过top命令看到的CPU核是逻辑核,linux下可以通过/proc/cupinfo来查看更加详细的信息:

1
2
3
4
5
#cat /proc/cpuinfo | grep "physical id" | sort | uniq #查看物理cpu个数

#cat /proc/cpuinfo| grep "cpu cores"| uniq #查看每个cpu的物理核数量

#cat /proc/cpuinfo | grep -E "core id|process|physical id" #查看逻辑核数量

缓存

286之前的时代的CPU本是没有缓存的,因为当时的CPU和内存速度差异没有现在这么大,CPU直接访问内存。但是到386时代,CPU和内存的速度不匹配了,第一次出现了缓存。而且最早的缓存并没有放在CPU模块里,而是放在主板上的。再往后CPU越来越快,现在CPU的速度比内存要快百倍以上,所以就逐步演化出了L1、L2、L3三级缓存结构,而且都集成到的CPU芯片里,以进一步提高访问速度。

现代Intel的CPU架构的基本结构:

cpu核心架构

L1最接近于CPU,速度也最快,但是容量最小。一般现代CPU的L1会分成两个,一个用来cache data,一个用来cache code,这是因为code和data的更新策略并不相同,而且因为CISC的变长指令,code cache要做特殊优化。一般每个核都有自己独立的data L1和code L1。
越往下,速度越慢,容量越大。L2一般也可以做到每个核一个独立的。但是L3一般就是整颗CPU共享的了。

linux下实际查看

Linux的内核的开发者定义了一套框架模型来完成这一目的,它就是CPUFreq系统。CPUFreq提供的sysfs接口,可以看到比/proc/cpuinfo更为详细的CPU详细信息。

1
2
3
4
5
6
7
# cd /sys/devices/system/cpu/;ll
drwxr-xr-x 9 root root 0 4月 8 14:15 cpu0/
drwxr-xr-x 9 root root 0 4月 8 14:15 cpu1/
drwxr-xr-x 9 root root 0 4月 8 14:15 cpu2/
drwxr-xr-x 9 root root 0 4月 8 14:15 cpu3/
drwxr-xr-x 9 root root 0 4月 8 14:15 cpu4/
......

L1一级缓存查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# cat cpu0/cache/index0/level
1
# cat cpu0/cache/index0/size
32K
# cat cpu0/cache/index0/type
Data
# cat cpu0/cache/index0/shared_cpu_list
0,4
# cat cpu0/cache/index1/level
1
# cat cpu0/cache/index1/size
32K
# cat cpu0/cache/index1/type
Instruction
# cat cpu0/cache/index1/shared_cpu_list
0,4

从上面的level接口可以看出index0和index1都是一级缓存,只不过一个是Data数据缓存,一个是Instruction也就是代码缓存
上面提到的是每个Core是独立的L1缓存,为什么shared_cpu_list显示有共享?这里看到的cpu0并不是物理Core,而是逻辑核,都是超线程技术虚拟出来的。实际上cpu0和cpu4是属于一个物理Core,所以每个Data L1和Instruction是这两个逻辑核共享的。本台电脑总共是有4个Data L1,4个Instrunction L1,大小都是32K。

L2二级缓存查看:

1
2
3
4
5
6
# cat cpu0/cache/index2/size
256K
# cat cpu0/cache/index2/type
Unified
# cat cpu0/cache/index2/shared_cpu_list
0,4

二级缓存要比一级缓存大不少,有256K,但是不分Data和Instruction。另外L2和L1一样,也是总共有4个,每两个逻辑核共享一个L2。

L3三级缓存查看:

1
2
3
4
5
6
# cat cpu0/cache/index3/size
8192K
# cat cpu0/cache/index3/type
Unified
# cat cpu0/cache/index3/shared_cpu_list
0-7

L3达到了8M,买CPU的时候商品里能看到的缓存属性一般告诉的就是L3属性。因为L3要比L2和L1看起来要大的多。但实际上我的这台电脑里L3只有以个,每个CPU各一个,不像是L2、L1有很多。

另外,Linux上还有个dmidecode命令,也能查看到一些关于CPU缓存的信息

1
$ sudo dmidecode -t cache

Cache Line

Cache Line是本级缓存向下一层取数据时的基本单位。可以通过如下方式查看:

1
2
3
4
5
6
7
8
9
# cd /sys/devices/system/cpu/;ll
# cat cpu0/cache/index0/coherency_line_size
64
# cat cpu0/cache/index1/coherency_line_size
64
# cat cpu0/cache/index2/coherency_line_size
64
# cat cpu0/cache/index3/coherency_line_size
64

可以看到L1、L2、L3的Cache Line大小都是64字节(注意是字节。内存中的实际情况是:一次IO其实吐出来的只有64比特,注意是比特。 一个cache line请求需要内存吐8次数据。 这个是64bit是由总线位宽决定的,没办法改)。就是说每次cpu从内存获取数据的时候,都是以该单位来进行的,哪怕只取一个bit,CPU也是给取一个Cache Line然后放到各级缓存里存起来。

TLB缓存

TLB(Translation Lookaside Buffer)是一个小的,虚拟寻址的缓存,其中每一行都保存着一个由单个PTE(Page Table Entry 页表项)组成的块。如果没有TLB,则每次取数据都需要两次访问内存,即查页表获得物理地址和取数据。缓存位于MMU内

虚拟内存

用户的视角里每个进程都有自己独立的地址空间,A进程的4GB和B进程4GB是完全独立不相关的,他们看到的都是操作系统虚拟出来的地址空间。但是呢,虚拟地址最终还是要落在实际内存的物理地址上进行操作的。操作系统就会通过页表的机制来实现进程的虚拟地址到物理地址的翻译工作。其中每一页的大小都是固定的

页表管理的两个关键点,分别是页面大小和页表级数

1.页面大小
在Linux下,通过如下命令可以查看到当前操作系统的页大小

1
2
# getconf PAGE_SIZE
4096

可以看到当前的Linux机器的页表是4KB的大小。

2.页表级数

1)如果页表级数越少,虚拟地址到物理地址的映射会很快,但是需要管理的页表项会很多,能支持的地址空间也有限。

2)相反页表级数越多,需要的存储的页表数据就会越少,而且能支持到比较大的地址空间,但是虚拟地址到物理地址的映射就会越慢

linux虚拟内存实现

32位系统的虚拟内存实现:二级页表

如果想支持32位的操作系统下的4GB进程虚拟地址空间,假设页表大小为4K,则共有2的20次方页面。如果采用速度最快的1级页表,对应则需要2的20次方个页表项。一个页表项假如4字节,那么一个进程就需要(1048576*4=)4M的内存来存页表项。
如果是采用2级页表,如图,则创建进程时只需要有一个页目录就可以了,占用(1024*4)=4KB的内存。剩下的二级页表项只有用到的时候才会再去申请。

二级页表

这样则只需要页目录1024个,页表项1024个,总共2028个页表管理条目,(2048*4=)8k就可以支持起4GB的地址空间转换。

64位系统的虚拟内存实现:四级页表

现在的操作系统需要支持的可是48位地址空间(理论上可以支持64位,但其实现在只支持到了48位,也足够用了),而且要支持成百上千的进程,如果不采用分级页表的方式,则创建进程时就需要为其维护一个2的36次方个页表项(64位Linux目前只使用了地址中的48位的,在这里面,最后12位都是页内地址,只有前36位才是用来寻找页表的), 2^36 *4Byte=32GB,这个更不能忍。也必须和32位系统一样,进一步提高页表的级数。

Linux在v2.6.11以后,最终采用的方案是4级页表,分别是:

  • PGD:page Global directory(47-39), 页全局目录
  • PUD:Page Upper Directory(38-30),页上级目录
  • PMD:page middle directory(29-21),页中间目录
  • PTE:page table entry(20-12),页表项

这样,一个64位的虚拟空间,初始创建的时候只需要维护一个29 大小的一个页全局目录就够了,现在的页表数据结构被扩展到了8byte。这个页全局目录仅仅需要(29 *8=)4K,剩下的中间页目录、页表项只需要在使用的时候再分配就好了。Linux就是通过这种方式支持起(2^48 =)256T的进程地址空间的。

TLB

使用多级页表虽然只需要4k的初始页目录就可以支持起一个256T的进程地址空间。但是,这也带来了额外的问题,页表是存在内存里的。那就是一次内存IO光是虚拟地址到物理地址的转换就要去内存查4次页表,再算上真正的内存访问,竟然需要5次内存IO才能获取一个内存数据!

CPU的L1、L2、L3的缓存思想一致,既然进行地址转换需要的内存IO次数多,且耗时。那么干脆就在CPU里把页表尽可能地cache起来不就行了么,所以就有了TLB(Translation Lookaside Buffer,位于MMU中,MMU位于CPU内),专门用于改进虚拟地址到物理地址转换速度的缓存。其访问速度非常快,和寄存器相当,比L1访问还快。

MMU

MMU(内存管理单元):包括从逻辑地址到虚拟地址(线性地址)再到内存地址的变换过程、页式存储管理、段式存储管理、段页式存储管理、虚拟存储管理(请求分页、请求分段、请求段页)。 MMU位于CPU内部,可以假想为一个进程的所需要的资源都放在虚拟地址空间里面,而CPU在取指令时,机器指令中的地址码部分为虚拟地址(线性地址),需要经过MMU转换成为内存地址,才能进行取指令。MMU完成两大功能:

  • 虚拟地址到内存地址的地址变换

  • 设置修改CPU对内存的访问级别。比如在Linux的虚拟地址空间中,3-4G为内核空间,访问级别最高,可以访问整个内存;而0-3G的用户空间只能访问用户空间的内容。其实这也是由MMU的地址变换机制所决定的。对于Inter(英特尔)CPU架构,CPU对内存的访问设置了4个访问级别:0、1、2、3,0最高,4最低。而Linux下,只是使用了CPU的两种级别:0、3。

CPU的状态属于程序状态字PSW的一位,系统模式(0),用户模式(1),CPU交替执行操作系统程序和用户程序。0级对应CPU的内核态(特权态、管态、系统态),而3级对应用户态(普通态或目态),这其实是对内核的一种保护机制。例如,在执行printf函数的时候,其本身是在用户空间执行,然后发生系统调用,调用系统函数write将用户空间的数据写入到内核空间,最后把内核的数据刷到(fsync)磁盘上,在这个过程中,CPU的状态发生了变化,从0级(用户态)到3级(内核态)。

有了TLB之后,CPU访问某个虚拟内存地址的过程如下:

  • CPU产生一个虚拟地址
  • MMU从TLB中获取页表,翻译成物理地址
  • MMU把物理地址发送给L1/L2/L3/缓存
  • L1/L2/L3/内存将地址对应数据返回给CPU

第2步是类似于寄存器的访问速度,所以如果TLB能命中,则虚拟地址到物理地址的时间开销几乎可以忽略。

扩展

因为TLB并不是很大,只有4k,而且现在逻辑核又造成会有两个进程来共享。所以可能会有cache miss的情况出现。而且一旦TLB miss造成的后果可比物理地址cache miss后果要严重一些,最多可能需要进行5次内存IO才行。

参考

https://mp.weixin.qq.com/s/PQTuFZO51an6OAe3WX4BVw

https://mp.weixin.qq.com/s/mssTS3NN7-w2df1vhYSuYw