OVS 源码分析整理


OVS 核心代码

  • datapath 目录
  • ovs-switchd
  • OVS数据库管理
  • ofproto

OVS 架构


OVS 主要的数据结构


数据结构关系图

主要的数据结构和数据结构的参数

数据结构代码

vvport

/**
- struct vport - one port within a datapath
- @rcu: RCU callback head for deferred destruction.
- @dp: Datapath to which this port belongs.
- @upcall_portids: RCU protected 'struct vport_portids'.
- @port_no: Index into @dp's @ports array.
- @hash_node: Element in @dev_table hash table in vport.c.
- @dp_hash_node: Element in @datapath->ports hash table in datapath.c.
- @ops: Class structure.
- @percpu_stats: Points to per-CPU statistics used and maintained by vport
- @err_stats: Points to error statistics used and maintained by vport
 */
struct vport {  
    struct rcu_head rcu; // 一种锁机制  
    struct datapath *dp; // 网桥结构体指针,表示该端口是属于哪个网桥的  
    u32 upcall_portid; // Netlink端口收到的数据包时使用的端口id  
    u16 port_no; // 端口号,唯一标识该端口  
  
// 因为一个网桥上有多个端口,而这些端口都是用哈希链表来存储的,  
// 所以这是链表元素(里面没有数据,只有next和prev前驱后继指针,数据部分就是vport结构体中的其他成员)  
    struct hlist_node hash_node;   
    struct hlist_node dp_hash_node; // 这是网桥的哈希链表元素  
    const struct vport_ops *ops; // 这是端口结构体的操作函数指针结构体,结构体里面存放了很多操作函数的函数指针  

    struct pcpu_tstats __percpu *percpu_stats;// vport指向每个cpu的统计数据使用和维护  
  
    spinlock_t stats_lock; // 自旋锁,防止异步操作,保护下面的两个成员  
    struct vport_err_stats err_stats; // 错误状态(错误标识)指出错误vport使用和维护的统计数字   
    struct ovs_vport_stats offset_stats; // 添加到实际统计数据,部分原因是为了兼容  
};  

vport_parms

/**
- struct vport_parms - parameters for creating a new vport
 *
- @name: New vport's name.
- @type: New vport's type.
- @options: %OVS_VPORT_ATTR_OPTIONS attribute from Netlink message, %NULL if
- none was supplied.
- @dp: New vport's datapath.
- @port_no: New vport's port number.
 */
struct vport_parms {  
    const char *name; // 新端口的名字  
    enum ovs_vport_type type; // 新端口的类型(端口不仅仅只有一种类型,后面会分析到)   
    struct nlattr *options; // 这个没怎么用到过,好像是从Netlink消息中得到的OVS_VPORT_ATTR_OPTIONS属性  
  
    /* For ovs_vport_alloc(). */  
    struct datapath *dp; // 新的端口属于哪个网桥的  
    u16 port_no; // 新端口的端口号  
    u32 upcall_portid; // 和Netlink通信时使用的端口id  
};  

vport_ops

/**
- struct vport_ops - definition of a type of virtual port
 *
- @type: %OVS_VPORT_TYPE_* value for this type of virtual port.
- @create: Create a new vport configured as specified.  On success returns
- a new vport allocated with ovs_vport_alloc(), otherwise an ERR_PTR() value.
- @destroy: Destroys a vport.  Must call vport_free() on the vport but not
- before an RCU grace period has elapsed.
- @set_options: Modify the configuration of an existing vport.  May be %NULL
- if modification is not supported.
- @get_options: Appends vport-specific attributes for the configuration of an
- existing vport to a &struct sk_buff.  May be %NULL for a vport that does not
- have any configuration.
- @get_name: Get the device's name.
- @send: Send a packet on the device.  Returns the length of the packet sent,
- zero for dropped packets or negative for error.
- @get_egress_tun_info: Get the egress tunnel 5-tuple and other info for
- a packet.
 */

struct vport_ops {  
    enum ovs_vport_type type; // 端口的类型  

    // 新vport端口的创建函数和销毁端口的函数  
    struct vport *(*create)(const struct vport_parms *); // 根据指定的参数配置创建个新的vport,成功返回新端口指针  
    void (*destroy)(struct vport *); // 销毁端口函数  
  
    // 得到和设置option成员函数  
    int (*set_options)(struct vport *, struct nlattr *);  
    int (*get_options)(const struct vport *, struct sk_buff *);  
  
    // 得到端口名称和配置以及发送数据包函数     
    const char *(*get_name)(const struct vport *); //
    int (*send)(struct vport *, struct sk_buff *); // 发送数据包到设备上  
};  

vport_ops_list

