FreeRtos学习2

FreeRTos任务管理分析

  (2011-08-05 16:22:01)
标签: 

freertos

 

杂谈

分类: freertos入门学习

FreeRTOS任务管理分析


freertos是一个轻量级的rtos,它目前实现了一个微内核,并且port到arm7, avr, pic18, coldfire等众多处理器上;目前已经在rtos的市场上占有不少的份额。它当然不是一个与vxworks之类的rtos竞争的操作系统,它的目标在于低性能小RAM的处理器上。整个系统只有3个文件,外加上port的和处理器相关的两个文件,实现是很简洁的。
与ucosii不同,它是free的,ucosii不是free的,虽然它的代码是公开的。FreeRTOS提供的功能包括:任务管理、时间管理、信号量、消息队列、内存管理。FreeRTOS内核支持优先级调度算法,每个任务可根据重要程度的不同被赋予一定的优先级,CPU总是让处于就绪态的、优先级最高的任务先运行。FreeRT0S内核同时支持轮换调度算法,系统允许不同的任务使用相同的优先级,在没有更高优先级任务就绪的情况下,同一优先级的任务共享CPU的使用时间。这一点是和ucosii不同的。
另外一点不同是freertos既可以配置为可抢占内核也可以配置为不可抢占内核。当FreeRTOS被设置为可剥夺型内核时,处于就绪态的高优先级任务能剥夺低优先级任务的CPU使用权,这样可保证系统满足实时性的要求;当FreeRTOS被设置为不可剥夺型内核时,处于就绪态的高优先级任务只有等当前运行任务主动释放CPU的使用权后才能获得运行,这样可提高CPU的运行效率。
这篇文章是以freertos v5.0版本的代码为例子分析下它的任务管理方面的实现。时间关系可能没有太多时间写的很详细了。
1.链表管理
freertos里面的任务管理,queue,semaphore管理等都借助于双向链表,它定义了个通用的数据结构


struct xLIST_ITEM
{
    portTickType xItemValue; //链表节点的数据项,通常用在任务延时,表示                              //一个任务延时的节拍数      
    volatile struct xLIST_ITEM * pxNext; //通过这两个成员变量将所有节点
    volatile struct xLIST_ITEM * pxPrevious;//链接成双向链表 
    void * pvOwner;  //指向该item的所有者,通常是任务控制块            
    void * pvContainer; //指向此链表结点所在的链表                    
};
这个数据结构定义了一个通用的链表节点;下面的数据结构定义了一个双向链表
typedef struct xLIST
{
    volatile unsigned portBASE_TYPE uxNumberOfItems;//表示该链表中节点的数目
    volatile xListItem * pxIndex;//用于遍历链表,指向上次访问的节点         
    volatile xMiniListItem xListEnd;//链表尾结点
} xList;

而下面这个数据结构用在xList中,只是为了标记一个链表的尾,是一个marker
struct xMINI_LIST_ITEM
{
    portTickType xItemValue;
    volatile struct xLIST_ITEM *pxNext;
    volatile struct xLIST_ITEM *pxPrevious;
};
typedef struct xMINI_LIST_ITEM xMiniListItem;

对于链表的操作也定义了一系列的函数和宏,在list.c文件中。如初始化个链表,吧一个节点插入链表等。
初始化链表:
void vListInitialise( xList *pxList )
{
   
    pxList->pxIndex = ( xListItem * ) &( pxList->xListEnd );

   
    pxList->xListEnd.xItemValue = portMAX_DELAY;

   
    pxList->xListEnd.pxNext = ( xListItem * ) &( pxList->xListEnd );
    pxList->xListEnd.pxPrevious = ( xListItem * ) &( pxList->xListEnd );

    pxList->uxNumberOfItems = 0;
}
把一个节点插入到链表尾部:
void vListInsertEnd( xList *pxList, xListItem *pxNewListItem )
{
volatile xListItem * pxIndex;

   
    pxIndex = pxList->pxIndex;

    pxNewListItem->pxNext = pxIndex->pxNext;
    pxNewListItem->pxPrevious = pxList->pxIndex;
    pxIndex->pxNext->pxPrevious = ( volatile xListItem * ) pxNewListItem;
    pxIndex->pxNext = ( volatile xListItem * ) pxNewListItem;
    pxList->pxIndex = ( volatile xListItem * ) pxNewListItem;

   
    pxNewListItem->pvContainer = ( void * ) pxList;

    ( pxList->uxNumberOfItems )++;
}
这些就不多说了。

 

