(学习日记)2024.03.25:UCOSIII第二十二节:系统启动流程详解

写在前面:
由于时间的不足与学习的碎片化,写博客变得有些奢侈。
但是对于记录学习(忘了以后能快速复习)的渴望一天天变得强烈。
既然如此
不如以天为单位,以时间为顺序,仅仅将博客当做一个知识学习的目录,记录笔者认为最通俗、最有帮助的资料,并尽量总结几句话指明本质,以便于日后搜索起来更加容易。


标题的结构如下:“类型”:“知识点”——“简短的解释”
部分内容由于保密协议无法上传。


点击此处进入学习日记的总目录

三十六、UCOSIII:系统启动流程详解

本章总结一下系统在刚启动时初始化的过程
介绍顺序为上电后的运行顺序:

1、运行启动文件

在系统上电的时候第一个执行的是启动文件里面由汇编编写的复位函数Reset_Handler

Reset_Handler   PROC
EXPORT  Reset_Handler             [WEAK]
IMPORT  __main
IMPORT  SystemInit
LDRR0, =SystemInit
                BLX     R0
LDRR0, =__main
                BX      R0
ENDP

复位函数的最后会调用C库函数__main__main函数的主要工作是初始化系统的堆和栈,最后调用C中的main()函数,从而去到C的世界。

2、主流程 main

首先看main函数:

int  main (void)
{
    OS_ERR  err;

    OSInit(&err);                                               /* Init uC/OS-III.                                      */

    OSTaskCreate((OS_TCB     *)&AppTaskStartTCB,                /* Create the start task                                */
                 (CPU_CHAR   *)"App Task Start",
                 (OS_TASK_PTR ) AppTaskStart,
                 (void       *) 0,
                 (OS_PRIO     ) APP_TASK_START_PRIO,
                 (CPU_STK    *)&AppTaskStartStk[0],
                 (CPU_STK_SIZE) APP_TASK_START_STK_SIZE / 10,
                 (CPU_STK_SIZE) APP_TASK_START_STK_SIZE,
                 (OS_MSG_QTY  ) 5u,
                 (OS_TICK     ) 0u,
                 (void       *) 0,
                 (OS_OPT      )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
                 (OS_ERR     *)&err);

    OSStart(&err);                                              /* Start multitasking (i.e. give control to uC/OS-III). */		
}

这种启动方式在野火的教程里被称作小心翼翼,十分谨慎法

这种方法是在main()函数中将硬件和RTOS系统先初始化好,然后创建一个启动任务后就启动调度器。
在启动任务里面创建各种应用任务,当所有任务都创建成功后,启动任务把自己删除,具体的伪代码实现如下:

int main (void)
{
    /* 硬件初始化 */
    HardWare_Init();		//(1)

    /* RTOS 系统初始化 */
    RTOS_Init();		//(2)

    /* 创建一个任务 */
    RTOS_TaskCreate(AppTaskCreate);		//(3)

    /* 启动RTOS,开始调度 */
    RTOS_Start();		//(4)
}

/* 起始任务,在里面创建任务 */
voidAppTaskCreate( void *arg )		//(5)
{
    /* 创建任务1,然后执行 */
    RTOS_TaskCreate(Task1);		//(6)

    /* 当任务1阻塞时,继续创建任务2,然后执行 */
    RTOS_TaskCreate(Task2);

    /* ......继续创建各种任务 */

    /* 当任务创建完成,删除起始任务 */
    RTOS_TaskDelete(AppTaskCreate);		//(7)
}

void Task1( void *arg )		//(8)
{
    while (1)
    {
        /* 任务实体,必须有阻塞的情况出现 */
    }
}

