1 背景介绍
在x86架构中,我们对Time Stamp Counter (TSC) 寄存器非常熟悉,通过这个寄存器对代码执行时间的衡量可精确到CPU Cycle级别。
但在ARM/ARMv8/aarch64架构中,并没有与x86 TSC对应的寄存器和直接对应的汇编指令rdtsc。
若想在ARMv8架构中,统计计算代码执行时间达到CPU Cycle级别,也需要读取类似x86的TSC寄存器。在ARMv8中,有Performance Monitors Control Register系列寄存器,其中PMCCNTR_EL0就类似于x86的TSC寄存器。本文介绍Linux下读取ARM TSC方法。
读取这个PMCCNTR_EL0寄存器值,就可以知道当前CPU已运行了多少Cycle。但在ARM下读取CPU Cycle和x86有所不同:
x86用户态代码可以随便读取TSC值。但在ARM,默认情况是用户态是不可以读的,需要在内核态使能后,用户态才能读取。
开关在由寄存器PMCR_EL0控制。实际上这个寄存器控制整个PMU寄存器在用户态是否可读写,不仅仅是PMCCNTR_EL0。
2 PMU读写使能
使能TSC需要在内核权限下,因此有两种方式,一种为module的方式,另一种为将代码块移植入kernel,一下对两种方式进行介绍;
2.1 module方式使能
/* Enable user-mode ARM performance counter access. */ #include #include #include #define ARMV8_PMCR_MASK 0x3f #define ARMV8_PMCR_E (1 << 0) /* Enable all counters */#define ARMV8_PMCR_LC (1 << 6) /* Cycle Counter 64bit overflow*/static inline u32 armv8pmu_pmcr_read(void) { u64 val = 0; asm volatile("mrs %0, pmcr_el0" : "=r" (val)); return (u32)val; } static inline void armv8pmu_pmcr_write(u32 val) { val &= ARMV8_PMCR_MASK; isb(); asm volatile("msr pmcr_el0, %0" : : "r" ((u64)val)); } static inline long long armv8_read_CNTPCT_EL0(void){ long long val; asm volatile("mrs %0, CNTVCT_EL0" : "=r" (val)); return val;}static void enable_cpu_counters(void* data) { asm volatile("msr pmuserenr_el0, %0" : : "r"(0xf)); armv8pmu_pmcr_write(ARMV8_PMCR_LC|ARMV8_PMCR_E); asm volatile("msr PMCNTENSET_EL0, %0" :: "r" ((u32)(1<<31))); armv8pmu_pmcr_write(armv8pmu_pmcr_read() | ARMV8_PMCR_E|ARMV8_PMCR_LC); printk("\nCPU:%d ", smp_processor_id());}static void disable_cpu_counters(void* data) { printk(KERN_INFO "\ndisabling user-mode PMU access on CPU #%d", smp_processor_id()); /* Program PMU and disable all counters */ armv8pmu_pmcr_write(armv8pmu_pmcr_read() |~ARMV8_PMCR_E); asm volatile("msr pmuserenr_el0, %0" : : "r"((u64)0));}static int __init init(void) {/*u64 cval;u32 val;isb();asm volatile("mrs %0, PMCCNTR_EL0" : "=r"(cval));printk("\nCPU Cycle count:%llu \n", cval);asm volatile("mrs %0, PMCNTENSET_EL0" : "=r"(val));printk("PMCNTENSET_EL0:%X ", val);asm volatile("mrs %0, PMCR_EL0" : "=r"(val));printk("\nPMCR_EL0 Register:%X ", val);*/on_each_cpu(enable_cpu_counters, NULL, 1); printk(KERN_INFO "Enable Access PMU Initialized"); return 0; }static void __exit fini(void) { on_each_cpu(disable_cpu_counters, NULL, 1); printk(KERN_INFO "Access PMU Disabled"); }module_init(init); module_exit(fini);MODULE_LICENSE("GPL");MODULE_AUTHOR("alan");
2.2 将代码块添加至内核
static inline u32 armv8pmu_pmcr_read(void){ u64 val=0; asm volatile("mrs %0, pmcr_el0" : "=r" (val)); return (u32)val;}static inline void armv8pmu_pmcr_write(u32 val) { val &= 0x3f; isb(); asm volatile("msr pmcr_el0, %0" : : "r" ((u64)val));}static inline void enable_cpu_counters(void* data){ asm volatile("msr pmuserenr_el0, %0" : : "r"(0xf)); armv8pmu_pmcr_write((1 << 6) | (1 << 0)); asm volatile("msr PMCNTENSET_EL0, %0" :: "r" ((u32)(1 << 31))); armv8pmu_pmcr_write(armv8pmu_pmcr_read() | (1 << 0) | (1 << 6)); printk("\nCPU:%d ", smp_processor_id());}static inline void enable_pmu_pmccntr(void){ on_each_cpu(enable_cpu_counters, NULL, 1); printk(KERN_INFO "Enable Access PMU Initialized");}
将以上代码段放入init/main.c文件中,在init/main.c文件的kernel_init()接口进行调用,如下图所示:
3 应用层调用
#define _GNU_SOURCE#include #include #include #include #include #include #include #include #include #define CPU_MASK 0static inline uint64_t arm64_pmccntr(void){ uint64_t tsc; asm volatile("mrs %0, pmccntr_el0" : "=r"(tsc)); return tsc;}static inline uint64_t rdtsc(void){ return arm64_pmccntr();}// 进程亲和-OK#if 0void cpu_affinty_set(void){ cpu_set_t mask; //CPU掩码 CPU_ZERO(&mask); //初始化set集,将set置为空 CPU_SET(CPU_MASK, &mask); //将本进程绑定到CPU0上 if (sched_setaffinity(0, sizeof(mask), &mask) == -1) { printf("Set CPU affinity failue, ERROR:%s\n", strerror(errno)); }}int main(void){cpu_affinty_set();while (1){uint64_t val = rdtsc();printf("rdtsc:%lu\n", val);usleep(1000);}}#endif// 使用线程方式绑定核0static int app_set_affinity(int coreid){cpu_set_t cpuset;CPU_ZERO(&cpuset);CPU_SET(coreid, &cpuset);return pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);}static void* get_cycle_loop(void *arg){if (app_set_affinity(0)){perror("app_set_affinity failed");}while (1){uint64_t val = rdtsc();printf("rdtsc:%lu\n", val);usleep(1000);}}int main(void){pthread_t tid;if (pthread_create(&tid, NULL, get_cycle_loop, NULL)){perror("pthread_create failed");}while (1){sleep(10);}}
4 注意事项
a. 在多核系统中,每个CPU有自己独立的PMU寄存器,并且每个CPU的cycle值是不一样的,所以在获取cycle值时,前后应该位于同一个线程,该线程需要亲和到某个CPU上;
b. 如果将使能代码段放于内核代码块时,需要注意enable_pmu_pmccntr接口调用位置,应该在内核完全启动后进行调用,例如在start_kernel调用时,只使能了主核的cycle获取功能,当应用层将线程绑定到其他核时,获取cycle值出现指令非法问题;
5 参考文章
https://ilinuxkernel.com/?p=1755