本文作者 / yogazhao

爱自然科学,赞叹于大师级码农高超的艺术境界;爱生命科学,诚服于古圣先贤的天地气象。

可能大多数瓜农都对《艺伎回忆录》比较熟悉,作为一个IT界的码农,当然也有自己类似的经历,该文分为上篇和下篇,此篇为《OVS BUG撸码回忆录•上篇》

回忆录缘起

以前为排查ovs的某个bug,无奈撸了把相关内核流程。当时因为调用链太多,脑袋栈溢出,处理不过来,所以临时用txt比较零散的记录了下关键点,做完了就丢了。后面想起来,无奈用everything找了好久才找到, 再读之,发现忘了很多,临时整理这零散的笔记,也算给自己加深下意识,因为是附带研究,如有错误,恳请指正。

<内核版本>

3.10

回忆录之收包流程

网卡种类较多,当时取了e1000这种比较通用的网卡为例。下述以精简代码的形式展示核心流程。

网卡收到报文->e1000_intr->__napi_schedule(&adapter->napi)

{

local_irq_save(flags); // 关中断

____napi_schedule(&__get_cpu_var(softnet_data), n);

local_irq_restore(flags); // 开中断

preempt_check_resched_rt();

}

____napi_schedule --> raise NET_RX_SOFTIRQ中断触发软中断NET_RX_SOFTIRQ处理函数net_rx_action被调用

net_rx_action

{

n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);

...

work = n->poll(n, weight); // e1000e_poll

}

e1000e_poll->e1000_receive_skb->napi_gro_receive->netif_receive_skb->__netif_receive_skb->__netif_receive_skb_core

{

skb_reset_network_header(skb);//把L3、L4的头都指向data数据结构,到这里的时候skb已经处理完L2层的头了

...

another_round: // vlan报文处理,循环剥掉vlan头,比如vlan over vlan 两个vlan都会剥掉

...

if (skb->protocol == cpu_to_be16(ETH_P_8021Q) ||

skb->protocol == cpu_to_be16(ETH_P_8021AD)) {

skb = vlan_untag(skb); //洞房花烛夜,赤诚相对

if (unlikely(!skb))

goto out;

}

list_for_each_entry_rcu(ptype, &ptype_all, list) {  //把包交给特定协议相关的处理函数前,先调用ptype_all中注册的函数

if (!ptype->dev || ptype->dev == skb->dev) {//最常见的为tcpdump,该工具就是从这里拿到所有收到的包的,所以tcpdump抓包在投递到协议栈处理前。

tcpdump这种经常使用的,激起了我的肾上腺素,顺便追根溯源看一把究竟

对于ptype_all

对于tcpdump,应用层调用

socket(PF_PACKET,SOCK_RAW,768)=3

对应内核,创建socket最终会调用

packet_create

{

po->prot_hook.func = packet_rcv;

...

if (proto) {

po->prot_hook.type = proto;

register_prot_hook(sk)

{

dev_add_pack(&po->prot_hook);

}

}

}

static inline struct list_head *ptype_head(const struct packet_type *pt)

{

if (pt->type == htons(ETH_P_ALL)) // Protocol类型为ETH_P_ALL(ntohs后为768), 即所有包,则放入ptype_all链表中

return &ptype_all;

else

return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];//Protocol类型为其他值,则放入ptype_base hash表中

}

void dev_add_pack(struct packet_type *pt)

{

struct list_head *head = ptype_head(pt);

spin_lock(&ptype_lock);

list_add_rcu(&pt->list, head);

spin_unlock(&ptype_lock);

}

Protocol类型为ETH_P_ALL(ntohs后为768), 即所有包,则放入ptype_all链表中

static struct packet_type arp_packet_type __read_mostly = {

.type = cpu_to_be16(ETH_P_ARP),

.func = arp_rcv,

};

static int arp_proc_init(void);

void __init arp_init(void)

{

neigh_table_init(&arp_tbl);

dev_add_pack(&arp_packet_type); // 就是添加到ptype_all

arp_proc_init();

ifdef CONFIG_SYSCTL

neigh_sysctl_register(NULL, &arp_tbl.parms, "ipv4", NULL);

endif

register_netdevice_notifier(&arp_netdev_notifier);

}

static struct packet_type ip_packet_type __read_mostly = {

.type = cpu_to_be16(ETH_P_IP),

.func = ip_rcv,

};

static int __init inet_init(void)

{

...

dev_add_pack(&ip_packet_type);

...

}

if (pt_prev)

ret = deliver_skb(skb, pt_prev, orig_dev);

pt_prev = ptype;          //pt_prev的加入是为了优化,只有当找到下一个匹配的时候,才执行这一次的回调函数

}

}

// rx_handler是重中之重,关注、关注、关注。linux内核为了高性能,很喜欢用rwlock的改进版rcu,这种思想是值得借鉴的。

rx_handler = rcu_dereference(skb->dev->rx_handler);

