Lely CANopen:can_net、io_can_net 与 CAN channel 的分层关系,以及 TX/RX Ring 的设计原理

@[toc]

一句话结论

Lely 中容易混淆的几个对象,实际处于不同层次:

  • can_net_t纯内存中的 CAN 逻辑调度核心,负责协议定时器、CAN-ID 接收分发和发送回调;它不知道 SocketCAN、文件描述符、poll、executor 或真实 CAN 控制器。
  • io_can_chan_t抽象 CAN 通道接口src/io2/linux/can_chan.c 是它在 Linux SocketCAN 上的具体后端,负责文件描述符、收发操作、I/O 就绪事件和写确认。
  • io_can_net_t桥接层,把 can_net_tio_timer_tio_tqueue_tio_can_chan_t 连接起来,使同步的协议核心能够运行在异步 I/O 框架上。
  • lely::io::CanNet 只是 io_can_net_t* 的 C++ RAII 包装,不是另一套独立实现;核心机制仍然由 C 文件 src/io2/can_net.csrc/can/net.c 完成。

tx_ringrxring 放在不同层,并不是不对称设计,而是由数据来源和所有权决定的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
协议对象产生发送帧

can_net_send()

io_can_net.tx_ring
↓ 解决协议突发、保存帧副本、串行提交写操作
io_can_chan_submit_write()

Linux SocketCAN

Linux SocketCAN 收到帧

Linux can_chan 后端

io_can_chan_impl.rxring
↓ 解决 fd 就绪与异步 read 请求之间的速率差
io_can_net 的 read completion

can_net_recv()

CANopen 协议接收器

最关键的区别是:

io_can_net.tx_ring 缓存的是“等待发送的 CAN 帧副本”;Linux can_chan 内的 write_queue 缓存的是“等待执行的异步写操作”。两者不是同一种队列,也不能互相替代。


1. 先澄清:这些名称不是同一个 net

1.1 名称与实际对象对照

名称 实际类型 所在层 核心职责
can_net_t struct __can_net can 逻辑核心层 定时器调度、CAN-ID 接收分发、发送回调
io_can_net_t struct io_can_net io2 桥接层 把逻辑核心接到 Timer、executor 和 CAN channel
lely::io::CanNet C++ 类 C++ 包装层 RAII 生命周期、虚函数错误回调、类型转换
io_can_chan_t 抽象接口,实际为 vtable 指针 io2 抽象设备层 定义同步/异步 CAN read、write 操作
io_can_chan_impl Linux 后端私有结构体 Linux SocketCAN 后端 fdpollrxring、操作队列、写确认
net 参数名或成员名 取决于上下文 仅是变量名称,必须看其声明类型

例如,src/io2/can_net.c 中有两层 net

1
2
3
4
5
函数参数 net
类型:io_can_net_t *

net->net
类型:can_net_t *

因此看到下面的表达式时:

1
can_net_recv(net->net, &net->read_msg)

应当读成:

1
2
把 io_can_net 对象中保存的 read_msg
交给其内部的 can_net_t 逻辑核心处理

而不是“net 调用另一个同名 net”。

1.2 CiA 301 与 Lely 实现边界

CiA 301 定义 CANopen 应用层、通信对象、对象字典和网络管理协议,并规定这些协议通过 CAN 数据链路层交换数据。

但以下名称不是 CiA 301 的标准对象名称:

  • can_net_t
  • io_can_net_t
  • io_can_chan_t
  • io_tqueue_t
  • spscring

它们是 Lely 为了实现协议栈、异步 I/O 和平台适配而设计的软件抽象。


2. 相关文件分别负责什么

用户列出的文件可以分成四组。

2.1 CAN 逻辑核心

1
2
include/lely/can/net.h
src/can/net.c

这组文件定义并实现:

  • can_net_t
  • can_timer_t
  • can_recv_t
  • can_net_set_time()
  • can_net_recv()
  • can_net_send()
  • can_net_set_next_func()
  • can_net_set_send_func()

它只处理:

1
2
3
4
时间
CAN 消息
回调函数
内存数据结构

它不直接执行:

1
2
3
4
5
6
read(fd)
write(fd)
poll/epoll
timerfd
线程阻塞
硬件驱动调用

