Linux内核bridge浅析

Linux网桥模型:

  Linux内核通过一个虚拟的网桥设备来实现桥接的,这个设备可以绑定若干个以太网接口设备,从而将它们桥接起来。如下图所示:

 

网桥设备br0绑定了eth0和eth1。对于网络协议栈的上层来说,只看得到br0,因为桥接是在数据链路层实现的,上层不需要关心桥接的细节。于是协议栈上层需要发送的报文被送到br0,网桥设备的处理代码再来判断报文该被转发到eth0或是eth1,或者两者皆是;反过来,从eth0或从eth1接收到的报文被提交给网桥的处理代码,在这里会判断报文该转发、丢弃、或提交到协议栈上层。

  而有时候eth0、eth1也可能会作为报文的源地址或目的地址,直接参与报文的发送与接收(从而绕过网桥)。

  相关数据结构:

 

其中最左边的net_device是一个代表网桥的虚拟设备结构,它关联了一个net_bridge结构,这是网桥设备所特有的数据结构。

  在net_bridge结构中,port_list成员下挂一个链表,链表中的每一个节点(net_bridge_port结构)关联到一个真实的网口设备的net_device。网口设备也通过其br_port指针做反向的关联(那么显然,一个网口最多只能同时被绑定到一个网桥)。

  net_bridge结构中还维护了一个hash表,是用来处理地址学习的。当网桥准备转发一个报文时,以报文的目的Mac地址为key,如果可以在hash表中索引到一个net_bridge_fdb_entry结构,通过这个结构能找到一个网口设备的net_device,于是报文就应该从这个网口转发出去;否则,报文将从所有网口转发。

  网桥数据包的处理流程:

  接收过程:

  对于数据包的处理流程并没有明显的主线,主要就是根据内核代码中网桥部分的源码进行分析。

  网口设备接收到的报文最终通过net_receive_skb函数被网络协议栈所接收。这个函数主要做三件事情:1、如果有抓包程序需要skb,将skb复制给它们;2、处理桥接;3、将skb提交给网络层。

  int netif_receive_skb(struct sk_buff *skb)

  {

  ......

  if (handle_bridge(&skb, &pt_prev, &ret, orig_dev))

  goto out;

  ......

  }

  static inline struct sk_buff *handle_bridge(struct sk_buff *skb,

  struct packet_type **pt_prev, int *ret,

  struct net_device *orig_dev)

  {

  struct net_bridge_port *port;

  //对于回环设备以及skb->dev->br_port为空(即不被任何网桥所包含)的数据包直接返回

  if (skb->pkt_type == PACKET_LOOPBACK ||

  (port = rcu_dereference(skb->dev->br_port)) == NULL)

  return skb;

  if (*pt_prev) {

  *ret = deliver_skb(skb, *pt_prev, orig_dev);

  *pt_prev = NULL;

  }

  //网桥的基本挂接点处理函数

  return br_handle_frame_hook(port, skb);

  }

  br_handle_frame_hook在网桥初始化模块br_init(void)函数中被赋值.

  br_handle_frame_hook = br_handle_frame;

  所以网桥对于数据包的处理过程是从br_handle_frame开始的。

