Rtthread信号量/互斥量实现分析

10 篇文章 1 订阅
8 篇文章 0 订阅

Rtthread信号量实现

信号量主要用作线程间的同步及互斥,信号量的获取不能在ISR中调用,会导致中断挂起,
系统不能有效的进行线程切换及运行。信号量分为动态创建信号量和静态创建信号量,当
创建信号量时系统会初始化IPC以及与semaphone相关的部分。在创建信号量指定的参数
过程中,flag参数定义FIFO时,信号量采取先入先出的方式,定义为IPRO时,信号量采取
优先级的方式,优先级高的线程将先获得等待的信号量。

代码实现:

/**
 * IPC flags and control command definitions
 */
#define RT_IPC_FLAG_FIFO                0x00            /**< FIFOed IPC. @ref IPC. */
#define RT_IPC_FLAG_PRIO                0x01            /**< PRIOed IPC. @ref IPC. */

#define RT_IPC_CMD_UNKNOWN              0x00            /**< unknown IPC command */
#define RT_IPC_CMD_RESET                0x01            /**< reset IPC object */

#define RT_WAITING_FOREVER              -1              /**< Block forever until get resource. */
#define RT_WAITING_NO                   0               /**< Non-block. */

/**
 * Base structure of IPC object
 */
struct rt_ipc_object
{
    struct rt_object parent;                            /**< inherit from rt_object */

    rt_list_t        suspend_thread;                    /**< threads pended on this resource */
};

信号量是依赖IPC来实现的,先看看IPC部分源码:

//ipc结构中维护了一个suspend thread链表,用来将阻塞的线程挂载到上面
rt_inline rt_err_t rt_ipc_object_init(struct rt_ipc_object *ipc)
{
    /* initialize ipc object */
    rt_list_init(&(ipc->suspend_thread));

    return RT_EOK;
}
//根据flag的值(RT_IPC_FLAG_FIFO/RT_IPC_FLAG_PRIO)决定按优先级排序还是按照先进先出的顺序将当前线程插入链表
rt_inline rt_err_t rt_ipc_list_suspend(rt_list_t        *list,
                                       struct rt_thread *thread,
                                       rt_uint8_t        flag)
{
    /* suspend thread */
    rt_thread_suspend(thread);//先将线程suspend,移除出就绪表,并将status切到suspend状态
    switch (flag) //判断flag
    {
    case RT_IPC_FLAG_FIFO: //先进先出
        rt_list_insert_before(list, &(thread->tlist));
        break;
    case RT_IPC_FLAG_PRIO://优先级
        {
            struct rt_list_node *n;
            struct rt_thread *sthread;

            /* find a suitable position */
            for (n = list->next; n != list; n = n->next)//遍历链表
            {
                sthread = rt_list_entry(n, struct rt_thread, tlist);
                /* find out */
                if (thread->current_priority < sthread->current_priority)//比较优先级
                {
                    /* insert this thread before the sthread */
                    rt_list_insert_before(&(sthread->tlist), &(thread->tlist));//插入到比它优先级低的线程前面(值越小越高)
                    break;
                }
            }
            if (n == list)//链表是空的情况
                rt_list_insert_before(list, &(thread->tlist));
        }
        break;
    default:
        break;
    }
    return RT_EOK;
}
//唤醒线程,将线程插入到就绪表
rt_inline rt_err_t rt_ipc_list_resume(rt_list_t *list)
{
    struct rt_thread *thread;

    /* get thread entry */
    thread = rt_list_entry(list->next, struct rt_thread, tlist);

    RT_DEBUG_LOG(RT_DEBUG_IPC, ("resume thread:%s\n", thread->name));

    /* resume it */
    rt_thread_resume(thread);//resume只将线程插入到就绪表中,跟suspend动作正好相反
    return RT_EOK;
}
//唤醒所有suspend阻塞的线程
rt_inline rt_err_t rt_ipc_list_resume_all(rt_list_t *list)
{
    struct rt_thread *thread;
    register rt_ubase_t temp;

    /* wakeup all suspended threads */
    while (!rt_list_isempty(list))
    {
        /* disable interrupt */
        temp = rt_hw_interrupt_disable();

        /* get next suspended thread */
        thread = rt_list_entry(list->next, struct rt_thread, tlist);
        /* set error code to RT_ERROR */
        thread->error = -RT_ERROR;

        /*
         * resume thread
         * In rt_thread_resume function, it will remove current thread from
         * suspended list
         */
        rt_thread_resume(thread);
        /* enable interrupt */
        rt_hw_interrupt_enable(temp);
    }
    return RT_EOK;
}

