LwIP系列--软件定时器(超时处理)详解

news2025/1/8 22:43:24

一、目的

在TCP/IP协议栈中ARP缓存的更新、IP数据包的重组、TCP的连接超时和超时重传等都需要超时处理模块(软件定时器)的参与。

本篇主要介绍LwIP中超时处理的实现细节。

上图为超时定时器链表,升序排序,其中next_timeout为链表头,指向超时定时器中的第一个定时器。

二、介绍

在介绍超时处理之前,我们先了解一下LwIP中与超时处理相关的数据结构

涉及的文件

timeouts.h
timeouts.c

相关宏

/** Returned by sys_timeouts_sleeptime() to indicate there is no timer, so we
 * can sleep forever.
 */
#define SYS_TIMEOUTS_SLEEPTIME_INFINITE 0xFFFFFFFF

用于指示当前没有超时定时器链表为空


超时定时器实现

/** Function prototype for a timeout callback function. Register such a function
 * using sys_timeout().
 *
 * @param arg Additional argument to pass to the function - set up by sys_timeout()
 */
typedef void (* sys_timeout_handler)(void *arg);

struct sys_timeo {
  struct sys_timeo *next;
  u32_t time;
  sys_timeout_handler h;
  void *arg;
#if LWIP_DEBUG_TIMERNAMES
  const char* handler_name;
#endif /* LWIP_DEBUG_TIMERNAMES */
};

各个字段含义:

next:下一个定时器指针

time:超时时间或者说是超时时刻,注意此处是绝对时间(即在此时刻超时事件发生)

h:超时回调函数

arg:回调函数入参

handler_name:超时事件描述(定义LWIP_DEBUG_TIMERNAMES宏时才定义)

因为LwIP内核中有各种超时事件,为了统一管理这些超时事件,故将各个超时事件定时器通过单链表的形式管理起来。

/** The one and only timeout list */
static struct sys_timeo *next_timeout;

定义超时定时器的链表头部

定时机制

一般实现定时器时我们可以使用相对时间或者绝对时间来表示;LwIP中的超时机制采用绝对时间的方式实现。

系统中有个tick计数器,例如每1ms,tick值增加一,当tick达到最大值时,重新从0开始计数。

假设用于记录绝对时间的数据类型为uint8_t,从0开始计数一直到255,然后再次从0开始。

假设当前时刻为255,那么如果设置定时时间12,那么255 + 12无符号数相加后的值为12,也就是超时的绝对时刻为12,当系统时间运行到12时,代表时间超时,此时需要处理超时函数。

创建超时定时器

void
sys_timeout_debug(u32_t msecs, sys_timeout_handler handler, void *arg, const char *handler_name)
#else /* LWIP_DEBUG_TIMERNAMES */
void
sys_timeout(u32_t msecs, sys_timeout_handler handler, void *arg)
#endif /* LWIP_DEBUG_TIMERNAMES */
{
  u32_t next_timeout_time;

  LWIP_ASSERT_CORE_LOCKED();  //①

  LWIP_ASSERT("Timeout time too long, max is LWIP_UINT32_MAX/4 msecs", msecs <= (LWIP_UINT32_MAX / 4));

  next_timeout_time = (u32_t)(sys_now() + msecs); /* overflow handled by TIME_LESS_THAN macro */  //②

#if LWIP_DEBUG_TIMERNAMES
  sys_timeout_abs(next_timeout_time, handler, arg, handler_name);
#else
  sys_timeout_abs(next_timeout_time, handler, arg);  //③
#endif
}

①:检查互斥锁是否上锁,避免多线程问题

②:计算超时绝对时间

③:通过sys_timeout_abs函数创建定时器

注意:超时时间不能超过LWIP_UINT32_MAX/4毫秒