struct sk_buff *br_handle_frame(struct net_bridge_port *p, struct sk_buff *skb)

  {

  const unsigned char *dest = eth_hdr(skb)->h_dest;

  int (*rhook)(struct sk_buff *skb);

  //判断是否为有效的物理地址,非全0地址以及非广播地址

  if (!is_valid_ether_addr(eth_hdr(skb)->h_source))

  goto drop;

  //判断skb包是否被共享skb->users != 1,若是,则复制一份,否则直接返回

  skb = skb_share_check(skb, GFP_ATOMIC);

  if (!skb)

  return NULL;

  //这个函数并非像想象的那样,判断是否为本地地址

  //而是在判断是否为链路本地多播地址,01:80:c2:00:00:0x

  if (unlikely(is_link_local(dest))) {

  /* Pause frames shouldn't be passed up by driver anyway */

  if (skb->protocol == htons(ETH_P_PAUSE))

  goto drop;

  /* If STP is turned off, then forward */

  if (p->br->stp_enabled == BR_NO_STP && dest[5] == 0)

  goto forward;

  if (NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, skb->dev,

  NULL, br_handle_local_finish))

  return NULL;  /* frame consumed by filter */

  else

  return skb;      /* continue processing */

  }

  forward:

  switch (p->state) {

  case BR_STATE_FORWARDING:

  //如果网桥处于forwarding状态,并且该报文必须要走L3层进行转发,则直接返回

  //br_should_route_hook钩子函数在ebtable里面设置为ebt_broute函数,它根据用户的规

  //决定该报文是否要通过L3层来转发;一般rhook为空

  rhook = rcu_dereference(br_should_route_hook);

  if (rhook != NULL) {

  if (rhook(skb))

  return skb;

  dest = eth_hdr(skb)->h_dest;

  }

  /* fall through */

  case BR_STATE_LEARNING:

  //如果数据包的目的mac地址为虚拟网桥设备的mac地址,则标记为host

  if (!compare_ether_addr(p->br->dev->dev_addr, dest))

  skb->pkt_type = PACKET_HOST;

  //调用网桥在NF_BR_PREROUTING处挂载的钩子函数,因为网桥在其钩子函数过//程中嵌套调用了INET层BR_PREROUTING的钩子函数,过程有些曲折,故最后//再分析

  NF_HOOK(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,

  br_handle_frame_finish);

  break;

  default:

  drop:

  kfree_skb(skb);

  }

  return NULL;

  }

  FORWARDING以及LEARNING为网桥的状态,网桥端口一般有5种状态:

  1)  disable 被管理员禁用

  2)  blcok 休息,不参与数据包转发

  3)  listening 监听

  4)  learning 学习ARP信息,准备向工作状态改变

  5)  forwarding 正常工作,转发数据包

  /* note: already called with rcu_read_lock (preempt_disabled) */

  int br_handle_frame_finish(struct sk_buff *skb)

  {

  const unsigned char *dest = eth_hdr(skb)->h_dest;

  struct net_bridge_port *p = rcu_dereference(skb->dev->br_port);

  struct net_bridge *br;

  struct net_bridge_fdb_entry *dst;

  struct sk_buff *skb2;

  //判断网桥状态

  if (!p || p->state == BR_STATE_DISABLED)

  goto drop;

  /* insert into forwarding database after filtering to avoid spoofing */

  //br为虚拟网桥结构

  br = p->br;

//根据数据包的源物理地址,更新网桥的转发表

  br_fdb_update(br, p, eth_hdr(skb)->h_source);

  if (p->state == BR_STATE_LEARNING)

  goto drop;

  /* The packet skb2 goes to the local host (NULL to skip). */

  //skb2数据包用于交付本机,skb数据包则用于forward

  skb2 = NULL;

  //如果网口处于混杂模式,复制一份交付主机

  if (br->dev->flags & IFF_PROMISC)

  skb2 = skb;

  dst = NULL;

  //如果为广播数据包,增加计数,同样需要发一份给主机

  if (is_multicast_ether_addr(dest)) {

  br->dev->stats.multicast++;

  skb2 = skb;

  }

  /*根据网桥口以及目标地址判断是否为本机数据包*/

  else if ((dst = __br_fdb_get(br, dest)) && dst->is_local) {

  skb2 = skb;

  /* Do not forward the packet since it's local. */

  skb = NULL;

  }

  if (skb2 == skb)

  skb2 = skb_clone(skb, GFP_ATOMIC);

  if (skb2)

  /*完成将数据包交付给本机的工作*/

  br_pass_frame_up(br, skb2);

  if (skb) {

  if (dst)

  //如果存在目的地址则将其转发

  br_forward(dst->dst, skb);

  else

  //否则,flood数据包,向除接收网口外的其余网口发送该数据包

  br_flood_forward(br, skb);

  }

  out:

  return 0;

  drop:

  kfree_skb(skb);

  goto out;

  }

  //此函数主要实现通过网桥接收发往本机的数据包

  static void br_pass_frame_up(struct net_bridge *br, struct sk_buff *skb)

  {

  struct net_device *indev, *brdev = br->dev;

  //完成数据包的统计计数

  brdev->stats.rx_packets++;

  brdev->stats.rx_bytes += skb->len;

  //将skb的dev改变为网桥结构的brdev

  //此时skb的dev选项由实际网络设备eth0等改变为虚拟网桥设备br0

  indev = skb->dev;

  skb->dev = brdev;

  //重新走数据包接收流程,netif_receive_skb

  //但因为dev的改变,dev的br_port字段不再为空,不会重走网桥流程,直接交付

  NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL,

  netif_receive_skb);

  }

  下面看一下转发数据包的流程,对于flood_forward的流程,同样通过br_forward来实现,只不过改为循环遍历hash表中的设备,对于每一个设备都调用一次br_forward流程。

  /* called with rcu_read_lock */

  void br_forward(const struct net_bridge_port *to, struct sk_buff *skb)

  {

  /*如果skb->dev 不等于网桥的dev,同时网桥状态为forwarding,则进行转发*/

  if (should_deliver(to, skb)) {

  __br_forward(to, skb);

  return;

  }

  kfree_skb(skb);

  }

  static void __br_forward(const struct net_bridge_port *to, struct sk_buff *skb)

  {

  struct net_device *indev;

  if (skb_warn_if_lro(skb)) {

  kfree_skb(skb);

  return;

  }

  //将skb的dev字段改为查找到的出口dev字段

  indev = skb->dev;

  skb->dev = to->dev;

  skb_forward_csum(skb);

  //遍历执行NF_BR_FORWARD钩子函数

  NF_HOOK(PF_BRIDGE, NF_BR_FORWARD, skb, indev, skb->dev,

  br_forward_finish);

  }

  int br_forward_finish(struct sk_buff *skb)

  {

  //继续跑NF_BR_POST_ROUTING处的钩子函数

  return NF_HOOK(PF_BRIDGE, NF_BR_POST_ROUTING, skb, NULL, skb->dev,

  br_dev_queue_push_xmit);

  }