2.2 抽象 CAN 通道

1
include/lely/io2/can.h

该文件定义 io_can_chan_t 的统一接口,包括:

  • 同步 read() / write()
  • 异步 submit_read() / submit_write()
  • 取消和中止操作;
  • io_can_chan_readio_can_chan_write 操作对象;
  • 每个操作完成后执行的 ev_task

其本质是一个 vtable 接口:

1
2
3
4
5
6
7
8
9
io_can_chan_t

struct io_can_chan_vtbl
├── get_dev
├── get_flags
├── read
├── submit_read
├── write
└── submit_write

2.3 CAN 网络桥接层

1
2
include/lely/io2/can_net.h
src/io2/can_net.c

该层把下面四部分连接起来:

1
2
3
4
can_net_t       协议逻辑核心
io_can_chan_t 实际 CAN 通道
io_timer_t 系统定时器
executor 异步任务执行器

其公开接口明确说明:发送帧会先进入用户态发送队列,再提交给底层 CAN channel。

2.4 C++ 包装层

1
include/lely/io2/can_net.hpp

lely::io::CanNet 的构造函数调用:

1
io_can_net_create(...)

析构函数调用:

1
io_can_net_destroy(...)

它还把 C 回调转换为可重写的 C++ 虚函数:

  • on_read_error()
  • on_queue_error()
  • on_write_error()
  • on_can_state()
  • on_can_error()

所以答案很明确:

有完整的 C 实现。can_net.hpp 只是 C API 的封装层,不负责底层运行机制。


3. 总体对象关系

3.1 静态对象图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
flowchart TD
CPP["lely::io::CanNet<br/>C++ RAII wrapper"] --> IONET["io_can_net_t<br/>I/O bridge"]

IONET --> CORE["can_net_t<br/>logical CAN core"]
IONET --> TQ["io_tqueue_t<br/>timer wait multiplexer"]
IONET --> CHAN["io_can_chan_t<br/>abstract CAN channel"]
IONET --> EXEC["ev_exec_t<br/>executor"]

TQ --> TIMER["io_timer_t<br/>system timer abstraction"]
CHAN --> LINUX["io_can_chan_impl<br/>Linux SocketCAN backend"]

LINUX --> FD["SocketCAN fd"]
LINUX --> POLL["io_poll watch"]
LINUX --> RXRING["rxring + rxbuf"]
LINUX --> OPQ["read/write/confirm operation queues"]

CORE --> THEAP["timer_heap<br/>pheap"]
CORE --> RTREE["recv_tree<br/>rbtree"]
CORE --> SENDFUNC["send_func callback"]

3.2 io_can_net_t 是整个连接点

struct io_can_net 内部同时保存:

1
2
3
4
io_timer_t *timer
io_tqueue_t *tq
io_can_chan_t *chan
can_net_t *net

并额外保存:

1
2
3
4
5
6
7
read operation
write operation
tx_ring + tx_buf
wait_next
wait_confirm
错误统计与回调
运行状态标志

因此 io_can_net_t 不是 CANopen 协议状态机本身,也不是 Linux CAN 驱动,而是:

将“同步回调式逻辑核心”适配到“异步 Timer + CAN channel + executor”的运行时对象。


4. can_net_t 到底是什么

4.1 内部只有三类核心资源

struct __can_net 可以概括为:

1
2
3
4
5
6
7
8
1. timer_heap
管理所有 can_timer_t

2. recv_tree
管理所有 can_recv_t

3. send_func
把发送请求交给外部实现

再加上:

1
2
3
time
next
next_func

用于维护逻辑时钟和最早定时器。

4.2 它不是设备驱动

can_net_t 不知道一帧 CAN 消息来自:

  • Linux SocketCAN;
  • Windows;
  • RTOS CAN 驱动;
  • 裸机中断;
  • 测试桩;
  • 仿真总线。

外部只要在收到消息时调用:

1
can_net_recv(core, msg)

并为发送注册:

1
can_net_set_send_func(core, send_func, data)

逻辑核心就可以工作。

4.3 接收方向:recv_tree

CANopen 协议对象会创建 can_recv_t,并按 CAN-ID 和 flags 注册到 can_net_t