#if LWIP_DEBUG_TIMERNAMES
sys_timeout_abs(u32_t abs_time, sys_timeout_handler handler, void *arg, const char *handler_name)
#else /* LWIP_DEBUG_TIMERNAMES */
sys_timeout_abs(u32_t abs_time, sys_timeout_handler handler, void *arg)
#endif
{
  struct sys_timeo *timeout, *t;

  timeout = (struct sys_timeo *)memp_malloc(MEMP_SYS_TIMEOUT);
  if (timeout == NULL) {
    LWIP_ASSERT("sys_timeout: timeout != NULL, pool MEMP_SYS_TIMEOUT is empty", timeout != NULL);
    return;
  }  //①

  timeout->next = NULL;
  timeout->h = handler;
  timeout->arg = arg;
  timeout->time = abs_time;   //②

#if LWIP_DEBUG_TIMERNAMES
  timeout->handler_name = handler_name;
  LWIP_DEBUGF(TIMERS_DEBUG, ("sys_timeout: %p abs_time=%"U32_F" handler=%s arg=%p\n",
                             (void *)timeout, abs_time, handler_name, (void *)arg));
#endif /* LWIP_DEBUG_TIMERNAMES */

  if (next_timeout == NULL) {  //③
    next_timeout = timeout;
    return;
  }
  if (TIME_LESS_THAN(timeout->time, next_timeout->time)) { //④
    timeout->next = next_timeout;
    next_timeout = timeout;
  } else {
    for (t = next_timeout; t != NULL; t = t->next) { //⑤
      if ((t->next == NULL) || TIME_LESS_THAN(timeout->time, t->next->time)) {
        timeout->next = t->next;  //⑥
        t->next = timeout;
        break;
      }
    }
  }
}

①:从MEMP_SYS_TIMEOUT内存池中分配内存池结构

②:设置各个参数

③:超时链表如果为空,代表当前是第一个定时器

④:如果当前新创建的定时器超时时间比链表头部的第一个时间小,则将新创建的定时器放在头部,并更新next_timeout指针

⑤:遍历定时器列表

⑥:将新创建的定时器插入在正确的位置上

采用升序进行排序

删除定时器

/**
 * Go through timeout list (for this task only) and remove the first matching
 * entry (subsequent entries remain untouched), even though the timeout has not
 * triggered yet.
 *
 * @param handler callback function that would be called by the timeout
 * @param arg callback argument that would be passed to handler
*/
void
sys_untimeout(sys_timeout_handler handler, void *arg)
{
  struct sys_timeo *prev_t, *t;

  LWIP_ASSERT_CORE_LOCKED();  //①

  if (next_timeout == NULL) {
    return;
  }

  for (t = next_timeout, prev_t = NULL; t != NULL; prev_t = t, t = t->next) { //②
    if ((t->h == handler) && (t->arg == arg)) {
      /* We have a match */
      /* Unlink from previous in list */
      if (prev_t == NULL) {
        next_timeout = t->next;
      } else {
        prev_t->next = t->next;
      }
      memp_free(MEMP_SYS_TIMEOUT, t); //③
      return;
    }
  }
  return;
}

①:检查互斥锁是否上锁,避免多线程问题

②:遍历超时链表找到匹配的定时器

③:将定时器占用的内存释放

周期定时器

在LwIP协议栈内部有些模块需要周期性定时,但是sys_timeo只是一个单次定时结构,为了实现周期性定时的功能,在此基础上定义了周期定时器

/** Function prototype for a stack-internal timer function that has to be
 * called at a defined interval */
typedef void (* lwip_cyclic_timer_handler)(void);

/** This struct contains information about a stack-internal timer function
 that has to be called at a defined interval */
struct lwip_cyclic_timer {
  u32_t interval_ms;
  lwip_cyclic_timer_handler handler;
#if LWIP_DEBUG_TIMERNAMES
  const char* handler_name;
#endif /* LWIP_DEBUG_TIMERNAMES */
};    

各个字段含义:

interval_ms:定时周期,单位ms

handler:回调函数

handler_name:超时事件描述(定义LWIP_DEBUG_TIMERNAMES宏时才定义)

系统定义的周期定时器列表

/** This array contains all stack-internal cyclic timers. To get the number of
 * timers, use LWIP_ARRAYSIZE() */
