鸿蒙内核源码分析(信号消费篇) | 谁让CPU连续四次换栈运行

本篇有相当的难度,涉及用户栈和内核栈的两轮切换,CPU四次换栈,寄存器改值,将围绕下图来说明.

解读

  • 为本篇理解方便,把图做简化标签说明:
    • user:用户空间
    • kernel:内核空间
    • source(…):源函数
    • sighandle(…):信号处理函数,
    • syscall(…):系统调用,参数为系统调用号,如sigreturn,N(表任意)
    • user.source():表示在用户空间运行的源函数
  • 系列篇已多次说过,用户态的任务有两个运行栈,一个是用户栈,一个是内核栈.栈空间分别来自用户空间和内核空间.两种空间是有严格的地址划分的,通过虚拟地址的大小就能判断出是用户空间还是内核空间.系统调用本质上是软中断,它使CPU执行指令的场地由用户栈变成内核栈.怎么变的并不复杂,就是改变(sp和cpsr寄存器的值).sp指向哪个栈就代表在哪个栈运行, 当cpu在用户栈运行时是不能访问内核空间的,但内核态任务可以访问整个空间,而且内核态任务没有用户栈.
  • 理解了上面的说明,再来说下正常系统调用流程是这样的: user.source() -> kernel.syscall(N) - > user.source() ,想要回到user.source()继续运行,就必须保存用户栈现场各寄存器的值.这些值保存在内核栈中,恢复也是从内核栈恢复.
  • 信号消费的过程的上图可简化表示为: user.source() -> kernel.syscall(N) ->user.sighandle() ->kernel.syscall(sigreturn) -> user.source() 在原本要回到user.source()的中间插入了信号处理函数的调用. 这正是本篇要通过代码来说清楚的核心问题.
  • 顺着这个思路可以推到以下几点,实际也是这么做的:
    • kernel.syscall(N) 中必须要再次保存user.source()的上下文sig_switch_context,为何已经保存了一次还要再保存一次?
    • 因为第一次是保存在内核栈中,而内核栈这部分数据会因回到用户态user.sighandle()运行而被恢复现场出栈了.保存现场/恢复现场是成双出队的好基友,注意有些文章说会把整个内核栈清空,这是不对的.
    • 第二次保存在任务结构体中,任务来源于任务池,是内核全局变量,常驻内存的.两次保存的都是user.source()运行时现场信息,再回顾下相关的结构体.关键是sig_switch_context
typedef struct {
    // ...
    sig_cb  sig;//信号控制块,用于异步通信
} LosTaskCB;
typedef struct {//信号控制块(描述符)
    sigset_t sigFlag;		//不屏蔽的信号集
    sigset_t sigPendFlag;	//信号阻塞标签集,记录那些信号来过,任务依然阻塞的集合.即:这些信号不能唤醒任务
    sigset_t sigprocmask; /* Signals that are blocked            */	//任务屏蔽了哪些信号
    sq_queue_t sigactionq;	//信号捕捉队列					
    LOS_DL_LIST waitList;	//等待链表,上面挂的是等待信号到来的任务, 请查找 OsTaskWait(&sigcb->waitList, timeout, TRUE)	理解						
    sigset_t sigwaitmask; /* Waiting for pending signals         */	//任务在等待哪些信号的到来
    siginfo_t sigunbinfo; /* Signal info when task unblocked     */	//任务解锁时的信号信息
    sig_switch_context context;	//信号切换上下文, 用于保存切换现场, 比如发生系统调用时的返回,涉及同一个任务的两个栈进行切换			
} sig_cb;
  • 还必须要改变原有PC/R0/R1寄存器的值.想要执行user.sighandle(),PC寄存器就必须指向它,而R0,R1就是它的参数.
  • 信号处理完成后须回到内核态,怎么再次陷入内核态? 答案是:__NR_sigreturn,这也是个系统调用.回来后还原sig_switch_context,即还原user.source()被打断时SP/PC等寄存器的值,使其跳回到用户栈从user.source()的被打断处继续执行.
  • 有了这三个推论,再理解下面的代码就是吹灰之力了,涉及三个关键函数 OsArmA32SyscallHandleOsSaveSignalContextOsRestorSignalContext本篇一一解读,彻底挖透.先看信号上下文结构体sig_switch_context.

