本文简要分析Zephyr网络部分从应用层到驱动层的纵向数据流,以作为分析了解Zephyr网络协议栈的基础。
概述
zephyr抽象出net_if提供一般性的API给ip layer使用,本文分析net_if如何基于L2将device drv和ip layer相连。
zephyr使用net_context/net_conn组织tcp/ip stack API,并提供一般性的API给app使用使用,本文分析net_context/net_conn如何和ip layer相连并传递数据。
关联
下图说明了net_if如何和其它部分进行关联
说明如下
- net_if是上下层连接层
- 编译期,通过NET_DEVICE_INIT,将device drv, l2注册入net_if(iface)
- 运行时,通过device drv init将net_if(iface)注册入device drv
- 运行时,通过net_if_ipv4_addr_add/net_if_ipv6_addr_add为net_if(iface)配置ip地址
- net_context是ip layer操作网络的句柄,net_context实例化为一个context
- 运行时,通过net_context_bind为context配置ip地址,配置的同时会将该ip地址的net_if(iface)注册入context
- 当ip layer通过context收发数据,而context会使用自己拥有的net_if(iface)来收发数据
编译期
编译期使用NET_DEVICE_INIT 完成了device的初始化并调用NET_IF_INIT完成net_if到l2&device drv的关联
1 | #define NET_DEVICE_INIT(dev_name, drv_name, init_fn, \ |
上面代码中DEVICE_AND_API_INIT是初始化一个网络设备驱动, 最后生成一个struct device结构体变量,详细参考zephyr驱动模型,这里说明一下和关联相关的部分
DEVICE_AND_API_INIT 会注册api和data
- api最开始的成员是struct net_if_api,其中init是device用于设置自己的net_if(iface)
- 通常情况下data在device drv实现时会在起内部预留字段用于保存net_if(iface), 运行时执行net_if_api的init就将net_if(iface)设置给device drv,让device drv知道自己归属于哪个net_if(iface)
1
2
3
4struct net_if_api {
void (*init)(struct net_if *iface);
int (*send)(struct net_if *iface, struct net_pkt *pkt);
};
编译期使用NET_L2_INIT定义一个net_l2结构体变量,将定义好的5个l2函数注册进去
1 | struct net_l2 { |
NET_IF_INIT主要完成一个net_if(iface)和device drv, l2的绑定. net_if可以找到device和l2
1 | #define NET_IF_INIT(dev_name, sfx, _l2, _mtu, _num_configs) \ |
运行期
运行期完成net_if(iface)到device的关联(device可以找到自己的iface)
运行期完成context到net_if(iface)的关联,二者的桥梁是ip地址
device到net_if的管理
device drv内部要知道自己所属的net_if(iface),尤其是在收到packet后,需要将packet送到对应的net_if(iface), net_if是在net初始化时被设置到device drv的
1 | SYS_INIT(net_init, POST_KERNEL, CONFIG_NET_INIT_PRIO); |
系统初始化定义了net_init在POST_KERNEL level时被调用,参考zephyr驱动模型
net_init内按照下面的调用顺序执行到net_if(iface)被设置到device内
net_init->init_rx_queues->net_if_init
1 | void net_if_init(void) |
context到net_if(iface)的关联
net_if和ip地址绑定
通过net_if_ipv4_addr_add/net_if_ipv6_addr_add,将ip地址保存存iface(net_if),以ipv6为例
1 | struct net_if_addr *net_if_ipv6_addr_add(struct net_if *iface, |
context和ip地址绑定
通过net_context_bind将ip地址和context绑定时,会根据ip地址查找对应的iface,同时将iface和context绑定
1 | int net_context_bind(struct net_context *context, const struct sockaddr *addr, |
关联总结
编译期:
定义device
定义l2
定义net_if
net_if -> device
net_if -> l2
运行期:
device->net_if(iface)
net_if->ip address
ip_address->net_if(iface)
context->ip_address
context->net_if(iface)
经过上面两个时期的关联:
上层通过context发送数据,可以找到net_if,再找到l2,经过l2的处理再找到device drv发送数据
device 接收数据可以通过device找到net_if
数据通路
网络上数据通路我们比较关心的收发,Zephyr将TCP/IP Stack进行包装成context(Network Connectivity API)和socket(BSD Sockets compatible API)两种形式, 这里基于context分析网络数据流向。
Zephyr官网上给出了数据流向图如下,可以看到最后数据的接收全部都是依赖callback
发送
数据包形成
从Network Connectivity API的应用者来看发送数据的数据包都是使用struct net_pkt net_pkt_get_tx(struct net_context context, s32_t timeout)来申请的,在申请的时候就为其指定了context。该context和对应的iface(net_if)被保存在pkt内。之后发送数据时会根据pkt内的context来选择发送数据通路
数据包发送
从Network Connectivity API的应用者来看发送数据都是基于TCP/UDP,二者最后都是走到net_send_data
UDP
net_context_send(struct net_pkt pkt,…)->sendto(pkt, …)->net_send_data(pkt)
TCP
net_tcp_send_data(struct net_pkt pkt,…)->net_tcp_send_pkt(pkt)->net_send_data(pkt)
1 | int net_send_data(struct net_pkt *pkt) |
在不同的l2中会对pkt做不同的处理(这里不对l2函数进行分析可以参考net_bt_send & ethernet_send内实现),在l2函数中调用net_if_queue_tx进行发送, 这里的发送实际只是将pkt放入work queue
1 | void net_if_queue_tx(struct net_if *iface, struct net_pkt *pkt) |
work queue的main thread收到work queue时使用process_tx_packet接收pkt
1 | static void process_tx_packet(struct k_work *work) |
ARP/ICMP
ARP/ICMP是TC/IP内的协议,不用Network Connectivity API来处理,而是在ip内就处理了,其直接通过net_if_queue_tx/net_send_data来发送数据
接收
数据包形成
在device drv内获取的数据都是网络封包净数据,device drv通过net_pkt_get_reserve_rx申请pkt,并将净数据放入其内,之后发送到自己所拥有的iface上, drv内的代码不做分析,可参见ipsp_recv & eth_enc28j60_rx
数据包接收
device准备好接收的pkt后调用net_recv_data把数据向上送
1 | int net_recv_data(struct net_if *iface, struct net_pkt *pkt) |
work queue的main thread收到work queue时使用process_rx_packet接收pkt
1 | static void process_rx_packet(struct k_work *work) |
之后经过几层函数包装调用到process_data
net_rx(struct net_if iface, struct net_pkt pkt) -> processing_data(pkt, …) -> process_data(net_pkt, …)
1 | static inline enum net_verdict process_data(struct net_pkt *pkt, |
ip层数据接收
ipv4和ipv6的数据接收主流程类似,最后都是分为icmp/udp/tcp,这里以ipv6代码示例,可以看到icmp使用process_icmpv6_pkt处理,而TCP/UDP统一使用net_conn_input处理
1 | enum net_verdict net_ipv6_process_pkt(struct net_pkt *pkt) |
ICMP
icmp处理可分为2部分来看, icmpv4和v6的处理流程相似,这里分析ipv6的代码
注册处理函数
net_init->l3_init->net_icmpv6_init->net_icmpv6_register_handler(&echo_request_handler)
实际icmp的处理函数是handle_echo_request
1 | static struct net_icmpv6_handler echo_request_handler = { |
ICMP数据处理
1 | enum net_verdict net_icmpv6_input(struct net_pkt *pkt, |
TCP/UDP
TCP/UDP都是使用net_conn_input进行接收数据的处理, 涉及到conn和context的横向涉及这里不做分析,只说明数据的垂直流向
net_conn_input要处理数据也分为2部分
注册处理函数
TCP和UDP都是将cb保存在context中,但之后的包装方式不太相同
1 | int net_context_recv(struct net_context *context, |
对于UDP来说
1 | static int recv_udp(struct net_context *context, |
对于TCP只将cb放入到context中不做其它注册动作
1 | int net_tcp_recv(struct net_context *context, net_context_recv_cb_t cb, |
但实际的注册动作纵向看相当曲折,这里简要列出
context使用者使用net_context_connect进行tcp连接,其作用就是将tcp callback tcp_synack_received注册到conn内
1 | int net_context_connect(struct net_context *context, |
直到这里tcp的conn还没有和net_tcp_recv注册的cb挂钩,继续看tcp_synack_received
1 | NET_CONN_CB(tcp_synack_received) |
数据处理
enum net_verdict net_conn_input(enum net_ip_protocol proto, struct net_pkt *pkt)
{
//查找最匹配的conn
for (i = 0; i < CONFIG_NET_MAX_CONN; i++) {
...
if (best_match >= 0 &&
net_sin(&conns[best_match].remote_addr)->sin_port) {
continue;
}
if (best_rank < conns[i].rank) {
best_rank = conns[i].rank;
best_match = i;
}
}
if (conns[best_match].cb(&conns[best_match], pkt,
conns[best_match].user_data) == NET_DROP) {
goto drop;
}
}
对于UDP: conns[best_match].cb 调用callback处理函数就是net_context_packet_received,然后在该函数内调用recv_udp注册到context的context->recv_cb 进行处理
对于TCP:
conns[best_match].cb 第一次调用tcp_synack_received 注册tcp_established到conns
conns[best_match].cb 第二次调用tcp_established, 在tcp_established内调用net_context_packet_received,然后在该函数内调用net_tcp_recv注册到context的context->recv_cb 进行处理
总结
UDP接收数据直接调用net_context_recv注册数据接收callback即可
TCP接收数据需要先net_context_connect建立连接,收到数据时net_context_recv注册的callback才会被调用
参考
https://docs.zephyrproject.org/latest/subsystems/networking/ip-stack-architecture.html