目录

1. 信号量概述

1.1 信号量概念

1.2 4种信号量

1.2.1 二值信号量

1.2.2 计数信号量

1.2.3 互斥信号量

1.2.4 递归互斥信号量

1.3 信号量相关控制结构

1.3.1 队列结构

1.3.2 任务结构

2. 二值信号量

2.1 创建二值信号量

2.2 获取二值信号量

2.2.1 任务级获取

2.2.2 中断级获取

2.3 释放二值信号量

2.3.1 任务级释放

2.3.2 中断级释放

3. 计数信号量

3.1 创建计数信号量

3.2 获取计数信号量

3.3 释放计数信号量

4. 互斥信号量

4.1 优先级翻转

4.1.1 优先级翻转现象

4.1.2 优先级继承原理

4.2 创建互斥信号量

4.3 获取互斥信号量

4.4 释放互斥信号量

5. 递归互斥信号量

5.1 创建递归互斥信号量

5.2 获取递归互斥信号量

5.3 释放递归互斥信号量


1. 信号量概述

1.1 信号量概念

① 信号量是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问

② 从本质上说,信号量是一个非负整数值,该数值对应有效的资源数量。所有成功获取信号量的任务都会将该整数减1,当整数值为0时,所有试图获取信号量的任务都会获取失败或进入阻塞状态

1.2 4种信号量

1.2.1 二值信号量

① 二值信号量通常用于互斥访问或同步,但是由于不具备优先级继承功能,所以更适合用于任务间或任务与中断间的同步

② 二值信号量在实现上是一个长度为1的队列,且队列项长度为0。当前队列中的消息数量,就标识了信号量的数量

说明:基于消息队列实现信号量,可以减小代码体积

1.2.2 计数信号量

① 计数信号量在实现上是一个长度可以不为1的队列,且队列项长度为0

② 在创建计数信号量时,可以同时指定信号量初始值

③ 计数信号量通常用于如下2个场景,

a. 事件计数

事件每发生一次就释放一次信号量,此时计数信号量的初始值应为0

b. 资源管理

信号量值标识当前可用资源个数,使用资源的任务需要先获取信号量,使用完成后释放信号量,此时计数信号量的初始值应为资源总数

1.2.3 互斥信号量

① 互斥信号量是拥有优先级继承机制的二值信号量

② 因为拥有优先级继承机制,互斥信号量更适合用于资源互斥访问

③ 互斥信号量不能用于中断服务函数中

1.2.4 递归互斥信号量

① 递归互斥信号量在互斥信号量的基础上,增加了递归持有特性,即持有互斥递归信号量的任务可以再次获取该信号量

② 递归释放的次数要与递归获取的次数相匹配

③ 互斥信号量也不能用于中断服务函数中

说明:互斥信号量与递归互斥信号量不能用于ISR中,是因为ISR中没有任务优先级的概念,无法正确处理优先级继承,详见后文

1.3 信号量相关控制结构

1.3.1 队列结构

① pcHead

在互斥信号量 & 递归互斥信号量中,被宏定义为uxQueueType,并被初始化为queueQUEUE_IS_MUTEX(NULL),用于标识当前队列类型

说明:为什么不通过ucQueueType判断队列类型呢 ?

ucQueueType成员被包含在configUSE_TRACE_FACILITY条件编译选项中,而该编译选项是不一定被开启的

② pcTail

在互斥信号量 & 递归互斥信号量中,被宏定义为pxMutexHolder,标识当前持有信号量的任务。初始值为NULL,即没有被任务持有

③ uxRecursiveCallCount

在递归互斥信号量中,标识当前信号量被递归获取的次数

④ uxMessagesWaiting

当前队列中的消息数量,在信号量中,就是当前可用的信号量数量

⑤ uxLength

队列长度,在信号量中,就是当前信号量的最大个数

⑥ uxItemSize

队列项长度,在信号量中,队列项长度必须为0

1.3.2 任务结构

在TCB_t结构中,如下2个字段与信号量有关,

① uxBasePriority

在互斥 & 递归互斥信号量中,记录任务的原始优先级,用于实现优先级继承机制

② uxMutexesHeld

