ThreadX内核源码分析(SMP) - 核间互斥(arm)

1、核间互斥介绍(_tx_thread_smp_protection)

        在单核的ThreadX内核中,内核的临界资源互斥通过关中断实现;在多核cpu上,关闭整个cpu的代价比较大,单核上仍然使用关中断实现(其他核的中断仍然开启),但是与其他核之间互斥通过_tx_thread_smp_protection实现:

        首先,单核已经关中断,那么正在访问资源的线程不会被切换出去,在退出临界资源之前,当前核上的线程不会被中断,也就是在退出临界资源前当前核不会有其他线程执行,当前核的线程之间是互斥的;

        其次,_tx_thread_smp_protection标记了占用_tx_thread_smp_protection的核,其他核的线程如果检查到_tx_thread_smp_protection被其他核占用,那么等待_tx_thread_smp_protection被释放,因此实现了核间互斥。

1.1、TX_THREAD_SMP_PROTECT结构

typedef struct TX_THREAD_SMP_PROTECT_STRUCT
{
    ULONG           tx_thread_smp_protect_in_force;
    ULONG           tx_thread_smp_protect_core;
    ULONG           tx_thread_smp_protect_count;
    ULONG           tx_thread_smp_protect_pad_0;
    ULONG           tx_thread_smp_protect_pad_1;
    ULONG           tx_thread_smp_protect_pad_2;
    ULONG           tx_thread_smp_protect_pad_3;
} TX_THREAD_SMP_PROTECT;

tx_thread_smp_protect_in_force: 是否被占用(0没有被占用,1被占用)

tx_thread_smp_protect_core: 被哪个cpu核占用(0xFFFFFFFF无效)

tx_thread_smp_protect_count: 被占用次数。

2、临界资源保护(_tx_thread_smp_protect)

        _tx_thread_smp_protect保护临界资源,实现的功能类似linux内核的自旋锁,主要用于核间的互斥,一个核在访问临界资源的时候,避免其他核访问该临界资源。

        _tx_thread_smp_protect基本原理就是关闭当前核的中断,检查_tx_thread_smp_protection是否被当前核占用,如果是,那么对_tx_thread_smp_protection的占用次数加1,返回关中断之前的中断标志(调用_tx_thread_smp_protect的时候可能就已经是关闭了中断,那么退出临界资源应该恢复关中断的状态,不能简单开启中断;内核的所有临界资源都用_tx_thread_smp_protection保护,存在临界资源嵌套,也就是在临界资源里面再次调用_tx_thread_smp_protect,所以前面对_tx_thread_smp_protection的占用次数加1)。

         _tx_thread_smp_protect主要实现代码如下:

076     .global  _tx_thread_smp_protect
077     .type    _tx_thread_smp_protect, @function
078 _tx_thread_smp_protect:
079 
080     /* Disable interrupts so we don't get preempted.  */
081 
082     MRS     x0, DAIF                            // Pickup current interrupt posture
083     MSR     DAIFSet, 0x3                        // Lockout interrupts
084 
085     /* Pickup the CPU ID.   */
086 
087     MRS     x2, MPIDR_EL1                       // Pickup the core ID
088 #ifdef TX_ARMV8_2
089 #if TX_THREAD_SMP_CLUSTERS > 1
090     UBFX    x7, x2, #16, #8                     // Isolate cluster ID
091 #endif
092     UBFX    x2, x2, #8, #8                      // Isolate core ID
093 #else
094 #if TX_THREAD_SMP_CLUSTERS > 1
095     UBFX    x7, x2, #8, #8                      // Isolate cluster ID
096 #endif
097     UBFX    x2, x2, #0, #8                      // Isolate core ID
098 #endif
099 #if TX_THREAD_SMP_CLUSTERS > 1
100     ADDS    x2, x2, x7, LSL #2                  // Calculate CPU ID
101 #endif
102 
103     LDR     x1, =_tx_thread_smp_protection      // Build address to protection structure
104     LDR     w3, [x1, #4]                        // Pickup the owning core
105     CMP     w3, w2                              // Is it this core?
106     BEQ     _owned                              // Yes, the protection is already owned
107 
108     LDAXR   w4, [x1, #0]                        // Pickup the protection flag
109     CBZ     w4, _get_protection                 // Yes, get the protection
110     MSR     DAIF, x0                            // Restore interrupts
111     ISB                                         //
112 #ifdef TX_ENABLE_WFE
113     WFE                                         // Go into standby
114 #endif
115     B       _tx_thread_smp_protect              // On waking, restart the protection attempt
116 
117 _get_protection:
118     MOV     x4, #1                              // Build lock value
119     STXR    w5, w4, [x1]                        // Attempt to get the protection
120     CBZ     w5, _got_protection                 // Did it succeed?  w5 = 0 means success!
121     MSR     DAIF, x0                            // Restore interrupts
122     B       _tx_thread_smp_protect              // Restart the protection attempt
123     
124 _got_protection:
125     DMB     ISH                                 // 
126     STR     w2, [x1, #4]                        // Save owning core
127 _owned:
128     LDR     w5, [x1, #8]                        // Pickup ownership count
129     ADD     w5, w5, #1                          // Increment ownership count
130     STR     w5, [x1, #8]                        // Store ownership count
131     DMB     ISH                                 //
132     RET

2.1、关当前核的中断

保存DAIF到x0寄存器(x0也是函数的返回值),设置DAIF的IF标志位,禁止IRQ、FIQ中断。

082     MRS     x0, DAIF                            // Pickup current interrupt posture // 保存关中断前的DAIF(下一行会修改IF标志位,退出临界资源后需要恢复DAIF;x0在这里除了保存DAIF外,也是_tx_thread_smp_protect的返回值)
083     MSR     DAIFSet, 0x3                        // Lockout interrupts // IF位设置为1,禁止当前核的IRQ、FIQ中断(当前核实现互斥,当前核在访问临界资源过程中不会被切换出去,当前核没有中断及线程被调度执行,当前核的临界资源不存在被中断的情况)

2.2、获取当前核的cpu ID

通过MPIDR_EL1寄存器读取当前核的cpu ID,保存到x2寄存器里面。

085     /* Pickup the CPU ID.   */
086 
087     MRS     x2, MPIDR_EL1                       // Pickup the core ID
088 #ifdef TX_ARMV8_2
089 #if TX_THREAD_SMP_CLUSTERS > 1
090     UBFX    x7, x2, #16, #8                     // Isolate cluster ID
091 #endif
092     UBFX    x2, x2, #8, #8                      // Isolate core ID
093 #else
094 #if TX_THREAD_SMP_CLUSTERS > 1
095     UBFX    x7, x2, #8, #8                      // Isolate cluster ID
096 #endif
097     UBFX    x2, x2, #0, #8                      // Isolate core ID
098 #endif
099 #if TX_THREAD_SMP_CLUSTERS > 1
100     ADDS    x2, x2, x7, LSL #2                  // Calculate CPU ID
101 #endif

2.3、比较占用_tx_thread_smp_protection的cpu ID

        获取占用_tx_thread_smp_protection的cpu ID(_tx_thread_smp_protection没有被占用时,cpu ID是一个无效值,不等于任何cpu ID),如果是当前cpu占用了_tx_thread_smp_protection,那么不需要再次占用,跳转到_owned,只需要简单对占用计算器加1即可。

103     LDR     x1, =_tx_thread_smp_protection      // Build address to protection structure // x1 = &_tx_thread_smp_protection
104     LDR     w3, [x1, #4]                        // Pickup the owning core // w3 = _tx_thread_smp_protection.tx_thread_smp_protect_core,通过成员变量的偏移读取成员变量的值
105     CMP     w3, w2                              // Is it this core? // w2: 当前核的cpu ID,w3: 占用_tx_thread_smp_protection核的cpu ID
106     BEQ     _owned                              // Yes, the protection is already owned // 占用_tx_thread_smp_protection的cpu ID等于当前核的cpu ID(在临界资源里面再次调用_tx_thread_smp_protect),跳转到_owned(已经获取到了...)

2.4、检查_tx_thread_smp_protection是否被占用

        _tx_thread_smp_protection的cpu ID有两种情况,一种是其他cpu的ID,另外一种是无效cpu ID,如果_tx_thread_smp_protection的cpu ID不是当前核的cpu ID,那么要检查这两种情况,主要就是检查tx_thread_smp_protect_in_force,如果tx_thread_smp_protect_in_force为0,那么_tx_thread_smp_protection就没有被占用;

        对tx_thread_smp_protect_in_force的判断改写要实现原子操作,读写过程并没有暂停其他cpu,因此使用LDAXR/STXR实现多核对tx_thread_smp_protect_in_force的互斥;

        尝试获取_tx_thread_smp_protection时,先检查tx_thread_smp_protect_in_force是否为0,如果为0,那么就可以获取_tx_thread_smp_protection,跳转到_get_protection去获取_tx_thread_smp_protection,否则恢复DAIF(主要是中断使能位,允许当前核处理中断),然后跳转到_tx_thread_smp_protect,重新去获取_tx_thread_smp_protection,类似ticket自旋锁。

103     LDR     x1, =_tx_thread_smp_protection      // Build address to protection structure // x1 = &_tx_thread_smp_protection
104     LDR     w3, [x1, #4]                        // Pickup the owning core // w3 = _tx_thread_smp_protection.tx_thread_smp_protect_core,通过成员变量的偏移读取成员变量的值
105     CMP     w3, w2                              // Is it this core? // w2: 当前核的cpu ID,w3: 占用_tx_thread_smp_protection核的cpu ID
106     BEQ     _owned                              // Yes, the protection is already owned // 占用_tx_thread_smp_protection的cpu ID等于当前核的cpu ID(在临界资源里面再次调用_tx_thread_smp_protect),跳转到_owned(已经获取到了...)
107 
108     LDAXR   w4, [x1, #0]                        // Pickup the protection flag // 读_tx_thread_smp_protection.tx_thread_smp_protect_in_force
109     CBZ     w4, _get_protection                 // Yes, get the protection // _tx_thread_smp_protection.tx_thread_smp_protect_in_force为0(LDAXR读的时候,_tx_thread_smp_protection没被其他核占用),跳转到_get_protection去获取_tx_thread_smp_protection
110     MSR     DAIF, x0                            // Restore interrupts // _tx_thread_smp_protection被其他线程占用,恢复DAIF(对于非嵌套进入临界资源,这里可能恢复中断,如果有中断就绪,那么应该尽快处理中断,所以这里要恢复DAIF)
111     ISB                                         //
112 #ifdef TX_ENABLE_WFE
113     WFE                                         // Go into standby
114 #endif
115     B       _tx_thread_smp_protect              // On waking, restart the protection attempt // 跳转到_tx_thread_smp_protect,重新尝试获取_tx_thread_smp_protection(上面已经恢复DAIF,_tx_thread_smp_protect开始的地方又会关中断,在恢复关中断几条指令间,如果有中断,cpu会优先处理中断)

2.5、尝试获取_tx_thread_smp_protection

        前面LDAXR并没有阻止其他核获取_tx_thread_smp_protection,LDAXR读取到_tx_thread_smp_protection没被占用之后,可能有其他核也去获取_tx_thread_smp_protection,所以使用STXR指令去写_tx_thread_smp_protection.tx_thread_smp_protect_in_force,写失败就表明别其他核改写了,如果写成功那么就获取到了_tx_thread_smp_protection。

118     MOV     x4, #1                              // Build lock value // LDAXR读取到_tx_thread_smp_protection.tx_thread_smp_protect_in_force为0,那么如果在STXR指令之前没有其他核修改_tx_thread_smp_protection.tx_thread_smp_protect_in_force,那么STXR指令就会成功,否则就会失败
119     STXR    w5, w4, [x1]                        // Attempt to get the protection // 尝试写1到_tx_thread_smp_protection.tx_thread_smp_protect_in_force(LDAXR并没有阻止其他核改写_tx_thread_smp_protection.tx_thread_smp_protect_in_force,如果其他核没有改写的话(读完之后,没有其他cpu获取到_tx_thread_smp_protection),那么STXR将会成功,否则,如果其他核先写_tx_thread_smp_protection.tx_thread_smp_protect_in_force(先获取_tx_thread_smp_protection),那么STXR将会失败)
120     CBZ     w5, _got_protection                 // Did it succeed?  w5 = 0 means success! // 1成功写入到_tx_thread_smp_protection.tx_thread_smp_protect_core,成功获取到_tx_thread_smp_protection,跳转到_got_protection
121     MSR     DAIF, x0                            // Restore interrupts // 读取_tx_thread_smp_protection.tx_thread_smp_protect_in_force到写_tx_thread_smp_protection.tx_thread_smp_protect_in_force过程,有其他核先写了_tx_thread_smp_protection.tx_thread_smp_protect_in_force,当前核写失败,需要重新去获取_tx_thread_smp_protection,与前面一样,这里要恢复DAIF,让cpu优先处理中断
122     B       _tx_thread_smp_protect              // Restart the protection attempt // 跳转到_tx_thread_smp_protect,重新获取_tx_thread_smp_protection

2.6、更新_tx_thread_smp_protection

第一次占用_tx_thread_smp_protection那么需要设置tx_thread_smp_protect_core,否则之需要对占用次数tx_thread_smp_protect_count加1即可,然后返回上一级函数(注意临界资源不能开中断,不能被抢占)。

124 _got_protection:
125     DMB     ISH                                 // 
126     STR     w2, [x1, #4]                        // Save owning core // 当前核的cpu ID写到_tx_thread_smp_protection.tx_thread_smp_protect_core
127 _owned:
128     LDR     w5, [x1, #8]                        // Pickup ownership count
129     ADD     w5, w5, #1                          // Increment ownership count
130     STR     w5, [x1, #8]                        // Store ownership count // 占用次数_tx_thread_smp_protection.tx_thread_smp_protect_count加1
131     DMB     ISH                                 //
132     RET // 返回,这里没有恢复也不能恢复DAIF,需要在退出临界资源时恢复DAIF(假设在临界资源里面恢复了中断,然后中断服务程序里面唤醒了更高优先级的线程或者时间片用完了调度其他线程,那么_tx_thread_smp_protection就得不到释放,_tx_thread_smp_protect就会进入无线循环!!!)

3、取消保护(_tx_thread_smp_unprotect)

退出临界资源。_tx_thread_smp_unprotect与_tx_thread_smp_protect函数一样,也是先关闭了当前核的中断(正常情况,临界资源退出时中断本来就是关闭的)并且读取了当前核的cpu ID(如果不是占用_tx_thread_smp_protection的核释放_tx_thread_smp_protection,那么就是错误的调用,不能释放其他核占用的_tx_thread_smp_protection),然后对占用次数tx_thread_smp_protect_count减1,如果tx_thread_smp_protect_count不为0,那么还要等待其他临界资源退出。

实现代码如下:

072     .global  _tx_thread_smp_unprotect
073     .type    _tx_thread_smp_unprotect, @function
074 _tx_thread_smp_unprotect:
075     MSR     DAIFSet, 0x3                        // Lockout interrupts
076 
077     MRS     x1, MPIDR_EL1                       // Pickup the core ID
078 #ifdef TX_ARMV8_2
079 #if TX_THREAD_SMP_CLUSTERS > 1
080     UBFX    x2, x1, #16, #8                     // Isolate cluster ID
081 #endif
082     UBFX    x1, x1, #8, #8                      // Isolate core ID
083 #else
084 #if TX_THREAD_SMP_CLUSTERS > 1
085     UBFX    x2, x1, #8, #8                      // Isolate cluster ID
086 #endif
087     UBFX    x1, x1, #0, #8                      // Isolate core ID
088 #endif
089 #if TX_THREAD_SMP_CLUSTERS > 1
090     ADDS    x1, x1, x2, LSL #2                  // Calculate CPU ID
091 #endif
092 
093     LDR     x2,=_tx_thread_smp_protection       // Build address of protection structure
094     LDR     w3, [x2, #4]                        // Pickup the owning core
095     CMP     w1, w3                              // Is it this core?
096     BNE     _still_protected                    // If this is not the owning core, protection is in force elsewhere
097 
098     LDR     w3, [x2, #8]                        // Pickup the protection count
099     CMP     w3, #0                              // Check to see if the protection is still active
100     BEQ     _still_protected                    // If the protection count is zero, protection has already been cleared
101 
102     SUB     w3, w3, #1                          // Decrement the protection count
103     STR     w3, [x2, #8]                        // Store the new count back
104     CMP     w3, #0                              // Check to see if the protection is still active
105     BNE     _still_protected                    // If the protection count is non-zero, protection is still in force
106     LDR     x2,=_tx_thread_preempt_disable      // Build address of preempt disable flag
107     LDR     w3, [x2]                            // Pickup preempt disable flag
108     CMP     w3, #0                              // Is the preempt disable flag set?
109     BNE     _still_protected                    // Yes, skip the protection release
110 
111     LDR     x2,=_tx_thread_smp_protection       // Build address of protection structure
112     MOV     w3, #0xFFFFFFFF                     // Build invalid value
113     STR     w3, [x2, #4]                        // Mark the protected core as invalid
114     DMB     ISH                                 // Ensure that accesses to shared resource have completed
115     MOV     w3, #0                              // Build release protection value
116     STR     w3, [x2, #0]                        // Release the protection
117     DSB     ISH                                 // To ensure update of the protection occurs before other CPUs awake
118 
119 _still_protected:
120 #ifdef TX_ENABLE_WFE
121     SEV                                         // Send event to other CPUs, wakes anyone waiting on the protection (using WFE)
122 #endif
123     MSR     DAIF, x0                            // Restore interrupt posture
124     RET

3.1、获取当前核的cpu ID

        通过MPIDR_EL1获取当前核的cpu ID,保存到x1寄存器。(需要检查_tx_thread_smp_protection是否是被当前核占用,不是的话,不能释放_tx_thread_smp_protection)

077     MRS     x1, MPIDR_EL1                       // Pickup the core ID
078 #ifdef TX_ARMV8_2
079 #if TX_THREAD_SMP_CLUSTERS > 1
080     UBFX    x2, x1, #16, #8                     // Isolate cluster ID
081 #endif
082     UBFX    x1, x1, #8, #8                      // Isolate core ID
083 #else
084 #if TX_THREAD_SMP_CLUSTERS > 1
085     UBFX    x2, x1, #8, #8                      // Isolate cluster ID
086 #endif
087     UBFX    x1, x1, #0, #8                      // Isolate core ID
088 #endif
089 #if TX_THREAD_SMP_CLUSTERS > 1
090     ADDS    x1, x1, x2, LSL #2                  // Calculate CPU ID
091 #endif

3.2、检查cpu ID

如果占用_tx_thread_smp_protection的核不是当前核,那么跳转到_still_protected(_still_protected恢复了DAIF,_tx_thread_smp_unprotect需要interrupt_save这个局部变量,而这个变量是_tx_thread_smp_protect返回的,也是在_tx_thread_smp_protect对应的宏TX_DISABLE定义的,那么调用_tx_thread_smp_unprotect能编译通过的前提是必须定义了interrupt_save,必须调用了_tx_thread_smp_protect,_tx_thread_smp_protection不是被当前核占用的条件就是_tx_thread_smp_unprotect多调用了,因此重复恢复DAIF的值,正常情况下是不影响的)

093     LDR     x2,=_tx_thread_smp_protection       // Build address of protection structure
094     LDR     w3, [x2, #4]                        // Pickup the owning core // 读取_tx_thread_smp_protection.tx_thread_smp_protect_core
095     CMP     w1, w3                              // Is it this core? // core比较
096     BNE     _still_protected                    // If this is not the owning core, protection is in force elsewhere // _tx_thread_smp_protection不是被当前核占用,跳转到_still_protected,不能释放_tx_thread_smp_protection
097 
098     LDR     w3, [x2, #8]                        // Pickup the protection count
099     CMP     w3, #0                              // Check to see if the protection is still active
100     BEQ     _still_protected                    // If the protection count is zero, protection has already been cleared // 占用计数器已经为0了,实际没有被占用,但是也没有让其他核占用(后面如果禁止抢占的话,是不会修改_tx_thread_smp_protection.tx_thread_smp_protect_core)

3.3、释放_tx_thread_smp_protection

_tx_thread_smp_protection占用计数器tx_thread_smp_protect_count减1,如果仍然占用,跳转到_still_protected,还在其他临界资源里面。

102     SUB     w3, w3, #1                          // Decrement the protection count
103     STR     w3, [x2, #8]                        // Store the new count back // 占用计数器_tx_thread_smp_protection.tx_thread_smp_protect_count减1
104     CMP     w3, #0                              // Check to see if the protection is still active
105     BNE     _still_protected                    // If the protection count is non-zero, protection is still in force // 占用计数器_tx_thread_smp_protection.tx_thread_smp_protect_count不为0,仍然占用,跳转到_still_protected

3.4、抢占检查

        从代码上看,退出临界资源后,如果禁止抢占,那么也不真正释放_tx_thread_smp_protection,也就是不让其他核获取到_tx_thread_smp_protection。

        猜测这个抢占检查应该是为了让更高优先级的线程尽快执行,简单的说就是禁止抢占的情况下,各个cpu核上等待_tx_thread_smp_protection的线程不一定是最高优先级就绪线程(在禁止抢占过程,有中断或者其他线程唤醒更高优先级就绪线程,但是禁止了抢占,所以等待_tx_thread_smp_protection的线程并不会被切换出去),如果禁止抢占期间立即释放_tx_thread_smp_protection,那么别的核就可以获取到_tx_thread_smp_protection并关闭中断,这就会导致禁止抢占期间唤醒的高优先级线程得不到及时的调度,所以禁止抢占期间不立即释放_tx_thread_smp_protection。

106     LDR     x2,=_tx_thread_preempt_disable      // Build address of preempt disable flag // 没有占用_tx_thread_smp_protection,已经完全退出了临界资源,检查_tx_thread_preempt_disable
107     LDR     w3, [x2]                            // Pickup preempt disable flag
108     CMP     w3, #0                              // Is the preempt disable flag set?
109     BNE     _still_protected                    // Yes, skip the protection release // 禁止抢占计数器_tx_thread_preempt_disable不为0,跳过_tx_thread_smp_protection释放(不释放_tx_thread_smp_protection)

3.5、释放_tx_thread_smp_protection

主要是恢复_tx_thread_smp_protection的成员变量为未占用状态(tx_thread_smp_protect_in_force、tx_thread_smp_protect_core),然后恢复DAIF寄存器。

111     LDR     x2,=_tx_thread_smp_protection       // Build address of protection structure
112     MOV     w3, #0xFFFFFFFF                     // Build invalid value
113     STR     w3, [x2, #4]                        // Mark the protected core as invalid
114     DMB     ISH                                 // Ensure that accesses to shared resource have completed
115     MOV     w3, #0                              // Build release protection value
116     STR     w3, [x2, #0]                        // Release the protection
117     DSB     ISH                                 // To ensure update of the protection occurs before other CPUs awake
118 
119 _still_protected:
120 #ifdef TX_ENABLE_WFE
121     SEV                                         // Send event to other CPUs, wakes anyone waiting on the protection (using WFE)
122 #endif
123     MSR     DAIF, x0                            // Restore interrupt posture
124     RET

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux SMP(Symmetric Multi-Processing)是一种在多处理器系统中运行的操作系统。该操作系统支持多个处理器核心之间的并行处理,能够更高效地利用系统资源,提升整体性能。 Linux SMP源码分析是对Linux SMP操作系统的内部实现进行深入研究和解析。其目的是理解和掌握Linux SMP操作系统的工作原理与核心机制,以便于进行系统调优和性能优化。 进行Linux SMP源码分析的过程中,首先要了解Linux SMP操作系统的基本结构和组成部分。这包括内核、进程调度器、内存管理器、文件系统等模块。然后,通过阅读和分析内核源代码,深入了解每个模块的实现细节和相互之间的关联关系。 在分析Linux SMP源代码的过程中,需要关注以下几个关键点: 1. 处理器调度:了解Linux SMP是如何进行多个处理器核心之间的任务调度和负载均衡的。需要分析调度算法和策略,以及与进程管理器的交互过程。 2. 内存管理:分析内核是如何进行多核心的内存管理和共享内存的管理。需要了解页面置换算法、缓存一致性和锁机制等相关知识。 3. 进程间通信:探究Linux SMP是如何实现多核心之间的进程间通信。需要研究信号量、互斥锁、条件变量等IPC机制的实现细节。 4. 文件系统:深入研究Linux SMP对文件系统的支持。了解多核心环境下的文件并发访问和文件系统缓存等相关内容。 通过对Linux SMP源码的详细分析,可以更好地理解和掌握操作系统的工作原理和机制,提高系统的性能和稳定性。此外,深入研究Linux SMP源码还可以为开发者提供更大的灵活性和自定义能力,实现定制化的功能和优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值