给定具有恒定 TSC 的 x86,这对于测量实时非常有用,如何使用 Linux 计算的 TSC 校准因子在 TSC 参考周期的“单位”和正常人类实时单位(如纳秒)之间进行转换在启动时?
也就是说,我们当然可以通过在某个间隔的两端进行 TSC 和时钟测量(例如,使用CLOCK_MONOTONIC
)来计算用户态的 TSC 频率,以确定 TSC 频率,但 Linux 已经在启动时进行了这一计算-time,因为它内部使用 TSC 来帮助计时。例如,您可以使用
dmesg | grep tsc
查看内核的结果:
[ 0.000000] tsc: PIT calibration matches HPET. 2 loops
[ 0.000000] tsc: Detected 3191.922 MHz processor
[ 1.733060] tsc: Refined TSC clocksource calibration: 3192.007 MHz
在最糟糕的情况下,我想你可以尝试在运行时从
dmesg
中 grep 结果,但坦率地说,这看起来很糟糕,脆弱且各种不好0。 使用内核确定的校准时间的优点有很多:
cpuid
leaf 0x15 宣传其 TSC 频率,因此并不总是需要校准)。
gettimeofday
和
clock_gettime
1)使用的 TSC 频率“一致”。
0 例如:系统可能未安装 dmesg
,您可能无法以普通用户身份运行它,累积的输出可能已回绕,因此线路不再存在,您可能会收到误报你的 grep,内核消息是英文散文,可能会发生变化,可能很难启动子进程等等,等等
1 这是否重要还存在争议 - 但如果您将 rdtsc
调用与也使用操作系统计时的代码混合,则可能会提高精度。
很好的答案列举了两种从内核获取此信息的方法:使用 BPF 或使用 perf。
以下代码片段包含获取该值的最简单的 BPF 程序。它通过tsc_khz
读取
/proc/kallsyms
(从
bpf_probe_read_kernel()
获得)来获取值。它滥用了 BPF 测试运行的返回值未被检查的事实——因此它省略了映射的创建。如果以后需要,可以非常简单地对该程序进行修改以使用 eBPF 映射。
const void* resolve_kernel_symbol(const char* name) {
FILE* file;
char buffer[0x100];
const void* result = NULL;
char* saveptr;
file = fopen("/proc/kallsyms", "rt");
if (!file)
return NULL;
while (fgets(buffer, sizeof buffer, file)) {
const char* next, *address = strtok_r(buffer, " ", &saveptr);
if (!address)
continue;
if (!strtok_r(NULL, " ", &saveptr))
continue;
next = strtok_r(NULL, "\n", &saveptr);
if (!next)
continue;
if (!strcmp(name, next)) {
result = (const void*)strtoul(address, &saveptr, 0x10);
goto end;
}
}
end:
fclose(file);
return result;
}
int read_kernel_int(const void* address) {
int prog_fd, result;
const struct bpf_insn program[] = { /* r10 = stack */
/* Load the first argument (the destination address, local variable). */
BPF_MOV64_REG(BPF_REG_1, BPF_REG_10), /* r1 = stk */
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, -4), /* r1 -= 4 */
/* Load the second argument, 4 (sizeof int). */
BPF_MOV64_IMM(BPF_REG_2, 4), /* r2 = 4 */
/* Load the third argument, address of symbol. */
BPF_LD_IMM64(BPF_REG_3, (long)address),
/* Call bpf_probe_read_kernel. */
BPF_JMP_IMM(BPF_CALL, 0, (long)bpf_probe_read_kernel, 0),
/* Set the exit code to the read value. */
/* r1 is caller-saved, so cannot be used. */
BPF_LDX_MEM(BPF_W, BPF_REG_0, BPF_REG_10, -4), /* r0 = *((u32*)stk-1) */
BPF_EXIT_INSN()
};
{
union bpf_attr attr = {0};
attr.prog_type = BPF_PROG_TYPE_RAW_TRACEPOINT;
attr.insns = (__u64)program;
attr.insn_cnt = sizeof program/sizeof program[0];
attr.license = (__u64)"GPL";
if ((prog_fd = syscall(SYS_bpf, BPF_PROG_LOAD, &attr, sizeof attr)) < 0)
return -1;
}
{
union bpf_attr attr = {0};
attr.test.prog_fd = prog_fd;
if (syscall(SYS_bpf, BPF_PROG_TEST_RUN, &attr, sizeof attr) < 0) {
result = -1;
goto out;
}
result = attr.test.retval;
}
out:
close(prog_fd);
return result;
}
int get_tsc_freq(void) {
return read_kernel_int(resolve_kernel_symbol("tsc_khz"));
}