硬实时系统的语言构造和转换----普渡大学

本文翻译来源于:

                                 Language Constructs and Transformation for Hard Real-time Systems
                                                                               Tai M. Chung and Hank G. Dietz
                                                                                School of Electrical Engineering
                                                                                             Purdue University
                                                                                      West Lafayette, IN 47907

1.前言

在实践中,硬实时系统的时间关键部分仍然是用低级编程语言实现的,并通过手工调整来满足所有的时间需求。现如今还没有一种实时语言,支持一个适当的方式指定时间限制为一个通用的硬实时系统,并为用户提供透明的高精度定时分析,因此对于实时系统特别是要求快速响应的系统,用户必须做出复杂的分析与编码。

在本文中,我们提出了可以添加到任何命令式编程语言中的新语言结构,以便扩展语言为用户提供一种方法来指定指令级任意操作之间的相对时间约束。本文还以图表的形式介绍了用于转换所建议语言的独特编译技术,图表是用于硬实时系统的编译器,它为目标执行模型生成有效的指令序列。

2.简介

在我们对实时系统的理解中,实时系统由控制子系统和被控制实体组成。控制子系统是一组计算机系统,而被控制的实体可以是范围很广的任何一个任何设备,从简单的搅拌机到复杂的机器人[19]。通常,控制子系统执行控制程序来接收来自环境的输入并适当地向被控制实体发送命令。

为了使实时系统正常工作,控制程序必须在逻辑上正确,控制子系统必须在没有定时故障的情况下执行程序。在硬实时系统中,如果不能在适当的时间执行某个操作,或者控制程序逻辑存在缺陷,都可能产生灾难性的后果。因此,满足时间限制在这样的系统中是非常重要的。

根据控制程序的调度时间,实时系统可以分为两类:动态系统和静态系统。动态系统中控制任务的执行顺序是由检查系统当前状态的调度程序在运行时动态确定的;通常,这个调度程序是操作系统的一部分。动态系统的灵活可变性,使其能充分的服务于调度开销和不可预测的任务。

相反,静态系统是在编译时根据程序分析、时间约束和资源使用预测[16]来调度的。静态系统在编译时根据被控制实体的预测行为和控制子系统的定时特性来调度执行顺序。因此,静态系统保证正确调度的代码将正常运行,而不会出现计时故障。然而,尽管保证了静态系统的及时执行使得静态系统在硬实时环境中非常具有吸引力,但是高精度的静态系统却很少受到关注,因为:

  1. 不可预测的机器行为:通用处理器通常执行指令的时间依赖于操作数值、管道冲突或内存层次结构的使用(例如,缓存丢失、动态RAM刷新、虚拟内存页错误)。这些变量中的任何一个都会降低所需的定时分析的准确性。
  2. 调度复杂性:指令级定时分析和代码重组都是NP完全问题[6]。因此,静态调度主要关注粗粒度任务,以减少问题的规模。
  3. 缺乏编程支持:没有编程语言支持一种机制来表示完全通用的实时约束。

上述的第一个问题虽不容易解决,但可以通过详细的计算机硬件设计而避免。例如,TMS320C40高性能DSP具有完全静态定时特性,前提是禁用中断并使用SRAM[10]实现本地内存。甚至可以使多个处理器之间的交互具有静态计时属性。例如,PAPERS(普渡大学为并行执行和快速同步而设计的适配器)可以提供具有精确静态定时属性[5]的细粒度通信和同步。

与此同时,我们认为[3]中所介绍的技术构成了合适的静态调度算法的基本基础。因此,第二个问题得到了部分解决,也有可能很快就会找到一个切实可行的解决方案。这同时也就将问题的矛头指向了提供合适的编程模型和语言。但是在高级编程语言的上下文中指定细粒度的时间限制似乎是自相矛盾的。用汇编语言编码的方式解决细粒度定时约束解决不了上面的问题。这样的程序难以编写而且自动化调度基本上是不可能的。直接使用现有的高级实时编程语言,比如ADA[2],Edison[9]或变成扩展的通用语言RT-Euclid[12],FLEX[15],MPL[17]和TCEL[7]也不能满足上面的要求,因为他们不提供一种机制使得在任何语言上用于指定时间构造细粒度比整个任务要好。这些语言使用编程模型:该模型将时间约束表示为源程序中词汇相邻的任务对之间的关系;这种邻接约束既是人为的,又是不合理的。任意(潜在的并行)操作之间的时序关系无法表达,因为有些这样的关系不能映射到程序中字典上相邻的位置。

