linux内核原子操作的实现

linux内核部件分析(二)——原子性操作atomic_t 在任何处理器平台下,都会有一些原子性操作,供操作系统使用,我们这里只讲x86下面的。在单处理器情况下,每条指令的执行都是原子性的,但在多处理器情况下,只有那些单独的读操作或写操作才是原子性的。为了弥补这一缺点,x86提供了附加的lock前缀,使带lock前缀的读修改写指令也能原子 阅读详情

所谓原子操作,就是“不可中断的一个或一系列操作”。

硬件级的原子操作:在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是“原子操作”,因为中断只发生在指令边缘。在多处理器结构中(Symmetric Multi-Processor)就不同了,由于系统中有多个处理器独立运行,即使能在单条指令中完成的操作也有可能受到干扰。在X86平台生,CPU提供了在指令执行期间对总线加锁的手段。CPU上有一根引线#HLOCK pin连到北桥,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。对于其他平台的CPU,实现各不相同,有的是通过关中断来实现原子操作(sparc),有的通过CMPXCHG系列的指令来实现原子操作(IA64)。本文主要探讨X86平台下原子操作的实现。

软件级别的原子操作:软件级别的原子操作实现依赖于硬件原子操作的支持。

Linux内核提供了两组原子操作接口:一组是针对整数进行操作;另一组是针对单独的位进行操作。

1、原子整数操作

原子操作通常针对int或bit类型的数据,但是Linux并不能直接对int进行原子操作,而只能通过atomic_t的数据结构来进行。目前了解到的原因有两个。

一是在老的Linux版本,atomic_t实际只有24位长,低8位用来做锁。这是由于Linux是一个跨平台的实现,可以运行在多种 CPU上,有些 类型的CPU比如SPARC并没有原生的atomic指令支持,所以只能在32位int使用8位来做同步锁,避免多个线程同时访问。(最新版SPARC实现已经突破此限制)。 原子整数操作最常见的用途就是实现计数器。常见的用法是:

atomic_t use_cnt;

atomic_set(&use_cnt, 2);

atomic_add(4, &use_cnt);

atomic_inc(use_cnt);

在X86平台上,atomic_t定义如下:

[cpp]  view plain copy
  1. typedef struct {  
  2.     int counter;  
  3. } atomic_t;  
下面选取atomic_add来进行分析:
[cpp]  view plain copy
  1. static inline void atomic_add(int i, atomic_t *v)  
  2. {  
  3.     asm volatile(LOCK_PREFIX "addl %1,%0"  
  4.              : "+m" (v->counter)  
  5.              : "ir" (i));  
  6. }  
可以看到,atomic_add使用了gcc提供的内嵌汇编来实现,是用一个addl指令来实现增加操作。重点看一下LOCK_PREFIX宏,它就是上文提到的锁总线操作,也就是它保证了操作的原子性。LOCK_PREFIX定义如下:
[cpp]  view plain copy
  1. #define LOCK_PREFIX \  
  2.                 ".section .smp_locks,\"a\"\n"   \  
  3.                 "  .align 4\n"                  \  
  4.                 "  .long 661f\n" /* address */  \  
  5.                 ".previous\n"                   \  
  6.                 "661:\n\tlock; "  
展开后变成:
[cpp]  view plain copy
  1. .section .smp_locks,"a"  
  2.   .align 4  
  3.   .long 661f  
  4. .previous  
  5. 661:  
  6.         lock;  
逐条解释如下:
.section .smp_locks,"a"
下面的代码生成到 .smp_locks 段里,属性为"a", allocatable
  .align 4
四字节对齐
  .long 661f
生成一个整数,值为下面的 661 标号的实际地址,f 表示向前引用,如果 661 标号出现
在前面,要写 661b。
.previous
代码生成恢复到原来的段,也就是 .text
661:
数字标号是局部标号,5.3 Symbol Names
        lock;
