内核启动过程中有如下打印信息:
CPU: PIPT / VIPT nonaliasing data cache, VIPT nonaliasing instruction cache
这行打印信息代表了处理器L1 CACHE所支持的寻址方式。
在kernel启动过程中,虽然这里第一次出现CACHE相关的打印信息,但是,此处并不是kernel第一次操作CACHE。
例如:对于zImage而言,解压缩过程中会开启CACHE并配置CACHE 写属性。
#ifdef CONFIG_CPU_DCACHE_WRITETHROUGH
#define CB_BITS 0x08
#else
#define CB_BITS 0x0c
#endif
CPU初始化的汇编文件head.S中,会根据kernel配置来使能CACHE。
#ifdef CONFIG_CPU_DCACHE_DISABLE
bic r0, r0, #CR_C
#endif
#ifdef CONFIG_CPU_BPREDICT_DISABLE
bic r0, r0, #CR_Z
#endif
#ifdef CONFIG_CPU_ICACHE_DISABLE
bic r0, r0, #CR_I
#endif
关于这条打印信息,在10几年前有过一次精彩的讨论。具体查看下面的PATCH。
[PATCH] arm: ARMv7 cache info printk
以上打印信息源自于 kernel 代码的setup.c中。
static void __init cacheid_init(void)
{
unsigned int arch = cpu_architecture();
if (arch >= CPU_ARCH_ARMv6) {
unsigned int cachetype = read_cpuid_cachetype();
if ((arch == CPU_ARCH_ARMv7M) && !(cachetype & 0xf000f)) {
cacheid = 0;
} else if ((cachetype & (7 << 29)) == 4 << 29) {
/* ARMv7 register format */
arch = CPU_ARCH_ARMv7;
cacheid = CACHEID_VIPT_NONALIASING;
switch (cachetype & (3 << 14)) {
case (1 << 14):
cacheid |= CACHEID_ASID_TAGGED;
break;
case (3 << 14):
cacheid |= CACHEID_PIPT;
break;
}
} else {
arch = CPU_ARCH_ARMv6;
if (cachetype & (1 << 23))
cacheid = CACHEID_VIPT_ALIASING;
else
cacheid = CACHEID_VIPT_NONALIASING;
}
if (cpu_has_aliasing_icache(arch))
cacheid |= CACHEID_VIPT_I_ALIASING;
} else {
cacheid = CACHEID_VIVT;
}
pr_info("CPU: %s data cache, %s instruction cache\n",
cache_is_vivt() ? "VIVT" :
cache_is_vipt_aliasing() ? "VIPT aliasing" :
cache_is_vipt_nonaliasing() ? "PIPT / VIPT nonaliasing" : "unknown",
cache_is_vivt() ? "VIVT" :
icache_is_vivt_asid_tagged() ? "VIVT ASID tagged" :
icache_is_vipt_aliasing() ? "VIPT aliasing" :
icache_is_pipt() ? "PIPT" :
cache_is_vipt_nonaliasing() ? "VIPT nonaliasing" : "unknown");
}
通过cpu_architecture()获取处理器内核版本号
int __pure cpu_architecture(void)
{
BUG_ON(__cpu_architecture == CPU_ARCH_UNKNOWN);
return __cpu_architecture;
}
处理器内核版本号通过数据结构proc_arch来维护,数据结构成员的值代表的ARM-vXXX
static const char *proc_arch[] = {
"undefined/unknown",
"3",
"4",
"4T",
"5",
"5T",
"5TE",
"5TEJ",
"6TEJ",
"7",
"7M",
"?(12)",
"?(13)",
"?(14)",
"?(15)",
"?(16)",
"?(17)",
};
处理器内核版本编号如下,例如__cpu_architecture为4则代表的是ARM-v5内核。
#define CPU_ARCH_UNKNOWN 0
#define CPU_ARCH_ARMv3 1
#define CPU_ARCH_ARMv4 2
#define CPU_ARCH_ARMv4T 3
#define CPU_ARCH_ARMv5 4
#define CPU_ARCH_ARMv5T 5
#define CPU_ARCH_ARMv5TE 6
#define CPU_ARCH_ARMv5TEJ 7
#define CPU_ARCH_ARMv6 8
#define CPU_ARCH_ARMv7 9
#define CPU_ARCH_ARMv7M 10
需要注意的这个范围内获取处理器内核版本号,不是通过读取MIDR寄存器来实现的。
针对具体的ARM 内核版本,获取CACHE所支持的寻址方式,并据此初始化cacheid。后面以cacheid为依据对CACHE的所支持的寻址方式进行分类。
在ARM处理器内部包含了CACHE类型信息的只读寄存器,这在芯片设计初期便已经定义好,后期无法修改。在这个寄存器中包含了CACHE查找策略。对于ARM v系列的处理器内核而言,获取CACHE类型的指令代码如下:
#define read_cpuid(reg) \
({ \
unsigned int __val; \
asm("mrc p15, 0, %0, c0, c0, " __stringify(reg) \
: "=r" (__val) \
: \
: "cc"); \
__val; \
})
基于读取CACHE类型寄存器得到信息初始化cacheid,利用cachetype.h中定义的宏,打印出cache类型。从kenrel启动的日志信息中可以看出,当前CPU内核的CACHE类型如下:
基于cacheid定义了如下的宏定义:
#define cache_is_vivt() cacheid_is(CACHEID_VIVT)
#define cache_is_vipt() cacheid_is(CACHEID_VIPT)
#define cache_is_vipt_nonaliasing() cacheid_is(CACHEID_VIPT_NONALIASING)
#define cache_is_vipt_aliasing() cacheid_is(CACHEID_VIPT_ALIASING)
#define icache_is_vivt_asid_tagged() cacheid_is(CACHEID_ASID_TAGGED)
#define icache_is_vipt_aliasing() cacheid_is(CACHEID_VIPT_I_ALIASING)
#define icache_is_pipt() cacheid_is(CACHEID_PIPT)
...
static inline unsigned int __attribute__((pure)) cacheid_is(unsigned int mask)
{
return (__CACHEID_ALWAYS & mask) |
(~__CACHEID_NEVER & __CACHEID_ARCH_MIN & mask & cacheid);
}
而本文开头中打印出来的信息,就是基于上面这些宏得出的结果。
通常来说,CACHE得寻址类型包括VIPT、PIPT、VIVT,而本文重点分析得是VIPT以及PIPT的工作原理。
具备MMU(TLB)、CACHE的ARM处理器,其地址翻译的流程如下:
那么上图中红框内的CACHE部分,根据索引或标签对应的是物理地址还是虚拟地址,将CACHE的寻址方式分为VIPT CACHE和PIPT CACHE。
VI的含义是使用虚拟地址来构建缓存索引
(index)。反之,PI的含义是使用物理地址来构建缓存索引。
缓存索引用于从缓存中提取标记,将它与从物理地址计算的缓存标记比对。如果匹配,则缓存中
命中
该查找,从缓存中提取相关的数据。否则,将从下一层缓存(或从内存)中提取。
PT的含义是使用物理地址来构建缓存标记
(tag),之所以使用PT作为CACHE查找索引是为了解决VIVT中存在的索引冲突,即两个进程可以为不同的物理地址使用相同的虚拟地址。
VIPT CACHE使用物理地址作为Tag,逻辑地址作为Index。通过Index查询CACHE获取到物理地址中的tag部分。同时呢,利用逻辑地址去查TLB,在TLB中获取到物理地址。然后将CACHE中查询到的物理地址Tag部分,同TLB中获取到的物理地址Tag部分作比较。若二者相同,则CACHE hit,否则CACHE miss。
对于VIPT CACHE而言,virtual_addr_A和virtual_addr_B虽然不同,但是它们指向了同一个物理地址PA。例如下面的代码:
mmap(virtual_addr_A,4096,prot,flags,file_descriptor,offset)
mmap(virtual_addr_B,4096,prot,flags,file_descriptor,offset)
VIPT使用虚拟地址作为CACHE Index,因此物理地址A的数据在CACHE中有两份,分别由virtual_addr_A和virtual_addr_B进行管理。这样的情形称之为CACHE 别名。
可以通过下面的4种方法来避免别名问题带来的影响:
当进行内存数据更新时进行cache invalid操作
多副本数据同步更新
以上两种方法需要进程通过虚实地址转换等操作,获取到是否有副本数据存在这样的信息。而VIPT的设计初衷是避免虚实地址转换,因此,这两种方法并不是非常可取。
缩小CACHE size
假设对于一个32bit位宽的虚拟地址而言,他的page offset为12bit。若CACHE size小于4K则不会产生别名问题,假设CACHE size大于4K,则会产生别名问题。但是CACHE过小,cache miss又会大幅增加。
page color或者cache cloring
若以上3种方法都没有解决CACHE别名问题,那么可以使用缓存着手的方法,这完全是一种软件层面的解决方案。
PIPT中的tag和index均为物理地址。而CPU发出的逻辑地址也称之为虚拟地址,因此,首先需要通过TLB或查询内存中的页表,将逻辑地址转换为对应的物理地址。再进行CACHE缓存查找。索引和标签都使用物理地址。虽然这很简单,避免了重名问题,但速度也很慢,因为必须先查找物理地址(这可能涉及TLB丢失和访问主内存),才能在缓存中查找该地址。