本文的第2部分提出了可以添加到任何命令式编程语言中的新语言结构,以便扩展语言为用户提供一种方法来指定单个指令级别上任意操作之间的相对时间约束。为了帮助读者理解如何提取和处理这些时间限制,

第3节简要介绍了相关编译器技术。第四部分总结了本文的研究成果,并提出了今后的研究方向。

3.实时语言

为了激发新的语言结构的设计,考虑图1中所示的非正式注释的控制程序片段。尽管这个例子非常简单,并且包含了在实际应用程序中很容易找到的时间限制,但是现有的编程语言都无法以一种便于进行静态时间分析和调度的形式来表达这一点。

3.1 时间约束模型

 本文提出的实时语言实现了一个图形化模型,记作G=(;)其中有向图G有一组节点和一组边组成。节点和边分别与指令和实时约束相关联。这种图模型称为有向时间图(DTG),它与传统的有向图有几方面的区别。最重要的是,DTG中的每条边不一定表示优先关系或依赖关系,但表示两个控制动作之间的时间关系方向。

根据指令的效果,\Gamma中的指令可以分为外部查看的指令(EVI)或内部查看的指令(IVI)。

IVI的作用仅限于内部计算,而EVI则改变控制环境的状态。例如,C语言[20]中定义为volatile的变量或使用RCCL[13]控制机器人的命令是EVIs,它们的执行必须满足指定的时间限制。由于EVIs可能依赖于IVIs计算的值,因此任何此类依赖关系还意味着必须保留相对的执行顺序。

\Theta中的有向边与多个属性相关联:源对象\gamma _s,接收对象\gamma _e,关系操作\eta,偏移\delta。约束\theta _k被定义为\theta _k = <\gamma _s\gamma _e\eta\delta>。每条边表示的关系类型由\eta指定:(<)表示之前,(>)表示之后,(=)表示同时发生的,(\neq)表示异步发生的。确切的来说,实际上,对于每一种边缘类型,\gamma _s\gamma _e之间的任何时间关系都可以用时间值\delta表示:

            约束之前:\gamma _e < \gamma _s + \delta  (\gamma _e必须发生在\gamma _s+\delta时间以后)

            约束之后:\gamma _e < \gamma _s + \delta  (\gamma _e必须发生在\gamma _s+\delta时间之前)

            等价约束:\gamma _e = \gamma _s + \delta  (\gamma _e必须发生恰好发生在\gamma _s+\delta时间)

            异步约束:\gamma _e \neq \gamma _s + \delta (\gamma _e必须发生恰好不发生在\gamma _s+\delta时间)

表示源指令(\gamma _s)和接收指令(\gamma _e)之间时间关系方向的边遵循一个简单的原则。\gamma _s\gamma _e分别出现在约束RHS和LHS中。

\gamma _s\gamma _e可以是任意一对指令。此外,该方法还可以在模型中使用小于n(n-1)/2的约束来指定n条指令之间的约束。也就是说,多个指令之间的任何约束都可以通过我们的约束模型来指定——甚至是不能通过顺序调度来满足的约束。

重要的是,可以在不引入任何其它类型的情况下表示简单的排序(优先级)约束。任何一个格式形如“x  uses  {y}'_s result”能够被翻译成形如“x happens at the earliest 0 after y”。因此,时间约束和优先约束都可以使用相同的语法表示。

3.2 实时语言特征

  • 任何编程系统最重要的一个方面是:该系统可以很容易地用于创建和维护工作程序。因此实时语言的外观和感觉应该像一种流行的传统语言,例如C。同样,由于没有可以用来表示时间约束的熟悉语法,因此代码的时间方面必须是直接可见的,并且易于修改。调用两个操作的任何时间约束都应该能够直接表示,而不必调整代码的其他部分。
  • 独立处理每个约束的能力部分取决于组合多个影响相同操作的约束的能力。虽然在实时系统上的大多数工作表明,这些约束总是通过要求满足所有约束来组合,但有时[8]有必要通过要求满足任何一个或多个约束来组合。在实时社区中,这种ORing约束的概念是相当新的。假设事件A_1可以在A_2之前或A_3之后执行,如图2所示。这可以用数学表示为A_1 < A_2A_1 > A_3。图2显示,基于A_2A_3的时间值,对于相同的约束,A_1的可能取值范围是不同的。尽管这种结构很有用,也很有必要,但在其它实时编程语言中,ORing约束明显是不存在的,这可能是因为通过支持这两种约束的组合,调度变得更加复杂。

