linux系统调度之时间

[摘要]

[背景]

[正文]

[总结]

[其他]

 

注意:请使用谷歌浏览器阅读(IE浏览器排版混乱)

 

【摘要】

linux系统的调度过程是基于时间子系统实现的。无论判断一个系统的性能还是计算一个进程的cpu占用率等,其本质都是进程或中断等占有cpu的时间。了解linux中时间的概念,尤为重要。本文将为你剖析一下与进程和调度有关的几个关键时间。若想了解整个linux的时间子系统,请参考博文.

【背景】

本文主要是为介绍调度子系统做铺垫。

【正文】

在linux时间子系统一文中我们知道,linux每个时钟中断(又称tick中断)处理中都会更新进程时间,即update_process_times。所以本文把update_process_times函数作为入口点进行分析,直接上代码。

1 时钟中断中更新进程相关的时间:

普通定时器 :arch_timer_handler_phys->tick_handle_periodic->update_process_times;

高精度定时器 : tick_sched_handle->update_process_times;

void update_process_times(int user_tick)
{
   struct task_struct *p=current;
   /* 找到多核中的cpu id */
   int cpu = smp_processor_id();
   /* user_tick根据cpu模式判断是用户态还是内核态。linux统计时间的方式:
   1 基于整个cpu的统计,user_tick表示cpu在用户态,内核态还是中断状态。此处把一个tick的时间累加到kstat_cpu(i).cpustat.
    /proc/stat中的统计值是在此处统计的,表示cpu在用户态,内核态,中断中各占用多少时间,对应 stat.c(fs/proc):static int __init proc_stat_init(void);
    2 基于进程的统计。linux还有一种统计时间的方法更细化,统计的是调度实体上的时间sum_exec_runtime,它在sched_clock_cpu函数中基于timer计算
    /proc/pid/stat,/proc/pid/task/tid/stat中的时间是在此处统计的,它统计了一个进程/线程占用cpu的时间,对应do_task_stat实现。
  */
  account_process_tick(p,user_tick);
  /*
  此处负责系统中的定时器到期操作。
  并未真正处理,只是实现raise_softirq(TIMER_SOFTIRQ),当这个tick中断退出irq_exit时,会处理TIMER_SOFTIRQ软中断;
  TIMER_SOFTIRQ软中断处理含税run_timer_softirq()中负责处理到期的定时器。
  */
  run_locl_timers();
  /* 与进程和调度用过的时间参数 */
  scheduler_tick();
}

2 每个tick时钟中断中都更新调度相关的时间

update_process_times->scheduler_tick()

/*
 * This function gets called by the timer code, with HZ frequency.
 * We call it with interrupts disabled.
 定时器中断时改变时间戳update_process_times->
 */
void scheduler_tick(void)
{
/* 处理当前中断的cpu id */
 int cpu = smp_processor_id();
/* 每个cpu上都有一个运行队列,运行队列rq上包含实时进程运行队列rt_rq和普通进程运行队列cfs_rq */
 struct rq *rq = cpu_rq(cpu);
 struct task_struct *curr = rq->curr;
/* 该函数为空实现:CONFIG_HAVE_UNSTABLE_SCHED_CLOCK
每个tick时钟中断中处理如下:
sched_clock_tick()中更新sched_clock_data结构体(调用关系scheduler_tick()->sched_clock_tick())。
sched_clock_cpu()中通过sched_clock_data结构计算时间差(调用关系:scheduler_tick()->update_rq_clock()),
update_rq_clock中通过sched_cock_cpu函数计算就绪调度队列上的时间:rq->clock;
*/
 sched_clock_tick();
 raw_spin_lock(&rq->lock);
/* 更新当前调度队列rq的clock */ 
 update_rq_clock(rq);
 update_cpu_load_active(rq);
 /*
 普通进程task_tick_fair  
 实时进程task_tick_rt
 实时进程在task_tick_rt中检测时间片是否用完。
 */
 curr->sched_class->task_tick(rq, curr, 0);
 raw_spin_unlock(&rq->lock);
 perf_event_task_tick();
#ifdef CONFIG_SMP
 rq->idle_balance = idle_cpu(cpu);
 trigger_load_balance(rq, cpu);
#endif
 rq_last_tick_reset(rq);
}