void Task2( void *arg )		//(9)
{
    while (1)
    {
        /* 任务实体,必须有阻塞的情况出现 */
    }
}
  • (1):硬件初始化。来到硬件初始化这一步还属于裸机的范畴, 我们可以把需要使用到的硬件都初始化好而且测试好,确保无误。
  • (2):RTOS系统初始化。比如RTOS里面的全局变量的初始化, 空闲任务的创建等。不同的RTOS,它们的初始化有细微的差别。
  • (3):创建一个开始任务。然后在这个初始任务里面创建各种应用任务。
  • (4):启动RTOS调度器,开始任务调度。这个时候调度器就去执行刚刚创建好的初始任务。
  • (5):我们通常说任务是一个不带返回值的无限循环的C函数, 但是因为初始任务的特殊性,它不能是无限循环的,只执行一次后就关闭。在初始任务里面我们创建我们需要的各种任务。
  • (6):创建任务。每创建一个任务后它都将进入就绪态,系统会进行一次调度, 如果新创建的任务的优先级比初始任务的优先级高的话,那将去执行新创建的任务, 当新的任务阻塞时再回到初始任务被打断的地方继续执行。反之,则继续往下创建新的任务,直到所有任务创建完成。
  • (7):各种应用任务创建完成后,初始任务自己关闭自己,使命完成。
  • (8)(9):任务实体通常是一个不带返回值的无限循环的C函数,函数体必须有阻塞的情况出现, 不然任务(如果优先权恰好是最高)会一直在while循环里面执行,其他任务没有执行的机会。

3、系统初始化函数OSInit()

在调用创建任务函数之前,我们必须要对系统进行一次初始化。
而系统的初始化是根据我们配置宏定义进行初始化的, 有一些则是系统必要的初始化,如空闲任务,时钟节拍任务等。

下面我们来看看系统初始化的主要源码:

void  OSInit (OS_ERR  *p_err)
{
    CPU_STK      *p_stk;
    CPU_STK_SIZE  size;

    if (p_err == (OS_ERR *)0)
    {
        OS_SAFETY_CRITICAL_EXCEPTION();
        return;
    }

    OSInitHook();   /*初始化钩子函数相关的代码*/

    OSIntNestingCtr= (OS_NESTING_CTR)0;     /*清除中断嵌套计数器*/

    OSRunning =  OS_STATE_OS_STOPPED;       /*未启动多任务处理*/

    OSSchedLockNestingCtr = (OS_NESTING_CTR)0;/*清除锁定计数器*/

    OSTCBCurPtr= (OS_TCB *)0;       /*将OS_TCB指针初始化为已知状态  */
    OSTCBHighRdyPtr = (OS_TCB *)0;

    OSPrioCur = (OS_PRIO)0;                 /*将优先级变量初始化为已知状态*/
    OSPrioHighRdy                   = (OS_PRIO)0;
    OSPrioSaved                     = (OS_PRIO)0;


    if (OSCfg_ISRStkSize > (CPU_STK_SIZE)0)
    {
        p_stk = OSCfg_ISRStkBasePtr;        /*清除异常栈以进行栈检查*/
        if (p_stk != (CPU_STK *)0)
        {
            size  = OSCfg_ISRStkSize;
            while (size > (CPU_STK_SIZE)0)
            {
                size--;
                *p_stk = (CPU_STK)0;
                p_stk++;
            }
        }
    }

    OS_PrioInit();  /*初始化优先级位图表*/

    OS_RdyListInit();       /*初始化就绪列表*/

    OS_TaskInit(p_err);   /*初始化任务管理器*/
    if (*p_err != OS_ERR_NONE)
    {
        return;
    }

    OS_IdleTaskInit(p_err);    /* 初始化空闲任务  */       
    if (*p_err != OS_ERR_NONE)
    {
        return;
    }

    OS_TickTaskInit(p_err);   /* 初始化时钟节拍任务*/        
    if (*p_err != OS_ERR_NONE)
    {
        return;
    }

    OSCfg_Init();
}

在这个系统初始化中,我们主要看两个地方

  • 一个是空闲任务的初始化
  • 一个是时钟节拍任务的初始化

这两个任务是必须存在的任务,否则系统无法正常运行。

1. 空闲任务的初始化

其实初始化就是创建一个空闲任务,空闲任务的相关信息由系统默认指定, 用户不能修改