const struct lwip_cyclic_timer lwip_cyclic_timers[] = {
#if LWIP_TCP
  /* The TCP timer is a special case: it does not have to run always and
     is triggered to start from TCP using tcp_timer_needed() */
  {TCP_TMR_INTERVAL, HANDLER(tcp_tmr)},
#endif /* LWIP_TCP */
#if LWIP_IPV4
#if IP_REASSEMBLY
  {IP_TMR_INTERVAL, HANDLER(ip_reass_tmr)},
#endif /* IP_REASSEMBLY */
#if LWIP_ARP
  {ARP_TMR_INTERVAL, HANDLER(etharp_tmr)},
#endif /* LWIP_ARP */
#if LWIP_DHCP
  {DHCP_COARSE_TIMER_MSECS, HANDLER(dhcp_coarse_tmr)},
  {DHCP_FINE_TIMER_MSECS, HANDLER(dhcp_fine_tmr)},
#endif /* LWIP_DHCP */
#if LWIP_ACD
  {ACD_TMR_INTERVAL, HANDLER(acd_tmr)},
#endif /* LWIP_ACD */
#if LWIP_IGMP
  {IGMP_TMR_INTERVAL, HANDLER(igmp_tmr)},
#endif /* LWIP_IGMP */
#endif /* LWIP_IPV4 */
#if LWIP_DNS
  {DNS_TMR_INTERVAL, HANDLER(dns_tmr)},
#endif /* LWIP_DNS */
#if LWIP_IPV6
  {ND6_TMR_INTERVAL, HANDLER(nd6_tmr)},
#if LWIP_IPV6_REASS
  {IP6_REASS_TMR_INTERVAL, HANDLER(ip6_reass_tmr)},
#endif /* LWIP_IPV6_REASS */
#if LWIP_IPV6_MLD
  {MLD6_TMR_INTERVAL, HANDLER(mld6_tmr)},
#endif /* LWIP_IPV6_MLD */
#if LWIP_IPV6_DHCP6
  {DHCP6_TIMER_MSECS, HANDLER(dhcp6_tmr)},
#endif /* LWIP_IPV6_DHCP6 */
#endif /* LWIP_IPV6 */
};
const int lwip_num_cyclic_timers = LWIP_ARRAYSIZE(lwip_cyclic_timers);

内部周期定时器组的初始化

/** Initialize this module */
void sys_timeouts_init(void)
{
  size_t i;
  /* tcp_tmr() at index 0 is started on demand */
  for (i = (LWIP_TCP ? 1 : 0); i < LWIP_ARRAYSIZE(lwip_cyclic_timers); i++) {
    /* we have to cast via size_t to get rid of const warning
      (this is OK as cyclic_timer() casts back to const* */
    sys_timeout(lwip_cyclic_timers[i].interval_ms, lwip_cyclic_timer, LWIP_CONST_CAST(void *, &lwip_cyclic_timers[i]));
  }
}

周期定时器通过回调函数lwip_cyclic_timer将定时器再次插入到定时器列表中

void
lwip_cyclic_timer(void *arg)
{
  u32_t now;
  u32_t next_timeout_time;
  const struct lwip_cyclic_timer *cyclic = (const struct lwip_cyclic_timer *)arg;

#if LWIP_DEBUG_TIMERNAMES
  LWIP_DEBUGF(TIMERS_DEBUG, ("tcpip: %s()\n", cyclic->handler_name));
#endif
  cyclic->handler();

  now = sys_now();
  next_timeout_time = (u32_t)(current_timeout_due_time + cyclic->interval_ms);  /* overflow handled by TIME_LESS_THAN macro */
  if (TIME_LESS_THAN(next_timeout_time, now)) {
    /* timer would immediately expire again -> "overload" -> restart without any correction */
#if LWIP_DEBUG_TIMERNAMES
    sys_timeout_abs((u32_t)(now + cyclic->interval_ms), lwip_cyclic_timer, arg, cyclic->handler_name);
#else
    sys_timeout_abs((u32_t)(now + cyclic->interval_ms), lwip_cyclic_timer, arg);
#endif

  } else {
    /* correct cyclic interval with handler execution delay and sys_check_timeouts jitter */
#if LWIP_DEBUG_TIMERNAMES
    sys_timeout_abs(next_timeout_time, lwip_cyclic_timer, arg, cyclic->handler_name);
#else
    sys_timeout_abs(next_timeout_time, lwip_cyclic_timer, arg);
#endif
  }
}

定时器的触发(超时检查)

/** Return the time left before the next timeout is due. If no timeouts are
 * enqueued, returns 0xffffffff
 */