void sched_clock_tick(void)

{

    struct sched_clock_data *scd;

     u64 now,now_gtod;

     now_gtod = ktime_to_ns(ktime_get());

     now = sched_clock();

      scd->tick_raw = now;

      scd->tick_gtod=now_gtod;

      sched_clock_local(scd);//scd->clock初始化

}

2.1 更新运行队列上时间:

1) 时钟中断中更新:update_process_times->scheduler_tick()->update_rq_clock()

void update_rq_clock(struct rq *rq)
{
 s64 delta;
 if (rq->skip_clock_update > 0)
  return;
/*
 两次相邻两次周期性调度器运行的时间差 */
 delta = sched_clock_cpu(cpu_of(rq)) - rq->clock;
/*
更新运行队列上的时钟:更新rq->clock_task 与rq->clock
*/
 rq->clock += delta;
/*
此处更新rq->clock_task+=delta,默认配置不开情况rq->clock_task=rq->clock;如果CONFIG_IRQ_TIME_ACCOUNTING配置打开,则rq->clock_task需要减去中断上的时间。
所以可以认为rq->clock队列时间每个tick中断都统计在内,甚至包括中断处理的时间;
而rq->clock_task是进程真正占用的时间,只不过很多情况下rq->clock_task=rq->clock,调度过程中使用rq->clock_task .
*/
 update_rq_clock_task(rq, delta);
}

2)进程出入调度队列时更新activate_task/deactivate_task->enqueue_task/dequeue_task->update_rq_clock:

static void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
 update_rq_clock(rq);
 sched_info_queued(p);
 /* 
 __setscheduler->
 SCHED_NORMAL: enqueue_task_fair 
 SCHED_RR: enqueue_task_rt
 */
 p->sched_class->enqueue_task(rq, p, flags);
}
static void dequeue_task(struct rq *rq, struct task_struct *p, int flags)
{
 update_rq_clock(rq);
 sched_info_dequeued(p);
 p->sched_class->dequeue_task(rq, p, flags);
}
/*wake_up_new_task->*/
void activate_task(struct rq *rq, struct task_struct *p, int flags)
{
 if (task_contributes_to_load(p))
  rq->nr_uninterruptible--;
 /* 
 __setscheduler-> enqueue_task_fair /enqueue_task_rt
 */
 enqueue_task(rq, p, flags);
}
void deactivate_task(struct rq *rq, struct task_struct *p, int flags)
{
 if (task_contributes_to_load(p))
  rq->nr_uninterruptible++;
 dequeue_task(rq, p, flags);
}

2.2 更新实时进程运行队列上调度相关的时间:

1)与进程调度有关的时间存放在如下结构体,一般task_struct结构中se表示sched_entity(如:current->se.exex_start)

struct sched_entity {
        u64                    exec_start;
        u64                    sum_exec_runtime;
        u64                    vruntime;
        u64                    prev_sum_exec_runtime;
}

其中 实时进程一般只使用exec_start和sum_exec_runtime;普通进程中这几个成员变量都会被使用.sched_entity中时间的初始化函数:

staticvoid __sched_fork(struct task_struct *p)
{
/* se是普通进程的调度实体struct sched_entity */
/*
见下面解释;
*/
        p->se.exec_start              = 0;
/*进程执行的总时间大小*/
        p->se.sum_exec_runtime                = 0;
/*
进程上一次运行总时间,即醒来时间。调度过程选择下一个进程是初始为
se.sum_exec_runtime, 
进程最近一次已获取cpu的时间:
sum_exec_runtime-prev_sum_exec_runtime
*/
        p->se.prev_sum_exec_runtime      = 0;
        p->se.nr_migrations                 = 0;
        /*进程在一个调度周期内的虚拟调度时间 */
        p->se.vruntime                           = 0;
}

2)update_process_times->scheduler_tick()->task_tick_rt()

