Linux电源管理_Wakeup count功能--(三)

本文深入探讨了Linux电源管理中的wakeupcount功能,包括其在电源管理中的位置、功能、实现逻辑以及与sysfs接口的交互。通过伪代码示例,详细解析了如何利用wakeupcount进行suspend操作,确保电源状态切换的同步性。
1. 前言

Wakeup count是Wakeup events framework的组成部分,用于解决“system suspend和system wakeup events之间的同步问题”。本文将结合“Linux电源管理(6)_Generic PM之Suspend功能”和“Linux电源管理(7)_Wakeup events framework”两篇文章,分析wakeup count的功能、实现逻辑、背后的思考,同时也是对这两篇文章的复习和总结。

2. wakeup count在电源管理中的位置

wakeup count的实现位于wakeup events framework中(drivers/base/power/wakeup.c),主要为两个模块提供接口:通过PM core向用户空间提供sysfs接口;直接向autosleep(请参考下一篇文章)提供接口。

wakeup count architecture

3. wakeup count的功能

wakeup count的功能是suspend同步,实现思路是这样的:

1)任何想发起电源状态切换的实体(可以是用户空间电源管理进程,也可以是内核线程,简称C),在发起状态切换前,读取系统的wakeup counts(该值记录了当前的wakeup event总数),并将读取的counts告知wakeup events framework。

2)wakeup events framework记录该counts到一个全局变量中(saved_count)。

3)随后C发起电源状态切换(如STR),执行suspend过程。

4)在suspend的过程中,wakeup events framework照旧工作(直到系统中断被关闭),上报wakeup events,增加wakeup events counts。

5)suspend执行的一些时间点(可参考“Linux电源管理(6)_Generic PM之Suspend功能”),会调用wakeup  events framework提供的接口(pm_wakeup_pending),检查是否有wakeup没有处理。

6)检查逻辑很简单,就是比较当前的wakeup counts和saved wakeup counts(C发起电源状态切换时的counts),如果不同,就要终止suspend过程。

4. wakeup count的实现逻辑
4.1 一个例子

在进行代码分析之前,我们先用伪代码的形式,写出一个利用wakeup count进行suspend操作的例子,然后基于该例子,分析相关的实现。

   1: do {
   2:     ret = read(&cnt, "/sys/power/wakeup_count");
   3:     if (ret) {
   4:         ret = write(cnt, "/sys/power/wakeup_count");
   5:     } else {
   6:         countine;
   7:     }
   8: } while (!ret);
   9:  
  10: write("mem", "/sys/power/state");
  11:  
  12: /* goto here after wakeup */

例子很简单:

a)读取wakeup count值,如果成功,将读取的值回写。否则说明有正在处理的wakeup events,continue。

b)回写后,判断返回值是否成功,如果不成功(说明读、写的过程中产生了wakeup events),继续读、写,直到成功。成功后,可以触发电源状态切换。

4.2 /sys/power/wakeup_count

