☰
QNX开发之ECAT专用网卡驱动ecpkt · 06-调试
2026/10/14 19:31:26 网站建设 项目流程

QNX开发之ECAT专用网卡驱动ecpkt · 06-调试

Iopkt原生网络框架的一大优势就是可以依赖OS提供的网络工具,比如tcpdump/netstat等等,对于ECAT调试来说,分析通信报文是不折不扣的屠龙技,通过分析报文可以解决ECAT使用中绝大多数的问题,尤其通过对照TwinCAT报文,对ECAT的调试有非常大的帮助。但是在ecpkt这里,对于OS来说,ecpkt并不属于通用的网络设备,所以也无法使用OS提供的网络工具,不过ecpkt里的报文数据完全是由ecpkt自己管理的,我们可以针对ecpkt在开发一套抓包设施,专门用来抓取ecpkt的通信报文进行分析。

Iopkt下报文抓取大致可以分为以下几块:

  • 通过BPF注入,获取报文副本(接收/发送)
  • 使用libpcap封装BPF,为应用层提供BPF配置和使用的接口
  • 应用层使用tcpdump工具,通过libpcap获取报文副本并输出给用户

我们在开发ecpkt下的抓包设施时可以仿照这个结构:

  • RM内额外新增采集端,用来获取报文副本
  • 用户态开发ec_caplib,对应libpcap,封装用于报文抓取的用户接口
  • 用户程序ec_capdump,对应tcpdump,通过ec_caplib获取报文副本并输出

另外,ecpkt抓包时将报文仿照libpcap的格式进行存储,这样就可以得到“.pcap”格式的报文文件,这个报文文件可以直接在wireshark里打开进行分析,就不用再专门开发报文解析的工具了。下面介绍具体的设计:

1. 抓包机制设计

我们通过在RM里额外再创建一个设备节点/dev/ec_gem_cap来进行抓包,使用这种方式有下面几点优势:

  • 使用新设备节点可以支持poll系统调用,兼容性更好
  • 通过设备fd隔离可能存在的多个用户态读者
  • 抓包能力从 /dev/ec_gem 的 ABI 里完全剥离。

新节点与设备节点的整体框架如下:

另外,消费者 cap_node.c 的 read 可以直接复用 /dev/ec_gem的延迟回复机制(pending 数组 + 自脉冲 + service_pending + unblock 回收),进一步减小工作量。

2. 报文副本获取

Iopkt框架下报文副本依靠BPF注入获取,原生devnp驱动中已经包含有接收报文和发送报文的挂载点:

  • 接收报文的挂载点在从bd里获得数据区地址后面
  • 发送报文的挂载点在数据区写入bd之后,触发发送动作之前

接收报文的挂载点比较清楚,没有什么问题,但是发送报文的挂载点其实有一些额外的考虑:如上面所说,发送报文的挂载点在数据区写入bd之后,发送之前,这里有个问题,BPF抓包注入实际上就是将报文拷贝到副本的缓存空间里,简单来说会有一次memcpy的调用,这个memcpy的调用是一个纯软件的耗时操作,而且是可以被抢占的,那就意味着在这个memcpy的调用中可能会给报文发送引入额外的抖动,那么在ECAT通信中就代表实时性降低,这样看似乎把挂载点放到触发发送动作之后更合适,可以尽量减少报文发送的抖动。

实际上如果把挂载点放在发送之后会存在一个问题:一般来说驱动中的网络报文发送都是可重入的(比如ECAT中通常会把sdo交互和pdo交互分成单独的线程,那么在驱动层面pdo报文和sdo报文同时到达的情况就不可避免),如果我们把挂载点放在发送之后,设想一种极端的场景,sdo报文传输在 xzynq_transmit 之后、cap_push 之前被更高优先级的 PDO 交互打断,PDO交互触发发送流程之前会遍历bdring释放完成发送的BD,那么sdo报文对应的bd由于完成了发送会被直接回收,然后这个bd可能世界分配给本轮pdo发送来使用,那么当pdo完成发送驱动回到sdo报文处理流程来执行memcpy获取sdo报文副本时,bd中的数据已经被替换掉了,这样抓取到的报文就不对了。