我们进一步指出,在可能的最细粒度水平上表达时间约束是至关重要的。因为更细的粒度在编译器的调度中产生更多的自由,这就产生了能够满足所有时间约束的最高概率。当然,调度中更多的自由也意味着更大的调度搜索空间,但是用户不需要知道编译器中额外的复杂性。

3.3 父语言

  • 到目前为止,还没有一种现有的实时语言提供上述所有特性。大多数实时语言都不容易编程,不基于精细的任务,并且有的也不用于表示任意控制操作之间的相对约束。因此,我们建议对命令式语言进行扩展,以简化这些特性。尽管任何命令式语言都可以针对这些特性进行扩展,但我们将开发新的语言构造到C语言的一个子集上,因为C用于大多数嵌入式实时系统,并结合汇编宏用于时间关键部分。
  • 我们建议的父语言包括所有使用算术运算符和关系运算符的表达式、if语句和while语句。此时,既不支持混叠,也不支持浮点计算。

3.4 提出语言结构

  • 为了在父语言上指定约束,我们引入了几个新的结构:定时块、时态表达式和循环块。定时块定义了一个代码扩展作为一个单元来维护约束规范,而时间表达式则根据定时块指定了相关的约束。非循环块是为无限运行或直到程序终止的代码序列定义的。一个程序可以包含多个循环块。
  • 在演示这些构造之前,请考虑图3中所示的一个控制程序示例。编写该程序是为了将数据从内存位置的命令读入内存位置,并在所需的时间将这些命令发送到设备。

3.4.1 时间块

定时块是用户定义的变量或语句序列,在传统的实时系统中可以将其视为任务。也就是说,它要么包含一个易失性变量(EVI),要么包含至少一个EVI组成的语句(s)。如果计时块不包含EVI,则忽略与时间变量关联的时间表达式,这意味着约束是自动满足的。

识别定时块的定时变量出现在“@”和“:”之间,后面跟着EVI或块体,如下面的语法所示:

这里,tvar是块的用户定义名称,而变量是一个常规变量,即EVI。符号(语句)+表示组成计时块的一个或多个语句。

例如,图3中的第12行定义了与变量tv2和tv3相关联的时间变量tv2和tv3,它们与变量Sensor2和语句b = Sensor2 + 4相关联;在本例中,tv2是tv3的嵌套块。

计时块具有常规块的所有属性,因此它具有词法范围,并且允许在块的开头声明变量。此外,计时块可以嵌套,除非它们是交错的。时间块的交错是不允许的,因为它增加了复杂性,破坏了结构化语言的特性。

3.4.2 循环块

循环块是定时块的一种特殊情况,在此意义上,块被重复执行,直到程序终止。因此,编译器在进行分析和调度时,应该区别对待这个块和其他块。循环块由时间变量tvar和这里定义的周期\delta标识。

 

cycle是一个关键字,表示下面由"{}"包围的块是一个循环块,tvar是块的时间变量名,\delta指定循环块必须在其中完成的时间要求。也就是说,计时块和循环开销中的语句必须在\delta中完成。

与循环块相关的时间约束可以在后面描述的时态表达式中表示。例如,图3中的程序由两个循环块tp1(第9行到第35行)和tp2(第37行到第42行)组成,循环块之间的时间关系在第44行中指定。第44行中的时间表达式表示,tp2的第一个动作不应该在tp1的最后一个动作之后的2个时间单位之后执行。

 

3.4.3 时间表达式

时态表达式分为时态赋值或时态关系。时间赋值是一种定义另一个与现有时间变量具有时间距离的时间变量的方法,而时间关系用于表示时间变量之间的关系。