void  OS_IdleTaskInit (OS_ERR  *p_err)
{
#ifdef OS_SAFETY_CRITICAL
    if (p_err == (OS_ERR *)0)
    {
        OS_SAFETY_CRITICAL_EXCEPTION();
        return;
    }
#endif

    OSIdleTaskCtr = (OS_IDLE_CTR)0;         //(1)
/* ---------------- CREATE THE IDLE TASK ---------------- */
    OSTaskCreate((OS_TCB     *)&OSIdleTaskTCB,
                (CPU_CHAR   *)((void *)"μC/OS-III Idle Task"),
                (OS_TASK_PTR)OS_IdleTask,
                (void       *)0,
                (OS_PRIO     )(OS_CFG_PRIO_MAX - 1u),
                (CPU_STK    *)OSCfg_IdleTaskStkBasePtr,
                (CPU_STK_SIZE)OSCfg_IdleTaskStkLimit,
                (CPU_STK_SIZE)OSCfg_IdleTaskStkSize,
                (OS_MSG_QTY  )0u,
                (OS_TICK     )0u,
                (void       *)0,
                (OS_OPT)(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR |OS_OPT_TASK_NO_TLS),
                (OS_ERR     *)p_err);               //(2)
}
  • (1):OSIdleTaskCtr在os.h头文件中定义,是一个32位无符号整型变量, 该变量的作用是用于统计空闲任务的运行的,怎么统计呢,我们在空闲任务中讲解。现在初始化空闲任务,系统就将OSIdleTaskCtr清零。
  • (2):我们可以很容易看到系统只是调用了OSTaskCreate()函数来创建一个任务,这个任务就是空闲任务, 任务优先级为OS_CFG_PRIO_MAX-1,OS_CFG_PRIO_MAX是一个宏,该宏定义表示μC/OS的任务优先级数值的最大值,我们知道, 在μC/OS系统中,任务的优先级数值越大,表示任务的优先级越低,所以空闲任务的优先级是最低的。 空闲任务栈大小为OSCfg_IdleTaskStkSize,它也是一个宏,在os_cfg_app.c文件中定义,默认为128, 则空闲任务栈默认为128*4=512字节。

2. 空闲任务的定义

空闲任务其实就是一个函数,其函数入口是OS_IdleTask

void  OS_IdleTask (void  *p_arg)
{
    CPU_SR_ALLOC();


    /* Prevent compiler warning for not using 'p_arg'*/
    p_arg = p_arg;

     while (DEF_ON)
     {
        CPU_CRITICAL_ENTER();
        OSIdleTaskCtr++;
#if OS_CFG_STAT_TASK_EN > 0u
        OSStatTaskCtr++;
#endif
        CPU_CRITICAL_EXIT();
        /* Call user definable HOOK */
        OSIdleTaskHook();
    }
}

空闲任务的作用还是很大的,它是一个无限的死循环。
因为其优先级是最低的,所以任何优先级比它高的任务都能抢占它从而取得CPU的使用权。

为什么系统要空闲任务呢?
因为CPU是不会停下来的,即使啥也不干,CPU也不会停下来,此时系统就必须保证有一个随时处于就绪态的任务, 而且这个任务不会抢占其他任务,当且仅当系统的其他任务处于阻塞中,系统才会运行空闲任务。
这个任务可以做很多事情,任务统计, 钩入用户自定义的钩子函数实现用户自定义的功能等,但是需要注意的是,在钩子函数中用户不允许调用任何可以使空闲任务阻塞的函数接口, 空闲任务是不允许被阻塞的。

3. 时钟节拍任务的初始化

OS_TickTaskInit()函数也是创建一个时钟节拍任务

void  OS_TickTaskInit (OS_ERR  *p_err)
{
#ifdef OS_SAFETY_CRITICAL
    if (p_err == (OS_ERR *)0)
    {
        OS_SAFETY_CRITICAL_EXCEPTION();
        return;
    }
#endif

    OSTickCtr         = (OS_TICK)0u; /* Clear the tick counter   */

    OSTickTaskTimeMax = (CPU_TS)0u;


    OS_TickListInit();/* Initialize the tick list data structures  */

    /* ---------------- CREATE THE TICK TASK ---------------- */
    if (OSCfg_TickTaskStkBasePtr == (CPU_STK *)0)
    {
        *p_err = OS_ERR_TICK_STK_INVALID;
        return;
    }

    if (OSCfg_TickTaskStkSize < OSCfg_StkSizeMin)
    {
        *p_err = OS_ERR_TICK_STK_SIZE_INVALID;
        return;
    }
    /* Only one task at the 'Idle Task' priority              */
    if (OSCfg_TickTaskPrio >= (OS_CFG_PRIO_MAX - 1u))
    {
        *p_err = OS_ERR_TICK_PRIO_INVALID;
        return;
    }

    OSTaskCreate((OS_TCB     *)&OSTickTaskTCB,
                (CPU_CHAR   *)((void *)"μC/OS-III Tick Task"),
                (OS_TASK_PTR )OS_TickTask,
                (void       *)0,
                (OS_PRIO     )OSCfg_TickTaskPrio,
                (CPU_STK    *)OSCfg_TickTaskStkBasePtr,
                (CPU_STK_SIZE)OSCfg_TickTaskStkLimit,
                (CPU_STK_SIZE)OSCfg_TickTaskStkSize,
                (OS_MSG_QTY  )0u,
                (OS_TICK     )0u,
                (void       *)0,
                (OS_OPT)(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR | OS_OPT_TASK_NO_TLS),
                (OS_ERR     *)p_err);
}