开始生成指令,lock 前缀
这段代码汇编后,在 .text 段生成一条 lock 指令前缀 0xf0,在 .smp_locks 段生成四个字节的 lock 前缀的地址,链接的时候,所有的.smp_locks 段合并起来,形成一个所有 lock 指令地址的数组,这样统计 .smp_locks 段就能知道代码里有多少个加锁的指令被生成,猜测是为了调试目的。

搜索了一下,找到了相关引用处,当一个内核模块被加载时,会调用module_finalize函数:

[cpp]  view plain copy
  1. int module_finalize(const Elf_Ehdr *hdr,  
  2.             const Elf_Shdr *sechdrs,  
  3.             struct module *me)  
  4. {  
  5.     const Elf_Shdr *s, *text = NULL, *alt = NULL, *locks = NULL,  
  6.         *para = NULL;  
  7.     char *secstrings = (void *)hdr + sechdrs[hdr->e_shstrndx].sh_offset;  
  8.   
  9.     for (s = sechdrs; s < sechdrs + hdr->e_shnum; s++) {   
  10.         if (!strcmp(".text", secstrings + s->sh_name))  
  11.             text = s;  
  12.         if (!strcmp(".altinstructions", secstrings + s->sh_name))  
  13.             alt = s;  
  14.         if (!strcmp(".smp_locks", secstrings + s->sh_name))  
  15.             locks= s;  
  16.         if (!strcmp(".parainstructions", secstrings + s->sh_name))  
  17.             para = s;  
  18.     }  
  19.   
  20.     if (alt) {  
  21.         /* patch .altinstructions */  
  22.         void *aseg = (void *)alt->sh_addr;  
  23.         apply_alternatives(aseg, aseg + alt->sh_size);  
  24.     }  
  25.     if (locks && text) {  
  26.         void *lseg = (void *)locks->sh_addr;  
  27.         void *tseg = (void *)text->sh_addr;  
  28.         alternatives_smp_module_add(me, me->name,  
  29.                         lseg, lseg + locks->sh_size,  
  30.                         tseg, tseg + text->sh_size);  
  31.     }  
  32.   
  33.     if (para) {  
  34.         void *pseg = (void *)para->sh_addr;  
  35.         apply_paravirt(pseg, pseg + para->sh_size);  
  36.     }  
  37.   
  38.     return module_bug_finalize(hdr, sechdrs, me);  
  39. }  
上面的代码说,如果模块有 .text 和 .smp_locks 段,就调这个来处理,做什么呢?

[cpp]  view plain copy
  1. void alternatives_smp_module_add(struct module *mod, char *name,  
  2.                  void *locks, void *locks_end,  
  3.                  void *text,  void *text_end)  
  4. {  
  5.     struct smp_alt_module *smp;  
  6.   
  7.     if (noreplace_smp)  
  8.         return;  
  9.   
  10.     if (smp_alt_once) {  
  11.         if (boot_cpu_has(X86_FEATURE_UP))  
  12.             alternatives_smp_unlock(locks, locks_end,  
  13.                         text, text_end);  
  14.         return;  
  15.     }  
  16.   
  17.     ........//省略无关代码  
  18. }  
上面的代码说,如果是单处理器(UP),就调这个:

[cpp]  view plain copy
  1. static void alternatives_smp_unlock(u8 **start, u8 **end, u8 *text, u8 *text_end)  
  2. {  
  3.     u8 **ptr;  
  4.     char insn[1];  
  5.   
  6.     if (noreplace_smp)  
  7.         return;  
  8.   
  9.     add_nops(insn, 1);  
  10.     for (ptr = start; ptr < end; ptr++) {  
  11.         if (*ptr < text)  
  12.             continue;  
  13.         if (*ptr > text_end)  
  14.             continue;  
  15.         text_poke(*ptr, insn, 1);  
  16.     };  
  17. }  
看到这里就能明白,这是内核配置了 smp,但是实际运行到单处理器上时,通过运行期间打补丁,根据 .smp_locks 里的记录,把 lock 指令前缀替换成 nop 以消除指令加锁的开销,这个优化真是极致了……,可能考虑很多用户直接使用的是配置支持 SMP 编译好的内核而特地对 x86/x64 做的这个优化。