在递归 & 互斥递归信号量中,记录该任务持有的递归 & 互斥递归信号量总数,只有当该计数减少到0时,才会恢复该任务的优先级

2. 二值信号量

2.1 创建二值信号量

#define semSEMAPHORE_QUEUE_ITEM_LENGTH   ( ( uint8_t ) 0U )#define xSemaphoreCreateBinary() \
xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, \
queueQUEUE_TYPE_BINARY_SEMAPHORE )

创建二值信号量,就是创建队列长度为1,队列项长度为0,类型为queueQUEUE_TYPE_BINARY_SEMAPHORE的队列

说明1:函数返回值

由于信号量是基于队列实现的,所以实际返回的是队列句柄。但是在semphr.h头文件中,将队列句柄类型重定义为信号量句柄,所以函数返回值类型为SemaphoreHandle_t

说明2:对应静态创建版本

与队列类似,信号量的创建也可以使用静态分配的内存,函数如下,

#define xSemaphoreCreateBinaryStatic( pxStaticSemaphore ) \
xQueueGenericCreateStatic( ( UBaseType_t ) 1, \
semSEMAPHORE_QUEUE_ITEM_LENGTH, NULL, pxStaticSemaphore, \
queueQUEUE_TYPE_BINARY_SEMAPHORE )

此时需要传递一个指向StaticSemaphore_t类型的变量的地址

说明3:旧版本创建二值信号量函数

#define vSemaphoreCreateBinary( xSemaphore ) \
{ \( xSemaphore ) = xQueueGenericCreate( ( UBaseType_t ) 1, \semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE ); \if( ( xSemaphore ) != NULL ) \{ \( void ) xSemaphoreGive( ( xSemaphore ) );\} \
} 

旧版本创建二值信号量函数与新版本有2处不同,

① 调用vSemaphoreCreateBinary时,需要将信号量句柄作为参数传递给函数

② 旧版本在创建完信号量之后会释放一次,也就是旧版本初始状态的二值信号量是可以获取的,而新版本初始状态的二值信号量是不可获取的

这点修改主要是作者为二值信号量预设的使用场景就是任务之间或任务和中断之间的同步

说明4:删除信号量

#define vSemaphoreDelete( xSemaphore ) \
vQueueDelete( ( QueueHandle_t ) ( xSemaphore ) )

对于各种信号量,删除信号量操作都是直接调用vQueueDelete函数删除队列。由于在FreeRTOS中,并不提倡不断动态创建 & 删除内核对象,所以删除队列的实现也是很简陋的(详见消息队列章节笔记)

2.2 获取二值信号量

2.2.1 任务级获取

#define xSemaphoreTake( xSemaphore, xBlockTime ) \
xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), \
NULL, ( xBlockTime ), pdFALSE )

任务级获取二值信号量,就是任务级获取消息

2.2.2 中断级获取

#define xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken ) \
xQueueReceiveFromISR( ( QueueHandle_t ) ( xSemaphore ), NULL, \
( pxHigherPriorityTaskWoken ) )

中断级获取二值信号量,就是中断级获取消息

说明:从实现中可知,在获取信号量时,仅关心消息(也就是信号量)的个数,而不是实际的消息内容

2.3 释放二值信号量

2.3.1 任务级释放

#define semGIVE_BLOCK_TIME   ( ( TickType_t ) 0U )#define xSemaphoreGive( xSemaphore ) \
xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, \
semGIVE_BLOCK_TIME, queueSEND_TO_BACK )

任务级释放二值信号量,就是任务级发送消息,其中发送等待时间为0

2.3.2 中断级释放

#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken ) \
xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), \
( pxHigherPriorityTaskWoken ) )

中断级释放二值信号量,就是中断级发送消息

注意:在中断中,只能使用二值信号量和计数信号量,不能使用互斥 & 递归互斥信号量

3. 计数信号量

3.1 创建计数信号量