当然在上面的例子中,主线的通信还是正常的,只是抓包的报文不可靠了,所以选在把发送报文副本获取的挂载点放在发送之前实际上是一种为了正确性牺牲实时性的妥协。

注:当前ecpkt的设计实际上是单write通道,多个write操作实际上是在RM内部串行处理的,不会触发上面的情况,这里还是先采用在发送之前抓包的设计,以便后面扩展RM为基于优先级的抢占式发送模式。

3. 报文副本 frame_cap 的设计

抓包副本用什么缓冲,第一反应往往是「复用 frame_pool」——报文本来就在那里。但 frame_pool 是给 DMA 用的:MAP_PHYS 承诺物理连续、PROT_NOCACHE 关掉 cache,这两样对一份 DMA 引擎永远不碰的副本缓冲是纯开销——NOCACHE 让 CPU 每次读都穿 cache 到 DRAM,物理连续限制了分配的灵活性。frame_cap 的环刻意不建在 frame_pool 上,就是一片普通的 calloc() 可缓存内存:CPU 写、CPU 读,cache 只帮忙不碍事。

环的布局:slots × stride 的一维数组,stride = ROUND_UP(sizeof(rec) + data_cap, 64)。rec 是 32 字节记录头——ready(发布戳)、ts_ns(CLOCK_MONOTONIC 纳秒时间戳)、dir(TX/RX 方向)、origlen(线上原始长度)、caplen(实际存储长度)——payload 紧随其后。默认 1024 槽 ×(32+1536)字节约 1.6 MB,init 时一次分配,帧路径上零 malloc。

并发模型是 MPSC:多生产者(TX dispatch 线程、RX worker 线程可能同时调 frame_cap_push),单消费者。协议分三步:第一步锁内预约——检查 head-tail < slots,满则 dropped++ 直接返回(丢帧不阻塞帧路径),否则拿走 seq=head++,临界区只有几条指令;第二步锁外填数——memcpy payload 进自己预约到的槽,这个槽在 tail 追上来之前只属于自己;第三步发布盖章——release 语义原子写 ready=seq+1,保证消费者看到匹配 ready 之前 payload 一定可见。

消费侧的关键是「完整发布谓词」:ready == tail+1 才算可读,head > tail 不够——生产者可能预约了序号但 payload 还没拷完。记录按序不可跳:队头记录没发布完,后面的即使发布完也要等,顺序保证了 pcap 文件里的时间线。另一条纪律是「失败不消耗」:take 时目标 buffer 不够大(EMSGSIZE),记录留在队头原地不动,调用方换个大 buffer 重试不丢帧。

消费者有两种形态,二选一:默认形态是模块内置的低优先级 writer 线程(SCHED_RR prio 10,低于 RX worker 和 iofunc),把环里的记录搬到 PRINT 和/或 PCAP sink——自包含,不需要客户端进程;PULL 形态(FRAME_CAP_FLAG_PULL)不建线程、不建 sink,环的拥有者自己调 frame_cap_ready() / frame_cap_take() 取帧——/dev/ec_gem_cap 的 cap_node 走的就是这条路,客户端通过 read()/poll() 拿记录。两种消费者共用同一套生产者和环协议,但绝不能同时存在——一个 head/tail 环只容得下一个 tail。

与 frame_pool 的接缝在 ec_cap_push:它从 frame_pool slot 里取数据(frame_pool_data / frame_pool_len),拷一份进 frame_cap 的环。两个子系统互相不知道对方存在——frame_pool 不需要懂抓包,frame_cap 不需要懂 DMA。挂点挂上之后,DMA 帧缓冲照样被 reap 回收、被下一帧复用,而副本已经安全落在抓包环自己的内存里。

以上就是在ecpkt下抓包设施的完整设计,实测可以正确的获得支持wireshark的报文文件,至此,整个ecpkt的开发就全部完成了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询