下面是信号量的实现

#ifdef RT_USING_SEMAPHORE
/**
 * Semaphore structure
 */
struct rt_semaphore
{
    struct rt_ipc_object parent;                        /**< inherit from ipc_object */

    rt_uint16_t          value;                         /**< value of semaphore. */
    rt_uint16_t          reserved;                      /**< reserved field */
};
typedef struct rt_semaphore *rt_sem_t;
#endif

//初始化部分:跟其他模块是一样的实现,包括静态/动态的创建方法和对应的分离/删除
rt_err_t rt_sem_init(rt_sem_t    sem,
                     const char *name,
                     rt_uint32_t value,
                     rt_uint8_t  flag)
{
    RT_ASSERT(sem != RT_NULL);
    RT_ASSERT(value < 0x10000U);
    /* initialize object */
    rt_object_init(&(sem->parent.parent), RT_Object_Class_Semaphore, name);
    /* initialize ipc object */
    rt_ipc_object_init(&(sem->parent));
    /* set initial value */
    sem->value = (rt_uint16_t)value;
    /* set parent */
    sem->parent.parent.flag = flag;
    return RT_EOK;
}
rt_err_t rt_sem_detach(rt_sem_t sem)
{
    /* parameter check */
    RT_ASSERT(sem != RT_NULL);
    RT_ASSERT(rt_object_get_type(&sem->parent.parent) == RT_Object_Class_Semaphore);
    RT_ASSERT(rt_object_is_systemobject(&sem->parent.parent));

    /* wakeup all suspended threads */
    rt_ipc_list_resume_all(&(sem->parent.suspend_thread));

    /* detach semaphore object */
    rt_object_detach(&(sem->parent.parent));

    return RT_EOK;
}

#ifdef RT_USING_HEAP
rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag)
{
    rt_sem_t sem;

    RT_DEBUG_NOT_IN_INTERRUPT;
    RT_ASSERT(value < 0x10000U);

    /* allocate object */
    sem = (rt_sem_t)rt_object_allocate(RT_Object_Class_Semaphore, name);
    if (sem == RT_NULL)
        return sem;
    /* initialize ipc object */
    rt_ipc_object_init(&(sem->parent));
    /* set initial value */
    sem->value = value;
    /* set parent */
    sem->parent.parent.flag = flag;
    return sem;
}
rt_err_t rt_sem_delete(rt_sem_t sem)
{
    RT_DEBUG_NOT_IN_INTERRUPT;

    /* parameter check */
    RT_ASSERT(sem != RT_NULL);
    RT_ASSERT(rt_object_get_type(&sem->parent.parent) == RT_Object_Class_Semaphore);
    RT_ASSERT(rt_object_is_systemobject(&sem->parent.parent) == RT_FALSE);
    /* wakeup all suspended threads */
    rt_ipc_list_resume_all(&(sem->parent.suspend_thread));
    /* delete semaphore object */
    rt_object_delete(&(sem->parent.parent));
    return RT_EOK;
}
#endif
//获取信号量
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time)
{
    register rt_base_t temp;
    struct rt_thread *thread;

    /* parameter check */
    RT_ASSERT(sem != RT_NULL);
    RT_ASSERT(rt_object_get_type(&sem->parent.parent) == RT_Object_Class_Semaphore);
    RT_OBJECT_HOOK_CALL(rt_object_trytake_hook, (&(sem->parent.parent)));
    /* disable interrupt */
    temp = rt_hw_interrupt_disable();//关中断
    RT_DEBUG_LOG(RT_DEBUG_IPC, ("thread %s take sem:%s, which value is: %d\n",
                                rt_thread_self()->name,
                                ((struct rt_object *)sem)->name,
                                sem->value));
    if (sem->value > 0)//资源空闲,可以被获取
    {
        /* semaphore is available */
        sem->value --;
        /* enable interrupt */
        rt_hw_interrupt_enable(temp);
    }
    else// <= 0
    {
        /* no waiting, return with timeout */
        if (time == 0) //等待时间为0,不等待,直接返回超时error 
        {
            rt_hw_interrupt_enable(temp);
            return -RT_ETIMEOUT;
        }
        else //等待时间不为0
        {
            /* current context checking */
            RT_DEBUG_IN_THREAD_CONTEXT;

            /* semaphore is unavailable, push to suspend list */
            /* get current thread */
            thread = rt_thread_self();//获取当前线程
            /* reset thread error number */
            thread->error = RT_EOK;
            RT_DEBUG_LOG(RT_DEBUG_IPC, ("sem take: suspend thread - %s\n",
                                        thread->name));
            //因为没有资源,并且有设置等待时间,那么就要阻塞该线程。将该线程suspend掉
            rt_ipc_list_suspend(&(sem->parent.suspend_thread),
                                thread,
                                sem->parent.parent.flag);
            /* has waiting time, start thread timer */
            if (time > 0)//等待时间大于0
            {
                RT_DEBUG_LOG(RT_DEBUG_IPC, ("set thread:%s to timer list\n",
                                            thread->name));
                //开启定时器,设置超时时间
                rt_timer_control(&(thread->thread_timer),
                                 RT_TIMER_CTRL_SET_TIME,
                                 &time);
                rt_timer_start(&(thread->thread_timer));
            }
            /* enable interrupt */
            rt_hw_interrupt_enable(temp);
            /* do schedule */
            rt_schedule();//启动线程调度,因为当前线程已经被suspend了。
            if (thread->error != RT_EOK)
            {
                return thread->error;
            }
        }
    }
    RT_OBJECT_HOOK_CALL(rt_object_take_hook, (&(sem->parent.parent)));
    return RT_EOK;
}
rt_err_t rt_sem_trytake(rt_sem_t sem)
{
    return rt_sem_take(sem, 0);//timeout 为0
}