// uxMaxCount:计数信号量最大值
// uxInitialCount:计数信号量初始值
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) \
xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_t uxMaxCount,
const UBaseType_t uxInitialCount )
{QueueHandle_t xHandle;configASSERT( uxMaxCount != 0 );configASSERT( uxInitialCount <= uxMaxCount );// 创建队列长度为uxMaxCount,队列项长度为0的队列xHandle = xQueueGenericCreate( uxMaxCount,queueSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_COUNTING_SEMAPHORE );// 如果队列创建成功,设置计数信号量初始值// 也就是当前队列中的可用消息数量if( xHandle != NULL ){( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount;}return xHandle;
}

说明:创建计数信号量也有相应的静态内存版本

#define xSemaphoreCreateCountingStatic( uxMaxCount, uxInitialCount, \
pxSemaphoreBuffer ) \
xQueueCreateCountingSemaphoreStatic( ( uxMaxCount ), ( uxInitialCount ), \
( pxSemaphoreBuffer ) )

3.2 获取计数信号量

与二值信号量相同

3.3 释放计数信号量

与二值信号量相同

4. 互斥信号量

4.1 优先级翻转

4.1.1 优先级翻转现象

① 导致优先级翻转的本质原因是不同优先级的任务需要获取相同的二值信号量

② 如果低优先级的L任务已经获取了二值信号量,那么高优先级的任务H获取二值信号量将失败,进而被阻塞

其实此时已经发生了优先级翻转,也就是低优先级任务L会在高优先级任务H之前运行

③ 如果有一个中等优先级且不需要获取该二值信号量的任务M,则会恶化优先级翻转的情况

由于任务M不需要持有该信号量,且可以抢占任务L,则会导致高优先级任务H在中低优先级任务M和L之后运行

4.1.2 优先级继承原理

① 当高优先级任务H想要获取互斥信号量,并且判断出信号量目前被更低优先级的任务L占有时,则暂时将任务L的优先级提升到与任务H优先级相同

此时,中等优先级的任务M将无法抢占原先的低优先级任务L,这样就可以确保任务L尽快完成并释放信号量

② 当任务L释放互斥信号量时,将任务L的优先级还原

说明1:优先级继承机制只能缓解优先级翻转现象

根据上述分析,即使拥有优先级继承机制,高优先级任务H也必须等待低优先级任务L运行完成并释放互斥信号量

说明2:由于RTOS中需要避免优先级翻转,因此在系统设计时就要考虑可能导致优先级翻转的场景,避免不同优先级的任务持有同一个互斥信号量

4.2 创建互斥信号量