u32_t
sys_timeouts_sleeptime(void)
{
  u32_t now;

  LWIP_ASSERT_CORE_LOCKED();  //①

  if (next_timeout == NULL) { //②
    return SYS_TIMEOUTS_SLEEPTIME_INFINITE;
  }
  now = sys_now();
  if (TIME_LESS_THAN(next_timeout->time, now)) { //③
    return 0;
  } else {
    u32_t ret = (u32_t)(next_timeout->time - now);
    LWIP_ASSERT("invalid sleeptime", ret <= LWIP_MAX_TIMEOUT);
    return ret;
  }
}

①:检查互斥锁是否上锁,避免多线程问题

②:检查是否有定时器存在,没有定时器存在则返回SYS_TIMEOUTS_SLEEPTIME_INFINITE

③:检查下一次超时时间,如果已经超时返回0;否则超时的差值(比如当前时刻为20,超时时刻为25,那么距离超时的值为5)

static void
tcpip_timeouts_mbox_fetch(sys_mbox_t *mbox, void **msg)
{
  u32_t sleeptime, res;

again:
  LWIP_ASSERT_CORE_LOCKED(); //①

  sleeptime = sys_timeouts_sleeptime();  //②
  if (sleeptime == SYS_TIMEOUTS_SLEEPTIME_INFINITE) {
    UNLOCK_TCPIP_CORE();
    sys_arch_mbox_fetch(mbox, msg, 0); //③
    LOCK_TCPIP_CORE();
    return;
  } else if (sleeptime == 0) {
    sys_check_timeouts();  //④
    /* We try again to fetch a message from the mbox. */
    goto again;
  }

  UNLOCK_TCPIP_CORE();
  res = sys_arch_mbox_fetch(mbox, msg, sleeptime); //⑤
  LOCK_TCPIP_CORE();
  if (res == SYS_ARCH_TIMEOUT) {
    /* If a SYS_ARCH_TIMEOUT value is returned, a timeout occurred
       before a message could be fetched. */
    sys_check_timeouts(); //⑥
    /* We try again to fetch a message from the mbox. */
    goto again;
  }
}

此tcpip_timeouts_mbox_fetch函数在tcpip_thread线程中被执行

①:检查互斥锁是否上锁,避免多线程问题

②:获取距离下一次超时的差值

③:没有定时器,则阻塞等待邮箱消息

④:已经有定时器超时,则调用sys_check_timeouts进行处理

⑤:超时等待消息邮箱进行处理

⑥:在邮箱超时或者有消息时再次检查定时器是否有超时事件需要处理

/**
 * @ingroup lwip_nosys
 * Handle timeouts for NO_SYS==1 (i.e. without using
 * tcpip_thread/sys_timeouts_mbox_fetch(). Uses sys_now() to call timeout
 * handler functions when timeouts expire.
 *
 * Must be called periodically from your main loop.
 */
void
sys_check_timeouts(void)
{
  u32_t now;

  LWIP_ASSERT_CORE_LOCKED();

  /* Process only timers expired at the start of the function. */
  now = sys_now();

  do {
    struct sys_timeo *tmptimeout;
    sys_timeout_handler handler;
    void *arg;

    PBUF_CHECK_FREE_OOSEQ(); //①

    tmptimeout = next_timeout;  
    if (tmptimeout == NULL) {  //②
      return;
    }

    if (TIME_LESS_THAN(now, tmptimeout->time)) {  //③
      return;
    }

    /* Timeout has expired */
    next_timeout = tmptimeout->next;
    handler = tmptimeout->h;
    arg = tmptimeout->arg;
    current_timeout_due_time = tmptimeout->time;  //④
#if LWIP_DEBUG_TIMERNAMES
    if (handler != NULL) {
      LWIP_DEBUGF(TIMERS_DEBUG, ("sct calling h=%s t=%"U32_F" arg=%p\n",
                                 tmptimeout->handler_name, sys_now() - tmptimeout->time, arg));
    }
#endif /* LWIP_DEBUG_TIMERNAMES */
    memp_free(MEMP_SYS_TIMEOUT, tmptimeout);  //⑤
    if (handler != NULL) {   //⑥
      handler(arg);
    }
    LWIP_TCPIP_THREAD_ALIVE();

    /* Repeat until all expired timers have been called */
  } while (1);
}

①:检查互斥锁是否上锁,避免多线程问题

②:检查是否超时定时器链表是否为空