int br_dev_queue_push_xmit(struct sk_buff *skb)

  {

  /* drop mtu oversized packets except gso */

  /*如果skb数据包的长度大于MTU值,则丢弃*/

  if (packet_length(skb) > skb->dev->mtu && !skb_is_gso(skb))

  kfree_skb(skb);

  else {

  /* ip_refrag calls ip_fragment, doesn't copy the MAC header. */

  if (nf_bridge_maybe_copy_header(skb))

  kfree_skb(skb);

  else {

  skb_push(skb, ETH_HLEN);

  // 此时skb的dev已经替换成进行转发的dev了,dev_queue_xmit将使

  //用该网口设备的发送函数完成数据包的发送

  dev_queue_xmit(skb);

  }

  }

  return 0;

  }

  发送过程:

  协议栈上层需要发送报文时,调用dev_queue_xmit(skb)函数。如果这个报文需要通过网桥设备来发送,则skb->dev指向一个网桥设备。网桥设备没有使用发送队列(dev->qdisc为空),所以dev_queue_xmit将直接调用dev->hard_start_xmit函数,而网桥设备的hard_start_xmit等于函数br_dev_xmit;

  /* net device transmit always called with no BH (preempt_disabled) */

  /*br_dev_xmit为网桥设备的数据包发送函数*/

  int br_dev_xmit(struct sk_buff *skb, struct net_device *dev)

  {

  struct net_bridge *br = netdev_priv(dev);

  const unsigned char *dest = skb->data;

  struct net_bridge_fdb_entry *dst;

  dev->stats.tx_packets++;

  dev->stats.tx_bytes += skb->len;

  skb_reset_mac_header(skb);

  skb_pull(skb, ETH_HLEN);

  /*如果为广播地址,则flood该数据包

  * 如果能够根据skb中的目的mac地址查找到对应的网口,则通过br_deliver发送该数据包

  * 如果查找不到,同样flood该数据包

  */

  if (dest[0] & 1)

  br_flood_deliver(br, skb);

  else if ((dst = __br_fdb_get(br, dest)) != NULL)

  br_deliver(dst->dst, skb);

  else

  br_flood_deliver(br, skb);

  return 0;

  }

  br_flood_deliver函数的实现过程,同样是遍历hash表,对于每一个网口设备都调用一次__br_deliver,所以下面就主要看一下br_deliver函数的流程

  void br_deliver(const struct net_bridge_port *to, struct sk_buff *skb)

  {

  if (should_deliver(to, skb)) {

  __br_deliver(to, skb);

  return;

  }

  kfree_skb(skb);

  }

  static void __br_deliver(const struct net_bridge_port *to, struct sk_buff *skb)

  {

  /*将skb中的dev改成出口设备所对应的dev*/

  skb->dev = to->dev;

  NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_OUT, skb, NULL, skb->dev,

  br_forward_finish);

  }

  /*最终仍然通过br_dev_queue_push_xmit完成数据包的发送过程*/

  int br_forward_finish(struct sk_buff *skb)

  {

  return NF_HOOK(PF_BRIDGE, NF_BR_POST_ROUTING, skb, NULL, skb->dev,

  br_dev_queue_push_xmit);

  }

  至此,整个网桥中数据的处理流程已经完全结束了。

  Netfilter:

  对于网桥中的netfilter的钩子函数的调度过程有些曲折,对于INET层的钩子函数全部被嵌套进BRIDGE层钩子函数的运行流程中。

  下面首先来看一下网桥一共挂载了哪些钩子函数

  static struct nf_hook_ops br_nf_ops[] __read_mostly = {

  { .hook = br_nf_pre_routing,

  .owner = THIS_MODULE,

  .pf = PF_BRIDGE,

  .hooknum = NF_BR_PRE_ROUTING,

  .priority = NF_BR_PRI_BRNF, },

  { .hook = br_nf_local_in,

  .owner = THIS_MODULE,

  .pf = PF_BRIDGE,

  .hooknum = NF_BR_LOCAL_IN,

  .priority = NF_BR_PRI_BRNF, },

  { .hook = br_nf_forward_ip,

  .owner = THIS_MODULE,

  .pf = PF_BRIDGE,

  .hooknum = NF_BR_FORWARD,

  .priority = NF_BR_PRI_BRNF - 1, },

{ .hook = br_nf_forward_arp,

  .owner = THIS_MODULE,

  .pf = PF_BRIDGE,

  .hooknum = NF_BR_FORWARD,

  .priority = NF_BR_PRI_BRNF, },

  { .hook = br_nf_local_out,

  .owner = THIS_MODULE,

  .pf = PF_BRIDGE,

  .hooknum = NF_BR_LOCAL_OUT,

  .priority = NF_BR_PRI_FIRST, },

  { .hook = br_nf_post_routing,

  .owner = THIS_MODULE,

  .pf = PF_BRIDGE,

  .hooknum = NF_BR_POST_ROUTING,

  .priority = NF_BR_PRI_LAST, },

  //以上为BRIDGE层挂载的钩子函数,一下为INET层挂载的钩子函数

  { .hook = ip_sabotage_in,

  .owner = THIS_MODULE,

  .pf = PF_INET,

  .hooknum = NF_INET_PRE_ROUTING,

  .priority = NF_IP_PRI_FIRST, },

  { .hook = ip_sabotage_in,

  .owner = THIS_MODULE,

  .pf = PF_INET6,

  .hooknum = NF_INET_PRE_ROUTING,

  .priority = NF_IP6_PRI_FIRST, },

  };

  其实在这些钩子函数内部并没有涉及数据包操作的具体流程,大多是一些条件判断和验证的工作,现在就拿发往本机的数据包在作为一个例子,看一下这个数据包在netfilter中的流经过程(自认为还算有代表意义)。

  对于发往本机网桥口的skb包,其skb->dev->br_port不为空,因此会进入网桥的处理流程,br_handler_frame,在此函数的退出处会首先运行NF_BR_PRE_ROUTING的钩子函数

  NF_HOOK(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,

  br_handle_frame_finish);

  网桥挂载在BR_PRE_ROUTING的函数为br_nf_pre_routing

  在此函数内部首先完成ipv4及ipv6数据的分流,这里只考虑ipv4数据,接着进行ipv4数据正确性的验证,以及网桥标记nf_bridge->mask |= BRNF_NF_BRIDGE_PREROUTING;(下面还要用到此标记)在函数的最后通过NF_HOOK(PF_INET, NF_INET_PRE_ROUTING, skb, skb->dev, NULL,br_nf_pre_routing_finish)完成对于INET层PRE_ROUTING钩子函数的遍历。

  遍历结束后进入br_nf_pre_routing_finish函数,这个函数首先对数据包打标记,nf_bridge->mask ^= BRNF_NF_BRIDGE_PREROUTING;(跟上一个标记对应)主要处理skb包需要进行dnat的情形(貌似,没有细看),在函数的最后

  NF_HOOK_THRESH(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,

  br_handle_frame_finish, 1);

  在这里通过NF_HOOK_THRESH而非NF_HOOK的意思是因为,NF_HOOK_THRESH是从当前优先级的下一个函数开始遍历所有的钩子函数,在这里也就是说从br_nf_pre_routing函数的下一个优先级函数开始遍历BR_PRE_ROUTING的后续函数,虽然目前网桥只挂载了这一个函数,但是这属于Netfilter的机制问题。

  接着,在跑完BR_PRE_ROUTING的剩余函数后就进入br_handle_frame_finish函数,对于本机数据包则继续调用br_pass_frame_up函数,在br_pass_frame_up函数中将skb的dev字段修改为虚拟网桥的dev,最后NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL,      netif_receive_skb);将数据包重新跑skb的接收流程,由于网桥设备的dev->br_port不为空,因此不会再进入网桥处理流程了。

  那网桥是怎样防止同一个skb包跑两次INET_PRE_ROUTING的流程呢,主要就是通过挂在在INET_PRE_ROUTING出优先级最高的NF_IP_PRI_FIRST处的函数ip_sabotage_in以及上文特殊提到的两个打标记的代码来完成的。

  static unsigned int ip_sabotage_in(unsigned int hook, struct sk_buff *skb,

  const struct net_device *in,

  const struct net_device *out,

  int (*okfn)(struct sk_buff *))

  {

  /*此钩子函数用于确保发往本机的skb包只跑一次INET_PRE_ROUTING的钩子函数

  * 在第一次进入BR_PRE_ROUTING时打上标记mask,在BR_PRE_ROUTING的钩子函数中遍历INET_PRE_ROUTING的钩子函数

  * 在br_pre_routing结束的时候,异或该mask值,这样就使得发往本机的skb包在第二次交付的时候可以不再跑钩子

  */

  if (skb->nf_bridge &&

  !(skb->nf_bridge->mask & BRNF_NF_BRIDGE_PREROUTING)) {

  return NF_STOP;

  }

  return NF_ACCEPT;

  }

  NF_STOP的意思就是说该skb包就立即结束检查而接受,不再进入链表中后续的hook节点。

  整个网桥模块大体就是这个样子,剩下的还有网桥的生成树协议,这周打算把这个搞懂。。。

  第一次写分析,自己给自己加油,继续努力~

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