#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType )
{Queue_t *pxNewQueue;const UBaseType_t uxMutexLength = ( UBaseType_t ) 1,uxMutexSize = ( UBaseType_t ) 0;// 创建队列长度为1,队列项长度为0的队列pxNewQueue = ( Queue_t * ) xQueueGenericCreate( uxMutexLength,uxMutexSize, ucQueueType );prvInitialiseMutex( pxNewQueue );return pxNewQueue;
}static void prvInitialiseMutex( Queue_t *pxNewQueue )
{if( pxNewQueue != NULL ){// 初始化互斥 & 递归互斥信号量的持有者与类型pxNewQueue->pxMutexHolder = NULL;pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX;// 递归互斥信号量在该成员上进行递归获取计数pxNewQueue->u.uxRecursiveCallCount = 0;// 释放一次互斥信号量// 互斥信号量的初始状态为可用( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U,queueSEND_TO_BACK );}
}

4.3 获取互斥信号量

获取互斥信号量仍然使用xSemaphoreTake函数,要点在于处理优先级继承(实际处理在xQueueGenericReceive函数中),这里分2种情况讨论,

① 可以获取互斥信号量

#if ( configUSE_MUTEXES == 1 )
{if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ){// 设置互斥 & 递归互斥信号量的持有者pxQueue->pxMutexHolder =( int8_t * ) pvTaskIncrementMutexHeldCount();}
}
#endif /* configUSE_MUTEXES */// 增加当前任务获取互斥 & 递归互斥信号量的个数
// 并返回当前任务句柄
void *pvTaskIncrementMutexHeldCount( void )
{if( pxCurrentTCB != NULL ){( pxCurrentTCB->uxMutexesHeld )++;}return pxCurrentTCB;
}

② 不可获取互斥信号量

#if ( configUSE_MUTEXES == 1 ) // 获取不到互斥信号量的分支
{if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ){// 进入临界段taskENTER_CRITICAL();{// 对互斥 & 递归互斥信号量持有者进行优先级继承处理vTaskPriorityInherit( ( void * ) pxQueue->pxMutexHolder );}taskEXIT_CRITICAL();}
}
#endifvoid vTaskPriorityInherit( TaskHandle_t const pxMutexHolder )
{TCB_t * const pxTCB = ( TCB_t * ) pxMutexHolder;if( pxMutexHolder != NULL ){// 只有当前进程优先级高于互斥 & 递归互斥信号量持有者时,// 才需要进行优先级继承操作if( pxTCB->uxPriority < pxCurrentTCB->uxPriority ){// 如果持有者任务的事件列表项排序值没有在被使用的话,// 则使用当前任务优先级修改该值if( ( listGET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ) ) &taskEVENT_LIST_ITEM_VALUE_IN_USE ) == 0UL ){listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ),( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) pxCurrentTCB->uxPriority );}if( listIS_CONTAINED_WITHIN(&( pxReadyTasksLists[ pxTCB->uxPriority ] ),&( pxTCB->xStateListItem ) ) != pdFALSE ){// 如果持有者任务在就绪列表// 则将该任务从当前就绪列表移除,并在继承优先级之后,// 加入新的就绪列表if( uxListRemove( &( pxTCB->xStateListItem ) ) ==( UBaseType_t ) 0 ){taskRESET_READY_PRIORITY( pxTCB->uxPriority );}pxTCB->uxPriority = pxCurrentTCB->uxPriority;prvAddTaskToReadyList( pxTCB );}else{// 如果持有者任务不在就绪列表,则仅继承优先级pxTCB->uxPriority = pxCurrentTCB->uxPriority;}}}
}

说明1:互斥 & 递归互斥信号量不能在中断ISR中使用

因为中断ISR中没有任务优先级的概念,所以无法正确进行优先级继承处理

说明2:对事件列表项排序值的使用

① 在FreeRTOS的事件标志组机制中会以代码中的方式使用该值,此时无法修改该值,详见相关章节笔记

② 事件列表项排序值的另一种更常见用法,是将任务加入内核对象的等待列表时进行排序,优先级越高,事件列表项排序值越小,则任务越在内核对象等待列表的队首位置

一个任务在获取互斥信号量之后,也可能被阻塞在其他内核对象中(e.g. 再次获取另一个信号量),因此其事件列表项可能被加入内核对象的等待队列

从原理上说,在进行优先级继承的过程中,应该以新的事件列表项排序值重新加入内核对象等待队列,但是代码中并未进行此操作

4.4 释放互斥信号量

释放互斥信号量仍然使用xSemaphoreGive函数,要点也在优先级继承处理,实际在prvCopyDataToQueue函数中实现

// 队列项长度为0,说明是处理信号量的分支
if( pxQueue->uxItemSize == ( UBaseType_t ) 0 )
{#if ( configUSE_MUTEXES == 1 ){if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ){// 处理优先级继承xReturn = xTaskPriorityDisinherit(( void * ) pxQueue->pxMutexHolder );// 将互斥 & 递归互斥信号量的持有者置为NULLpxQueue->pxMutexHolder = NULL;}}#endif /* configUSE_MUTEXES */
}BaseType_t xTaskPriorityDisinherit( TaskHandle_t const pxMutexHolder )
{TCB_t * const pxTCB = ( TCB_t * ) pxMutexHolder;BaseType_t xReturn = pdFALSE;if( pxMutexHolder != NULL ){// 互斥 & 递归互斥信号量只能由持有者释放configASSERT( pxTCB == pxCurrentTCB );configASSERT( pxTCB->uxMutexesHeld );// 减少当前任务持有的互斥 & 递归互斥信号量计数( pxTCB->uxMutexesHeld )--;// 只有确实发生过优先级继承,此处才需要恢复优先级if( pxTCB->uxPriority != pxTCB->uxBasePriority ){// 只有当持有者任务持有的互斥 & 递归互斥信号量个数为0时,// 才恢复任务优先级if( pxTCB->uxMutexesHeld == ( UBaseType_t ) 0 ){// 释放互斥 & 递归互斥信号量的一定是持有者,// 且持有者任务当前一定正在运行// 将该任务从当前就绪列表移出,恢复优先级后// 重新加入对应原始优先级的就绪列表if( uxListRemove( &( pxTCB->xStateListItem ) ) ==( UBaseType_t ) 0 ){taskRESET_READY_PRIORITY( pxTCB->uxPriority );}pxTCB->uxPriority = pxTCB->uxBasePriority;listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ),( TickType_t ) configMAX_PRIORITIES -( TickType_t ) pxTCB->uxPriority );prvAddTaskToReadyList( pxTCB );// 标识进行了恢复优先级的操作// 外部需要触发一次任务调度// 恢复优先级一定是降低了当前任务的优先级// 所以要唤醒正在等待的高优先级任务(假设他还在等待的话)xReturn = pdTRUE;}}}return xReturn;
}

5. 递归互斥信号量

5.1 创建递归互斥信号量

#define xSemaphoreCreateRecursiveMutex() \
xQueueCreateMutex( queueQUEUE_TYPE_RECURSIVE_MUTEX )

可见初始化递归互斥信号量和初始化互斥信号量的操作是一样的,只是注册时的信号量类型不同

5.2 获取递归互斥信号量

#define xSemaphoreTakeRecursive( xMutex, xBlockTime ) \
xQueueTakeMutexRecursive( ( xMutex ), ( xBlockTime ) )BaseType_t xQueueTakeMutexRecursive( QueueHandle_t xMutex,
TickType_t xTicksToWait )
{BaseType_t xReturn;Queue_t * const pxMutex = ( Queue_t * ) xMutex;configASSERT( pxMutex );if( pxMutex->pxMutexHolder == ( void * ) xTaskGetCurrentTaskHandle() ){// 如果该递归互斥信号量已经被当前任务持有过,// 则增加递归持有计数( pxMutex->u.uxRecursiveCallCount )++;xReturn = pdPASS;}else{// 如果该递归互斥信号量的持有者不是当前任务,// 可能没有被任务持有,或者已经被其他任务持有,// 则进行一次正常的获取操作,如果获取成功,则增加递归持有计数xReturn = xQueueGenericReceive( pxMutex, NULL, xTicksToWait,pdFALSE );if( xReturn != pdFAIL ){( pxMutex->u.uxRecursiveCallCount )++;}}return xReturn;
}

5.3 释放递归互斥信号量

#define xSemaphoreGiveRecursive( xMutex ) \
xQueueGiveMutexRecursive( ( xMutex ) )BaseType_t xQueueGiveMutexRecursive( QueueHandle_t xMutex )
{BaseType_t xReturn;Queue_t * const pxMutex = ( Queue_t * ) xMutex;configASSERT( pxMutex );// 只有递归互斥信号量的持有者才能释放if( pxMutex->pxMutexHolder == ( void * ) xTaskGetCurrentTaskHandle() ){// 减少递归持有计数( pxMutex->u.uxRecursiveCallCount )--;// 当递归持有计数为0时,实际释放信号量// 在xQueueGenericSend函数中会处理优先级继承if( pxMutex->u.uxRecursiveCallCount == ( UBaseType_t ) 0 ){( void ) xQueueGenericSend( pxMutex, NULL,queueMUTEX_GIVE_BLOCK_TIME, queueSEND_TO_BACK );}xReturn = pdPASS;}else{xReturn = pdFAIL;}return xReturn;
}

FreeRTOS源码分析与应用开发05:信号量相关推荐

  1. FreeRTOS源码分析与应用开发02:任务管理

    目录 1. 任务概述 1.1 任务表示 1.2 任务状态 1.2.1 运行态 1.2.2 就绪态 1.2.3 阻塞态 1.2.4 挂起态 1.3 任务优先级 1.3.1 FreeRTOS优先级配置 1 ...

  2. FreeRTOS源码分析与应用开发01:中断配置与临界段

    目录 1. 异常与中断的基本概念 1.1 异常分类 1.2 中断概述 1.2.1 中断处理宜短暂 1.2.2 临界段影响中断实时性 1.3 中断硬件基础 1.3.1 外设 1.3.2 中断控制器 1. ...

  3. FreeRTOS源码分析与应用开发07:事件标志组

    目录 1. 概述 2. 事件标志组类型 3. 创建事件标志组 4. 删除事件标志组 5. 设置事件标志位 5.1 任务级设置 5.2 中断级设置 6. 清除事件标志位 6.1 任务级清除 6.2 中断 ...

  4. FreeRTOS源码分析与应用开发04:消息队列

    目录 1. 队列结构 2. 创建队列 2.1 动态创建队列 2.1.1 xQueueCreate函数 2.1.2 xQueueGenericCreate函数 2.1.3 xQueueGenericRe ...

  5. FreeRTOS源码分析与应用开发09:低功耗Tickless模式

    目录 1. STM32F4低功耗模式简介 2. Tickless模式详解 2.1 如何降低功耗 2.2 关闭SysTick的问题与解决方案 2.2.1 关闭SysTick导致系统节拍计数器停止 2.2 ...

  6. FreeRTOS源码分析与应用开发11(完):编译、链接与部署

    目录 1. 存储设备布局 2. 链接器脚本 2.1 链接器脚本生成 2.2 链接器脚本分析 2.2.1 分散加载文件 2.2.2 加载区 & 运行区 2.2.3 ER_IROM1运行区分析 2 ...

  7. FreeRTOS源码分析与应用开发10:内存管理

    目录 1. 概述 1.1 RTOS中内存分配特点 1.2 内存堆(heap space)来源 1.2.1 ucHeap数组 1.2.2 链接器设置的堆 1.2.3 多个非连续内存堆 1.3 关于字节对 ...

  8. FreeRTOS源码分析与应用开发08:任务通知

    目录 1. 概述 1.1 任务通知概念 1.2 任务通知控制结构 2. 发送任务通知 2.1 任务级发送 2.2 中断级发送 2.2.1 xTaskNotifyFromISR函数 2.2.2 vTas ...

  9. FreeRTOS源码分析与应用开发06:软件定时器

    目录 1. 概述 1.1 软件定时器 & 硬件定时器 1.2 软件定时器精度 1.3 单次模式 & 周期模式 2. 软件定时器组件 2.1 定时器任务 2.2 定时器列表 2.3 定时 ...

最新文章

  1. 「打造中国人自己的开放 AI」:清华教授唐杰宣布成立AI新期刊
  2. RTP/RTSP/RTCP 协议详解
  3. MKL学习——向量操作
  4. Win11系统怎么取消登录密码 Win11取消登录密码图文教程
  5. dmesg的时间戳转换为对应的时间
  6. OpenGL ES着色器语言之变量和数据类型(二)(官方文档第四章)
  7. 华为堡垒机_浪潮无线分析,华为云堡垒机
  8. Fluent 三种初始化使用方法
  9. Android事件分发理解
  10. 存算分离后,VxRail动态计算节点构建虚拟化更给力
  11. 今日头条怎么引流?头条暴力引流方法
  12. 彻底搞清楚STM32CubeMX生成工程再次下载后SWD无法使用问题
  13. vue 阻止事件冒泡和捕获
  14. 数据结构:平衡二叉树(AVL树)、树的高度
  15. 融合蓝牙与IoT 拓展Wi-Fi商业价值
  16. TeamViewer远程工具使用安装方法图解
  17. 关于ubuntu18.04 的碎碎念
  18. 高清中国风墨迹墨点ps笔刷
  19. java c3p0 配置文件_C3P0默认配置文件
  20. 完成新增商品功能当中的一个功能(如何在用户选择商品分类的时候,根据商品分类id去在数据库里面匹配对应的品牌信息,并且返回到浏览器给用户进行选择品牌)

热门文章

  1. as 从java_从Java调用AS400 RPG
  2. linux ssh端口是否打开,如何查看linux中的ssh端口开启状态
  3. python类的应用_python中文件类的应用
  4. 苹果手机解压缩软件_装X教科书:买苹果电脑前应该了解哪些东西?
  5. Centos在线安装nginx
  6. VUE axios发送cookie
  7. jmeter下TPS插件的安装
  8. IBM MQ 创建以及常见问题
  9. Git生成ssh密钥
  10. php rabbitmq延迟队列示例