③:检查当前时刻是否小于定时器的超时时刻,如果大于则说明有超时事件需要处理

④:更新超时定时器链表

⑤:将当前超时的定时器释放

⑥:执行当前定时器的回调函数

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

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

相关文章

pyLoad远程代码执行漏洞(CVE-2023-0297)复现以及原理流量特征分析

声明&#xff1a; 请勿用于非法入侵&#xff0c;仅供学习。传送门 -》中华人民共和国网络安全法 文章目录声明&#xff1a;pyLoad介绍漏洞介绍影响版本不受影响版本漏洞原理漏洞环境搭建以及复现流量特征分析pyLoad介绍 pyLoad是一个用 Python 编写的免费和开源下载管理器&am…

计算GPS两个点之间的距离

参考&#xff1a;Https://blog.csdn.net/u011339749/article/details/125048180任意两点对应的经纬度A(lat0,long0),B(lat1,long1)则C(lat1,long0),D(lat0,long1)。通过A、B、C、D四个点可以确定一个四边形平面。同一纬度相互平行&#xff0c;可知连接ACBD四点构成了一个等腰梯…

干货|PCB板上的丝印位号与极性符号的组装性设计

PCB板上的字符很多&#xff0c;那么字符在后期起着那些非常重要的作用呢&#xff1f;一般常见的字符:“R”代表着电阻&#xff0c;"C”代表着电容&#xff0c;“RV”表示的是可调电阻&#xff0c;“L”表示的是电感&#xff0c;“Q”表示的是三极管&#xff0c;“D”表示的…

剑指Offer 第27天 JZ75 字符流中第一个不重复的字符

字符流中第一个不重复的字符_牛客题霸_牛客网 描述 请实现一个函数用来找出字符流中第一个只出现一次的字符。例如&#xff0c;当从字符流中只读出前两个字符 "go" 时&#xff0c;第一个只出现一次的字符是 "g" 。当从该字符流中读出前六个字符 “google&…

MDS75-16-ASEMI三相整流模块MDS75-16

编辑-Z MDS75-16在MDS封装里采用的6个芯片&#xff0c;是一款工业焊机专用大功率整流模块。MDS75-16的浪涌电流Ifsm为920A&#xff0c;漏电流(Ir)为5mA&#xff0c;其工作时耐温度范围为-40~150摄氏度。MDS75-16采用GPP硅芯片材质&#xff0c;里面有6颗芯片组成。MDS75-16的电…

ThreadPoolExecutor原理解析

1. 工作原理1.1 流程图1.2 执行示意图从上图得知如果当前运行的线程数小于corePoolSize(核心线程数)&#xff0c;则会创建新线程作为核心线程来执行任务(注意&#xff0c;执行这一步需要获取全局锁)。如果运行的线程等于或多于corePoolSize&#xff0c;则将任务加入BlockingQue…

C语言const的用法详解

有时候我们希望定义这样一种变量&#xff0c;它的值不能被改变&#xff0c;在整个作用域中都保持固定。例如&#xff0c;用一个变量来表示班级的最大人数&#xff0c;或者表示缓冲区的大小。为了满足这一要求&#xff0c;可以使用const关键字对变量加以限定&#xff1a;constin…

大型智慧校园系统源码 智慧班牌 智慧安防 家校互联 智慧校园小程序源码

一款针对中小学研发的智慧校园系统源码&#xff0c;智慧学校源码&#xff0c;系统有演示&#xff0c;可正常上线运营正版授权。 技术架构&#xff1a; 后端&#xff1a;Java 框架&#xff1a;springboot 前端页面&#xff1a;vue element-ui 小程序&#xff1a;小程序原生…

【CDP】CDP集群修改solr 存储路径 引发组件的ranger-audit 大量报错的解决方案

前言 我们生产上公司是使用的CDP集群&#xff0c;一次管理员通知&#xff0c;Solr 组件的数据存放路径磁盘空间不够。 我们的solr 组件时为 Ranger 服务提供日志审计功能&#xff0c; 在我们更改了磁盘路径&#xff0c;并重启了Solr 组件&#xff0c;然后发现相关组件&#…

立创eda专业版学习笔记(6)(pcb板移动节点)