sig_switch_context

//任务中断上下文
#define TASK_IRQ_CONTEXT \
        unsigned int R0;     \
        unsigned int R1;     \
        unsigned int R2;     \
        unsigned int R3;     \
        unsigned int R12;    \
        unsigned int USP;    \
        unsigned int ULR;    \
        unsigned int CPSR;   \
        unsigned int PC;

typedef struct {//信号切换上下文
    TASK_IRQ_CONTEXT
    unsigned int R7;	//存放系统调用的ID
    unsigned int count;	//记录是否保存了信号上下文
} sig_switch_context;
  • 保存user.source()现场的结构体,USPULR代表用户栈指针和返回地址.
  • CPSR寄存器用于设置CPU的工作模式,CPU有7种工作模式, ​谈论的用户态(usr普通用户)和内核态(sys超级用户)对应的只是其中的两种.二者都共用相同的寄存器.还原它就是告诉CPU内核已切到普通用户模式运行.
  • 其他寄存器没有保存的原因是系统调用不会用到它们,所以不需要保存.
  • R7是在系统调用发生时用于记录系统调用号,在信号处理过程中,R0将获得信号编号,作为user.sighandle()的第一个参数.
  • count记录是否保存了信号上下文

OsArmA32SyscallHandle 系统调用总入口

/* The SYSCALL ID is in R7 on entry.  Parameters follow in R0..R6 */
/******************************************************************
由汇编调用,见于 los_hw_exc.s    / BLX    OsArmA32SyscallHandle
SYSCALL是产生系统调用时触发的信号,R7寄存器存放具体的系统调用ID,也叫系统调用号
regs:参数就是所有寄存器
注意:本函数在用户态和内核态下都可能被调用到
//MOV     R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数
******************************************************************/
LITE_OS_SEC_TEXT UINT32 *OsArmA32SyscallHandle(UINT32 *regs)
{
   ​UINT32 ret;
   ​UINT8 nArgs;
   ​UINTPTR handle;
   ​UINT32 cmd = regs[REG_R7];//C7寄存器记录了触发了具体哪个系统调用

   ​if (cmd >= SYS_CALL_NUM) {//系统调用的总数
       ​PRINT_ERR("Syscall ID: error %d !!!\n", cmd);
       ​return regs;
   ​}
	//用户进程信号处理函数完成后的系统调用 svc 119 #__NR_sigreturn
   ​if (cmd == __NR_sigreturn) {
       ​OsRestorSignalContext(regs);//恢复信号上下文,回到用户栈运行.
       ​return regs;
   ​}

   ​handle = g_syscallHandle[cmd];//拿到系统调用的注册函数,类似 SysRead 
   ​nArgs = g_syscallNArgs[cmd / NARG_PER_BYTE]; /* 4bit per nargs */
   ​nArgs = (cmd & 1) ? (nArgs >> NARG_BITS) : (nArgs & NARG_MASK);//获取参数个数
   ​if ((handle == 0) || (nArgs > ARG_NUM_7)) {//系统调用必须有参数且参数不能大于8个
       ​PRINT_ERR("Unsupport syscall ID: %d nArgs: %d\n", cmd, nArgs);
       ​regs[REG_R0] = -ENOSYS;
       ​return regs;
   ​}
	//regs[0-6] 记录系统调用的参数,这也是由R7寄存器保存系统调用号的原因
   ​switch (nArgs) {//参数的个数 
       ​case ARG_NUM_0:
       ​case ARG_NUM_1:
           ​ret = (*(SyscallFun1)handle)(regs[REG_R0]);//执行系统调用,类似 SysUnlink(pathname);
           ​break;
       ​case ARG_NUM_2://如何是两个参数的系统调用,这里传三个参数也没有问题,因被调用函数不会去取用R2值
       ​case ARG_NUM_3:
           ​ret = (*(SyscallFun3)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2]);//类似 SysExecve(fileName, argv, envp);
           ​break;
       ​case ARG_NUM_4:
       ​case ARG_NUM_5:
           ​ret = (*(SyscallFun5)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3],
                                        ​regs[REG_R4]);
           ​break;
       ​default:	//7个参数的情况
           ​ret = (*(SyscallFun7)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3],
                                        ​regs[REG_R4], regs[REG_R5], regs[REG_R6]);
   ​}

   ​regs[REG_R0] = ret;//R0保存系统调用返回值
   ​OsSaveSignalContext(regs);//如果有信号要处理,将改写pc,r0,r1寄存器,改变返回正常用户态路径,而先去执行信号处理程序.

   ​/* Return the last value of curent_regs.  This supports context switches on return from the exception.
    ​* That capability is only used with the SYS_context_switch system call.
    ​*/
   ​return regs;//返回寄存器的值
}

