ovs源码分析

源码版本

ovs 内核部分的代码在linux内核的 /net/openswitch目录下,应用层控制面代码在ovs项目中。

  • Linux kernel: version 6.2.0

  • Ovs: v3.4.1

总体架构

整体结构图

ovs的架构如下图所示,主要由内核datapath、vswitchd、ovsdb以及用户空间的ovs-vsctl/ovs-ofctl/ovs-dpctl等组成。

  • vswitchd是一个守护进程,是ovs的管理和控制服务,通过unix socket将配置信息保存到ovsdb,并通过netlink和内核模块交互。

  • ovsdb则是ovs的数据库,保存了ovs配置信息。

  • datapath是负责数据交换的内核模块,比如把接受端口收到的包放到流表中进行匹配,并执行匹配后的动作等。它在初始化和port binding的时候注册钩子函数,把端口的报文处理接管到内核模块。

实机环境

ovs 守护进程

root@node1:~# ps -aux|grep vswitchd
root       906  3.5  0.7 531888 74196 ?        S<Lsl 17:40   0:01 ovs-vswitchd unix:/var/run/openvswitch/db.sock -vconsole:emer -vsyslog:err -vfile:info --mlockall --no-chdir --log-file=/var/log/openvswitch/ovs-vswitchd.log --pidfile=/var/run/openvswitch/ovs-vswitchd.pid --detach

ovsdb 守护进程

root@node1:~# ps -aux|grep ovsdb
root      5904  0.0  0.0  14900  3400 ?        S<s  17:40   0:00 ovsdb-server: monitoring pid 5905 (healthy)
root      5905  0.4  0.0  16220  8392 ?        S<   17:40   0:00 ovsdb-server /etc/openvswitch/conf.db -vconsole:emer -vsyslog:err -vfile:info --remote=punix:/var/run/openvswitch/db.sock --private-key=db:Open_vSwitch,SSL,private_key --certificate=db:Open_vSwitch,SSL,certificate --bootstrap-ca-cert=db:Open_vSwitch,SSL,ca_cert --no-chdir --log-file=/var/log/openvswitch/ovsdb-server.log --pidfile=/var/run/openvswitch/ovsdb-server.pid --detach --monitor
root      6925  0.0  0.0  14904  3408 ?        Ss   17:40   0:00 ovsdb-server: monitoring pid 120 (healthy)
root      6926  0.1  0.0 163628  8080 ?        Sl   17:40   0:00 ovsdb-server -vconsole:off -vfile:info --log-file=/var/log/ovn/ovsdb-server-nb.log --remote=punix:/var/run/ovn/ovnnb_db.sock --pidfile=/var/run/ovn/ovnnb_db.pid --unixctl=/var/run/ovn/ovnnb_db.ctl --detach --monitor --remote=db:OVN_Northbound,NB_Global,connections --private-key=db:OVN_Northbound,SSL,private_key --certificate=db:OVN_Northbound,SSL,certificate --ca-cert=db:OVN_Northbound,SSL,ca_cert --ssl-protocols=db:OVN_Northbound,SSL,ssl_protocols --ssl-ciphers=db:OVN_Northbound,SSL,ssl_ciphers --remote=ptcp:6641:[::] /etc/ovn/ovnnb_db.db
root      7066  0.0  0.0  14904  3408 ?        Ss   17:40   0:00 ovsdb-server: monitoring pid 143 (healthy)
root      7068  0.1  0.1 165384 10172 ?        Sl   17:40   0:00 ovsdb-server -vconsole:off -vfile:info --log-file=/var/log/ovn/ovsdb-server-sb.log --remote=punix:/var/run/ovn/ovnsb_db.sock --pidfile=/var/run/ovn/ovnsb_db.pid --unixctl=/var/run/ovn/ovnsb_db.ctl --detach --monitor --remote=db:OVN_Southbound,SB_Global,connections --private-key=db:OVN_Southbound,SSL,private_key --certificate=db:OVN_Southbound,SSL,certificate --ca-cert=db:OVN_Southbound,SSL,ca_cert --ssl-protocols=db:OVN_Southbound,SSL,ssl_protocols --ssl-ciphers=db:OVN_Southbound,SSL,ssl_ciphers --remote=ptcp:6642:[::] /etc/ovn/ovnsb_db.db
root     20535  0.0  0.0  12300  2816 pts/0    S+   17:42   0:00 grep --color=auto ovsdb

ovs内核模块

root@node1:~# lsmod |grep openvswitch
openvswitch           212992  18
nsh                    12288  1 openvswitch
nf_conncount           24576  1 openvswitch
nf_nat                 61440  4 ip6table_nat,openvswitch,iptable_nat,xt_MASQUERADE
nf_conntrack          208896  8 xt_conntrack,nf_nat,nfnetlink_cttimeout,openvswitch,nf_conntrack_netlink,nf_conncount,xt_MASQUERADE,ip_vs
libcrc32c              12288  5 nf_conntrack,nf_nat,openvswitch,nf_tables,ip_vs

 

关键概念

Datapath

使用kubesphere 开启ovn 安装的单节点集群。

Datapath(数据通路):在网络交换机和路由器中,数据通路是指转发数据包的路径。在 OVS 中,数据通路是用于处理和转发数据包的核心组件,查询到的网桥 br-int, 是ovs在所在节点创建的虚拟网桥。

VPort

VPort 实现:在 OVS 中,VPort 是通过内核模块和用户态进程协同工作来实现的。内核模块负责处理数据包的转发和接收,而用户态进程则负责控制和管理 VPort。

VPort 标识:每个 VPort 都有一个唯一的标识符,用于在 OVS 数据结构中进行识别和管理。

VPort 的使用场景

  • 数据包处理:当数据包到达一个 VPort 时,内核模块会将数据包从物理设备或其他 VPort 接收,并根据流表规则进行处理。处理可能包括数据包转发、过滤、NAT 等操作。

  • 与虚拟机连接:VPort 通常用于连接虚拟机或容器。通过 VPort,虚拟机可以与 OVS 进行通信,实现虚拟网络的构建和管理。

  • 虚拟交换机间通信:除了连接虚拟机外,VPort 也可以用于连接不同的虚拟交换机,实现虚拟网络之间的通信。

  • 管理和配置:用户态进程负责管理和配置 VPort,包括创建、删除、绑定到特定虚拟机等操作。

