当前位置: 首页 > news >正文

【freertos-kernel】queue(创建)

文章目录

  • 补充
    • 任务进出临界区
      • ISR临界区(中断服务函数中使用)
      • 普通临界区(任务中使用)
    • xTaskRemoveFromEventList
      • prvResetNextTaskUnblockTime
      • prvYieldForTask
  • 队列结构体
    • Queue_t/QueueDefinition
      • QueuePointers_t
      • SemaphoreData
  • 队列相关创建
    • xQueueGenericCreate
      • prvInitialiseNewQueue
        • xQueueGenericReset
          • queueYIELD_IF_USING_PREEMPTION
    • xQueueCreateSet
    • xQueueCreateMutex

边学边记,朋友们多多指教。

补充

补充一些东西。

任务进出临界区

临界区(Critical Section) 是一段不允许被中断的代码区域。

//task.h
#define taskENTER_CRITICAL()                 portENTER_CRITICAL()
#if ( configNUMBER_OF_CORES == 1 )#define taskENTER_CRITICAL_FROM_ISR()    portSET_INTERRUPT_MASK_FROM_ISR()
#else#define taskENTER_CRITICAL_FROM_ISR()    portENTER_CRITICAL_FROM_ISR()
#endif#define taskEXIT_CRITICAL()                    portEXIT_CRITICAL()
#if ( configNUMBER_OF_CORES == 1 )#define taskEXIT_CRITICAL_FROM_ISR( x )    portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
#else#define taskEXIT_CRITICAL_FROM_ISR( x )    portEXIT_CRITICAL_FROM_ISR( x )
#endif

以gcc/arm-cm3(单核)为例,多核的临界区进出更复杂,除了要关闭中断,还要防止不同 CPU 同时修改共享数据。

ISR临界区(中断服务函数中使用)

ARM Cortex-M 系列提供了一个叫 BASEPRI 的寄存器,用于控制中断屏蔽级别,只有比他优先级更低(优先级号更大)的中断才会响应。

