FreeRTOS 任务通知

Freertos

FreeRtos

简述

FreeRTOS 从版本 V8.2.0开始提供任务通知这个功能,每个任务都有一个32位的通知值。按照 FreeRTOS 官方的说法,使用消息通知比通过二进制信号量方式解除阻塞任务快 45%, 并且更加省内存(无需创建队列)。

FreeRTOS 提供以下几种方式发送通知给任务 :

  • 发送消息给任务, 如果有通知未读, 不覆盖通知值
  • 发送消息给任务,直接覆盖通知值
  • 发送消息给任务,设置通知值的一个或者多个位
  • 发送消息给任务,递增通知值

通过对以上方式的合理使用,可以在一定场合下替代原本的信号量,队列等。

当然,消息通知也有其限制 :

  • 通知只能一对一,因为通知必须指定任务
  • 等待通知的任务可以被阻塞, 但是发送消息的任务,任何情况下都不会被阻塞等待

分析的源码版本是 v9.0.0

通知 API

FreeRTOS 关于任务通知的 API 如下

API 功能
xTaskNotifyGive() 发送通知,没有通知值 (信号量类型)
ulTaskNotifyTake() 获取通知,(对应 Give)
xTaskNotify() 发送通知, 带通知值
xTaskNotifyAndQuery() 发送通知,带通知值,并且返回原通知值
xTaskNotifyWait() 等待通知
vTaskNotifyGiveFromISR() xTaskNotifyGive 的中断版本
xTaskNotifyAndQueryFromISR() xTaskNotifyAndQuery 的中断版本
xTaskNotifyFromISR() ulTaskNotifyTake 的中断版本
xTaskNotifyStateClear() 清除所有未读消息

可能你会想,消息通知就一个发送一个接收 API 不就好了,为什么要搞出这么多个 API ?
实际上, 以上的 API,有的是宏定义,而如此实现是方便特定情况下使用,比如用通知去实现轻量化的二进制信号量,计数信号量,队列等。

数据结构

方便下文叙述,先介绍下实现的相关变量定义。
为了实现任务通知,任务控制块 TCB_t 结构体中有两个任务通知的相关变量, 默认情况下, 任务通知这个功能是打开的,也就是宏 configUSE_TASK_NOTIFICATIONS 设置为 1
源码

#if( configUSE_TASK_NOTIFICATIONS == 1 )
    volatile uint32_t ulNotifiedValue;
    volatile uint8_t ucNotifyState;
#endif
  1. 变量 ulNotifiedValue 存储任务通知的数值, 初始化为 0。
  2. 变量 ucNotifyState 存储当前任务通知的状态,对应存在以下三种状态
// 1 没有未读通知,任务没有等待通知
#define taskNOT_WAITING_NOTIFICATION    ( ( uint8_t ) 0 )
// 2 任务等待通知
#define taskWAITING_NOTIFICATION        ( ( uint8_t ) 1 )
// 3 通知等待任务读取
#define taskNOTIFICATION_RECEIVED       ( ( uint8_t ) 2 )

该变量初始化为 taskNOT_WAITING_NOTIFICATION

文章开头提到发送任务通知的几种方式,对应系统源码中定义了如下 5 种命令类型 :

typedef enum
{
    // 1 发送通知,但是没有更新通知值
    eNoAction = 0,              
    // 2 发送通知,将新通知值与原通知值或操作(置位)
    eSetBits,                   
    // 3 发送通知,原通知值加 1
    eIncrement,                 
    // 4 发送通知,直接修改通知值(不过上次通知是否已经读取)
    eSetValueWithOverwrite,     
    // 5 发送通知,如果没有未读消息,设置通知值
    eSetValueWithoutOverwrite   
} eNotifyAction;

轻量级二进制信号量

我在 << FreeRTOS 信号量 >> 一文中举过一个例子,用二进制信号量进行同步。
这里,我们使用任务通知来实现同样的任务同步功能。

先看例子源码 :

// 等待通知的任务句柄
static TaskHandle_t xTaskToNotify = NULL;

