博主未授权任何人或组织机构转载博主任何原创文章,感谢各位对原创的支持!
博主链接
本人就职于国际知名终端厂商,负责modem芯片研发。
在5G早期负责终端数据业务层、核心网相关的开发工作,目前牵头6G算力网络技术标准研究。
博客内容主要围绕:
5G/6G协议讲解
算力网络讲解(云计算,边缘计算,端计算)
高级C语言讲解
Rust语言讲解
文章目录
- 如何在ARM上实现x86的rdtsc()函数
- 一、使用ARMv8提供的独立定时器CNTVCT_EL0
- 二、使用ARMv8的PMU计数器PMCCNTR_EL0
- 2.1 关键寄存器介绍
- PMCCNTR_EL0(Performance Monitors Cycle Count Register)
- PMCR_EL0(Performance Monitors Control Register)
- PMUSERENR_EL0(Performance Monitors User Enable Register)
- PMCNTENCLR_EL0(Performance Monitors Count Enable Clear register)
- 2.2 内核使能代码
- 2.3 用户态代码
- 2.4 测试时遇到的问题
如何在ARM上实现x86的rdtsc()函数
一、使用ARMv8提供的独立定时器CNTVCT_EL0
System counter是Arm64下独立于CPU core的计数器,在系统上电时,会给此计数器设置固定的频率。一个映射System counter计数器内容的寄存器为CNTVCT_EL0
,可在用户态下读取此寄存器获取counter值。而CNTFRQ_EL0
保存的是counter的频率值(详细内容参考《【ARMv8】通用定时器总结》)。通过下面的函数实现获取counter值及频率值:
static inline uint64_t
arm64_cntvct(void)
{
uint64_t tsc;
asm volatile("mrs %0, cntvct_el0" : "=r" (tsc));
return tsc;
}
static inline uint64_t
arm64_cntfrq(void)
{
uint64_t freq;
asm volatile("mrs %0, cntfrq_el0" : "=r" (freq));
return freq;
}
static inline uint64_t
rdtsc(void)
{
return arm64_cntvct();
}
但是System counter的精度从Armv8.0到Armv8.5,范围通常在1-50MHz;从Armv8.6开始,以1GHz的固定频率递增。虽然1GHz的频率已经足够高了,但是还是达不到CPU cycle级别的精度。
二、使用ARMv8的PMU计数器PMCCNTR_EL0
在ARMv8中,有Performance Monitors Control Register系列寄存器,其中PMCCNTR_EL0就类似于x86的TSC寄存器。但是如果想在用户态访问这些寄存器,需要在内核代码中开启PMU用户态访问开关。
2.1 关键寄存器介绍
PMCCNTR_EL0(Performance Monitors Cycle Count Register)
保存了处理器周期计数器的值,其结构如下:
PMCR_EL0(Performance Monitors Control Register)
PMU配置寄存器,其结果如下:
其中和我们关系密切的几个参数含义:
- LC:设置为1,表示开启64bit的周期计数器;否则,使用32bit的计数器(32bit的已经摒弃);
- D:设置为1,表示每64个时钟周期,计时器累加一次(已经摒弃);否则,每个时钟周期计数器累加一次;
- C:设置为1,表示重置计数器;
- E:设置为1,表示开启计数器PMCCNTR_EL0;
PMUSERENR_EL0(Performance Monitors User Enable Register)
用于开启或关闭用户态下是否可以访问PMU寄存器,相关结构如下:
其中和我们关系密切的几个参数含义:
- ER:设置为1,表示用户态下可以读写PMU寄存器;否则不可以读写;
- EN:设置为1,表示用户态软件可以访问所有PMU特定的寄存器;
PMCNTENCLR_EL0(Performance Monitors Count Enable Clear register)
设置启用的计数器和事件计数器,相关结构如下:
其中和我们关系密切的几个参数含义:
- C:设置为1,表示启用PMCCNTR_EL0计数器;
2.2 内核使能代码
/*
* Enable user-mode ARM performance counter access.
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/smp.h>
#define ARMV8_PMCR_MASK 0x3f
#define ARMV8_PMCR_E (1 << 0) /* Enable all counters */
#define ARMV8_PMCR_P (1 << 1) /* Reset all counters */
#define ARMV8_PMCR_C (1 << 2) /* Cycle counter reset */
#define ARMV8_PMCR_D (1 << 3) /* CCNT counts every 64th cpu cycle */
#define ARMV8_PMCR_X (1 << 4) /* Export to ETM */
#define ARMV8_PMCR_DP (1 << 5) /* Disable CCNT if non-invasive debug*/
#define ARMV8_PMCR_LC (1 << 6) /* Cycle Counter 64bit overflow*/
#define ARMV8_PMCR_N_SHIFT 11 /* Number of counters supported */
#define ARMV8_PMCR_N_MASK 0x1f
#define ARMV8_PMUSERENR_EN_EL0 (1 << 0) /* EL0 access enable */
#define ARMV8_PMUSERENR_CR (1 << 2) /* Cycle counter read enable */
#define ARMV8_PMUSERENR_ER (1 << 3) /* Event counter read enable */
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 void
enable_cpu_counters(void* data)
{
u32 val=0;
asm volatile("msr pmuserenr_el0, %0" : : "r"(0xf));
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)
{
isb();
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");
2.3 用户态代码
#include <stdio.h>
#define u64 unsigned long long
#define isb() asm volatile("isb" : : : "memory")
static inline u64 arch_counter_get_cntpct(void)
{
u64 cval;
isb();
asm volatile("mrs %0, PMCCNTR_EL0" : "+r"(cval));
return cval;
}
2.4 测试时遇到的问题
可能有同学会用下面的代码测试定时精度,
int main()
{
u64 begin,end;
begin = arch_counter_get_cntpct();
sleep(1);
end= arch_counter_get_cntpct();
printf("The count is %llu.\n",end-begin);
return 0;
}
但是会发现使用统计的计数值与CPU当前的始终频率计算后,时间不是1s。这是因为Linux的省电功能导致的,sleep会使当前进程让出CPU,如果此时CPU任务队列中没有任务,就会进入低功耗(例如,WFI)甚至offline,如果进入上述状态PMU计数器就会停止计数,导致计数值不准确。
毕竟PMU是为调式使用的,如果此时CPU没有任务,也确实没有必要继续统计了。所以使用PMU寄存器计数是,不应该有主动让出CPU的行为,可能会导致计数不准确。
可以尝试关闭省电模式:
echo 1 > /sys/devices/system/cpu/cpu<X>/cpuidle/state<Y>/disable