【RTOS学习】模拟实现任务切换 | 寄存器和栈的变化
🐱作者:一只大喵咪1201
🐱专栏:《RTOS学习》
🔥格言:你只管努力,剩下的交给时间!
目录
- 🏀认识任务切换
- 🏐切换的实质
- 🏐栈中的内容
- 🏐切换过程
- 🏀实现任务切换
- 🏐伪造现场
- 🏐启动任务
- 🏐切换任务
- 🏀栈和寄存器变化
- 🏐创建任务时
- 🏐任务启动时
- 🏐任务切换时
- 🏀总结
🏀认识任务切换
🏐切换的实质
如上图所示代码,定义两个任务函数task_a
和task_b
,在mymian
函数中调用这两个函数,在调用的时候传入不同的参数。在任务函数中,打印出自己的函数名称后便开始死循环打印各自形参接收到的字符串。
如上图所示,在调用task_a
以后,该函数在它的栈中运行,局部变量保存在栈中,在其内部调用的puts
和putchar
函数也会有自己的栈,这两个函数的栈紧挨着task_a
的栈。
假设能执行到task_b
函数,该函数也有一个栈,进行和上面相同的操作。
- 每一个函数都对应着一个自己的栈。
而FreeRTOS执行的任务也是函数,它们也有自己的栈,每一个任务就对应着一个自己的栈。
裸机程序中,函数在执行的过程中,使用的是函数自己的栈中的内容,各自的操作也是在自己的栈中完成,包括数据的保存,修改等等。
FreeRTOS中不同任务在执行的时候,也是使用任务函数自己栈中的内容,各自的操作也是在自己的栈中完成的。
- 任务切换的本质,就是切换不同任务的栈让CPU来操作。
- 任务切换其实就是在切换栈。
🏐栈中的内容
我们知道,FreeRTOS中任务的切换是在SysTick_Handler
中断中完成的,也就是说在该中断函数中完成了栈的切换。
首先要知道的就是,在切换栈的时候,栈中有什么!!!
如上图所示,便是在SysTick_Handler
中切换任务时,当前任务栈中的内容,包含R0~R3,R12,LR,返回地址,xPSR
以及R4~R11
这些寄存器中的内容。
在学习中断的保存现场时候本喵讲解过:
- 在产生中断,调用中断服务函数之前,硬件保存
R0~R3,R12,LR,返回地址,xPSR
这些寄存器中的数据到栈中。 - 在中断服务函数中,软件保存
R4~R11
这些寄存器中的数据到栈中。
硬件保存不用我们管,软件保存就需要代码来实现了,但是我们在写中断服务函数的时候,从来也没有写过保存R4~R11
寄存器的代码,这是因为这部分代码由编译器替我们完成了。
- 如果中断函数或者普通函数会使用到
R4~R11
寄存器,那么在一进入函数时需要先保存这些寄存器中的值到栈中,调用完毕时再将原本的值恢复到寄存器中。
🏐切换过程
如上图所示,任务A执行一定时间后产生了Tick
中断,在中断函数中切换成了任务B,随后开始执行任务B,执行一定时间后再次在Tick
中断函数中切换成任务A,如此反复。
关键就在于SysTick_Handler
中断服务函数中到底做了什么:
- 暂停任务A:保存任务A的现场
R0~R3,R12,LR,返回地址,xPSR
由硬件保存到任务A的栈中R4~R11
由软件保存到任务A的栈中
- 运行任务B:恢复任务B的现场
- 任务B栈中的
R4~R11
寄存器值由软件恢复到寄存器中 - 任务B栈中的
R0~R3,R12,LR,返回地址,xPSR
寄存器值由硬件恢复到寄存器中。
- 任务B栈中的
🏀实现任务切换
🏐伪造现场
继续看这张图,任务A和B的切换发生在Tick
中断函数中,在切换任务执行A的时候,需要恢复任务A的现场,但是在任务A第一次运行的时候,它的现场哪里来的呢?
在第一次执行任务A时,它的现场并不存在,因为没有在执行任务A期间发生过Tick
中断,所以任务A的栈中没有硬件保存的R0~R3,R12,LR,返回地址,xPSR
寄存器值,也没有软件保存过R4~R11
寄存器的值。
- 所以,需要在创建任务的时候伪造一个该任务的现场,在第一次执行该任务的时候,有现场可恢复
如上图所示代码,定义创建任务函数create_task
,该函数的第一个参数就是要执行的任务函数,所以使用typedef void(*task_function)(void* param)
重名名任务函数指针为task_function
,方便后面使用。
在创建任务的函数create_task
中伪造该任务现场时,需要的参数有:
- 要执行的任务函数指针
f
- 传给任务函数的参数
prama
- 属于该任务的栈
stack
- 以及栈的大小
stack_len
- 这里本喵仅实现静态任务创建,由我们自己指定任务的栈。
在函数内部,由于形参stack
指向的是栈的起始地址,也就是这段内存的最低地址,但是栈是从高地址向低地址向下生长的,所以需要得到栈顶的位置top
,因为存放的16个32位的寄存器值,所以(stack + stack_len)
得到的就是栈顶的地址。
下一步就是真正的伪造现场,用值填充这个栈,由于高地址存放的是高编号寄存器中的值这个规则,所以我们从栈底开始依次存放数据。
- 先存放
R4~R11
- 再存放
R0~R3,R12,LR,返回地址,xPSR
存放过程中,必须严格按照前面讲解的SysTick_Handler
中断函数中栈中的内容顺序存放。
对于R4~R11
,任务第一次启动时并不关心它里面的内容是什么,所以全部设置为0,R0
是用来传递任务函数的参数param
的,不能设置为0,R1~R3,R12
也无所谓,全部设置为0。
对于LR
,由于该任务是第一次运行,它必然不会是被其他函数调用的,所以无所谓返回地址,也设置为0。
对于返回地址,这才是Tick
中断函数第一次启动任务后,退出Tick
中断函数时真正的返回地址。将任务函数的地址放在这里,任务启动并退出中断函数后就会执行相应的任务函数,所以这里放入任务函数的指针f
。
对于状态寄存器xPSR
,虽然任务是第一次执行,之前没有任何状态存在,但是它不能是0,必须将它的第24位置1:
如上图所示,程序状态寄存器xPSR
中的第24位T
,该位为1表示使用Thumb
指令集,为0表示使用ARM
指令集,而本喵使用的Cortex-M3
只能使用Thumb
指令集,所以该位必须是1。
现场伪造完毕以后,需要将该任务的栈记录下来,方便下次切换时能够找到该任务的栈:
- 创建全局数组
task_stacks
存放每个任务的栈顶 - 使用
task_count
计数任务个数
如上图所示,创建两个大小为1024字节的char
类型静态全局数组stack_a
和stack_b
,这两个数组就是两个任务的栈。
- 在常规创建数组语句的后面加上
__attribute__ ((aligned (4)))
表示让该数组在内存中要4字节对齐。
如果不要求4字节对齐的话,在将该数组作为任务的栈时,由于CPU是32位处理器,所以在访问栈时可能由于不是4字节对齐而出现读取或者写入错误,导致程序出现问题。
- 如此,在创建任务的时候就完成了现场伪造。
🏐启动任务
如上图代码所示,在创建好两个任务之后,任务的启动也是在Tick
中断函数中,该中断函数本喵定义成一个汇编函数SysTick_Handler_asm
。
在Tick
中断产生后,硬件会调用SysTick_Handler_asm
中断服务函数,在一进入中断函数时,硬件已经完成了R0~R3,R12,LR,返回地址,xPSR
寄存器的保存,将这些寄存器中的值保存到了栈中。
但是R4~R11
寄存器中的值硬件并不管,需要软件完成,所以在一进入中断函数时,就使用STMDB SP!, {R4 - R11}
汇编指令将R4~R11
的值保存到栈中。
由于是中断函数,所以此时LR
寄存器中的值并不是返回地址,而是那个特殊值,由于在后面会使用BL SysTick_Handler
调用C函数来实现任务栈的切换,会改变LR
中的值,所以这里要将此时LR
中的特殊值也保存到栈中。
将LR
中的特殊值赋给R0
进行传参,将真正栈(不包括栈中的LR
)赋值给R1
作为另一个参数进行传参。
- 在任务没启动时,现场保护所操作的栈并不是我们指定的任务栈,而默认的栈。
如上图代码所示,这是Tick
中断中调用的回调C函数SysTick_Handler
,在函数内部,先调用is_task_running()
判断任务有没有创建好,如果没有创建好就直接返回,此时这仅仅是一次普通的Tick
中断。
如果任务创建好了,根据cur_task
当前任务的值判断这是不是第一次启动任务,如果该值是-1,说明这是第一次启动任务,调用get_stack
从记录任务栈的数组中得到第一个任务的栈,然后调用StartTask_asm
函数将该任务的栈切换过来,开始执行。
- 调用
StartTask_asm
传的参数中,stack
是指定要切换的栈,lr_bak
是特殊值,是为了软件恢复完毕后触发硬件恢复。
如上图所示代码,StartTask_asm
函数本喵同样定义成了一个汇编代码,在一进入该函数时,就将存放在要启动任务栈中的R4~R11
值恢复到对应的寄存器中。
R0
寄存器中的值是在调用StartTask_asm
时传过来的,表示要启动任务的栈。
软件恢复完毕后,此时的R0
指向栈中存放硬件要恢复的R0
值所在的位置,所以使用MSR MSP, R0
将该栈指针赋值给MSP
,让真正的栈顶指针SP
来管理这部分栈。
然后使用BX R1
跳转,由于R1
中的是一个特殊值,所以触发了硬件恢复,将栈中剩下的R0~R3,R12,LR,返回地址,xPSR
值恢复到了对应的寄存器中。
- 由于此时恢复的现场是我们伪造出来的,所以返回地址是该任务要执行的任务函数。
此时第一个任务就执行起来了。
补充用到的功能函数:
如上图所示是用到的功能函数,这些函数都放在task.c
文件中。启动任务只是将全局变量标志task_running
置一,然后陷入死循环,此时在发生Tick
中断时,通过该标志就可以知道任务是否创建成功,因为任务没有创建的时候,也有可能发生Tick
中断。
🏐切换任务
如上图所示,在切换任务时,仍然是在Tick
中进行的,在调用SysTick_Handler
回调C函数时,已经完成了现场保护,在C函数中执行的是红色框中的代码。
任务切换时,说明在前面已经有任务在运行了,得到前面的任务,再调用get_next_task()
得到要切换的新任务,然后判断这是否只有一个任务,如果新任务和前一个任务相同的话,就说明只有一个任务,此时直接返回就可以,现场保存和现场恢复等操作都发生在这个任务上。
不止一个任务时,使用set_task_stack()
将前一个任务的栈继续保存到记录任务栈的数组中,因为前一个任务在运行过程中栈会变化,然后获取新任务的栈,再更新当前任务的下标cur_task
,最后调用StartTask_asm
来切换任务。
StartTask_asm
中的操作和启动第一个任务时一样,也是进行软件恢复和硬件恢复,然后返回到新任务函数处执行,此时就完成了任务的切换。
如此一来,两个任务就可以交替执行了。
将SysTick
定时器的超时时间设置成1ms,此时每隔1ms就会发生一次任务切换,两个任务交替执行。
在Tick
中断发生时,硬件会保存当前任务的R0~R3,R12,LR,返回地址,xPSR
寄存器的值到当前任务的栈中,其中返回地址就是发生中断时那条指令的下一条地址。
当任务再次被切换回来以后,会将返回地址赋值给PC,该任务就会接着被切换走的位置继续执行。
- 只有第一次启动任务和第一次被切换上来的任务,返回地址是任务函数的地址,任务从函数的起始位置执行。
如上图所示,在创建两个任务a和b时,传给任务函数的参数分别是a和b,运行起来后,可以看到串口中字符a和b在交替打印。
如上图所示,再创建一个task_c
任务来进行一些计算,并且打印计算结果。
如上图所示,此时串口中打印出来的结果有字符a和b,还有计算任务计算的结果,三个任务在同时运行,也完美的实现了任务之间的切换。
🏀栈和寄存器变化
现在本喵已经自己实现了多任务之间的切换,下面来看看每个过程中栈和寄存器的变化。
🏐创建任务时
创建任务的时候,在create_task
中伪造了现场:
如上图所示,严格按照寄存器在栈中的存放顺序伪造好了现场,这个栈是我们在创建任务时给该任务指定的栈,也就是那个全局数组。
下面来看看,创建任务时,真正内存中的值和我们分析的是否一致:
如上图所示,打开该工程的反汇编文件,找到stack_a
全局数组所在的地址是0x2000000c
,本喵这里仅带大家看一个任务a的创建。
如上图所示,首先要根据stack_a
全局数组的地址计算出任务A栈的起始地址0x2000000C + 1024 = 0x2000040C
,又因为伪造现场时栈中存放16个寄存器的值,所以0x2000040C + 16 * 4 = 0x200003CC
得到就是伪造完现场后,任务A栈的SP
所在位置。
将SP
所指位置在调试过程中查看该位置的内存,可以清楚的看到,从0x200003CC
处开始向高地址增长的内存中,存放着我们要伪造的16个寄存器数据。
- 伪造的
R0
处存放的值是0x00000061
,这是任务A函数的形参,也就是字符a
的ASCII码。- 伪造的
返回地址
处存放的值是0x0800054D
,这是任务A要执行函数的入口地址。
如上图所示,从反汇编文件中查找0x0800054C
,发现这是task
任务函数的入口地址。之所以在内存中存放的是0x0800054C + 1 = 0x0800054D
,是因为最低位为1表示该函数使用的是Thumb
指令集。
🏐任务启动时
如上图所示,在SysTick_Handler_asm
中断函数中打断点,让程序在启动任务时第一次进入Tick
中断中完成软件保存后停下来。
从此时寄存器中的值可以看到SP = 0x2000FFBC
,此时操作的栈就是这里,红色框中内存里的值是软件将对应寄存器中的值保存到栈中,蓝色框中是在产生Tick
中断时,硬件将对应寄存器中的值保存到栈中。此时完成了现场保护。
- 此时的栈
0x2000FFBC
和我们指定的任务A的栈0x2000000C
相差甚远,所以必然不是一个栈。
所以第一次启动任务过程中,Tick
中断产生后,保存现场发生在程序启动时默认的栈中,不属于任何一个任务的栈。
如上图,在Tcik
中断函数中完成现场保护以后会调用回调函数SysTick_Handler
,在调回调函数中,获取到了任务A的栈stack = 0x200003CC
,和我们前面计算出来的结果相符。
如上图所示,回调函数的又调用StartTask_asm
汇编函数来完成现场恢复,在调用汇编函数时,传入的参数stack = 0x200003CC
, lr_bak = 特殊值
。
在汇编函数中,首先把任务A栈中伪造的R4~R11
的值通过软件恢复到对应的寄存器中,此时R0
寄存器指向任务A栈里伪造的R0
处,硬件恢复就从这里开始。
- 然后使用
MSR MSP, R0
将任务A的栈交给了SP
寄存器管理。
此时我们伪造的任务A栈里的现场仅剩下R0~R3,R12,LR,返回地址,xPSR
的值等待硬件恢复了。
如上图所示,在StartTask_asm
函数中完成软件恢复以后,用指令BX R1
触发硬件恢复,此时R1
中的值是由回调函数传递过来的特殊值。
硬件恢复完成以后,可以看到,程序从我们伪造的返回地址0x0800054D
处开始执行,也就是任务A函数task
,而且此时寄存器SP = 0x20000040C
,该值是前面我们算出来的任务A栈的起始顶部位置。
- 此时任务A在执行过程中,操作的就是属于该任务的栈了,完成了栈的切换。
🏐任务切换时
如上图所示,在任务A启动以后执行的过程中,再次产生了Tick
中断,打断点让程序停止在中断函数中。
在断点位置已经完成了现场保存,红色框中的部分是软件将LR,R4~R11
寄存器中的值保存到栈中,蓝色框中的部分是硬件将R0~R3,R12,LR,返回地址,xPSR
保存到栈中。
SP = 0x200003C4
,该值位于任务A的栈0x2000000C~0x2000040C
之间。
所以说现场保存发生在任务A的栈中,此时就保存了任务A的现场。
如上图所示,中断函数再次调用了回调C函数SysTick_Handler
,在该函数中,得到了任务B的栈stack = 0x200007CC
,从内存窗口中可以看到任务B栈里存放的是创建任务B时伪造的现场。
- 因为任务B是第一次被执行,所以恢复的是创建任务时伪造出来的现场。
可以看到,任务B栈中的返回地址也是0x0800054D
,这是因为任务A和任务B执行的是一个任务函数。
如上图,回调函数再次调用了StartTask_asm
函数来完成任务B的现场恢复,在该函数中,软件先恢复任务B栈中R0~R11
的值到对应寄存器中,然后将此时的R0 = 0x200007EC
赋值给SP
,等待硬件从这里开始恢复剩下的R0~R3,R12,LR,返回地址,xPSR
。
如上图,在StartTask_asm
中使用BX R1
触发硬件恢复以后,蓝色框中的R0~R3,R12,LR,返回地址,xPSR
由硬件恢复到了对应的寄存器中,而且此时SP = 0x2000080C
,这是任务B栈的栈顶位置。
- 此后任务B在执行过程中使用的就是它自己的栈。
此时就恢复了任务B的现场,当再次产生Tick
中断时,就会保存任务B的现场,恢复任务A的现场,如此往复就实现了两个任务之间的切换。
🏀总结
实时操作系统中最重要的就是多任务之间的切换,在这里我们自己动手实现了一遍,对任务的切换有了一个更深的认识。
要时刻记住,任务切换的本质就是栈的切换,任务创建的本质就是伪造现场。
相关文章:

【RTOS学习】模拟实现任务切换 | 寄存器和栈的变化
🐱作者:一只大喵咪1201 🐱专栏:《RTOS学习》 🔥格言:你只管努力,剩下的交给时间! 目录 🏀认识任务切换🏐切换的实质🏐栈中的内容🏐切…...
1.2 轻量级数据交互格式–JSON
对于接口来说,数据交互大部分都是使用的JSON格式,我们这里说的数据,就是我们上一章里讲解HTTP协议的时候,HTTP协议结构里的实体,也就是放在body里。body里存放需要传输的数据,数据是JSON格式,然后通过HTTP协议来传输给接口,接口再以同样的方式给我们返回。理解了这一层…...
charCodeAt() 方法
charCodeAt() 是 JavaScript 中用于获取字符串指定位置字符的 Unicode 编码的方法 语法如下: str.charCodeAt(index) str:要获取字符的字符串。index:要获取的字符在字符串中的索引。返回值是一个表示给定索引处字符 Unicode 编码的整数。…...
Flask中redis的配置与使用
注意点: 在__init__.py中需要将redis_store设置成全局变量,这样方便其他文件导入 一、config.py import logging import os from datetime import timedeltafrom redis import StrictRedisclass Config:# 调试信息DEBUG TrueSECRET_KEY os.urandom(3…...
生产者与消费者模型
初识linux之线程同步与生产者消费者模型_生产者线程和消费者线程-CSDN博客 Linux线程(三)—— 多线程(生产者消费者模型、信号量、线程池)-CSDN博客...

