Linux 进程的一生(二):从 CFS 到 EEVDF,调度器的演进与原理

在上一章节,我们深入分析了 Linux 进程与线程的创建过程,探讨了 fork()clone() 的实现细节。然而,进程的生命周期不仅仅止步于创建,在上节中我们说到,在创建完task_struct结构体之后,将新的任务放在调度队列上等待调度。而调度是进程从“创建”走向“运行”的关键环节

Linux 内核调度器负责在多个任务之间合理分配 CPU 资源,以确保系统的高效性、公平性和实时性。随着计算机硬件的发展,Linux 进程调度器经历了从 O(n) 复杂度的调度算法,到 O(1) 调度器,再到 CFS 的演进。最近,Linux 内核又引入了 EEVDF(Earliest Eligible Virtual Deadline First)作为对 CFS 的改进,以进一步优化低延迟任务的公平性和响应性。

本篇文章将围绕 Linux 进程调度器的演进、CFS 调度器的核心机制,以及 EEVDF 调度算法 展开详细分析,探索调度器如何影响进程运行的效率与公平性。

调度器的发展历史

1.O(n)调度器的发展过程

之前到2.4版本的Linux调度器经历了先来先服务、短作业优先、时间片轮转以及优先级时间片轮转的发展过程,一步一步弥补了之前的不足:

调度算法主要思想改进点(相较于前一算法)主要不足
先来先服务按照进程到达的顺序进行调度,先到先执行,直到进程完成最基础的调度算法,实现简单可能导致“短作业等待长作业”问题,容易产生“饥饿”现象
短作业优先选择最短的作业优先运行,减少平均等待时间解决先来先服务 的“短作业等待长作业”问题,提高吞吐量需要提前知道 CPU Burst Time,不适用于动态环境,可能导致长作业“饥饿”
时间片轮转每个进程被分配一个固定时间片,时间片到后被切换解决短作业优先可能导致的“饥饿”问题,所有进程公平获得 CPU 时间可能增加上下文切换的开销,选择合适的时间片长度较难,且有的进程较为“迫切“
优先级时间片轮转在时间片轮转的基础上引入优先级,高优先级的进程比低优先级的进程更容易获得 CPU结合了优先级调度和 RR,保证高优先级进程快速响应,同时维持公平性低优先级进程可能长期得不到调度,可能需要引入“优先级提升”机制
动态优先级时间片轮转静态优先级 的基础上,引入 动态优先级 机制,根据进程行为、等待时间等动态调整优先级解决固定优先级调度的“饥饿”问题,使得长期等待的低优先级进程能逐渐获得 CPU 机会

2.Linux2.5 O(1)调度器

上述算法的复杂度都是O(n),但是随着多核系统的出现,若多个CPU都使用同一个运行队列,则会导致锁竞争加剧。且遍历整个运行队列也较为低效,故发展重点转为了如何提高多核系统下的调度器的性能。

在Linux2.5中,针对上述问题,每个CPU都维护一个运行队列,减少了锁竞争。并且引入了多优先级任务队列:即每一个优先级有一个队列,数值越低,优先级越高。并且在查找时引入了bitmap辅助实现O(1)查找,通过每个比特来表示对应的优先级是否有任务。逻辑图大致如下:

在这里插入图片描述

并且每一个runqueue上有两个任务队列:active(当前周期要调度的进程列表)、expired(当前周期时间片用完的进程列表)。当active中的task全部执行完毕时,active和expired队列会交换,进行下一个周期的调度运转:

在这里插入图片描述

然而在调度优先级上,实时进程和普通进程是不同的,在实时进程中,优先级是静态的,高优先级的进程一定会先调度,使用SCHED_FIFOSCHED_RR去分配CPU。

但对于普通进程来说,使用的是上面讲过的动态优先级的方法,静态优先级不变,但是动态优先级会随着对时间片的使用来动态的调整:

优先级类型适用调度策略适用场景
0-99实时优先级SCHED_FIFO(先来先服务) SCHED_RR(时间片轮转)硬件中断处理、音视频处理、工业控制
100-139普通优先级SCHED_NORMAL(CFS) SCHED_BATCH(批处理) SCHED_IDLE绝大部分用户进程,如浏览器、编译任务等

调度策略

