Lely CANopen: Timer 的实现原理与运行流程

在这里插入图片描述

@[toc]

一句话结论

Lely 的 lely::io::Timer 不是一个独立线程,也不是一个直接执行回调的传统软件定时器。它本质上是一个异步事件适配器:

1
2
3
4
5
6
7
8
9
Linux timerfd 负责计时

io_poll 负责监听 timerfd 的可读事件

内部 wait_task 负责读取到期计数

ev_exec 负责调度完成任务

io_timer_wait 对应的回调最终被执行

创建 Timer、设置到期时间和提交等待操作是三个不同动作:

1
2
3
创建 Timer:建立 timerfd 并注册到 io_poll
设置时间:调用 timerfd_settime(),决定何时到期
提交等待:把 io_timer_wait 放入 wait_queue,决定到期后通知谁

从 CANopen 主站视角看,还要再增加两层调度:

1
2
3
4
5
6
7
8
9
BasicMaster / Node
↓ 传入专用 io::TimerBase&
io::CanNet / io_can_net
↓ 创建 io_tqueue,并注册 can_net 的 next 回调
io_tqueue
↓ 用一个底层 io_timer_wait 复用同一个系统 Timer
can_net.timer_heap
↓ 管理 NMT、Heartbeat、SDO、PDO、SYNC 等 can_timer_t
具体协议定时回调

因此,Lely CANopen 的核心设计不是“每个协议功能创建一个 Linux timerfd”,而是:

一个 CANopen Node 使用一个专用系统 Timer;io_tqueuecan_net 的两级最小堆把大量逻辑超时复用到这个 Timer 上。


1. 从 C++ 创建语句开始

上层代码:

1
2
3
4
5
impl_->timer.reset(
new lely::io::Timer(
*impl_->poll,
*impl_->executor,
CLOCK_MONOTONIC));

构造函数:

1
2
3
4
5
Timer(io_poll_t* poll, ev_exec_t* exec, clockid_t clockid)
: TimerBase(io_timer_create(poll, exec, clockid)) {
if (!timer)
util::throw_errc("Timer");
}

可以确认的创建链路为:

1
2
3
4
5
6
7
8
9
10
11
lely::io::Timer::Timer()
└─ io_timer_create()
├─ io_timer_alloc()
└─ io_timer_init()
├─ 初始化 io_timer_impl
├─ 初始化 watch 回调
├─ 初始化内部 wait_task
├─ io_timer_impl_open()
│ ├─ timerfd_create()
│ └─ io_poll_watch()
└─ io_ctx_insert()

这里完成的是“创建 I/O 定时器基础设施”,还没有指定具体的到期时刻。

1.1 CLOCK_MONOTONIC 的作用

Timer 使用:

1
CLOCK_MONOTONIC

表示定时器基于单调时钟运行。它适合协议超时和周期调度,因为系统日历时间被人工修改时,单调时钟不会发生向前或向后的跳变。

对 CANopen 而言,心跳超时、SDO 超时、PDO 事件定时、SYNC 周期等都属于持续时间或截止时间问题,而不是日历时间问题,因此使用单调时钟符合这类任务的需求。


2. io_timer_impl:Timer 的真实内部对象

Linux 实现中的核心结构体为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct io_timer_impl {
const struct io_dev_vtbl *dev_vptr;
const struct io_timer_vtbl *timer_vptr;
io_poll_t *poll;
struct io_svc svc;
io_ctx_t *ctx;
ev_exec_t *exec;
clockid_t clockid;
struct io_poll_watch watch;
int tfd;
struct ev_task wait_task;
#if !LELY_NO_THREADS
pthread_mutex_t mtx;
#endif
unsigned shutdown : 1;
unsigned wait_posted : 1;
struct sllist wait_queue;
int overrun;
};

关键字段如下:

字段 职责
poll 监听 timerfd 是否出现可读事件
exec 执行内部任务和等待完成任务
clockid 指定 CLOCK_MONOTONICCLOCK_REALTIME
tfd Linux timerfd 文件描述符
watch 注册到 io_poll 的监听对象
wait_task timerfd 就绪后投递给 executor 的内部处理任务
wait_queue 保存等待下一次 Timer 完成的 io_timer_wait
wait_posted 防止同一到期事件重复投递内部任务
shutdown 标记 Timer 已进入关闭流程
overrun 保存额外到期次数
mtx 保护等待队列和状态位

2.1 一个对象同时暴露多个接口

io_timer_impl 同时包含:

1
2
3
dev_vptr    → io_dev_t 接口
timer_vptr → io_timer_t 接口
svc → io_svc 服务接口

Lely 使用类似 container_of()structof() 机制,由成员地址反推出完整对象地址:

1
2
3
4
5
static inline struct io_timer_impl *
io_timer_impl_from_timer(const io_timer_t *timer)
{
return structof(timer, struct io_timer_impl, timer_vptr);
}

这也是为什么 io_timer_alloc() 返回的不是 impl,而是:

1
return &impl->timer_vptr;

3. 打开 Timer:timerfd_create()io_poll_watch()

真正创建 Linux 定时器的代码位于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int
io_timer_impl_open(struct io_timer_impl *impl)
{
...

impl->tfd = timerfd_create(
impl->clockid,
TFD_NONBLOCK | TFD_CLOEXEC);

if (impl->tfd == -1)
...

if (io_poll_watch(
impl->poll,
impl->tfd,
IO_EVENT_IN,
&impl->watch) == -1)
...

return 0;
}

3.1 TFD_NONBLOCK

TFD_NONBLOCK 让:

1
read(impl->tfd, ...)

在当前没有到期计数可读时立即返回 EAGAINEWOULDBLOCK,而不是阻塞 executor 线程。

这是异步事件循环中的关键要求。若内部处理任务在 read() 上阻塞,使用同一个 executor 的其他 CANopen 任务也可能被阻塞。

3.2 TFD_CLOEXEC

TFD_CLOEXEC 表示进程执行 exec() 加载新程序时,内核自动关闭该文件描述符,避免子程序意外继承 Timer 资源。

它只影响 exec(),不阻止 fork() 继承。fork() 的处理由后面的 io_timer_impl_svc_notify_fork() 完成。

3.3 io_poll_watch(..., IO_EVENT_IN, ...)

该调用的语义是:

1
2
请 io_poll 监听 impl->tfd;
当该 fd 可读时,通过 impl->watch 调用注册的 watch 回调。

IO_EVENT_IN 对应“可读事件”。对 timerfd 而言,可读意味着至少发生过一次定时到期,内核中存在可读取的 64 位到期计数。

这条 if 判断的是“注册监听是否失败”,不是判断 Timer 是否已经到期。


4. 设置时间:io_timer_impl_settime()

Timer 的虚函数表包含:

1
2
3
4
5
6
7
8
static const struct io_timer_vtbl io_timer_impl_vtbl = {
&io_timer_impl_get_dev,
&io_timer_impl_get_clock,
&io_timer_impl_getoverrun,
&io_timer_impl_gettime,
&io_timer_impl_settime,
&io_timer_impl_submit_wait
};

其中设置时间的实现为:

1
2
3
4
5
6
7
8
9
10
11
12
static int
io_timer_impl_settime(io_timer_t *timer, int flags,
const struct itimerspec *value, struct itimerspec *ovalue)
{
struct io_timer_impl *impl = io_timer_impl_from_timer(timer);

int flags_ = 0;
if (flags & TIMER_ABSTIME)
flags_ |= TFD_TIMER_ABSTIME;

return timerfd_settime(impl->tfd, flags_, value, ovalue);
}

4.1 flags_ |= TFD_TIMER_ABSTIME 是什么

这句把 Lely 通用定时接口中的:

1
TIMER_ABSTIME

转换为 Linux timerfd 使用的:

1
TFD_TIMER_ABSTIME

两种模式的区别:

模式 it_value 的含义
未设置 TFD_TIMER_ABSTIME 从现在开始再等待多长时间
设置 TFD_TIMER_ABSTIME 当指定 clock 到达哪个绝对时间点时到期

假设当前 CLOCK_MONOTONIC 为 100 秒,it_value 为 10 秒:

1
2
相对模式:在 110 秒时到期
绝对模式:目标时刻是 10 秒,已过期,因此会立即就绪

绝对时间模式常用于避免周期调度的累计漂移。上层可以始终维护固定截止时间轴,而不是在每次任务执行完后再重新增加一个相对周期。

需要注意:TFD_TIMER_ABSTIME 主要改变首次到期值 it_value 的解释方式;周期字段 it_interval 仍表示每次到期后的相对间隔。

4.2 TimerBase 的完整 C++ API 已可确认

官方 include/lely/io2/timer.hpp 显示,TimerBase 不是只有一个模糊的“异步等待接口”,而是提供了完整的时间和等待操作封装:

C++ 接口 下层 C 接口 语义
get_clock() io_timer_get_clock() 获取 Timer 使用的时钟
getoverrun() io_timer_getoverrun() 获取最近一次额外到期次数
gettime() io_timer_gettime() 获取剩余时间和周期
settime(duration, period) io_timer_settime(..., 0, ...) 按相对时间设置
settime(time_point, period) io_timer_settime(..., TIMER_ABSTIME, ...) 按绝对时间设置
submit_wait(io_timer_wait&) io_timer_submit_wait() 提交已有等待对象
submit_wait(F) make_timer_wait_wrapper() 包装回调并提交;完成后自动释放 wrapper
cancel_wait() io_timer_cancel_wait() 取消并以错误结果完成
abort_wait() io_timer_abort_wait() 移除任务,不走正常完成路径
async_wait() io_timer_async_wait() 返回 ev::Future<int, int>

其中,回调包装器最终执行的函数签名为:

1
void(int overrun, std::error_code ec)

overrun == -1 时,ecwait->r.errc 构造;正常到期时,overrun 为额外到期次数。make_timer_wait_wrapper() 创建的对象在任务执行完成后会 delete self,因此提交以后调用方不能再手动释放它。


5. 提交等待:io_timer_impl_submit_wait()

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static void
io_timer_impl_submit_wait(io_timer_t *timer, struct io_timer_wait *wait)
{
struct io_timer_impl *impl = io_timer_impl_from_timer(timer);
assert(wait);
struct ev_task *task = &wait->task;

if (!task->exec)
task->exec = impl->exec;
ev_exec_on_task_init(task->exec);

#if !LELY_NO_THREADS
pthread_mutex_lock(&impl->mtx);
#endif
if (impl->shutdown) {
#if !LELY_NO_THREADS
pthread_mutex_unlock(&impl->mtx);
#endif
io_timer_wait_post(wait, -1, ECANCELED);
} else {
sllist_push_back(&impl->wait_queue, &task->_node);
#if !LELY_NO_THREADS
pthread_mutex_unlock(&impl->mtx);
#endif
}
}

5.1 它提交的不是“立即执行任务”

更准确的语义是:

将一个一次性的异步等待操作登记到 Timer;当 Timer 下一次到期、出错或被取消时,再把该等待对象中的任务投递给 executor。

因此:

1
2
3
4
5
io_timer_impl_submit_wait()
≠ 启动 Timer
≠ 设置 Timer 时间
≠ 立即调用回调
= 把 io_timer_wait 加入 wait_queue

5.2 默认 executor

如果调用者没有为 wait->task 指定 executor:

1
2
if (!task->exec)
task->exec = impl->exec;

就使用创建 Timer 时传入的默认 executor。

5.3 ev_exec_on_task_init()

这不是执行任务,而是通知 executor:存在一个尚未完成的异步任务。它与完成时的:

1
ev_exec_on_task_fini(exec);

形成生命周期配对。

5.4 shutdown 后提交

如果 Timer 已关闭,新提交的 wait 不会进入队列,而是以:

1
2
result = -1
errc = ECANCELED

完成,并被投递给 executor。这样调用者不会得到一个永远无法结束的悬空异步操作。

5.5 同一个 wait 不能重复挂入

队列保存的是:

1
&task->_node

同一个链表节点在完成或取消前不能重复加入同一个或其他队列,否则可能破坏链表结构。


6. Timer 到期后发生什么

核心流程分成两个阶段:

1
2
poll 阶段:发现 fd 就绪,投递内部任务
executor 阶段:读取 timerfd,完成所有 wait

6.1 第一阶段:io_timer_impl_watch_func()

创建时已经绑定:

1
2
impl->watch = IO_POLL_WATCH_INIT(
&io_timer_impl_watch_func);

因此 timerfd 可读时,io_poll 调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void
io_timer_impl_watch_func(struct io_poll_watch *watch, int events)
{
struct io_timer_impl *impl =
structof(watch, struct io_timer_impl, watch);

...

int post_wait = !impl->wait_posted && !impl->shutdown;
if (post_wait)
impl->wait_posted = 1;

...

if (post_wait)
ev_exec_post(impl->wait_task.exec, &impl->wait_task);
}

它不读取 timerfd,也不直接执行用户回调,只做两件事:

  1. wait_posted 防止重复投递;
  2. 把内部 wait_task 投递给 executor。

这样可避免在 I/O poll 回调上下文中执行较重的处理逻辑。

6.2 为什么需要 wait_posted