透析回溯的模板
关卡名 认识回溯思想 我会了✔️ 内容 1.复习递归和N叉树,理解相关代码是如何实现的 ✔️ 2.理解回溯到底怎么回事 ✔️ 3.掌握如何使用回溯来解决二叉树的路径问题 ✔️ 回溯可以视为递归的拓展,很多思想和解法都与递归密切相关,在很多…...

浅谈web性能测试
什么是性能测试? web性能应该注意些什么? 性能测试,简而言之就是模仿用户对一个系统进行大批量的操作,得出系统各项性能指标和性能瓶颈,并从中发现存在的问题,通过多方协助调优的过程。而web端的性能测试…...

Qt 容器QGroupBox带有标题的组框框架
控件简介 QGroupBox 小部件提供一个带有标题的组框框架。一般与一组或者是同类型的部件一起使用。教你会用,怎么用的强大就靠你了靓仔、靓妹。 用法示例 例 qgroupbox,组框示例(难度:简单),使用 3 个 QRadioButton 单选框按钮,与QVBoxLayout(垂直布局)来展示组框的…...

Linux系统解决“Key was rejected by service”
Linux系统下加载驱动模块出现如上错误提示的原因为:此驱动未经过签名。 方法一、关闭Secure Boot 如果是物理机,需要开机进入BIOS,找到“Secure Boot”的选项,然后关闭。 如果是虚拟机,可以打开虚拟设置,…...
【C++ Primer Plus学习记录】字符函数库cctype
C从C语言继承了一个与字符相关的、非常方便的函数软件包,它可以简化诸如确定字符是否为大写字母、数字、标点符号等工作,这些函数的原型是在头文件cctype中定义的。 cctype中的字符函数 函数名称返回值isalnum()如果参数是字母或数字,该函数返…...