4、启动任务AppTaskStart()

系统在启动任务里面创建各种应用任务,当所有任务都创建成功后,启动任务把自己删除

static  void  AppTaskStart (void *p_arg)
{
    CPU_INT32U  cpu_clk_freq;
    CPU_INT32U  cnts;
    OS_ERR      err;


   (void)p_arg;

    BSP_Init();                                                 /* Initialize BSP functions                             */
    CPU_Init();

    cpu_clk_freq = BSP_CPU_ClkFreq();                           /* Determine SysTick reference freq.                    */
    cnts = cpu_clk_freq / (CPU_INT32U)OSCfg_TickRate_Hz;        /* Determine nbr SysTick increments                     */
    OS_CPU_SysTickInit(cnts);                                   /* Init uC/OS periodic time src (SysTick).              */

    Mem_Init();                                                 /* Initialize Memory Management Module                  */

#if OS_CFG_STAT_TASK_EN > 0u
    OSStatTaskCPUUsageInit(&err);                               /* Compute CPU capacity with no task running            */
#endif

    CPU_IntDisMeasMaxCurReset();


    OSTaskCreate((OS_TCB     *)&AppTaskLed1TCB,                /* Create the Led1 task                                */
                 (CPU_CHAR   *)"App Task Led1",
                 (OS_TASK_PTR ) AppTaskLed1,
                 (void       *) 0,
                 (OS_PRIO     ) APP_TASK_LED1_PRIO,
                 (CPU_STK    *)&AppTaskLed1Stk[0],
                 (CPU_STK_SIZE) APP_TASK_LED1_STK_SIZE / 10,
                 (CPU_STK_SIZE) APP_TASK_LED1_STK_SIZE,
                 (OS_MSG_QTY  ) 5u,
                 (OS_TICK     ) 0u,
                 (void       *) 0,
                 (OS_OPT      )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
                 (OS_ERR     *)&err);
								 
    OSTaskCreate((OS_TCB     *)&AppTaskLed2TCB,                /* Create the Led2 task                                */
                 (CPU_CHAR   *)"App Task Led2",
                 (OS_TASK_PTR ) AppTaskLed2,
                 (void       *) 0,
                 (OS_PRIO     ) APP_TASK_LED2_PRIO,
                 (CPU_STK    *)&AppTaskLed2Stk[0],
                 (CPU_STK_SIZE) APP_TASK_LED2_STK_SIZE / 10,
                 (CPU_STK_SIZE) APP_TASK_LED2_STK_SIZE,
                 (OS_MSG_QTY  ) 5u,
                 (OS_TICK     ) 0u,
                 (void       *) 0,
                 (OS_OPT      )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
                 (OS_ERR     *)&err);

    OSTaskCreate((OS_TCB     *)&AppTaskLed3TCB,                /* Create the Led3 task                                */
                 (CPU_CHAR   *)"App Task Led3",
                 (OS_TASK_PTR ) AppTaskLed3,
                 (void       *) 0,
                 (OS_PRIO     ) APP_TASK_LED3_PRIO,
                 (CPU_STK    *)&AppTaskLed3Stk[0],
                 (CPU_STK_SIZE) APP_TASK_LED3_STK_SIZE / 10,
                 (CPU_STK_SIZE) APP_TASK_LED3_STK_SIZE,
                 (OS_MSG_QTY  ) 5u,
                 (OS_TICK     ) 0u,
                 (void       *) 0,
                 (OS_OPT      )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
                 (OS_ERR     *)&err);
		
		
		OSTaskDel ( & AppTaskStartTCB, & err );
		
		
}

其中需要注意的如下:

CPU_Init();
//时间戳初始化

OS_CPU_SysTickInit(cnts);
//MCU的内核定时器SysTick初始化

Mem_Init();
//内存初始化

1. 时间戳初始化

在启动任务AppTaskStart()中,有一个CPU初始化函数,CPU初始化函数可以初始化时间戳

