FreeRTOS调度器启动过程分析

目录

引出思考

vTaskStartScheduler()启动任务调度器

xPortStartScheduler()函数

FreeRTOS启动第一个任务

vPortSVCHandler()函数

总结


引出思考

首先想象一下如何启动第一个任务?

假设我们要启动的第一个任务是任务A,那么就需要将任务A的寄存器值恢复到CPU寄存器

任务A的寄存器值,在一开始创建任务的时候就保存在任务A的栈中了,这个在创建任务的细节博文中我已经分析过FreeRTOS任务创建及细节-CSDN博客

注意:

1、中断产生时,硬件将xPSR,PC(R15)、LR(R14)、R12、R3-R0保存和恢复,而R4-R11需要手动保存和恢复

2、进入中断后硬件会强制使用MSP指针,此时LR(R14)的值将会自动被更新为特殊的EXC_RETURN

使用FreeRTOS,一个最基本的程序框架如下所示:

int main(void)
{  
    // 必要的初始化工作;
    // 创建任务1;
    // 创建任务2;
    // ...
    vTaskStartScheduler();  /*启动调度器*/
    while(1);   
}

任务创建完成后,静态变量指针pxCurrentTCB,指向优先级最高的就绪任务,但此时任务不能运行,因为接下来还有最为关键的一步:启动FreeRTOS调度器。

调度器是FreeRTOS操作系统的核心,主要负责任务切换,即找出最高优先级的就绪任务,并使之获得CPU运行权。调度器并非自动运行的,需要人为启动它。

vTaskStartScheduler()启动任务调度器

API函数vTaskStartScheduler()用于启动调度器,它会创建一个空闲任务,初始化一些静态变量,最主要的,它会初始化系统节拍定时器并设置好相应的中断,然后启动第一个任务。这里我们分析启动调度器的过程,和之前一样,启动调度器也涉及到硬件架构的一些知识(比如系统节拍定时器初始化),因此本文这里以CortexM3架构为例。

启动调度器的API函数vTaskStartScheduler()的源码这里我直接精简一下屏蔽掉条件编译的部分:

void vTaskStartScheduler( void )
{
BaseType_t xReturn;
StaticTask_t *pxIdleTaskTCBBuffer= NULL;
StackType_t *pxIdleTaskStackBuffer= NULL;
uint16_t usIdleTaskStackSize =tskIDLE_STACK_SIZE;
 
    /*如果使用静态内存分配任务堆栈和任务TCB,则需要为空闲任务预先定义好任务内存和任务TCB空间*/
    #if(configSUPPORT_STATIC_ALLOCATION == 1 )
    {
       vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &usIdleTaskStackSize);
    }
    #endif /*configSUPPORT_STATIC_ALLOCATION */
 
    /* 创建空闲任务,使用最低优先级*/
    xReturn =xTaskGenericCreate( prvIdleTask, "IDLE",usIdleTaskStackSize, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT), &xIdleTaskHandle,pxIdleTaskStackBuffer,pxIdleTaskTCBBuffer, NULL );
 
    if( xReturn == pdPASS )
    {
        /* 先关闭中断,确保节拍定时器中断不会在调用xPortStartScheduler()时或之前发生.当第一个任务启动时,会重新启动中断*/
       portDISABLE_INTERRUPTS();
       
        /* 初始化静态变量 */
       xNextTaskUnblockTime = portMAX_DELAY;
       xSchedulerRunning = pdTRUE;
        xTickCount = ( TickType_t ) 0U;
 
        /* 如果宏configGENERATE_RUN_TIME_STATS被定义,表示使用运行时间统计功能,则下面这个宏必须被定义,用于初始化一个基础定时器/计数器.*/
       portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
 
        /* 设置系统节拍定时器,这与硬件特性相关,因此被放在了移植层.*/
        if(xPortStartScheduler() != pdFALSE )
        {
            /* 如果调度器正确运行,则不会执行到这里,函数也不会返回*/
        }
        else
        {
            /* 仅当任务调用API函数xTaskEndScheduler()后,会执行到这里.*/
        }
    }
    else
    {
        /* 执行到这里表示内核没有启动,可能因为堆栈空间不够 */
       configASSERT( xReturn );
    }
 
    /* 预防编译器警告*/
    ( void ) xIdleTaskHandle;
}