2、原子位操作的实现

编写代码时,以如下的方式进行操作

unsigned long word = 0;

set_bit(0, &word); /*第0位被设置*/

set_bit(1, &word); /*第1位被设置*/

clear_bit(1, &word); /*第1位被清空*/

change_bit(0, &word); /*翻转第0位*/

为什么关注原子操作?
1)在确认一个操作是原子的情况下,多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能开销昂贵的锁。
2)借助于原子操作,我们可以实现互斥锁。
3)借助于互斥锁,我们可以把一些列操作变为原子操作。

我们重点关注一下以下两个函数的实现:

[cpp]  view plain copy
  1. /** 
  2.  * clear_bit - Clears a bit in memory 
  3.  * @nr: Bit to clear 
  4.  * @addr: Address to start counting from 
  5.  * 
  6.  * clear_bit() is atomic and may not be reordered.  However, it does 
  7.  * not contain a memory barrier, so if it is used for locking purposes, 
  8.  * you should call smp_mb__before_clear_bit() and/or smp_mb__after_clear_bit() 
  9.  * in order to ensure changes are visible on other processors. 
  10.  */  
  11. static inline void clear_bit(int nr, volatile void *addr)  
  12. {  
  13.     asm volatile(LOCK_PREFIX "btr %1,%0" : ADDR : "Ir" (nr));  
  14. }  
[cpp]  view plain copy
  1. /* 
  2.  * clear_bit_unlock - Clears a bit in memory 
  3.  * @nr: Bit to clear 
  4.  * @addr: Address to start counting from 
  5.  * 
  6.  * clear_bit() is atomic and implies release semantics before the memory 
  7.  * operation. It can be used for an unlock. 
  8.  */  
  9. static inline void clear_bit_unlock(unsigned nr, volatile void *addr)  
  10. {  
  11.     barrier();  
  12.     clear_bit(nr, addr);  
  13. }  
第一个clear_bit函数比较好理解,和上面atomic系列的函数实现类似。但是注意到clear_bit_unlock函数中多了一个barrier函数,这是什么操作呢?
这就是有名的“内存屏障“或”内存栅栏“操作,先来补充一下这方面的知识。

可以看一下barrier的定义:

[cpp]  view plain copy
  1. #define barrier() __asm__ __volatile__("": : :"memory")  

解释一下:__volatitle__是防止编译器移动该指令的位置或者把它优化掉。"memory",是提示编译器该指令对内存修改,防止使用某个寄存器中已经load 的内存的值。lock 前缀是让cpu 的执行下一行指令之前,保证以前的指令都被正确执行。

事实上,不止barrier,还有一个mb系列的函数也起着内存屏障的功能:
[cpp]  view plain copy
  1. #include <asm/system.h>  
  2. "void rmb(void);"  
  3. "void wmb(void);"  
  4. "void mb(void);"  

这些函数在已编译的指令流中插入硬件内存屏障,具体的插入方法是平台相关的。rmb(读内存屏障)保证了屏障之前的读操作一定会在后来的读操作执行之前完成。wmb保证写操作不会乱序,mb 指令保证了两者都不会。这些函数都是 barrier 函数的超集。解释一下:编译器或现在的处理器常会自作聪明地对指令序列进行一些处理,比如数据缓存,读写指令乱序执行等等。如果优化对象是普通内存,那么一般会提升性能而且不会产生逻辑错误。但如果对I/O 操作进行类似优化很可能造成致命错误。所以要使用内存屏障,以强制该语句前后的指令以正确的次序完成。

其实在指令序列中放一个wmb 的效果是使得指令执行到该处时,把所有缓存的数据写到该写的地方,同时使得wmb 前面的写指令一定会在wmb后面 的写指令之前执行。