2.任务控制块

typedef struct tskTaskControlBlock

{

       volatile portSTACK_TYPE     *pxTopOfStack;//指向堆栈顶

       xListItem                      xGenericListItem;   //通过它将任务连入就绪链表或者延时链表或者挂起链表中

       xListItem                      xEventListItem;//通过它把任务连入事件等待链表

       unsigned portBASE_TYPE    uxPriority;//优先级

       portSTACK_TYPE               *pxStack;              //指向堆栈起始位置

       signed portCHAR                 pcTaskName[ configMAX_TASK_NAME_LEN ];

       #if ( portCRITICAL_NESTING_IN_TCB == 1 )

              unsigned portBASE_TYPE uxCriticalNesting;

       #endif

 

       #if ( configUSE_TRACE_FACILITY == 1 )

              unsigned portBASE_TYPE    uxTCBNumber;//用于trace,debug时候提供方便

       #endif    

             

       #if ( configUSE_MUTEXES == 1 )

              unsigned portBASE_TYPE uxBasePriority;//当用mutex发生优先级反转时用

       #endif

 

       #if ( configUSE_APPLICATION_TASK_TAG == 1 )

              pdTASK_HOOK_CODE pxTaskTag;

       #endif

             

} tskTCB;

其中uxBasePriority用于解决优先级反转,freertos采用优先级继承的办法解决这个问题,在继承时,将任务原先的优先级保存在这个成员中,将来再从这里恢复任务的优先级。

 

3.系统全局变量

freertos将任务根据他们的状态分成几个链表。所有就绪状态的任务根据任务优先级加到对应的就绪链表中。系统为每个优先级定义了一个xList。如下:

static xList pxReadyTasksLists[ configMAX_PRIORITIES ];   

此外,所有延时的任务加入到两个延时链表之一。

static xList xDelayedTaskList1;                                       

static xList xDelayedTaskList2;    

还定义了两个指向延时链表的指针:

static xList * volatile pxDelayedTaskList;                         

static xList * volatile pxOverflowDelayedTaskList;                   

freertos弄出两个延时链表是因为它的延时任务管理的需要。freertos根据任务延时时间的长短按序将任务插入这两个链表之一。在插入前先把任务将要延时的xTicksToDelay数加上系统当前tick数,这样得到了一个任务延时due time(到期时间)的绝对数值。但是有可能这个相加操作会导致溢出,如果溢出则加入到pxOverflowDelayedTaskList指向的那个链表,否则加入pxDelayedTaskList指向的链表。

freertos还定义了个pending链表:

static xList xPendingReadyList;

这个链表用在调度器被lock(就是禁止调度了)的时期,如果一个任务从非就绪状态变为就绪状态,它不直接加到就绪链表中,而是加到这个pending链表中。等调度器重新启动(unlock)的时候再检查这个链表,把里面的任务加到就绪链表中

 

static volatile xList xTasksWaitingTermination          

static volatile unsigned portBASE_TYPE uxTasksDeleted = ( unsigned portBASE_TYPE ) 0;

一个任务被删除的时候加入到xTasksWaitingTermination链表中,uxTasksDeleted跟中系统中有多少任务被删除(即加到xTasksWaitingTermination链表的任务数目).

static xList xSuspendedTaskList;                              

这个链表记录着所有被xTaskSuspend挂起的任务,注意这不是那些等待信号量的任务。

 

static volatile unsigned portBASE_TYPE uxCurrentNumberOfTasks;记录了当前系统任务的数目

static volatile portTickType xTickCount;是自启动以来系统运行的ticks数

static unsigned portBASE_TYPE uxTopUsedPriority;记录当前系统中被使用的最高优先级,

static volatile unsigned portBASE_TYPE uxTopReadyPriority;记录当前系统中处于就绪状态的最高优先级。