这个API函数首先创建一个空闲任务,空闲任务使用最低优先级0,空闲任务的任务句柄存放在静态变量xIdleTaskHandle中,可以调用API函数xTaskGetIdleTaskHandle()获得空闲任务句柄。

如果任务创建成功,则关闭中断(调度器启动结束时会再次使能中断的),初始化一些静态变量,然后调用函数xPortStartScheduler()来启动系统节拍定时器并启动第一个任务。因为设置系统节拍定时器涉及到硬件特性,因此函数xPortStartScheduler函数由移植层提供,不同的硬件架构,这个函数的代码是不一样的。

函数vTaskStartScheduler()用于启动任务调度器,任务调度器启动后,FreeRTOS便会开始进行任务调度,除非调用函数xTaskEndScheduler()停止调度器,否则不会再返回。

函数vTaskStartScheduler()主要做了六件事情:

  • 创建空闲任务,根据是否支持静态内存管理,使用静态方式或者动态方式创建空闲任务
  • 创建定时器服务任务,创建定时器服务任务需要配置启用软件定时器,创建定时器服务任务,同样是根据是否配置支持静态内存管理,使用静态或者动态方式创建定时器服务任务。
  • 关闭中断,使用portDISABLE_INTERRUPTS()关闭中断,这种方式只关闭受FreeRTOS管理的中断。关闭中断主要是为了防止SysTick中断在任务调度器开启之前或过程中,产生中断。FreeRTOS会在开始运行第一个任务时,重新打开中断。
  • 初始化一些全局变量,并将任务调度器的运行标志设置为已运行
  • 初始化任务运行时间统计功能的时基定时器,任务运行时间统计功能需要一个硬件定时器提供高精度的计数,这个硬件定时器就在这里进行配置,如果配置不启用任务运行时间统计功能的,就无需进行这项硬件定时器的配置。
  • 最后就是调用xPortStartScheduler()

xPortStartScheduler()函数

对于Cortex-M4架构,函数xPortStartScheduler()函数的实现如下:

	/* Pop the core registers. */
	ldmia r0!, {r4-r11, r14}
	msr psp, r0
	isb
	mov r0, #0
	msr	basepri, r0
	bx r14
}
/*-----------------------------------------------------------*/

__asm void prvStartFirstTask( void )
{
	PRESERVE8

	/* Use the NVIC offset register to locate the stack. */
	ldr r0, =0xE000ED08
	ldr r0, [r0]
	ldr r0, [r0]
	/* Set the msp back to the start of the stack. */
	msr msp, r0
	/* Clear the bit that indicates the FPU is in use in case the FPU was used
	before the scheduler was started - which would otherwise result in the
	unnecessary leaving of space in the SVC stack for lazy saving of FPU
	registers. */
	mov r0, #0
	msr control, r0
	/* Globally enable interrupts. */
	cpsie i
	cpsie f
	dsb
	isb
	/* Call SVC to start the first task. */
	svc 0
	nop
	nop
}
/*-----------------------------------------------------------*/

__asm void prvEnableVFP( void )
{
	PRESERVE8

	/* The FPU enable bits are in the CPACR. */
	ldr.w r0, =0xE000ED88
	ldr	r1, [r0]

	/* Enable CP10 and CP11 coprocessors, then save back. */
	orr	r1, r1, #( 0xf << 20 )
	str r1, [r0]
	bx	r14
	nop
}
/*-----------------------------------------------------------*/

/*
 * See header file for description.
 */