void  CPU_Init (void)
{
/* --------------------- INIT TS ---------------------- */
#if ((CPU_CFG_TS_EN     == DEF_ENABLED) || \
    (CPU_CFG_TS_TMR_EN == DEF_ENABLED))
    CPU_TS_Init();     /* 时间戳测量的初始化   */

#endif
/* -------------- INIT INT DIS TIME MEAS -------------- */
#ifdef  CPU_CFG_INT_DIS_MEAS_EN
    CPU_IntDisMeasInit();  /* 最大关中断时间测量初始化     */

#endif

/* ------------------ INIT CPU NAME ------------------- */
#if (CPU_CFG_NAME_EN == DEF_ENABLED)
    CPU_NameInit();         //CPU 名字初始化
#endif
}

时间戳,它的精度高达ns级别,是CPU内核的一个重要资源。

在Cortex-M(注意:M0内核不可用)内核中有一个外设叫DWT(Data Watchpoint and Trace),是用于系统调试及跟踪, 它有一个32位的寄存器叫CYCCNT。
CYCCNT是一个向上的计数器,记录的是内核时钟运行的个数。
内核时钟跳动一次, 该计数器就加1,当CYCCNT溢出之后,会清零重新开始向上计数。
CYCCNT的精度非常高,其精度取决于内核的频率是多少, 如果是STM32F1系列,内核时钟是72M,那精度就是1/72M = 14ns,而程序的运行时间都是微秒级别的,所以14ns的精度是远远够的。
CYCCNT最长能记录的时间为:60s=2的32次方/72000000(假设内核频率为72M,内核跳一次的时间大概为1/72M=14ns), 而如果是STM32H7系列这种400M主频的芯片,那它的计时精度高达2.5ns(1/400000000 = 2.5)。
如果是i.MX RT1052这种比较厉害的处理器,最长能记录的时间为: 8.13s=2的32次方/528000000(假设内核频率为528M, 内核跳一次的时间大概为1/528M=1.9ns) 。

想要启用DWT外设,需要由另外的内核调试寄存器DEMCR的位24控制,写1启用,DEMCR的地址是0xE000 EDFC。
在这里插入图片描述
启用DWT_CYCCNT寄存器之前,先清零。
让我们看看DWT_CYCCNT的基地址,从ARM-Cortex-M手册中可以看到其基地址是0xE000 1004, 复位默认值是0,而且它的类型是可读可写的,我们往0xE000 1004这个地址写0就将DWT_CYCCNT清零了。

在这里插入图片描述
关于CYCCNTENA,它是DWT控制寄存器的第一位,写1启用,则启用CYCCNT计数器,否则CYCCNT计数器将不会工作,它的地址是0xE000EDFC。
在这里插入图片描述
所以想要使用DWT的CYCCNT步骤:

  • 先启用DWT外设,这个由另外内核调试寄存器DEMCR的位24控制,写1启用
  • 在启用CYCCNT寄存器之前,先清零。
  • 启用CYCCNT寄存器,这个由DWT的CYCCNTENA 控制,也就是DWT控制寄存器的位0控制,写1启用

这样子,我们就能去看看μC/OS的时间戳的初始化了

#define  DWT_CR      *(CPU_REG32 *)0xE0001000
#define  DWT_CYCCNT  *(CPU_REG32 *)0xE0001004
#define  DEM_CR      *(CPU_REG32 *)0xE000EDFC

#define  DEM_CR_TRCENA                   (1 << 24)

#define  DWT_CR_CYCCNTENA                (1 <<  0)

#if (CPU_CFG_TS_TMR_EN == DEF_ENABLED)
void  CPU_TS_TmrInit (void)
{
    CPU_INT32U  cpu_clk_freq_hz;

    /* Enable Cortex-M3's DWT CYCCNT reg. */
    DEM_CR         |= (CPU_INT32U)DEM_CR_TRCENA;

    DWT_CYCCNT      = (CPU_INT32U)0u;
    DWT_CR         |= (CPU_INT32U)DWT_CR_CYCCNTENA;

    cpu_clk_freq_hz = BSP_CPU_ClkFreq();
    CPU_TS_TmrFreqSet(cpu_clk_freq_hz);
}
#endif

2. SysTick初始化