总的来说,VPort 在 OVS 中扮演了连接虚拟网络设备的重要角色,通过 VPort,OVS 能够实现虚拟网络的构建、管理和数据包处理。

  • 其中internal 接口类型是一个特定的端口类型,通常会被标记为 type: internal。它的流量通常不会直接暴露到物理网络中。

  • 这种类型的接口通常不具备物理网络接口的一些功能,例如无需设置 MAC 地址或与物理设备的接口绑定。

端口特性

下面是查询到的ovs具体场景中的网卡

root@node1:~# ovs-vsctl show
7fb85a99-5ae5-4321-938f-fc647f81c52dBridge br-int                    //网桥fail_mode: securedatapath_type: systemPort br-intInterface br-int         //代表网桥本身的端口,其他端口的流量会通过流入该端口进入ovstype: internal       Port mirror0Interface mirror0       // 主要用于调试数据的流量镜像端口(ovn创建,非ovs核心功能端口)type: internalPort "9c9076b62a19_h"        // 连接业务容器的端口Interface "9c9076b62a19_h"    Port c42b8d13d6ea_hInterface c42b8d13d6ea_h // 连接业务容器的端口Port "8f5f5cc5dc66_h"Interface "8f5f5cc5dc66_h"Port ovn0                   // ovn创建的作为网关的端口(ovn创建,非ovs核心功能端口)Interface ovn0type: internal//...ovs_version: "2.17.3"

对于OVS来讲,几种网卡类型的特性如下

1) netdev: 通用网卡设备 eth0 veth

接收: 一个nedev在L2收到报文后会直接通过ovs接收函数处理,不会再走传统内核协议栈.

发送: ovs中的一条流指定从该netdev发出的时候就通过该网卡设备发送

2) internal: 一种虚拟网卡设备

接收: 当从系统发出的报文路由查找通过该设备发送的时候,就进入ovs接收处理函数

发送: ovs中的一条流指定从该internal设备发出的时候,该报文被重新注入内核协议栈

3) gre device: gre设备. 不管用户态创建多少个gre tunnel, 在内核态有且只有一个gre设备

接收: 当系统收到gre报文后,传递给L4层解析gre header, 然后传递给ovs接收处理函数

发送: ovs中的一条流制定从该gre设备发送, 报文会根据流表规则加上gre头以及外层包裹ip,查找路由发送

br-int端口

  1. 基本功能

  • 作为虚拟设备的接口 Interface br-int 是与网桥 br-int 关联的虚拟网络设备,代表网桥自身。通过它,主机可以与网桥通信。

  • 配置和控制网桥 作为网桥的内部接口,它用于接收管理流量(如配置命令)或处理流量转发规则。


  1. 工作原理

  • 桥接点 br-int 作为集成网桥,通过 Interface br-int 与主机的网络栈交互。主机发送到 br-int 的数据包,实际上会通过 Interface br-int 进入 OVS 数据路径。

  • 辅助实现内部通信 br-int 网桥的所有端口(如连接容器的端口)之间的数据流量最终会通过 br-int 的转发逻辑实现。


  1. 使用场景

  • OpenStack 和 OVN 在云平台(如 OpenStack)和虚拟网络(如 OVN)中,br-int 是虚拟机和容器网络的核心桥梁。通过 Interface br-int,虚拟机、容器、以及外部网关可以完成通信。

  • 调试和监控 系统管理员可以使用工具(如 tcpdump)在 Interface br-int 上抓包,检查流量流动或排查问题。


  1. 示例作用

假设一个虚拟机通过一个端口(如 tap0)连接到 br-int 网桥。流量路径如下:

  1. 数据从虚拟机发出,经 tap0 端口进入 br-int

  2. br-int 根据流表规则处理数据流,可能转发到其他端口。

  3. 如果数据需要发到主机,流量会通过 Interface br-int 进入主机网络栈。


总结

Interface br-int 的作用可以概括为:

  1. 作为网桥 br-int 的内部接口,与主机网络栈交互。

  2. 提供一个管理和调试的入口。

  3. 在虚拟化环境中支持虚拟机、容器与主机之间的通信。

它是网桥 br-int 功能实现不可或缺的一部分。

流表

查看该网桥流表信息, 并选取几个流表项进行分析

Flow Table(流表):流表是 OVS 数据通路中的一种数据结构,用于存储和匹配数据包流的规则。当数据包到达 OVS 时,将根据流表中的规则进行匹配,并决定如何处理数据包。

root@node1:~# ovs-ofctl dump-flows br-int
// 数据包的输入端口是 "8f5f5cc5dc66_h",这看起来是连接到业务容器的一个端口, 该流将提交到8号流表继续处理cookie=0x90ed7d93, duration=135.488s, table=0, n_packets=7447, n_bytes=15326624, priority=100,in_port=ovn0 actions=load:0x2->NXM_NX_REG13[],load:0x1->NXM_NX_REG11[],load:0x7->NXM_NX_REG12[],load:0x2->OXM_OF_METADATA[],load:0x2->NXM_NX_REG14[],resubmit(,8)// 11 号流表 匹配 IPv6 ICMP 类型 136(邻居请求)的数据包,元数据字段为 0x1,网络 TTL 为 255,ICMP 类型为 136,ICMP 代码为 0// 该类流将丢弃cookie=0x92283387, duration=41.987s, table=11, n_packets=0, n_bytes=0, idle_age=41, priority=85,icmp6,metadata=0x1,nw_ttl=255,icmp_type=136,icmp_code=0 actions=drop// 该流表 匹配 SCTP 协议的数据包(sctp, reg0=0x4/0x4, metadata=0x2) 执行连接跟踪(ct),提交到流表 16// 使用寄存器 NXM_NX_REG13 为连接跟踪区(zone),执行 NAT。cookie=0xf7ce3e3a, duration=41.991s, table=15, n_packets=0, n_bytes=0, idle_age=41, priority=120,sctp,reg0=0x4/0x4,metadata=0x2 actions=move:NXM_OF_IP_DST[]->NXM_NX_XXREG0[64..95],move:OXM_OF_SCTP_DST[]->NXM_NX_XXREG0[32..47],ct(table=16,zone=NXM_NX_REG13[0..15],nat)// 这个流表项用于处理 TCP 数据包,执行连接跟踪和 NAT 转换。cookie=0xf0ac6d85, duration=41.995s, table=70, n_packets=0, n_bytes=0, idle_age=41, priority=100,tcp,reg1=0xae90ee0,reg2=0x19f3/0xffff actions=ct(commit,zone=NXM_NX_REG12[0..15],nat(src=10.233.14.224))

数据流向