进程线程的调度依赖于调度策略,可以使用chrt命令查看当前系统支持的调度策略

SCHED_OTHER 最小/最大优先级     : 0/0
SCHED_FIFO 最小/最大优先级      : 1/99
SCHED_RR 最小/最大优先级        : 1/99
SCHED_BATCH 最小/最大优先级     : 0/0
SCHED_IDLE 最小/最大优先级      : 0/0
SCHED_DEADLINE 最小/最大优先级  : 0/0

其各个特点如下表:

调度策略优先级范围调度特点适用场景
SCHED_OTHER (默认)100-139默认调度策略(CFS) - 使用 nice 值(-20~19)调整权重 - 采用 红黑树 实现公平调度普通用户进程(大多数应用程序)
SCHED_FIFO(实时)1-99固定优先级调度 - 只要进程不主动放弃 CPU,它会一直运行实时任务(音视频处理、工业控制)
SCHED_RR(实时)1-99类似 FIFO,但使用时间片轮转 - 相同优先级的进程轮流执行,防止饥饿需要公平 CPU 时间的实时任务(如网络包处理)
SCHED_BATCH100-139适用于 CPU 密集型任务 - 进程不会主动抢占 CPU,只会在空闲时运行批处理任务(大规模数据处理、机器学习训练)
SCHED_IDLE100-139仅在 CPU 空闲时运行 - 适用于 低优先级任务后台计算、不重要的任务(如文件索引、日志分析)
SCHED_DEADLINE最高优先级的实时进程基于 Earliest Deadline First调度算法 - 适用于 硬实时任务 - 需要设置 运行时间、周期、截止时间严格实时任务(机器人控制、金融交易)

CFS调度器

在 Linux 进程调度中,CFS(Completely Fair Scheduler)完全公平调度器 负责普通进程的调度。CFS 通过调度实体维护进程的调度信息,核心指标是 虚拟运行时间 vruntime,它决定了进程在调度队列中的优先级。

1.sched_entity

task_struct 结构体中,每个普通进程都有一个 sched_entity,它包含进程的调度权重(weight)虚拟运行时间(vruntime) 等信息:

struct sched_entity {
    struct load_weight    load;      // 进程的调度权重
    u64                   vruntime;  // 进程的虚拟运行时间
    struct list_head      group_node;
    struct cfs_rq         *cfs_rq;   // 所属的 CFS 运行队列
    ...
};

2.计算进程权重(weight)

CFS 使用 nice 值来计算进程的调度权重nice 取值范围为 -20(最高优先级)到 19(最低优先级)。
nice=0 的进程默认权重为 1024,其余 nice 值的权重可通过查表获取:

const int sched_prio_to_weight[40] = {
 /* -20 */     88761,     71755,     56483,     46273,     36291,
 /* -15 */     29154,     23254,     18705,     14949,     11916,
 /* -10 */      9548,      7620,      6100,      490 4,      3906,
 /*  -5 */      3121,      2501,      1991,      1586,      1277,
 /*   0 */      1024,       820,       655,       526,       423,
 /*   5 */       335,       272,       215,       172,       137,
 /*  10 */       110,        87,        70,        56,        45,
 /*  15 */        36,        29,        23,        18,        15,
};

内核中提供一个函数set_load_weight来查表,用来计算一个任务的权重,即上述调度实体结构体中的load。

void set_load_weight(struct task_struct *p, bool update_load){//p是设置调度权重的进程,
    int prio = p->static_prio - MAX_RT_PRIO;//得到用于 sched_prio_to_weight[] 的索引
	    struct load_weight lw;

    if (task_has_idle_policy(p)) {
        lw.weight = scale_load(WEIGHT_IDLEPRIO);
        lw.inv_weight = WMULT_IDLEPRIO;
    } else {
        lw.weight = scale_load(sched_prio_to_weight[prio]);
        lw.inv_weight = sched_prio_to_wmult[prio];
    }//计算调度权重,判断任务类型
    
    if (update_load && p->sched_class->reweight_task)
        p->sched_class->reweight_task(task_rq(p), p, &lw);
    else
        p->se.load = lw;//更新进程的负载
......
}

3.vruntime的计算

在 CFS 调度器中,vruntime 作为调度依据,vruntime 最小的进程会被优先调度
vruntime 的计算公式:

vruntime = nice_0_weight ⋅ delta_exec weight \text{vruntime} = \frac{\text{nice\_0\_weight} \cdot \text{delta\_exec}}{\text{weight}} vruntime=weightnice_0_weightdelta_exec

在内核中的实现源码为:

static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
    if (unlikely(se->load.weight != NICE_0_LOAD))
        delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
//计算进程 vruntime 增长的调整值 delta
    return delta;
}

weight ≠ 1024 时,调用 __calc_delta() 进行缩放计算,使得 nice=-20 的进程 vruntime 增长最慢,nice=19 的进程 vruntime 增长最快

static u64 __calc_delta(u64 delta_exec, unsigned long weight, struct load_weight *lw) {
    u64 fact = scale_load_down(weight);
    u32 fact_hi = (u32)(fact >> 32);
    int shift = WMULT_SHIFT;
    int fs;

    __update_inv_weight(lw);  // 更新反权重 inv_weight

    if (unlikely(fact_hi)) {
        fs = fls(fact_hi);
        shift -= fs;
        fact >>= fs;
    }

    fact = mul_u32_u32(fact, lw->inv_weight);

    return mul_u64_u32_shr(delta_exec, fact, shift);
}

4.cfs_rq

该结构体表示CFS就绪队列的数据结构,其定义如下:

struct cfs_rq {
    struct load_weight	load;      /* 运行队列的总权重 */
    unsigned int		nr_running;  /* 队列中的进程数 */
    unsigned int		h_nr_running;  /* 仅统计 `SCHED_NORMAL/BATCH/IDLE` 任务 */
    unsigned int		idle_nr_running;  /* `SCHED_IDLE` 任务数量 */
    unsigned int		idle_h_nr_running; /* `SCHED_IDLE` 任务的高优先级任务数 */
    unsigned int		h_nr_delayed;  /* 被延迟执行的任务数量 */
    s64 avg_vruntime;  /* 任务的平均 `vruntime` */
    u64 avg_load;      /* 任务的平均负载 */
    u64 min_vruntime;  /* 运行队列中的最小 `vruntime` */
    struct rb_root_cached tasks_timeline;  /* CFS 任务的红黑树 */
	struct sched_entity *curr;  /* 当前运行的进程 */
    struct sched_entity *next;  /* 下一个要运行的进程 */
	......

};

在这个对象中,核心为rb_root_cached类型的tasks_timeline对象,这就是CFS调度器管理就绪任务的红黑树,红黑树中存放的是sched_entity调度实体,而所有可运行的调度实体按照vruntime排序插入红黑树。整体结构如下图所示。

在这里插入图片描述

CFS选择红黑树最左边的进程运行。随着系统时间的推移,原来左边运行过的进程慢慢的会移动到红黑树的右边,原来右边的进程也会最终跑到最左边。因此红黑树中的每个进程都有机会运行。

EEVDF调度器

与 CFS 类似,EEVDF 的目标是在相同优先级的所有可运行任务之间均等地分配 CPU 时间。为此,它为每个任务分配了一个虚拟运行时间,并计算出一个“滞后”值,用以判断某任务是否获得了其应有的 CPU 时间份额。具体来说,正滞后值表示该任务欠 CPU 时间,而负滞后值则表示任务已超出其分配。EEVDF 会选择滞后值大于或等于零的任务,并为每个任务计算一个虚拟截止时间(Vitual Deadline),然后选择虚拟截止时间最早的任务作为下一执行任务。这一机制允许时间片较短的延迟敏感任务获得优先,从而提高它们的响应性。

目前对于如何管理滞后值仍存在讨论,尤其是针对处于睡眠状态的任务;但截至目前,EEVDF 使用了一种基于虚拟运行时间(VRT)的“衰减”机制。这种机制防止任务通过短暂睡眠来重置其负滞后值:当任务进入睡眠状态时,它仍保留在就绪队列中,但被标记为“延迟出队”,使得其滞后值随着虚拟运行时间而衰减。因此,长时间睡眠的任务最终会重置其滞后值。最后,当某个任务的虚拟截止时间更早时,可以抢占其他任务,同时任务还可以使用新的 sched_setattr() 系统调用请求特定的时间片,这进一步便利了延迟敏感应用程序的调度。