rt_err_t rt_sem_release(rt_sem_t sem)
{
    register rt_base_t temp;
    register rt_bool_t need_schedule;

    /* parameter check */
    RT_ASSERT(sem != RT_NULL);
    RT_ASSERT(rt_object_get_type(&sem->parent.parent) == RT_Object_Class_Semaphore);
    RT_OBJECT_HOOK_CALL(rt_object_put_hook, (&(sem->parent.parent)));
    need_schedule = RT_FALSE;
    /* disable interrupt */
    temp = rt_hw_interrupt_disable();
    RT_DEBUG_LOG(RT_DEBUG_IPC, ("thread %s releases sem:%s, which value is: %d\n",
                                rt_thread_self()->name,
                                ((struct rt_object *)sem)->name,
                                sem->value));
     //当前信号量阻塞的线程链表不为空,其他线程会回收信号量,所以这里不用改变sem->value的值
    if (!rt_list_isempty(&sem->parent.suspend_thread))//他的parent是rt_ipc_object
    {
        /* resume the suspended thread */
        rt_ipc_list_resume(&(sem->parent.suspend_thread));//取出第一个线程,并恢复它
        need_schedule = RT_TRUE;
    }
    else //没有等待同步的线程,这里需要自己恢复sem->value
    {
        if(sem->value < RT_SEM_VALUE_MAX)//回收信号量
        {
            sem->value ++; /* increase value */
        }
        else
        {
            rt_hw_interrupt_enable(temp); /* enable interrupt */
            return -RT_EFULL; /* value overflowed */
        }
    }
    /* enable interrupt */
    rt_hw_interrupt_enable(temp);
    /* resume a thread, re-schedule */
    if (need_schedule == RT_TRUE)
        rt_schedule();
    return RT_EOK;
}
rt_err_t rt_sem_control(rt_sem_t sem, int cmd, void *arg)
{
    rt_ubase_t level;
    /* parameter check */
    RT_ASSERT(sem != RT_NULL);
    RT_ASSERT(rt_object_get_type(&sem->parent.parent) == RT_Object_Class_Semaphore);
    if (cmd == RT_IPC_CMD_RESET)
    {
        rt_ubase_t value;
        /* get value */
        value = (rt_ubase_t)arg;
        /* disable interrupt */
        level = rt_hw_interrupt_disable();
        /* resume all waiting thread */
        rt_ipc_list_resume_all(&sem->parent.suspend_thread);
        /* set new value */
        sem->value = (rt_uint16_t)value;
        /* enable interrupt */
        rt_hw_interrupt_enable(level);
        rt_schedule();
        return RT_EOK;
    }
    return -RT_ERROR;
}