收到帧后:

1
2
3
4
5
6
7
8
9
can_net_recv()

根据 CAN-ID + flags 生成 key

rbtree_find()

遍历相同 key 下的 receiver list

调用对应 can_recv_func_t

因此 can_net_t 在接收方向是一个:

CAN-ID 到协议回调的同步分发器。

4.4 发送方向:只调用回调

can_net_send() 不直接发送到硬件。

它只做:

1
2
3
4
5
检查 send_func 是否存在

调用 send_func(msg, send_data)

返回该回调的结果

这意味着 can_net_t 对发送端的唯一要求是:

1
外部提供一个“接收发送请求”的函数

io_can_net_t 中,这个函数就是:

1
io_can_net_send_func()

4.5 定时方向:timer_heap

can_timer_t 通过 pnode 插入 can_net.timer_heap

can_net_set_time() 每次推进逻辑时间时:

1
2
3
4
5
6
7
8
9
10
11
查看 timer_heap 的 root

如果 timer.start <= current time

移除该 timer

周期 timer 重新计算 start 并入堆

调用 timer callback

继续处理新的 root

处理完成后,can_net_set_next() 取新的最早 timer,并通过 next_func 通知 io_can_net_t

这正是前一篇 io_tqueue + pheap 学习笔记中“两级最早截止时间压缩”的第一层。


5. io_can_chan_t 与 Linux can_chan.c

5.1 io_can_chan_t 是接口,不是完整对象布局

io_can_chan_t 通过 vtable 调用后端实现。

调用:

1
io_can_chan_submit_read(chan, read)

最终会跳转到当前后端的:

1
io_can_chan_impl_submit_read()

调用:

1
io_can_chan_submit_write(chan, write)

最终会跳转到:

1
io_can_chan_impl_submit_write()

Linux、POSIX、Windows 或其他后端可以使用不同实现,但上层 io_can_net_t 不需要知道差异。

5.2 Linux 后端维护的资源

src/io2/linux/can_chan.c 中的 io_can_chan_impl 主要包含:

1
2
3
4
5
6
7
8
9
10
11
SocketCAN fd
io_poll_watch
rxbuf_task
read_task
write_task
rxring + rxbuf
read_queue
write_queue
confirm_queue
current_write
flags / state / mutex

其中:

  • rxbuf_task:从 SocketCAN 读取数据并填充接收环形缓冲区;
  • read_task:用缓冲区中的帧完成上层 read operation;
  • write_task:执行等待中的 write operation;
  • confirm_queue:在启用 MSG_CONFIRM 时等待发送确认。

6. io_can_net_t 初始化时做了什么

io_can_net_init() 的核心步骤可压缩为:

1
2
3
4
5
6
7
8
9
10
flowchart TD
A["保存 executor、timer、channel"] --> B["创建 io_tqueue"]
B --> C["初始化 wait_next 和 wait_confirm"]
C --> D["初始化异步 read/write operation"]
D --> E["初始化 tx_ring 并分配 tx_buf"]
E --> F["创建内部 can_net_t"]
F --> G["读取系统时间并调用 can_net_set_time"]
G --> H["注册 can_net next_func"]
H --> I["注册 can_net send_func"]
I --> J["注册到 io_ctx"]

两个关键注册动作是:

1
2
can_net_set_next_func(core, io_can_net_next_func, io_net)
can_net_set_send_func(core, io_can_net_send_func, io_net)

这两个回调分别完成:

1
2
协议 timer_heap → I/O Timer
协议发送请求 → 异步 CAN channel

6.1 io_can_net_start()

启动后主要做两件事:

1
2
1. 等待 tx_ring 中出现待发送帧
2. 向 CAN channel 提交一个异步 read operation

read operation 完成后会再次提交,因此形成持续接收循环。


7. 完整接收流程

7.1 从 SocketCAN 到 CANopen 协议对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sequenceDiagram
participant K as SocketCAN
participant P as io_poll
participant RXB as rxbuf_task
participant R as rxring
participant RT as read_task
participant ION as io_can_net
participant CORE as can_net_t
participant CO as CANopen receiver