简单来说,EEVDF 调度器的原理可以比作一个“欠账系统”。每个任务都有一个“账单”,账单上记录了它应得的 CPU 时间和实际得到的 CPU 时间之间的差额,这个差额叫做“lag”。具体原理如下:

  1. 公平分账: 每个任务根据它的权重应得一部分 CPU 时间。如果一个任务实际获得的时间少于它应得的部分,它就“欠”了一些时间(lag 为正);反之则是“超前”了(lag 为负)。
    2.资格判断:当系统的虚拟时间达到任务的某个“门槛时间”时,这个任务就有资格运行。如果任务欠账,它会立即进入候选队列;如果超前,则需要等到其他任务赶上。

  2. 设定截止日期:每次任务请求一段 CPU 时间时,根据它的资格时间和请求长度,调度器会计算一个虚拟截止时间。可以把截止时间看作任务“还账”的最后期限——在这个时刻前任务应完成获得所请求的时间。
    4.调度决策:调度器总是选择截止时间最早的任务执行。这样,那些欠账多的任务(截止时间早)会优先获得 CPU 时间,以尽快补上它们的欠款,从而实现整体公平。

总之,EEVDF 就是通过跟踪任务应得和实际获得的 CPU 时间差(lag),并根据任务的权重和请求长度为每个任务设定一个虚拟截止时间,最终始终优先选择截止时间最早的任务运行,从而确保所有任务最终都能按公平比例获得 CPU 时间。

其主要流程如下:

在这里插入图片描述

关键概念

1.公平运行时间

S ( t 1 , t 2 ) = ∫ t 1 t 2 w i W   d t S(t_1, t_2) = \int_{t_1}^{t_2} \frac{w_i}{W} \, dt S(t1,t2)=t1t2Wwidt

该公式表示任务 i 在时间段 [t1,t2] 内应获得的 CPU 时间。wi 是任务 i 的权重。W 是系统中所有活跃任务的权重总和。

直观上说,如果任务 i 的权重占所有任务权重的比例为W/wi,那么在任意时刻,它就“理应”分得这部分比例的 CPU 时间。通过对时间段内这一比例积分,就得到了任务 i 在这段时间内应该获得的运行时间 S(t_1, t_2) 。

2.系统虚拟时间

V t = ∫ 0 t 1 W   d t V_t = \int_{0}^{t} \frac{1}{W} \, dt Vt=0tW1dt

其作用是将实际时间与系统中任务的总权重联系起来,提供一种度量方式,使得调度器能根据系统负载动态地计算各任务应得的时间。

利用虚拟时间 Vt 的定义,可以将公平运行时间公式重写为:
S ( t 1 , t 2 ) = w i ( V t 2 − V t 1 ) S(t_1, t_2) = w_i (V_{t_2} - V_{t_1}) S(t1,t2)=wi(Vt2Vt1)
这表示任务 i在时间段 [t1,t2] 内应获得的运行时间,等于它的权重 wi 与虚拟时间从 t1到 t2的增长量的乘积。

3.eligible time合格时间

任务 i 的 eligible time 是指这样一个时刻:从任务开始活跃时刻 t0到这个时刻,任务 i 实际获得的运行时间 s(t0,t) 正好等于它应得的运行时间 S(t0,e)(“应得时间”是根据任务权重计算出来的公平分配时间)。

换句话说,eligible time 就是任务 i 完全“赶上”它应得 CPU 时间的那一点,在这个时刻任务 i 的运行时间与应得运行时间相等,此时虚拟时间记为 Ve,即:
S ( t 0 , e ) = s ( t 0 , t ) S(t_0, e) = s(t_0, t) S(t0,e)=s(t0,t)
结合
S ( t 0 , e ) = w i ( V e − V t 0 ) S(t_0, e) = w_i(V_e - V_{t_0}) S(t0,e)=wi(VeVt0)
可以得到:
w i ( V e − V t 0 ) = s ( t 0 , t ) w_i(V_e - V_{t_0}) = s(t_0, t) wi(VeVt0)=s(t0,t)
从这里解出Ve得到:
V e = V t 0 + s ( t 0 , t ) w i V_e = V_{t_0} + \frac{s(t_0, t)}{w_i} Ve=Vt0+wis(t0,t)
因此,eligible time Ve 就表示任务 i 在虚拟时间上“赶上”它应得的 CPU 时间所需要达到的虚拟时间点。当系统的当前虚拟时间 Vt大于或等于 Ve时,就认为任务 i 的请求变得“合格”,也就是说它有资格作为候选任务去占用 CPU。