解读

  • 这是系统调用的总入口,所有的系统调用都要跑这里要统一处理.通过系统号(保存在R7),找到注册函数并回调.完成系统调用过程.
  • OsArmA32SyscallHandle总体理解起来是被信号的保存和还原两个函数给包夹了.注意要在运行过程中去理解调用两个函数的过程,对于同一个任务来说,一定是先执行OsSaveSignalContext,第二次进入OsArmA32SyscallHandle后再执行OsRestorSignalContext.
  • OsSaveSignalContext,由它负责保存user.source() 的上下文,其中改变了sp,r0/r1寄存器值,切到信号处理函数user.sighandle()运行.
  • 在函数的开头,碰到系统调用号__NR_sigreturn,直接恢复信号上下文就退出了,因为这是要切回user.source()继续运行的操作.
//用户进程信号处理函数完成后的系统调用 svc 119 #__NR_sigreturn
if (cmd == __NR_sigreturn) {
   ​OsRestorSignalContext(regs);//恢复信号上下文,回到用户栈运行.
   ​return regs;
}

OsSaveSignalContext 保存信号上下文

有了上面的铺垫,就不难理解这个函数的作用.

/**********************************************
产生系统调用时,也就是软中断时,保存用户栈寄存器现场信息
改写PC寄存器的值
**********************************************/
void OsSaveSignalContext(unsigned int *sp)
{
   ​UINTPTR sigHandler;
   ​UINT32 intSave;
   ​LosTaskCB *task = NULL;
   ​LosProcessCB *process = NULL;
   ​sig_cb *sigcb = NULL;
   ​unsigned long cpsr;

   ​OS_RETURN_IF_VOID(sp == NULL);
   ​cpsr = OS_SYSCALL_GET_CPSR(sp);//获取系统调用时的 CPSR值
   ​OS_RETURN_IF_VOID(((cpsr & CPSR_MASK_MODE) != CPSR_USER_MODE));//必须工作在CPU的用户模式下,注意CPSR_USER_MODE(cpu层面)和OS_USER_MODE(系统层面)是两码事.
   ​SCHEDULER_LOCK(intSave);//如有不明白前往 https://my.oschina.net/weharmony 翻看工作模式/信号分发/信号处理篇
   ​task = OsCurrTaskGet();
   ​process = OsCurrProcessGet();
   ​sigcb = &task->sig;//获取任务的信号控制块
	//1.未保存任务上下文任务
	//2.任何的信号标签集不为空或者进程有信号要处理
   ​if ((sigcb->context.count == 0) && ((sigcb->sigFlag != 0) || (process->sigShare != 0))) {
       ​sigHandler = OsGetSigHandler();//获取信号处理函数
       ​if (sigHandler == 0) {//信号没有注册
           ​sigcb->sigFlag = 0;
           ​process->sigShare = 0;
           ​SCHEDULER_UNLOCK(intSave);
           ​PRINT_ERR("The signal processing function for the current process pid =%d is NULL!\n", task->processID);
           ​return;
       ​}
       ​/* One pthread do the share signal */ 
       ​sigcb->sigFlag |= process->sigShare;//扩展任务的信号标签集
       ​unsigned int signo = (unsigned int)FindFirstSetedBit(sigcb->sigFlag) + 1;
       ​OsProcessExitCodeSignalSet(process, signo);//设置进程退出信号
       ​sigcb->context.CPSR = cpsr;		//保存状态寄存器
       ​sigcb->context.PC = sp[REG_PC]; //获取被打断现场寄存器的值
       ​sigcb->context.USP = sp[REG_SP];//用户栈顶位置,以便能从内核栈切回用户栈
       ​sigcb->context.ULR = sp[REG_LR];//用户栈返回地址
       ​sigcb->context.R0 = sp[REG_R0];	//系统调用的返回值
       ​sigcb->context.R1 = sp[REG_R1];
       ​sigcb->context.R2 = sp[REG_R2];
       ​sigcb->context.R3 = sp[REG_R3]; 
       ​sigcb->context.R7 = sp[REG_R7];//为何参数不用传R7,是因为系统调用发生时 R7始终保存的是系统调用号.
       ​sigcb->context.R12 = sp[REG_R12];//详见 https://my.oschina.net/weharmony/blog/4967613
       ​sp[REG_PC] = sigHandler;//指定信号执行函数,注意此处改变保存任务上下文中PC寄存器的值,恢复上下文时将执行这个函数.
       ​sp[REG_R0] = signo;		//参数1,信号ID
       ​sp[REG_R1] = (unsigned int)(UINTPTR)(sigcb->sigunbinfo.si_value.sival_ptr); //参数2
       ​/* sig No bits 00000100 present sig No 3, but  1<< 3 = 00001000, so signo needs minus 1 */
       ​sigcb->sigFlag ^= 1ULL << (signo - 1);
       ​sigcb->context.count++;	//代表已保存
   ​}
   ​SCHEDULER_UNLOCK(intSave);
}