C# WebSocket简单使用
文章目录 前言Fleck调试工具初始化简单使用 前言 最近接到了一个需求,需要网页实现上位机的功能。那就对数据传输的实时性要求很高。那就只能用WebSocket了。这里简单说一下我的WebSocket如何搭建 Fleck C# WebSocket(Fleck) 客户端:html Winfrom Fleck Github官网…...

uni-app 一些实用的页面模板
时间倒计时 <!-- 时间倒计时 --> <template><view class"container"><view class"flex-row time-box"><view class"time-item">{{ laveTimeList[0] }}</view><text>天</text><view class&qu…...

STM32——震动传感器点亮LED灯
震动传感器简单介绍 若产品不震动,模块上的 DO 口输出高电平; 若产品震动,模块上的 DO 口输出低电平,D0-LED绿色指示灯亮。 震动传感器与STM32的接线 编程实现 需求:当震动传感器接收到震动信号时,使用中断…...

使用 Timm 库替换 YOLOv8 主干网络 | 1000+ 主干融合YOLOv8
文章目录 前言版本差异说明替换方法parse_moedl( ) 方法_predict_once( ) 方法修改 yaml ,加载主干论文引用timm 是一个包含最先进计算机视觉模型、层、工具、优化器、调度器、数据加载器、数据增强和训练/评估脚本的库。 该库内置了 700 多个预训练模型,并且设计灵活易用。…...
PHP中什么是闭包(Closure)?
在PHP中,闭包(Closure)是一种匿名函数,它可以作为变量传递、作为参数传递给其他函数,或者被作为函数的返回值。闭包可以在定义时捕获上下文中的变量,并在以后的执行中使用这些变量。闭包在处理回调函数、事…...
boost::graph学习
boost::graph API简单小结 boost::graph是boost为图算法提供的API,简单易用。 API说明 boost::add_vertex 创建一个顶点。 boost::add_edge 创建一条边。 boost::edges 获取所有的边。 boost::vertices 获取所有的顶点。 graph.operator[vertex_descriptor] 获…...

