总体概览如下:
假设upcall handler线程有两个,vport有四个,那么每个vport下都将持有两个NetLink连接的信息,这两个NetLink连接将被用来上送upcall消息。
每个NetLink连接被对应的upcall handler线程接管,多个vport在同一线程中的NetLink连接被epoll管理。
1:每个vport下都挂多个NetLink连接,数量等同于upcall处理线程的数量
2:线程中routine函数为udpif_upcall_handler,伪码如下:
routine {
while(线程不设自杀标记) {
if(从epoll中收到了一些upcall消息,则进一步处理upcall) {
continue;
} else {
注册poll:当epoll_fd有骚动时解除当前线程的block
}
block当前线程
}
}
3:每个NetLink连接被称为一个upcall channel
1:upcall处理线程的创建
upcall处理线程随着vswitchd进程的启动而被创建,概览如下:
最终所有线程信息会被记录在 ofproto_dpif->backer->udpif下
2:vport的创建与channels的创建
在基于本机内核的datapath实现的openflow交换机场景下,用户态的datapath接口将采用dpif_netlink_***系列函数。vport的创建在用户态最终将调用到dpif_netlink_port_add,这是dpif_netlink_port_add__的包裹函数。
先说一点我对NetLink连接的理解
1:创建socket
2:可选的bind步骤,在这一步,应当在本端地址结构中赋值nl_pid为当前进程ID,并bind之。
3:建立连接connect,这一步对端地址结构的nl_pid值如果为非0值 ,那么连接的就是一个用户态进程。如果nl_pid值为0,则连接的是内核。
如果没有第2步,那么在connect之后,内核会为这个NetLink连接的本端bind地址结构自动指定一个nl_pid值,以标识该连接。此时通过getsockname取到的本端地址结构中的nl_pid字段将不是发起连接一方的进程ID。
3:内核cache miss
内核cache miss后,会发送upcall消息,
netdev_frame_hook(pskb)
|
\-> netdev_port_receive(skb, NULL/skb_tunnel_info(skb))
|
\-> ovs_vport_receive(ovs_netdev_get_vport(skb->dev), skb, tun_info = NULL/skb_tunnel_info(skb))
|
\-> error = ovs_flow_key_extract(tun_info, skb, &key)
|
\-> ovs_dp_process_packet(skb, &key)
|
\-> flow = ovs_flow_tbl_lookup_stats(&dp->table, key, skb_get_hash(skb), &n_mask_hit)
if(unlikely(!flow)) {
...
upcall.cmd = OVS_PACKET_CMD_MISS;
upcall.portid = ovs_vport_find_upcall_portid(p, skb);
|
\-> 在vport下的upcall_portids里钦定一个port号,作为NetLink的目的地
...
error = ovs_dp_upcall(dp, skb, key, &upcall, 0);
}
即是从upcall的vport下挂的socksp中选择一个nl_pid,以这个nl_pid做为upcall消息发送的目的地址
由上面的图能看出来,在vport下选择不同的socksp中的nl_pid,会导致消息被发往不同的epoll
4:dpif_handler[]->epoll_fd与用户态线程间的关联
那么在udpif_upcall_handler() -> recv_upcalls() -> dpif_recv() -> dpif_netlink_recv() -> dpif_netlink_recv__()中有以下代码:
这里的参数handler_id来自于线程routine函数的入参handler实例下的handler_id。
即对于x号线程,它会取dpif_handler[]数组中的x号元素下的epoll_fd去处理。
body,td { font-family: Consolas; font-size: 10pt }