在该语法中,tvar是一个时间变量的名称,etvar是一个扩展的时间变量,具体使用前缀点或后缀点表示开始时间或完成时间。在图3中。第14行的tv1.表示tv1的完成时间,第15行的.tv3表示tv3的开始时间。此外,\eta是上一节定义的时间关系运算符之一,\delta是任何时间单位(如块周期)的偏移量。此外,时间关系(| etvar1 \eta etvar2 (+|-) \delta )*表示零个或多个时间关系来指定ORing约束。

正如我们所看到的在你语法的时间分配,计算时间变量是允许的,例如,时间分配tv2 = tv1 + 7 和时间关系.tv3 < .tv2 + 7在一起意味着tv3的开始时间必须早于开始时间后的14个时间单位tv3相当于.tv3 < .tv1 + 7 + 7。

图3演示了一个用于调用迭代的时间约束的惟一时间表达式。当考虑出现在迭代循环中的块时,需要指定不同迭代中的实例之间的时间约束。我们引入一系列的时间变量来解决这个问题。如图3的第31行所示,实例之间的距离表示为数组的索引。例如,第31行说循环应该在25个时间单位内执行,因为时态表达式要求(i+1)次迭代的第一个操作应该在(i)次迭代的最后一个操作之后的25个时间单位之前开始。此时,我们正式将etar定义为:

其中tvar是一个时间变量,{[i]}是[i]的一个可选表达式,表示迭代的第i个实例。

3.4.4 非法表达式

图3第17 - 20行定义了时间变量tv4和tv5,第22 - 23行显示了相应的时间表达式、时间赋值和时间关系。它说(then 子句)中的操作应该在tv2完成后的14个时间单元之后执行。因此,只有当a < b成立时才考虑时间约束。但是,任何具有一对指令\gamma _s\gamma _e的时态表达式都是非法的,因为其中一条来自(then子句),另一条来自(else子句),在具体的执行过程中不可能同时运行两个分支。

另一种非法表达式是语句,其指令对必须包含具有不可预测计时属性的语句。这些语句是时间不可预测的任何指令,例如循环中的break语句、不可预测的中断数。例如,只要a<b保持不变,就会重复执行tv6(第26行到第32行)。因此,如果a和b的范围是不确定的,那么第34行中的时态表达式就是非法表达式。

 

指定及时冲突操作的时态表达式也被归类为非法表达式。考虑第14行和第15行中的两个表达式,将第14行修改为tv3.>.tv2 + 4,然后是tv3. > .tv2 + 4和 .tv3 < tv2.+ 3没有解决方案,这意味着代码不能有一个有效的调度。因此,只有通过彻底的语义分析才能发现那些非法的表达式。

4 .编译技术

在本节中,我们将简要说明处理语言转换的独特编译技术。特别是编译器的组织、中间表示的数据结构和循环块调度是重点,因为它们与传统的编译技术[1][4]或编译的新概念有很大的不同。

4 .1.编译组织

如图4所示,图表由四个软件模块组成:语法分析、语义分析、代码调度和代码生成模块。每个模块的设计和执行都是独立的,因此软件可以单独替换以增强功能。例如,我们开发的调度算法可以替换为一个更好的算法,不影响任何其他模块,除了他们之间的接口最小的修改。在我们的实现中,这些模块是在PCCTS(普渡编译器构建工具集)的支持下用C语言编写的。

语法分析模块从源程序构建中间表示。由于时间约束规范和循环块,图表的中间表示是惟一的,如第3.2节所述。

语义分析模块的功能是根据语言语义验证程序上下文,包括以下内容:

  • 计时块是否包含至少一个EVI
  • 时态表达式在语义上合法嘛
  • 他是否与父语言兼容

代码调度模块的目标是重新组织指令序列,以满足所有的时序约束和逻辑正确性。这个模块和代码生成模块完全依赖于系统,硬件组件和指令集的计时属性可以硬连接到编译器中,也可以从系统文件中读取,如图4所示。调度模块的输入是时间特征与目标代码接近的元组,从而在代码生成时保留元组的执行时间。

4 .2.中间表示

除了词法分析器和解析器的传统角色之外,语法分析模块还从程序中提取约束和定时块的信息,并构建一个中间表示。如图5所示,中间表示的数据结构由两个表和三个树组成。表由符号表(SymTab)和时态变量表(TVarTab)组成,而树结构包括抽象语法树(AST)、循环语法树(CST)和时态语法树(TST),它们都是子同胞树[18]。

 