下面再看看mutex的实现

#ifdef RT_USING_MUTEX
/**
 * Mutual exclusion (mutex) structure
 */
struct rt_mutex
{
    struct rt_ipc_object parent;                        /**< inherit from ipc_object */

    rt_uint16_t          value;                         /**< value of mutex */

    rt_uint8_t           original_priority;             /**< priority of last thread hold the mutex */
    rt_uint8_t           hold;                          /**< numbers of thread hold the mutex */

    struct rt_thread    *owner;                         /**< current owner of mutex */
};
typedef struct rt_mutex *rt_mutex_t;
#endif
#ifdef RT_USING_MUTEX
//mutex初始化函数
rt_err_t rt_mutex_init(rt_mutex_t mutex, const char *name, rt_uint8_t flag)
{
    /* parameter check */
    RT_ASSERT(mutex != RT_NULL);
    /* initialize object */
    rt_object_init(&(mutex->parent.parent), RT_Object_Class_Mutex, name);
    /* initialize ipc object */
    rt_ipc_object_init(&(mutex->parent));
    mutex->value = 1;
    mutex->owner = RT_NULL;
    mutex->original_priority = 0xFF;
    mutex->hold  = 0;
    /* set flag */
    mutex->parent.parent.flag = flag;
    return RT_EOK;
}
rt_err_t rt_mutex_detach(rt_mutex_t mutex)
{
    /* parameter check */
    RT_ASSERT(mutex != RT_NULL);
    RT_ASSERT(rt_object_get_type(&mutex->parent.parent) == RT_Object_Class_Mutex);
    RT_ASSERT(rt_object_is_systemobject(&mutex->parent.parent));
    /* wakeup all suspended threads */
    rt_ipc_list_resume_all(&(mutex->parent.suspend_thread));
    /* detach semaphore object */
    rt_object_detach(&(mutex->parent.parent));
    return RT_EOK;
}

#ifdef RT_USING_HEAP
rt_mutex_t rt_mutex_create(const char *name, rt_uint8_t flag)
{
    struct rt_mutex *mutex;
    RT_DEBUG_NOT_IN_INTERRUPT;
    /* allocate object */
    mutex = (rt_mutex_t)rt_object_allocate(RT_Object_Class_Mutex, name);
    if (mutex == RT_NULL)
        return mutex;
    /* initialize ipc object */
    rt_ipc_object_init(&(mutex->parent));

    mutex->value              = 1;
    mutex->owner              = RT_NULL;
    mutex->original_priority  = 0xFF;
    mutex->hold               = 0;
    /* set flag */
    mutex->parent.parent.flag = flag;

    return mutex;
}

/**
 * This function will delete a mutex object and release the memory
 *
 * @param mutex the mutex object
 *
 * @return the error code
 *
 * @see rt_mutex_detach
 */
rt_err_t rt_mutex_delete(rt_mutex_t mutex)
{
    RT_DEBUG_NOT_IN_INTERRUPT;

    /* parameter check */
    RT_ASSERT(mutex != RT_NULL);
    RT_ASSERT(rt_object_get_type(&mutex->parent.parent) == RT_Object_Class_Mutex);
    RT_ASSERT(rt_object_is_systemobject(&mutex->parent.parent) == RT_FALSE);
    /* wakeup all suspended threads */
    rt_ipc_list_resume_all(&(mutex->parent.suspend_thread));
    /* delete mutex object */
    rt_object_delete(&(mutex->parent.parent));
    return RT_EOK;
}
#endif

/**
 * This function will take a mutex, if the mutex is unavailable, the
 * thread shall wait for a specified time.
 *
 * @param mutex the mutex object
 * @param time the waiting time
 *
 * @return the error code
 */
rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t time)
{
    register rt_base_t temp;
    struct rt_thread *thread;

    /* this function must not be used in interrupt even if time = 0 */
    RT_DEBUG_IN_THREAD_CONTEXT;
    /* parameter check */
    RT_ASSERT(mutex != RT_NULL);
    RT_ASSERT(rt_object_get_type(&mutex->parent.parent) == RT_Object_Class_Mutex);

    /* get current thread */
    thread = rt_thread_self();//获取当前线程
    /* disable interrupt */
    temp = rt_hw_interrupt_disable();
    RT_OBJECT_HOOK_CALL(rt_object_trytake_hook, (&(mutex->parent.parent)));
    RT_DEBUG_LOG(RT_DEBUG_IPC,
                 ("mutex_take: current thread %s, mutex value: %d, hold: %d\n",
                  thread->name, mutex->value, mutex->hold));
    /* reset thread error */
    thread->error = RT_EOK;
    if (mutex->owner == thread)//当前mutex的拥有者就是当前线程(重复加锁)
    {
        if(mutex->hold < RT_MUTEX_HOLD_MAX) 
        {
            /* it's the same thread */
            mutex->hold ++;
        }
        else
        {
            rt_hw_interrupt_enable(temp); /* enable interrupt */
            return -RT_EFULL; /* value overflowed */
        }
    }
    else //其他线程获取mutex
    {
        /* The value of mutex is 1 in initial status. Therefore, if the
         * value is great than 0, it indicates the mutex is avaible.
         */
        if (mutex->value > 0) //锁未被使用
        {
            /* mutex is available */
            mutex->value --;

            /* set mutex owner and original priority */
            mutex->owner             = thread;//更新mutex拥有者owner线程
            mutex->original_priority = thread->current_priority;//保存线程优先级
            if(mutex->hold < RT_MUTEX_HOLD_MAX)
            {
                mutex->hold ++;//hold标志++
            }
            else
            {
                rt_hw_interrupt_enable(temp); /* enable interrupt */
                return -RT_EFULL; /* value overflowed */
            }
        }
        else //锁已经被其他线程占有
        {
            /* no waiting, return with timeout */
            if (time == 0) //不等待
            {
                /* set error as timeout */
                thread->error = -RT_ETIMEOUT;

                /* enable interrupt */
                rt_hw_interrupt_enable(temp);

                return -RT_ETIMEOUT;//返回超时错误
            }
            else //等待
            {
                /* mutex is unavailable, push to suspend list */
                RT_DEBUG_LOG(RT_DEBUG_IPC, ("mutex_take: suspend thread: %s\n",
                                            thread->name));
                /* change the owner thread priority of mutex */
                if (thread->current_priority < mutex->owner->current_priority)//需要获取锁的线程优先级比当前占有锁的优先级高
                {
                    //这里将当前持锁的线程优先级提高到与等待持锁线程一样的优先级
                    rt_thread_control(mutex->owner,
                                      RT_THREAD_CTRL_CHANGE_PRIORITY,
                                      &thread->current_priority);//修改当前占有锁的线程的优先级为当前线程
                }

                /* suspend current thread */
                rt_ipc_list_suspend(&(mutex->parent.suspend_thread),
                                    thread,
                                    mutex->parent.parent.flag);//因为锁被其他线程占用,所以先挂起当前线程等待

                /* has waiting time, start thread timer */
                if (time > 0)//等待时间大于0
                {
                    RT_DEBUG_LOG(RT_DEBUG_IPC,
                                 ("mutex_take: start the timer of thread:%s\n",
                                  thread->name));

                    /* reset the timeout of thread timer and start it */
                    rt_timer_control(&(thread->thread_timer),
                                     RT_TIMER_CTRL_SET_TIME,
                                     &time);
                    rt_timer_start(&(thread->thread_timer));//开启定时
                }
                /* enable interrupt */
                rt_hw_interrupt_enable(temp);//使能中断

                /* do schedule */
                rt_schedule();//因为当前线程已经挂起,所以开启调度

                if (thread->error != RT_EOK)
                {
                    /* return error */
                    return thread->error;
                }
                else
                {
                    /* the mutex is taken successfully. */
                    /* disable interrupt */
                    temp = rt_hw_interrupt_disable();
                }
            }
        }
    }
    /* enable interrupt */
    rt_hw_interrupt_enable(temp);

    RT_OBJECT_HOOK_CALL(rt_object_take_hook, (&(mutex->parent.parent)));

    return RT_EOK;
}