//portmacro.h
#define portSET_INTERRUPT_MASK_FROM_ISR()         ulPortRaiseBASEPRI()
//port.c
//BASEPRI设置为configMAX_SYSCALL_INTERRUPT_PRIORITY屏蔽中断响应,返回原BASEPRI值,方便后面恢复
portFORCE_INLINE static uint32_t ulPortRaiseBASEPRI( void )
{uint32_t ulOriginalBASEPRI, ulNewBASEPRI;__asm volatile("   mrs %0, basepri \n" \ //将当前 BASEPRI 寄存器的值读入变量 ulOriginalBASEPRI"   mov %1, %2 \n" \ //把configMAX_SYSCALL_INTERRUPT_PRIORITY(对应 %2)赋值给寄存器 %1(即 ulNewBASEPRI)"   msr basepri, %1 \n" \ //将新的优先级值写入 BASEPRI 寄存器,开始屏蔽中断"   isb \n" \"   dsb \n" \: "=r" ( ulOriginalBASEPRI ), "=r" ( ulNewBASEPRI ) : "i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY ) : "memory");return ulOriginalBASEPRI;
}//portmacro.h
#define portCLEAR_INTERRUPT_MASK_FROM_ISR( x )    vPortSetBASEPRI( x )
//port.c
//设置中断屏蔽等级ulNewMaskValue
portFORCE_INLINE static void vPortSetBASEPRI( uint32_t ulNewMaskValue )
{__asm volatile("   msr basepri, %0 " ::"r" ( ulNewMaskValue ) : "memory");
}

普通临界区(任务中使用)

#define portDISABLE_INTERRUPTS()                  vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS()                   vPortSetBASEPRI( 0 )

使能中断就是把basepri设为0(优先级最高),
失能中断就是把basepri设为configMAX_SYSCALL_INTERRUPT_PRIORITY (优先级最低)。

//portmacro.h
#define portENTER_CRITICAL()                      vPortEnterCritical()
#define portEXIT_CRITICAL()                       vPortExitCritical()//port.c
void vPortEnterCritical( void )
{portDISABLE_INTERRUPTS();uxCriticalNesting++;if( uxCriticalNesting == 1 ){//表示当前不是在中断服务程序中执行configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );}
}
void vPortExitCritical( void )
{configASSERT( uxCriticalNesting );uxCriticalNesting--;if( uxCriticalNesting == 0 ){portENABLE_INTERRUPTS();}
}

uxCriticalNesting是一个全局变量,记录当前任务进入临界区的嵌套次数。
ISR临界区没有中断嵌套计数。

xTaskRemoveFromEventList

从一个事件列表中移除一个任务,并根据情况将其添加到就绪列表,从而实现任务的唤醒机制。

BaseType_t xTaskRemoveFromEventList( const List_t * const pxEventList )
{TCB_t * pxUnblockedTCB;BaseType_t xReturn;traceENTER_xTaskRemoveFromEventList( pxEventList );pxUnblockedTCB = listGET_OWNER_OF_HEAD_ENTRY( pxEventList );configASSERT( pxUnblockedTCB );listREMOVE_ITEM( &( pxUnblockedTCB->xEventListItem ) );移除pxEventList的第一个任务(优先级最高)if( uxSchedulerSuspended == ( UBaseType_t ) 0U )	//如果调度器没有被挂起{listREMOVE_ITEM( &( pxUnblockedTCB->xStateListItem ) );//把任务从原来的状态链表中移除prvAddTaskToReadyList( pxUnblockedTCB );//把它加入就绪链表#if ( configUSE_TICKLESS_IDLE != 0 ){prvResetNextTaskUnblockTime();//更新下一个要唤醒的时间点}#endif}else	// 如果调度器被挂起,暂存到 xPendingReadyList{listINSERT_END( &( xPendingReadyList ), &( pxUnblockedTCB->xEventListItem ) );}#if ( configNUMBER_OF_CORES == 1 ){if( pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority ){//如果被唤醒的任务优先级高于当前正在运行的任务xReturn = pdTRUE;xYieldPendings[ 0 ] = pdTRUE;//设置 xYieldPendings[0] 标志,表示需要调度}else{xReturn = pdFALSE;}}#else /* #if ( configNUMBER_OF_CORES == 1 ) */{xReturn = pdFALSE;#if ( configUSE_PREEMPTION == 1 ){prvYieldForTask( pxUnblockedTCB );//判断是否需要触发调度if( xYieldPendings[ portGET_CORE_ID() ] != pdFALSE ){xReturn = pdTRUE;}}#endif /* #if ( configUSE_PREEMPTION == 1 ) */}#endif /* #if ( configNUMBER_OF_CORES == 1 ) */traceRETURN_xTaskRemoveFromEventList( xReturn );return xReturn;
}

prvResetNextTaskUnblockTime

更新系统变量 xNextTaskUnblockTime,记录下一个要被唤醒的任务的时间点 ,以便在进入Tickless Idle时知道最多可以休眠多久。

static void prvResetNextTaskUnblockTime( void )
{if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )//没有任何任务在等待延时唤醒{xNextTaskUnblockTime = portMAX_DELAY;}else	//如果链表不为空,则获取第一个任务的唤醒时间{xNextTaskUnblockTime = listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxDelayedTaskList );}
}

prvYieldForTask

根据一个被唤醒的任务的优先级,判断是否需要在某个 CPU 核心上触发一次上下文切换(yield),以确保高优先级任务能够尽快运行。

static void prvYieldForTask( const TCB_t * pxTCB )
{BaseType_t xLowestPriorityToPreempt;BaseType_t xCurrentCoreTaskPriority;BaseType_t xLowestPriorityCore = ( BaseType_t ) -1;BaseType_t xCoreID;#if ( configRUN_MULTIPLE_PRIORITIES == 0 )BaseType_t xYieldCount = 0;#endif /* #if ( configRUN_MULTIPLE_PRIORITIES == 0 ) */configASSERT( portGET_CRITICAL_NESTING_COUNT() > 0U );//必须在临界区中调用这个函数#if ( configRUN_MULTIPLE_PRIORITIES == 0 )//如果只运行单一优先级的任务if( pxTCB->uxPriority >= uxTopReadyPriority )//只有当新唤醒任务优先级 ≥ 当前最高就绪优先级时才处理#else//正常多优先级系统if( taskTASK_IS_RUNNING( pxTCB ) == pdFALSE )//如果该任务不在运行态,才考虑抢占#endif{xLowestPriorityToPreempt = ( BaseType_t ) pxTCB->uxPriority;--xLowestPriorityToPreempt;//遍历所有核心for( xCoreID = ( BaseType_t ) 0; xCoreID < ( BaseType_t ) configNUMBER_OF_CORES; xCoreID++ ){//获取当前核心任务优先级xCurrentCoreTaskPriority = ( BaseType_t ) pxCurrentTCBs[ xCoreID ]->uxPriority;if( ( pxCurrentTCBs[ xCoreID ]->uxTaskAttributes & taskATTRIBUTE_IS_IDLE ) != 0U ){xCurrentCoreTaskPriority = ( BaseType_t ) ( xCurrentCoreTaskPriority - 1 );}//检查是否可以抢占,当前核心上有任务在运行,并且还没有 pending yield//如果当前核心运行任务优先级 ≤ 被唤醒任务;//并且符合亲和性设置;//并且没有禁止抢占;//则更新为最合适抢占的核心;if( ( taskTASK_IS_RUNNING( pxCurrentTCBs[ xCoreID ] ) != pdFALSE ) && ( xYieldPendings[ xCoreID ] == pdFALSE ) ){#if ( configRUN_MULTIPLE_PRIORITIES == 0 )if( taskTASK_IS_RUNNING( pxTCB ) == pdFALSE )#endif{if( xCurrentCoreTaskPriority <= xLowestPriorityToPreempt ){#if ( configUSE_CORE_AFFINITY == 1 )if( ( pxTCB->uxCoreAffinityMask & ( ( UBaseType_t ) 1U << ( UBaseType_t ) xCoreID ) ) != 0U )#endif{#if ( configUSE_TASK_PREEMPTION_DISABLE == 1 )if( pxCurrentTCBs[ xCoreID ]->xPreemptionDisable == pdFALSE )#endif{xLowestPriorityToPreempt = xCurrentCoreTaskPriority;xLowestPriorityCore = xCoreID;}}}...}//如果是单一优先级系统,但当前任务优先级较低,也尝试 yield#if ( configRUN_MULTIPLE_PRIORITIES == 0 ){if( ( xCurrentCoreTaskPriority > ( ( BaseType_t ) tskIDLE_PRIORITY - 1 ) ) &&( xCurrentCoreTaskPriority < ( BaseType_t ) pxTCB->uxPriority ) ){prvYieldCore( xCoreID );xYieldCount++;}...}#endif /* #if ( configRUN_MULTIPLE_PRIORITIES == 0 ) */}...}#if ( configRUN_MULTIPLE_PRIORITIES == 0 )if( ( xYieldCount == 0 ) && ( xLowestPriorityCore >= 0 ) )#else /* #if ( configRUN_MULTIPLE_PRIORITIES == 0 ) */if( xLowestPriorityCore >= 0 )#endif /* #if ( configRUN_MULTIPLE_PRIORITIES == 0 ) */{	//找到了最合适 yield 的核心prvYieldCore( xLowestPriorityCore );//会触发PendSV中断来切换任务}...}
}

队列结构体

Queue_t/QueueDefinition

QueueDefinition 结构体其他名称有Queue_txQUEUE
QueueHandle_tQueueSetHandle_tQueueSetMemberHandle_t都是结构体QueueDefinition的指针类型。

typedef struct QueueDefinition
{int8_t * pcHead;	// 队列缓冲区起始地址int8_t * pcWriteTo; // 当前写入位置union						//这是一个联合体,根据队列用途不同,用的字段不同{QueuePointers_t xQueue;//如果是普通队列,使用该字段管理读写指针SemaphoreData_t xSemaphore;//如果是信号量,则使用此字段保存信号量的数据,信号量本质上也可以看作是一种特殊的队列。} u;List_t xTasksWaitingToSend;		// 等待发送的任务列表List_t xTasksWaitingToReceive;	// 等待接收的任务列表volatile UBaseType_t uxMessagesWaiting; // 当前队列中已有的消息数量UBaseType_t uxLength;	// 队列最大容量(元素个数)UBaseType_t uxItemSize;	// 每个元素大小(字节)volatile int8_t cRxLock;// 接收操作锁定计数器(用于中断保护)volatile int8_t cTxLock; // 发送操作锁定计数器(用于中断保护)#if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )uint8_t ucStaticallyAllocated;// 标记是否静态分配内存#endif#if ( configUSE_QUEUE_SETS == 1 )struct QueueDefinition * pxQueueSetContainer;// 所属队列集合#endif#if ( configUSE_TRACE_FACILITY == 1 )UBaseType_t uxQueueNumber; 	// 调试用:队列编号uint8_t ucQueueType;		// 调试用:队列类型#endif
} xQUEUE;

QueuePointers_t

typedef struct QueuePointers
{int8_t * pcTail;	//指向队列缓冲区的末尾地址int8_t * pcReadFrom;//当前读取的位置指针
} QueuePointers_t;

SemaphoreData

typedef struct SemaphoreData
{TaskHandle_t xMutexHolder;			//当前持有该mutex的任务句柄UBaseType_t uxRecursiveCallCount;	//递归调用计数器
} SemaphoreData_t;

队列集合(Queue Set) 是一个可以包含多个 队列(Queue) 或 信号量(Semaphore) 的对象。

队列相关创建

xQueueGenericCreate

以动态创建为例。
xQueueGenericCreate

QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,	//队列最大元素个数const UBaseType_t uxItemSize,//每个元素大小const uint8_t ucQueueType )	//队列类型{Queue_t * pxNewQueue = NULL;size_t xQueueSizeInBytes;uint8_t * pucQueueStorage;traceENTER_xQueueGenericCreate( uxQueueLength, uxItemSize, ucQueueType );if( ( uxQueueLength > ( UBaseType_t ) 0 ) &&			//确保队列长度合法( ( SIZE_MAX / uxQueueLength ) >= uxItemSize ) &&	//防止 uxQueueLength * uxItemSize 溢出( ( UBaseType_t ) ( SIZE_MAX - sizeof( Queue_t ) ) >= ( uxQueueLength * uxItemSize ) ) )//确保总的内存分配不会超出限制{//计算所需内存大小xQueueSizeInBytes = ( size_t ) ( ( size_t ) uxQueueLength * ( size_t ) uxItemSize );//分配内存(队列结构体+队列数据缓冲区)pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );if( pxNewQueue != NULL ){pucQueueStorage = ( uint8_t * ) pxNewQueue;pucQueueStorage += sizeof( Queue_t );#if ( configSUPPORT_STATIC_ALLOCATION == 1 ){//标记字段pxNewQueue->ucStaticallyAllocated = pdFALSE;}#endif /* configSUPPORT_STATIC_ALLOCATION *///初始化 Queue_t 结构体的所有字段prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );}else{traceQUEUE_CREATE_FAILED( ucQueueType );mtCOVERAGE_TEST_MARKER();}}else{configASSERT( pxNewQueue );mtCOVERAGE_TEST_MARKER();}traceRETURN_xQueueGenericCreate( pxNewQueue );return pxNewQueue;}

队列类型有:

#define queueQUEUE_TYPE_BASE                  ( ( uint8_t ) 0U )
#define queueQUEUE_TYPE_SET                   ( ( uint8_t ) 0U )
#define queueQUEUE_TYPE_MUTEX                 ( ( uint8_t ) 1U )
#define queueQUEUE_TYPE_COUNTING_SEMAPHORE    ( ( uint8_t ) 2U )
#define queueQUEUE_TYPE_BINARY_SEMAPHORE      ( ( uint8_t ) 3U )
#define queueQUEUE_TYPE_RECURSIVE_MUTEX       ( ( uint8_t ) 4U )

prvInitialiseNewQueue

static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength,const UBaseType_t uxItemSize,uint8_t * pucQueueStorage,//队列数据缓冲区指针const uint8_t ucQueueType,Queue_t * pxNewQueue )//队列结构体指针
{( void ) ucQueueType;//如果是信号量(uxItemSize == 0),不需要数据缓冲区,将 pcHead 指向队列结构体本身,防止野指针if( uxItemSize == ( UBaseType_t ) 0 ){pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;}else{pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage;}pxNewQueue->uxLength = uxQueueLength;pxNewQueue->uxItemSize = uxItemSize;//重置队列(新建队列)( void ) xQueueGenericReset( pxNewQueue, pdTRUE );#if ( configUSE_TRACE_FACILITY == 1 ){pxNewQueue->ucQueueType = ucQueueType;}#endif /* configUSE_TRACE_FACILITY */#if ( configUSE_QUEUE_SETS == 1 ){pxNewQueue->pxQueueSetContainer = NULL;}#endif /* configUSE_QUEUE_SETS */traceQUEUE_CREATE( pxNewQueue );
}
xQueueGenericReset
BaseType_t xQueueGenericReset( QueueHandle_t xQueue,BaseType_t xNewQueue )
{BaseType_t xReturn = pdPASS;Queue_t * const pxQueue = xQueue;traceENTER_xQueueGenericReset( xQueue, xNewQueue );configASSERT( pxQueue );if( ( pxQueue != NULL ) &&			//确保传入的队列不为空( pxQueue->uxLength >= 1U ) &&	//队列长度至少为 1( ( SIZE_MAX / pxQueue->uxLength ) >= pxQueue->uxItemSize ) )//检查 uxLength * uxItemSize 不会溢出{taskENTER_CRITICAL();//在修改队列状态期间,防止中断干扰,保证线程安全{//设置队列基本指针和状态pxQueue->u.xQueue.pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->uxItemSize );pxQueue->uxMessagesWaiting = ( UBaseType_t ) 0U;pxQueue->pcWriteTo = pxQueue->pcHead;pxQueue->u.xQueue.pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - 1U ) * pxQueue->uxItemSize );pxQueue->cRxLock = queueUNLOCKED;pxQueue->cTxLock = queueUNLOCKED;if( xNewQueue == pdFALSE )	//如果这不是一个新队列{//检查是否有任务正在等待发送数据if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ){//如果有,就从等待发送的任务列表里移除优先级最高的任务,并判断是否需要强制进行一次上下文切换if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}...}...}else	//如果是新队列,则初始化两个任务等待链表{vListInitialise( &( pxQueue->xTasksWaitingToSend ) );vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );}}taskEXIT_CRITICAL();//退出临界区}else{xReturn = pdFAIL;}configASSERT( xReturn != pdFAIL );traceRETURN_xQueueGenericReset( xReturn );return xReturn;
}
queueYIELD_IF_USING_PREEMPTION
#if ( configUSE_PREEMPTION == 0 )#define queueYIELD_IF_USING_PREEMPTION()
#else#if ( configNUMBER_OF_CORES == 1 )//使用底层平台相关的 yield 实现;//portYIELD_WITHIN_API() 通常是通过触发 PendSV 异常来实现任务切换,和portYIELD()一样#define queueYIELD_IF_USING_PREEMPTION()    portYIELD_WITHIN_API()#else /* #if ( configNUMBER_OF_CORES == 1 ) */#define queueYIELD_IF_USING_PREEMPTION()    vTaskYieldWithinAPI()#endif /* #if ( configNUMBER_OF_CORES == 1 ) */
#endifvoid vTaskYieldWithinAPI( void )
{traceENTER_vTaskYieldWithinAPI();if( portGET_CRITICAL_NESTING_COUNT() == 0U ){portYIELD();}else{xYieldPendings[ portGET_CORE_ID() ] = pdTRUE;}traceRETURN_vTaskYieldWithinAPI();
}

后面不同类型的队列创建都会用到xQueueGenericCreate
在这里插入图片描述

xQueueCreateSet

QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength )
{QueueSetHandle_t pxQueue;traceENTER_xQueueCreateSet( uxEventQueueLength );pxQueue = xQueueGenericCreate( uxEventQueueLength, ( UBaseType_t ) sizeof( Queue_t * ), queueQUEUE_TYPE_SET );traceRETURN_xQueueCreateSet( pxQueue );return pxQueue;
}

xQueueCreateMutex

QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType )
{QueueHandle_t xNewQueue;const UBaseType_t uxMutexLength = ( UBaseType_t ) 1, uxMutexSize = ( UBaseType_t ) 0;traceENTER_xQueueCreateMutex( ucQueueType );xNewQueue = xQueueGenericCreate( uxMutexLength, uxMutexSize, ucQueueType );prvInitialiseMutex( ( Queue_t * ) xNewQueue );traceRETURN_xQueueCreateMutex( xNewQueue );return xNewQueue;
}
static void prvInitialiseMutex( Queue_t * pxNewQueue )
{if( pxNewQueue != NULL ){pxNewQueue->u.xSemaphore.xMutexHolder = NULL;//表示当前没有任务持有这个互斥量,这个字段在获取和释放互斥量时会被更新pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX;//标记这个队列是一个互斥量//如果这是一个递归互斥量(recursive mutex) ,这个字段记录当前任务调用 xSemaphoreTake() 的次数;//只有当计数器减到 0 时,互斥量才会真正被释放,避免同一个任务多次获取互斥量造成死锁pxNewQueue->u.xSemaphore.uxRecursiveCallCount = 0;traceCREATE_MUTEX( pxNewQueue );//向队列发送一个空消息(初始化信号量值为 1,表示资源可用)( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U, queueSEND_TO_BACK );}else...
}
#define queueSEMAPHORE_QUEUE_ITEM_LENGTH    ( ( UBaseType_t ) 0 )QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_t uxMaxCount,const UBaseType_t uxInitialCount )
{QueueHandle_t xHandle = NULL;traceENTER_xQueueCreateCountingSemaphore( uxMaxCount, uxInitialCount );if( ( uxMaxCount != 0U ) &&( uxInitialCount <= uxMaxCount ) ){xHandle = xQueueGenericCreate( uxMaxCount, queueSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_COUNTING_SEMAPHORE );if( xHandle != NULL ){//初始化信号量的当前值( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount;traceCREATE_COUNTING_SEMAPHORE();}...}...traceRETURN_xQueueCreateCountingSemaphore( xHandle );return xHandle;
}
http://www.xdnf.cn/news/657199.html

相关文章:

  • 企业网络综合实训
  • Zephyr OS: periodic_adv_rsp代码架构和实现
  • GPT-4o 风格提示词案例大全(持续更新 ing...)
  • 小白成长之路-计算机网络(二)
  • 前后端分离项目之新增编辑功能
  • 4800H 低负载黑屏或者蓝屏
  • JS逆向【抖查查】逆向分析 | sign | secret签名验证
  • 亚马逊竞争指数下降20%?这些类目正成新蓝海
  • linux centos 服务器性能排查 vmstat、top等常用指令
  • 算法-二进制运算
  • 将 Docker 镜像从服务器A迁移到服务器B的方法
  • DNS 详情 新增 DNS 自适应服务器 ip
  • AI时代新词-AI驱动的自动化(AI - Driven Automation)
  • 【Sqoop基础】Sqoop定位:关系型数据库与Hadoop生态间的高效数据桥梁
  • Coze教程:10分钟打造你的AI智能管家
  • 使用 `.inl` 文件和 `#pragma once` 解决模板函数头文件膨胀问题指南
  • linux 1.0.2
  • Web字体本地化的一种方案
  • 基于谷歌浏览器的Web Crypto API生成一对2048位的RSA密钥(公钥+私钥),并以JSON格式(JWK)打印到浏览器控制台
  • rocky linux-系统基本管理
  • uniapp 配置本地 https 开发环境(基于 Vue2 的 uniapp)
  • Maven-概述-介绍安装
  • 数字ic后端设计从入门到精通5(含fusion compiler, tcl教学)def详解
  • 什么是BFC,如何触发BFC,BFC有什么特性?
  • Linux系统平均负载与top、uptime命令详解
  • 液体散货装卸管理人员备考指南
  • 对话魔数智擎CEO柴磊:昇腾AI赋能,大小模型融合开启金融风控新范式
  • 【区间dp】-----例题4【凸多边形的划分】
  • python_入门基础语法(2)
  • OpenHarmony平台驱动使用(二),CLOCK