虽然在传统编译器中只有AST和SymTab就已经足够了,但我们提出的实时系统编译器需要更多的信息。它们被组织在TVarTab中,以存储定时块的信息,例如块类型、入口/出口点、用于操作所有要定期执行的块的CST(循环语法树),以及表示时态表达式的TST(时态语法树)。

特别是,CST和时间表达式结合用来指定循环块之间的时间限制是相当复杂,并且其在编译器社区也是一个新概念,(考虑到时间和时间约束在循环块,多个循环块需要无限期地执行)。

4 .3.循环块调度

调度循环块与传统的编译技术非常不同,因为传统的编译没有交叉循环的概念。循环块需要合并成一个循环块,这个循环块可以在没有显式动态调度器或调度器的情况下执行。在本节中,我们提出了一种新的方案,将给定的与不同周期相关的循环块集成到一个循环块中。集成的循环块将无限期或半无限期地执行,直到程序终止。

\psi _1,\psi _2,...,\psi _n为周期为p_1,p_2,...,p_n的循环块,即\psi _i块中的第一个时间变量的执行应该每p_i次启动一次。使用循环任务调度的基本特性,将循环块重新排列为单个块\psi进行如下的处理[14]:

1.计算p = \zeta\pounds M_{i=1}^{n}(p_i)  :   \zeta\pounds M_{i=1}^{n}(p_i)表示周期p_1,p_2,...,p_n的最小公倍数。

2.对于任意的i,循环\psi _i中的指令,执行p/p_i次。

3.对于任意的i,扩展时间语义。注意,在我们的模型中,展开是对时态表达式的简单添加,因为依赖关系是简单约束的子类型。定义\gamma_{i(x)}^{k}作为\psi _i的第k个实例中的指令x。\psi _i是与周期p_i相关的原始循环块。\gamma_{i(x)}^{}代表着在\psi _i中的指令x。约束展开是在以下分析的基础上进行的:

  • 自我扩展:复制的指令添加了时间约束来维护这些复制指令的执行顺序。在这个扩展中,周期约束是执行中的控制动作。对于任意的i和j,如果\gamma_x\psi _i中的第一个控制动作则\delta = p_i,否则\delta = 0,时间表达式\gamma_{i(x)}^{k} < \gamma_{i(x)}^{k+1} + \delta将被添加进去。当指令1, \gamma_{i(2)}^{k}\psi_i中的第一个控制动作时,图6中的(a)中描述了自展开的图形表示。
  • 周期进行扩展:假设\gamma_{i(x)}^{k+1}是依赖于\gamma_{i(y)}^{k}的,比如当k>0,\gamma_{i(y)}^{k} < \gamma_{i(x)}^{k+1}。在本例中,对于 1 \leq k \leq n-1\psi_i的每个副本都强制执行依赖关系,添加时态表达式\gamma_{i(y)}^{k} < \gamma_{i(y)}^{k+1}。展开应用于每一个循环携带的依赖项,如图6 (b)所示。在图中,\gamma_{i(2)}^{k+1}使用的是\gamma_{i(3)}^{k}的结果。当k > 0,这种关系的时间表达式被表示成\gamma_{i(3)}^{k} < \gamma_{i(2)}^{k+1}
  • intra-cycle扩展:必要时复制指定同一块中指令之间约束的时态表达式。在每个\psi_i副本中,\gamma_{i(x)}^{0} < \gamma_{i(y)}^{0} + \delta,\gamma_{i(x)}^{k} < \gamma_{i(y)}^{k}将被添加进去。对于任意的k,在\psi_{i}^{k}中时间表达式\gamma_{i(3)}^{k} > \gamma_{i(2)}^{k} + \delta是重复的。
  • inter-cycle扩展:必要时,复制指定一个块之外(而不是块内)指令之间约束的时态表达式。在\psi_{i}^{k}中,\varepsilon_{i(x)}^{k}表示\gamma_{i(x)}^{k}的开始时间,然后,\varepsilon_{i(x)}^{k}的范围可以表示为(k - 1)\times p_i\leq \varepsilon_{i(x)}^{k} \leq (k+1)\times p_i。对于任意的p,满足\varepsilon_{i(x)}^{k} \wedge \varepsilon_{i(y)}^{k} \neq \phi的原始循环块\psi_{j}^{p},周期间时间表达式\gamma_{i(x)} < \gamma_{i(y)} + \delta得到时间表达式\gamma_{i(x)}^{k} < \gamma_{i(y)}^{k} + \delta。在图6-d中,对于\varepsilon_{i(x)}^{k} \wedge \varepsilon_{i(y)}^{k} \neq \phi,我们假设p = (p_1 \vee p_2),表达式\gamma_{i(3)}^{k} < \gamma_{i(2)}^{p1} + \delta\gamma_{i(3)}^{k} < \gamma_{i(2)}^{p2} + \delta将被添加进去。