如果 poll 在内部处理任务完成前多次报告同一 fd 可读,而每次都投递一个读取任务,就可能出现多个任务竞争读取同一个 timerfd。

wait_posted 保证任意时刻最多有一个内部读取任务待执行:

1
2
3
第一次事件:wait_posted 0 → 1,投递 wait_task
后续重复事件:wait_posted 已为 1,不重复投递
任务处理结束:wait_posted 重新置 0

7. 第二阶段:io_timer_impl_wait_task_func()

内部任务由 executor 执行:

1
2
static void
io_timer_impl_wait_task_func(struct ev_task *task)

该函数是整个 Timer 完成路径的核心。

7.1 读取 timerfd 到期计数

1
2
uintmax_t value = 0;
result = read(impl->tfd, &value, sizeof(value));

一次成功读取返回 8 字节计数,表示自上次成功读取后累计发生了多少次到期。

例如周期为 10 ms,但 executor 35 ms 后才处理,该值可能为 3。

代码循环读取,直到返回 EAGAIN

1
2
3
4
5
持续读取所有当前积压的到期计数

读空后得到 EAGAIN

重新监听下一次 IO_EVENT_IN

因为创建时设置了 TFD_NONBLOCK,读空时不会阻塞。

7.2 overrun 的含义

变量初始为:

1
int overrun = -1;

每读取到 value 后执行累计。结果语义为:

1
overrun = 总到期次数 - 1

示例:

到期计数 overrun 含义
1 0 正常到期一次
2 1 额外积压一次
3 2 额外积压两次

代码使用饱和处理避免超过 INT_MAX

7.3 将内部 wait 队列整体转移到局部队列

1
2
3
4
5
6
7
struct sllist queue;
sllist_init(&queue);

...

if (overrun >= 0 || errno)
sllist_append(&queue, &impl->wait_queue);

这里不是复制,而是将当前 impl->wait_queue 中的等待对象整体移入局部 queue

效果可以理解为:

1
2
3
转移前:impl->wait_queue = [A, B, C]
转移后:impl->wait_queue = []
local queue = [A, B, C]

在解锁后新提交的 wait 会进入新的 impl->wait_queue,不会被本次到期事件完成。

7.4 Linux 后端确定使用 epoll + EPOLLONESHOT

官方 src/io2/linux/poll.c 已经直接证明 Linux 后端的行为:

1
2
#define EPOLL_EVENT_INIT(events, fd) \
{ mapped_events | EPOLLONESHOT, { .fd = (fd) } }

事件映射关系为:

1
2
3
4
IO_EVENT_IN  → EPOLLIN | EPOLLRDHUP
IO_EVENT_PRI → EPOLLPRI
IO_EVENT_OUT → EPOLLOUT
并始终附加 EPOLLONESHOT

io_poll_watch() 使用 epoll_ctl() 完成:

1
2
3
第一次监听      → EPOLL_CTL_ADD
修改/重新激活 → EPOLL_CTL_MOD
events == 0 → EPOLL_CTL_DEL

轮询线程通过 epoll_pwait() 获取事件,io_poll_process() 在调用 watch 回调之前先执行:

1
2
watch->_events = 0;
poll->nwatch--;

这意味着一次事件发生后,该 watch 在 Lely 的内部状态中也被标记为未激活。Timer 在读空 timerfd、得到 EAGAIN 后执行:

1
2
events |= IO_EVENT_IN;
io_poll_watch(impl->poll, impl->tfd, events, &impl->watch);

不是保守性重复注册,而是对 EPOLLONESHOT 的必要 rearm。完整路径已经可以确定为:

1
2
3
4
5
6
7
epoll_pwait()
→ io_poll_process()
→ watch->_events = 0
→ io_timer_impl_watch_func()
→ executor 读取 timerfd
→ io_poll_watch(..., IO_EVENT_IN, ...)
→ epoll_ctl(EPOLL_CTL_MOD)

8. io_timer_wait_queue_post() 会调用所有回调吗

内部队列完成函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static inline size_t
io_timer_wait_queue_post(struct sllist *queue, int result, int errc)
{
size_t n = 0;

struct slnode *node;
while ((node = sllist_pop_front(queue))) {
struct ev_task *task = ev_task_from_node(node);
struct io_timer_wait *wait = io_timer_wait_from_task(task);
io_timer_wait_post(wait, result, errc);
n += n < SIZE_MAX;
}

return n;
}

答案是:

它会处理传入局部 queue 中的所有 io_timer_wait,但不是在当前调用栈中直接同步调用所有用户函数,而是逐个把每个 wait 的 ev_task 投递到对应 executor。