/* List of statically compiled vport implementations.  Don't forget to also
- add yours to the list at the bottom of vport.h.
 */
static const struct vport_ops *vport_ops_list[] = {  
    &ovs_netdev_vport_ops,
    &ovs_internal_vport_ops,
    &ovs_geneve_vport_ops,
#if IS_ENABLED(CONFIG_NET_IPGRE_DEMUX)
    &ovs_gre_vport_ops,
    &ovs_gre64_vport_ops,
#endif
    &ovs_vxlan_vport_ops,
    &ovs_lisp_vport_ops,
};

datapath

struct datapath {  
    struct rcu_head rcu; // RCU调延迟破坏。  
    struct list_head list_node; // 网桥哈希链表元素,里面只有next和prev前驱后继指针,数据时该结构体其他成员  
  
    /* Flow table. */  
    struct flow_table __rcu *table;// 这是哈希流表,里面包含了哈希桶的地址指针。该哈希表受_rcu机制保护  
  
    /* Switch ports. */  
    struct hlist_head *ports;// 一个网桥有多个端口,这些端口都是用哈希链表来链接的  
  
    /* Stats. */  
    struct dp_stats_percpu __percpu *stats_percpu;  
  
#ifdef CONFIG_NET_NS  
    /* Network namespace ref. */  
    struct net *net;  
#endif  
};  

sw_flow_key

struct sw_flow_key {  
       // 这是隧道相关的变量  
    struct ovs_key_ipv4_tunnel tun_key;  /* Encapsulating tunnel key. */  
    struct {  
       // 包的优先级  
        u32 priority; // 包的优先级  
        u32 skb_mark; // 包的mark值  
        u16 in_port; // 包进入的端口号  
    } phy; // 这是包的物理层信息结构体提取到的  
    struct {  
        u8     src[ETH_ALEN]; // 源mac地址  
        u8     dst[ETH_ALEN]; // 目的mac地址  
        __be16 tci; // 这好像是局域网组号  
        __be16 type; // 包的类型,即:是IP包还是ARP包  
    } eth; // 这是包的二层帧头信息结构体提取到的   
    struct {  
        u8     proto; // 协议类型 TCP:6;UDP:17;ARP类型用低8位表示  
        u8     tos; // 服务类型  
        u8     ttl; // 生存时间,经过多少跳路由  
        u8     frag; // 一种OVS中特有的OVS_FRAG_TYPE_*.   
    } ip; // 这是包的三层IP头信息结构体提取到的  
       // 下面是共用体,有IPV4和IPV6两个结构,为了后期使用IPV6适应  
    union {  
        struct {  
            struct {  
                __be32 src; // 源IP地址  
                __be32 dst; // 目标IP地址  
            } addr; // IP中地址信息  
                        // 这又是个共用体,有ARP包和TCP包(包含UDP)两种  
            union {  
                struct {  
                    __be16 src; // 源端口,应用层发送数据的端口  
                    __be16 dst; // 目的端口,也是指应用层传输数据端口  
                } tp; // TCP(包含UDP)地址提取  
                struct {  
                    u8 sha[ETH_ALEN]; // ARP头中源Mac地址  
                    u8 tha[ETH_ALEN]; // ARP头中目的Mac地址  
                } arp;ARP头结构地址提取  
            };  
        } ipv4;  
               // 下面是IPV6的相关信息,基本和IPV4类似,这里不讲  
        struct {  
            struct {  
                struct in6_addr src;    /* IPv6 source address. */  
                struct in6_addr dst;    /* IPv6 destination address. */  
            } addr;  
            __be32 label;           /* IPv6 flow label. */  
            struct {  
                __be16 src;     /* TCP/UDP source port. */  
                __be16 dst;     /* TCP/UDP destination port. */  
            } tp;  
            struct {  
                struct in6_addr target; /* ND target address. */  
                u8 sll[ETH_ALEN];   /* ND source link layer address. */  
                u8 tll[ETH_ALEN];   /* ND target link layer address. */  
            } nd;  
        } ipv6;  
    };  
};  

flow_table

struct flow_table {  
    struct flex_array *buckets; //哈希桶地址指针  
    unsigned int count, n_buckets; // 哈希桶个数  
    struct rcu_head rcu; // rcu包含机制  
    struct list_head *mask_list; // struct sw_flow_mask链表头指针  
    int node_ver;  
    u32 hash_seed; //哈希算法需要的种子,后期匹配时要用到  
    bool keep_flows; //是否保留流表项  
};  

};

sw_flow

struct sw_flow {  
    struct rcu_head rcu; // rcu保护机制  
    struct hlist_node hash_node[2]; // 两个节点指针,用来链接作用,前驱后继指针  
    u32 hash; // hash值  
  