一般的数据包在linux网络协议栈中的流向为黑色箭头流向:从网卡上接受到数据包后层层往上分析,最后离开内核态,把数据传送到用户态。当然也有些数据包只是在内核网络协议栈中操作,然后再从某个网卡发出去。

但当其中有openVswitch时,数据包的流向就不一样了。首先是创建一个网桥:ovs-vsctl add-br br0;然后是绑定某个网卡:绑定网卡:ovs-vsctl add-port br0 eth0;这里默认为绑定了eth0网卡。数据包的流向是从网卡eth0上然后到openVswitch的端口vport上进入openVswitch中,然后根据key值进行流表的匹配。

如果匹配成功,则根据流表中对应的action找到其对应的操作方法,完成相应的动作(这个动作有可能是把一个请求变成应答,也有可能是直接丢弃,也可以自己设计自己的action);

如果匹配不成功,则执行默认的动作,有可能是放回内核网络协议栈中去处理(在创建网桥时就会相应的创建一个端口连接内核协议栈的)。

源码分析

数据结构

  • 网桥模块datapath:

/*** struct datapath - datapath for flow-based packet switching* @rcu: RCU callback head for deferred destruction.* @list_node: Element in global 'dps' list.* @table: flow table.* @ports: Hash table for ports.  %OVSP_LOCAL port always exists.  Protected by* ovs_mutex and RCU.* @stats_percpu: Per-CPU datapath statistics.* @net: Reference to net namespace.* @max_headroom: the maximum headroom of all vports in this datapath; it will* be used by all the internal vports in this dp.* @upcall_portids: RCU protected 'struct dp_nlsk_pids'.** Context: See the comment on locking at the top of datapath.c for additional* locking information.*/struct datapath {struct rcu_head rcu;struct list_head list_node;  //将网桥连接为链组织/* Flow table. */struct flow_table table;    // 网桥版定的流表/* Switch ports. */struct hlist_head *ports;    //网桥绑定的端口/* Stats. */struct dp_stats_percpu __percpu *stats_percpu;/* Network namespace ref. */possible_net_t net;u32 user_features;u32 max_headroom;/* Switch meters. */struct dp_meter_table meter_tbl;struct dp_nlsk_pids __rcu *upcall_portids;
};
  • vport端口:

/*** struct vport - one port within a datapath* @dev: Pointer to net_device.* @dev_tracker: refcount tracker for @dev reference* @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.* @upcall_stats: Upcall stats of every ports.* @detach_list: list used for detaching vport in net-exit call.* @rcu: RCU callback head for deferred destruction.*/
struct vport {struct net_device *dev;        //指向网络设备结构体的指针,表示与此虚拟端口相关联的网络设备netdevice_tracker dev_tracker;struct datapath *dp;                //端口关联的网桥struct vport_portids __rcu *upcall_portids;u16 port_no;                        //端口号,唯一标识struct hlist_node hash_node;struct hlist_node dp_hash_node;const struct vport_ops *ops;       //指向虚拟端口操作函数集struct vport_upcall_stats_percpu __percpu *upcall_stats;struct list_head detach_list;struct rcu_head rcu;
};
  • 流表结构flow_table

// 表通用结构,该表的buckets 是alloc的定长数组,数组中每个格子存放的是 对应数据的双向链表
// table_instance.buckects = malloc(sizeof(hlist_head) * n_buckets)
struct table_instance {struct hlist_head *buckets;    // 链表数组unsigned int n_buckets;        // buckets 数组长度struct rcu_head rcu;int node_ver;u32 hash_seed;
};
// 流表管理
struct flow_table {// 流表的主哈希表,存储流表项(flow entries),// 这些流表项是通过五元组匹配字段(如源IP、目的IP、端口等)组织的。struct table_instance __rcu *ti;//指向以 UFID(Unique Flow Identifier) 为索引的辅助哈希表。// 用于快速查找流表项,通过 UFID(一种全局唯一标识符)来定位特定流表项。struct table_instance __rcu *ufid_ti;// 流表的掩码缓存(mask cache)。提高流表查找效率,通过缓存最近使用的掩码//(mask)来减少掩码匹配的开销。掩码通常用于实现子网匹配或特定字段的模糊匹配。struct mask_cache __rcu *mask_cache;// 流表的掩码数组。// 存储所有有效的掩码,供流表匹配时使用。掩码是流表项中用于选择性匹配某些字段的模板。struct mask_array __rcu *mask_array;// 记录上次重哈希(rehash)的时间戳。// 在负载因子达到一定阈值时,触发对流表哈希的重构,以保持高效的查找性能。unsigned long last_rehash;// 当前流表中存储的流表项数量。// 提供流表的大小信息,用于控制哈希表的扩展或缩小。unsigned int count;// 以 UFID 为索引的流表项数量。// 记录辅助哈希表中的流表项数量,用于管理和调试。unsigned int ufid_count;
};

哈希表 (ti) 和辅助哈希表 (ufid_ti) 的协作

哈希表 (ti)

  以流的匹配字段(如源IP、目的IP、端口等五元组)为键存储流表项。

  主要用于数据平面快速匹配数据包。

辅助哈希表 (ufid_ti)

  以 UFID(Unique Flow Identifier) 作为键存储流表项。

  主要用于控制平面通过 UFID 操作流表(例如添加、删除、更新流表项)。

  UFID 是一个全局唯一的标识符,解决了复杂匹配条件下的直接查找问题。

配合方式

数据平面:通过 ti 使用匹配字段快速定位流表项。

控制平面:通过 ufid_ti 使用 UFID 高效地对流表项进行管理。

这些哈希表之间的数据同步通过流表项的生命周期管理机制完成。

掩码数组(mask_array)

mask_array 是一个包含流表项掩码(mask)的数组。掩码在流表匹配中用于定义如何比较流表项的特定字段。mask_array 中的掩码有助于优化流表的匹配过程,使得 OVS 可以更高效地处理网络流量。掩码通常用于匹配IP地址、端口号等字段中的某些部分(例如子网匹配)。