富文本CKEditor5简易入门,包括自定义上传图片(html版+vue.js)

一、安装及引入 官网&#xff1a;https://ckeditor.com/ckeditor-5/download/ 我这边使用的是自定义构建&#xff0c;然后下载下来。 二、简单使用 引入js <script src"../../../assets/plugins/ckeditor5/ckeditor.js"></script>html&#xff1a;…

能直接运营的校园跑腿代买拿寄取小程序开发

​说到做跑腿生意&#xff0c;除了做同城跑腿配送外&#xff0c;校园跑腿可是即成本又低又好操作的一个项目。 一般省会城市大学大专基本都是有好几所的&#xff0c;学校的特点是人员密集&#xff0c;跑腿配送周期短&#xff0c;且配送人员好招募&#xff0c;推广人员好招募。…

【Postman】- 基本用法

一、用例集 1.1 用例集 Collections&#xff1a;用例集。目录下可以创建子目录。 1.2 导出用例集 1.3 导入用例集 二、Postman断言 断言&#xff1a;让程序判断预期结果和实际结果是否一致 2.1 特点 Postman的断言是使用JavaScript语言编写的&#xff0c;写在"Tests&…

智慧导诊系统源码:基于springboot+redis+mybatis plus和mysql开发

智慧导诊系统源码 智慧导诊小程序源码&#xff0c;智慧导诊APP源码 人们经常去医院以不知道挂什么科而烦恼&#xff0c;有些病人不方便问又不好意思问。在互联网医院中挂号且又不知该挂什么科&#xff0c;找什么类型的医生&#xff0c;这些不足&#xff0c;给患者带来了极大的…