4.deadline time截止时间

deadline time 是指任务 i 的请求在虚拟时间区间[Ve,Vd] 内可以获得的运行时间刚好等于请求长度 r。也就是说,从虚拟时间 Ve 开始,到 Vd 为止,按照任务的权重分配,任务 i 应该获得的运行时间等于 r。

换句话说,deadline time 就像是任务的“截止日期”——到了这个虚拟时刻,任务的这次请求理论上应该已经得到了满足。

根据上述定义为:
S ( e , d ) = r S(e, d) = r S(e,d)=r
改写为:
w i ( V d − V e ) = r w_i(V_d - V_e) = r wi(VdVe)=r
解出Vd:
V d = V e + r w i V_d = V_e + \frac{r}{w_i} Vd=Ve+wir

5.多次请求下的递推公式

对于任务 i 的连续多个请求(假设每次请求的长度都是 r),我们有如下递推关系:

  • 第一次请求:

V e 1 = V t 0 和 V d 1 = V t 0 + r w i V_{e1} = V_{t_0} \quad \text{和} \quad V_{d1} = V_{t_0} + \frac{r}{w_i} Ve1=Vt0Vd1=Vt0+wir

  • 后续第 k 次请求:

V e k = V d k − 1 和 V d k = V e k + r w i V_{e_k} = V_{d_{k-1}} \quad \text{和} \quad V_{d_k} = V_{e_k} + \frac{r}{w_i} Vek=Vdk1Vdk=Vek+wir

这意味着每次新的请求的起点(eligible time)都等于上一次请求的截止时间,而新的截止时间则在前一次基础上增加了 r/wi的虚拟时间。

举例说明

我们可以利用前面介绍的公式来看看 c1 和 c2两个任务在这种情况下如何计算它们的 eligible time 和 deadline time。

假设两个任务的权重相同,都为 2,即
w 1 = w 2 = 2 w_1 = w_2 = 2 w1=w2=2

对于 c1

  • c1 在 t=0 加入,此时我们假定它开始活跃的系统虚拟时间为 Vt0=0。

  • c1 的请求长度 r1=2。

  • **Eligible time 的计算:**由于 c1 刚加入,还没有运行,所以它在发起请求时的实际运行时间为 0,故

V e 1 = V t 0 + s ( t 0 , t ) w 1 = 0 + 0 2 = 0. V_{e1} = V_{t0} + \frac{s(t_0, t)}{w_1} = 0 + \frac{0}{2} = 0. Ve1=Vt0+w1s(t0,t)=0+20=0.

这表示 c1 从一开始就“欠”它应得的时间。

  • Deadline time 的计算:

根据公式
V d = V e + r w i V_d = V_e + \frac{r}{w_i} Vd=Ve+wir
对 c1 有
V d 1 = V e 1 + r 1 w 1 = 0 + 2 2 = 1 V_{d1} = V_{e1} + \frac{r_1}{w_1} = 0 + \frac{2}{2} = 1 Vd1=Ve1+w1r1=0+22=1

对于c2

  • c2 在 t=1加入。此时,系统的虚拟时间 Vt0 对于 c2 是它加入时刻对应的虚拟时间。

假设系统在这段期间内一直由 c1 独占运行(实际情况会依赖于调度器如何推进虚拟时间,但为了示例我们假定系统虚拟时间与实际时间接近,即 V(1)=1),那么 c2 的起始虚拟时间为 1。

  • 请求长度:c2 的请求长度 r2=1。
  • **Eligible time 的计算:**c2 刚加入时还未获得任何运行时间,所以

V e 2 = V ( 1 ) + 0 w 2 = 1 V_{e2} = V(1) + \frac{0}{w_2} = 1 Ve2=V(1)+w20=1

  • Deadline time 的计算:

V d 2 = V e 2 + r 2 w 2 = 1 + 1 2 = 1.5 V_{d2} = V_{e2} + \frac{r_2}{w_2} = 1 + \frac{1}{2} = 1.5 Vd2=Ve2+w2r2=1+21=1.5

