命令字:!ndx.uptime

用法描述

显示系统的运行时间和空闲时间统计信息。相当于cat /proc/uptime

开发过程(分别为两部分,cpu空闲时间,系统启动时间)

1.找到关键全局变量

1.1cpu空闲时间相关全局变量:

基地址kernel_cpustat

偏移数组 _percpu_offset

cpu数量:nr_cpu_ids

在遍历的时候,kcpustat_cpu_fetch(&kcs, i)获取当前cpu编号对应的结构体kcs,cpu编号肯定有个范围,通过全局变量nr_cpu_ids获取。

通过 kcpustat_cpu(cpu)获取对应的结构体kcs

在include\linux\kernel_stat.h
DECLARE_PER_CPU(struct kernel_cpustat, kernel_cpustat);为每个cpu定义了percpu变量kernel_cpustat,它的类型为struct kernel_cpustat
然后是一系列的宏定义:kcpustat_cpu(cpu)->per_cpu(kernel_cpustat, cpu)->(*per_cpu_ptr(&(var), cpu))

在per_cpu_ptr(ptr, cpu)中有具体一点的实现。
调用SHIFT_PERCPU_PTR这个宏实现获取每个cpu对应的相关结构的地址。具体逻辑:就是通过基地址加偏移,而偏移量用一个数组_percpu_offset维护。

现在我们清楚了,cpu空闲时间的相关的全局变量,可以通过这个三个变量结合相关数据结构获取空闲时间,数据结构将在下一点解释。

1.2系统启动时间的相关全局变量:tk_core

ktime_get_boottime_ts64(&uptime)获取系统从启动到当前时刻的时间信息,并将其存储在 uptime 变量中

然后是系列的函数调用:
ktime_get_boottime_ts64——>ktime_to_timespec64(ktime_get_boottime()) //这里ktime_to_timespec64是格式转换函数。
ktime_get_boottime()->ktime_get_with_offset(TK_OFFS_BOOT)
在ktime_get_with_offset中,return ktime_add_ns(base, nsecs).而这个变量的数据都和全局变量tk_core相关。

2.相关数据结构

2.1cpu空闲时间相关数据结构

我们可以获得每个cpu的结构体kernel_cpustat,它只包含了一个数组,这个数组记录了系统运行过程中各类工作负载消耗的 CPU 时间。


而在get_idle_time获取当前cpu空闲时间的时候,通过索引CPUTIME_IDLE,获取数组中记录的空闲时间。

2.2系统时间相关数据结构

ktime_get_with_offset中

struct timekeeper *tk = &tk_core.timekeeper;//先从全局变量中tk_core获取了时间管理的核心数据结构timekeeper
ktime_t base, *offset = offsets[offs];//定义了一个ktime_t类型的变量base,让offset指针指向offsets数组的第offs个元素,传入的参数是TK_OFFS_BOOT,即指向tk_core.timekeeper.offs_boot。
base = ktime_add(tk->tkr_mono.base, *offset);//将基准时间值和时间偏移量相加赋值给base变量。

内核中最终的结果是由base+nsecs计算精确的启动时间,当前我们实现的启动时间没有添加nsecs.

3.读取变量和相关偏移,输出信息

仿照内核中的格式化输出

