进程切换与TSS

本文介绍了Linux内核如何利用任务状态段(TSS)实现高效进程切换。与Intel原设计不同,Linux为每个CPU分配一个TSS,仅更新其中的ESP0以指向新进程的内核栈,而非替换整个TSS,从而减少了任务切换时的开销。
部署运行你感兴趣的模型镜像

[转] http://www.eefocus.com/article/09-06/74895s.html

Intel i386 体系结构包括了一个特殊的段类型,叫任务状态段(TSS),如图5.4所示。每个任务包含有它自己最小长度为104字节的TSS段,在/include/ i386/processor.h 中定义为tss_struct结构:

struct tss_struct {
unsigned short back_link,__blh;
unsigned long esp0;
unsigned short ss0,__ss0h;/*0级堆栈指针,即Linux中的内核级 */
unsigned long esp1;
unsigned short ss1,__ss1h; /* 1级堆栈指针,未用*/
unsigned long esp2;
unsigned short ss2,__ss2h; /* 2级堆栈指针,未用*/
unsigned long __cr3;
unsigned long eip;
unsigned long eflags;
unsigned long eax,ecx,edx,ebx;
unsigned long esp;
unsigned long ebp;
unsigned long esi;
unsiged long edi;
unsigned short es, __esh;
unsigned short cs, __csh;
unsigned short ss, __ssh;
unsigned short ds, __dsh;
unsigned short fs, __fsh;
unsigned short gs, __gsh;
unsigned short ldt, __ldth;
unsigned short trace, bitmap;
unsigned long io_bitmap[IO_BITMAP_SIZE+1];
/*
* pads the TSS to be cacheline-aligned (size is 0x100)
*/
unsigned long __cacheline_filler[5];
};

 每个TSS有它自己 8字节的任务段描述符(Task State Segment Descriptor ,简称TSSD)。这个描述符包括指向TSS起始地址的32位基地址域,20位界限域,界限域值不能小于十进制104(由TSS段的最小长度决定)。TSS描述符存放在GDT中,它是GDT中的一个表项。

后面将会看到,Linux在进程切换时,只用到TSS中少量的信息,因此Linux内核定义了另外一个数据结构,这就是thread_struct 结构

struct thread_struct {
unsigned long esp0;
unsigned long eip;
unsigned long esp;
unsigned long fs;
unsigned long gs;
/* Hardware debugging registers */
unsigned long debugreg[8]; /* %%db0-7 debug registers */
/* fault info */
unsigned long cr2, trap_no, error_code;
/* floating point info */
union i387_union i387;
/* virtual 86 mode info */
struct vm86_struct * vm86_info;
unsigned long screen_bitmap;
unsigned long v86flags, v86mask, v86mode, saved_esp0;
/* IO permissions */
int ioperm;
unsigned long io_bitmap[IO_BITMAP_SIZE+1];
};

用这个数据结构来保存cr2寄存器、浮点寄存器、调试寄存器及指定给Intel 80x86处理器的其他各种各样的信息。需要位图是因为ioperm( ) 及 iopl( )系统调用可以允许用户态的进程直接访问特殊的I/O端口。尤其是,如果把eflag寄存器中的IOPL 域设置为3,就允许用户态的进程访问对应的I/O访问权位图位为0的任何一个I/O端口。

那么,进程到底是怎样进行切换的?

从第三章我们知道,在中断描述符表(IDT)中,除中断门、陷阱门和调用门外,还有一种“任务们”。任务门中包含有TSS段的选择符。当CPU因中断而穿过一个任务门时,就会将任务门中的段选择符自动装入TR寄存器,使TR指向新的TSS,并完成任务切换。CPU还可以通过JMP或CALL指令实现任务切换,当跳转或调用的目标段(代码段)实际上指向GDT表中的一个TSS描述符项时,就会引起一次任务切换。

Intel的这种设计确实很周到,也为任务切换提供了一个非常简洁的机制。但是,由于i386的系统结构基本上是CISC的,通过JMP指令或CALL(或中断)完成任务的过程实际上是“复杂指令”的执行过程,其执行过程长达300多个CPU周期(一个POP指令占12个CPU周期),因此,Linux内核并不完全使用i386CPU提供的任务切换机制。