struct sw_flow {struct rcu_head rcu; // rcu保护机制struct {// node[0] 是哈希链表节点,表示流表项在 ti 哈希表中的位置// node[1] 是哈希链表节点,表示流表项在 ufid_ti 哈希表中的位置struct hlist_node node[2]; u32 hash;                 // hash值} flow_table, ufid_table;int stats_last_writer;      /* CPU id of the last writer on* 'stats[0]'.*/struct sw_flow_key key;    // 流表中的key值struct sw_flow_id id;    // 存储该流表项的唯一标识符 UFIDstruct cpumask *cpu_used_mask;struct sw_flow_mask *mask;    // 要匹配的mask结构体struct sw_flow_actions __rcu *sf_acts;    // 相应的action动作struct sw_flow_stats __rcu *stats[]; /* One for each CPU.  First one* is allocated at flow creation time,* the rest are allocated on demand* while holding the 'stats[0].lock'.*/
};struct sw_flow_key_range {unsigned short int start;    // key值匹配数据开始部分unsigned short int end;    // key值匹配数据结束部分
};// 流掩码
struct sw_flow_mask {int ref_count;struct rcu_head rcu;struct sw_flow_key_range range; // 对key赋予范围,构成掩码struct sw_flow_key key; // 要和数据包操作的key,将要被用来匹配的key值
};#define MAX_UFID_LENGTH 16 /* 128 bits */// UFID, hash计算出的字符串
struct sw_flow_id {u32 ufid_len;union {u32 ufid[MAX_UFID_LENGTH / 4];struct sw_flow_key *unmasked_key;};
};

添加网桥

  • 键入命令ovs-vsctl add-br testBR

  • 内核中的 openvswitch.ko 收到一个添加网桥的命令时候——即收到 OVS_DATAPATH_FAMILY通道的 OVS_DP_CMD_NEW命令。该命令绑定的回调函数为 ovs_dp_cmd_new

OVS_DP_CMD_NEW 注册 回调函数 ovs_dp_cmd_new