K->>P: fd readable
P->>RXB: post rxbuf_task
RXB->>K: read CAN frame
RXB->>R: store frame and timestamp
R->>RT: signal frame available
RT->>ION: complete io_can_chan_read
ION->>ION: update logical time
ION->>CORE: can_net_recv(msg)
CORE->>CO: dispatch matching receiver callback
ION->>RT: submit next read operation

7.2 Linux 后端为什么先放入 rxring

文件描述符“可读”与上层 read operation 的执行时机不是同一件事:

1
2
3
SocketCAN 已经有数据

executor 现在立刻执行上层 completion task

Linux 后端需要把:

1
2
3
4
5
6
fd readiness
系统调用读取
原始 CAN/CAN FD 帧
接收时间戳
MSG_CONFIRM 标志
异步 read operation

连接起来。

因此 rxring 是 I/O 后端内部的数据缓冲区,而不是协议队列。

7.3 rxring 中保存的不是单纯 can_msg

Linux 后端的接收缓冲元素还包含:

1
2
3
SocketCAN can_frame 或 canfd_frame
实际字节数 nbytes
接收时间戳 timespec

这些信息与 Linux SocketCAN 的读取过程直接相关,所以放在 Linux channel 后端最合理。

7.4 写确认也必须在接收后端处理

启用 txwait 后,SocketCAN 的发送确认以接收路径中的 MSG_CONFIRM 返回。

后端读取帧后必须先判断:

1
2
3
4
5
普通接收帧
→ 进入 rxring

MSG_CONFIRM
→ 匹配 confirm_queue 中的 write operation

如果把接收环移到 io_can_net_t,底层后端仍然必须先解析 MSG_CONFIRM,反而会造成职责拆分和重复缓存。


8. 完整发送流程

8.1 从 CANopen 协议对象到 SocketCAN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sequenceDiagram
participant CO as CANopen protocol
participant CORE as can_net_t
participant TXR as io_can_net tx_ring
participant ION as io_can_net write operation
participant CH as io_can_chan
participant WQ as Linux write_queue
participant K as SocketCAN

CO->>CORE: can_net_send(msg)
CORE->>TXR: io_can_net_send_func copies msg
TXR->>ION: consumer signal when frame becomes available
ION->>TXR: fetch one frame into write_msg
ION->>CH: submit_write
CH->>WQ: queue write operation
WQ->>K: write CAN frame
K-->>CH: completion or confirmation
CH-->>ION: write completion task
ION->>TXR: commit one consumed frame
ION->>TXR: fetch next frame if available

8.2 can_net_send() 成功不等于总线发送成功

io_can_net_t 而言,can_net_send() 返回成功通常表示:

1
该帧已经成功复制到 io_can_net.tx_ring

它并不表示:

1
2
3
4
SocketCAN write 已完成
CAN 仲裁已成功
对端已收到
发送确认已返回

真正的异步写错误通过 on_write_error 回调报告。

tx_ring 已满时:

1
2
3
4
5
6
7
io_can_net_send_func()

返回 ERRNUM_AGAIN

增加 tx_errcnt

触发 on_queue_error

因此 tx_ring 同时定义了协议发送端的背压边界。


9. 为什么 tx_ringio_can_net_t

9.1 原因一:它服务的是 can_net_send() 的语义

can_net_t 的发送接口是同步回调:

1
2
调用 send_func
立即得到成功或失败

而底层 CAN channel 使用异步 operation:

1
2
提交 write
稍后由 executor 执行 completion task

两者之间必须有一个适配层:

1
2
3
4
5
同步产生帧

保存帧副本

异步逐帧提交

这个适配层就是 io_can_net_t,因此发送 ring 属于它。

9.2 原因二:协议层可能在一个回调中产生多帧

例如某个时间推进或接收回调可能连续触发:

  • 多个 PDO;
  • SDO 应答;
  • 心跳;
  • EMCY;
  • NMT 相关消息;
  • 多个同时到期的协议 timer。

这些调用都通过 can_net_send() 快速产生消息。

如果每次都要求底层写操作立即完成,协议状态机会被 I/O 延迟直接阻塞。

tx_ring 允许:

1
2
协议逻辑快速入队
I/O 层按实际通道速度串行发送