wakeup_count文件是在kernel/power/main.c中,利用power_attr注册的,如下(大家可以仔细研读一下那一大段注释,内核很多注释写的非常好,而好的注释,就是软件功力的体现):

   1: /*
   2:  * The 'wakeup_count' attribute, along with the functions defined in
   3:  * drivers/base/power/wakeup.c, provides a means by which wakeup events can be
   4:  * handled in a non-racy way.
   5:  *
   6:  * If a wakeup event occurs when the system is in a sleep state, it simply is
   7:  * woken up.  In turn, if an event that would wake the system up from a sleep
   8:  * state occurs when it is undergoing a transition to that sleep state, the
   9:  * transition should be aborted.  Moreover, if such an event occurs when the
  10:  * system is in the working state, an attempt to start a transition to the
  11:  * given sleep state should fail during certain period after the detection of
  12:  * the event.  Using the 'state' attribute alone is not sufficient to satisfy
  13:  * these requirements, because a wakeup event may occur exactly when 'state'
  14:  * is being written to and may be delivered to user space right before it is
  15:  * frozen, so the event will remain only partially processed until the system is
  16:  * woken up by another event.  In particular, it won't cause the transition to
  17:  * a sleep state to be aborted.
  18:  *
  19:  * This difficulty may be overcome if user space uses 'wakeup_count' before
  20:  * writing to 'state'.  It first should read from 'wakeup_count' and store
  21:  * the read value.  Then, after carrying out its own preparations for the system
  22:  * transition to a sleep state, it should write the stored value to
  23:  * 'wakeup_count'.  If that fails, at least one wakeup event has occurred since
  24:  * 'wakeup_count' was read and 'state' should not be written to.  Otherwise, it
  25:  * is allowed to write to 'state', but the transition will be aborted if there
  26:  * are any wakeup events detected after 'wakeup_count' was written to.
  27:  */
  28:  
  29: static ssize_t wakeup_count_show(struct kobject *kobj,
  30:                                 struct kobj_attribute *attr,
  31:                                 char *buf)
  32: {
  33:         unsigned int val;
  34:  
  35:         return pm_get_wakeup_count(&val, true) ?
  36:                 sprintf(buf, "%u\n", val) : -EINTR;
  37: }
  38:  
  39: static ssize_t wakeup_count_store(struct kobject *kobj,
  40:                                 struct kobj_attribute *attr,
  41:                                 const char *buf, size_t n)
  42: {
  43:         unsigned int val;
  44: int error;
  45:  
  46: error = pm_autosleep_lock();
  47: if (error)
  48:         return error;
  49:  
  50: if (pm_autosleep_state() > PM_SUSPEND_ON) {
  51:         error = -EBUSY;
  52:         goto out;
  53: }
  54:  
  55: error = -EINVAL;
  56: if (sscanf(buf, "%u", &val) == 1) {
  57:         if (pm_save_wakeup_count(val))
  58:                 error = n;
  59: }
  60:  
  61:  out:
  62: pm_autosleep_unlock();
  63: return error;
  64: }
  65:  
  66: tr(wakeup_count);

实现很简单:read时,直接调用pm_get_wakeup_count(注意第2个参数);write时,直接调用pm_save_wakeup_count(注意用户空间的wakeup count功能和auto sleep互斥,会在下篇文章解释原因)。这两个接口均是wakeup events framework提供的接口,跟着代码往下看吧。

4.3 pm_get_wakeup_count

pm_get_wakeup_count的实现如下:

   1: bool pm_get_wakeup_count(unsigned int *count, bool block)
   2: {
   3:         unsigned int cnt, inpr;
   4:  
   5:         if (block) {
   6:                 DEFINE_WAIT(wait);
   7:  
   8:                 for (;;) {
   9:                         prepare_to_wait(&wakeup_count_wait_queue, &wait,
  10:                                         TASK_INTERRUPTIBLE);
  11:                         split_counters(&cnt, &inpr);
  12:                         if (inpr == 0 || signal_pending(current))
  13:                                 break;
  14:  
  15:                         schedule();
  16:                 }
  17:                 finish_wait(&wakeup_count_wait_queue, &wait);
  18:         }
  19:  
  20:         split_counters(&cnt, &inpr);
  21:         *count = cnt;
  22:         return !inpr;
  23: }

该接口有两个参数,一个是保存返回的count值得指针,另一个指示是否block,具体请参考代码逻辑:

a)如果block为false,直接读取registered wakeup events和wakeup events in progress两个counter值,将registered wakeup events交给第一个参数,并返回wakeup events in progress的状态(若返回false,说明当前有wakeup events正在处理,不适合suspend)。

b)如果block为true,定义一个等待队列,等待wakeup events in progress为0,再返回counter。

 

注1:由4.2小节可知,sysfs发起的read动作,block为true,所以如果有正在处理的wakeup events,read进程会阻塞。其它模块(如auto sleep)发起的read,则可能不需要阻塞。

4.4 pm_save_wakeup_count

pm_save_wakeup_count的实现如下:

   1: bool pm_save_wakeup_count(unsigned int count)
   2: {
   3:         unsigned int cnt, inpr;
   4:         unsigned long flags;
   5:  
   6:         events_check_enabled = false;
   7:         spin_lock_irqsave(&events_lock, flags);
   8:         split_counters(&cnt, &inpr);
   9:         if (cnt == count && inpr == 0) {
  10:                 saved_count = count;
  11:                 events_check_enabled = true;
  12:         }
  13:         spin_unlock_irqrestore(&events_lock, flags);
  14:         return events_check_enabled;
  15: }