DECLARE_API(uptime)
{
    ULONG read;
    // 获取 CPU 数量
    int nr_cpu_ids = get_nr_cpus();

    // 获取 tk_core 全局变量地址
    ULONG64 tk_core_addr = 0;
    GetExpressionEx("lk!tk_core", &tk_core_addr, NULL);
    if (tk_core_addr == 0) {
        dprintf("Failed to get tk_core address. Please fix symbol.\n");
        return;
    }

    // 计算 timekeeper 结构体在 tk_core 中的偏移量
    ULONG timekeeper_offset = 0;
    GetFieldOffset("tk_core", "timekeeper", &timekeeper_offset);

    // 计算 offs_boot 字段在 timekeeper 结构体中的偏移量
    ULONG offs_boot_offset = 0;
    GetFieldOffset("timekeeper", "offs_boot", &offs_boot_offset);

    // 读取 tk_core.timekeeper.offs_boot 值
    ULONG64 offs_boot_addr = tk_core_addr + timekeeper_offset + offs_boot_offset;
    ULONG64 offs_boot = 0;
    ExtReadMemory(offs_boot_addr, &offs_boot, sizeof(offs_boot), &read);
    if (read != sizeof(offs_boot)) {
        dprintf("Failed to read offs_boot from memory.\n");
        return;
    }

    // 获取当前单调时间 (ktime_get)
    ULONG64 mono_base_addr = tk_core_addr + timekeeper_offset;
    ULONG mono_base_offset = 0;
    GetFieldOffset("timekeeper", "tkr_mono", &mono_base_offset);
    ULONG base_offset = 0;
    GetFieldOffset("tk_read_base", "base", &base_offset);

    ULONG64 mono_base_field_addr = mono_base_addr + mono_base_offset + base_offset;
    ULONG64 mono_base = 0;
    ExtReadMemory(mono_base_field_addr, &mono_base, sizeof(mono_base), &read);
    if (read != sizeof(mono_base)) {
        dprintf("Failed to read mono base from memory.\n");
        return;
    }


    // 获取 kernel_cpustat 基址
    ULONG64 kernel_cpustat_addr = 0;
    GetExpressionEx("lk!kernel_cpustat", &kernel_cpustat_addr, NULL);
    if (kernel_cpustat_addr == 0) {
        kernel_cpustat_addr = 0xffff80000a24d8a0;  // 临时方案
        dprintf("use kernel_cpustat_test_addr = %p\n", kernel_cpustat_addr);
    }
    // 获取 per-cpu offset 数组
    ULONG64 per_cpu_offset_addr = 0;
    GetExpressionEx("lk!__per_cpu_offset", &per_cpu_offset_addr, NULL);
    if (per_cpu_offset_addr == 0) {
        dprintf("Failed to get __per_cpu_offset address. Please fix symbol.\n");
        return;
    }

    // 获取 cpustat字段在结构体偏移
    ULONG nCpustatOffset = 0;
    GetFieldOffset("kernel_cpustat", "cpustat", &nCpustatOffset);

    // idle 字段是 cpustat[5],每个元素8字节(u64)
    ULONG idle_index = 5; // CPUTIME_IDLE
    ULONG idle_offset = nCpustatOffset + idle_index * sizeof(ULONG64);

    //遍历cpu计算总空闲时间
    ULONG64 idle_time = 0, cpu_idle = 0, cpu_offset = 0;
    for (int i = 0; i < nr_cpu_ids; i++) {
        // 读取 __per_cpu_offset[i]
        ExtReadMemory(per_cpu_offset_addr + i * sizeof(ULONG64), &cpu_offset, sizeof(cpu_offset), &read);
        if (read != sizeof(cpu_offset)) {
            continue;
        }
        // 计算该 CPU 的 kernel_cpustat->cpustat[5] 地址
        ULONG64 cpu_addr = kernel_cpustat_addr + cpu_offset + idle_offset;
        ExtReadMemory(cpu_addr, &cpu_idle, sizeof(cpu_idle), &read);
        if (read == sizeof(cpu_idle)) {
            // 每个cpu的空闲时间(纳秒/百分位)
            ULONG64 cpu_idle_sec = cpu_idle / NSEC_PER_SEC;
            ULONG64 cpu_idle_frac = (cpu_idle % NSEC_PER_SEC) / (NSEC_PER_SEC / 100);
            dprintf("CPU[%d]: idle=%llu.%02llu\n", i, cpu_idle_sec, cpu_idle_frac);
            idle_time += cpu_idle;
        }
    }

    // 计算 boottime = mono_base + offs_boot
    ULONG64 uptime_nsec = mono_base + offs_boot;
    ULONG64 uptime_sec = uptime_nsec / NSEC_PER_SEC;
    ULONG64 uptime_frac = (uptime_nsec % NSEC_PER_SEC) / (NSEC_PER_SEC / 100);

    // 总空闲时间 纳秒拆分
    ULONG64 idle_nsec = idle_time;
    ULONG64 idle_sec = idle_nsec / NSEC_PER_SEC;
    ULONG64 idle_frac = (idle_nsec % NSEC_PER_SEC) / (NSEC_PER_SEC / 100);

    dprintf("%llu.%02llu %llu.%02llu\n", uptime_sec, uptime_frac, idle_sec, idle_frac);
}

4.实例测试

!ndx.uptime

cat /proc/uptime

作者:郭建程  创建时间:2025-09-08 16:34
最后编辑:郭建程  更新时间:2025-09-11 16:05