时钟节拍的频率表示操作系统每1秒钟产生多少个tick。
tick即是操作系统节拍的时钟周期,时钟节拍就是系统以固定的频率产生中断(时基中断), 并在中断中处理与时间相关的事件,推动所有任务向前运行。
时钟节拍需要依赖于硬件定时器,在STM32 裸机程序中经常使用的SysTick时钟是 MCU的内核定时器,通常都使用该定时器产生操作系统的时钟节拍。

用户需要先在“ os_cfg_app.h”中设定时钟节拍的频率,该频率越高, 操作系统检测事件就越频繁,可以增强任务的实时性,但太频繁也会增加操作系统内核的负担加重,所以用户需要权衡该频率的设置。

我们在这里采用默认的 1000Hz(之后若无特别声明,均采用 1000 Hz),也就是时钟节拍的周期为 1 ms。

函数OS_CPU_SysTickInit()用于初始化时钟节拍中断,初始化中断的优先级,SysTick中断的启用等等,这个函数要跟不同的CPU进行编写, 并且在系统任务的第一个任务开始的时候进行调用,如果在此之前进行调用,可能会造成系统奔溃,因为系统还没有初始化好就进入中断, 可能在进入和退出中断的时候会调用系统未初始化好的一些模块

cpu_clk_freq = BSP_CPU_ClkFreq();	/* Determine SysTick reference freq. */
cnts = cpu_clk_freq / (CPU_INT32U)OSCfg_TickRate_Hz;
OS_CPU_SysTickInit(cnts); 	/*Init μC/OS periodic time src (SysTick).*/

3. 内存初始化

我们都知道,内存在嵌入式中是很珍贵的存在,而一个系统是软件,则必须要有一块内存属于系统所管理的。
所以在系统创建任务之前, 就必须将系统必要的东西进行初始化。
μC/OS采用一块连续的大数组作为系统管理的内存, CPU_INT08U Mem_Heap[LIB_MEM_CFG_HEAP_SIZE], 在使用之前就需要先将管理的内存进行初始化

Mem_Init();

5、任务调度器启动函数OSStart()

在创建完任务的时候,我们需要开启调度器。
因为创建仅仅是把任务添加到系统中,还没真正调度,那怎么才能让系统支持运行呢?
μC/OS为我们提供一个系统启动的函数接口——OSStart(),我们使用OSStart()函数就能让系统开始运行

void  OSStart (OS_ERR  *p_err)
{
#ifdef OS_SAFETY_CRITICAL
    if (p_err == (OS_ERR *)0) {
        OS_SAFETY_CRITICAL_EXCEPTION();
        return;
    }
#endif

    if (OSRunning == OS_STATE_OS_STOPPED) {
        OSPrioHighRdy   = OS_PrioGetHighest();/* Find the highest priority */
        OSPrioCur       = OSPrioHighRdy;
        OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;
        OSTCBCurPtr     = OSTCBHighRdyPtr;
        OSRunning       = OS_STATE_OS_RUNNING;
        OSStartHighRdy();/* Execute target specific code to start task  */
        *p_err           = OS_ERR_FATAL_RETURN;
        /* OSStart() is not supposed to return  */
    }
    else
    {
        *p_err           = OS_ERR_OS_RUNNING; /* OS is already running */
    }
}

最近更新

  1. TCP协议是安全的吗?

    2024-03-31 00:36:03       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-31 00:36:03       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-31 00:36:03       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-31 00:36:03       20 阅读

热门阅读

  1. C++ | filesystem file not found

    2024-03-31 00:36:03       20 阅读
  2. brctl 命令

    2024-03-31 00:36:03       19 阅读
  3. ChatGPT写作攻略:完善学术论文技巧指南

    2024-03-31 00:36:03       20 阅读
  4. Linux IRC

    Linux IRC

    2024-03-31 00:36:03      21 阅读
  5. 工作日志- 不定期更新

    2024-03-31 00:36:03       19 阅读
  6. P19:注释

    2024-03-31 00:36:03       20 阅读
  7. 实现元素水平垂直居中的方法

    2024-03-31 00:36:03       23 阅读
  8. SpringBoot -- 错误处理机制

    2024-03-31 00:36:03       20 阅读
  9. vue3从精通到入门1: vue3特点以及与vue2的区别

    2024-03-31 00:36:03       19 阅读
  10. 面试宝典:深入分析golang 的反射(reflect)

    2024-03-31 00:36:03       18 阅读
  11. 深入Python元编程:从基础到实践

    2024-03-31 00:36:03       15 阅读