9.3 原因三:发送队列长度是 io_can_net 的公开策略

io_can_net_create() 直接接收:

1
txlen

用于配置用户态发送队列长度。

这说明该队列是 CAN 网络桥接层对外暴露的行为策略,而不是 Linux 后端的私有实现细节。

不同后端都应共享同样的:

  • 入队语义;
  • 队列满错误;
  • 丢帧计数;
  • on_queue_error 回调;
  • 顺序发送行为。

所以它应当放在平台无关的 src/io2/can_net.c 中。

9.4 原因四:io_can_net_t 只复用一个 write operation

io_can_net_t 内部只维护:

1
2
一个 write_msg
一个 io_can_chan_write write

运行方式是:

1
2
3
4
5
6
7
8
9
从 tx_ring 取一帧

复制到 write_msg

提交同一个 write operation

完成后释放 ring 中这一帧

继续下一帧

因此:

1
2
tx_ring = 多帧数据队列
write = 当前唯一在途操作

这使发送状态机非常清晰。

9.5 原因五:写确认超时属于桥接策略

io_can_net_t 还维护:

1
2
wait_confirm
txtimeo

每次提交底层 write 后,会通过 io_tqueue 注册确认超时。

收到完成或写错误后再终止该等待。

这说明 io_can_net_t 管理的是:

1
2
3
4
5
帧排队
逐帧写入
写确认等待
确认超时
错误统计

这些是完整的“逻辑网络发送策略”,不应散落在某个特定平台的 CAN channel 后端中。


10. 为什么 rxring 在 Linux can_chan 底层

10.1 原因一:接收入口由底层设备决定

发送帧的生产者是上层协议,io_can_net_t 可以控制是否接受新帧。

接收帧的生产者则是:

1
CAN 控制器 / SocketCAN 内核队列

当文件描述符可读时,底层必须尽快处理 I/O 就绪事件,避免:

  • 内核接收队列堆积;
  • 丢帧;
  • poll 事件反复触发;
  • executor 调度延迟扩大。

因此最接近 fd 的 channel 后端需要拥有接收缓冲。

10.2 原因二:它连接的是 I/O readiness 与 read operation

Linux 后端有两类队列:

1
2
3
4
5
rxring
保存已经收到的数据

read_queue
保存等待数据的 read operation

二者分别代表:

1
2
数据先到,操作后执行
操作先到,数据后到

read_task 的职责就是在二者之间配对。

10.3 原因三:接收数据包含平台相关信息

Linux 后端需要处理:

  • struct can_frame
  • struct canfd_frame
  • CAN_MTU / CANFD_MTU
  • MSG_CONFIRM
  • SocketCAN 接收时间戳;
  • EAGAIN / EWOULDBLOCK
  • IO_EVENT_IN
  • 非阻塞 read()

这些都属于 channel 后端,不属于 io_can_net_t

10.4 原因四:channel 可以脱离 io_can_net_t 独立使用

io_can_chan_t 是通用 CAN I/O 接口。

用户可以直接:

1
2
submit_read()
submit_write()

而不创建 io_can_net_t 或 CANopen 协议对象。

如果接收缓冲只放在 io_can_net_t 中,那么独立使用 channel 时就失去异步接收所需的缓冲和调度能力。

因此接收 ring 必须属于 channel 实现。

10.5 原因五:避免二级接收缓存

io_can_net_t 始终保持一个异步 read operation:

1
2
3
4
5
read 完成

can_net_recv()

再次 submit_read()

底层 channel 已经用 rxring 解决了 I/O 与 task 调度之间的解耦。

如果 io_can_net_t 再增加一个 RX ring,会形成:

1
2
3
4
5
6
7
SocketCAN

channel.rxring

io_can_net.rx_ring

can_net_recv

其结果通常只是:

  • 多一次内存复制;
  • 多一层容量配置;
  • 多一套溢出策略;
  • 增加延迟;
  • 更难定位丢帧发生在哪一层。

所以 Lely 没有在 io_can_net_t 中重复实现 RX ring。


11. 关键点:write_queue 不是 tx_ring

这是理解当前设计时最容易混淆的地方。

11.1 各队列保存的内容不同