由于i386CPU要求软件设置TR及TSS,Linux内核只不过“走过场”地设置TR及TSS,以满足CPU的要求。但是,内核并不使用任务门,也不使用JMP或CALL指令实施任务切换内核只是在初始化阶段设置TR,使之指向一个TSS,从此以后再不改变TR的内容了。也就是说,每个CPU(如果有多个CPU)在初始化以后的全部运行过程中永远使用那个初始的TSS。同时,内核也不完全依靠TSS保存每个进程切换时的寄存器副本,而是将这些寄存器副本保存在各个进程自己的内核栈中(参见上一章task_struct结构的存放)。

这样以来,TSS中的绝大部分内容就失去了原来的意义。那么,当进行任务切换时,怎样自动更换堆栈?我们知道,新任务的内核栈指针(SS0和ESP0)应当取自当前任务的TSS,可是,Linux中并不是每个任务就有一个TSS,而是每个CPU只有一个TSS。Intel原来的意图是让TR的内容(即TSS)随着任务的切换而走马灯似地换,而在Linux内核中却成了只更换TSS中的SS0和ESP0,而不更换TSS本身,也就是根本不更换TR的内容。这是因为,改变TSS中SS0和ESP0所化的开销比通过装入TR以更换一个TSS要小得多。因此,在Linux内核中,TSS并不是属于某个进程的资源,而是全局性的公共资源。在多处理机的情况下,尽管内核中确实有多个TSS,但是每个CPU仍旧只有一个TSS。

5.4.2 进程切换

前面所介绍的schedule()中调用了switch_to宏,这个宏实现了进程之间的真正切换,其代码存放于include/ i386/system.h:

1 #define switch_to(prev,next,last) do { \
2 asm volatile("pushl %%esi\n\t" \
3 "pushl %%edi\n\t" \
4 "pushl %%ebp\n\t" \
5 "movl %%esp,%0\n\t" /* save ESP */ \
6 "movl %3,%%esp\n\t" /* restore ESP */ \
7 "movl $1f,%1\n\t" /* save EIP */ \
8 "pushl %4\n\t" /* restore EIP */ \
9 "jmp __switch_to\n" \
10 "1:\t" \
11 "popl %%ebp\n\t" \
12 "popl %%edi\n\t" \
13 "popl %%esi\n\t" \
14 :"=m" (prev->thread.esp),"=m" (prev->thread.eip), \
15 "=b" (last) \
16 :"m" (next->thread.esp),"m" (next->thread.eip), \
17 "a" (prev), "d" (next), \
18 "b" (prev)); \
19 } while (0)

switch_to宏是用嵌入式汇编写成,比较难理解,为描述方便起见,我们给代码编了行号,在此我们给出具体的解释:

· thread的类型为前面介绍的thread_struct结构。
· 输出参数有三个,表示这段代码执行后有三项数据会有变化,它们与变量及寄存器的对应关系如下:
0%与prev->thread.esp对应,1%与prev->thread.eip对应,这两个参数都存放在内存,而2%与ebx寄存器对应,同时说明last参数存放在ebx寄存器中。
· 输入参数有五个,其对应关系如下:
3%与next->thread.esp对应,4%与next->thread.eip对应,这两个参数都存放在内存,而5%,6%和7%分别与eax,edx及ebx相对应,同时说明prev,next以及prev三个参数分别放在这三个寄存器中。表5.1列出了这几种对应关系:

· 第2~4行就是在当前进程prev的内核栈中保存esi,edi及ebp寄存器的内容。
· 第5行将prev的内核堆栈指针ebp存入prev->thread.esp中。
· 第6行把将要运行进程next的内核栈指针next->thread.esp置入esp寄存器中。从现在开始,内核对next的内核栈进行操作,因此,这条指令执行从prev到next真正的上下文切换,因为进程描述符的地址与其内核栈的地址紧紧地联系在一起(参见第四章),因此,改变内核栈就意味着改变当前进程。如果此处引用current的话,那就已经指向next的task_struct结构了。从这个意义上说,进程的切换在这一行指令执行完以后就已经完成。但是,构成一个进程的另一个要素是程序的执行,这方面的切换尚未完成。
· 第7行将标号“1”所在的地址,也就是第一条popl指令(第11行)所在的地址保存在prev->thread.eip中,这个地址就是prev下一次被调度运行而切入时的“返回”地址。
· 第8行将next->thread.eip压入next的内核栈。那么,next->thread.eip究竟指向那个地址?实际上,它就是 next上一次被调离时通过第7行保存的地址,也就是第11行popl指令的地址。因为,每个进程被调离时都要执行这里的第7行,这就决定了每个进程(除了新创建的进程)在受到调度而恢复执行时都从这里的第11行开始。
· 第9行通过jump指令(而不是 call指令)转入一个函数__switch_to()。这个函数的具体实现将在下面介绍。当CPU执行到__switch_to()函数的ret指令时,最后进入堆栈的next->thread.eip就变成了返回地址,这就是标号“1”的地址。
· 第11~13行恢复next上次被调离时推进堆栈的内容。从现在开始,next进程就成为当前进程而真正开始执行。

下面我们来讨论__switch_to()函数。

在调用__switch_to()函数之前,对其定义了fastcall :

extern void FASTCALL(__switch_to(struct task_struct *prev, struct task_struct *next));

fastcall对函数的调用不同于一般函数的调用,因为__switch_to()从寄存器(如表5.1)取参数,而不像一般函数那样从堆栈取参数,也就是说,通过寄存器eax和edx把prev和next 参数传递给__switch_to()函数。

 

void __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
struct thread_struct *prev = &prev_p->thread,
*next = &next_p->thread;
struct tss_struct *tss = init_tss + smp_processor_id();
unlazy_fpu(prev_p);/* 如果数学处理器工作,则保存其寄存器的值*/
/* 将TSS中的内核级(0级)堆栈指针换成next->esp0,这就是next 进程在内核
栈的指针
tss->esp0 = next->esp0;
/* 保存fs和gs,但无需保存es和ds,因为当处于内核时,内核段
总是保持不变*/
asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->fs));
asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->gs));
/*恢复next进程的fs和gs */
loadsegment(fs, next->fs);
loadsegment(gs, next->gs);
/* 如果next挂起时使用了调试寄存器,则装载0~7个寄存器中的6个寄存器,其中第4、5个寄存器没有使用 */
if (next->debugreg[7]){
loaddebug(next, 0);
loaddebug(next, 1);
loaddebug(next, 2);
loaddebug(next, 3);
/* no 4 and 5 */
loaddebug(next, 6);
loaddebug(next, 7);
}
if (prev->ioperm || next->ioperm) {
if (next->ioperm) {
/*把next进程的I/O操作权限位图拷贝到TSS中 */
memcpy(tss->io_bitmap, next->io_bitmap,
IO_BITMAP_SIZE*sizeof(unsigned long));
/* 把io_bitmap在tss中的偏移量赋给tss->bitmap */
tss->bitmap = IO_BITMAP_OFFSET;
} else
/*如果一个进程要使用I/O指令,但是,若位图的偏移量超出TSS的范围,
* 就会产生一个可控制的SIGSEGV信号。第一次对sys_ioperm()的调用会
* 建立起适当的位图 */
tss->bitmap = INVALID_IO_BITMAP_OFFSET;
}
}

从上面的描述我们看到,尽管Intel本身为操作系统中的进程(任务)切换提供了硬件支持,但是Linux内核的设计者并没有完全采用这种思想,而是用软件实现了进程切换,而且,软件实现比硬件实现的效率更高,灵活性更大。

 

-----------------------------------------------------

[转] http://www.linuxidc.com/Linux/2011-03/33367.htm

tss的作用举例:保存不同特权级别下任务所使用的寄存器,特别重要的是esp,因为比如中断后,涉及特权级切换时(一个任务切换),首先要切换栈,这个栈显然是内核栈,那么如何找到该栈的地址呢,这需要从tss段中得到,这样后续的执行才有所依托(在x86机器上,c语言的函数调用是通过栈实现的)。只要涉及地特权环到高特权环的任务切换,都需要找到高特权环对应的栈,因此需要esp2,esp1,esp0起码三个esp,然而Linux只使用esp0。

tss是什么:tss是一个段,段是x86的概念,在保护模式下,段选择符参与寻址,段选择符在段寄存器中,而tss段则在tr寄存器中。