io_timer_wait_post() 做的是:

1
2
3
4
5
6
wait->r.result = result;
wait->r.errc = errc;

ev_exec_t *exec = wait->task.exec;
ev_exec_post(exec, &wait->task);
ev_exec_on_task_fini(exec);

所以实际链路为:

1
2
3
4
5
6
7
8
io_timer_wait_queue_post()
└─ 遍历当前局部 queue
└─ io_timer_wait_post()
├─ 写入 result / errc
├─ ev_exec_post()
└─ ev_exec_on_task_fini()

executor 之后执行 task 绑定的函数

8.1 “所有”的边界

这里的“所有”只包括:

1
本次被转移到局部 queue 的等待对象

不包括:

  • 其他 Timer 的 wait;
  • 已完成、已取消或已 abort 的 wait;
  • 队列转移后才提交的新 wait;
  • 系统中所有历史注册函数;
  • 未包装为 io_timer_wait 的普通函数。

8.2 wait 是一次性的

队列使用:

1
sllist_pop_front(queue)

每个 wait 完成后都会离开等待队列。因此即使底层 timerfd 是周期性的,一次 submit_wait() 也只完成一次。若上层需要持续接收每个周期,必须重新提交 wait,或者由更高层封装自动重新提交。


9. 正常到期的完整时序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sequenceDiagram
participant App as 上层/CANopen逻辑
participant Timer as lely::io::Timer
participant TFD as Linux timerfd
participant Poll as io_poll
participant Exec as ev_exec
participant Wait as io_timer_wait任务

App->>Timer: 设置 itimerspec
Timer->>TFD: timerfd_settime()
App->>Timer: submit_wait(wait)
Timer->>Timer: wait 加入 wait_queue

Note over TFD: 到达截止时间
TFD-->>Poll: fd 可读 / IO_EVENT_IN
Poll->>Timer: io_timer_impl_watch_func()
Timer->>Exec: ev_exec_post(wait_task)
Exec->>Timer: io_timer_impl_wait_task_func()
Timer->>TFD: read() 到期计数
Timer->>Timer: 移走当前 wait_queue
Timer->>Exec: 逐个 ev_exec_post(wait->>task)
Exec->>Wait: 执行完成任务
Wait-->>App: result / errc / overrun

用一句话概括:

1
内核产生到期事件,poll 发现事件,executor 读取事件,再由 executor 执行用户等待任务。

10. 取消、abort 与 shutdown

10.1 cancel

1
io_timer_impl_dev_cancel()

将指定 wait 或全部 wait 从队列中取出,然后:

1
io_timer_wait_queue_post(&queue, -1, ECANCELED);

调用者仍会收到完成通知,只是结果为取消。

10.2 abort

1
io_timer_impl_dev_abort()

将任务移出队列并调用:

1
ev_task_queue_abort(&queue);

语义上,abort 不走正常完成回调路径。

10.3 shutdown

1
2
static void
io_timer_impl_svc_shutdown(struct io_svc *svc)

关闭流程:

1
2
3
4
5
6
7
8
9
shutdown = 1

停止监听 timerfd

尝试 abort 已投递的内部 wait_task

取消所有仍在 wait_queue 中的等待操作

等待者以 ECANCELED 完成

之后 io_timer_fini() 会关闭 timerfd 并销毁互斥锁。


11. io_timer_impl_svc_notify_fork() 为什么存在

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int
io_timer_impl_svc_notify_fork(struct io_svc *svc, enum io_fork_event e)
{
struct io_timer_impl *impl = io_timer_impl_from_svc(svc);

if (e != IO_FORK_CHILD || impl->shutdown)
return 0;

struct itimerspec value = { { 0, 0 }, { 0, 0 } };
timerfd_gettime(impl->tfd, &value);
io_timer_impl_close(impl);
io_timer_impl_open(impl);
timerfd_settime(impl->tfd, 0, &value, NULL);

...
}

11.1 Timer 不会主动 fork

这段函数并不意味着 Timer 会创建子进程。它只是作为 io_svc 回调注册到 io_ctx

1
2
impl->svc = IO_SVC_INIT(&io_timer_impl_svc_vtbl);
io_ctx_insert(impl->ctx, &impl->svc);

只有宿主程序或依赖库确实执行了 fork(),并且 io_ctx 收到相应通知时,该回调才会被调用。

11.2 为什么子进程要重建 timerfd