void vATask( void * pvParameters )
{
    uint32_t ulNotificationValue;
    // 设置等待通知阻塞时间
    const TickType_t xMaxBlockTime = pdMS_TO_TICKS( 200 );
    // 获取任务句柄
    xTaskToNotify = xTaskGetCurrentTaskHandle();
    // ...
    for (;;) {
        // ...
        // 等待通知
        ulNotificationValue = ulTaskNotifyTake( pdTRUE,
                                            xMaxBlockTime );
        if( ulNotificationValue > 0 ) {
            // 收到通知
        } else {
           // 超时
        }   
    }
}

// 中断 发送通知
void vTimerISR( void * pvParameters )
{
    static signed BaseType_t xHigherPriorityTaskWoken;
    xHigherPriorityTaskWoken = pdFALSE;
    // 发送通知
    vTaskNotifyGiveFromISR( xTaskToNotify, 
        &xHigherPriorityTaskWoken );
    // 传递参数判断是否有高优先级任务就绪
    // 判断是否需要触发任务切换
    portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

上面例子, 任务调用 API ulTaskNotifyTake 等待中断发送通知,中断中调用 API vTaskNotifyGiveFromISR给任务发送通知, 如果是在任务中发送消息,则调用 API vTaskNotifyGive
例子中的任务通知实现了二进制型号量的任务同步功能。

下面分析下任务通知这个功能如何实现信号量获取和释放。

获取信号量

任务中,调用了函数 ulTaskNotifyTake等待通知,相当于尝试获取信号量。

为了实现二进制信号量,函数的第一个参数设置为pdTRUE, 在接收到通知后,读取并清除通知值(设置为0)。(此处可以对比后续的计数信号量)
第二参数是阻塞等待时间。

直接查看该函数的实现 :

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit,
                            TickType_t xTicksToWait )
{
    uint32_t ulReturn;
    taskENTER_CRITICAL();
    {
        // 如果通知值为 0 ,阻塞任务
        // 默认初始化通知值为 0, 说明没有未读通知
        if( pxCurrentTCB->ulNotifiedValue == 0UL )
        {
            // 标记任务状态 : 等待消息通知
            pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;
            // 挂起任务等待消息或者超时
            if( xTicksToWait > ( TickType_t ) 0 )
            {
                prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
                portYIELD_WITHIN_API();
            }
        }
    }
    taskEXIT_CRITICAL();
    // 到此处,有通知 或者 等待超时
    taskENTER_CRITICAL();
    {
        // 判断通知值
        ulReturn = pxCurrentTCB->ulNotifiedValue;
        if( ulReturn != 0UL )
        {
            // 接收到通知
            // 第一个参数为真,读取后清零通知值
            if( xClearCountOnExit != pdFALSE )
            {
                pxCurrentTCB->ulNotifiedValue = 0UL;
            }
            else
            {
                // 否则 通知值减 1
                pxCurrentTCB->ulNotifiedValue = ulReturn - 1;
            }
        }
        // 恢复任务通知状态变量
        pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
    }
    taskEXIT_CRITICAL();
    return ulReturn;
}

对于这个函数,任务通知值为0, 对应信号量无效,如果任务设置了阻塞等待,任务被阻塞挂起。当其他任务或中断发送通知修改了通知值使其不为0后,信号量变为有效,等待通知的任务会读取通知,根据传递的第一个参数清零通知值或者执行递减操作。
对于二进制信号量,信号量读取一次后就失效,所以直接清零。

释放信号量

例子中是在中断中发送通知,所以必须调用带有FromISR后缀的API。发送通知调用的函数是 vTaskNotifyGiveFromISR, 对应函数名,也可以看出是一个释放信号量的操作。

对该函数进行简化说明,看看其实如何给出信号量的