Ubuntu下打开QtCreator环境变量LD_LIBRARY_PATH与终端不一致

问题描述&#xff1a; 在unbuntu下使用QtCreator编译、运行程序时&#xff0c;总是出现XXX.so: cannot open shared object file: No such file or directory这类问题&#xff0c;但是在终端中编译或者运行程序则不会出现这些问题。在网上查了好久才明白QtCreator在打开时&…

CSS3 Flexbox

Flex 是 Flexible Box 的缩写&#xff0c;意为弹性盒子布局。 CSS3中一种新的布局模式&#xff1a;W3C在2009年提出的一种布局方案&#xff0c;一种当页面需要适应不同的屏幕大小以及设备类型时确保元素拥有恰当的行为的布局方式。其目的是提供一种更加有效的方式来对一个容器…

怎么学习JavaWeb开发? - 易智编译EaseEditing

学习JavaWeb开发可以按照以下步骤进行&#xff1a; 掌握Java基础&#xff1a; 在学习JavaWeb开发之前&#xff0c;确保你对Java编程语言有一定的掌握&#xff0c;包括面向对象编程、基本语法、数据类型、流程控制等。 学习HTML、CSS和JavaScript&#xff1a; JavaWeb开发主要…

# jellyfin安装设置使用散记

jellyfin安装设置使用散记 文章目录 jellyfin安装设置使用散记0 软件简介1 安装2 视频转码问题2.1 局域网转码情况测试&#xff08;不同网段&#xff09;2.2 局域网jellyfin app默认转码问题解决2.3 外网转码情况测试 3 一些坑4 插件5 最后 0 软件简介 Jellyfin 是一个自由的软…