fork() 后,子进程继承父进程的文件描述符,父子进程中的 fd 指向同一个底层 timerfd 对象。若双方都读取到期计数,可能互相消费事件。

因此子进程执行:

1
2
3
4
timerfd_gettime()   保存剩余时间和周期
io_timer_impl_close() 关闭继承的 timerfd
io_timer_impl_open() 创建独立 timerfd 并重新注册 poll
timerfd_settime() 按剩余时间恢复

父进程继续使用原 timerfd,子进程使用新 timerfd,双方不再共享到期计数。

11.3 与 TFD_CLOEXEC 的区别

场景 处理方式
fork() 后仍继续运行 Lely io_timer_impl_svc_notify_fork() 重建 timerfd
随后 exec() 加载新程序 TFD_CLOEXEC 自动关闭 timerfd

12. 从 BasicMaster 到 Linux Timer:官方源码中的真实所有权

此前不能确认的“完整 CANopen 主站工程调用关系”,在官方 coappio2can 源码中可以完整闭环。

12.1 BasicMaster 不创建 Timer,而是接收外部 Timer

BasicMaster 的构造函数接收:

1
io::TimerBase& timer

然后直接传给父类 Node

1
2
BasicMaster::BasicMaster(..., io::TimerBase& timer, ...)
: Node(exec, timer, chan, ...), ... {}

Node 的接口文档明确要求:

传入的 Timer 用于 CANopen 事件,且不得用于其他目的。

Node 构造时继续把 Timer 传给 io::CanNet

1
2
Node::Node(..., io::TimerBase& timer, ...)
: io::CanNet(exec, timer, chan, 0, 0), ... {}

所以在你的工程中:

1
impl_->timer.reset(new lely::io::Timer(...));

impl_->timer 是 Timer 的所有者;Lely 的 BasicMaster/Node 只接收引用并使用它,不负责创建该系统 Timer。

12.2 真正直接保存 io_timer_t* 的是 io_can_net

io::CanNet 将 Timer 传给:

1
io_can_net_create(exec, timer, chan, ...)

struct io_can_net 中直接保存:

1
2
3
4
io_timer_t *timer;
io_tqueue_t *tq;
struct io_tqueue_wait wait_next;
struct io_tqueue_wait wait_confirm;

初始化时:

1
2
3
net->timer = timer;
net->tq = io_tqueue_create(net->timer, NULL);
can_net_set_next_func(net->net, &io_can_net_next_func, net);

因此系统 Timer 的直接使用者是 I/O CAN 网络适配层 io_can_net,而不是 NMT、SDO、PDO 模块。

12.3 io_tqueue 如何用一个系统 Timer 管理多个绝对截止时间

struct io_tqueue 包含:

1
2
3
4
io_timer_t *timer;          // 唯一底层 Timer
struct io_timer_wait wait; // 唯一底层 Timer wait
struct timespec next; // 当前最早截止时间
struct pheap queue; // 多个 io_tqueue_wait 的最小堆

当上层提交一个 io_tqueue_wait 时,io_tqueue_submit_wait()

  1. 将等待对象按绝对时间插入 pheap

  2. 如果它成为最早截止时间,调用:

    1
    io_timer_settime(tq->timer, TIMER_ABSTIME, &value, NULL);
  3. 若底层尚未提交等待,则只提交一个:

    1
    io_timer_submit_wait(tq->timer, &tq->wait);

底层 Timer 到期后,io_tqueue_wait_func()

  1. 获取当前时刻;
  2. 从堆中弹出所有 value <= now 的等待对象;
  3. 将底层 Timer 重新设置为下一个最早截止时间;
  4. 如果堆仍非空,再次提交同一个 tq->wait
  5. 将所有已到期的 io_tqueue_wait 投递给各自 executor。

这证明 io_tqueue 是第一层逻辑定时复用器。

12.4 io_can_net 中 TimerQueue 的两个直接用途

io_can_net 在同一 io_tqueue 中至少维护两个等待操作:

等待对象 目的
wait_next 等待下一个 CAN/CANopen 逻辑定时器到期
wait_confirm 等待 CAN 帧发送确认超时

io_can_net_next_func() 收到 can_net 更新后的最早时间后,会设置:

1
2
net->wait_next.value = *tp;
io_tqueue_submit_wait(net->tq, &net->wait_next);

而发送 CAN 帧时,io_can_net_do_write() 会为写确认计算绝对截止时间并提交 wait_confirm。因此同一个系统 Timer 不仅承载 CANopen 协议定时器,也承载 CAN 通道发送确认超时。