解读

  • 先是判断执行条件,确实是有信号需要处理,有处理函数.自定义处理函数是由用户进程安装进来的,所有进程旗下的任务都共用,参数就是信号signo,注意可不是系统调用号,有区别的.信号编号长这样.
#define SIGHUP    1	//终端挂起或者控制进程终止
#define SIGINT    2	//键盘中断(ctrl + c)
#define SIGQUIT   3	//键盘的退出键被按下
#define SIGILL    4	//非法指令
#define SIGTRAP   5	//跟踪陷阱(trace trap),启动进程,跟踪代码的执行
#define SIGABRT   6	//由abort(3)发出的退出指令
#define SIGIOT    SIGABRT //abort发出的信号
#define SIGBUS    7	//总线错误 
#define SIGFPE    8	//浮点异常
#define SIGKILL   9	//常用的命令 kill 9 123 | 不能被忽略、处理和阻塞

系统调用号长这样,是不是看到一些很熟悉的函数.

#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
#define __NR_creat 8
#define __NR_link 9
#define __NR_unlink 10
#define __NR_execve 11
#define __NR_chdir 12
#define __NR_time 13
#define __NR_mknod 14
#define __NR_chmod 15
#define __NR_lchown 16
#define __NR_break 17
  • 最后是最最最关键的代码,改变pc寄存器的值,此值一变,在_osExceptSwiHdl中恢复上下文后,cpu跳到用户空间的代码段 user.sighandle(R0,R1) 开始执行,即执行信号处理函数.
sp[REG_PC] = sigHandler;//指定信号执行函数,注意此处改变保存任务上下文中PC寄存器的值,恢复上下文时将执行这个函数.
sp[REG_R0] = signo;		//参数1,信号ID
sp[REG_R1] = (unsigned int)(UINTPTR)(sigcb->sigunbinfo.si_value.sival_ptr); //参数2

OsRestorSignalContext 恢复信号上下文

