QNX开发之ECAT专用网卡驱动ecpkt · 01-背景
QNX上使用网络通信框架iopkt来统一管理网络通信:应用程序通过通用socket接口向iopkt发起通信请求,网卡驱动则由系统预先注册到iopkt框架中,iopkt再通过这些注册进来的驱动控制网卡硬件进行报文收发,从而完成网络通信。这是iopkt的框架图:
EtherCAT本质上还是网络通信,所以按照传统方法在QNX上运行ECAT主站,报文管理还是要交给iopkt。主站(这里以SOEM为例)在上图中扮演的就是Application的角色:SOEM调用socket接口发起通信,请求被转交给iopkt(微内核体系下它同样是一个进程),iopkt再通过Drivers控制网卡硬件完成报文收发,最后数据仍经由socket接口回到SOEM。
使用这种方法实现主站是最快捷的:开发人员直接使用socket接口,完全不用关心协议栈和硬件控制,很快就能实现一个可用的主站。但这种方法有几个问题:
-iopkt原生不支持RAW SOCKET通信
EtherCAT通信本质上是链路层通信,通信报文的组包和解包完全由主站自己完成,不需要TCP/IP协议栈介入,所以主站在socket层面一般直接使用RAW SOCKET进行通信。但iopkt不支持RAW SOCKET,要在iopkt框架下进行链路层的RAW SOCKET通信,需要使用BPF接口。直接使用BPF原始接口进行网络报文收发,开发门槛极高,调试也非常困难;好在QNX自带基于BPF的pcap支持,可以使用pcap封装的API来代替。下面是我使用pcap进行链路层通信的代码,作为示例供参考:
- pcap 链路层socket的创建和配置:
*psock = pcap_create(ifname, errbuf); if (*psock == NULL) { fprintf(stderr, "failed to create pcap handler: %s\n", errbuf); return -1; } if (pcap_set_buffer_size(*psock, 65536) != 0) { fprintf(stderr, "failed to set pcap buffer size: %s\n",pcap_geterr(*psock)); pcap_close(*psock); return -1; } if (pcap_set_timeout(*psock, ECAT_TICK_PERIOD) != 0) { fprintf(stderr, "failed to set pcap timeout: %s\n", pcap_geterr(*psock)); pcap_close(*psock); return -1; } // Optional // - PCAP_OPENFLAG_PROMISCUOUS:pcap_set_promisc(handle, 1); (1=on,0=off) // - filter pcap_set_promisc(*psock, 1); pcap_set_immediate_mode(*psock, 1); if (pcap_activate(*psock) != 0) { fprintf(stderr, "failed to active pcap handler: %s\n", pcap_geterr(*psock)); pcap_close(*psock); return -1; } if (pcap_lookupnet(ifname, &net, &mask, errbuf) == -1) { perror("lookup net");} if (pcap_compile(*psock, &fp, filter_exp, 0, net) == -1) { perror("compile");} if (pcap_setfilter(*psock, &fp) == -1) { perror("set filter");} if (NULL == *psock) { printf("interface %s could not open with pcap\n", ifname); return 0; }
- pcap 链路层socket的收发:
rval = pcap_sendpacket(*stack->sock, (*stack->txbuf)[idx], lp); res = pcap_next_ex(*stack->sock, &header, &pkt_data);
-iopkt处理报文的路径过长,会给实时通信引入抖动
EtherCAT报文的收发路径是:
应用程序 → socket → iopkt 进程 → 协议栈 → mbuf → devnp 驱动 → GEM 硬件
对普通TCP/IP流量这条路径毫无问题;但对EtherCAT这种周期性硬实时报文(毫秒级周期、抖动敏感),它引入了三类不可控延迟:
- mbuf管理——每帧都要经过mbuf池的分配、链式组织、释放;
- 协议栈穿行——裸L2帧也要在栈的输入/输出队列里排队;
- work thread调度——iopkt用多线程处理驱动事件,报文何时被处理取决于线程调度,而不是"硬件何时到达"。
-EtherCAT和Ethernet报文混杂在一起由iopkt处理,Ethernet报文吞吐量大时会严重影响EtherCAT实时性
这才是最致命的问题。在iopkt框架下,所有的socket通信都会统一交给iopkt管理。虽然iopkt内部会通过多线程并行来降低报文阻塞的风险,但它的多线程模型无法进行精细管理。比如我们可以在应用层通过提高EtherCAT进程优先级、指定进程CPU亲和性等方式,保证EtherCAT报文优先提交给iopkt;但iopkt内部的报文处理线程无法绑定到应用程序上,也就无法在iopkt内部保证接收到的EtherCAT报文被优先处理。这样一来,Ethernet报文吞吐量大时,EtherCAT的实时性必然会受到影响。
其实QNX对这个问题有一个折中方案:运行多个iopkt实例,分别管理指定的网卡,不同实例可以使用不同的进程优先级,以此处理不同优先级的通信。
但这种做法有个限制:多个iopkt实例之间有点类似于"用户组"的概念,同一个用户(进程)只能使用一个用户组下的资源。简单来说,EtherCAT应用只能使用管理EtherCAT网卡的iopkt实例的网络资源,Ethernet应用只能使用管理Ethernet网卡的实例的网络资源,这就要求系统必须将EtherCAT功能和Ethernet功能拆分为独立进程,两者通过IPC进行数据交互。这样一来,一是IPC的效率低于进程内线程间的数据交互,二是给用户系统额外增加了一层限制,所以并不是一种最优的方案。
总结一下:上面这些问题的根源,是把为TCP/IP设计的iopkt框架强行套在了EtherCAT身上;而EtherCAT本质是链路层通信,对通信框架的依赖非常低——它完全不需要TCP/IP协议栈的参与。所以我们考虑将EtherCAT通信从iopkt中分离出来,通过专用的网卡驱动框架ecpkt直接驱动硬件,彻底绕开iopkt框架。使用ecpkt后,通信链路会变成下面这样:
应用程序 → write/read → ecpkt → GEM 硬件
系统层面上的混合通信,使用iopkt时是这样:
使用ecpkt之后会是这样:
ecpkt将作为一个Resource Manager独立运行在系统里。改造也有现成的起点:iopkt本身已经包含了一个适配自己框架的可用网卡驱动,具备完整的初始化、数据收发管理、中断管理等功能。我们将会基于这个驱动,改造出一个满足我们需求的功能完备的专用驱动——这也是本系列后续章节的主线。