13. CANopen 协议定时器如何映射到同一个 timerfd

13.1 can_net 是第二层逻辑定时复用器

struct __can_net 保存:

1
2
3
4
struct pheap timer_heap;
struct timespec time;
struct timespec next;
can_timer_func_t *next_func;

每个 can_timer_t 保存:

1
2
3
4
struct timespec start;
struct timespec interval;
can_timer_func_t *func;
void *data;

can_timer_start() 将逻辑 Timer 插入 can_net.timer_heap,再调用 can_net_set_next()。后者取堆顶,也就是最早截止时间,并调用注册的 next_func。在 io_can_net 中,这个回调就是 io_can_net_next_func()

13.2 一次协议 Timer 到期的完整、可验证调用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
NMT / Heartbeat / SDO / PDO / SYNC 模块
│ can_timer_start()

can_net.timer_heap
│ can_net_set_next():取最早 start

io_can_net_next_func()
│ 更新 net->next,提交 wait_next

io_tqueue_submit_wait()
│ 插入 io_tqueue.pheap
│ io_timer_settime(TIMER_ABSTIME)
│ io_timer_submit_wait(&tq->wait)

io_timer_impl_settime()
│ timerfd_settime(TFD_TIMER_ABSTIME)

Linux timerfd 到期

epoll_pwait() + EPOLLONESHOT

io_timer_impl_watch_func()
│ ev_exec_post(wait_task)

io_timer_impl_wait_task_func()
│ read(timerfd)
│ io_timer_wait_queue_post()

io_tqueue_wait_func()
│ 弹出全部已到期 io_tqueue_wait
│ 重新设置下一个绝对截止时间

io_can_net_wait_next_func()
│ io_can_net_set_time()

can_net_set_time()
│ 弹出所有 start <= now 的 can_timer_t
│ 周期 Timer 先重入堆
│ 调用 timer->func(&net->time, timer->data)

具体协议回调

这里有一个重要设计点:can_net_set_time() 在调用周期 Timer 的协议回调之前,先把该 Timer 的 start 增加 interval 并重新插回堆中。这可以降低回调内部修改或停止 Timer 时的状态复杂度。

13.3 哪些 CANopen 模块持有逻辑 can_timer_t

协议模块不直接持有 lely::io::Timer 或 Linux timerfd,而是持有注册在 can_net.timer_heap 中的 can_timer_t

模块 源码中的逻辑 Timer 用途
NMT 主/从服务 ec_timer life guarding 或 heartbeat 生产
NMT Master cs_timer 发送缓冲的 NMT 命令
每个 NMT slave 状态 timer node guarding
Heartbeat consumer timer 消费者心跳超时,每个 consumer 一个
Client-SDO timer SDO 请求/分段/块传输超时
TPDO timer_event event timer、事件驱动 TPDO
TPDO timer_swnd 同步窗口截止时间
RPDO timer_event deadline monitoring
RPDO timer_swnd 同步窗口截止时间
SYNC timer SYNC producer 周期

对应官方源码:

13.4 “哪个模块直接持有 Timer”的准确答案

可以分三层回答:

层级 直接持有对象 说明
应用/运行时 std::unique_ptr<lely::io::Timer> 你的 impl_->timer 负责所有权
I/O CAN 网络层 io_timer_t *timerio_tqueue_t *tq io_can_net 直接使用系统 Timer
CANopen 协议层 can_timer_t * NMT、Heartbeat、SDO、PDO、SYNC 等只持有逻辑 Timer

因此不能说“SDO 直接调用 Linux timerfd”,也不能说“每个 NMT/SDO/PDO 都有独立 Timer”。准确关系是:

1
2
3
4
5
6
协议模块 can_timer_t
→ can_net 最小堆
→ io_can_net.wait_next
→ io_tqueue 最小堆
→ 单个 io::Timer
→ 单个 Linux timerfd

14. 完整生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
flowchart TD
A[C++ Timer 构造] --> B[io_timer_create]
B --> C[io_timer_init]
C --> D[timerfd_create]
D --> E[io_poll_watch IO_EVENT_IN]
E --> F[Timer 已创建但未必已设置时间]

F --> G[settime 设置截止时间]
F --> H[submit_wait 登记等待对象]
G --> I[Linux 内核计时]
H --> J[wait_queue]

I --> K[timerfd 可读]
K --> L[watch_func]
L --> M[executor 执行 wait_task]
M --> N[read 到期计数]
N --> O[转移当前 wait_queue]
O --> P[逐个 post wait task]
P --> Q[用户完成任务执行]