void vTaskNotifyGiveFromISR( TaskHandle_t xTaskToNotify, 
    BaseType_t *pxHigherPriorityTaskWoken )
{
    TCB_t * pxTCB;
    uint8_t ucOriginalNotifyState;
    UBaseType_t uxSavedInterruptStatus;

    pxTCB = ( TCB_t * ) xTaskToNotify;
    // 中断优先级临时设置
    uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
    {
        // 保存任务通知的原状态
        ucOriginalNotifyState = pxTCB->ucNotifyState;
        // 更新通知状态 : 收到通知
        pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;

        // Give 类似返回信号量, 直接递增通知值
        ( pxTCB->ulNotifiedValue )++;

        // 判断被通知任务是否正在阻塞等待通知
        if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )
        {
            // 如果任务调度器运行中
            if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
            {
                // 把等待任务移到就绪链表
                ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
                prvAddTaskToReadyList( pxTCB );
            }
            else
            {
                // 调度器挂起,中断依然正常发生,但是不能直接操作就绪链表
                // 加入到就绪挂起链表,任务调度恢复后会移动到就绪链表
                vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );
            }

            // 被通知任务优先级高
            if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
            {
                // 设置返回参数,标识需要任务切换
                if( pxHigherPriorityTaskWoken != NULL )
                {
                    *pxHigherPriorityTaskWoken = pdTRUE;
                }
                else
                {
                    // 如果用户没有设置参数,则使用系统全局变量
                    // 目的都是说明,需要任务切换
                    xYieldPending = pdTRUE;
                }
            }
        }
    }
    // 恢复中断的优先级
    portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
}

每次调用该函数都会递增任务通知值。
在开头的例子中, 任务通过接收函数返回值是否大于零,判断是否获取到了通知,任务通知值初始化为0, 对应表达信号量无效。
当任务或者中断调用发送函数给出信号量时,递增通知值,使其大于零,使其表示的信号量变为有效,恢复阻塞等待的任务。

如果是在任务中调用,则调用的接口变为 xTaskNotifyGive, 查看该接口,发现实际是一个宏

#define xTaskNotifyGive( xTaskToNotify ) 
    xTaskGenericNotify( ( xTaskToNotify ), ( 0 ), eIncrement, NULL )

该宏对一个比较通用的函数进行了封装,xTaskGenericNotify 这个函数实际上实现了所有任务通知的方式,通过在调用的时候指定命令类型。

该函数调用说明 :

BaseType_t xTaskGenericNotify( 
        /*通知任务句柄*/
        TaskHandle_t xTaskToNotify,
        /*新通知值*/
        uint32_t ulValue, 
        /*发送通知命令类型*/
        eNotifyAction eAction,
        /*任务原本通知值返回*/
        uint32_t *pulPreviousNotificationValue );

该函数实现如下,

BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify,
        uint32_t ulValue, eNotifyAction eAction,
        uint32_t *pulPreviousNotificationValue )
{
    TCB_t * pxTCB;
    BaseType_t xReturn = pdPASS;
    uint8_t ucOriginalNotifyState;
    pxTCB = ( TCB_t * ) xTaskToNotify;
    
    taskENTER_CRITICAL();
    {
        // 返回任务原来的通知值到传递参数
        if( pulPreviousNotificationValue != NULL )
        {
            *pulPreviousNotificationValue = pxTCB->ulNotifiedValue;
        }

        // 保存原来的任务通知状态
        ucOriginalNotifyState = pxTCB->ucNotifyState;
        // 设置新的任务通知状态 : 收到消息
        pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;

        switch( eAction )
        {
        case eSetBits   :
            // 置位 : 原通知值与新通知值进行或
            pxTCB->ulNotifiedValue |= ulValue;
            break;
        case eIncrement :
            // 增加 : 原通知值加 1
            ( pxTCB->ulNotifiedValue )++;
            break;
        case eSetValueWithOverwrite :
            // 覆盖 : 新通知值直接覆盖
            pxTCB->ulNotifiedValue = ulValue;
            break;
        case eSetValueWithoutOverwrite :
            //不覆盖 : 不覆盖未读消息
            if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED )
            {
                // 没有未读消息,设置通知值
                pxTCB->ulNotifiedValue = ulValue;
            }
            else
            {
                // 有未读消息, 不覆盖通知值
                xReturn = pdFAIL;
            }
            break;
        case eNoAction:
            // 不修改通知值
            break;
        }

        // 如果被通知任务由于等待任务通知而挂起
        if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )
        {
            // 唤醒任务, 插入就绪链表
            ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
            prvAddTaskToReadyList( pxTCB );

#if( configUSE_TICKLESS_IDLE != 0 )
            {
                //更新下一个阻塞任务超时时间
                prvResetNextTaskUnblockTime();
            }
#endif
            // 被通知任务优先级比当前任务高
            if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
            {
                // 触发 PendSV , 切换高优先级任务运行
                taskYIELD_IF_USING_PREEMPTION();
            }
        }
    }
    taskEXIT_CRITICAL();
    return xReturn;
}