    struct sw_flow_key key; // 流表中的key值  
    struct sw_flow_key unmasked_key; // 也是流表中的key  
    struct sw_flow_mask *mask; // 要匹配的mask结构体  
    struct sw_flow_actions __rcu *sf_acts; // 相应的action动作  
  
    spinlock_t lock; // 保护机制自旋锁  
    unsigned long used; // 最后使用的时间  
    u64 packet_count; // 匹配过的数据包数量  
    u64 byte_count; // 匹配字节长度  
    u8 tcp_flags; // TCP标识  
};    

sw_flow_mask

struct sw_flow_mask {  
        int ref_count;  
        struct rcu_head rcu;  
        struct list_head list;// mask链表元素,因为mask结构是个双链表结构体  
        struct sw_flow_key_range range;// 操作范围结构体,因为key值中有些数据时不要用来匹配的  
        struct sw_flow_key key;// 要和数据包操作的key,将要被用来匹配的key值  
};  

datapath 模块


datapath 简介

datapath为 ovs内核模块,负责执行数据交换,也就是把从接收端口收到的数据包在流表中进行匹配,并执行匹配到的动作。

一个datapath可以对应多个vport,一个vport类似物理交换机的端口概念。一个datapth关联一个flow table,一个flow table包含多个条目,每个条目包括两个内容:一个match/key和一个action


datapath 代码

  • dp_init()
  • ovs_dp_process_received_packet()

dp_init 代码

static int __init dp_init(void)
{
    int err;

    BUILD_BUG_ON(sizeof(struct ovs_skb_cb) > FIELD_SIZEOF(struct sk_buff, cb));

    pr_info("Open vSwitch switching datapath %s, built "__DATE__" "__TIME__"\n",
        VERSION);
    err = ovs_flow_init();//申请 flow_cache和 flow_stats_cache
    if (err)
        goto error;

    err = ovs_vport_init();//vport 数据结构初始化,申请 dev_table
    if (err)
        goto error_flow_exit;

    err = register_pernet_device(&ovs_net_ops);//注册网络名字空间设备
    if (err)
        goto error_vport_exit;

    err = register_netdevice_notifier(&ovs_dp_device_notifier);//注册设备通知事件
    if (err)
        goto error_netns_exit;

    err = dp_register_genl();//dp_register_genl 初始化 dp 相关的 netlink 的 family和ops
    if (err < 0)
        goto error_unreg_notifier;

    return 0;

error_unreg_notifier:  
    unregister_netdevice_notifier(&ovs_dp_device_notifier);
error_netns_exit:  
    unregister_pernet_device(&ovs_net_ops);
error_vport_exit:  
    ovs_vport_exit();
error_flow_exit:  
    ovs_flow_exit();
error:  
    return err;
}

vswitchd 模块


vswitchd 代码

set_program_name(argv)

设置程序名称、版本、编译日期等信息

proctitle_int(argh,argv)

复制出输入的参数列表到新的存储中,让argv指向这块内存【主要是为了后面的proctitle_set()函数准备】

service_start(&argc,&argv)

注册回调和服务管理器出现故障错误时操作的配置

remote=parse_options(argh,argv,&unixctl_path)

解析参数,其中unixctl_path存储unixctrl域的sock名,作为接受外部控制命令的渠道;而remote存储连接到ovsdb的信息,即连接到配置数据库的sock名

ovsrec_init()

数据表结构初始化,包括13张数据表

daemonize_start()