因此,在调度决策中,调度器会优先考虑那些拥有较早截止时间的任务。在这个例子中,c1 的截止时间 1 早于 c2 的 1.5,所以即使 c2在 t=1加入竞争,c1 的请求因其更早的截止时间将被优先满足。

这就是如何利用权重、请求长度以及系统虚拟时间来计算并比较两个任务的 eligible time 和 deadline time,从而决定哪个任务更早满足其公平分配的要求。

lag的概念

任务 i 应得时间和已得时间不总是相等的,两者的差值称作 lag:
lag i ( t ) = S i ( t 0 , t ) − s i ( t 0 , t ) \text{lag}_i(t) = S_i(t_0, t) - s_i(t_0, t) lagi(t)=Si(t0,t)si(t0,t)
这个 lag 值反映了任务是否“欠”运行时间(lag 为正)或者“超前”运行时间(lag 为负)。

任务退出竞争时的情况

假设任务 a 在某时刻 t 退出了竞争。为了确保系统的公平性,其它仍然活跃的任务需要“接管”任务 a 剩余的未补偿的运行时间(也就是任务 a 的 lag)。这就是“lag”重新分配的思想。

1.退出前后系统虚拟时间的更新

当任务 a 退出时,系统中其他任务在 [t0,t+] 这段时间内可分配的运行时间,会因为任务 a 的退出而发生变化。
S i ( t 0 , t + ) = ( t − t 0 − s a ( t 0 , t ) ) w i W S_i(t_0, t_+) = (t - t_0 - s_a(t_0, t)) \frac{w_i}{W} Si(t0,t+)=(tt0sa(t0,t))Wwi

  • t−t0 是从任务开始活跃到当前的总时长;
  • sa(t0,t) 是任务 a 在这段时间内实际获得的运行时间;
  • W 是任务 a 退出后,系统中剩余所有任务的权重之和。
2.利用任务 a 的 lag 表达 sa(t0,t)

根据 lag 的定义,对于任务 a 有:
lag a ( t ) = S a ( t 0 , t ) − s a ( t 0 , t ) \text{lag}_a(t) = S_a(t_0, t) - s_a(t_0, t) laga(t)=Sa(t0,t)sa(t0,t)
可推算:
s a ( t 0 , t ) = ( t − t 0 ) w a W − lag a ( t ) s_a(t_0, t) = (t - t_0) \frac{w_a}{W} - \text{lag}_a(t) sa(t0,t)=(tt0)Wwalaga(t)

3.推导系统虚拟时间的更新

S i ( t 0 , t + ) = w i ( V t − V t 0 ) + w i W lag a ( t ) S_i(t_0, t_+) = w_i(V_t - V_{t_0}) + \frac{w_i}{W} \text{lag}_a(t) Si(t0,t+)=wi(VtVt0)+Wwilaga(t)

这说明,在任务 a 退出后,其他任务 i 的应得运行时间在系统虚拟时间中的增量不仅包括原来的部分 wi(Vt−Vt0),还额外获得了“补偿”。

由此,系统虚拟时间也相应地更新为:
V t + = V t + lag a ( t ) W V_{t_+} = V_t + \frac{\text{lag}_a(t)}{W} Vt+=Vt+Wlaga(t)

4.其它任务的 lag 更新

对于任意其他任务 i,新的 lag 就变为原来的 lag 加上因任务 a 退出而“转嫁”来的那部分:
lag i ( t + ) = lag i ( t ) + w i W lag a ( t ) \text{lag}_i(t_+) = \text{lag}_i(t) + \frac{w_i}{W} \text{lag}_a(t) lagi(t+)=lagi(t)+Wwilaga(t)
这就意味着,退出任务 a 的 lag 会根据各个任务的权重比例均匀地加到剩余任务的 lag 上。

总结

在本篇文章中,我们回顾了 Linux 进程调度器的演进,从早期的 O(n) 复杂度调度算法,到 O(1) 调度器,再到目前主流的 CFS,并分析了 EEVDF 在 CFS 之上的改进。
然而,理解调度器的原理仅仅是第一步,在下一篇文章中,我们将深入 Linux 内核源码,分析具体的调度流程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值