### RT-Thread Version master, commit `a59e72fb5c2f272761a2813fc9bd9fe18eb29d7d`. ### Affected area libc / POSIX timers, `components/libc/compilers/common/ctime.c`, `timer_gettime()`. ### Hardware / Architecture / Toolchain 采用源码级算术隔离复现,不依赖特定开发板。该提交中的受影响实现由 `RT_USING_POSIX_TIMER && RT_USING_CLOCK_TIME` 启用。本报告尚未进行目标板复现。 ### 问题描述 / Describe the bug `timer_gettime()` 将总剩余纳秒存入 `rt_uint32_t`,之后才拆分秒和纳秒: ```c rt_uint32_t seconds, nanoseconds; /* ... */ nanoseconds = ((remain_cnt - rt_clock_time_get_counter()) * rt_clock_time_get_res_scaled()) / RT_CLOCK_TIME_RESMUL; seconds = nanoseconds / NANOSECOND_PER_SECOND; nanoseconds = nanoseconds % NANOSECOND_PER_SECOND; ``` [固定版本源码 L1337–1375](https://github.com/RT-Thread/rt-thread/blob/a59e72fb5c2f272761a2813fc9bd9fe18eb29d7d/components/libc/compilers/common/ctime.c#L1337-L1375) 即使计数器表达式正确算出 10 秒,即 `10,000,000,000 ns`,赋给 32 位无符号变量时也会变成 `1,410,065,408 ns`。查询结果因此是 `{tv_sec=1, tv_nsec=410065408}`,而不是 `{10, 0}`。 ### 可运行的隔离复现 / Standalone arithmetic reproduction 下面明确模拟计数器换算已经正确得到总纳秒的情况,不调用宿主 libc 的 `timer_gettime()`: ```c #include <assert.h> #include <stdint.h> #include <stdio.h> int main(void) { uint64_t converted_ns = UINT64_C(10000000000); uint32_t seconds, nanoseconds; nanoseconds = converted_ns; seconds = nanoseconds / 1000000000; nanoseconds %= 1000000000; printf("%u.%09u\n", (unsigned)seconds, (unsigned)nanoseconds); assert(seconds == 1 && nanoseconds == 410065408); return 0; } ``` 输出:`1.410065408`;预期剩余时间为 `10.000000000`。 此前还在本地旧版同一函数的隔离计数器桩中复现过该结果;最新上游同样保留上述窄化顺序。完整目标端验证仍待进行。 ### Scope / Suggested direction - 问题从总纳秒超过 `UINT32_MAX`(约 4.295 秒)起出现,不是几十年后的时间范围问题。 - 这是剩余时间**查询结果**错误,不表示定时器一定提前或延后触发。 - 后面的 `(rt_int32_t)seconds` 不是这里的根因,数据在拆秒之前已经丢失。 - 建议总纳秒使用足够宽的类型,再拆分秒和纳秒;或在安全的单位下先拆秒、再换算余数。还应保留对计数器回绕及中间乘法范围的检查。 ### 引入提交和版本 / Regression history - 引入提交:[0eb75ced70](https://github.com/RT-Thread/rt-thread/commit/0eb75ced70),2023-07-17,`[time]时钟框架重构 (#7794)`;首个包含该提交的正式 release 为 **v5.0.2**。 - 此前使用 64 位 `remaining` 保存总纳秒,再分别赋值给秒和纳秒字段;该提交改为将总纳秒直接赋给 32 位 `nanoseconds`,然后再拆秒。 - `rt_uint32_t seconds, nanoseconds` 的声明本身在更早实现中已经存在,但当时用于保存拆分后的字段,并非这次回归的来源。 - 后续 [743b614875](https://github.com/RT-Thread/rt-thread/commit/743b614875) 的时间子系统重构更换了计数接口名称,但保留了该窄化顺序。 ### 其他实现参考 / Comparison [Linux](https://github.com/torvalds/linux/blob/master/kernel/time/posix-timers.c) 使用 64 位 `ktime_t` 转 `timespec64`;[Zephyr](https://github.com/zephyrproject-rtos/zephyr/blob/main/subsys/portability/posix/options/timer.c) 则先从剩余毫秒中拆秒,再将余数转换为纳秒。两种方式均避免这里的总纳秒 32 位中转。这不是对这些 OS 所有计时边界行为的保证。
Activity
github-actions commentedon Oct 23, 2026
如需认领并处理此 Issue,请评论
/claim。To claim and work on this issue, comment
/claim.Yunlk commentedon Oct 24, 2026
在 QEMU 上实测了一下,uint32 截断这一点确认成立,但同时发现:只把宽度改成 64 位还不够,在这块 BSP 上算出来的剩余时间本身就不对。分开说。
1. uint32 截断:确认
ctime.c:1337的timer_gettime():总纳秒塞进
rt_uint32_t,超过UINT32_MAX(约 4.29 s)就截断。10 s 定时器的算术对得上你的数字:10,000,000,000 mod 2^32 = 1,410,065,408 →{1 s, 410065408 ns}。2. 实测:截断去掉后暴露出更深的问题
我把总纳秒改成
rt_uint64_t后重跑(bsp/qemu-vexpress-a9,10 s 一次性定时器,CLOCK_MONOTONIC,SIGEV_SIGNAL,create/settime/gettime三个调用都返回 0):{7598 s, 0 ns}{4347 s, 265926290 ns}{1096 s, 531852580 ns}{2 s, 640384201 ns}{2193 s, 73705161 ns}注意两件事:
{2 s, 640384201 ns},不是你说的{1 s, 410065408 ns}——取模相位取决于读取时刻,这个差异本身正常这组数据说明
remain_cnt - rt_clock_time_get_counter()这个减法在做的时候就下溢了:32 位时代乘法回绕把大数又绕回了"看起来合理"的小范围,所以截断问题掩盖了它。3. 想请你确认的点
RT_TIMER_CTRL_GET_REMAIN_TIME在你的实现里返回的是绝对到期计数还是剩余时长?代码这里当成绝对到期计数和rt_clock_time_get_counter()相减;如果它其实返回剩余时长,这个减法就没有意义。bsp/qemu-vexpress-a9默认配置(RT_USING_POSIX_TIMER/RT_USING_CLOCK_TIME本来就开),RT_TICK_PER_SECOND用 BSP 默认。你复现时的 BSP 和时钟配置是什么?4. 关于修复建议
你的建议是"总纳秒改 64 位再拆秒,或先拆秒再换算余数"。第 1 种我试了,单独做不够(见第 2 节)。如果确认
remain_cnt的语义就是问题所在,那修复应该同时处理这个减法——比如拿到期计数与当前计数比较前先确认二者在同一计数域。我没有提 PR:一是我还没搞清楚第 2 节那个下溢的来源,二是这种"改了反而更离谱"的补丁不该直接进仓库。确认之后我可以按结论出补丁 + utest(utest 里可以同时锁住"10 s 读到 10 s"和"4.29 s 边界不翻转")。
附:utest 的写法建议
如果要用 utest 覆盖,注意别只断言
tv_sec == 10——那样在时钟域有偏差的 BSP 上会误报。建议同时断言:tv_sec等于设定值(±1 容差)Yunlk commentedon Oct 24, 2026
/claim
github-actions commentedon Oct 24, 2026
@Yunlk has claimed this issue.
Yunlk commentedon Oct 24, 2026
接着上一条评论继续往下查了,结论要修正:问题比"uint32 截断"更靠上游,而且我试的两种修法都没修好。把过程和失败记录放上来,避免别人重复走我这条路。
我试了两轮,都无效
第一轮:把总纳秒改成 64 位(
rt_uint64_t total_ns),拆秒用 64 位。第二轮:在上一轮基础上,把减法也改成像 hrtimer 核心那样在 32 位空间做(无符号回绕 + 过半判过期):
两轮实测(
bsp/qemu-vexpress-a9,SIGEV_SIGNAL,create/settime/gettime全部返回 0):第二轮和第一轮几乎完全一样,说明我改的那两处根本没碰到病根:读回来的
remain_cnt本身就不是一个可用的"剩余量"。代码层面为什么
三处关键事实:
timeout_cnt是 32 位(components/drivers/include/drivers/clock_time.h:147-148)::165):timeout_cnt直接相减。它走_cnt_convert()(clock_hrtimer.c:127):注意
:147用的是event_res(hrtimer 事件分辨率),不是RT_CLOCK_TIME_RESMUL。而timer_gettime()用的是后者。而
timer_gettime()做的是(ctime.c:1358-1360):它把 32 位的
timeout_cnt和 64 位的原始计数器相减,再按RESMUL换算——既没有_cnt_convert()的回绕/过期处理,也没有用 event 分辨率。所以拿到的数必然是错的,改宽度只能改变错误的"形状"(从截断的小数变成巨大的乱数)。结论
timer_gettime()的问题不是"少一个 64 位变量",而是它自己重新实现了一遍剩余时间换算,且用的是错误的计数域和分辨率。正确的做法应该是复用一个和_cnt_convert()同源的接口,比如给 hrtimer 增加一个返回"剩余 tick(已换算到 event 分辨率)"的控制命令,timer_gettime只负责把它拆成秒和纳秒。这需要动
clock_time.h/clock_hrtimer.c的接口,不是 libc 单文件能解决的,所以我没有提 PR——我这两轮的补丁既然修不好,提交上去只会浪费评审时间。想请你确认的两点
{1 s, 410065408 ns},是在真实定时器上读出来的,还是纯算术构造?如果是前者,你那边的res_scaled/event_res/ 计数器频率是多少?我这边算出来的数差了几个数量级,怀疑是分辨率假设不同。RT_TIMER_CTRL_GET_REMAIN_TIME的文档写的是"获取绝对到期计数值"。按上面第 3 点看,这个"绝对"是 32 位环绕空间里的绝对,直接和 64 位计数器比较是错的。你认同这个理解吗?如果认同,那这个 API 的返回语义本身也需要在文档里说清楚,否则每个调用方都会踩同一个坑。如果需要,我可以按"新增一个返回剩余 tick 的控制命令"的方向出一版补丁——但那要先确认第 2 点的语义,我不想再猜一次。
Yunlk commentedon Oct 24, 2026
补最后一组诊断数据——它解释了为什么我前面两轮修改都无效,也说明问题可能不在
timer_gettime()本身。实测:两个"分辨率"互相矛盾
在
bsp/qemu-vexpress-a9上直接读时钟子系统的公开接口:换算:
delta per 1 s = 100推,计数器是 100 Hz,每 tick = 10 msres_scaled / RT_CLOCK_TIME_RESMUL = 2.33e13 / 1e6推,每 tick = 23.3 ms,即约 43 Hz两者不一致。 也就是说
rt_clock_time_get_counter()返回的计数域,和rt_clock_time_get_res_scaled()描述的分辨率,在这块 BSP 上不是同一个时钟。对照
timer_settime()里那条逆运算:10 秒 →
reload = 1000(诊断里也算出 1000),与 100 Hz 自洽。所以 arm 用的是真实计数器频率。而
timer_gettime():减法得到的是按 100 Hz 计的 tick 数,乘的却是按 43 Hz 折算的 res_scaled。两边单位不匹配,结果自然不对——这也解释了为什么我把总纳秒改成 64 位(第一轮)、再把减法改成 32 位回绕安全(第二轮)都没有任何变化:都不是病根。
10 秒定时器读出
{2 s, 600384201 ns},反推remain_cnt ≈ 111tick,而按 100 Hz 应该是 ~1000 tick。这个 111 与"1000 按某个比例缩放"也不吻合,说明除了分辨率不匹配,剩余值本身可能还经过了别的换算,我没有继续追。我现在的判断
这不是
timer_gettime()一个函数的问题,而是时钟接口的契约问题:timeout_cnt/delay_cnt是unsigned long(32 位),而rt_clock_time_get_counter()返回rt_uint64_t——两者比较/相减时的域不一致(我前面评论里已经指出)。rt_clock_time_get_res_scaled()与rt_clock_time_get_counter()在 qemu-vexpress-a9 上描述的不是同一个时钟。任何把这两个接口混用的调用方都会算错,timer_gettime()只是其中一个受害者。所以我把问题交回给你 / 维护者
我这边已经连续两轮改错(都记录在上一条评论里),再猜下去只是在消耗评审时间。需要确认的是:
rt_clock_time_get_res_scaled()在你的 BSP 上是否也等于 2.33e13?如果是不同的值,那说明这块 BSP 的 clock_time 实现本身有问题,issue 应该改成"res_scaled 与 counter 不一致"。timer_gettime()应该用哪个?我倾向复用_cnt_convert()的逻辑(它同时处理回绕并按event_res换算),但那是clock_hrtimer.c的静态函数,需要先由维护者提供一个公开的"剩余 tick"接口。我不再提交补丁,除非上述语义被确认。这条 issue 的修复方向需要熟悉 clock_time 子系统的人给出契约说明,然后我可以在明确契约的前提下补丁 + utest。