BaseType_t xPortStartScheduler( void )
{
	/* configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to 0.
	See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html */
	configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );

	/* This port can be used on all revisions of the Cortex-M7 core other than
	the r0p1 parts.  r0p1 parts should use the port from the
	/source/portable/GCC/ARM_CM7/r0p1 directory. */
	configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
	configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );

	#if( configASSERT_DEFINED == 1 )
	{
		volatile uint32_t ulOriginalPriority;
		volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
		volatile uint8_t ucMaxPriorityValue;

		/* Determine the maximum priority from which ISR safe FreeRTOS API
		functions can be called.  ISR safe functions are those that end in
		"FromISR".  FreeRTOS maintains separate thread and ISR API functions to
		ensure interrupt entry is as fast and simple as possible.

		Save the interrupt priority value that is about to be clobbered. */
		ulOriginalPriority = *pucFirstUserPriorityRegister;

		/* Determine the number of priority bits available.  First write to all
		possible bits. */
		*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;

		/* Read the value back to see how many bits stuck. */
		ucMaxPriorityValue = *pucFirstUserPriorityRegister;

		/* The kernel interrupt priority should be set to the lowest
		priority. */
		configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );

		/* Use the same mask on the maximum system call priority. */
		ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

		/* Calculate the maximum acceptable priority group value for the number
		of bits read back. */
		ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
		while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
		{
			ulMaxPRIGROUPValue--;
			ucMaxPriorityValue <<= ( uint8_t ) 0x01;
		}

		#ifdef __NVIC_PRIO_BITS
		{
			/* Check the CMSIS configuration that defines the number of
			priority bits matches the number of priority bits actually queried
			from the hardware. */
			configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
		}
		#endif

		#ifdef configPRIO_BITS
		{
			/* Check the FreeRTOS configuration that defines the number of
			priority bits matches the number of priority bits actually queried
			from the hardware. */
			configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
		}
		#endif

		/* Shift the priority group value back to its position within the AIRCR
		register. */
		ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
		ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

		/* Restore the clobbered interrupt priority register to its original
		value. */
		*pucFirstUserPriorityRegister = ulOriginalPriority;
	}
	#endif /* conifgASSERT_DEFINED */

	/* Make PendSV and SysTick the lowest priority interrupts. */
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

	/* Start the timer that generates the tick ISR.  Interrupts are disabled
	here already. */
	vPortSetupTimerInterrupt();

	/* Initialise the critical nesting count ready for the first task. */
	uxCriticalNesting = 0;

	/* Ensure the VFP is enabled - it should be anyway. */
	prvEnableVFP();

	/* Lazy save always. */
	*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;

	/* Start the first task. */
	prvStartFirstTask();

	/* Should not get here! */
	return 0;
}

从源码上面来看,其中开始的一大段都是冗余代码。因为Cortex-M4的中断优先级有点反直觉:在Cortex-M内核中,优先级的数值越大,表明优先级越低,数值越小代表优先级越高。根据官方统计,在Cortex-M4硬件上使用FreeRTOS,绝大多数问题都是优先级数值设置不对的问题,因此,为了使得FreeRTOS更健壮,FreeRTOS的作者在编写Cortex-M架构的移植层代码时,特意增加了冗余代码。关于详细的Cortex-M架构的中断优先级设置,后续有机会再写一篇博客。

在Cortex-M4架构中,FreeRTOS为了任务启动和任务切换使用了三个重要的异常:SVC、PendSV、SysTick。SVC(系统服务调用)用于任务启动(只在启动第一个任务的时候会调用,以后都不会用到),有些操作系统不允许应用程序直接访问硬件,而是通过提供一些系统服务函数、通过SVC来调用;PendSV(可挂起系统调用)用于完成任务切换,它的最大特性是如果当前有优先级比它高的中断在运行,PendSV会推迟执行,直到高优先级中断执行完毕;SysTick用于产生系统时钟节拍时钟,提供一个时间片,如果多个任务共享同一个优先级,则每次Systick中断,下一个任务将获得一个时间片。关于详细的SVC、PendSV异常描述,推荐《Cortex-M3和M4权威指南》一书的“异常”部分。

 这里将PendSV和SysTick异常优先级设置为最低,这样任务切换不会打断某个中断服务程序,中断服务程序也不会被延迟,这样简化了设计,有利于系统稳定。

      接下来调用函数vPortSetupTimerInterrupt()设置SysTick定时器中断周期并使能定时器运行这个函数比较简单,就是设置SysTick硬件的相应寄存器。