static volatile signed portBASE_TYPE xSchedulerRunning  ;表示当前调度器是否在运行,也即内核是否启动了

 

4.任务管理

freertos与ucosii不同,它的任务控制块并不是静态分配的,而是在创建任务的时候动态分配。另外,freertos的优先级是优先级数越大优先级越高,和ucosii正好相反。任务控制块中也没有任务状态的成员变量,这是因为freertos中的任务总是根据他们的状态连入对应的链表,没有必要在任务控制块中维护一个状态。此外freertos对任务的数量没有限制,而且同一个优先级可以有多个任务。

 

先看任务创建:

 

signed portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode, const signed portCHAR * const pcName, unsigned portSHORT usStackDepth, void *pvParameters, unsigned portBASE_TYPE uxPriority, xTaskHandle *pxCreatedTask )

{

signed portBASE_TYPE xReturn;

tskTCB * pxNewTCB;

#if ( configUSE_TRACE_FACILITY == 1 )

       static unsigned portBASE_TYPE uxTaskNumber = 0;

#endif

 

      

       pxNewTCB = prvAllocateTCBAndStack( usStackDepth );

  

       if( pxNewTCB != NULL )

       {           

              portSTACK_TYPE *pxTopOfStack;

       

              prvInitialiseTCBVariables( pxNewTCB, pcName, uxPriority );

 

             

              #if portSTACK_GROWTH < 0

              {

                     pxTopOfStack = pxNewTCB->pxStack + ( usStackDepth - 1 );

              }

              #else

              {

                     pxTopOfStack = pxNewTCB->pxStack;     

              }

              #endif

 

             

              pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pvTaskCode, pvParameters );

 

             

              portENTER_CRITICAL();

              {

                     uxCurrentNumberOfTasks++;

                     if( uxCurrentNumberOfTasks == ( unsigned portBASE_TYPE ) 1 )

                     {

                           

                            pxCurrentTCB =  pxNewTCB;

 

                           

                            prvInitialiseTaskLists();

                     }

                     else

                     {    

                           

                            if( xSchedulerRunning == pdFALSE )

                            {

                                   if( pxCurrentTCB->uxPriority <= uxPriority )

                                   {

                                          pxCurrentTCB = pxNewTCB;      

                                   }

                            }

                     }                         

 

                    

                     if( pxNewTCB->uxPriority > uxTopUsedPriority )

                     {

                            uxTopUsedPriority = pxNewTCB->uxPriority;

                     }

 

                     #if ( configUSE_TRACE_FACILITY == 1 )

                     {

                           

                            pxNewTCB->uxTCBNumber = uxTaskNumber;

                            uxTaskNumber++;

                     }

                     #endif

                    

                     prvAddTaskToReadyQueue( pxNewTCB );

 

                     xReturn = pdPASS;

                     traceTASK_CREATE( pxNewTCB );

              }

              portEXIT_CRITICAL();

       }

 

       else

       {

              xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;

              traceTASK_CREATE_FAILED( pxNewTCB );

       }

 

       if( xReturn == pdPASS )

       {

              if( ( void * ) pxCreatedTask != NULL )

              {

                    

              if( xSchedulerRunning != pdFALSE )

              {

                    

                     if( pxCurrentTCB->uxPriority < uxPriority )

                     {

                            taskYIELD();

                     }

              }

       }

 

       return xReturn;

}

 

其中prvAllocateTCBAndStack分配tcb和stack内存,这个里面调用了pvportMalloc和pvPortFree函数来分配和释放内存,这两个函数对应于C标准库里面的malloc和free。但是标准库中的mallo和free存在以下缺点:并不是在所有的嵌入式系统中都可用,要占用不定的程序空间,可重人性欠缺以及执行时间具有不可确定性,而且多次反复调用可能导致严重的内存碎片。因此freertos在内存管理那块自己实现了这两个函数。

static tskTCB *prvAllocateTCBAndStack( unsigned portSHORT usStackDepth )