Q --> R{是否再次等待}
R -- 是 --> H
R -- 否 --> S[等待结束]

F --> T[shutdown/fini]
T --> U[停止 poll 监听]
U --> V[取消未完成 waits]
V --> W[close timerfd]

15. 调试一次真实到期流程

推荐断点顺序:

1
2
3
4
5
6
7
8
9
10
lely::io::Timer::Timer
io_timer_create
io_timer_init
io_timer_impl_open
io_timer_impl_settime
io_timer_impl_submit_wait
io_timer_impl_watch_func
io_timer_impl_wait_task_func
io_timer_wait_queue_post
io_timer_wait_post

重点观察:

1
2
3
4
5
6
7
8
9
impl->tfd
impl->clockid
impl->shutdown
impl->wait_posted
impl->wait_queue
impl->overrun
value
result
errno

15.1 查看 timerfd 状态

1
2
ls -l /proc/<pid>/fd
cat /proc/<pid>/fdinfo/<tfd>

15.2 使用 strace

1
2
3
strace -f -tt \
-e trace=timerfd_create,timerfd_settime,read,close,epoll_ctl,epoll_wait \
./your_program

预期可观察到类似链路:

1
2
3
4
timerfd_create(..., TFD_CLOEXEC|TFD_NONBLOCK) = 7
timerfd_settime(7, TFD_TIMER_ABSTIME, ...) = 0
epoll_wait(...) = 1
read(7, ..., 8) = 8

15.3 确认程序是否 fork

1
strace -ff -e trace=process ./your_program

GDB 可设置:

1
2
3
4
5
break fork
break __libc_fork
break vfork
break posix_spawn
break io_timer_impl_svc_notify_fork

io_timer_impl_svc_notify_fork() 从未命中,说明该兼容路径没有参与当前程序运行。


16. 常见误区

误区 1:创建 Timer 就开始计时

错误。创建阶段只建立 timerfd 和 poll 监听,真正计时由 timerfd_settime() 启动或更新。

误区 2:submit_wait() 会立即执行任务

错误。它只把 wait 放入 wait_queue。只有到期、错误或取消后,wait 才会被投递给 executor。

误区 3:io_timer_wait_queue_post() 直接调用所有用户函数

不准确。它逐个调用 ev_exec_post(),由 executor 决定具体何时、在哪个执行上下文运行任务。

误区 4:周期 timerfd 会自动反复执行同一个 wait

错误。io_timer_wait 是一次性等待操作。完成后已从队列移除,下一周期需要重新提交。

误区 5:Timer 内部会主动 fork

错误。Timer 只注册 fork 通知回调;是否 fork 由宿主程序或依赖库决定。

误区 6:TFD_TIMER_ABSTIME 表示周期时间是绝对值

错误。它改变首次到期值 it_value 的解释;it_interval 仍是相对周期。


17. 最终心智模型

阅读完整源码时,应把它拆成八个角色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1. can_timer_t
NMT、Heartbeat、SDO、PDO、SYNC 的协议逻辑 Timer

2. can_net.timer_heap
对全部协议 Timer 排序,只上报最早截止时间

3. io_can_net
把 can_net 的最早截止时间转换为 io_tqueue_wait

4. io_tqueue
复用一个系统 Timer,管理 wait_next、wait_confirm 和 Node 用户等待

5. io::Timer / timerfd
设置唯一的内核截止时间并累计到期次数

6. io_poll / epoll
使用 EPOLLONESHOT 发现 timerfd 可读,并要求显式 rearm

7. wait_task / wait_queue
从 poll 上下文切到 executor,读取 timerfd 并分发完成任务

8. executor
执行 tqueue、CAN 网络和最终协议回调

完整运行公式:

1
2
3
4
5
6
can_timer_start 决定“协议逻辑何时到期”
can_net 决定“所有协议 Timer 中谁最早”
io_tqueue 决定“所有 I/O 等待中谁最早”
TimerBase/settime 决定“内核何时唤醒”
epoll 决定“如何发现唤醒”
executor 决定“在哪里推进状态机和执行回调”

最终应形成的核心心智模型是:

Lely CANopen 用两级最小堆将大量协议定时事件压缩为一个绝对截止时间,再由一个 timerfd 唤醒事件循环。到期后时间从底层向上回灌,最终由 can_net_set_time() 批量执行所有已到期的协议回调。


参考资料

官方 Lely 源码基线