/****************************************************
恢复信号上下文,由系统调用之__NR_sigreturn产生,这是一个内部产生的系统调用.
为什么要恢复呢?
因为系统调用的执行由任务内核态完成,使用的栈也是内核栈,CPU相关寄存器记录的都是内核栈的内容,
而系统调用完成后,需返回任务的用户栈执行,这时需将CPU各寄存器回到用户态现场
所以函数的功能就变成了还原寄存器的值
****************************************************/
void OsRestorSignalContext(unsigned int *sp)
{
   ​LosTaskCB *task = NULL; /* Do not adjust this statement */
   ​LosProcessCB *process = NULL;
   ​sig_cb *sigcb = NULL;
   ​UINT32 intSave;

   ​SCHEDULER_LOCK(intSave);
   ​task = OsCurrTaskGet();
   ​sigcb = &task->sig;//获取当前任务信号控制块

   ​if (sigcb->context.count != 1) {//必须之前保存过,才能被恢复
       ​SCHEDULER_UNLOCK(intSave);
       ​PRINT_ERR("sig error count : %d\n", sigcb->context.count);
       ​return;
   ​}

   ​process = OsCurrProcessGet();//获取当前进程
   ​sp[REG_PC] = sigcb->context.PC;//指令寄存器
   ​OS_SYSCALL_SET_CPSR(sp, sigcb->context.CPSR);//重置程序状态寄存器
   ​sp[REG_SP] = sigcb->context.USP;//用户栈堆栈指针, USP指的是 用户态的堆栈,即将回到用户栈继续运行
   ​sp[REG_LR] = sigcb->context.ULR;//返回用户栈代码执行位置
   ​sp[REG_R0] = sigcb->context.R0;
   ​sp[REG_R1] = sigcb->context.R1;
   ​sp[REG_R2] = sigcb->context.R2;
   ​sp[REG_R3] = sigcb->context.R3;
   ​sp[REG_R7] = sigcb->context.R7;
   ​sp[REG_R12] = sigcb->context.R12;
   ​sigcb->context.count--;	//信号上下文的数量回到减少
   ​process->sigShare = 0;	//回到用户态,信号共享清0
   ​OsProcessExitCodeSignalClear(process);//清空进程退出码
   ​SCHEDULER_UNLOCK(intSave);
}

解读

  • 在信号处理函数完成之后,内核会触发一个__NR_sigreturn的系统调用,又陷入内核态,回到了OsArmA32SyscallHandle.
  • 恢复的过程很简单,把之前保存的信号上下文恢复到内核栈sp开始位置,数据在栈中的保存顺序可查看 用栈方式篇 ,最重要的看这几句.
sp[REG_PC] = sigcb->context.PC;//指令寄存器
sp[REG_SP] = sigcb->context.USP;//用户栈堆栈指针, USP指的是 用户态的堆栈,即将回到用户栈继续运行
sp[REG_LR] = sigcb->context.ULR;//返回用户栈代码执行位置

注意这里还不是真正的切换上下文,只是改变内核栈中现有的数据.这些数据将还原给寄存器.USPULR指向的是用户栈的位置.一旦PCUSPULR从栈中弹出赋给寄存器.才真正完成了内核栈到用户栈的切换.回到了user.source()继续运行.

  • 真正的切换汇编代码如下,都已添加注释,在保存和恢复上下文中夹着OsArmA32SyscallHandle
@ Description: Software interrupt exception handler
_osExceptSwiHdl: @软中断异常处理,注意此时已在内核栈运行
@保存任务上下文(TaskContext) 开始... 一定要对照TaskContext来理解
SUB     SP, SP, #(4 * 16)	@先申请16个栈空间单元用于处理本次软中断
STMIA   SP, {R0-R12}		@TaskContext.R[GEN_REGS_NUM] STMIA从左到右执行,先放R0 .. R12
MRS     R3, SPSR			@读取本模式下的SPSR值
MOV     R4, LR				@保存回跳寄存器LR