先要看一个设置方面的东西&#xff1a; 进入设置-pcb-通用 我鼠标放到竖着的线上面&#xff0c;第一次点左键是这样选中的&#xff1a; 再点一次左键是这样选中的&#xff1a; 这个时候&#xff0c;把鼠标放到转角的地方&#xff0c;点右键&#xff0c;就会出现对于节点的选项…

关于VSCode安装go插件问题

比较常见的go开发编辑工具有VSCode、GoLand等&#xff0c;其中&#xff0c;使用VSCode需要下载相关的go语言插件。但是大多数情况都会下载失败&#xff0c;因为有些资源需要翻墙的原因&#xff0c;有时候翻墙了还是会报错。   本文将介绍一种帮助大家成功下载go插件的方法&am…

流水线使用(测试->构建->部署上线)

流水线介绍&#xff08;可直接查阅云效中流水线介绍&#xff09; 流水线在项目中的使用 1、选择我的流水线—>新建流水线 2、选择流水线模板&#xff08;可以根据需求选择不同模板&#xff09; 3、流水线配置 ①选择代码源&#xff1a;我目前展示的是直接使用codeup中的代码…

apipost-一键压测

apipost新功能可实现一键压测接口压测实践使用场景对指定接口进行性能测试。实现方式为实现高性能的并发需求&#xff0c;使用自研的压测引擎&#xff0c;可以实现一万以上并发。项目已经开源&#xff0c;github地址&#xff1a;https://github.com/Apipost-Team/runnerGo压测结…

antd日期组件时间范围动态跟随

这周遇到了一个很诡异但又很合理的需求。掉了一周头发&#xff0c;死了很多脑细胞终于上线了。必须总结一下&#xff0c;不然对不起自己哈哈哈。 一、需求描述 默认当前日期时间不可清空。 功能 默认时间如下&#xff1a; 目的&#xff1a;将时间改为 2014-08-01 ~ 2014-08…

网络工程师测试命令排行榜,快来看一看吧! -ccie网络工程师

网络工程师测试命令排行榜&#xff0c;快来看一看吧&#xff01; 01 Ping命令 ping命令的主要功能是用来检测网络的连通情况和分析网络运行速度。它是基于TCP/IP协议、通过发送和接收数据包来检测两台计算机间的连接状况。 网络工程师用 ping查看网络情况&#xff0c;主要…

【无标题】tcpdump 命令

tcp一款sniffer工具&#xff0c;是Linux上的抓包工具&#xff0c;嗅探器语法tcpdump (选项)选项-c&#xff1a; 指定要抓取的包数量。注意&#xff0c;是最终要获取这么多个包。例如&#xff0c;指定"-c 10"将获取10个包&#xff0c;但可能已经处理了100个包&#xf…

如何基于声网互动白板实现一个多人数独游戏

本文作者是声网社区的开发者“tjss”。他基于 Vue、声网的互动白板的代码模板&#xff0c;搭建出了一个支持多人互动的数独游戏。本文记录了他的实现过程&#xff0c;欢迎大家也可以尝试实现自己的小游戏或应用。 我基于声网互动白板的 SDK 与 Window Manager 开发了一个场景化…

第二十三周周报

学习内容&#xff1a; 修改ViTGAN代码 学习时间&#xff1a; 2.3-2.10 学习产出&#xff1a; 现在的效果 可以看到在700k左右fid开始上升&#xff0c;相比vitgan&#xff0c;改的vitgan鉴别器loss有所下降&#xff0c;但是fid没有降下来&#xff0c;最好为23.134&#xf…

Elasticsearch7.8.0版本进阶——分布式集群(水平扩容)

目录一、Elasticsearch集群的安装1.1、Elasticsearch集群的安装&#xff08;win10环境&#xff09;1.2、Elasticsearch集群的安装&#xff08;linux环境&#xff09;二、水平扩容&#xff08;win10环境集群演示&#xff09;三、想要扩容超过 6 个节点怎么办3.1、主分片和副分片…

IDEA合并分支(从开发分支到测试分支)

IDEA合并分支(从开发分支到测试分支) 1、先在当前分支拉去最新的代码且提交自己的修改到远程分支上 2、切换到目标分支(你要合并到的分支上),test测试分支 3、进行分支合并,这里其实有3个选项比较常用 ①Compare with ‘test’ 与当前分支(test)比较,这个比较回弹出个窗口…