1)注意这个变量,events_check_enabled,如果它不为真,pm_wakeup_pending接口直接返回false,意味着如果不利用wakeup count功能,suspend过程中不会做任何wakeup events检查,也就不会进行任何的同步

2)解除当前的registered wakeup events、wakeup events in progress,保存在变量cnt和inpr中。

3)如果写入的值和cnt不同(说明读、写的过程中产生events),或者inpr不为零(说明有events正在被处理),返回false(说明此时不宜suspend)。

4)否则,events_check_enabled置位(后续的pm_wakeup_pending才会干活),返回true(可以suspend),并将当前的wakeup count保存在saved count变量中。

4.5 /sys/power/state

再回忆一下“Linux电源管理(6)_Generic PM之Suspend功能”中suspend的流程,在suspend_enter接口中,suspend前的最后一刻,会调用pm_wakeup_pending接口,代码如下:

   1: static int suspend_enter(suspend_state_t state, bool *wakeup)
   2: {
   3:     ...
   4:     error = syscore_suspend();
   5:     if (!error) {
   6:         *wakeup = pm_wakeup_pending();
   7:         if (!(suspend_test(TEST_CORE) || *wakeup)) {
   8:                 error = suspend_ops->enter(state);
   9:                 events_check_enabled = false;
  10:         }
  11:         syscore_resume();
  12:     }
  13:     ...
  14: }

在write wakeup_count到调用pm_wakeup_pending这一段时间内,wakeup events framework会照常产生wakeup events,因此如果pending返回true,则不能“enter”,终止suspend吧!

注2:wakeup后,会清除events_check_enabled标记。

Linux电源管理(7)_Wakeup events framework”中已经介绍过pm_wakeup_pending了,让我们再看一遍吧:

   1: bool pm_wakeup_pending(void)
   2: {
   3:         unsigned long flags;
   4:         bool ret = false;
   5:  
   6:         spin_lock_irqsave(&events_lock, flags);
   7:         if (events_check_enabled) {
   8:                 unsigned int cnt, inpr;
   9:  
  10:                 split_counters(&cnt, &inpr);
  11:                 ret = (cnt != saved_count || inpr > 0);
  12:                 events_check_enabled = !ret;
  13:         }
  14:         spin_unlock_irqrestore(&events_lock, flags);
  15:  
  16:         if (ret)
  17:                 print_active_wakeup_sources();
  18:  
  19:         return ret;
  20: }

a)首先会判断events_check_enabled是否有效,无效直接返回false。有效的话:

b)获得cnt和inpr,如果cnt不等于saved_count(说明这段时间内有events产生),或者inpr不为0(说明有events正在被处理),返回true(告诉调用者,放弃吧,时机不到)。同时清除events_check_enabled的状态。

c)否则,返回false(放心睡吧),同时保持events_check_enabled的置位状态(以免pm_wakeup_pending再次调用)。

Okay,结束了,等待wakeup吧~~~~

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net


debug方式

1. 先确认一下您的系统是否使能了trace功能(默认情况下都会使能)。
2. 如果使能,会存在“/sys/kernel/debug/tracing/”目录。
3. echo 1 > /sys/kernel/debug/tracing/events/power/wakeup_source_activate/enable,使能wakeup source activate的trace输出。
4. cat /sys/kernel/debug/tracing/trace,查看哪些driver一直处于activate状态,输出信息如下:
# entries-in-buffer/entries-written: 132/132   #P:1
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
    kworker/u8:0-28958 [000] d..2 11250.490538: wakeup_source_activate: delay_lock state=0x2f900004
    kworker/u8:0-28958 [000] d..4 11250.491522: wakeup_source_activate: atc260x-wall state=0x2f910004
    kworker/u8:0-28958 [000] d..4 11250.491553: wakeup_source_activate: atc260x-usb state=0x2f910005
     kworker/0:0-25134 [000] d..4 11250.491645: wakeup_source_activate: NETLINK state=0x2f910006
主要包括driver的名称,和当前的“combined_event_count”的值。