static const struct genl_small_ops dp_datapath_genl_ops[] = {{ .cmd = OVS_DP_CMD_NEW,.validate = GENL_DONT_VALIDATE_STRICT | GENL_DONT_VALIDATE_DUMP,.flags = GENL_UNS_ADMIN_PERM, /* Requires CAP_NET_ADMIN privilege. */.doit = ovs_dp_cmd_new}, 

 

  • ovs_dp_cmd_new 函数除了初始化 dp 结构

static int ovs_dp_cmd_new(struct sk_buff *skb, struct genl_info *info)
{//...// 申请dp 即datapath 结构 dp = kzalloc(sizeof(*dp), GFP_KERNEL);if (dp == NULL)goto err_destroy_reply;ovs_dp_set_net(dp, sock_net(skb->sk));/* Allocate table. */// 初始化流表err = ovs_flow_tbl_init(&dp->table);if (err)goto err_destroy_dp;// 初始化状态err = ovs_dp_stats_init(dp);if (err)goto err_destroy_table;// 初始化datapath的 vport 表err = ovs_dp_vport_init(dp);if (err)goto err_destroy_stats;err = ovs_meters_init(dp);if (err)goto err_destroy_ports;// 初始化端口,对网桥br-int ,将初始化化内部端口 br-int internal/* Set up our datapath device. */parms.name = nla_data(a[OVS_DP_ATTR_NAME]);parms.type = OVS_VPORT_TYPE_INTERNAL;parms.options = NULL;parms.dp = dp;parms.port_no = OVSP_LOCAL;parms.upcall_portids = a[OVS_DP_ATTR_UPCALL_PID];parms.desired_ifindex = a[OVS_DP_ATTR_IFINDEX]? nla_get_s32(a[OVS_DP_ATTR_IFINDEX]) : 0;/* So far only local changes have been made, now need the lock. */ovs_lock();err = ovs_dp_change(dp, a);if (err)goto err_unlock_and_destroy_meters;// 网桥自身vport的生成vport = new_vport(&parms);if (IS_ERR(vport)) {//...}
  • 调用 new_vport 函数来生成新的 vport

可以看到vport创建乎是连接入 hash表中的链表进行管理的

static struct vport *new_vport(const struct vport_parms *parms)
{struct vport *vport;//创建vportvport = ovs_vport_add(parms);// 放入hash表管理if (!IS_ERR(vport)) {struct datapath *dp = parms->dp;struct hlist_head *head = vport_hash_bucket(dp, vport->port_no);hlist_add_head_rcu(&vport->dp_hash_node, head);}return vport;
}
  • new_vport 函数调用 ovs_vport_add()来尝试生成一个新的 vport

struct vport *ovs_vport_add(const struct vport_parms *parms)
{//... ops = ovs_vport_lookup(parms);if (ops) {struct hlist_head *bucket;if (!try_module_get(ops->owner))return ERR_PTR(-EAFNOSUPPORT);// 创建vport, 这里将调用注册的函数方法vport = ops->create(parms);if (IS_ERR(vport)) {module_put(ops->owner);return vport;}//...}// ...
}
  • ovs_vport_add()函数会检查 vport 类型(通过 vport_ops_list[]数组),并调用相关的 create()函数来生成 vport 结构

在 net/openvswitch/ 目录下可找到多个 net/openvswitch/vport-xxx.c 的文件,对应各个类型端口的注册回调

net/openvswitch/vport-geneve.c

static struct vport_ops ovs_geneve_vport_ops = { .type           = OVS_VPORT_TYPE_GENEVE,.create         = geneve_create,                                                                                                                                                                    .destroy        = ovs_netdev_tunnel_destroy,.get_options    = geneve_get_options,.send           = dev_queue_xmit,
};  

net/openvswitch/vport-gre.c

static struct vport_ops ovs_gre_vport_ops = { .type           = OVS_VPORT_TYPE_GRE,.create         = gre_create,                                                                                                                                                                       .send           = dev_queue_xmit,.destroy        = ovs_netdev_tunnel_destroy,
};  

net/openvswitch/vport-internal_dev.c

此处可以看到internal_vport 的特殊性, 其包send函数是 internal 的接收函数 internal_dev_recv,即通过internal 端口发送的包都将进入internal 的接收函数,从而进入ovs继续处理分发。

static struct vport_ops ovs_internal_vport_ops = { .type           = OVS_VPORT_TYPE_INTERNAL,.create         = internal_dev_create,.destroy        = internal_dev_destroy,.send           = internal_dev_recv,                                                                                                                                                                
}; 

net/openvswitch/vport-netdev.c

static struct vport_ops ovs_netdev_vport_ops = { .type           = OVS_VPORT_TYPE_NETDEV,.create         = netdev_create,                                                                                                                                                                    .destroy        = netdev_destroy,.send           = dev_queue_xmit,
};     

net/openvswitch/vport-vxlan.c

static struct vport_ops ovs_vxlan_netdev_vport_ops = {.type                   = OVS_VPORT_TYPE_VXLAN,.create                 = vxlan_create,                                                                                                                                                             .destroy                = ovs_netdev_tunnel_destroy,.get_options            = vxlan_get_options,.send                   = dev_queue_xmit,
};
  • 当dp是网络设备时(vport_netdev.c),最终由 ovs_vport_add()函数调用的是 netdev_create()【在 vport_ops_listovs_netdev_ops 中】

  • netdev_create()函数最关键的一步是注册了收到网包时的回调函数

static struct vport *netdev_create(const struct vport_parms *parms)
{struct vport *vport;vport = ovs_vport_alloc(0, &ovs_netdev_vport_ops, parms);if (IS_ERR(vport))return vport;// 在该函数中注册收包函数return ovs_netdev_link(vport, parms->name);
}struct vport *ovs_netdev_link(struct vport *vport, const char *name)
{//...// 注册 netdev_frame_hook, 即收报处理函数err = netdev_rx_handler_register(vport->dev, netdev_frame_hook,vport);//...
}
  • register_netdevice(vport->dev)操作是将 netdev_vport->dev 收到网包时的相关数据由 netdev_frame_hook()函数来处理,都是些辅助处理,依次调用各处理函数,在 netdev_port_receive()【这里会进行数据包的拷贝,避免损坏】进入 ovs_vport_receive()回到 vport.c,从 ovs_dp_process_packet()回到 datapath.c,进行统一处理

/* Called with rcu_read_lock and bottom-halves disabled. */
static rx_handler_result_t netdev_frame_hook(struct sk_buff **pskb)
{//...netdev_port_receive(skb);return RX_HANDLER_CONSUMED;
}/* Must be called with rcu_read_lock. */
static void netdev_port_receive(struct sk_buff *skb)
{// ...ovs_vport_receive(vport, skb, skb_tunnel_info(skb));return;
error:kfree_skb(skb);
}int ovs_vport_receive(struct vport *vport, struct sk_buff *skb,const struct ip_tunnel_info *tun_info)
{    // 提取数据包特征用于流表匹配//.../* Extract flow from 'skb' into 'key'. */error = ovs_flow_key_extract(tun_info, skb, &key);if (unlikely(error)) {kfree_skb(skb);return error;}// 处理数据包ovs_dp_process_packet(skb, &key);return 0;
}
  • 收包流程:netdev_frame_hook()->netdev_port_receive->ovs_vport_receive->ovs_dp_process_received_packet()

数据包处理

  • ovs_vport_receive()调用ovs_flow_extract基于skb生成key值,并检查是否有错,然后调用ovs_dp_process_packet。交付给datapath处理

int ovs_flow_key_extract(const struct ip_tunnel_info *tun_info,struct sk_buff *skb, struct sw_flow_key *key)
{
#if IS_ENABLED(CONFIG_NET_TC_SKB_EXT)struct tc_skb_ext *tc_ext;
#endifbool post_ct = false, post_ct_snat = false, post_ct_dnat = false;int res, err;u16 zone = 0;// 是否是隧道封装发送过来的包进行区别处理/* Extract metadata from packet. */if (tun_info) {// 获取隧道协议key->tun_proto = ip_tunnel_info_af(tun_info);//提取隧道信息memcpy(&key->tun_key, &tun_info->key, sizeof(key->tun_key));if (tun_info->options_len) {BUILD_BUG_ON((1 << (sizeof(tun_info->options_len) *8)) - 1> sizeof(key->tun_opts));ip_tunnel_info_opts_get(TUN_METADATA_OPTS(key, tun_info->options_len),tun_info);key->tun_opts_len = tun_info->options_len;} else {key->tun_opts_len = 0;}} else  {// 关键字中隧道信息放置为空信息key->tun_proto = 0;key->tun_opts_len = 0;memset(&key->tun_key, 0, sizeof(key->tun_key));}// ...// 进一步提取更通用的信息,如以太网头信息,Ip头信息err = key_extract(skb, key);if (!err) {ovs_ct_fill_key(skb, key, post_ct);   /* Must be after key_extract(). */if (post_ct) {if (!skb_get_nfct(skb)) {key->ct_zone = zone;} else {if (!post_ct_dnat)key->ct_state &= ~OVS_CS_F_DST_NAT;if (!post_ct_snat)key->ct_state &= ~OVS_CS_F_SRC_NAT;}}}return err;
}
  • ovs_flow_tbl_lookup_stats。基于前面生成的key值进行流表查找,返回匹配的流表项,结构为sw_flow

  • 若不存在匹配,则调用ovs_dp_upcall上传至userspace进行匹配。 (包括包和key都要上传)

  • 若存在匹配,则直接调用ovs_execute_actions执行对应的action,比如添加vlan头,转发到某个port等。

/* Must be called with rcu_read_lock. */
void ovs_dp_process_packet(struct sk_buff *skb, struct sw_flow_key *key)
{//...// 流表匹配flow = ovs_flow_tbl_lookup_stats(&dp->table, key, skb_get_hash(skb),&n_mask_hit, &n_cache_hit);if (unlikely(!flow)) {// 未匹配上流表项,调用upcall struct dp_upcall_info upcall;memset(&upcall, 0, sizeof(upcall));upcall.cmd = OVS_PACKET_CMD_MISS;if (dp->user_features & OVS_DP_F_DISPATCH_UPCALL_PER_CPU)upcall.portid =ovs_dp_get_upcall_portid(dp, smp_processor_id());elseupcall.portid = ovs_vport_find_upcall_portid(p, skb);upcall.mru = OVS_CB(skb)->mru;error = ovs_dp_upcall(dp, skb, key, &upcall, 0);//...}// 流找到匹配项,按照关联的action处理error = ovs_execute_actions(dp, skb, sf_acts, key);//...
}
  • 流表查询调用路径为

ovs_flow_tbl_lookup_stats-->尝试查询缓存-->flow_lookup-->masked_flow_lookup

/* Flow lookup does full lookup on flow table. It starts with* mask from index passed in *index.* This function MUST be called with BH disabled due to the use* of CPU specific variables.*/
static struct sw_flow *flow_lookup(struct flow_table *tbl,struct table_instance *ti,struct mask_array *ma,const struct sw_flow_key *key,u32 *n_mask_hit,u32 *n_cache_hit,u32 *index)
{struct mask_array_stats *stats = this_cpu_ptr(ma->masks_usage_stats);struct sw_flow *flow;struct sw_flow_mask *mask;int i;if (likely(*index < ma->max)) {mask = rcu_dereference_ovsl(ma->masks[*index]);if (mask) {flow = masked_flow_lookup(ti, key, mask, n_mask_hit);if (flow) {u64_stats_update_begin(&stats->syncp);stats->usage_cntrs[*index]++;u64_stats_update_end(&stats->syncp);(*n_cache_hit)++;return flow;}}}for (i = 0; i < ma->max; i++)  {if (i == *index)continue;mask = rcu_dereference_ovsl(ma->masks[i]);if (unlikely(!mask))break;// 遍历所有mask,在下面函数中使用遍历到的mask与key做掩码操作后// 进行流表查询flow = masked_flow_lookup(ti, key, mask, n_mask_hit);if (flow) { /* Found */*index = i;u64_stats_update_begin(&stats->syncp);stats->usage_cntrs[*index]++;u64_stats_update_end(&stats->syncp);return flow;}}return NULL;
}
static struct sw_flow *masked_flow_lookup(struct table_instance *ti,const struct sw_flow_key *unmasked,const struct sw_flow_mask *mask,u32 *n_mask_hit)
{struct sw_flow *flow;struct hlist_head *head;u32 hash;struct sw_flow_key masked_key;// 对flow_key进行掩码操作ovs_flow_mask_key(&masked_key, unmasked, false, mask);// 对掩盖结果进行hash计算hash = flow_hash(&masked_key, &mask->range);// 根据hash索引到对应的流表桶head = find_bucket(ti, hash);(*n_mask_hit)++;// 遍历查找到的流表桶进行逐一精确匹配hlist_for_each_entry_rcu(flow, head, flow_table.node[ti->node_ver],lockdep_ovsl_is_held()) {if (flow->mask == mask && flow->flow_table.hash == hash &&flow_cmp_masked_key(flow, &masked_key, &mask->range))// 匹配到时返回flowreturn flow;}return NULL;
}
  • 动作执行

在流表中匹配到流表项后,将使用找到的sw_flow 索引该流绑定的处理方法进行处理

调用路径为

ovs_execute_actions-->do_execute_actions

/* Execute a list of actions against 'skb'. */
static int do_execute_actions(struct datapath *dp, struct sk_buff *skb,struct sw_flow_key *key,const struct nlattr *attr, int len)
{const struct nlattr *a;int rem;// attr ,len 参数为该流关联的action数组,遍历执行actionfor (a = attr, rem = len; rem > 0;a = nla_next(a, &rem)) {int err = 0;if (trace_ovs_do_execute_action_enabled())trace_ovs_do_execute_action(dp, skb, key, a, rem);/* Actions that rightfully have to consume the skb should do it* and return directly.*/// 根据attr 的类型区别处理switch (nla_type(a)) {case OVS_ACTION_ATTR_OUTPUT: {//...}case OVS_ACTION_ATTR_TRUNC: {//...}case OVS_ACTION_ATTR_USERSPACE://...case OVS_ACTION_ATTR_HASH:execute_hash(skb, key, a);break;case OVS_ACTION_ATTR_PUSH_MPLS: {struct ovs_action_push_mpls *mpls = nla_data(a);err = push_mpls(skb, key, mpls->mpls_lse,mpls->mpls_ethertype, skb->mac_len);break;}//...}}if (unlikely(err)) {ovs_kfree_skb_reason(skb, OVS_DROP_ACTION_ERROR);return err;}}ovs_kfree_skb_reason(skb, OVS_DROP_LAST_ACTION);return 0;
}

各个action 说明如下

在 Open vSwitch (OVS) 中,流表的 action 定义了当某个数据包与流表项匹配时,应该执行的动作。OVS 提供了多种不同的 action,用于控制数据包的处理方式。以下是 OVS 流表中常见的 action 类型及其详细说明:

1. Output Action

output 操作是 OVS 中最常见的动作,表示将匹配的数据包发送到指定的端口。

语法output:port

功能:将数据包发送到指定的端口,可以是物理端口、虚拟端口或特殊端口。

  示例:output:2 表示将数据包发送到端口 2。

2. Drop Action

drop 操作用于丢弃匹配到的流表项的数据包。

功能:数据包被丢弃,不会被转发或处理。这是网络安全或流量控制中的常见动作。

  示例:如果某条流表项没有指定 output 操作,就相当于隐式地执行了 drop 操作。

3. Resubmit Action

resubmit 操作将数据包提交给指定的流表,进行重新匹配。

语法resubmit:[table],port

功能:允许数据包重新提交到指定的流表(或同一流表)的特定表项。可以实现多阶段流表处理。

  示例:resubmit:1 表示将数据包提交给流表表 1 进行进一步匹配。

4. Set Field Action

set_field 操作用于修改数据包的字段,例如修改数据包的 IP 地址、MAC 地址或 VLAN 标签。

语法set_field:value->field

功能:修改指定的包头字段。

  示例:set_field:10.0.0.1->ip_dst 表示将数据包的目标 IP 地址修改为 10.0.0.1

5. Push/Pop VLAN Action

这些操作用于插入或移除 VLAN 标签,通常用于处理 VLAN 隔离的网络。

push_vlan:为数据包添加一个 VLAN 标签。

  语法push_vlan:ethertype

  示例:push_vlan:0x8100 表示给数据包添加一个 VLAN 标签。

pop_vlan:从数据包中移除 VLAN 标签。

  语法pop_vlan

  示例:pop_vlan 表示从数据包中移除最外层的 VLAN 标签。

6. Push/Pop MPLS Action

这些操作用于插入或移除 MPLS(多协议标签交换)标签。

push_mpls:为数据包添加一个 MPLS 标签。

  语法push_mpls:ethertype

  示例:push_mpls:0x8847 表示给数据包添加 MPLS 标签。

pop_mpls:从数据包中移除 MPLS 标签。

  语法pop_mpls:ethertype

  示例:pop_mpls:0x0800 表示移除 MPLS 标签,并将数据包的以太网类型设置为 0x0800(IP 数据包)。

7. Group Action

group 操作用于执行复杂的组操作,通常用于负载均衡和多路径转发。

语法group:group_id

功能:将数据包发送到指定的组(group),每个组可以包含多个输出端口,并支持多种转发模式,如 all(将数据包发送到组中的所有端口)或 select(随机选择一个端口)。

  示例:group:1 表示将数据包提交到组 1 进行处理。

8. Set Queue Action

set_queue 操作用于将数据包发送到特定的队列,通常用于实施 QoS(服务质量)策略。

语法set_queue:queue_id

功能:将数据包标记为特定队列中的成员,不同队列可以有不同的优先级或带宽限制。

  示例:set_queue:3 表示将数据包分配到队列 3。

9. Modifying MAC/IP Address

除了 set_field 操作,OVS 还提供一些特定操作用于修改 MAC 和 IP 地址。

mod_dl_src:修改数据包的源 MAC 地址。

  语法mod_dl_src:mac_address

  示例:mod_dl_src:00:11:22:33:44:55 表示将源 MAC 地址修改为 00:11:22:33:44:55

mod_dl_dst:修改数据包的目标 MAC 地址。

  语法mod_dl_dst:mac_address

  示例:mod_dl_dst:66:77:88:99:AA:BB 表示将目标 MAC 地址修改为 66:77:88:99:AA:BB

mod_nw_src:修改数据包的源 IP 地址。

  语法mod_nw_src:ip_address

  示例:mod_nw_src:192.168.1.1 表示将源 IP 地址修改为 192.168.1.1

mod_nw_dst:修改数据包的目标 IP 地址。

  语法mod_nw_dst:ip_address

  示例:mod_nw_dst:192.168.1.2 表示将目标 IP 地址修改为 192.168.1.2

10. Controller Action

controller 操作用于将数据包发送到控制器处理,常用于 OpenFlow 中。

语法controller

功能:将数据包发送到控制器(如 OpenFlow 控制器),由控制器决定如何处理数据包。

  示例:controller 表示将数据包发送给控制器处理。

11. Normal Action

normal 操作用于将数据包交给 OVS 内置的常规交换机行为处理,即模拟传统的 L2 交换机转发数据包。

语法normal

功能:按照传统交换机的学习规则,查找 MAC 表并转发数据包。适合用在简化的网络配置中,作为一种默认的 L2 处理方式。

  示例:normal 表示数据包按照常规 L2 交换机转发规则处理。

12. Flood Action

flood 操作用于将数据包泛洪到除输入端口外的所有端口。

语法flood

功能:将数据包泛洪到网桥中除了输入端口之外的所有端口。常用于未知 MAC 地址的场景,类似传统 L2 交换机的泛洪行为。

  示例:flood 表示泛洪数据包。

13. Mirror Action

mirror 操作将数据包复制并发送到一个或多个指定的端口,通常用于流量监控或分析。

功能:将数据包副本发送到指定的监控端口,可以同时发送多个副本到不同的端口。

14. Continue Action

continue 操作用于继续执行后续的流表操作,而不会停止当前的流表处理。

功能:允许多个流表操作连续进行,而不在遇到匹配时停止处理。

upcall 消息处理

  1. ovs_dp_upcall()首先调用 err=queue_userspace_packet()将信息排队发到用户空间去

  2. dp_ifindex=get_dpifindex(dp)获取网卡设备索引号

  3. 调整 VLANMAC 地址头指针

  4. 网络链路属性,如果不需要填充则调用此函数

  5. len=upcall_msg_size(),获得 upcall 发送消息的大小

  6. user_skb=genlmsg_new_unicast,创建一个新的 netlink 消息

  7. upcall=genlmsg_put()增加一个新的 netlink 消息到 skb

  8. err=genlmsg_unicast(),发送消息到用户空间去处理

数据包未匹配到flow时将调用ovs_dp_upcall将数据发送到用户空间

int ovs_dp_upcall(struct datapath *dp, struct sk_buff *skb,const struct sw_flow_key *key,const struct dp_upcall_info *upcall_info,uint32_t cutlen)
{struct dp_stats_percpu *stats;int err;if (trace_ovs_dp_upcall_enabled())trace_ovs_dp_upcall(dp, skb, key, upcall_info);// 检测端口id,排除portid为0的包,因为该函数为数据包上报用户空间if (upcall_info->portid == 0) {err = -ENOTCONN;goto err;}if (!skb_is_gso(skb))// 入队上报用户空间err = queue_userspace_packet(dp, skb, key, upcall_info, cutlen);elseerr = queue_gso_packets(dp, skb, key, upcall_info, cutlen);ovs_vport_update_upcall_stats(skb, upcall_info, !err);if (err)goto err;return 0;err:stats = this_cpu_ptr(dp->stats_percpu);u64_stats_update_begin(&stats->syncp);stats->n_lost++;u64_stats_update_end(&stats->syncp);return err;
}

参考文档

https://www.kancloud.cn/digest/openvswitch/117251

https://feisky.gitbooks.io/sdn/content/ovs/internal.html

https://github.com/dcui/sdn-handbook-1/blob/master/ovs/ovn.md

https://www.jianshu.com/p/bf112793d658

https://chentingz.github.io/2019/12/30/%E3%80%8COpenFlow%E3%80%8D%E5%8D%8F%E8%AE%AE%E5%85%A5%E9%97%A8/

https://blog.csdn.net/u014044624/article/details/103911224

https://www.cnblogs.com/oxspirt/p/16435926.html

https://blog.51cto.com/u_15127581/4599785

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/69835.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue3实战-----使用mock模拟接口数据

vue3实战-----使用mock模拟接口数据 1.安装和配置2.创建mock数据3.使用axios测试 1.安装和配置 安装依赖:https://www.npmjs.com/package/vite-plugin-mock pnpm install -D vite-plugin-mock mockjs在 vite.config.js 配置文件启用插件: import { viteMockServe } from vit…

DeepSeek+Excel 效率翻倍

2025年初&#xff0c;DeepSeek以惊人的效率突破技术壁垒&#xff0c;用极低的成本实现了与行业顶尖AI相媲美的性能&#xff0c;瞬间成为全球科技领域的热门话题。 那么AI工具的普及将如何改变我们的工作方式&#xff1f;Excel会被取代吗&#xff1f; 今天&#xff0c;珠珠带你…

Rhel Centos环境开关机自动脚本

Rhel Centos环境开关机自动脚本 1. 业务需求2. 解决方法2.1 rc.local2.2 rc.d2.3 systemd2.4 systemd附着的方法2.5 tuned 3. 测试 1. 业务需求 一台较老的服务器上面业务比较简单,提供一个简单的网站,但已经没有业务的运维人员. 想达到的效果: 由于是非标准的apache或者nginx…

pyside6 中信号有的地方用connect有的用emit为什么

在 PySide6 的 Qt 框架中&#xff0c;connect 和 emit 是信号与槽机制的两个核心操作&#xff0c;但它们的作用完全不同&#xff1a; 1. connect()&#xff1a;建立信号与槽的绑定 作用&#xff1a;将某个信号&#xff08;Signal&#xff09;与一个槽函数&#xff08;Slot&…

React历代主要更新

一、React 16之前更新 React Fiber是16版本之后的一种更新机制&#xff0c;使用链表取代了树&#xff0c;是一种fiber数据结构&#xff0c;其有三个指针&#xff0c;分别指向了父节点、子节点、兄弟节点&#xff0c;当中断的时候会记录下当前的节点&#xff0c;然后继续更新&a…

使用 EDOT 监测由 OpenAI 提供支持的 Python、Node.js 和 Java 应用程序

作者&#xff1a;来自 Elastic Adrian Cole Elastic 很自豪地在我们的 Python、Node.js 和 Java EDOT SDK 中引入了 OpenAI 支持。它们为使用 OpenAI 兼容服务的应用程序添加日志、指标和跟踪&#xff0c;而无需任何代码更改。 介绍 去年&#xff0c;我们宣布了 OpenTelemetry…

RabbitMQ使用guest登录提示:User can only log in via localhost

guest用户默认是无法使用远程访问的&#xff0c;生产环境建议直接在对应服务器登录使用。 1、通过创建新增用户并赋予权限实现远程登录 添加新用户 rabbitmqctl add_user zjp zjp 设置管理员 rabbitmqctl set_user_tags zjp administrator 设置新用户的权限 rabbitmqctl…

Eclipse JSP/Servlet 深入解析

Eclipse JSP/Servlet 深入解析 引言 随着互联网的快速发展,Java Web开发技术逐渐成为企业级应用开发的主流。在Java Web开发中,JSP(JavaServer Pages)和Servlet是两个核心组件,它们共同构成了Java Web应用程序的基础。本文将深入解析Eclipse平台下的JSP/Servlet技术,帮…

【Uniapp】关于实现下拉刷新的三种方式

在小程序、h5等地方中&#xff0c;常常会用到下拉刷新这个功能&#xff0c;今天来讲解实现这个功能的三种方式&#xff1a;全局下拉刷新&#xff0c;组件局部下拉刷新&#xff0c;嵌套组件下拉刷新。 全局下拉刷新 这个方式简单&#xff0c;性能佳&#xff0c;最推荐&#xf…

Redis过期删除与内存淘汰策略面试题剖析

一、谈谈Redis过期删除策略 参考我的这篇博客“二、过期删除策略&内存淘汰策略”部分 高性能分布式缓存Redis-数据管理与性能提升之道_redis 高性能缓存数据库-CSDN博客 二、谈谈Redis内存淘汰策略 参考我的这篇博客“二、过期删除策略&内存淘汰策略”部分 高性能…

基于STM32的学习环境控制系统设计

&#x1f91e;&#x1f91e;大家好&#xff0c;这里是5132单片机毕设设计项目分享&#xff0c;今天给大家分享的是学习环境控制。 设备的详细功能见网盘中的文章《21、基于STM32的学习环境控制系统设计》&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1uWSZX2zbZwy9sY…

Linux中getifaddrs函数

文章目录 **函数原型****参数****返回值****释放资源****`struct ifaddrs` 结构****示例代码****输出示例****相关函数****总结**getifaddrs 是 Linux(以及其他 Unix-like 系统)中用于获取本机网络接口信息的系统调用。它提供了一种简单的方法来获取所有网络接口的地址信息,…

前端知识速记--JS篇:instanceof

前端知识速记–JS篇&#xff1a;instanceof 在JavaScript中&#xff0c;instanceof运算符用于检测一个对象是否是另一个对象的实例。它的基本语法为&#xff1a;obj instanceof Constructor。如果obj是Constructor的实例&#xff0c;它将返回true&#xff0c;否则返回false。这…

数智百问 | 制造企业如何降低产线检测数据的存储和管理成本?

在《“十四五”智能制造发展规划》等政策的推动下&#xff0c;以及新能源汽车、消费电子等品牌商对产品质量和供应商智能化水平要求的提升&#xff0c;半导体、电子制造、动力电池等先进制造行业企业纷纷推进产线智能化升级&#xff0c;并投入大量机器视觉检测设备以实现自动化…

数据科学之数据管理|统计学

使用python学习统计 目录 01 统计学基础 7 一、 统计学介绍 7 二、 数据和变量 8 02 描述统计 10 一、 描述统计概述 10 二、 分类变量的描述 11 三、 等距数值变量的描述 13 四、 等比数值变量的描述 16 五、 常用软件包介绍 16 六、 数值变量的描述统计 18 (一)…

Spring Boot 配置 Mybatis 读写分离

JPA 的读写分离配置不能应用在 Mybatis 上, 所以 Mybatis 要单独处理 为了不影响原有代码, 使用了增加拦截器的方式, 在拦截器里根据 SQL 的 CRUD 来路由到不同的数据源 需要单独增加Mybatis的配置 Configuration public class MyBatisConfig {Beanpublic SqlSessionFactory…

MongoDB 基本操作

一、数据库操作 1. 切换或创建数据库 使用use命令切换到指定数据库&#xff0c;若该数据库不存在&#xff0c;在首次插入数据时会自动创建。 use myDatabase 2. 查看所有数据库 使用show dbs命令查看 MongoDB 实例中的所有数据库。 show dbs 3. 删除当前数据库 使用db.…

网络安全事件分级

对网络安全事件进行必要分级&#xff0c;是做好应急响应工作的前提。网络安全事件分级要统筹考虑诸多因素&#xff0c;直观展示信息安全事件的风险程度&#xff0c;为后续处置工作提供重要参考。 一、网络安全事件的分级要素 对网络安全事件的分级主要考虑3个要素&#xff1a…

三步本地部署deepseekr1,支持macOs,ubuntu,Windows

一、ollama安装: ollama官网:Ollama Ollama 是一款支持在 Windows、macOS 和 Linux 上本地运行大型语言模型的工具。以下是针对不同操作系统的安装指南: 1、Windows 系统 下载安装包:访问 Ollama 官方下载页面,选择适用于 Windows 的安装包进行下载。 运行安装程序:下…

前端开发中处理浮点数精度丢失问题的多种方法

1. 使用 toFixed() 方法 toFixed()是 JavaScript 内置的 Number 对象方法&#xff0c;它会根据指定的小数位数返回一个字符串。这个方法在输出时对结果进行了四舍五入&#xff0c;因此它并不总是能保证数学上的精确性&#xff0c;但它对于展示目的来说通常是足够的。 let num…