AND     R1, R3, #CPSR_MASK_MODE                          @ Interrupted mode 获取中断模式
CMP     R1, #CPSR_USER_MODE                              @ User mode	是否为用户模式
BNE     OsKernelSVCHandler                               @ Branch if not user mode 非用户模式下跳转
@ 当为用户模式时,获取SP和LR寄出去值
@ we enter from user mode, we need get the values of  USER mode r13(sp) and r14(lr).
@ stmia with ^ will return the user mode registers (provided that r15 is not in the register list).
MOV     R0, SP											 @获取SP值,R0将作为OsArmA32SyscallHandle的参数
STMFD   SP!, {R3}                                        @ Save the CPSR 入栈保存CPSR值 => TaskContext.regPSR
ADD     R3, SP, #(4 * 17)                                @ Offset to pc/cpsr storage 跳到PC/CPSR存储位置
STMFD   R3!, {R4}                                        @ Save the CPSR and r15(pc) 保存LR寄存器 => TaskContext.PC
STMFD   R3, {R13, R14}^                                  @ Save user mode r13(sp) and r14(lr) 从右向左 保存 => TaskContext.LR和SP
SUB     SP, SP, #4										 @ => TaskContext.resved
PUSH_FPU_REGS R1	@保存中断模式(用户模式)											
@保存任务上下文(TaskContext) 结束
MOV     FP, #0                                           @ Init frame pointer
CPSIE   I	@开中断,表明在系统调用期间可响应中断
BLX     OsArmA32SyscallHandle	/*交给C语言处理系统调用,参数为R0,指向TaskContext的开始位置*/
CPSID   I	@执行后续指令前必须先关中断
@恢复任务上下文(TaskContext) 开始
POP_FPU_REGS R1											 @弹出FPU值给R1
ADD     SP, SP,#4										 @ 定位到保存旧SPSR值的位置
LDMFD   SP!, {R3}                                        @ Fetch the return SPSR 弹出旧SPSR值
MSR     SPSR_cxsf, R3                                    @ Set the return mode SPSR 恢复该模式下的SPSR值

@ we are leaving to user mode, we need to restore the values of USER mode r13(sp) and r14(lr).
@ ldmia with ^ will return the user mode registers (provided that r15 is not in the register list)

LDMFD   SP!, {R0-R12}									 @恢复R0-R12寄存器
LDMFD   SP, {R13, R14}^                                  @ Restore user mode R13/R14 恢复用户模式的R13/R14寄存器
ADD     SP, SP, #(2 * 4)								 @定位到保存旧PC值的位置
LDMFD   SP!, {PC}^                                       @ Return to user 切回用户模式运行
@恢复任务上下文(TaskContext) 结束

鸿蒙全栈开发全新学习指南

也为了积极培养鸿蒙生态人才,让大家都能学习到鸿蒙开发最新的技术,针对一些在职人员、0基础小白、应届生/计算机专业、鸿蒙爱好者等人群,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线【包含了大厂APP实战项目开发】

本路线共分为四个阶段:

第一阶段:鸿蒙初中级开发必备技能

第二阶段:鸿蒙南北双向高工技能基础:gitee.com/MNxiaona/733GH

第三阶段:应用开发中高级就业技术

第四阶段:全网首发-工业级南向设备开发就业技术:https://gitee.com/MNxiaona/733GH

《鸿蒙 (Harmony OS)开发学习手册》(共计892页)

如何快速入门?

1.基本概念
2.构建第一个ArkTS应用
3.……

开发基础知识:gitee.com/MNxiaona/733GH

1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……

基于ArkTS 开发

1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……

鸿蒙开发面试真题(含参考答案):gitee.com/MNxiaona/733GH

鸿蒙入门教学视频:

美团APP实战开发教学:gitee.com/MNxiaona/733GH

写在最后

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习资源,请移步前往小编:gitee.com/MNxiaona/733GH

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

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

相关文章

炫酷Chrome:插件大礼包