static irqreturn_t tp_irq_thread_fn(int irq, void *dev_id) { struct touchpanel_data *ts = (struct touchpanel_data *)dev_id; #ifdef CONFIG_TOUCHPANEL_TRUSTED_TOUCH #ifndef CONFIG_ARCH_QTI_VM if (atomic_read(&ts->trusted_touch_transition) || atomic_read(&ts->trusted_touch_enabled)) goto exit; #endif #endif ts->monitor_data.irq_to_report_timer = ktime_get(); if (ts->ws) { __pm_stay_awake(ts->ws); } /*for qaulcomn to stop cpu go to C4 idle state*/ #ifdef CONFIG_TOUCHIRQ_UPDATE_QOS if (ts->pm_qos_state && !ts->is_suspended && !ts->touch_count) { ts->pm_qos_value = PM_QOS_TOUCH_WAKEUP_VALUE; #if LINUX_VERSION_CODE >= KERNEL_VERSION(5,10,0) if (!cpu_latency_qos_request_active(&ts->pm_qos_req)) { cpu_latency_qos_add_request(&ts->pm_qos_req, ts->pm_qos_value); } else { cpu_latency_qos_update_request(&ts->pm_qos_req, ts->pm_qos_value); } #else pm_qos_update_request(&ts->pm_qos_req, ts->pm_qos_value); #endif } #endif if (ts->ts_ops->tp_irq_throw_away) { if (ts->ts_ops->tp_irq_throw_away(ts->chip_data)) { goto exit; } } /*for stop system go to sleep*/ /*wake_lock_timeout(&ts->wakelock, 1*HZ);*/ /*for check bus i2c/spi is ready or not*/ if (ts->bus_ready == false) { /*TP_INFO(ts->tp_index, "Wait device resume!");*/ wait_event_interruptible_timeout(ts->wait, ts->bus_ready, msecs_to_jiffies(ts->irq_need_dev_resume_time)); /*TP_INFO(ts->tp_index, "Device maybe resume!");*/ } if (false == monitor_irq_bus_ready(ts)) { goto exit; } /*for some ic such as samsung ic*/ if (ts->sec_long_low_trigger) { disable_irq_nosync(ts->irq); TP_INFO(ts->tp_index, "disable_irq_nosync common 2\n"); } /*for normal ic*/ if (ts->int_mode == BANNABLE) { mutex_lock(&ts->mutex); /*Only if the following four conditions are met at the same time: tp is suspend && gesture is disable && irq flag is edge-triggered && disable_suspend_irq_handler is set to 1, tp_work_func(ts) will be ignored */ if (!ts->is_suspended || (TP_ALL_GESTURE_SUPPORT && TP_ALL_GESTURE_ENABLE) || !(ts->irq_flags & (IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)) || !(ts->disable_suspend_irq_handler)) { tp_work_func(ts); } mutex_unlock(&ts->mutex); } else { /*for some ic such as synaptic tcm need get data by interrupt*/ tp_work_func(ts); } if (ts->sec_long_low_trigger) { TP_INFO(ts->tp_index, "enable_irq common 2\n"); enable_irq(ts->irq); } exit: #ifdef CONFIG_TOUCHIRQ_UPDATE_QOS if (PM_QOS_TOUCH_WAKEUP_VALUE == ts->pm_qos_value) { ts->pm_qos_value = PM_QOS_DEFAULT_VALUE; #if LINUX_VERSION_CODE >= KERNEL_VERSION(5,10,0) cpu_latency_qos_remove_request(&ts->pm_qos_req); #else pm_qos_update_request(&ts->pm_qos_req, ts->pm_qos_value); #endif } #endif if (ts->ws) { __pm_relax(ts->ws); } return IRQ_HANDLED; } 详细解释下这段代码
最新发布
12-25
<think>我们正在分析Linux内核中的`print_wakeup_sources`函数。这个函数通常用于调试和记录唤醒源(wakeup sources)的信息。唤醒源是内核中用于跟踪哪些设备或事件可以唤醒系统从挂起状态恢复的机制。 首先,我们需要了解这个函数的定义和调用位置。根据内核源码,`print_wakeup_sources`函数定义在`kernel/power/wakelock.c`文件中(在较新的内核中,可能位于`drivers/base/power/wakeup.c`)。它的主要作用是打印当前所有活跃的唤醒源的信息。 ### 函数实现分析 以下是基于Linux内核版本5.15的`print_wakeup_sources`函数实现(简化版): ```c void print_wakeup_sources(void) { struct wakeup_source *ws; int srcuidx; srcuidx = srcu_read_lock(&wakeup_srcu); list_for_each_entry_rcu(ws, &wakeup_sources, entry) { if (!ws->active) continue; pr_info("active wakeup source: %s\n", ws->name); } srcu_read_unlock(&wakeup_srcu, srcuidx); } EXPORT_SYMBOL_GPL(print_wakeup_sources); ``` ### 函数作用 1. **遍历唤醒源列表**:函数使用`list_for_each_entry_rcu`遍历全局链表`wakeup_sources`中的所有唤醒源。 2. **检查活跃状态**:对于每个唤醒源,检查其`active`字段是否为真。如果为真,则表示该唤醒源当前正在阻止系统挂起(或已经触发了唤醒)。 3. **打印信息**:对于每个活跃的唤醒源,使用`pr_info`打印其名称(`ws->name`)。 ### 调用场景 这个函数通常在内核的电源管理代码中被调用,尤其是在系统挂起/恢复过程中出现问题时,用于调试。例如,当系统从挂起状态恢复时,如果发现是某个唤醒源导致系统提前唤醒,就可以调用这个函数来查看是哪些唤醒源处于活跃状态。 ### 详细实现说明 1. **同步机制**:使用`srcu_read_lock`和`srcu_read_unlock`来保护对唤醒源列表的访问。这是一种读-拷贝-更新(RCU)的同步机制,允许多个读者同时访问,而写者则通过替换整个数据结构来更新。 2. **遍历条件**:只打印活跃(`active`)的唤醒源,因为非活跃的唤醒源不会阻止系统挂起。 3. **输出格式**:输出的每一行格式为`active wakeup source: [name]`,其中`[name]`是唤醒源的名称,通常由设备驱动在注册唤醒源时指定。 ### 示例输出 在内核日志中,调用`print_wakeup_sources`可能产生如下输出: ``` [ 12.345678] active wakeup source: ehci_hcd:usb1 [ 12.345679] active wakeup source: i2c_designware.0 ``` 这表明有两个活跃的唤醒源:一个是USB控制器(ehci_hcd),另一个是I2C控制器。 ### 相关内核参数 在调试系统挂起/恢复问题时,可以通过以下内核参数启用更详细的日志: - `pm_debug_messages`:启用电源管理调试消息,包括唤醒源信息。 - `initcall_debug`:跟踪初始化函数调用,有助于发现驱动初始化问题。 ### 实际应用 在系统挂起失败或意外唤醒时,可以通过以下方式手动调用该函数(或使用内核参数触发): ```bash # 在系统挂起前,查看当前活跃的唤醒源 echo dump > /sys/power/wakeup_sources ``` 或者通过内核调试工具: ```bash # 通过sysrq触发 echo l > /proc/sysrq-trigger # 这会打印所有活动CPU的回溯,包括唤醒源信息(在某些配置下) ``` ### 总结 `print_wakeup_sources`函数是一个调试辅助函数,用于在内核日志中打印当前活跃的唤醒源。它帮助开发者和系统管理员识别哪些设备或事件正在阻止系统进入挂起状态,或者导致系统从挂起状态中唤醒。在电源管理问题排查中非常有用。 §§相关问题§§ 1. 如何在内核运行时动态获取唤醒源的状态? 2. 唤醒源在内核中是如何注册和注销的? 3. 除了`print_wakeup_sources`,还有哪些工具或方法可以调试系统挂起/恢复问题?</think>### Linux 内核 `print_wakeup_sources` 函数分析与实现 `print_wakeup_sources` 是 Linux 内核电源管理子系统中的关键调试函数,用于输出系统中所有唤醒源(wakeup sources)的详细信息。它在诊断系统挂起/恢复问题时至关重要。 --- #### 1. 函数作用与定位 - **核心功能**:遍历并打印所有注册的唤醒源状态,包括: - 唤醒源名称(通常关联设备驱动) - 当前激活状态 - 阻止系统挂起的次数 - 最后一次活动时间戳 - **调用场景**: - 系统从挂起状态恢复失败时 - 通过 `dmesg` 或 `sysfs` 主动触发调试 - 电源管理子系统异常检测 --- #### 2. 源码实现分析(基于 Linux 5.15 内核) ```c // drivers/base/power/wakeup.c void print_wakeup_sources(void) { struct wakeup_source *ws; int srcuidx, active = 0; srcuidx = srcu_read_lock(&wakeup_srcu); // 获取RCU读锁 // 遍历全局唤醒源链表 list_for_each_entry_rcu(ws, &wakeup_sources, entry) { if (ws->active) { active = 1; // 标记存在活跃唤醒源 // 打印唤醒源详细信息 pr_info("active wakeup source: %s\n", ws->name); pr_info(" event_count: %u\n", ws->event_count); pr_info(" wakeup_count: %u\n", ws->wakeup_count); pr_info(" expire_count: %u\n", ws->expire_count); pr_info(" last_time: %lld\n", ktime_to_ms(ws->last_time)); } } if (!active) pr_info("No active wakeup sources\n"); srcu_read_unlock(&wakeup_srcu, srcuidx); // 释放RCU锁 } EXPORT_SYMBOL_GPL(print_wakeup_sources); ``` 关键数据结构: ```c struct wakeup_source { const char *name; // 唤醒源名称(如"ehci-pci") struct list_head entry; // 全局链表节点 spinlock_t lock; ktime_t last_time; // 最后活动时间 unsigned long event_count; // 事件计数 unsigned long wakeup_count; // 唤醒计数 bool active:1; // 当前激活状态 // ... 其他字段省略 }; ``` --- #### 3. 关键实现机制 1. **RCU保护机制**: - 使用 `srcu_read_lock()`/`srcu_read_unlock()` 保护全局链表 `wakeup_sources` - 确保在遍历时唤醒源列表不会被并发修改 2. **唤醒源状态检测**: ```c if (ws->active) { ... } ``` - 仅打印当前激活的唤醒源(阻止系统挂起的设备) 3. **时间统计**: ```c ktime_to_ms(ws->last_time) // 转换为毫秒时间戳 ``` - 帮助确定最后一次活动的时间点 4. **计数指标**: - `event_count`:唤醒事件总数 - `wakeup_count`:成功唤醒系统的次数 - `expire_count`:唤醒超时次数 --- #### 4. 典型输出示例 ```log [ 12.345678] active wakeup source: ehci-pci:0000:00:1d.0 [ 12.345679] event_count: 42 [ 12.345680] wakeup_count: 5 [ 12.345681] expire_count: 0 [ 12.345682] last_time: 1640995200000 [ 12.345683] active wakeup source: i2c_designware.0 [ 12.345684] event_count: 18 ... ``` --- #### 5. 实际调试应用 **手动触发打印**: ```bash # 通过sysfs触发 echo 1 > /sys/kernel/debug/wakeup_sources/print_active # 内核日志查看 dmesg | grep "active wakeup source" ``` **在挂起流程中的调用点**: ```c // kernel/power/suspend.c static int suspend_enter(suspend_state_t state, bool *wakeup) { ... if (*wakeup) { print_wakeup_sources(); // 唤醒时打印源 return -EBUSY; } ... } ``` --- #### 6. 唤醒源生命周期 ```mermaid graph LR A[设备驱动注册] -->|wakeup_source_register| B[加入全局链表] B --> C[事件触发唤醒] C --> D[更新计数/时间戳] D --> E[系统唤醒] E --> F[print_wakeup_sources记录] F --> G[驱动卸载时移除] ``` --- ### 总结 `print_wakeup_sources` 通过遍历全局唤醒源链表并筛选活跃项,输出关键调试信息: 1. 使用 RCU 机制保证并发安全 2. 聚焦于 `active=true` 的唤醒源 3. 提供时间戳和计数等关键指标 4. 是诊断 "unknown wakeup cause" 问题的核心工具 通过分析其输出,可快速定位阻止系统挂起的设备(如 USB 控制器、网卡等),为电源管理问题提供直接证据[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值