回到上面的函数,当clear_bit函数不用于实现锁的目的时,不用给它加上内存屏障(我的理解:不管是不是读到最新的数据,这一位就是要清零,不管加不加内存屏障,结果都是一样的);而当用于实现锁的目的时,必须使用clear_bit_unlock函数,其实现中使用了内存屏障,以此来确保此处的修改能在其他CPU上看到(我的理解:加锁操作就是为了在多个CPU间进行同步的目的,所以要避免寄存器优化,其他CPU每次都读内存这样才能看到最新的变化,这块不是太明白)。这种操作也叫做serialization,即在执行这条指令前,CPU必须要完成前面所有对memory的访问指令(read and write),这样是为了避免编译器进行某些优化。

同样使用serialization操作的还有test_and_set_bit函数:

[cpp]  view plain copy
  1. /** 
  2.  * test_and_set_bit - Set a bit and return its old value 
  3.  * @nr: Bit to set 
  4.  * @addr: Address to count from 
  5.  * 
  6.  * This operation is atomic and cannot be reordered. 
  7.  * It also implies a memory barrier. 
  8.  */  
  9. static inline int test_and_set_bit(int nr, volatile void *addr)  
  10. {  
  11.     int oldbit;  
  12.   
  13.     asm volatile(LOCK_PREFIX "bts %2,%1\n\t"  
  14.              "sbb %0,%0" : "=r" (oldbit), ADDR : "Ir" (nr) : "memory");  
  15.   
  16.     return oldbit;  
  17. }  
解释一下:
1)memory 强制gcc 编译器假设RAM 所有内存单元均被汇编指令修改,这样cpu 中的registers 和cache 中已缓存的内存单元中的数据将作废。cpu 将不得不在需要的时候重新读取内存中的数据。这就阻止了cpu 又将registers,cache 中的数据用于去优化指令,而避免去访问内存。

2)sbb  $0,0(%%esp)表示将数值0 减到esp 寄存器中,而该寄存器指向栈顶的内存单元。减去一个0,esp 寄存器的数值依然不变。即这是一条无用的汇编指令。在此利用这条无价值的汇编指令来配合lock 指令,在__asm__,__volatile__,memory 的作用下,用作cpu 的内存屏障。

这种写法和前面的clear_bit_unlock中先写一个barrier函数,再写一个正常内嵌汇编函数的功能是一样的。

2.2.1 GNU C内嵌汇编语言 一个64位操作系统的实现 阅读详情

相关推荐

Linux内核实现原子性操作cmpxchg指令的理解

cmpxchg这个指令是为了

个人博客 4603

Linux内核原子操作 原理

摘自:http://ilinuxkernel.com/?p=1158 本文基linux内核版本 2.6.32-131.17.1.el6.i686源码。 1、原子读、赋值 00016: /** 00017:  *atomic_read-readatomicvariable 00018:  * @v:pointeroftypeatomic_t 00019:  * 00020:  *Ato

向往大海专栏 3309

Quartus II设计程序计数器PC、由PC产生ROM的地址(含仿真)

设计程序计数器PC,正常工作模式下,系统启动,设置PC的初始值,之后每来一个clk,PC的输出值应该增加1;当程序发生跳转时,PC的值应该发生改变,级将跳转的地址值赋进行输出。PC后端连接一个ROM,由PC来产生ROM的地址,仿真进行验证。

m0_73700621的博客 1815

set_bit、clear_bit、wait_on_bit_lock、wake_up_bit整理

set_bit、clear_bit、wait_on_bit_lock、wake_up_bit整理

qq_27392987的博客 2788

腾讯2020 unreal虚幻面试题

解析一下,腾讯的unreal客户端面试题: 1.lua性能优化的方向 申请的内存,是造成卡顿的原因之一,在C++与lua交互中,如果参数或者返回值有类变量,那么这个变量不会再交互的栈中存放数据,而会通过申请内存,再指定在交互,这样就造成卡顿,因此可以通过把C++的对象的每个参数分开单独传递,可以解决问题,当时我们游戏改完这个问题后,整体提升了2帧。 其实,主要是Tick循环中,减少与C++的交互,需要的数据不要每帧都去拿,其次,经常拿的一些数据(比方说主角信息),可以做一个lua缓存,...

qq_17347313的博客 1万+

