ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Open vSwitch源码阅读:从datapath到OpenFlow的完整路径解析

Open vSwitch源码阅读:从datapath到OpenFlow的完整路径解析 简介Open vSwitch 源码阅读笔记围绕 OVS 2.3.90 版本重点模块的源码实现做系统梳理适合刚接触 OVS 源码的读者作为入门指引。资源为单个 PDF 文档大小约 1012KB便于携带查阅目前已有 975 人学习下载。内容从 OVS 网络架构入手梳理了 ovs-vswitchd、ovsdb-server、ovs-vsctl 等核心组件的职责分工并深入分析了 datapath 内核模块的数据包处理流程、流表哈希桶结构、流表创建过程等关键路径同时涵盖 ofproto 接口层、netdev 设备抽象以及 OpenFlow 协议的应用机制能够帮助读者理解数据包从接收、流表匹配到动作执行的完整流程。这份笔记以图文结合的方式呈现源码调用流程和数据结构适合网络方向学生、SDN 研究者以及需要定制或调试 OVS 的开发者参考使用。1. 初读OVS源码这份2.3.90阅读笔记解决什么做网络虚拟化或者SDN控制面工作的工程师大概率都经历过打开Open vSwitch源码却不知道从哪里下手的阶段。这份《OpenvSwitch源码阅读笔记》基于2.3.90版本把OVS最核心的几条代码路径拆开讲了一遍内核datapath的收包与流表查询、用户态vswitchd的主循环、ofproto接口抽象、OpenFlow协议的match和instruction解析。它不是逐行注释而是沿着“数据包从端口进来、查流表、执行action、没命中就upcall到用户态”这条主线走的。适合刚接触OVS源码、想搞清楚虚拟交换机内部到底怎么工作的初级到中级读者也能帮想深入做二次开发的人快速建立地图省掉大量翻代码的时间。2. 内核datapath收包、流表哈希与upcall机制datapath是OVS的内核模块负责实际的数据包处理。源码阅读顺序建议从这里开始因为用户态代码依赖内核的数据结构和行为约定先理解底层再往上看会比较顺。2.1 数据流向对比先明确OVS介入后的数据路径。没有OVS时网卡eth0收到报文后判断走向本地报文送用户态转发报文按路由或交换逻辑从eth1出去。有OVS时报文从eth0进入后不直接走Linux协议栈而是进入ovs的vport根据提取的key做流表匹配匹配成功执行动作失败则通过upcall送用户态处理。理解这条路径后再看代码就走得通了。收包处理的入口链是vport注册的netdev_frame_hook()往下传递的// datapath/dev.c 起点的收包回调链 static int netdev_frame_hook(struct sk_buff *skb) { skb_push(skb, ETH_HLEN); // 把skb的data指针退回以太网头部起点 return netdev_port_receive(skb); // vport统一入口 } int ovs_vport_receive(struct vport *vport, struct sk_buff *skb) { struct sw_flow_key key; ovs_flow_key_extract(skb, key); // 从skb提取流表key ovs_dp_process_packet(skb, key); // 进入流表匹配与action执行 }skb_push(skb, ETH_HLEN)是为了在进入vport前把报文头部完整暴露给key提取逻辑否则偏移会少掉以太网头ovs_flow_key_extract()生成的是一个定长加变长的复合结构后面流表比较都基于它。这里有个值得留意的点key提取是每个包都要做的所以它性能好不好直接决定OVS的吞吐上限。2.2 流表哈希桶与收包处理流表不是线性数组而是哈希桶加链表。笔记里明确提到了flex_array的分配策略先用alloc_buckets()初始化哈希桶桶内元素空间按页分配超过FLEX_ARRAY_BASE_BYTES_LEFT后再动态扩展。实现层面需要关注这几个数据结构struct table_instance { struct flex_array *buckets; // 哈希桶本体元素动态扩展 unsigned int n_buckets; // 桶数量 }; struct sw_flow { struct hlist_node node; // 挂入哈希桶链表的节点 struct sw_flow_key key; // 完整的key内容 struct sw_flow_mask *mask; // 当前生效的mask决定匹配范围 u32 hash; // 哈希值用于快速比较 };哈希桶初始化时element_size、hlist_head大小、每页元素个数会参与计算。实际阅读中建议你画一张图一页空间里先是若干个hlist_head再跟着元素区元素区不够用就申请新的flex_array part。这个结构对理解后面的masked_flow_lookup()很有帮助因为查找时hash命中只是第一步后续的mask匹配才是开销大头。2.3 流表查询与mask缓存流表查询入口是ovs_flow_tbl_lookup_stats()但它真正花心思的地方在mask的缓存策略。每个mask可能对应大量流表项逐个比较代价很高所以OVS引入了mask_cache_entry把最近命中的mask缓存下来下次直接从缓存找// 流表查询的缓存加速逻辑 static struct sw_flow *ovs_flow_tbl_lookup_stats(...) { // 第一次查percpu的mask_cache_entry cache mask_cache[skb_get_hash(skb) MASK_CACHE_MASK]; if (cache-mask) { flow masked_flow_lookup(ti, key, cache-mask); if (flow) { ... return flow; } } // 缓存未命中走完整mask遍历 flow flow_lookup(ti, mask_array, key, n_mask_hit); // 找到后回填cache供后续包使用 }mask_cache是按CPU核分开的每个核维护自己的缓存条目减少锁竞争。masked_flow_lookup()里比较key时用sw_flow_key_range限定比较区间只比mask为1的部分不是整块key做memcmp这也是性能优化的关键点。如果你在后面做流表性能调优多半会动到这个逻辑比如调整缓存桶数量或者分段比较的粒度。2.4 action处理与upcallaction执行入口是do_execute_actions()根据nla_type()拿到动作类型后分派。笔记里列出的类型值得整理成对照表action类型处理函数行为OVS_ACTION_ATTR_OUTPUTdo_output()发往指定端口OVS_ACTION_ATTR_USERSPACEoutput_userspace()上传用户态OVS_ACTION_ATTR_HASHexecute_hash()更新skb hashOVS_ACTION_ATTR_PUSH_VLANpush_vlan()封装VLAN头OVS_ACTION_ATTR_POP_VLANpop_vlan()剥除VLAN头OVS_ACTION_ATTR_RECIRC添加deferred_action二次进入流水线OVS_ACTION_ATTR_SETexecute_set_action()修改报文/元数据OVS_ACTION_ATTR_SAMPLE概率上报sflow样本RECIRC值得一提它把deferred action放进全局数组实现报文重新进入流表处理是OpenFlow流水线多级表的核心支撑。如果没有多级流表需求这个动作平时用不太到但理解它有助于看懂后面upcall的处理逻辑。当流表没命中时内核走ovs_dp_upcall()发送netlink消息到用户态。消息体三段结构OVS_PACKET_ATTR_KEY携带flow keyOVS_PACKET_ATTR_USERDATA携带用户态下发的元数据OVS_PACKET_ATTR_PACKET携带完整报文。数据部分用skb_zerocopy()拷贝避免大包复制造成的性能损耗。upcall是高并发场景的主要瓶颈观察它可以用这个命令# 查看datapath的统计信息关注miss数 ovs-dpctl show # 查看当前流表locovs 表示用户态流表 ovs-dpctl dump-flowsovs-dpctl show输出的miss计数能直接看出有多少包没命中流表走了upcall路径。如果这个数字持续增长要么流表老化太快要么规则没下发到位这是排查控制器下发问题时第一步要看的数据。3. 用户态vswitchd与ofproto配置如何下发到内核看完内核datapath再看用户态就清楚它要干什么了。vswitchd是OVS的主守护进程它在ovsdb、OpenFlow控制器和内核datapath三者之间做翻译和调度。3.1 vswitchd主循环从main()函数看整体流程是最快的vswitchd的主循环结构如下// vswitchd/ovs-vswitchd.c 主循环骨架 int main(int argc, char *argv[]) { ... daemonize_start(); // 转为守护进程 unixctl_server_create(unixctl_path, unixctl); // 建ovs-appctl控制通道 bridge_init(remote); // 建立到ovsdb-server的JSON-RPC连接初始化网桥模型 for (;;) { bridge_run(); // 核心业务处理网桥增删、端口配置、ofproto运行 unixctl_server_run(unixctl); // 处理ovs-appctl发来的命令 netdev_run(); // 通过netlink获取netdev状态并更新 poll_block(); // 阻塞等待注册的事件或超时 } }bridge_run()内部调用ofproto_run()、connmgr_run()、bridge_run__()等涉及数据库读取和ofproto层交互。用ovs-appctl可以和这个循环实时交互排查状态时常用# 查看vswitchd当前版本与运行状态 ovs-appctl version # 查看网桥端口映射确认netdev是否正常 ovs-appctl dpif/show # 查看当前所有注册的接口类 ovs-appctl dpif-netdev/if-not-foundovs-appctl dpif/show的输出里能直接看到每个端口对应的dpif索引和类型如果某端口状态异常这里会显示不出来或者错误信息。这是定位“配置了端口但流量不通”的第一现场。3.2 ofproto接口抽象与创建流程ofproto层是OVS用户态最核心的抽象。它定义了ofproto_class接口具体实现是ofproto_dpif_class底层再通过dpif_class对接内核datapath。笔记里列出了四个关键对象ofproto一个OpenFlow switch实例、ofport端口、ofrule一条OpenFlow规则、ofgroup行为组合。创建流程的完整链路是这样的// ofproto实例创建的核心调用链 bridge_init_ofproto() - ofproto_init() // 注册ofproto_class放入全局shash - ofproto_create() // 分配ofproto对象 - alloc() // 分配ofproto_dpif包含ofproto - construct() // 回调初始化ofproto_dpif - open_dpif_backer() // 打开内核datapath创建udpif - ofproto_init_tables() // 初始化各级流表 - init_ports() // 遍历端口逐个ofport_open()open_dpif_backer()这一步实际调用dpif_class-open()创建dpif对象同时udpif_create()会拉起处理upcall的线程组。换句话说vswitchd启动时不光是读配置底层内核datapath和用户态收包线程是同步建好的。ofport_install()里会调用netdev_open()打开对应的netdev设备比如system类型对应Linux内核网卡internal类型对应OVS内部虚拟网卡。3.3 udpif多线程upcall处理udpif是用户态处理内核upcall的接口层它用多线程方式来消化内核netlink送来的未命中报文。线程模型初始化入口是udpif_set_threads()void udpif_set_threads(struct udpif *udpif, size_t n_handlers, size_t n_revalidators) { udpif_start_threads(udpif); /* 创建n_handlers个handler线程回调udpif_upcall_handler() */ ovs_thread_create(handler, udpif_upcall_handler, udpif); /* revalidator线程负责验证和删除无用流表项 */ ovs_thread_create(revalidator, udpif_revalidator, udpif); }handler线程数量通常与CPU核数相关核心逻辑是recv_upcalls()// udpif/upcall.c 接收处理主流程 recv_upcalls(udpif) - dpif_recv() // 从内核接收upcall消息 - dpif_netlink_recv__() // epoll_wait阻塞等待nl_sock_recv收包 - parse_odp_packet() // 把netlink数据转成dpif_upcall - process_upcall() // 分类处理MISS、ACTION等 - upcall_xlate() // 慢路径翻译 - xlate_actions() // 生成内核需要的action列表process_upcall()里会区分upcall类型MISS_UPCALL是流表未命中需要走upcall_xlate()生成新的action如果是ACTION_UPCALL说明之前已经下发过action这里是通知用户态做一些统计或sflow采样。理解了这层再去看OVS的性能调优你会发现handler线程数和revalidator线程数的配比直接决定高并发表现这也是为什么很多人调整n-handler-threads配置后吞吐变化明显。4. OpenFlow协议flow_mod的解析与指令分发OpenFlow是OVS面向控制器的接口语言。没有配置控制器时ovs-ofctl就是最常用的OpenFlow客户端配置了控制器后由controller通过协议下发流表。你要读懂OVS的OpenFlow实现核心是两条连接怎么建立消息怎么解析。4.1 连接建立与报文处理框架笔记把它分成两类主动连接ofconn和被动监听ofservice。主动连接是OVS作为客户端去连控制器入口在bridge_configure_remotes()里调用链是// 主动连接控制器的调用链 connmgr_set_controllers() - add_controller() - ofconn_create() // 创建ofconn对象 - rconn_connect() // 建立rconn连接 - vconn_open() // 打开vconn - tcp_open() // 实际TCP socket连接被动监听是OVS开一个本地unix socket或TCP端口等控制器来连核心是ofservice_create()监听建立走pvconn_open()接受连接后创建ofconn并把连接加入all_conns链表。两者最终都汇聚到ofconn_run()统一处理报文读写。4.2 flow_mod消息格式flow_mod是最核心的消息类型它能被拆成四段OpenFlow头部、flow_mod固定字段、match字段和instruction字段。报文格式示意/* OpenFlow 1.3 flow_mod 的报文布局 */ struct ofp_flow_mod { struct ofp_header header; // version type length xid uint64_t cookie; // 控制器用于标识流表项 uint32_t buffer_id; // 如果带包标记buffer里的包 uint16_t out_port; // 用于delete等操作时的匹配条件 uint16_t out_group; // 作用同上 uint16_t priority; // 匹配优先级越大越优先 uint8_t table_id; // 目标流表编号 uint8_t command; // ADD / MODIFY / DELETE /* 后面是match字段和instruction字段 */ };实际抓包排错时tcpdump是最直接的工具# 抓取OpenFlow控制流量文件存下来用wireshark分析 tcpdump -i any -s 0 -w of_trace.pcap tcp port 6633 or tcp port 6653抓包看flow_mod时重点看两个地方match里的OXM TLV是否带mask以及instruction里的apply-actions顺序。很多控制器下发失败或行为不符合预期都是这两个地方写错了。4.3 match字段的OXM解析match字段解析在ofputil_pull_ofp11_match()核心是nx_pull_raw()。OXM格式是TLV结构每个字段有一个32位headeroxm_class占8位、oxm_field占7位、hasmask占1位、oxm_length占8位。解析函数nx_pull_match_entry()的逻辑是// 从TLV中解析一个match字段并做合法性检查 nx_pull_match_entry() - nx_pull_entry__() // 拆出mf_field_id和value - mf_are_prereqs_ok() // 检查依赖字段是否满足 - mf_is_all_wild() // 字段重复性检测 - mf_is_mask_valid() // mask范围合法性 - mf_is_value_valid() // value范围合法性 - mf_set() // 写回match结构这几步检查里mf_are_prereqs_ok()最容易踩坑。比如匹配IPv4源地址时如果flow里没先匹配eth_type0x0800这个字段会被判定为非法。很多自测流表下发失败的原因不在OVS本身而在match字段的前置条件没满足。4.4 instruction字段的处理instruction解析在ofpacts_pull_openflow_instructions()核心是decode_openflow11_instructions()它按类型把解析结果放到insts[N_OVS_INSTRUCTIONS]数组里。OVS_INSTRUCTIONS宏定义了6种类型instruction类型对应的ofpact处理行为OFPIT11_METERofpact_put_METER关联meter限速OFPIT11_APPLY_ACTIONSofpacts_decode立即执行actionOFPIT11_CLEAR_ACTIONSofpact_put_CLEAR_ACTIONS清空action集合OFPIT11_WRITE_ACTIONSofpacts_decode_for_action_set写入action集合OFPIT11_WRITE_METADATAofpact_put_WRITE_METADATA写metadataOFPIT11_GOTO_TABLEofpact_put_GOTO_TABLE跳转其他流表注意区分APPLY_ACTIONS和WRITE_ACTIONS。前者立即执行后者只是写进action集合等流水线结束后统一处理。如果你在做多级流表GOTO_TABLE和WRITE_METADATA的组合要特别留意——metadata的作用范围是整个流水线这点和寄存器类似读源码时如果只盯着单个表项理解它很容易把metadata的作用域搞错。5. 源码阅读避坑版本、职责边界与复现环境读这份笔记和读OVS源码的过程中我整理了几条真实的踩坑记录按照现象、原因、解决三个维度复盘一下能帮你省下不少时间。5.1 版本不一致导致的行号错位现象对照笔记的代码路径去找netdev_frame_hook()发现本地源码里根本没有这个函数或者行数完全对不上。原因笔记基于2.3.90版本而很多发行版内置的OVS已经到2.5甚至3.x函数名和模块拆分都有变动。解决阅读前先确认源版本用git checkout切到对应tag。想复现笔记内容的话最省事的办法是直接拉2.3.90的源码树再读。git checkout v2.3.90阅读底层网络代码时版本敏感度要高改个结构体字段名就能让整条链路的理解崩掉。遇到对不上的情况优先检查版本不要怀疑自己理解错了。5.2 把OVS当OpenFlow路由器用现象想用OVS实现三层转发写了大量匹配IP的流表结果流量行为一直不符合预期。原因OVS的定位是虚拟交换机虽然支持L3匹配字段但路由决策、ARP处理、TTL修改这些它默认不帮你做那不是它的职责边界。解决先搞清楚OVS的L2交换核心再把L3功能通过流表组合实现必要时搭配独立的路由程序。理解这一点非常重要它决定了你读源码时的关注点。如果按“OVS能代替路由器”的思路去读代码你会反复纠结为什么不处理ARP而实际上OVS只是把ARP报文当成普通数据包做流表匹配而已。5.3 只追代码不画调用链现象从ovs_dp_process_packet()开始追追到masked_flow_lookup()里面就迷路了返回上下文全丢了。原因OVS的调用链动辄十几层特别是从用户态到内核态的跨层调用纯靠记笔记很难维持上下文。解决每追完一条链路立刻画一张调用链图标注每一层的数据结构做了什么修改。笔记里那些大括号嵌套的结构图就是这么来的。我个人的习惯是先把链路画在纸上再对照笔记的结构图补细节。画完一遍你对这个函数的理解深度会远超直接读十遍源码。5.4 用户态与内核模块版本不匹配现象ovs-vswitchd日志频繁报错upcall处理异常甚至出现连接断开重连。原因用户态程序版本和内核datapath模块版本不一致最典型的场景是系统自带的openvswitch.ko比较老但用户想升级用户态工具而没同步编译内核模块。解决检查两者版本是否匹配。# 查看内核模块版本 modinfo openvswitch | grep version # 查看用户态版本 ovs-vswitchd --version如果发现版本不一致重新编译并加载内核模块加载前记得先停掉vswitchd再rmmod openvswitch最后重新insmod顺序乱了会直接导致旧模块还在内存里被继续使用这种问题排查起来比较隐蔽。5.5 忽略recirc和slow path导致行为理解偏差现象读action处理时看到OVS_ACTION_ATTR_RECIRC但没当回事后面理解多级流表或bonding场景时怎么都想不通。原因recirc不是辅助逻辑它让报文重新回到流表查询入口是实现OpenFlow多级流水线和一些复杂动作的关键机制。忽略它就等于跳过了一半的datapath行为。解决看到RECIRC、slow path这些关键词别跳过先搞懂它把报文送回了哪个阶段再往下读。upcall里的slow path也同理它是“来不及或没必要在内核快速处理”的降级路径很多流量异常问题最后都出在这里。读OVS源码时把这些特殊路径当成一等公民对待而不是附带说明。6. 推荐一套“代码命令抓包”的三步读法这套方法是我读OVS过程中摸索出来的核心思路是每条代码路径都配上一个可观察的系统行为读代码的同时在真实环境验证这样理解不会飘。第一步读代码时锁定一条主线入口。比如从ovs_dp_process_packet()开始往下走到masked_flow_lookup()然后往左走到ovs_dp_upcall()。读的时候把笔记里的结构图和自己的调用链图对照着看重点标记每个函数对哪个数据结构做了修改。读完这一条主线你对datapath的整体框架就有把握了。第二步用ovs-appctl和ovs-dpctl验证状态。读uofproto创建流程时先在环境里跑一遍ovs-appctl dpif/show对照代码里init_ports()的初始化逻辑看端口是否存在、状态是否一致。读upcall线程时运行ovs-dpctl dump-flows看流表的hit和miss计数判断慢路径是否在工作。这种方式能让代码里的函数名和实际运行状态建立起对应关系比单纯看代码牢靠得多。第三步用抓包验证协议解析。读OpenFlow的match解析时起一个简单控制器下发一条带OXM的flow_mod同时用tcpdump抓包把报文里的TLV字段和nx_pull_raw()的解析结果逐字段对照你会发现书上写的OXM layout和线上实际传输的格式完全一致这种感觉很扎实。# 快速验证用ovs-ofctl下发一条带OXM的流表 ovs-ofctl add-flow br0 \ table0,ip,nw_src192.168.1.0/24,actionsoutput:1这条命令里的ip和nw_src会被转成OXM TLV字段下发后立刻用ovs-ofctl dump-flows br0查看实际存储的match内容你会看到OVS如何把人类可读的规则变成内部结构。这套三步读法还有一个隐藏好处遇到问题时有明确的排查顺序。先看代码确认预期行为再看状态确认实际行为最后抓包确认协议交互是否正常。从那以后我每次拿到新版本的OVS源码都会强制走一遍这个流程先跑通一条收包链路再扩展分支逻辑。几轮下来OVS在你眼里就不再是一个黑匣子了。希望帮到你。本文还有配套的精品资源点击获取
返回列表