1. 报文接收/发送队列
每个协议模块,在每个核上,拥有两个队列,分别是接收与发送队列。
例如,rcv_que_eth、snd_que_eth、rcv_que_ip、snd_que_ip、rcv_que_ipsec、snd_que_ipsec、rcv_que_udp、snd_que_udp、
2. 每个协议模块,有两个入口函数,一个是接收入口——负责从接收队列接收处理本模块的报文,一个是发送入口——负责将发送队列中的报文发送出去。
例如,rcv_eth、snd_eth、rcv_ip、snd_ip等。
3. 媒体面线程(每核一个)主循环。
int main_loop()
{
get a packat from packet pool into rcv_que_eth;
rcv_eth(); /*处理rcv_que_eth中的报文,送到rcv_que_ip中 */
rcv_ip(); /*处理rcv_que_ip中的报文,送到上层协议模块的接收队列中 */
rcv_ipsec(); rcv_udp(); rcv_tcp(); /* 各协议模块处理接收队列中的报文,送入相应模块的接收/发送队列中 */
snd_tcp(); snd_udp(); snd_ipsec();
snd_eth(); /*将snd_que_ip中的报文全部发送出去 */
return 0;
}
上述架构,虽然采用的是非常明显的流水线式的处理模式,但却是以同步的方式执行的。
为什么要这样设计,我们可以考虑如下问题。
一、如果将流水线不同阶段的工作分配到不同的核上,会有两个难题:
1. 各阶段的处理工作,需要几个核?弄得不好,可能造成性能损失。而且各种协议的流量是动态变化的,固定的分配难以达到最佳性能。
2. 多个核之间,可能会存在同步/互斥的开销,另外还可能有cache开销。
二、如果在每个核上为流水线不同阶段的工作创建线程,会有如下问题:
1. 如果大家同时运行,可能会带来性能损失。
2. 如果在有报文时唤醒相应线程,可能难以适应流量突发的情况。有时一瞬间流量可能会达到上百万pps。
三、如果不采用流水线式的处理模式,则可能会有如下难题:
某些隧道报文处理完成后,可能需要再次经过ip层的入队接收。这时可能会造成递归调用IP层的接收函数。让软件变得复杂。
而且,实际应用中,可能存在多重隧道(例如gre与ipsec隧道并存),并且隧道的封装顺序在不同的组网环境下是由用户动态配置的。这种情况下,软件就变得更加复杂了。
而采用流水线式的处理模式,就好办了。
构造一张函数指针表,根据用户的隧道配置情况,将相应模块的函数指针填入表中,然后main_loop()中按顺序调用其中的函数即可。
例如,某个环境下,用户配置了ipsec隧道与gre隧道。并且,封装顺序是先进行ipsec封装,再进行gre封装。
我们得到用户的配置后,生成如下的函数指针表proto_chain。
proto_chain:
rcv_eth();
rcv_ip();
rcv_gre();
rcv_ipsec();
rcv_ip();
rcv_udp();
rcv_tcp();
snd_tcp();
snd_udp();
snd_ipsec();
snd_gre();
snd_eth();
main_loop()的实现就简单了,直接按顺序调用proto_chain中的函数即可。
而协议模块的实现也很简单,如果解封装后得到ip报文,直接放入rcv_que_ip即可。