这里小小的总结一下这个xPortStartScheduler()函数的工作🤣:

  • 在启用断言的情况下,函数xPortStartScheduler会检测用户在FreeRTOSConfig.h文件中对中断的相关配置是否有误
  • 配置PendSV和SysTick的中断优先级为最低优先级
  • 调用函数vPortSetupTimerInterrupt()配置SysTick,函数vPortSetupTimerInterrupt()首先会将SysTick当前计数值清空,并根据FreeRTOSConfig.h文件中配置configSYSTICK_CLOCK_HZ(SysTick 时钟源频率)和 configTICK_RATE_HZ(系统时钟节拍频率)计算并设置 SysTick 的重装载值,然后启动 SysTick 计数和中断。
  • 初始化临界区嵌套计数器为0
  • 调用函数prvEnableVFP()使能FPU,Cortex-M3内核没有FPU,M4内核是有FPU的,执行该函数后,FPU被开启
  • 接下来将FPCCR寄存器的[31:30]置1,这样在进出异常时,FPU的相关寄存器就会自动地保存和恢复,同样地M3内核的代码是没有的,M4内核有这部分代码
  • 调用函数prvStartFirstTask()启动第一个任务

FreeRTOS启动第一个任务

这里是这篇文章比较重要的关键点和难点🧐

      再接下来有一个关键的函数是prvStartFirstTask(),这个函数用来启动第一个任务。我们先看一下源码:

__asm void prvStartFirstTask( void )
{
    /*8字节对齐*/
	PRESERVE8

	/* Use the NVIC offset register to locate the stack. */
	ldr r0, =0xE000ED08  /*0xE000ED08 为 VTOR 地址*/
	ldr r0, [r0]         /*获取VTOR的值*/
	ldr r0, [r0]         /*获取MSP的初始值*/

	/* 初始化MSP */
	msr msp, r0
	/* Clear the bit that indicates the FPU is in use in case the FPU was used
	before the scheduler was started - which would otherwise result in the
	unnecessary leaving of space in the SVC stack for lazy saving of FPU
	registers. */
	mov r0, #0
	msr control, r0
	/*使能全局中断 */
	cpsie i
	cpsie f
	dsb
	isb
	/* 调用SVC启动第一个任务 */
	svc 0
	nop
	nop
}

函数prvStartFirstTask()用于初始化启动第一个任务前的环境,主要是设置MSP指针,并使能全局中断,具体的代码如上面所示;

从上面的代码可以看出,函数prvStartFirstTask()是一段汇编代码,分析它的工作:

  • 首先是使用了PRESERVE8,进行8字节对齐,这是因为,栈在任何时候都是需要4字节对齐的,而在调用入口得8字节对齐,在进行C编程的时候,编译器会自动完成对齐操作,而对于汇编,就需要手动进行对齐
  • 接下来就是为了获得MSP指针得初始值,那么这里肯定会引出两个问题🤣 

1. 什么是MSP指针?

程序在运行过程中需要一定得栈空间来保存局部变量等一些信息。当有信息需要保存到栈中时,MCU会自动更新SP指针,使SP指针指向最后一个入栈元素,那么程序就可以根据SP指针来从栈中存取信息。对于ARM的Cortex-M内核提供了两个栈空间,这两个栈空间的堆栈指针分别是MSP(主堆栈指针)和PSP(进程堆栈指针)。在Free RTOS中MSP是给系统栈空间使用的,而PSP是给任务栈使用的,也就是说,FreeRTOS任务的栈空间是通过PSP指向的,而在进入中断服务函数时,则是使用MSP指针。当使用不同的堆栈指针时,SP会等于当前使用的堆栈指针。

2. 为什么是0xE00ED08?

0xE00ED08是VTOR(向量表偏移寄存器)的地址,VTOR中保存了向量表的偏移地址。一般来说向量表其实是从0x00000000开始的,但是在有些情况下,可能需要修改或重定向向量表的首地址,因此ARM Cortex-M提供了VTOR对向量表进行重定向。而向量表是用来保存中断异常的入口函数地址,即栈顶地址的,并且向量表中的第一个字保存的就是栈底的地址,在start_stm32xxxxx.s文件中有如下定义:

__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved

以上就是向量表的部分内容,可以看到向量表的第一个元素就是栈指针的初始值,也就是栈底地址。

在了解了这两个问题之后,接下来再来看看代码。首先是获取 VTOR 的地址,接着获取
VTOR 的值,也就是获取向量表的首地址,最后获取向量表中第一个字的数据,也就是栈底指
针了。
  • 在获取了栈顶指针后,将MSP指针重新赋值为栈底指针。这个操作相当于丢弃了程序之前保存在栈中的数据,因为FreeRTOS从开启任务调度器到启动第一个任务都是不会返回的,是一条不归路,因此将栈中的数据丢弃,也不会有影响。
  • 重新赋值MSP后,接下来就重新使能全局中断,因为之前在函数vTaskStartScheduler()中关闭了受FreeRTOS管理的中断。
  • 最后使用SVC指令,并传入系统调用号0,触发SVC中断

vPortSVCHandler()函数

当使能了全局中断,并且手动触发了SVC中断之后,就会进入到SVC的中断服务函数中。SVC的中断服务函数为vPortSVCHandler(),该函数在port.c文件中有定义,具体的代码如下所示:

__asm void vPortSVCHandler( void )
{
    /*8字节对齐*/
	PRESERVE8

	/*获取任务栈地址. */
	ldr	r3, =pxCurrentTCB /*r3指向优先级最高的就绪态任务的任务控制块*/
	ldr r1, [r3]           /*r1为任务控制块地址*/
	ldr r0, [r1]            /*r0为任务控制块的第一个元素(栈顶)*/

	/* 模拟出栈,并设置PSP */ 
	ldmia r0!, {r4-r11, r14}  /*任务栈弹出到CPU寄存器*/
	msr psp, r0               /*设置PSP为任务栈指针*/ 
	isb

    /*使能所有中断*/
	mov r0, #0
	msr	basepri, r0

	bx r14    /* 使用 PSP 指针,并跳转到任务函数 */
}

从上面的代码中看出,函数vPortSVCHandler就是用来跳转到第一个任务函数中去的,该函数具体解析如下:

  1. 首先通过pxCurrentTCB获取优先级最高的就绪任务的任务栈地址,优先级最高的就绪态任务就是系统将要运行的任务。pxCurrentTCB是一个全局变量,用于指向系统中优先级最高的就绪态任务的任务控制块。
  2. 接下来通过任务的栈顶指针,将任务栈中的内容出栈到CPU寄存器中,任务栈中的内容在调用任务创建函数的时候,已经初始化好了,然后再设置PSP指针,那么,这么一来,任务的运行环境就准备好了。
  3. 通过往BASEPRI寄存器中写0,允许中断
  4. 最后通过汇编指令bx r14,使CPU跳转到任务的函数中去执行

                r14寄存器为链接寄存器LR,用于保存函数的返回地址。但是在异常或中断处理函数中,r14为EXC_RETURN,这个值的各个比特位有特殊的含义

因为此时是在 SVC 的中断服务函数中,因此此时的 r14 应为 EXC_RETURN ,将 r14 0xd
作或操作,然后将值写入 r14 ,那么就是将 r14 的值设置为了 0xFFFFFFED 0xFFFFFFED (具
体看是否使用了浮点单元),即返回后进入线程模式,并使用 PSP 。这里要注意的是, SVC 中断
服务函数的前面,将 PSP 指向了任务栈。
说了这么多, FreeRTOS 对于进入中断后 r14 EXC_RETURN 的具体应用就是,通过判断
EXC_RETURN bit4 是否为 0 ,来判断任务是否使用了浮点单元。
最后通过 bx r14 指令,跳转到任务的任务函数中执行,执行此指令, CPU 会自动从 PSP
向的栈中出栈 R0 R1 R2 R3 R12 LR PC xPSR 寄存器,并且如果 EXC_RETURN
bit4 0 (使用了浮点单元),那么 CPU 还会自动恢复浮点寄存器。

总结

最后,对于程序是个状态机和计算机是个状态机的理解更加深刻了,最后套用韦老师的一句话:

栈是一个真正的幕后英雄🤣 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/216742.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