/*
 定时器中断时更新实时进程时间update_process_times->scheduler_tick->:
*/
static void task_tick_rt(struct rq *rq, struct task_struct *p, int queued)
{
 struct sched_rt_entity *rt_se = &p->rt;
/*
更新实时进程相关统计时间
*/
 update_curr_rt(rq);
 watchdog(rq, p);
 /*
  * RR tasks need a special form of timeslice management.
  * FIFO tasks have no timeslices.
  */
/*
检查SCHED_RR进程时间片是否用光;SCHED_FIFO只有主动放弃CPU使用权;
*/
 if (p->policy != SCHED_RR)
  return;
/*
时间片减一,若未运行完时间则直接返回,
否则再次分配时间片,加入队列尾部,设置调度标记 TIF_NEED_RESCHED
*/
 if (--p->rt.time_slice)
  return;
/*
实时进程SCHED_RR
初始时间片为100 ms,即time_slice=10个时钟中断
*/
 p->rt.time_slice = sched_rr_timeslice;
 /*
  * Requeue to the end of queue if we (and all of our ancestors) are the
  * only element on the queue
  */
 for_each_sched_rt_entity(rt_se) {
  if (rt_se->run_list.prev != rt_se->run_list.next) {
/*
 如果时间片耗尽,则把实时进程SCHED_RR的运行实体从active优先级队列上删除,如此schedule调度过程无法
 通过运行实体找到该进程p进行调度,从而实现系统调度器。
 那么进程p何时能重新加入调度队列呢?例如wake_up_process过程可以重新入队列,具体可参照调度子系统介绍
*/
   requeue_task_rt(rq, p, 0);
   /* 该标记表示需要重新调度 */
   set_tsk_need_resched(p);
   return;
  }
 }
}

3)实时进程统计时间更新:update_process_times->scheduler_tick()->task_tick_rt()->update_curr_rt()

此时更新实时进程占有cpu的总时间se.sum_exec_runtime和一个tick间隔内进程开始执行的时间se.exec_start.

/
 * Update the current task's runtime statistics. Skip current tasks that
 * are not in our scheduling class.
 */
static void update_curr_rt(struct rq *rq)
{
 struct task_struct *curr = rq->curr;
 struct sched_rt_entity *rt_se = &curr->rt;
 struct rt_rq *rt_rq = rt_rq_of_se(rt_se);
 u64 delta_exec;
 if (curr->sched_class != &rt_sched_class)
  return;
/*
exec_start分析:
1)exec_start表示进程在一个时钟中断间隔内开始运行的时间,主要用于在时钟中断中计算时间差,
  该时间差主要用于计算进程获取cpu的时间(se.sum_exec_runtime累加该时间差:update_curr : rq->clock_task-exec_start),
2) 此时clock_task为时钟中断开始时间,exec_start可以为之前时钟中断中任一时间,且不必是时钟中断开始的时间点。
  exec_start在每个tick中断中都更新为队列时间rq->clock_task.
3) 时钟中断到来时clock_task先在update_rq_clock更新。然后再用clock_task减去exec_start即获取当前进程在一个时钟中断中获取到cpu的时间。
如果exec_start恰好等于上一个时钟中断时队列上时间clock_task,则表示进程在这个时钟中断中一直占有cpu,未发生调度。
delta_exec分析:
delta_exec表示进程在一个时钟周期内,所获得的cpu执行权的时间。 把delta_exec累加,就是进程获取cpu的总时间
注意此处使用rq->clock_task而不是rq->clock,注意update_rq_clock中二者区别。
*/
 delta_exec = rq->clock_task - curr->se.exec_start;
 if (unlikely((s64)delta_exec <= 0))
  return;
 schedstat_set(curr->se.statistics.exec_max,
        max(curr->se.statistics.exec_max, delta_exec));
/* sum_exec_runtime和exec_start是如何更新的?
更新当前进程总运行时间,进程的运行的时间等于进程在每个tick时钟中断中运行的时间的和,即rq运行队列上的时间减去这个时钟中断间隔内进程开始执行的时间;
举例说明:从第n到n+1时钟中断,此时cpu处理第n+1个tick中断。(相差1HZ,假设10ms)。
1)如果进程p一直没有被调度到,即current!=p,则显然进程p的p->se.sum_exec_runtime不会累加。
2)如果进程p一直占有cpu,即current=p,在第n个时钟中断时p->se.exec_start更新为第n个时钟中断时的队列时间rq->clock_task;
注意此时rq->clock_task 是第n+1个时钟中断时的队列时间rq->clock_task。
通过上面知道delta_exec = rq->clock_task - curr->se.exec_start;,所以delta刚好是一个tick的时间(10ms)。
3)如果进程p在第n个时钟中断发生时没有执行,即current!=p,而在第n个时钟中断发生后开始执行,即current=p,此处说明第n个tick中断之后有调度发生,
而调度过程会在选择下一个进程pick_next_task->pick_next_task_rt中将p->se.exec_start=rq->clock_start,(注意选择下一进程之前,会更新队列时间)
从而实现将进程p在一个时钟中断中的起始执行时间赋值给exec_start;注意此时rq->clock_task 是第n+1个时钟中断时的队列时间rq->clock_task。
通过上面知道delta_exec = rq->clock_task - curr->se.exec_start;,所以delta小于一个tick的时间(10ms)。
*/
 curr->se.sum_exec_runtime += delta_exec;
 account_group_exec_runtime(curr, delta_exec);
/*
tick中断处理中更新进程在一个时钟中断间隔内开始运行的时间;选择下一个进程时(pick_next_task_rt)也有可能更新
*/
 curr->se.exec_start = rq->clock_task;
 cpuacct_charge(curr, delta_exec);
 sched_rt_avg_update(rq, delta_exec);
/*开启sysctl_sched_rt_runtime 表示实时进程的运行时间是0.95s */
 if (!rt_bandwidth_enabled())
  return;
 for_each_sched_rt_entity(rt_se) {
  rt_rq = rt_rq_of_se(rt_se);
  if (sched_rt_runtime(rt_rq) != RUNTIME_INF) {
   raw_spin_lock(&rt_rq->rt_runtime_lock);
   rt_rq->rt_time += delta_exec;
   if (sched_rt_runtime_exceeded(rt_rq))
    resched_task(curr);
   raw_spin_unlock(&rt_rq->rt_runtime_lock);
  }
 }
}     