队列 所在对象 保存内容 作用
tx_ring + tx_buf io_can_net_t struct can_msg 的副本 缓存协议层待发送帧
write_queue Linux io_can_chan_impl io_can_chan_write.task 节点 缓存等待执行的异步写操作
confirm_queue Linux io_can_chan_impl 已写出但等待确认的 write operation 匹配 SocketCAN MSG_CONFIRM
rxring + rxbuf Linux io_can_chan_impl 收到的原始 CAN/CAN FD 帧与时间戳 缓存已到达的数据
read_queue Linux io_can_chan_impl io_can_chan_read.task 节点 缓存等待数据的异步读操作

11.2 数据队列与操作队列不能替代

tx_ring 解决:

1
有很多 CAN 帧要发

write_queue 解决:

1
有很多异步 write 请求等待 channel 执行

io_can_net_t 的用法中,虽然 tx_ring 可以有很多帧,但它通常只向 channel 提交一个在途 write operation。

也就是说:

1
2
3
4
5
100 个待发送帧
可以位于 tx_ring

底层 write_queue
可能只有 1 个 io_can_net.write operation

这正是两级队列职责不同的证明。

11.3 为什么不把所有帧都变成独立 write operation

若每个协议发送帧都动态创建一个 io_can_chan_write

  • 需要额外分配或管理 operation 对象;
  • 每个对象都要保证 msg 指针生命周期;
  • 取消和确认超时管理更复杂;
  • 协议层突发发送会制造大量 task;
  • 很难统一实现固定长度的丢帧策略。

Lely 选择:

1
2
3
固定长度帧 ring
+
单个可复用 write operation

从而减少对象数量并明确所有权。


12. TX 与 RX 的背压模型本来就不同

12.1 发送方向可以拒绝上层

tx_ring 已满时,io_can_net_send_func() 可以:

1
2
3
返回 ERRNUM_AGAIN
记录 dropped frame
通知 on_queue_error

也就是说,发送方向能够把压力反馈给帧生产者。

12.2 接收方向不能要求总线暂停

当远端节点已经在 CAN 总线上发送帧后,本机不能通过软件调用告诉它:

1
请暂停,等我的 executor 空闲后再发

接收侧只能:

  • 尽快从控制器或内核队列取走;
  • 缓存;
  • 或在容量不足时丢弃。

因此 RX 缓冲必须靠近 I/O 入口。

12.3 Linux 内核本身已有发送队列

SocketCAN 写入后,内核和 CAN 控制器还有自己的发送队列。

底层 channel 再增加一个通用“帧副本 TX ring”会造成多级不可见排队:

1
2
3
4
5
6
7
io_can_net.tx_ring

channel frame ring

SocketCAN send queue

CAN controller mailbox

这样会使:

  • 实际排队深度难以计算;
  • 发送延迟难以预测;
  • 错误归属模糊;
  • txlen 的意义失真。

因此 channel 后端只管理写操作和系统 I/O,不重复承担网络层发送缓存策略。


13. spscring 在两处的生产者和消费者

spscring 是 single-producer / single-consumer ring。

13.1 io_can_net.tx_ring

1
2
3
4
5
6
7
8
9
Producer:io_can_net_send_func()
p_alloc
写 tx_buf
p_commit

Consumer:io_can_net_do_wait() / io_can_net_write_func()
c_alloc
复制到 write_msg
写完成后 c_commit

当 ring 为空时,consumer 注册等待:

1
spscring_c_submit_wait()

producer 提交新帧后,ring 调用:

1
io_can_net_c_wait_func()

进而启动发送。

13.2 Linux can_chan.rxring

1
2
3
4
5
6
7
8
9
Producer:rxbuf_task
从 SocketCAN read
写 rxbuf
p_commit

Consumer:read_task 或同步 read
c_alloc
转换为 can_msg / can_err
c_commit

当 ring 为空时,consumer 注册等待;producer 放入帧后唤醒 read_task

13.3 相同数据结构,不同层次语义

Ring Producer Consumer 业务语义
tx_ring 协议发送适配回调 异步 write 状态机 待发送逻辑 CAN 帧
rxring SocketCAN 读取任务 异步 read 状态机 已接收底层帧

所以不能仅因为两者都使用 spscring,就认为它们应位于同一结构体中。


