在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。
使能TSC需要在内核权限下,因此有两种方式,一种为module的方式,另一种为将代码块移植入kernel,一下对两种方式进行介绍;
/* 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");
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()接口进行调用,如下图所示:
#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define CPU_MASK 0
static 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 0
void 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
// 使用线程方式绑定核0
static 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);
}
}
a. 在多核系统中,每个CPU有自己独立的PMU寄存器,并且每个CPU的cycle值是不一样的,所以在获取cycle值时,前后应该位于同一个线程,该线程需要亲和到某个CPU上;
b. 如果将使能代码段放于内核代码块时,需要注意enable_pmu_pmccntr接口调用位置,应该在内核完全启动后进行调用,例如在start_kernel调用时,只使能了主核的cycle获取功能,当应用层将线程绑定到其他核时,获取cycle值出现指令非法问题;
https://ilinuxkernel.com/?p=1755