intel的建议:为每一个进程准备一个独立的tss段,进程切换的时候切换tr寄存器使之指向该进程对应的tss段,然后在任务切换时(比如涉及特权级切换的中断)使用该段保留所有的寄存器。

Linux的做法:

1.Linux没有为每一个进程都准备一个tss段,而是每一个cpu使用一个tss段,tr寄存器保存该段。进程切换时,只更新唯一tss段中的esp0字段到新进程的内核栈。

2.Linux的tss段中只使用esp0和iomap等字段,不用它来保存寄存器,在一个用户进程被中断进入ring0的时候,tss中取出esp0,然后切到esp0,其它的寄存器则保存在esp0指示的内核栈上而不保存在tss中。

3.结果,Linux中每一个cpu只有一个tss段,tr寄存器永远指向它。符合x86处理器的使用规范,但不遵循intel的建议,这样的后果是开销更小了,因为不必切换tr寄存器了。

4.在Linux中,对于同一个CPU,所有的进程都使用一个TSS,只是在进程切换时, 被切换到的进程将会把自己的ESP0保存到TSS.ESP0中去,那为什么不把自己的SS0也保存到TSS.SS0中 呢,这是因为所有进程的SS0都是统一的,为内核的SS,而内核在初始化的时候,已经将该TSS.SS0设置为自己的SS,因此无需继续设置SS0。


Linux的实现:

1.定义tss:
struct tss_struct init_tss[NR_CPUS] __cacheline_aligned = { [0 ... NR_CPUS-1] = INIT_TSS };(arch/i386/kernel/init_task.c)
INIT_TSS定义为:
#define INIT_TSS  {                            \
    .esp0        = sizeof(init_stack) + (long)&init_stack,    \
    .ss0        = __KERNEL_DS,                    \
    .esp1        = sizeof(init_tss[0]) + (long)&init_tss[0],    \
    .ss1        = __KERNEL_CS,                    \
    .ldt        = GDT_ENTRY_LDT,                \
    .io_bitmap_base    = INVALID_IO_BITMAP_OFFSET,            \
    .io_bitmap    = { [ 0 ... IO_BITMAP_LONGS] = ~0 },        \
}

 

 

http://www.linuxidc.com/Linux/2011-03/33367.htm

您可能感兴趣的与本文相关的镜像

Stable-Diffusion-3.5

Stable-Diffusion-3.5

图片生成
Stable-Diffusion

Stable Diffusion 3.5 (SD 3.5) 是由 Stability AI 推出的新一代文本到图像生成模型,相比 3.0 版本,它提升了图像质量、运行速度和硬件效率