专项练习-04编程语言-03JAVA-01

1. 以下有关构造方法的说法&#xff0c;正确的是&#xff1a;&#xff08;&#xff09; A 一个类的构造方法可以有多个 B 构造方法在类定义时被调用 C 构造方法只能由对象中的其他方法调用 D 构造方法可以和类同名&#xff0c;也可以和类名不同 正确答案&#xff1a;A 官方解析…

智能井盖:科技赋能城市脚下安全

在智能化飞速发展的今天&#xff0c;智能井盖作为城市基础设施的一部分&#xff0c;正逐渐走进人们的视野。它利用现代科技手段&#xff0c;实现了对城市井盖的实时监控、及时响应和高效管理&#xff0c;为城市管理、市民出行等方面带来了诸多便利。 城市中井盖数量庞大&#x…

【ribbon】Ribbon的使用与原理

负载均衡介绍 负载均衡&#xff08;Load Balance&#xff09;&#xff0c;其含义就是指将负载&#xff08;工作任务&#xff09;进行平衡、分摊到多个操作单元上进行运行&#xff0c;例如FTP服务器、Web服务器、企业核心应用服务器和其它主要任务服务器等&#xff0c;从而协同…

[Cotex-M3学习教程]-0.1-Cortex-M3概述

目录 1 Cortex-M3概述 1.1 ARM 处理器 1.2 cortex-M3介绍 1.3 cortex-M3结构概览图 1.4 cortex-M3组件 1.4.1 内核系统 1.4.2 NVIC 1.4.3 寄存器组 控制寄存器&#xff08;CONTROL&#xff09; 程序计数寄存器&#xff08;PC:R15&#xff09; 堆栈指针寄存器&#xf…