{

tskTCB *pxNewTCB;

 

      

       pxNewTCB = ( tskTCB * ) pvPortMalloc( sizeof( tskTCB ) );

 

       if( pxNewTCB != NULL )

       {

             

              pxNewTCB->pxStack = ( portSTACK_TYPE * ) pvPortMalloc( ( ( size_t )usStackDepth ) * sizeof( portSTACK_TYPE ) );

 

              if( pxNewTCB->pxStack == NULL )

              {

                    

                     vPortFree( pxNewTCB );                   

                     pxNewTCB = NULL;                  

              }           

              else

              {

                    

                     memset( pxNewTCB->pxStack, tskSTACK_FILL_BYTE, usStackDepth * sizeof( portSTACK_TYPE ) );

              }

       }

 

       return pxNewTCB;

}

 

再看任务删除

freertos的任务删除分两步完成,第一步在vTaskDelete中完成,FreeRTOS先把要删除的任务从就绪任务链表和事件等待链表中删除,然后把此任务添加到任务删除链表(即那个xTasksWaitingTermination),若删除的任务是当前运行任务,系统就执行任务调度函数.第2步则是在idle任务中完成,idle任务运行时,检查xTasksWaitingTermination链表,如果有任务在这个表上,释放该任务占用的内存空间,并把该任务从任务删除链表中删除。

 

void vTaskDelete( xTaskHandle pxTaskToDelete )

{

    tskTCB *pxTCB;

 

        taskENTER_CRITICAL();

        {

           

            if( pxTaskToDelete == pxCurrentTCB )

            {

                pxTaskToDelete = NULL;

            }

 

           

            pxTCB = prvGetTCBFromHandle( pxTaskToDelete );

 

            traceTASK_DELETE( pxTCB );

 

           

            vListRemove( &( pxTCB->xGenericListItem ) );

 

                          

            if( pxTCB->xEventListItem.pvContainer )

            {//如果是,则把它从事件等待链表中删除

                vListRemove( &( pxTCB->xEventListItem ) );

            }

          //插入等待删除链表

            vListInsertEnd( ( xList * ) &xTasksWaitingTermination, &( pxTCB->xGenericListItem ) );

           //增加uxTasksDeleted计数

            ++uxTasksDeleted;

        }

        taskEXIT_CRITICAL();

 

       

        if( xSchedulerRunning != pdFALSE )

        {

            if( ( void * ) pxTaskToDelete == NULL )

            {

                taskYIELD();

            }

        }

}

再看空闲任务做的第2步工作:

static portTASK_FUNCTION( prvIdleTask, pvParameters )