2.3 更新普通进程调度相关时间:

1) tick中断处理中的更新: update_process_times->scheduler_tick()->task_tick_fair()->entity_tick->update_curr()

static void update_curr(struct cfs_rq *cfs_rq)
{
 struct sched_entity *curr = cfs_rq->curr;
 u64 now = rq_of(cfs_rq)->clock_task;
 unsigned long delta_exec;
 if (unlikely(!curr))
  return;
 /*
  * Get the amount of time the current task was running
  * since the last time we changed load (this cannot
  * overflow on 32 bits):
  */
 /*
此处参考update_curr_rt中对exec_start的解释;
 */
 delta_exec = (unsigned long)(now - curr->exec_start);
 if (!delta_exec)
  return;
 __update_curr(cfs_rq, curr, delta_exec);
/*更新进程下次运行的起始时间
如果被抢占,下次调度时将会更新
*/ 
 curr->exec_start = now;
 if (entity_is_task(curr)) {
  struct task_struct *curtask = task_of(curr);
  trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
  cpuacct_charge(curtask, delta_exec);
  account_group_exec_runtime(curtask, delta_exec);
 }

更新当前进程运行时间统计数据update_curr()->__update_curr()

/*
 * Update the current task's runtime statistics. Skip current tasks that
 * are not in our scheduling class.
 */
static inline void __update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
       unsigned long delta_exec)
{
 unsigned long delta_exec_weighted;
 schedstat_set(curr->statistics.exec_max,
        max((u64)delta_exec, curr->statistics.exec_max));
/* 更新该进程获得cpu执行权的总时间  ,可参考update_curr_rt函数中sum_exec_runtime累计处的解释。
此处更新进程下一次的运行时间:表示进程在一个时钟中断内开始运行的时间 时钟中断到来时clock_task先在update_rq_clock更新。
然后再用clock_task减去exec_start即获取当前进程在一个时钟中断中获取到cpu的时间。如果exec_start恰好等于上一个时钟中断时
队列上时间clock_task,则表示进程在这个时钟中断中未发生调度。
*/
 curr->sum_exec_runtime += delta_exec;
 schedstat_add(cfs_rq, exec_clock, delta_exec);
/*
calc_delta_fair用来将真实时间转化为虚拟时间。进程的优先级不同,它在系统中的地位(权重)也不同,
进程的优先级越高,它的虚拟时间走的越慢。
*/
 delta_exec_weighted = calc_delta_fair(delta_exec, curr);
/*更新该进程获得cpu执行权的虚拟时间*/
 curr->vruntime += delta_exec_weighted;
 update_min_vruntime(cfs_rq);
}