该函数基本流程如下

  1. 保存原有通知值,写入传入的指针
  2. 根据命令类型设置通知值
  3. 判断被通知任务是否等待通知,根据优先级判断是否需要唤醒
  4. 返回操作结果

如果调用函数的时候传递了读取旧值的指针, 函数会把原有通知值写到该指针所指内存,之后按照命令类型更新通知值。

回到这一部分二进制信号量,看到该宏调用上面这个函数时,传递的命令类型是 eIncrement, 也就是在原有通知值基础上递增1, 这样看来,就和 xTaskNotifyGiveFromISR的效果一样了。

轻量级计数信号量

上面提到二进制信号量,在被通知任务,也就是获取信号量的任务获取了信号量后,会把通知值直接设置为0,这对应了二进制信号量的特点 : 不管任务或者中断调用了几次通知发送函数递增通知值,只要被通知任务读取了一次通知,就会直接把该值清零

而计数信号量不同在于读取一次通知后不会直接把通知值清零,而是递减1,因此,任务被通知几次,对应被通知任务就可以执行读取几次,直到通知值递减为0。

到此,我们基本直到,为了实现计数信号量,只需要简单地修改下二进制信号量的获取函数的第一个参数,就可以了。

ulNotificationValue = ulTaskNotifyTake( pdFALSE,/*调用递减不直接清0*/
                                            xMaxBlockTime );

第一个参数设置为 pdFALSE, 函数ulTaskNotifyTake不会直接把通知值清零,而是每调用一次递减1,直到0为止。
可以查看上文该函数的实现。

轻量级事件标记组

二进制信号量或者计数信号量只能通知任务一个事件,如果有两种不同的事件,他们就无法实现了。这时候就需要利用事件分组了。

举个应用例子,
一个处理串口事件的任务,串口事件包括接收和发送,对应在其中断中发送通知,我们利用任务通知实现事件分组如下实现 :

/定义事件位标记
#define TX_BIT    0x01
#define RX_BIT    0x02
//任务句柄
static TaskHandle_t xHandlingTask;

// 发送事件中断
void vTxISR( void )
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // ...
    // 发送通知
    xTaskNotifyFromISR( xHandlingTask,
            TX_BIT,
            eSetBits,
            &xHigherPriorityTaskWoken );

    portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

//接收事件中断
void vRxISR( void )
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // ...
    // 发送通知
    xTaskNotifyFromISR( xHandlingTask,
            RX_BIT,
            eSetBits,
            &xHigherPriorityTaskWoken );

    portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

// 处理任务 被通知任务
static void prvHandlingTask( void *pvParameter )
{
    const TickType_t xMaxBlockTime = pdMS_TO_TICKS( 500 );
    BaseType_t xResult;

    for( ;; )
    {
        // 等待通知
        xResult = xTaskNotifyWait( pdFALSE,/*接收前不清除任何位*/
                ULONG_MAX,/* 接收后清除所有位*/
                &ulNotifiedValue, /* 保存通知值*/
                xMaxBlockTime );

        if( xResult == pdPASS )
        {
            if( ( ulNotifiedValue & TX_BIT ) != 0 )
            {
                prvProcessTx();
            }

            if( ( ulNotifiedValue & RX_BIT ) != 0 )
            {
                prvProcessRx();
            }
        }
        else
        {
            prvCheckForErrors();
        }
    }
}