if (rx_handler){

...

switch (rx_handler(&skb)) // 交给rx_handler处理,例如ovs, linux bridge等 此类接口处理报文在协议栈之前,因此netfilter对此类接口不起作用,所以在云环境(openstack)中,需要在虚拟机tap口与虚拟交换机之间增加Linux bridge设备来使报文经过协议栈(netfilter起作用)来实现security group。

肾上腺素爆棚,rx_handler必须要一探究竟。

  1. 看看【ovs】的rx_handler

int netdev_rx_handler_register(struct net_device *dev,

rx_handler_func_t *rx_handler,

void *rx_handler_data)

{

ASSERT_RTNL();

if (dev->rx_handler)

return -EBUSY;

/* Note: rx_handler_data must be set before rx_handler */

rcu_assign_pointer(dev->rx_handler_data, rx_handler_data);

rcu_assign_pointer(dev->rx_handler, rx_handler);

return 0;

}

./net/openvswitch/vport-netdev.c:104:  err = netdev_rx_handler_register(netdev_vport->dev, netdev_frame_hook,

static struct vport *netdev_create(const struct vport_parms *parms)

{

...

err = netdev_rx_handler_register(netdev_vport->dev, netdev_frame_hook,

vport);

...

}

总结之就是 rx_handler=netdev_frame_hook

  1. 看看【bridge】的rx_handler

./net/bridge/br_if.c:375:err= netdev_rx_handler_register(dev, br_handle_frame, p);

也即 rx_handler=br_handle_frame

}

/*type,二层封装内的协议,IP为 0x0800*/

type = skb->protocol;

/*获取协议注册的入口函数,ip为 ip_rcv,声明的变量为 ip_packet_type*/

list_for_each_entry_rcu(ptype,

&ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {

if (ptype->type == type &&

(ptype->dev == null_or_dev || ptype->dev == skb->dev ||

ptype->dev == orig_dev)) {

if (pt_prev)

ret = deliver_skb(skb, pt_prev, orig_dev); // 根据具体协议投递包(如 arp、ip)

pt_prev = ptype; //很喜欢这种写法

}

}

}->deliver_skb{

pt_prev->func // 如ip,则对应的就是ip_rcv ---- 进入第三层

}

ip_rcv

{

return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,

ip_rcv_finish);

}->ip_rcv_finish->dst_input->ip_local_deliver

{

/*

*     Reassemble IP fragments. //重组分片

*/

if (ip_is_fragment(ip_hdr(skb))) {

if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))

return 0;

}

return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,

ip_local_deliver_finish);

}->ip_local_deliver_finish{

ipprot->handler(skb)//对应的就是udp_rcv, 进入第四层

}

udp_rcv->__udp4_lib_rcv->udp_queue_rcv_skb->encap_rcv/vxlan_udp_encap_recv

vxlan_socket_create

{

udp_sk(sk)->encap_type = 1;

udp_sk(sk)->encap_rcv = vxlan_udp_encap_recv;

}

盘丝洞翻遍终于让我找到了你: vxlan_udp_encap_recv

vxlan_udp_encap_recv

{

/* pop off outer UDP header */ -- 去udp头

__skb_pull(skb, sizeof(struct udphdr));

不是vxlan丢弃,不符合条件的vni丢弃

...

__skb_pull(skb, sizeof(struct vxlanhdr));//剥掉vxlan头

// 没有l2头则丢弃(这里的l2头指的是original l2 frame的l2头)

if (!pskb_may_pull(skb, ETH_HLEN)) {

vxlan->dev->stats.rx_length_errors++;

vxlan->dev->stats.rx_errors++;

goto drop;

}

/*重置skb mac数据,因为解包了(既然真爱,那就真面目相见)*/

skb_reset_mac_header(skb);

/*学习报文记录到本地 vxlan fdb表*/

if ((vxlan->flags & VXLAN_F_LEARN) &&

vxlan_snoop(skb->dev, oip->saddr, eth_hdr(skb)->h_source))

goto drop;

/*更改skb收包接口*/

__skb_tunnel_rx(skb, vxlan->dev);

...

/*skb处理完成,进入主要函数*/

netif_rx(skb);

}

这里有点绕:前面有讲在包递交到协议栈之前,ovs有截获。而这里netif_rx又是走的本机协议栈处理,到底走哪个?

[root@node01 ~]# ovs-dpctl show

system@ovs-system:

lookups: hit:79272720733 missed:2990364171 lost:830495930

flows: 70550

masks: hit:623377534012 total:7 hit/pkt:7.58

port 0: ovs-system (internal)

port 1: qvoab699415-b9

port 2: qvof45c0031-aa

port 112: qvo318ac212-4a

port 113: br-int (internal)

port 114: qvo860fecf7-cf

port 115: qvod7e180b2-db

port 333: qvo59a425ee-ae

port 334: vxlan_sys_4789 (vxlan: df_default=false, ttl=0)

port 335: br-tun (internal)

ovs的port有好几种类型,br-tun为internal类型, internal接口报文一般会从system类型接口传入,从system接口收包处理过程继续,internal类型接口定义的send函数为’internal_dev_recv’。static int internal_dev_recv(struct vport *vport, struct sk_buff *skb) { struct net_device *netdev = netdev_vport_priv(vport)->dev; int len;

len = skb->len;

skb_dst_drop(skb);
nf_reset(skb);
secpath_reset(skb);

skb->dev = netdev;
skb->pkt_type = PACKET_HOST;
skb->protocol = eth_type_trans(skb, netdev);

netif_rx(skb);

return len;

}

const struct vport_ops ovs_internal_vport_ops = {

.type             = OVS_VPORT_TYPE_INTERNAL,

.create          = internal_dev_create,

.destroy = internal_dev_destroy,

.get_name    = ovs_netdev_get_name,

.send            = internal_dev_recv,

};

internal类型的接口没有设置'rx_handler', 因此也就是没有走截获,而直接走的正常网络栈流程,最终跑到vxlan_udp_encap_recv

如果是vport接口,其注册了rx_handler,故会走到netdev_frame_hook

至此上篇完结。

下篇预告: 回忆OVS内部内核态与用户态的流程及发包处理流程。

猜你还想看这些内容

 分分钟get腾讯云TStack技术汇总!

 小甲陪你一起看Ceph (OSDC | 上篇)

● 这次,千辛万苦搭好的虚拟机终于不用重头来过了!

● 玩转K8S AdmissionWebhook

· END ·


点它!

文章来源于腾讯云开发者社区,点击查看原文