4.\Psi是由指令级细粒度调度算法调度的。引用[3]中给出了一种利用遗传搜索算法寻找有效调度的方法。

        

5.References

 

  1. A. V. Aho, R. Sethi, and J. D. Ullman. Compiler Principles, Techniques, and Tools. Addison-Wesley Publishing Company, Reading, Massachusetts,1986.
  2. R. F. Brender. What is ADA? IEEE Computer,14(6):17 { 24, June 1981.
  3. T. M. Chung and H. G. Dietz. Static scheduling of hard real-time code with instruction-level timing accuracy. submitted to 16th Real-Time Systems Symposium, December 1995.
  4. H. G. Dietz. The Rened-Language Approach to Compiling for Paral lel Supercomputers. PhD thesis, Polytechnic University, Brooklyn, New York,June 1987.
  5. H. G. Dietz, T. M. Chung, T. I. Mattox, and T. Muhammad. TTL PAPERS: An Implementation of Purdue's Adapter for Parallel Execution and Rapid Synchronization. Internal Report, Purdue University, W. Lafayette, IN. 1995
  6. M. R. Garey and D. J. Johnson. Computers and Intractability, a Guide to Theory of NPCompleteness. W. H. Freeman Company, San Francisco, CA, 1979.
  7. R. Gerber and S. Hong. Semantic-based compiler transformation for enhanced schedulability. In Proc. of Real-Time Systems Symposium, pages 232 - 242, December 1993.
  8. D. W. Gillibs and J. W. Liu. Scheduling tasks with and/or precedence constraints. Technical Report UIUC-ENG-1766, University of Illinois, October 1991.
  9. P. B. Hansen. Edison: A multiprocessor language.Technical Report Dept. of Computer Science, University of Southern California, September 1980.
  10. Texas Instruments Inc. Third Generation TMS320 User's Guide. Texas Instruments Inc., 1988.
  11. Unimation Incorporated. Unimate Industrial Robot Programming Manual, User's Guide to VAL II.Unimation Incorporated, Danbury,Connecticut,December 1986.
  12. E. Kligerman and A. D. Stoyenko. Real-Time EUCLID: A Language for Reliable Real-Time Systems. IEEE Transactions on Software Engineering,12(9):941 - 949, September 1986.
  13.            J. S. Lee, S. Hayati, V. Hayward, and J. E. Lloyd.Implementation of RCCL, a Robot Control C Library on a MicroVAX II. In Intel ligent Robots and Computer Vision, pages 472 { 480, Cambridge,MA, Oct 1988.
  14.           J. Y. T. Leung and M. L. Merril. A note on preemptive scheduling of periodic real-time tasks. Information Science Letter, 20(3):115 -118, March1980.
  15.           K. J. Lin and S. Natara jan. Expressing and Maintaining Timing Constraints in FLEX. In Proc. of Real-Time Systems Symposium, pages 96 - 105,December 1988.
  16.           D. C. Locke. Software architecture for hard realtime applications: cyclic vs. xed priority executives. Real-time Systems, 4(1):37 - 53, March 1992.
  17.           V. M. Nirkhe, S. K. Tritaphi, and A. K. Agrawala.Language Support for the Maruti Real-time System. In Proc. of Real-Time Systems Symposium,pages 257 - 266, December 1990.
  18.           T. Parr, H. Dietz, and W. Cohen. PCCTS Reference Manual, Version 1.0. ACM SIGPLAN Notices, 27(2):88{165, February 1992.
  19.           J. A. Stankovic and K. Ramamritham. Hard RealTime Systems Tutorial. IEEE Computer Society Press, 1991.
  20.           ANSI X3.159. Programming Language C. American National Standard Institute, 1989.
     

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值