鸿蒙内核源码分析(信号消费篇) | 谁让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_CONTEXTunsigned int R7;	//存放系统调用的IDunsigned 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				@保存回跳寄存器LRAND     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.mzph.cn/bicheng/8402.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【操作系统】进程与线程概念基础知识

进程与线程概念基础知识 进程进程的概念进程控制块进程状态进程三状态模型挂起进程模模型进程的上下文切换进程控制 线程为什么引入线程线程的概念线程与进程的比较线程的上下文切换线程的实现 进程 进程的概念 1. 进程的定义&#xff1a; 进程是指一个具有一定独立功能的程序…

【面试干货】http请求报文的组成与作用?

【面试干货】http请求报文的组成与作用&#xff1f; 一、http 的请求报文组成二、请求行&#xff08;Request Line&#xff09;三、请求头部&#xff08;Request Headers&#xff09;四、请求体&#xff08;Request Body&#xff09;五、响应头部 &#xff08;Response Headers…

iframe的基本用法

一、背景 在web页面开发中,有时会遇到在一个应用A的页面中引用应用B的页面,此时就需要在应用A中使用iframe嵌入引用B的页面了,比如多租户的web应用,或者门户系统的智能客服助手弹框应用,博主也是在做通用智能客服应用的过程中用到了iframe。网络上有关iframe的信息虽然很…

银河麒麟桌面版开机后网络无法自动链接 麒麟系统开机没有连接ens33

1.每次虚拟机开机启动麒麟操作系统&#xff0c;都要输入账号&#xff0c;密码。 进入点击这个ens33 内网才连接 2. 如何开机就脸上呢&#xff1f; 2.1. 进入 cd /etc/sysconfig/network-scripts 2.2 修改参数 onbootyes 改为yes 2.3 重启即可 a. 直接重启机器查看是否正常&…

工程伦理课堂记录

文章目录 0. 导论0.1 工程伦理教育的意义0.2 工程伦理教育要实现的目标 1. 工程与伦理1.1 工程伦理学科的产生1.2 工程和技术1.3 工程概念的理解演进1.4 工程的过程1.5 工程的特点1.6 工程活动的七个维度总结 2. 伦理2.1 道德伦理的概念2.2 道德伦理的关系2.3 伦理规范2.4 伦理…

【吃透Java手写】2-Spring(下)-AOP-事务及传播原理

【吃透Java手写】Spring&#xff08;下&#xff09;AOP-事务及传播原理 6 AOP模拟实现6.1 AOP工作流程6.2 定义dao接口与实现类6.3 初始化后逻辑6.4 原生Spring的方法6.4.1 实现类6.4.2 定义通知类&#xff0c;定义切入点表达式、配置切面6.4.3 在配置类中进行Spring注解包扫描…

Blender动画与云渲染:创造高质量作品的未来路径

Blender作为开源的3D图形软件&#xff0c;在多个领域广受欢迎。但随着项目复杂度提升&#xff0c;传统渲染方式受限。云渲染技术的兴起突破了这些限制&#xff0c;为创作者提供了更自由、高效的创作环境。 一、Blender动画项目的挑战 传统上&#xff0c;Blender动画渲染需要依…

代码审计-php篇之某CRM系统多处sql注入

&#x1f31f; ❤️ 作者&#xff1a;yueji0j1anke 首发于公号&#xff1a;剑客古月的安全屋 字数&#xff1a;3516 阅读时间: 35min 声明&#xff1a;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果…

实战 | 实时手部关键点检测跟踪(附完整源码+代码详解)

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

苹果平板HOME键成历史,全面屏时代到来?2024平板电脑市场趋势分析

近期苹果公司在“放飞吧”发布会上推出了新款iPad Pro和iPad Air平板电脑&#xff0c;并下架了最后一款带有实体Home按键的iPad 9。这一变化标志着Home键在苹果iPad产品线中成为了历史&#xff0c;引起了不少网友的怀念和感慨。 与此同时&#xff0c;今年3月线上平板电脑市场迎…