14. 接收任务之间如何协作

Linux channel 有三个与接收相关的对象:

1
2
3
watch
rxbuf_task
read_task

14.1 watch

io_poll_watch 只负责通知:

1
fd 当前可读或可写

它不执行完整协议处理。

收到 IO_EVENT_IN 后,watch callback 通常只投递 rxbuf_task

14.2 rxbuf_task

主要职责:

1
2
3
4
5
从 fd 读取
识别普通帧或 MSG_CONFIRM
普通帧写入 rxring
确认帧完成 confirm_queue 中的写操作
遇到 EAGAIN 时重新监听 IO_EVENT_IN

14.3 read_task

主要职责:

1
2
3
4
5
检查 read_queue
检查 rxring
把一帧复制给一个 read operation
完成并投递该 operation 的 completion task
ring 为空时注册 consumer wait

14.4 为什么拆成两个 task

如果把 fd 读取、ring 管理、read operation 完成和协议回调全部塞进 poll callback:

  • poll callback 执行时间不可控;
  • 容易在 I/O 监视上下文中执行大量协议逻辑;
  • 难以处理阻塞与非阻塞模式;
  • 难以统一 executor 的任务生命周期;
  • 锁边界更复杂。

拆分后,poll callback 只负责唤醒,executor 负责任务执行。


15. 发送任务之间如何协作

io_can_net_t 与 Linux channel 各自维护一段发送状态机。

15.1 io_can_net_t 发送状态机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tx_ring empty

注册 consumer wait

新帧入队后被唤醒

取一帧到 write_msg

submit_write

等待 completion

释放 tx_ring 当前帧

继续下一帧

15.2 Linux channel 发送状态机

1
2
3
4
5
6
7
8
9
10
11
12
13
submit_write

operation 进入 write_queue

write_task 调用 SocketCAN write

立即错误

成功且无需确认

进入 confirm_queue 等待 MSG_CONFIRM

完成 operation

15.3 两层分别解决的问题

1
2
3
4
5
io_can_net_t
解决“多帧如何排队并逐帧提交”

Linux channel
解决“一个异步 write operation 如何在 fd 上完成”

这就是 TX ring 不应下沉到 Linux channel 的核心原因。


16. 与 io_tqueue + pheap 的完整连接

发送、接收和 Timer 三条链路最终在 io_can_net_t 汇合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
flowchart TD
CT["CANopen can_timer_t objects"] --> CHP["can_net.timer_heap"]
CHP --> NF["can_net_set_next"]
NF --> NCB["io_can_net_next_func"]
NCB --> WN["wait_next"]
WN --> TQ["io_tqueue.queue"]
TQ --> ST["system timer"]
ST --> WNF["io_can_net_wait_next_func"]
WNF --> CST["can_net_set_time"]

CO["CANopen send request"] --> CS["can_net_send"]
CS --> TX["io_can_net.tx_ring"]
TX --> CW["io_can_chan write"]

CR["SocketCAN receive"] --> RX["can_chan.rxring"]
RX --> RR["io_can_net read completion"]
RR --> CNR["can_net_recv"]

因此整个系统可以分成三种适配:

1
2
3
协议时间 → 系统 Timer
协议发送 → 异步 write
底层接收 → 协议 receiver

这三种适配都由 io_can_net_t 组织,但只有平台相关接收缓冲位于 channel 后端。


参考源码

CAN 逻辑核心

  • lely-core/include/lely/can/net.h
  • lely-core/src/can/net.c

I/O 桥接层

  • lely-core/include/lely/io2/can_net.h
  • lely-core/include/lely/io2/can_net.hpp
  • lely-core/src/io2/can_net.c

CAN channel 抽象与 Linux 后端

  • lely-core/include/lely/io2/can.h
  • lely-core/src/io2/linux/can_chan.c

数据结构

  • lely-core/include/lely/util/spscring.h
  • lely-core/include/lely/util/pheap.h
  • lely-core/src/util/pheap.c

前置学习笔记

  • Lely_CANopen_io_tqueue_pheap_机制原理与运行流程(1).md

协议参考

  • CiA 301 V4.2.0:CANopen 应用层和通信协议