{

   

    ( void ) pvParameters;

 

    for( ;; )

    {

       

        prvCheckTasksWaitingTermination();

        …………………………….

这里prvCheckTasksWaitingTermination()就是干这第2步的工作:每次调用它删除一个任务

static void prvCheckTasksWaitingTermination( void )

                         

    #if ( INCLUDE_vTaskDelete == 1 )

    {              

        portBASE_TYPE xListIsEmpty;

 

       

        if( uxTasksDeleted > ( unsigned portBASE_TYPE ) 0 )

        {//禁止调度

            vTaskSuspendAll();

                xListIsEmpty = listLIST_IS_EMPTY( &xTasksWaitingTermination );              //打开调度

            xTaskResumeAll();

 

            if( !xListIsEmpty )

            {

                tskTCB *pxTCB;

                //关中断

                portENTER_CRITICAL();

                {          

                    pxTCB = ( tskTCB * ) listGET_OWNER_OF_HEAD_ENTRY( ( ( xList * ) &xTasksWaitingTermination ) );

                    vListRemove( &( pxTCB->xGenericListItem ) );

                    --uxCurrentNumberOfTasks;

                    --uxTasksDeleted;

                }

                portEXIT_CRITICAL();

               //释放内存,删除tcb

                prvDeleteTCB( pxTCB );

            }

        }

    }

    #endif

 

调度器的禁止和打开

这是一种同步机制,比关中断要温和点。禁止调度由vTaskSuspendAll实现,打开调度由xTaskResumeAll实现。

void vTaskSuspendAll( void )

{

    portENTER_CRITICAL();

        ++uxSchedulerSuspended;

    portEXIT_CRITICAL();

}

这个很简单,系统维护一个计数uxSchedulerSuspended,当它大于0时候表示禁止调度,等于0则打开调度(允许调度)。

signed portBASE_TYPE xTaskResumeAll( void )

{

register tskTCB *pxTCB;

signed portBASE_TYPE xAlreadyYielded = pdFALSE;

 

   

    portENTER_CRITICAL();

    {//将计数减一

        --uxSchedulerSuspended;

       //如果等于0,则允许调度

        if( uxSchedulerSuspended == ( unsigned portBASE_TYPE ) pdFALSE )

        {          

            if( uxCurrentNumberOfTasks > ( unsigned portBASE_TYPE ) 0 )

            {

                portBASE_TYPE xYieldRequired = pdFALSE;

               

               

                while( ( pxTCB = ( tskTCB * ) listGET_OWNER_OF_HEAD_ENTRY(  ( ( xList * ) &xPendingReadyList ) ) ) != NULL )

                {

                    vListRemove( &( pxTCB->xEventListItem ) );

                    vListRemove( &( pxTCB->xGenericListItem ) );

                    prvAddTaskToReadyQueue( pxTCB );

                   

                   

                    if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )

                    {

                        xYieldRequired = pdTRUE;

                    }

                }

 

               

                if( uxMissedTicks > ( unsigned portBASE_TYPE ) 0 )

                {

                    while( uxMissedTicks > ( unsigned portBASE_TYPE ) 0 )

                    {

                        vTaskIncrementTick();

                        --uxMissedTicks;

                    }

 

                   

                    #if configUSE_PREEMPTION == 1

                    {

                        xYieldRequired = pdTRUE;

                    }

                    #endif

                }

               

                if( ( xYieldRequired == pdTRUE ) || ( xMissedYield == pdTRUE ) )

                {

                    xAlreadyYielded = pdTRUE;

                    xMissedYield = pdFALSE;

                    taskYIELD();

                }

            }

        }

    }

    portEXIT_CRITICAL();

 

    return xAlreadyYielded;

}

 

任务挂起与唤醒

freertos的任务关起与ucosii也不大一样。它把所有挂起的任务加到xSuspendedTaskList中,而且一旦调用vTaskSuspend()函数挂起一个任务,该任务就将从所有它原先连入的链表中删除(包括就绪表,延时表和它等待的事件链表),也就是说,和 ucosii不同,一旦一个任务被挂起,它将取消先前它的延时和对事件的等待。ucosii中是不同的,在ucosii里面一个任务被挂起仅仅是把任务的状态或上一个OS_STAT_SUSPEND并从就绪表中删除,如果先前这个任务正在等待某事件,则并不取消等待。

//如果传进来的pxTaskToSuspend==NULL,则表示挂起当前任务

void vTaskSuspend( xTaskHandle pxTaskToSuspend )

{

    tskTCB *pxTCB;

 

        taskENTER_CRITICAL();

        {

           

            if( pxTaskToSuspend == pxCurrentTCB )

            {

                pxTaskToSuspend = NULL;

            }

 

           

            pxTCB = prvGetTCBFromHandle( pxTaskToSuspend );

 

            traceTASK_SUSPEND( pxTaskToSuspend );

 

           

            vListRemove( &( pxTCB->xGenericListItem ) );

 

                                                           

            if( pxTCB->xEventListItem.pvContainer )

            {

                vListRemove( &( pxTCB->xEventListItem ) );

            }

           //插到xSuspendedTaskList

            vListInsertEnd( ( xList * ) &xSuspendedTaskList, &( pxTCB->xGenericListItem ) );

        }

        taskEXIT_CRITICAL();

 

       

        if( ( void * ) pxTaskToSuspend == NULL )

        {

            taskYIELD();

        }

    }

 

相反的唤醒就是把任务从xSuspendedTaskList中删除,加到对应的就绪链表中(根据任务的优先级),然后如果唤醒的任务优先级高于当前任务优先级,则调度。

void vTaskResume( xTaskHandle pxTaskToResume )

    {

    tskTCB *pxTCB;

 

       

        pxTCB = ( tskTCB * ) pxTaskToResume;

 

       

        if( pxTCB != NULL )

        {

            taskENTER_CRITICAL();

            {

                if( prvIsTaskSuspended( pxTCB ) == pdTRUE )

                {

                    traceTASK_RESUME( pxTCB );

 

                   

                    vListRemove(  &( pxTCB->xGenericListItem ) );

                    prvAddTaskToReadyQueue( pxTCB );

 

                   

                    if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )

                    {

                       

                        taskYIELD();

                    }

                }

            }

            taskEXIT_CRITICAL();

        }

    }

 