2)无论普通进程还是实时进程,都会在选择下一进程时更新一个进程在一个时钟中断中开始执行的时间exec_start,下面以

 普通进程为例进行介绍pick_next_task_fair->set_next_entity():

 ps:实时进程在pick_next_task_rt()->__pick_next_task_rt()中更新se.exec_start;

static void set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
 /* 'current' is not kept within the tree. */
 if (se->on_rq) {
  /*
   * Any task has to be enqueued before it get to execute on
   * a CPU. So account for the time it spent waiting on the
   * runqueue.
   */
  update_stats_wait_end(cfs_rq, se);
  __dequeue_entity(cfs_rq, se);
 }
/*
 update_stats_curr_start()函数实现:se->exec_start =rq_of(cfs_rq)->clock_task 
调度器在选择下一个进程执行时,会更新exec_start为队列时间rq->clock_task
*/
 update_stats_curr_start(cfs_rq, se);
 cfs_rq->curr = se;
#ifdef CONFIG_SCHEDSTATS
 /*
  * Track our maximum slice length, if the CPU's load is at
  * least twice that of our own weight (i.e. dont track it
  * when there are only lesser-weight tasks around):
  */
 if (rq_of(cfs_rq)->load.weight >= 2*se->load.weight) {
  se->statistics.slice_max = max(se->statistics.slice_max,
   se->sum_exec_runtime - se->prev_sum_exec_runtime);
#endif
/*
prev_sum_exec_runtime表示进程截止目前获取cpu的时间(即执行时间);
可以使用sum_exec_runtime- prev_sum_exec_runtime计算进程最近一次调度内获取cpu使用权的时间。
不过sum_exec_runtime是使用exec_start计算出的时间差的累加。
*/
 se->prev_sum_exec_runtime = se->sum_exec_runtime;
 }

2.4 以上讨论了进程与调度相关时间的更新,可以关注三个方面:

1) tick中断处理过程中的更新,主要通过update_rq_clock函数更新调度队列上的时间rq->clock_task; 

通过task_tick_rt/task_tick_fair更新进程在一个时钟中断内开始执行的时间exec_start和进程执行的总时间,sum_exec_runtime;

2) 出入调度队列时,也会更新调度队列上的时间rq->clock_task,schedule调度过程也可能更新队列时间。

3) 无论普通进程还是实时进程,都会在选择下一进程时更新一个进程在一个时钟中断中开始执行的时间exec_start。

【总结】

本文讨论了每个tick时钟中断中,对  与进程和调度相关时间 的更新,为后续介绍调度子系统做铺垫。

注意:本文讨论的时间不是墙上时间(如:年月日时分),而是运行时间,比如一个程序运行了1s,则时间戳就是1s,

而不是某年某月某时某分等等诸如此类.

【其他】

1 查看进程时间的方法:

void show_taskTime(struct task_struct *tsk)
{
   unsigned long long fork_time;
   unsigned long long new_start_time;   
   unsigned long long sum_exec_runtime;
   unsigned long long prev_sum_exec_runtime;   
   unsigned long long curr_fsum_exec_runtime;
   int policy;
   /*以下时间:单位ns;从0开始累计*/
   /* 进程创建的时间点; */
   fork_time=(unsigned long long)tsk->real_start_time.tv_sec*1000000000+
           + tsk->real_start_time。tv_nsec;
   /* 进程最近一次开始执行的时间点 */
   new_start_time=tsk->se.exec_start;
   /*进程一共执行(即获取到cpu)的时间长度*/
   sum_exec_runtime=tsk->se.sum_exec_runtime;
   /*进程上一次一共执行的时间长度;注意在schedule选择下一进程时,
     下一进程tsk->se.prev_sum_exec_runtime初始化为sum_exec_runtime;
     而当前进程的prev_sum_exec_runtime是在之前切换到当前进程时初始化的;
   */
   prev_sum_exec_runtime=tsk->se.prev_sum_exec_runtime;
   /*当前进程最近一次获取到cpu的时间长度;*/
   curr_sum_exec_runtime=tsk->se.sum_exec_runtime-tsk->se.prev_sum_exec_runtime;
   policy=tsk->policy;
   printk("[%s_%d_%d:%llu_%llu_%llu_%llu_%llu]\n",tsk->comm,tsk->pid,policy,
         fork_time,new_start_time,sum_exec_runtime,prev_sum_exec_runtime,
         curr_sum_exec_runtime);
}