X540t2关于手动安装intel驱动

首先去intel驱动官网下载&#xff0c;win10和win11驱动一样 https://www.intel.cn/content/www/cn/zh/download/18293/intel-network-adapter-driver-for-windows-10.html 然后下载下来解压 将Wired_driver_28.2_x64.exe修改成Wired_driver_28.2_x64.zip文件再解压 打开设备管…

mybatis的数据库连接池

直接看原文 原文链接:【MyBatis】 连接池技术_mybatis自带连接池-CSDN博客 本文先不说springBoot整合mybatis后的 本文讲的是没有被springBoot整合前的mybatis自己的默认的连接池 --------------------------------------------------------------------------------------…

v-on 可以监听多个方法吗?

目录 前言 详解&#xff1a;v-on 指令的基本概念 用法&#xff1a;v-on 指令监听多个方法 解析&#xff1a;v-on 指令的优势和局限性 优势 - **强大的事件处理**&#xff1a;v-on允许你处理各种DOM事件&#xff0c;从点击到输入等。 - **多方法监听**&#xff1a;可以轻…

全网最新最全的自动化测试教程:python+pytest接口自动化(9)-cookie绕过登录(保持登录状态

在编写接口自动化测试用例或其他脚本的过程中&#xff0c;经常会遇到需要绕过用户名/密码或验证码登录&#xff0c;去请求接口的情况&#xff0c;一是因为有时验证码会比较复杂&#xff0c;比如有些图形验证码&#xff0c;难以通过接口的方式去处理&#xff1b;再者&#xff0c…

【数据分享】2015-2023年我国区县逐月二手房房价数据(Excel/Shp格式)

房价是一个城市发展程度的重要体现&#xff0c;一个城市的房价越高通常代表这个城市越发达&#xff0c;对于人口的吸引力越大&#xff01;因此&#xff0c;房价数据是我们在各项城市研究中都非常常用的数据&#xff01;之前我们分享过2015-2023年我国地级市逐月房价数据&#x…

多表操作、其他字段和字段参数、django与ajax(回顾)

多表操作 1 基于对象的跨表查 子查询----》执行了两句sql&#xff0c;没有连表操作 2 基于双下滑线的连表查 一次查询&#xff0c;连表操作 3 正向和反向 放在ForeignKey,OneToOneField,ManyToManyField的-related_namebooks&#xff1a;双下滑线连表查询&#xff0c;反向…

13、pytest为失败的断言定义自己的解释

官方实例 # content of ocnftest.py from test_foocompare import Foodef pytest_assertrepr_compare(op, left, right):if isinstance(left, Foo) and isinstance(right, Foo) and op "":return["Comparing Foo instances:",f" vals:{left.val} !…

抖音集团面试挂在2面,复盘后,决定二战.....

先说下我基本情况&#xff0c;本科不是计算机专业&#xff0c;现在是学通信&#xff0c;然后做图像处理&#xff0c;可能面试官看我不是科班出身没有问太多计算机相关的问题&#xff0c;因为第一次找工作&#xff0c;字节的游戏专场又是最早开始的&#xff0c;就投递了&#xf…

用友NC FileUploadServlet 反序列化RCE漏洞复现

0x01 产品简介 用友 NC 是用友网络科技股份有限公司开发的一款大型企业数字化平台。 0x02 漏洞概述 用友 NC nc.file.pub.imple.FileUploadServlet 反序列化漏洞,攻击者可通过该漏洞在服务器端任意执行代码,写入后门,获取服务器权限,进而控制整个web服务器。 0x03 复现环…

什么是服务端渲染,SSR解决了什么问题

面试官&#xff1a;SSR解决了什么问题&#xff1f;有做过SSR吗&#xff1f;你是怎么做的&#xff1f; 一、是什么 Server-Side Rendering 我们称其为SSR&#xff0c;意为服务端渲染 指由服务侧完成页面的 HTML 结构拼接的页面处理技术&#xff0c;发送到浏览器&#xff0c;然…

OTFX欧汇提供更优质的外汇交易产品和服务

OTFX的外汇交易明智决策能力&#xff1a;准确捕捉外汇市场机会&#xff0c;实现稳定盈利 把握机遇&#xff0c;重要的是争取而不是等待。在金融市场中&#xff0c;明智的决策能力对于外汇交易成败至关重要。及时的断绝&#xff0c;果断的出手&#xff0c;才能够保证出手的成功…

【Flink】Flink核心概念简述

目录 一、Flink 简介二、Flink 组件栈1. API & Libraries 层2. runtime层3. 物理部署层 三、Flink 集群架构四、Flink基本编程模型五、Flink 的优点 一、Flink 简介 Apache Flink 的前身是柏林理工大学一个研究性项目&#xff0c; 在 2014 被 Apache 孵化器所接受&#xf…

【黑马甄选离线数仓day08_会员主题域开发】

1. 会员主题域需求说明 1.1 各类会员数量统计 说明&#xff1a;公司为了对不同会员进行不同的营销策略&#xff0c;对各类会员的数量都非常敏感&#xff0c;比如注册会员、消费会员、复购会员、活跃会员、沉睡会员。不仅需要看新增数量还要看累积数量。 指标&#xff1a;新增…

用chatGPT开发项目:我想的无人的智慧树网站 流量之神 利用人工智能的算法将人吸引住 GPT4是不是越来越难用了,问一下就要证明一下自己是不是人类

广度发散&#xff1a;让AI给出时代或今日或你关注的热点事件 比如采集新闻头条&#xff0c;根据内容或标题&#xff0c;以不同的角度&#xff0c;或各种人群的角色&#xff0c;生成50篇简短的文章。一下就能占传统的搜索引擎。这是AI最擅长的【千人千面&#xff0c;海量生成】…

开源CDN软件GoEdge —— 筑梦之路

官方网站&#xff1a;GoEdge CDN - 制作自己的CDN - GoEdge CDN | 自建CDN GoEdge是一款管理分布式CDN边缘节点的开源工具软件&#xff0c;目的是让用户轻松地、低成本地创建CDN/WAF等应用。 特性 免费 - 开源、免费、自由、开放 简单 - 架构简单清晰&#xff0c;安装简单&a…

Autosar MCAL-RH850P1HC Port配置

文章目录 PortPortGroupPortPin PortFilterGroupConfigDigital Filter Clock Selection0Digital Filter Clock Selection1 PortGeneralCritical Section ProtectionDev Error DetectDevice NameMax ModeSet Pin Default Modee ApiSet Pin Modee ApiSet to Dio Alt Modee ApiUse…

为什么要替换 Object.defineProperty?

目录 前言&#xff1a;为什么要替换 Object.defineProperty&#xff1f; 详解&#xff1a;为什么要替换 Object.defineProperty&#xff1f; 总结&#xff1a; 前言&#xff1a;为什么要替换 Object.defineProperty&#xff1f; JavaScript中的Object.defineProperty是一种…

个人Windows电脑通过Cloudreve+Cpolar搭建PHP云盘系统公网可访问

文章目录 1、前言2、本地网站搭建2.1 环境使用2.2 支持组件选择2.3 网页安装2.4 测试和使用2.5 问题解决 3、本地网页发布3.1 cpolar云端设置3.2 cpolar本地设置 4、公网访问测试5、结语 1、前言 自云存储概念兴起已经有段时间了&#xff0c;各互联网大厂也纷纷加入战局&#…

flask web开发学习之初识flask(二)

文章目录 一、创建程序实例并注册路由1. 为视图绑定绑定多个URL2. 动态URL 二、启动开发服务器1. 自动发现程序实例2. 管理环境变量3. 使用pycharm运行服务器4. 更多的启动选项5. 设置运行环境6. 调试器7. 重载器 一、创建程序实例并注册路由 app.py # 从flask包中导入flask类…

一次性客户的笔记总结

创建一次性客户&#xff0c;系统会给出一个客户编码&#xff1b; 每次记账的时候&#xff0c;在录入过账码及客户编码后&#xff0c;点击回车&#xff0c;都需要录入这个客户的详细信息&#xff08;比如 客户名称等&#xff09; 一次性客户的信息存储在BSEC表中&#xff0c;这种…
最新文章