Chrome浏览器以其强大的功能和丰富的扩展插件库而闻名。 其中&#xff0c;有些插件专为提升用户的浏览体验而设计&#xff0c;例如更换Chrome网页背景图、自定义鼠标点击样式&#xff0c;以及提供便捷的页面跳转工具等。 最近&#xff0c;有一款被称为“宝藏插件包”的工具引…

【软考】模拟考卷错题本2024-05-07

1 项目路径 这里的图没有加载出来&#xff0c;没u哦i关系了。其实主要是的算出最长的路径中包含那些元素即可。这里是蒙圈了&#xff0c;没有考虑到还有更长的。要顾头也顾尾。 2 算法分析-贪心 该问题主要考核的是算法设计策略来达到目标的方式。主要的设计策略有&#xff1a;…

文件加密软件排行榜前四名|好用的四款文件加密软件分享

在数据泄露事件频发的今天&#xff0c;文件加密软件成为了保护个人隐私与企业信息安全的必备工具。 选择一款高效、可靠且易用的加密软件至关重要。 本文精选了当前市场上备受好评的十款文件加密软件&#xff0c;旨在为您在数据保护之旅中提供方向。 1.域智盾 域智盾软件是一…

智慧养老解决方案

PART 1 行业背景及发展趋势 数字看中国人口老龄化 第七次全国人口普查数据显示&#xff0c;我国老年人口总量高达2.64亿人&#xff0c;其中60岁以上人群占比提高至18.7&#xff05;&#xff0c;65岁以上人群占比提高至13.5&#xff05;。 据统计&#xff0c;到2050年&#…

为 Flutter 应用设置主题:ThemeData 和 ColorScheme 指南

在媒体和其他来源中有许多关于这个主题的文章&#xff0c;那么这篇文章的必要性是什么&#xff1f; 在本文中&#xff0c;我计划仅关注 ThemeData 小部件的关键点以及我的开发经验中最常用的参数&#xff0c;并且您将获得有关每个参数如何对您的应用程序执行操作的简要说明。 …

2023年谷歌拒了228万应用,禁了33.3万账号,开发者们应如何应对2024的挑战?

谷歌在上周一公布了去年如何应对恶意应用和恶意行为。 报告指出&#xff0c;去年谷歌在Google Play平台上&#xff0c;通过不断升级安全系统、更新政策规定、运用先进的机器学习技术&#xff0c;以及严格把关应用审核流程&#xff0c;成功阻止了高达228万个不合规的应用程序上架…

人工智能|推荐系统——工业界的推荐系统之重排

一、相似性的度量 基于物品属性标签 基于物品向量表征 ⽤召回的双塔模型学到的物品向量&#xff08;不好&#xff09; 基于内容的向量表征&#xff08;好&#xff09; 二、Maximal Marginal Relevance (MMR) 三、重排的规则 最多连续出现&#x1d458; 篇某种笔记 每&#x…

js如何控制一次只加载一张图片,加载完成后再加载下一张

公众号&#xff1a;程序员白特&#xff0c;欢迎一起交流学习~ 原文&#xff1a;https://juejin.cn/post/7340167256267391012 今天看到一个面试题&#xff0c;是关于img图片加载方面的&#xff0c;有必要记录一下。其实关于这个问题&#xff0c;只要知道图片什么时候加载完成就…

(自适应手机端)物流运输快递仓储网站模板 - 带三级栏目

(自适应手机端)物流运输快递仓储网站模板 - 带三级栏目PbootCMS内核开发的网站模板&#xff0c;该模板适用于物流运输网站、仓储货运网站等企业&#xff0c;当然其他行业也可以做&#xff0c;只需要把文字图片换成其他行业的即可&#xff1b;自适应手机端&#xff0c;同一个后台…

3D模型实时变形算法

最近&#xff0c;在尝试渲染一些奇怪的形状后&#xff0c;我陷入了计算机图形学的困境。事实证明&#xff0c;对于我试图解决的具体问题&#xff0c;没有现有的选项完全适合我想要做的事情。几周后&#xff0c;我终于带着一些答案再次浮出水面&#xff0c;写了很多行代码&#…

3.yolov5训练前的图片处理详解(python)