任务调度

freertos支持多个任务具有相同的优先级,因此,当它被配置为可抢占内核时,调度算法既支持基于优先级的调度,也支持时间片轮流调度。任何时候调度器运行时它都选择处于就绪状态下的优先级最高的那个任务;如果有多个任务处于同一优先级,则freertos每个时钟节拍的中断服务程序中,将对这些任务应用换调度算法轮流执行这些任务。

系统用uxTopReadyPriority全局变量记录当前处于就绪态的任务的最高优先级。调度的时候就根据这个uxTopReadyPriority直接找到就绪链表中pxReadyTasksLists[ uxTopReadyPriority ]的任务,进行运行。

一个任务可以通过调用taskYIELD()让出cpu,从而调度令一个任务运行。它的实现如下:

#define taskYIELD()                 portYIELD()

而portYIELD()是一个体系结构相关的函数,对于不同的mcu需要实现这么一个函数完成调度。我拿atmel的atmega323 mcu为例子,说明下具体实现。

 

 

extern void vPortYield( void ) __attribute__ ( ( naked ) );

#define portYIELD()                                  vPortYield()

 

void vPortYield( void ) __attribute__ ( ( naked ) );

void vPortYield( void )

{

       portSAVE_CONTEXT();

       vTaskSwitchContext();

       portRESTORE_CONTEXT();

 

       asm volatile ( "ret" );

}

portYIELD()就是vportYield(),它保存现场,然后调用vTaskSwitchContext()这个函数选择下一个运行的任务,然后portRESTORE_CONTEXT()完成任务切换。

 

void vTaskSwitchContext( void )

{

       traceTASK_SWITCHED_OUT();

 

       if( uxSchedulerSuspended != ( unsigned portBASE_TYPE ) pdFALSE )

       {

             

              xMissedYield = pdTRUE;

              return;

       }

 

       taskCHECK_FOR_STACK_OVERFLOW();

 

      

       while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopReadyPriority ] ) ) )

       {

              --uxTopReadyPriority;

       }

 

      

       listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopReadyPriority ] ) );

 

       traceTASK_SWITCHED_IN();

       vWriteTraceToBuffer();

}

 

这里注意的是listGET_OWNER_OF_NEXT_ENTRY()宏并不是简单的从队列中取下第一个任务,而是walk through这个队列,比如上一次调度它从这个队列上取下的是第一个任务,那么这次调度选中的则是该队列中的第2个任务。这样就保证了同一优先级的多个任务之间公平的平分处理器时间。

选中任务后(用pxCurrentTCB指向它)。那么在portRESTORE_CONTEXT()中就完成最后的切换。因此这个地方有些有趣,函数vTaskSwitchContext()从名称看给人感觉是完成任务切换的,但是其实并不是这样,它只完成选择下一个运行的任务(也就是将要切换过去的任务),真正的切换时在portRESTORE_CONTEXT()中就完成的。

 

任务调度还可以发生在时钟节拍中断isr中,这个当然也是与cpu体系结构相关的。仍然以atmega323为例。它用的是定时器1的比较中断A作为时钟节拍产生器。其中断isr是:

void SIG_OUTPUT_COMPARE1A( void ) __attribute__ ( ( signal, naked ) );

       void SIG_OUTPUT_COMPARE1A( void )

       {

              vPortYieldFromTick();

              asm volatile ( "reti" );

       }

而vPortYieldFromTick()就是完成调度。代码如下:

void vPortYieldFromTick( void ) __attribute__ ( ( naked ) );

void vPortYieldFromTick( void )

{

    //保存现场

       portSAVE_CONTEXT();

   

       vTaskIncrementTick();

    //挑选下一个运行的任务,准备切换过去

       vTaskSwitchContext();

   //完成任务切换

       portRESTORE_CONTEXT();

 

       asm volatile ( "ret" );


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值