【驱动】I2C读写时序

1、I2C总线 I2C使用两条线在主控制器和从机之间通信,SCL(串行时钟线)和SDA(串行数据线),这两条线需接5~10欧上拉电阻,总线空闲空闲时,SCL和SDA处于高电平,I2C总线标准模式速度可以达到100K/S,快速模式可以达到400K/S。 2、状态 I2C总线有四种状态:空闲、启动、忙碌、…

Unity与C#的关系

第一&#xff0c;我们首先需要知道Unity与C#的关系是什么&#xff1f; 第二&#xff0c;我们要明白为什么Unity会使用C#&#xff0c;而不是C&#xff1f; 第三&#xff0c;我们需要知道Unity是怎么使用C#的&#xff1f; 第一点&#xff1a; 先说结论&#xff1a;C#是Unity用…

LabVIEW波浪发电平台浮筒取能效率数据采集系统

LabVIEW波浪发电平台浮筒取能效率数据采集系统 随着化石能源的逐渐减少以及能源价格的上升&#xff0c;寻找可替代的、可再生的、清洁的能源成为了世界各国的共识。波浪能作为一种重要的海洋能源&#xff0c;因其巨大的潜力和清洁性&#xff0c;近年来受到了广泛关注。开发了一…

Blender修改器

修改器 Modifier&#xff0c;对模型进行修改&#xff0c;相当于一个函数。 修改器图标是界面右下角的扳手样式 每个修改器的顶部都有如下样式&#xff0c;从左到右分别为&#xff1a;展开/折叠&#xff0c;修改器类型&#xff0c;修改器名称&#xff0c;编辑模式按钮&#xff…

TCP三次握手四次挥手 UDP

TCP是面向链接的协议&#xff0c;而UDP是无连接的协议 TCP的三次握手 三次传输过程是纯粹的不涉及数据&#xff0c;三次握手的几个数据包中不包含数据内容。它的应用层&#xff0c;数据部分是空的&#xff0c;只是TCP实现会话建立&#xff0c;点到点的连接 TCP的四次挥手 第四…

Python生成文学编程风格文档库之pycco使用详解

概要 Pycco是一个Python库,用于生成文学编程风格的文档。它受到了Docco(一个快速生成源代码文档的工具)的启发,并通过解析源代码旁边的注释来创建一个美观的文档页面,使代码的解释与代码本身并排显示。 安装 安装Pycco非常简单,可以通过Python的包管理器pip进行安装: …

vue3与js的router基本使用方式

title: vue3与js的router基本使用方式 tags: vue3js abbrlink: ‘57270957’ date: 2024-04-17 18:54:47 第一步快捷引入的别名 使用路由需要大量在src文件中引用所需要的地址&#xff0c;并且组件中也需要很多的包的引用&#xff0c;将快速跳转到src这一文件的步骤进行简化操…

Redis实际应用中的解决方案

Redis缓存使用问题 1数据一致性 分析一下几种方案&#xff1a; 1&#xff1a;先更新缓存&#xff0c;再更新数据库 2&#xff1a;先更新数据库&#xff0c;在更新缓存 3&#xff1a;先删除缓存&#xff0c;后更新数据库 4&#xff1a;想更新数据库&#xff0c;后删除缓存 …

前端 Android App 上架详细流程 (Android App)

1、准备上架所需要的材料 先在需要上架的官方网站注册账号。提前把手机号&#xff0c;名字&#xff0c;身份证等等材料准备好&#xff0c;完成开发者实名认证&#xff1b;软著是必要的&#xff0c;提前准备好&#xff0c;软著申请时间比较长大概需要1-2周时间才能下来&#xf…

需求文档怎么写?

1. 导言 我也来个导言: 写这篇博客的目的就是来解答一下下面几个问题&#xff1a; 需求文档怎么写&#xff1f;需求文档都应该包含哪些内容&#xff1f;怎样才算一个合格的需求文档&#xff1f; 产品需求文档&#xff08;Product requriement document&#xff09;&#xff…