ArcGIS Pro中协同区位熵方法在商业选址分析中的应用

本文详细介绍了如何利用ArcGIS Pro中的协同区位熵方法进行科学的商业选址分析。该方法通过量化目标客户与竞争对手在空间上的关联程度,精准识别市场空白区与竞争红海,帮助决策者从“凭感觉”转向“用数据说话”,有效提升选址成功率和投资回报。

weixin_29214559的博客 359

原子性操作atomic_t

内核定义了atomic_t 数据类型,作为对整数计数器的原子操作的基础。 各个CPU平台有各自的原子操作实现方式,基本都是通过汇编实现的。  原子操作 原子操作是Linux中提供的一种实现同步的方法,所谓原子操作是指某一操作在执行过程中是不可以被打断的,它要么全部执行完毕,要么就一点也不执行。也就是说原子操作是绝对不会出现该操作执行了一半,内核又去执行其他操作的情况。 原子操作分为原子位

被遗忘的前辈 1万+

在AutoDL的A100云服务器上,用Docker搞定Sapien+RobotWin环境(附完整conda list)

本文详细介绍了在AutoDL的A100云服务器上使用Docker构建Sapien+RobotWin环境的完整指南。通过配置Vulkan渲染管线、精确复现Conda环境以及解决常见问题,帮助研究人员高效搭建稳定的机器人仿真开发环境,特别适合需要高性能GPU加速的复杂仿真任务。

weixin_30689307的博客 430

【Linux】linux内核原子操作的实现

所谓原子操作,就是“不可中断的一个或一系列操作”。 硬件级的原子操作:在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是“原子操作”,因为中断只发生在指令边缘。在多处理器结构中(Symmetric Multi-Processor)就不同了,由于系统中有多个处理器独立运行,即使能在单条指令中完成的操作也有可能受到干扰。在X86平台生,CPU提供了在指令执行

AG的博客 1482

C++常见面试题总结

C++常见面试题总结

xu_fu_yong的专栏 8万+

基于CNN-LSTM融合模型的环卫车动态称重算法研究:从频率感知到精准质量估计

摘要: 针对环卫车辆动态称重需求,本研究提出一种基于CNN-LSTM融合网络的频率分析方法。通过在实验室构建振动频率采集系统,获取不同载重下的结构响应数据,经MATLAB进行信号处理和特征提取后,建立频率-质量的非线性映射模型。实验表明,该算法在静态和模拟动态场景下平均绝对误差小于1.5%,显著优于传统方法,为环卫车智能化管理提供了高精度实时称重方案。 关键词: 动态称重;深度学习;频率分析;CNN-LSTM;MATLAB。

AI的内部世界的博客 374

int linux 原子操作_在Linux内核中读写原子操作实现

Recently I've peeked into the Linux kernel implementation of an atomic read and write and a few questions came up.First the relevant code from the ia64 architecture:typedef struct {int counter;} atomi...

weixin_30878135的博客 210

计算机视觉教程2-3:图解双线性插值算法&最近邻插值算法

提出此算法的背景是基于图片的缩放,在图片缩放的过程中,实质上就是将原图像像素矩阵像素值,填到目标图像像素矩阵中,目标图像像素矩阵可能比原图像像素矩阵大(图片放大),也可能小(图片缩小)。我们假设图片的宽(WidthWidthWidth)和高(HeightHeightHeight)是按同比例缩放的,那么 srcXsrcWidth=dstXdstWidth\frac{srcX}{srcWidth}= \frac{dstX}{dstWidth}srcWidthsrcX​=dstWidthdstX​ ...

FRIGIDWINTER的博客 7280

int linux 原子操作,在Linux内核中读写原子操作实现

Recently I've peeked into the Linux kernel implementation of an atomic read and write and a few questions came up.First the relevant code from the ia64 architecture:typedef struct {int counter;} atomi...

weixin_39754831的博客 99

【数据湖仓】:Iceberg、Hudi 和 Paimon 对比