【C语言:动态内存管理】
文章目录 前言1.malloc2.free3.calloc4.realloc5.动态内存常见错误6.动态内存经典笔试题分析7.柔性数组8.C/C中的内存区域划分 前言 文章的标题是动态内存管理,那什么是动态内存管理?为什么有动态内存管理呢? 回顾一下以前学的知识ÿ…...
【Python基础】迭代器
文章目录 [toc]什么是迭代可迭代对象判断数据类型是否是可迭代类型 迭代器对可迭代对象进行迭代的本质获取可迭代对象的迭代器通过迭代器获取数据StopIteration异常 自定义迭代器__iter__()方法__next__()方法判断数据类型是否是可迭代类型自定义迭代器案例分离模式整合模式 fo…...

QVTK 可视化
#ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow>#include <vtkNew.h> // 智能指针 #include <QVTKOpenGLNativeWidget.h> #include <vtkCylinderSource.h> // 圆柱#include <vtkPolyDataMapper.h&g…...

【初阶C++】入门(超详解)
C入门 前言1. C关键字(C98)2. 命名空间2.1 命名空间定义2.2 命名空间使用2.3嵌套命名空间 3. C输入&输出4. 缺省参数4.1 缺省参数概念4.2 缺省参数分类 5. 函数重载5.1 函数重载概念5.2 C支持函数重载的原理--名字修饰(name Mangling) 6. 引用6.1 引用概念6.2 引用特性6.3 …...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...
Admin.Net中的消息通信SignalR解释
定义集线器接口 IOnlineUserHub public interface IOnlineUserHub {/// 在线用户列表Task OnlineUserList(OnlineUserList context);/// 强制下线Task ForceOffline(object context);/// 发布站内消息Task PublicNotice(SysNotice context);/// 接收消息Task ReceiveMessage(…...

Mac软件卸载指南,简单易懂!
刚和Adobe分手,它却总在Library里给你写"回忆录"?卸载的Final Cut Pro像电子幽灵般阴魂不散?总是会有残留文件,别慌!这份Mac软件卸载指南,将用最硬核的方式教你"数字分手术"࿰…...
【决胜公务员考试】求职OMG——见面课测验1
2025最新版!!!6.8截至答题,大家注意呀! 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:( B ) A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...

(转)什么是DockerCompose?它有什么作用?
一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器。 Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...

USB Over IP专用硬件的5个特点
USB over IP技术通过将USB协议数据封装在标准TCP/IP网络数据包中,从根本上改变了USB连接。这允许客户端通过局域网或广域网远程访问和控制物理连接到服务器的USB设备(如专用硬件设备),从而消除了直接物理连接的需要。USB over IP的…...
IP如何挑?2025年海外专线IP如何购买?
你花了时间和预算买了IP,结果IP质量不佳,项目效率低下不说,还可能带来莫名的网络问题,是不是太闹心了?尤其是在面对海外专线IP时,到底怎么才能买到适合自己的呢?所以,挑IP绝对是个技…...

elementUI点击浏览table所选行数据查看文档
项目场景: table按照要求特定的数据变成按钮可以点击 解决方案: <el-table-columnprop"mlname"label"名称"align"center"width"180"><template slot-scope"scope"><el-buttonv-if&qu…...

MyBatis中关于缓存的理解
MyBatis缓存 MyBatis系统当中默认定义两级缓存:一级缓存、二级缓存 默认情况下,只有一级缓存开启(sqlSession级别的缓存)二级缓存需要手动开启配置,需要局域namespace级别的缓存 一级缓存(本地缓存&#…...
华为OD最新机试真题-数组组成的最小数字-OD统一考试(B卷)
题目描述 给定一个整型数组,请从该数组中选择3个元素 组成最小数字并输出 (如果数组长度小于3,则选择数组中所有元素来组成最小数字)。 输入描述 行用半角逗号分割的字符串记录的整型数组,0<数组长度<= 100,0<整数的取值范围<= 10000。 输出描述 由3个元素组成…...