Raki的读paper小记:LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS

Abstract&Introduction&Related Work 研究任务 对大模型进行部分微调 已有方法和相关工作 现有技术通常通过扩展模型深度引入推理延迟&#xff08;Houlsby 等人&#xff0c;2019&#xff1b;Rebuffi 等人&#xff0c;2017&#xff09;&#xff0c;或通过减少模型可用序…

28.JavaWeb-Elasticsearch

1.Elasticsearch概述 Elasticsearch 是一个分布式的全文检索引擎。采用Java语言开发&#xff0c;基于Apache协议的开源项目&#xff0c;具有实时搜索&#xff0c;稳定&#xff0c;可靠&#xff0c;快速的特点。 1.1 全文检索引擎 分为通用搜索引擎&#xff08;百度、谷歌&…

基于Java+SpringBoot+vue前后端分离在线商城系统设计实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

java项目之人事管理系统(ssm+mysql+jsp)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于ssm的人事管理系统。技术交流和部署相关看文章末尾&#xff01; 开发环境&#xff1a; 后端&#xff1a; 开发语言&#xff1a;Java 框架&…

【MySQL进阶(一)】MySQL在Linux中的配置信息和数据备份工具

MySQL在Linux中安装的话可以看这篇博客&#xff1a;MySQL在Linux中的安装&#xff0c;我觉得总结的很好。 my.cnf 中的配置信息 当 MySQL 启动的时候&#xff0c;会从安装目录中加载软件数据&#xff0c;即使用 mysqld 工具初始化设置的 --basedir&#xff0c;会从数据目录中…

【iOS】—— block,KVC,KVO,Category等问题解答

文章目录 block1.block的原理是怎样的&#xff1f;本质是什么&#xff1f;2.__block的作用是什么&#xff1f;有什么使用注意点&#xff1f;3.block的属性修饰词为什么是copy&#xff1f;使用block有哪些使用注意&#xff1f;4.block在修改NSMutableArray&#xff0c;需不需要添…

不止工具:音视频开发「利器」的新机遇

Boxing的制胜关键是快、准、稳&#xff0c;与“音视频开发”有异曲同工之妙。 数字化浪潮席卷、视频化形态加速、终端性能挑战加剧、端侧算力遭遇瓶颈...... 是否存在一种可能性&#xff0c;让所有企业从复杂的音视频开发工程中抽身&#xff0c;重新回归业务本身&#xff1f; …

消息队列总结(3)- RabbitMQ Kafka RocketMQ高可用方案

目录 1. 什么是高可用&#xff1f; 1.1 常见的高可用方法 1.2 消息队列的高可用 2. RabbitMQ的高可用方案 2.1 镜像队列 2.2 消息生产的确认机制 2.3 消息的持久化 3. Kafka的高可用方案 3.1 消息备份 3.2 ISR & IEO & HW 3.3 消息生产的确认机制 4. Rocke…