/**
 * This function will release a mutex, if there are threads suspended on mutex,
 * it will be waked up.
 *
 * @param mutex the mutex object
 *
 * @return the error code
 */
rt_err_t rt_mutex_release(rt_mutex_t mutex)
{
    register rt_base_t temp;
    struct rt_thread *thread;
    rt_bool_t need_schedule;

    /* parameter check */
    RT_ASSERT(mutex != RT_NULL);
    RT_ASSERT(rt_object_get_type(&mutex->parent.parent) == RT_Object_Class_Mutex);

    need_schedule = RT_FALSE;

    /* only thread could release mutex because we need test the ownership */
    RT_DEBUG_IN_THREAD_CONTEXT;

    /* get current thread */
    thread = rt_thread_self();//获取当前线程

    /* disable interrupt */
    temp = rt_hw_interrupt_disable();

    RT_DEBUG_LOG(RT_DEBUG_IPC,
                 ("mutex_release:current thread %s, mutex value: %d, hold: %d\n",
                  thread->name, mutex->value, mutex->hold));

    RT_OBJECT_HOOK_CALL(rt_object_put_hook, (&(mutex->parent.parent)));

    /* mutex only can be released by owner */
    if (thread != mutex->owner)//谁加锁谁释放
    {
        thread->error = -RT_ERROR;

        /* enable interrupt */
        rt_hw_interrupt_enable(temp);

        return -RT_ERROR;
    }

    /* decrease hold */
    mutex->hold --;
    /* if no hold */
    if (mutex->hold == 0)//完全释放
    {
        /* change the owner thread to original priority */
        //因为加锁的时候有提高当前线程的优先级,所以释放的时候要调整回来
        if (mutex->original_priority != mutex->owner->current_priority)
        {
            rt_thread_control(mutex->owner,
                              RT_THREAD_CTRL_CHANGE_PRIORITY,
                              &(mutex->original_priority));
        }

        /* wakeup suspended thread */
        if (!rt_list_isempty(&mutex->parent.suspend_thread))//唤醒在等待锁的线程
        {
            /* get suspended thread */
            thread = rt_list_entry(mutex->parent.suspend_thread.next,
                                   struct rt_thread,
                                   tlist);

            RT_DEBUG_LOG(RT_DEBUG_IPC, ("mutex_release: resume thread: %s\n",
                                        thread->name));

            /* set new owner and priority */
            mutex->owner             = thread;
            mutex->original_priority = thread->current_priority;
            if(mutex->hold < RT_MUTEX_HOLD_MAX)//给唤醒的线程加上hold标志
            {
                mutex->hold ++;
            }
            else
            {
                rt_hw_interrupt_enable(temp); /* enable interrupt */
                return -RT_EFULL; /* value overflowed */
            }

            /* resume thread */
            rt_ipc_list_resume(&(mutex->parent.suspend_thread));//resume 线程

            need_schedule = RT_TRUE;//需要调度
        }
        else //没有线程在等待锁
        {
            if(mutex->value < RT_MUTEX_VALUE_MAX)
            {
                /* increase value */
                mutex->value ++;
            }
            else
            {
                rt_hw_interrupt_enable(temp); /* enable interrupt */
                return -RT_EFULL; /* value overflowed */
            }

            /* clear owner */
            mutex->owner             = RT_NULL;
            mutex->original_priority = 0xff;
        }
    }

    /* enable interrupt */
    rt_hw_interrupt_enable(temp);

    /* perform a schedule */
    if (need_schedule == RT_TRUE)
        rt_schedule();//调度

    return RT_EOK;
}

/**
 * This function can get or set some extra attributions of a mutex object.
 *
 * @param mutex the mutex object
 * @param cmd the execution command
 * @param arg the execution argument
 *
 * @return the error code
 */
rt_err_t rt_mutex_control(rt_mutex_t mutex, int cmd, void *arg)
{
    /* parameter check */
    RT_ASSERT(mutex != RT_NULL);
    RT_ASSERT(rt_object_get_type(&mutex->parent.parent) == RT_Object_Class_Mutex);

    return -RT_ERROR;
}
#endif /* end of RT_USING_MUTEX */
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tony++

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值