SocketCAN 架构解析:Linux 内核中的 CAN 子系统
好,咱们今天来聊聊 SocketCAN 的内核架构。说实话,我刚接触这块的时候,也被它那套“既是网络设备又是字符设备”的玩法搞晕过。但搞懂了之后你会发现,这套设计其实非常巧妙。
SocketCAN 不是简单的驱动堆叠。它是 Linux 内核里一个完整的子系统。你想想看,CAN 总线上的数据流,本质上和网络数据包很像——都有帧、都有ID、都需要路由。所以内核社区干脆把它塞进了网络协议栈。这个决策,我个人觉得非常高明。
双面娇娃:字符设备与网络设备模型
SocketCAN 最让我印象深刻的一点,就是它同时支持两套设备模型。为什么要这么搞?
- 网络设备模型:每个 CAN 接口对应一个 net_device,比如 can0、can1。你可以用 ifconfig 去配置它,用 tcpdump 去抓包。这套模型负责 CAN 帧的收发、过滤、协议处理。
- 字符设备模型:通过 /dev/can0 这样的节点,你可以用 read/write 直接操作。这套模型主要用于 CAN 总线诊断、固件升级这类需要原始访问的场景。
我在一个车载项目中遇到过一个问题:客户想同时用 SocketCAN 的 RAW socket 收报文,又想让诊断工具通过字符设备访问同一个接口。当时我查了半天,发现内核里这两套模型其实是共享同一个接收队列的。嗯,这里要注意——它们不是独立的,底层数据流是互通的。
核心要点:网络设备模型负责“协议栈”,字符设备模型负责“裸数据”。两者通过内核内部的 sk_buff 队列桥接。
关键数据结构:从 sk_buff 到 can_frame
搞 SocketCAN 内核编程,有几个数据结构你必须刻在脑子里。我列个表,方便你对照着看。
| 数据结构 | 所在文件 | 作用 |
|---|---|---|
struct sk_buff |
include/linux/skbuff.h | 网络子系统通用数据包结构,SocketCAN 用它承载 CAN 帧 |
struct can_frame |
include/uapi/linux/can.h | CAN 2.0 标准帧结构,包含 ID、DLC、数据 |
struct canfd_frame |
include/uapi/linux/can.h | CAN FD 帧结构,支持最多 64 字节数据 |
struct net_device |
include/linux/netdevice.h | 网络设备核心结构,CAN 接口的抽象 |
struct can_priv |
include/linux/can/dev.h | CAN 设备私有数据,包含波特率、状态、中断等 |
你可能会问:为什么 CAN 帧要套一层 sk_buff?直接传 can_frame 不行吗?
原因很简单——复用。sk_buff 提供了现成的队列管理、内存分配、协议头操作函数。SocketCAN 只需要在 sk_buff 的 data 区域塞一个 can_frame,就能享受整个网络子系统的红利。我刚开始写 CAN 驱动时,也想过自己搞一套队列,后来发现 sk_buff 的 API 实在太香了。
内核 CAN 子系统的分层架构
咱们用一张图来理清整个架构。这张图我画了很多遍,每次给团队新人讲都拿它当提纲。
这张图里,我最想强调的就是 CAN 核心层。它是个“隐形英雄”——大部分开发者只跟协议族层打交道,但真正做过滤、路由、错误处理的,是这一层。我调过的一个 bug,就是核心层里过滤规则哈希表冲突导致的丢帧。当时排查了整整两天。
字符设备模型的实现细节
除了网络设备模型,SocketCAN 还通过 can-dev 模块提供了字符设备接口。这个接口的实现思路其实很直接:
- 注册一个 miscdevice,主设备号 10,次设备号动态分配。
- open 时关联到对应的 net_device。
- read/write 时直接操作 sk_buff 队列。
- ioctl 用于设置波特率、配置过滤器、查询状态。
我曾经在一个工控项目里,用字符设备接口写了个 CAN 总线监控脚本。当时图省事,直接用 cat /dev/can0 看原始数据。结果发现数据全是乱码——因为字符设备输出的是二进制 can_frame,不是 ASCII。嗯,这个坑我替你们踩过了。
小技巧:如果你只想快速验证 CAN 收发,可以用 cat /dev/can0 | hexdump -C。但正式项目还是老老实实用 SocketCAN 的 RAW socket 吧。
关键数据结构深度解析
咱们再深入看看几个核心结构体。我挑三个最常用的,给你拆开揉碎讲。
struct can_frame
struct can_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
__u8 can_dlc; /* 数据长度 (0..8) */
__u8 __pad; /* 填充字节 */
__u8 __res0; /* 保留 */
__u8 __res1; /* 保留 */
__u8 data[8] __attribute__((aligned(8)));
};
注意 can_id 这个字段。它不只是 ID,高 3 位还藏着 EFF、RTR、ERR 标志位。我见过有人直接拿 can_id 做哈希,结果把标志位也带进去了,导致过滤失效。正确的做法是先用 CAN_EFF_MASK 或 CAN_SFF_MASK 做掩码。
struct can_priv
struct can_priv {
struct can_device_stats can_stats;
struct can_bittiming bittiming;
struct can_bittiming_const *bittiming_const;
struct can_clock clock;
enum can_state state;
u32 ctrlmode;
int restart_ms;
struct delayed_work restart_work;
int (*do_set_bittiming)(struct net_device *dev);
int (*do_set_mode)(struct net_device *dev, enum can_mode mode);
int (*do_get_state)(struct net_device *dev, enum can_state *state);
};
这个结构体是每个 CAN 设备驱动的“控制面板”。我最常用的是 bittiming 和 state。调试总线问题时,先看 state 是不是 CAN_STATE_ERROR_ACTIVE,如果不是,说明总线已经出问题了。
struct sk_buff 中的 CAN 相关字段
sk_buff 本身很庞大,但 SocketCAN 主要用了这几个:
skb->protocol:设置为htons(ETH_P_CAN)或ETH_P_CANFDskb->pkt_type:标记帧类型(PACKET_HOST / PACKET_BROADCAST)skb->dev:指向对应的 net_deviceskb->data:指向 can_frame 或 canfd_frame
注意:千万不要直接修改 skb->data 指针,除非你清楚自己在做什么。我曾经在驱动里为了省一次 memcpy,直接移动了 data 指针,结果导致上层协议解析全乱套。内核里有些坑,看着小,踩进去就是大半天。
设备注册与初始化流程
最后,咱们捋一遍 CAN 设备从注册到工作的完整流程。这个流程我建议你背下来,写驱动时每一步都可能出问题。
- 分配 net_device:调用
alloc_candev(),传入 priv 结构体大小。 - 设置硬件参数:填充 can_priv 中的 bittiming、clock、ops 函数指针。
- 注册网络设备:
register_candev(),内部会调用register_netdev()。 - 启动设备:用户执行
ip link set can0 up type can bitrate 500000。 - 内核回调:驱动层的
open()函数被调用,初始化硬件、申请中断。 - 开始收发:中断处理函数调用
netif_rx()或napi_gro_receive()将帧送入协议栈。
我记得第一次写 CAN 驱动时,卡在了第 4 步。用户一执行 up 命令,内核就 panic。查了半天,发现是 bittiming 结构体里的 brp 字段填错了,导致时钟分频除零。从那以后,我每次写驱动都会先打印一遍 bittiming 参数,确认无误再注册。
好了,SocketCAN 的内核架构就讲到这里。你把这些数据结构、分层模型、注册流程搞清楚了,后面讲过滤机制和性能优化时,你就能知道每个优化点到底在内核的哪一层起作用。这是基本功,但也是最值钱的部分。