<think>首先,用户的问题是:“我想了解进程切换的工作原理或相关技术细节 请问进程切换原理及实现机制”。这直接询问进程切换的原理和实现机制。 根据系统级指令: - 所有行内数学表达式必须使用$...$格式。 - 独立公式必须使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如:[^1]。 用户可见层指令: - 回答结构清晰,帮助用户逐步解决问题。 - 尽量保证回答真实可靠。 参考站内引用: - 引用[1]: 讨论了进程和协程切换原理,包括上下文切换等。 - 引用[2]: 描述了修改Linux 0.11中的进程切换,从TSS切换到基于堆栈的切换。 - 引用[3]: 关于实时操作系统的定义。 - 引用[4]: Binder机制原理,涉及IPC通信。 - 引用[5]: Redis原理,涉及IO多路复用。 这些引用中,引用[1]和[2]最相关,因为它们直接讨论进程切换。引用[1]提到了上下文切换,引用[2]具体描述了基于堆栈的切换实现。 我的回答需要结构清晰,逐步解释进程切换的原理和实现机制。确保使用中文。 大纲回答结构: 1. **引入进程切换的概念**:解释什么是进程切换,为什么需要它。 2. **进程切换的原理**:详细描述上下文切换、CPU状态保存和恢复等。 3. **实现机制**:讨论基于TSS和基于堆栈的切换,如引用[2]提到的。 4. **相关技术细节**:包括用户模式和内核模式、中断处理等。 5. **生成相关问题**:在末尾添加相关问题。 使用引用:在相关部分末尾添加引用标识,如[^1]或[^2]。 确保数学表达式正确:例如,如果有寄存器或地址,使用$...$格式。 例如,描述寄存器状态时,可以用$eip$、$esp$等。 独立公式如果需要,比如时间片轮转的公式,但可能不需要复杂公式。重点在描述。 现在,构建回答: - **步骤1: 什么是进程切换?** 解释进程切换是操作系统从一个运行进程切换到另一个的过程,通常通过调度器触发。 - **步骤2: 切换原理** - 上下文切换:保存当前进程的上下文(寄存器状态、程序计数器等),加载下一个进程的上下文。 - 涉及CPU模式:用户模式和内核模式。切换通常在内核模式发生。 - 中断和异常:如时钟中断触发切换。 参考引用[1]:"进程用户模式和内核模式上下文切换" - **步骤3: 实现机制** - 传统方法:使用TSS(Task State Segment)在x86架构中。但效率低。 - 现代方法:基于堆栈的切换,如引用[2]提到的。修改Linux 0.11使用堆栈切换。 描述如何保存和恢复寄存器状态到堆栈。 - 汇编级实现:如switch_to函数。 - **步骤4: 相关细节** - 性能考虑:切换开销。 - 协程切换对比:引用[1]提到协程切换更轻量。 生成相关问题: - 基于主题,生成3-5个相关问题。 例如: 1. 进程切换协程切换有什么区别? 2. 如何优化进程切换的性能? 3. 实时操作系统中的进程切换有什么特殊要求?参考引用[3]。 4. 进程切换在IPC中的作用?参考引用[4]。 确保回答真实可靠:基于操作系统原理知识。 完整回答结构: 1. 介绍进程切换。 2. 详细解释原理。 3. 描述实现机制。 4. 总结。 5. 添加相关问题。</think>### 进程切换原理及实现机制 进程切换是操作系统中一个核心机制,它允许多个进程共享CPU资源,通过调度器在进程间高效切换,实现并发执行。当操作系统决定暂停当前运行进程(称为上下文切换)并启动另一个就绪进程时,就会发生进程切换。这通常由中断(如时钟中断)或系统调用触发。下面我将逐步解释进程切换的工作原理和相关技术细节,确保内容结构清晰、易于理解。回答基于操作系统原理和参考站内引用,力求真实可靠。 #### 1. **进程切换的基本概念** - **为什么需要进程切换?** 操作系统通过时间片轮转(time slicing)等方式,让多个进程“同时”运行。每个进程占用CPU一段时间(时间片),然后切换到下一个进程,防止单个进程独占资源。这提高了系统响应性和资源利用率。 - 例如,在时间片轮转中,时间片长度可表示为 $\Delta t$,其中 $\Delta t$ 是调度器分配的时间单位。 - **关键触发事件**: - **中断(Interrupt)**:硬件事件如时钟中断(每毫秒触发一次),强制CPU切换到内核模式处理切换。 - **异常(Exception)**:如系统调用(例如进程主动调用 `fork()` 或 `exit()`),导致控制权转移到内核。 - 引用[1]提到,进程切换涉及用户模式和内核模式的转换,这确保了安全隔离:用户程序崩溃不会影响内核[^1]。 #### 2. **进程切换的原理:上下文切换** 进程切换的核心是上下文切换(Context Switch),即保存当前进程的状态(上下文),并加载下一个进程的状态。上下文包括所有CPU寄存器的值、程序计数器(PC)、堆栈指针(SP)、内存管理单元(MMU)状态等。 - **上下文切换步骤**: 1. **保存当前进程状态**:当触发中断或异常时,CPU暂停用户模式执行,切换到内核模式。内核保存当前进程的寄存器状态(如 $eip$(指令指针)、$esp$(堆栈指针)、$eax$ 等通用寄存器)到其进程控制块(PCB)中。 2. **调度新进程**:调度器(如Linux的CFS)从就绪队列中选择下一个进程运行。 3. **加载新进程状态**:内核从新进程的PCB中恢复寄存器状态,包括程序计数器(指向下一条指令地址)和堆栈指针。 4. **切换地址空间**:如果新进程使用不同虚拟地址空间,MMU会更新页表基址寄存器(如x86的 $cr3$),确保内存隔离。 - 整个过程在微秒级别完成,但频繁切换会引入开销(例如,保存/恢复寄存器消耗CPU周期)。 - 引用[1]强调,上下文切换是异常控制流的一部分,异常类别包括中断和陷阱,进程切换通常由中断触发[^1]。 - **数学表示**: 上下文切换时间可建模为: $$ T_{\text{switch}} = T_{\text{save}} + T_{\text{load}} + T_{\text{sched}} $$ 其中 $T_{\text{save}}$ 是保存状态时间,$T_{\text{load}}$ 是加载状态时间,$T_{\text{sched}}$ 是调度决策时间。优化目标是减少 $T_{\text{switch}}$。 - **模式切换细节**: - **用户模式到内核模式**:中断发生时,CPU自动保存部分状态(如 $eip$ 和 $eflags$)到内核堆栈,然后执行中断处理程序。 - **内核模式返回用户模式**:切换完成后,通过 `iret` 指令恢复用户态执行。 - 引用[1]指出,用户模式和内核模式的分离是上下文切换的基础,确保系统稳定[^1]。 #### 3. **进程切换的实现机制** 实现机制依赖于硬件架构(如x86)和操作系统设计。历史上,x86使用TSS(Task State Segment),但现代系统更倾向于基于堆栈的切换,以提高效率。 - **传统方法:TSS切换** - 在早期x86系统中,TSS是一个硬件数据结构,存储进程状态(寄存器、I/O权限等)。切换时,CPU通过 `call` 或 `jmp` 指令加载新TSS。 - 缺点:TSS切换涉及内存访问延迟,效率低(每次切换需多次内存读写),且灵活性差。 - 引用[2]提到,Linux 0.11最初使用TSS,但后来被替换为更高效的堆栈切换[^2]。 - **现代方法:基于堆栈的切换** - 这是当前主流实现(如Linux、Windows)。核心思想:手动保存/恢复寄存器状态到内核堆栈,避免TSS开销。 - **实现步骤**: 1. 中断发生时,CPU自动将部分状态($eip$, $cs$, $eflags$)压入当前进程的内核堆栈。 2. 内核处理程序保存剩余通用寄存器(如 $eax$, $ebx$)到堆栈。 3. 调度器选择新进程后,切换堆栈指针($esp$)到新进程的内核堆栈。 4. 从新堆栈恢复寄存器状态,并执行 `iret` 返回用户模式。 - 示例汇编代码(简化版,基于x86): ```asm ; switch_to 函数实现(基于堆栈) switch_to: pusha ; 保存所有通用寄存器到当前堆栈 mov [current_pcb], esp ; 保存当前堆栈指针到PCB mov esp, [next_pcb] ; 加载新进程的堆栈指针 popa ; 从新堆栈恢复寄存器 ret ; 返回到新进程的上下文 ``` - **优点**:堆栈切换减少了内存访问次数,更快更灵活。引用[2]描述了一个实验:修改Linux 0.11,移除TSS切换,实现基于堆栈的 `switch_to` 函数,显著提升了性能[^2]。 - **协程对比**:引用[1]指出,协程切换(如libco)更轻量,因为它只保存用户级寄存器,不涉及内核模式,但进程切换需要完整上下文保存,确保系统安全[^1]。 - **其他技术细节**: - **中断处理**:时钟中断(通过APIC或HPET)定期触发切换,时间片由调度算法决定。 - **性能优化**: - 减少切换频率:使用更智能的调度器(如Linux CFS)。 - 硬件辅助:现代CPU提供快速上下文切换指令(如x86的 `syscall`/`sysret`)。 - **实时系统要求**:引用[3]强调,实时操作系统(RTOS)要求进程切换在严格时间内完成(例如微秒级),以保证及时响应[^3]。 #### 总结 进程切换是操作系统多任务的基础,通过上下文切换实现:保存当前进程状态、调度新进程、加载其状态。传统TSS机制已被高效堆栈切换取代,后者通过手动寄存器管理降低开销。关键挑战是平衡切换速度和系统安全,尤其在实时环境中。优化策略包括精简切换代码和利用硬件特性。如果您有具体场景(如Linux实现),可以进一步讨论。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值