前面介绍到一个通知发送函数 xTaskGenericNotify, 可以设置命令类型,上面例子中,使用的命令类型是 eSetBits。在开头可以看到对应每个事件, 用一个bit 去对应指代他,当该事件发生时,发送通知,并且置位通知值的对应位,这样,被通知任务就可以根据通知值的位区分出什么事件通知。

前面实现信号量提到的接收通知的函数是 ulTaskNotifyTake, 该函数判断是否有未读通知是根据通知值是否为零,相对来说,该函数实现主要是针对信号量那种类型。

例子中任务调用的等待函数,xTaskNotifyWait,该函数判断是否有通知是依据另外一个变量 ucNotifyState, 算起来,这里,通知值才算真正承载了有用的通知内容。

该函数的参数说明 :

BaseType_t xTaskNotifyWait( 
    /*接收通知前清除通知值指定位 对应 1 的bit清除*/
    uint32_t ulBitsToClearOnEntry, 
    /*接收通知后清除通知值指定位*/
    uint32_t ulBitsToClearOnExit, 
    /*接收到的通知值*/
    uint32_t *pulNotificationValue, 
    /*等待时间*/
    TickType_t xTicksToWait );

具体看看该函数实现 :

BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait )
{
    BaseType_t xReturn;

    taskENTER_CRITICAL();
    {
        // 判断有没有未读通知
        if( pxCurrentTCB->ucNotifyState != taskNOTIFICATION_RECEIVED )
        {
            // 接收通知前,清除通知值中的指定位
            pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnEntry;

            // 设置任务通知状态 : 等待消息通知
            pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;

            // 挂起任务等待通知或者超时
            if( xTicksToWait > ( TickType_t ) 0 )
            {
                prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
                portYIELD_WITHIN_API();
            }
        }
    }
    taskEXIT_CRITICAL();

    taskENTER_CRITICAL();
    {
        if( pulNotificationValue != NULL )
        {
            // 返回通知值 (超时情况下,该值并没有被更新)
            *pulNotificationValue = pxCurrentTCB->ulNotifiedValue;
        }

        // 通过任务通知状态,判断是否收到通知
        if( pxCurrentTCB->ucNotifyState == taskWAITING_NOTIFICATION )
        {
            // 超时,没有收到通知
            xReturn = pdFALSE;
        }
        else
        {
            // 接收通知后,清除通知值的指定位
            pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnExit;
            // 返回接收到通知
            xReturn = pdTRUE;
        }
        // 设置任务通知状态
        pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
    }
    taskEXIT_CRITICAL();
    return xReturn;
}

调用该函数, 在接收到新的通知值前, 会根据第一个参数清除通知值上的特定位(第一个参数为1的位,对应通知值清0)。
接收到通知后,读取通知值保存到参数 *pulNotificationValue后,会根据第二个参数清除通知值上对应位的值。
在事件分组这个例子中,任务接收到事件通知后,通过通知值上置的位判断什么事件发生了,然后清除通知值,等待下一次事件发生,置位通知。

轻量级消息邮箱

把通知值作为内容,任务通知相当于是一个深度为1的队列。给任务发送通知就相当于投递了。
另外,如果邮件内容大于32bit,也可以把指针作为内容发送出去,接收任务读取通知值后,转换为指针读取实际内容。
思路如此,不做赘述。


参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,174评论 11 349
  • @(嵌入式) 简述 FreeRTOS 信号量和互斥锁是基于队列实现的, 队列介绍见 << FreeRTOS 消息...
    orientlu阅读 7,852评论 0 3
  • 无数次的对自己说,让她三分又如何?但,当善良再也无能为力支撑自私、邪恶的灵魂时,你会彻底崩溃,放下的心痛又一次回到...
    睿姿阅读 556评论 1 1
  • 岁末,这几年让我感触颇多。那些不堪回首的往事,如今早已释怀。我觉得生命的意义是成长、是坚强。 莫泊桑在《一...
    倩倩Clover阅读 148评论 1 0