2 查看调试信息方法:

void show_debugInfo(void)
{
  /*查看当前进程的执行时间*/
  show_taskTime(current);
  /*查看系统内存使用情况*/
  show_mem(0);
  /*查看当前进程的栈*/
  show_stack(current,NULL);
}

3 为计算进程最近一次获取cpu的时间长度,需要使用prev_sum_exec_runtime参数,而 对实时进程来说,没有使用task->se.prev_sum_exec_runtime,所以需要修改kernel代码如下:(注意如果是当前进程,则sum_exec_runtime-prev_sum_exec_runtime计算的是当前进程获取cpu以来的执行时间)

/*schedule->pick_next_task_rt->_pick_next_task_rt选择下一进程时*/

static struct task_struct *_pick_next_task_rt(struct rq *rq)
{
    p->se.exec_start=rq->clock_task;
    /* 添加如下代码 */
    p->se.prev_sum_exec_runtime=p->se.sum_exec_runtime;
    /*end*/
   return p;   
}

【c标准接口】

times()函数: 系统调用所在文件 kernel/sys.c;

Linux内核中的jiffies及其作用介绍及jiffies等相关函数详解;

使用系统sched_clock计算时间:

static void update_time_stamp(void)
{
       unsigned long long timeNow = 0;
       unsigned long long  timeInc = 0;
       static unsigned long long preTimeStamp = 0;
       static unsigned long long timePtsNs = 0;
       static unsigned long long timePts = 0;//返回ns

       timeNow = sched_clock();  //返回ns
      //64bit溢出
       if(preTimeStamp > timeNow)
       {
          timeInc = (0xFFFFFFFFFFFFFFFF-preTimeStamp)+timeNow;
       }
       else
       {
            timeInc = timeNow - preTimeStamp;
       }
        timeInc += timePtsNs;//timePtsNs为上一次计算的精度损失.timePts为毫秒级,因此有损失,需要在增量timeInc上加timePtsNs
        /*timePtsNs为除后余数;do_div计算后timeInc为除后结果:毫秒*/
        timePtsNs = do_div(timeInc,1000000);
        timePts = timePts + timeInc; //timepts为要获取的ms时间戳
        preTimeStamp = timeNow;

        return ;
}

sched_clock->sched_clock_func=sched_clock_32->read_sched_clock()

read_sched_clock初始化可以在setup_sched_clock中完成;

如1arch/arm/mach-xx/timer.c中通过setup_sched_clock设置(read_sched_clock=xx_read_sched_clock)

xx_read_sched_clock实现读定时器.

2 sched_clock_register(arch_timer_read_counter);(read_sched_clock=arch_timer_read_counter)

中断的负载均衡: 
1) echo 4 > /proc/irq/49/smp_affinity 
4=0100 表示bit3=1;即49号中断在cpu2上处理; 
可以通过 cat /proc/interrupts查看中断号; 
可以通过 cat /proc/softirqs 查看中断数量;

进程的负载均衡: 
cat /proc/loadavg 查看负载均衡参数;uptime命令也可以查看. 
loadavg:表示系统的平均负荷; 
参考文章:load average 
top -d 1 查看进程cpu占用率 
敲入c :查看不同核;敲入H:查看线程cpu占用率

在linux下,top -H查看线程情况,包括CPU,内存等占用情况。

进入到top环境后,可以使用P来按照CPU排序,使用M按照内存排序。

cpu stall 

进程、线程的时间: /proc/pid/sched ; /proc/pid/task/tid/sched

 

 

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值