如果系统守护进程被配置了,启动系统守护进程,通过派生和在返回的子进程。父进程徘徊,直到
让子进程知道它完成启动成功(通过调用daemon_complete()),或者它没有启动(用非零退出
退出代码。

unixctl_server_create(unixctl_path,&unixctl)

创建一个unixctl_server(存放在unixctl),并监听在unixctl_path指定的punix路径,该路径作为ovs-appctl发送命令给ovsd的通道

unixctl_command_register

注册unixctl命令

bridge_init

从remote数据库获取配置信息,并初始化bridge

主循环
memory_run()

运行内存监视器,客户端调用memory_should_report()。此函数以及该模块的接口的剩余部分,仅被一个线程调用。

bridge_run

主要对网包进行完整处理过程。包括完成必要的配置更新【在配置更新中会从数据库中读取配置信息,生成必要的bridge和dp等数据结构】

ovsdb_idl_run(idl);

处理了一批从'IDL'数据库服务器的消息。这可能会导致IDL的内容发生变化。客户端可以检查与ovsdb_idl_get_seqno()。

system_stats_enable(false);

因为我们不运行system_stats的run()在这个进程中有多个OVS-vswitchd守护进程的现状,关闭系统自动统计信息收集。

bridge_init_ofproto(cfg)

初始化ofproto库。这仅需要执行一次,但配置设置之后它必须要做的。如果已经出现了初始化,bridge_init_ofproto()立即返回。

bridge_run__(void)

#######ofproto_run中的p->ofproto_class->run(p)上的run函数依次调用函数

  • 必选调用dpif_run()处理所有注册的netlink notifier的汇报事件
  • 必选调用run_fast()处理常见的周期事件,包括对upcalls的处理等
  • 可选调用netflow_run()和sflow_run(),进行对netflow和sflow的支持

可选调用较多,自行查看

#######connmr_run函数处理与控制器的周期性交互

  • 首先检查是否存在in_band的控制器
  • 调用ofconn_run()处理对ofproto的协议解析和行动
  • rconn_run(ofconn->rconn)负责连接到controller
  • rconn_recv(ofconn->rconn)负责从controller收取消息
  • handle_openflow()最终调用handle_openflow__()(ofproto/ofproto.c)来完成对各个Of消息的处理

以 PACKET_OUT消息为例,调用的是handle_packout 函数

首先调用ofputil_decode_packet_out()对of消息进行解析
调用ofconn_pktbuf_retrieve()获取payload信息
利用ofproto_class->packet_out()将网包发出
packet_out()
{   
    ofproto_dpif_execute_actions()
        {
        dpif_flow_stats_extract() 流状态提取
        xlate_actions()将ofpacts转化为dp的行动格式odp_actions
        调用dpif_execute()函数让dpif执行给定的action构建OVS_PACKET_CMD_EXECUTE netlink消息并发给datapath
        datapath中将对应调用ovs_packet_cmd_execute函数处理收到的nlmsg
        ovs_packet_cmd_execute的调用过程
        ovs_packet_cmd_execute()->ovs_execute_actions()->do_execute_actions()
        }
}
重新配置SSL

通过主循环每一次遍历,而不是只当数据库的变化,因为密钥和证书文件的内容可以更改在数据库不更改中。我们完成这些在bridge_reconfigure()之前,因为该功能可能会启动SSL连接之前做到这一点,因此需要SSL进行配置。

对all_bidge上的每个bridge的ofproto执行ofproto_run()
netdev_run()

如果打开了一些netted,则执行对应在netdev_classes上定义的每个netdev_class实体,调用它们的run()包括处理网卡注册的各个通知事件,获取网卡的最新的信息等

unixctl_server_run(unixctl)

从unixctl指定的server中获取来自ovs-appctl发出的命令数据,并执行对应的命令

循环等待事件处理

包括memory、bridge、unixctl_server、netted等事件,被poll_fd_wait()注册的最短时间

poll_block(void)

阻塞知道之前被poll_fd_wait()注册过的事件发生,或者等待时间超过poll_timer_wait()注册的最短时间

清理工作

退出bridge,关闭unixctl连接

动态过程分析


数据流流向


一般的数据包在 Linux网络协议中的流向为黑色箭头流向:网卡收到数据包后层层网上分析,最后离开内核态,把数据传送到用户态。
有 OVS时:数据流流向不同
(1)创网桥(ovs-vsctl add-br br0)
(2)绑网卡(ovs-vsctl add-port bro eth0 默认为 eth0)
数据流:
从网卡 eth0到 ovs 的 vport 进入OVS,根据 key值流表匹配
成功——>执行流表 action
失败——>upcall处理

添加网桥


1. 键入命令ovs-vsctl add-br testBR
2. 内核中的 openvswitch.ko 收到一个添加网桥的命令时候——即收到 OVS_DATAPATH_FAMILY通道的 OVS_DP_CMD_NEW命令。该命令绑定的回调函数为 ovs_dp_cmd_new
3. ovs_dp_cmd_new 函数除了初始化 dp 结构外,调用 new_vport 函数来生成新的 vport
4. new_vport 函数调用 ovs_vport_add()来尝试生成一个新的 vport
5. ovs_vport_add()函数会检查 vport 类型(通过 vport_ops_list[]数组),并调用相关的 create()函数来生成 vport 结构
6. 当dp是网络设备时(vport_netdev.c),最终由 ovs_vport_add()函数调用的是 netdev_create()【在 vport_ops_list的ovs_netdev_ops 中】
7. netdev_create()函数最关键的一步是注册了收到网包时的回调函数
8. err=netdev_rx_handler_register(netdev_vport->dev,netdev_frame_hook,vport);
9. 操作是将 netdev_vport->dev 收到网包时的相关数据由 netdev_frame_hook()函数来处理,都是些辅助处理,依次调用各处理函数,在 netdev_port_receive()【这里会进行数据包的拷贝,避免损坏】进入 ovs_vport_receive()回到 vport.c,从 ovs_dp_process_receive_packet()回到 datapath.c,进行统一处理
10. 流程:netdev_frame_hook()->netdev_port_receive->ovs_vport_receive->ovs_dp_process_received_packet()
11. net_port_receive()首先检测是否 skb 被共享,若是则得到 packet 的拷贝。
12. net_port_receive()其调用ovs_vport_receive(),检查包的校验和,然后交付给我们的vport通用层来处理。

netdev_rx_handler_register()
linux 内核实现的一个函数,为网络设备 dev 注册一个handler_frame_hook,rx_handle_data 指向的是handler_frame_hook 内存的区域,这个 handler 以后会被__netif_receive_skb()呼叫,就是说netdev_rx_handler_register(netdev_vport->dev,netdev_frame_hook,vport);在收到packet 后会调用 netdev_frame_hook 函数处理

收包处理


1.ovs_vport_receive_packets()调用ovs_flow_extract基于skb生成key值,并检查是否有错,然后调用ovs_dp_process_packet。交付给datapath处理
2.ovs_flow_tbl_lookup_stats。基于前面生成的key值进行流表查找,返回匹配的流表项,结构为sw_flow。 
3.若不存在匹配,则调用ovs_dp_upcall上传至userspace进行匹配。 (包括包和key都要上传) 
若存在匹配,则直接调用ovs_execute_actions执行对应的action,比如添加vlan头,转发到某个port等。

流表匹配


1. flow_lookup()查找对应的流表项
2. for 循环调用 rcu_dereference_ovs 对流表结构体中的 mask_list 成员遍历,找到对应的的 成员
3. flow=masked_flow_lookup()遍历进行下一级 hmap查找,找到为止
4. 进入 包含函数 ovs_flow_mask_key(&masked_key,unmasked,mask),将最开始提取的 Key 值和 mask 的 key 值进行“与”操作,结果存放在 masked_key 中,用来得到后面的 Hash 值
5. hash=flow_hash(&masked_key,key_start,key_end)key 值的匹配字段只有部分
6. ovs_vport_add()函数会检查 vport 类型(通过 vport_ops_list[]数组),并调用相关的 create()函数来生成 vport 结构
7. 可见,当 dp 时网络设备时(vport_netdev.c),最终由 ovs_vport_add()函数调用的是 netdev_create()【在 vport_ops_list的ovs_netdev_ops 中】
8. netdev_vport->dev 收到网包时的相关数据由 netdev_frame_hook()函数来处理,都是些辅助处理,依次调用各处理函数,在 netdev_port_receive()【这里会进行数据包的拷贝,避免损坏】进入 ovs_vport_receive()回到 vport.c,从 ovs_dp_process_receive_packet()回到 datapath.c,进行统一处理

upcall 消息处理


1. ovs_dp_upcall()首先调用 err=queue_userspace_packet()将信息排队发到用户空间去
2. dp_ifindex=get_dpifindex(dp)获取网卡设备索引号
3. 调整 VLAN的 MAC 地址头指针
4. 网络链路属性,如果不需要填充则调用此函数
5. len=upcall_msg_size(),获得 upcall 发送消息的大小
6. user_skb=genlmsg_new_unicast,创建一个新的 netlink 消息
7. upcall=genlmsg_put()增加一个新的 netlink 消息到 skb
8. err=genlmsg_unicast(),发送消息到用户空间去处理

相关内容


Linux RCU锁机制分析


RCU是linux的新型锁机制(RCU是在linux 2.6内核版本中开始正式使用)

传统读写锁rwlock运行机制

读锁(共享锁):若请求是读数据时,上读锁,多个读锁不排斥(即访问数据的读者上限未达到时,可以对数据区再上读锁),若请求是写数据时,不能马上上写锁,得等数据区的所有锁(包括读锁和写锁)都释放才能上写锁

写锁(独占锁):要操作的数据区上了写锁,不管什么请求都要等到数据区的写锁释放掉后才能上锁访问

RCU 锁机制——RCU(read\copy\update)对数据的读、复制、修改的保护锁机制

写数据:(1)不需读写锁那样等待所有锁释放【拷贝一份数据区的副本,在副本中修改,修改完后,用副本替代原来的数据区】(2)替换的时候需要读写锁上写锁那样,等到数据区上所有访问者退出后,才进行数据的替换
(3)RCU锁可以有多个写者,拷贝多份数据区数据,修改后,各个数据区陆续替换掉原数据区内容
读数据:不用上任何锁,几乎不需要等待(读写锁需要等写锁释放)就可以直接访问数据
,“几乎”,因为写数据中替换原数据,只需修改个指针,消耗的时间几乎不算

RCU 锁机制特性

• 允许多个读者和多个写者同时访问共享数据区内容
• 对多读少写的数据来说非常高效,可以减少 CPU 开销
• 写数据操作多了,就不如读写锁那么好了,因为RCU 对写数据开销大,需要拷贝数据,修改,等待替换

RCU 机制 API

rcu_read_lock();
• 这不是和上读写锁的那种上锁,这仅仅只是标识了临界区的开始位置。表明在临界区内不能阻塞和休眠,也不能让写者进行数据的替换(其实这功能远不止这些)。rcu _read_unlock()则是和上面rcu_read_lock()对应的,用来界定一个临界区(就是要用锁保护起来的数据区)。
synchronize_rcu();
• 当该函数被一个CPU调用时(一般是有写者替换数据时调用),而其他的CPU都在RCU保护的临界区读数据,那么synchronize_rcu()将会保证阻塞写者,直到所有其它读数据的CPU都退出临界区时,才中止阻塞,让写着开始替换数据。该函数作用就是保证在替换数据前,所有读数据的CPU能够安全的退出临界区。同样,还有个call_rcu()函数功能也是类似的。如果call_rcu()被一个CPU调用,而其他的CPU都在RCU保护的临界区内读数据,相应的RCU回调的调用将被推迟到其他读临界区数据的CPU全部安全退出后才执行(可以看linux内核源文件的注释,在Rcupdate.h文件中rcu_read_look()函数前面的注释)。
rcu_dereference();
• 获取在一个RCU保护的指针,指向RCU读端临界区。他的指针以后可能会被安全地解除引用。说到底就是一个RCU保护指针。
list_add_rcu();
• 往RCU保护的数据结构中添加一个数据节点进去。这个和一般的往链表中增加一个节点操作是类似的,唯一不同的是多了这条代码:rcu_assign_pointer(prev->next, new); 代码大概含义:分配指向一个新初始化的结构指针,将由RCU读端临界区被解除引用,返回指定的值。
list_for_each_entry_rcu();
• 这是个遍历RCU链表的操作,和一般的链表遍历差不多。不同点就是必须要进入RCU保护的CPU(即:调用了rcu_read_lock()函数的CPU)才能调用这个操作,可以和其他CPU共同遍历这个RCU链表。

Generic Netlink 通信机制


    +---------------------+      +---------------------+
      | (3) application "A" |      | (3) application "B" |
      +------+--------------+      +--------------+------+
             |                                    |
             \                                    /
              \                                  /
               |                                |
       +-------+--------------------------------+-------+
       |        :                               :       |   user-space
  =====+        :   (5)  Kernel socket API      :       +================
       |        :                               :       |   kernel-space
       +--------+-------------------------------+-------+
                |                               |
          +-----+-------------------------------+----+
          |        (1)  Netlink subsystem            |
          +---------------------+--------------------+
                                |
          +---------------------+--------------------+
          |       (2) Generic Netlink bus            |
          +--+--------------------------+-------+----+
             |                          |       |
     +-------+---------+                |       |
     |  (4) Controller |               /         \
     +-----------------+              /           \
                                      |           |
                   +------------------+--+     +--+------------------+
                   | (3) kernel user "X" |     | (3) kernel user "Y" |
                   +---------------------+     +---------------------+

(5)API向用户空间和内核空间分别提供接口。
Netlink子系统(1)是所有genl通信的基础。Netlink子系统中收到的所有Generic类型的netlink数据都被送到genl总线(2)上;从内核发出的数据也经由genl总线送至netlink子系统,再打包送至用户空间。
Generic Netlink控制器(4)作为内核的一部分,负责动态地分配genl通道(即genl family id),并管理genl任务。genl控制器是一个特殊的genl内核用户,它负责监听genl bus上的通信通道。genl通信建立在一系列的通信通道的基础上,每个genl family对应多个通道,这些通道由genl控制器动态分配。

Generic Netlink相关结构体

genl family

Generic Netlink是基于客户端-服务端模型的通信机制。服务端注册family(family是对genl服务的各项定义的集合)。控制器和客户端都通过已注册的信息与服务端通信。
genl family的结构体如下:

struct genl_family
{
      unsigned int            id;
      unsigned int            hdrsize;
      char                    name[GENL_NAMSIZ];
      unsigned int            version;
      unsigned int            maxattr;
      struct nlattr **        attrbuf;
      struct list_head        ops_list;
      struct list_head        family_list;
};
  • id: family id。当新注册一个family的时候,应该用GENL_ID_GENERATE宏(0x0),表示请控制器自动为family分配的一个id。0x10保留供genl控制器使用。
  • hdrsize: 用户自定议头部长度。即图2中User Msg的长度。如果没有用户自定义头部,这个值被赋为0。
  • version: 版本号,一般填1即可。
  • name: family名,要求不同的family使用不同的名字。以便控制器进行正确的查找。
  • maxattr:genl使用netlink标准的attr来传输数据。此字段定义了最大attr类型数。(注意:不是一次传输多少个attr,而是一共有多少种attr,因此,这个值可以被设为0,为0代表不区分所收到的数据的attr type)。在接收数据时,可以根据attr type,获得指定的attr type的数据在整体数据中的位置。
  • struct nlattr **attrbuf
  • struct list_head ops_list
  • struct list_head family_list
    以上的三个字段为私有字段,由系统自动配置,开发者不需要做配置。
genl 报文格式
genl_ops 结构体
struct genl_ops
{
      u8                      cmd;
      unsigned int            flags;
      struct nla_policy       *policy;
      int                     (*doit)(struct sk_buff *skb,
                                      struct genl_info *info);
      int                     (*dumpit)(struct sk_buff *skb,
                                          struct netlink_callback *cb);
      struct list_head        ops_list;
};

cmd: 命令名。用于识别各genl_ops
flag: 各种设置属性,以“或”连接。在需要admin特权级别时,使用GENL_ADMIN_PERM
policy:定义了attr规则。如果此指针非空,genl在触发事件处理程序之前,会使用这个字段来对帧中的attr做校验(见nlmsg_parse函数)。该字段可以为空,表示在触发事件处理程序之前,不做校验。

doit:这是一个回调函数。在generic netlink收到数据时触发,运行在进程上下文。
doit传入两个参数,skb为触发此回调函数的socket buffer。第二个参数是一个genl_info结构体

struct genl_info
        {
             u32                     snd_seq;
             u32                     snd_pid;
             struct nlmsghdr *       nlhdr;
             struct genlmsghdr *     genlhdr;
             void *                  userhdr;
             struct nlattr **        attrs;
        };
  • snd_seq:发送序号
  • snd_pid:发送客户端的PID
  • nlhdr:netlink header的指针
  • genlmsghdr:genl头部的指针(即family头部)
  • userhdr:用户自定义头部指针
  • attrs:attrs,如果定义了genl_ops->policy,这里的attrs是被policy过滤以后的结果。在完成了操作以后,如果执行正确,返回0;否则,返回一个负数。负数的返回值会触发NLMSG_ERROR消息。当genl_ops的flag标志被添加了NLMSG_ERROR时,即使doit返回0,也会触发NLMSG_ERROR消息。

dumpit
这是一个回调函数,当genl_ops的flag标志被添加了NLM_F_DUMP以后,每次收到genl消息即会回触发这个函数。dumpit与doit的区别是:dumpit的第一个参数skb不会携带从客户端发来的数据。相反地,开发者应该在skb中填入需要传给客户端的数据,然后,并skb的数据长度(可以用skb->len)return。skb中携带的数据会被自动送到客户端。只要dumpit的返回值大于0,dumpit函数就会再次被调用,并被要求在skb中填入数据。当服务端没有数据要传给客户端时,dumpit要返回0。如果函数中出错,要求返回一个负值。关于doit和dumpit的触发过程,可以查看源码中的genl_rcv_msg函数。

ops_list
为私有字段,由系统自动配置,开发者不需要做配置。

Generic Netlink 服务端(内核)初始化

初始化Generic Netlink的过程分为以下四步:定义family,定义operation,注册family,注册operation。
Datapath使用 generic netlink

在 dp_init()函数(datapath.c)中,调用 dp_register_genl()完成对四种类型的 family 以 及相应操作的注册,包括 datapath、vport、flow 和 packet。前三种 family,都对应四种操 作都包括 NEW、DEL、GET、SET,而 packet 的操作仅为 EXECUTE。
这些 family 和操作的定义均在 datapath.c 中。 以 flow family 为例。代码为

static const struct nla_policy flow_policy[OVS_FLOW_ATTR_MAX + 1] = { 
    [OVS_FLOW_ATTR_KEY] = { .type = NLA_NESTED }, 
    [OVS_FLOW_ATTR_ACTIONS] = { .type = NLA_NESTED }, 
    [OVS_FLOW_ATTR_CLEAR] = { .type = NLA_FLAG },
};
static struct genl_family dp_flow_genl_family = { 
    .id = GENL_ID_GENERATE,
    .hdrsize = sizeof(struct ovs_header),
    .name = OVS_FLOW_FAMILY,
    .version = OVS_FLOW_VERSION,
    .maxattr = OVS_FLOW_ATTR_MAX, SET_NETNSOK
};

绑定的 ops 的定义

static struct genl_ops dp_flow_genl_ops[] = { 
    { .cmd = OVS_FLOW_CMD_NEW,
      .flags = GENL_ADMIN_PERM, /* Requires CAP_NET_ADMIN privilege. */  
      .policy = flow_policy,
      .doit = ovs_flow_cmd_new_or_set
    },
    { .cmd = OVS_FLOW_CMD_DEL,
      .flags = GENL_ADMIN_PERM, /* Requires CAP_NET_ADMIN privilege. */ 
      .policy = flow_policy,
      .doit = ovs_flow_cmd_del 
    },
    { .cmd = OVS_FLOW_CMD_GET,
      .flags = 0, /* OK for unprivileged users. */ 
      .policy = flow_policy,
      .doit = ovs_flow_cmd_get,
      .dumpit = ovs_flow_cmd_dump
    },
    { .cmd = OVS_FLOW_CMD_SET,
      .flags = GENL_ADMIN_PERM, /* Requires CAP_NET_ADMIN privilege. */ 
      .policy = flow_policy,
      .doit = ovs_flow_cmd_new_or_set,
    }, 
};

ovsd 使用 netlink
ovsd 对于 netlink 的实现,主要在 lib/netlink-socket.c 文件中。而对这些 netlink 操作的 调用,主要在 lib/dpif-linux.c 文件(以 dpif_linux_class 为例)中对于各个行为的处理,各 种可能的消息类型在 datapath 模块中事先进行了内核注册。
datapath 中对 netlink family 类型进行了注册,ovsd 在使用这些 netlink family 之前需要 获取它们的信息,这一过程主要在 lib/dpif-linux.c 文件(以 dpif_linux_class 为例), dpif_linux_init()函数。代码为

static int dpif_linux_init(void) 
{
    static int error = -1;
    if (error < 0) {
        unsigned int ovs_vport_mcgroup;
        error = nl_lookup_genl_family(OVS_DATAPATH_FAMILY,&ovs_datapath_family);
        if (error) {
            VLOG_ERR("Generic Netlink family '%s' does not exist. ""The Open vSwitch kernel module is probably not loaded.",OVS_DATAPATH_FAMILY); }
        if (!error) {
            error = nl_lookup_genl_family(OVS_VPORT_FAMILY, &ovs_vport_family);}
        if (!error) {
            error = nl_lookup_genl_family(OVS_FLOW_FAMILY, &ovs_flow_family); }
        if (!error) {
            error = nl_lookup_genl_family(OVS_PACKET_FAMILY,&ovs_packet_family);}
        if (!error) {
            error = nl_sock_create(NETLINK_GENERIC, &genl_sock); }
        if (!error) {
            error = nl_lookup_genl_mcgroup(OVS_VPORT_FAMILY, OVS_VPORT_MCGROUP,&ovs_vport_mcgroup, OVS_VPORT_MCGROUP_FALLBACK_ID);}
        if (!error) {
            static struct dpif_linux_vport vport;
            nln = nln_create(NETLINK_GENERIC, ovs_vport_mcgroup,
            dpif_linux_nln_parse, &vport);}
     }
    return error; 
}

完成这些查找后,ovsd 即可利用 dpif 中的 api,通过发出这些 netlink 消息给 datapath 实现对 datapath 的操作。

相关的中间层 API 定义主要在 dpif_class(位于 lib/dpif-provider.h)的抽象类型中
dpif_class结构体的注释:

/* Datapath interface class structure, to be defined by each implementation of
-a datapath interface.
 *
- These functions return 0 if successful or a positive errno value on failure,
- except where otherwise noted.
 *
- These functions are expected to execute synchronously, that is, to block as
- necessary to obtain a result.  Thus, they may not return EAGAIN or
- EWOULDBLOCK or EINPROGRESS.  We may relax this requirement in the future if
- and when we encounter performance problems. */

一共有两种dpif_class实例化类型,分别为dpif_netlink_class和dpif_netdev_class。dpif_netlink_class表示的是通过netlink和本地的datapath通信,而dpif_netdev_class通过网络协议和远程的datapath通信
ovsd使用netlink进行消息发送的过程:


参考内容


庾志辉OVS 专栏
http://blog.csdn.net/column/details/openvswitch.html
datapath 模块分析
http://vinllen.com/ovs-datapathbi-ji/
Baohua Yang的OpenvSwitch 代码分析
OpenvSwitch2.4.0源码解读
http://www.cnblogs.com/cotyb/p/5103035.html?utm_source=tuicool&utm_medium=referral
GenerRic Netlink 详解
http://www.tuicool.com/articles/jE7nim

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容