适用于需要多计算引擎支持和复杂查询的离线批处理场景。其中立性和灵活性使其成为构建新型数据平台的理想选择。:在需要高频数据更新和高效小文件管理的场景中表现出色,适合实时数据处理和增量数据更新的业务需求。:专为实时流式数据处理设计,适用于对数据实时性要求高的业务场景,如实时数据分析和实时指标监控。通过以上分析,可以看出 Apache Iceberg、Apache Hudi 和 Apache Paimon 各有其独特的优势和适用场景。企业应根据自身业务需求和技术栈选择合适的数据湖仓方案,以充分发挥其价值。

欢迎来到我的博客,一起探索代码里的世界! 1607

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

linux内核对原子操作的实现

/* * ARMv6 UP and SMP safe atomic ops. We use load exclusive and * store exclusive to ensure that these are atomic. We may loop * to ensure that the update happens. */ #define ATOMIC_OP(op, c_op, asm_op) \ static inline void arch_atomic_##op(in.

u010843408的专栏 961

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

Linux内核原子操作实现原理几问几答

用锁当然可以实现独占原子性的操作,但是要么引发调度,要么自旋空转,对于简单的内存操作没有必要。cmpxchg在各个平台都有各自的实现,且其实现的方式都以原子性指令实现,比如x86架构的cmpxchg指令,ARM64架构的ldxr和stxr指令。首先,armv8.0架构上可以调用casx指令,但是个人理解应该是伪指令的形式实现的,本质上还是利用ldxr和stxr实现的。高并发访问的时候可以根据需求使用原子操作API,可以使用共用定义基于cmpxchg实现的API也可以使用架构优化的版本。

906

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

linux原子操作实现原理,Linux2.6内核的原子操作的实现

Linux2.6.18之后,删除了和,GCC提供了内置的原子操作函数,更适合用户态的程序使用。现在atomic.h在内核头文件中,不在gcc默认搜索路径下,即使像下面这样强行指定路径,还是会出现编译错误。1. #includegcc从4.1.2提供了__sync_*系列的built-in函数,用于提供加减和逻辑运算的原子操作,可以对1,2,4或8字节长度的数值类型或指针进行原子操作,其声明如下1...

weixin_30016961的博客 234

Linux内核原子操作实战:从命名规则到ARMv8架构实现

本文深入解析Linux内核原子操作的命名规则、使用场景与ARMv8架构下的实现差异。通过剖析函数命名逻辑(如_fetch取旧值、_return取新值)和内存屏障语义(acquire/release),指导开发者根据并发需求选择正确函数。重点对比了ARMv8.1 LSE指令集与LL/SC机制的性能差异,为编写高效、正确的内核同步代码提供实战指导。

4k5l6j7h8的博客 725

Linux内核中的原子操作的实现

原子操作主要使用汇编语言实现,因为使用到总线锁和寄存器等操作。

陈嘉怡的专栏 2006

Linux内核原子操作(2)接口与实现

处理器在提供了硬件级别的原子操作保障后,软件可以基于硬件机制实现软件级别的原子操作接口,提供给软件模块使用。

Aspiresky的专栏 1006

Linux2.6内核的原子操作的实现

Linux2.6.18之后,删除了和,GCC提供了内置的原子操作函数,更适合用户态的程序使用。现在atomic.h在内核头文件中,不在gcc默认搜索路径下,即使像下面这样强行指定路径,还是会出现编译错误。 1.  #include  gcc从4.1.2提供了__sync_*系列的built-in函数,用于提供加减和逻辑运算的原子操作,可以对1,2,4或8

740

Linux内核原子操作实战:从命名规则到ARMv8指令级实现(附避坑指南)

本文深入解析Linux内核原子操作的命名规则、内存序语义及其在ARMv8架构下的实现机制。通过对比传统的LL/SC循环与现代LSE指令集的性能差异,并结合实战案例,为开发者提供了一份详尽的并发编程避坑指南,帮助编写更健壮、高效的内核代码。

sat99的博客 445
上一篇: 单链表的逆置-C++实现
下一篇: C语言的预处理问题
计算机与认知
博客等级 码龄14年 31粉丝 19原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值