其实&#xff0c;yolov5模型可以分为深度网络、数据处理&#xff08;图片处理&#xff09;、损失函数、优化器选择、训练和预测及部分构成&#xff0c;相信大家对训练和预测的代码比较熟悉。前面两章我们根据代码和结构图了解了yolov5的深度网络&#xff0c;接下来看数据处理的…

Spring中FactoryBean的作用和实现原理

Spring中FactoryBean的作用和实现原理 BeanFactory与FactoryBean&#xff0c;相信很多刚翻看Spring源码的同学跟我一样很好奇这俩货怎么长得这么像&#xff0c;分别都是干啥用的。 BeanFactory是Spring中Bean工厂的顶层接口&#xff0c;也是我们常说的SpringIOC容器&#xff…

Android广播机制简介

文章目录 Android广播机制简介广播的基本概念广播的类型广播的使用场景Android广播的优缺点优点缺点 使用Android广播的一些最佳实践: Android广播机制简介 Android广播是一种轻量级的消息传递机制&#xff0c;用于应用程序之间或系统与应用程序之间进行通信。它类似于订阅-发…

ENG-2 AM,129423-53-6主要用于检测生物体系中的Na+浓度

引言&#xff1a;在化学研究的海洋中&#xff0c;优质的化学试剂是实验成功的关键。今天&#xff0c;我要为大家分享一款备受好评的化学试剂——ENG-2。这款试剂以其独特的性能和广泛的应用领域&#xff0c;赢得了众多科研人员的青睐。 中文名称&#xff1a;钠离子荧光探针ENG-…

[leetcode] 68. 文本左右对齐

文章目录 题目描述解题方法贪心java代码复杂度分析 题目描述 给定一个单词数组 words 和一个长度 maxWidth &#xff0c;重新排版单词&#xff0c;使其成为每行恰好有 maxWidth 个字符&#xff0c;且左右两端对齐的文本。 你应该使用 “贪心算法” 来放置给定的单词&#xff…

运用远期交易防范外汇风险

随着全球化的深入&#xff0c;跨境贸易和投资愈加频繁&#xff0c;外汇风险成为各类企业和投资者必须面对的现实问题。汇率的波动可能导致交易和投资的成本大幅增加&#xff0c;甚至引发利润损失。在这种情况下&#xff0c;远期交易作为一种有效的外汇风险对冲工具&#xff0c;…

2023年全国职业院校技能大赛(高职组)“云计算应用”赛项赛卷1(私有云)

#需要资源&#xff08;软件包及镜像&#xff09;或有问题的&#xff0c;可私聊博主&#xff01;&#xff01;&#xff01; #需要资源&#xff08;软件包及镜像&#xff09;或有问题的&#xff0c;可私聊博主&#xff01;&#xff01;&#xff01; #需要资源&#xff08;软件包…

源代码加密

代码加密&#xff0c;特别是源代码加密&#xff0c;是一种安全措施&#xff0c;旨在保护软件的源代码不被未授权访问或泄露。源代码是软件应用程序的原始编程文本&#xff0c;它包含了程序的逻辑、算法和设计思想。由于源代码包含了软件的核心知识&#xff0c;因此它具有极高的…

数智算网,链启未来 | 算力网络子链诚邀各方加入

4月28日&#xff0c;在中国移动算力网络大会期间&#xff0c;由中国移动集团主办&#xff0c;中国移动研究院和云能力中心联合承办的“数智算网&#xff0c;链启未来”共链行动算力网络专场会议成功召开。中国移动研究院副院长段晓东&#xff0c;中国移动集团首席专家、云能力中…

MySQL·内置函数

目录 函数 日期函数 案例1&#xff1a;创建一张表&#xff0c;记录生日 案例2&#xff1a;创建一个留言表 案例3&#xff1a;请查询在2分钟内发布的帖子 字符串函数 案例1&#xff1a; 获取emp表的ename列的字符集 案例2&#xff1a;要求显示exam_result表中的信息&am…
最新文章