本随笔是非常菜的菜鸡写的。如有问题请及时提出。
可以联系:1160712160@qq.com
扩展内核,能够显示操作系统切换任务的过程。
首先先回忆一下操作系统切换任务的过程.
因此只需要在这些关键节点加上println!即可.
首先是在Trap的时候输出,这里其余的都有了,只增加了Interrupt::SupervisorTimer的输出:
//os/src/trap/mod.rs#[no_mangle]///handleaninterrupt,exception,orsystemcallfromuserspacepubfntrap_handler(cx:&mutTrapContext)->&mutTrapContext{letscause=scause::read();//gettrapcauseletstval=stval::read();//getextravaluematchscause.cause(){Trap::Exception(Exception::UserEnvCall)=>{cx.sepc+=4;cx.x[10]=syscall(cx.x[17],[cx.x[10],cx.x[11],cx.x[12]])asusize;}Trap::Exception(Exception::StoreFault)|Trap::Exception(Exception::StorePageFault)=>{println!("[kernel]PageFaultinapplication,kernelkilledit.");exit_current_and_run_next();}Trap::Exception(Exception::IllegalInstruction)=>{println!("[kernel]IllegalInstructioninapplication,kernelkilledit.");exit_current_and_run_next();}Trap::Interrupt(Interrupt::SupervisorTimer)=>{println!("\nTimerinterrupt,timesliceusedup!");set_next_trigger();println!("Nexttimerset!");suspend_current_and_run_next();}_=>{panic!("Unsupportedtrap{:},stval={:#x}!",scause.cause(),stval);}}cx}修改Task,在修改当前任务的状态之后输出:
//os/src/task/mod.rs///当前任务主动放弃CPU使用权pubfnsuspend_current_and_run_next(){mark_current_suspended();println!("\nTask{}suspended",TASK_MANAGER.inner.exclusive_access().current_task);run_next_task();}///当前任务退出pubfnexit_current_and_run_next(){mark_current_exited();println!("\nTask{}exited",TASK_MANAGER.inner.exclusive_access().current_task);run_next_task();}在寻找到下一个task后,输出下一个task.
//os/src/task/mod.rsimplTaskManager{......fnrun_next_task(&self){ifletSome(next)=self.find_next_task(){println!("\nFoundnexttask{}",next);letmutinner=self.inner.exclusive_access();letcurrent=inner.current_task;inner.current_task=next;inner.tasks[next].task_status=TaskStatus::Running;letcurrent_task_cx_ptr=&mutinner.tasks[current].task_cxas*mutTaskContext;letnext_task_cx_ptr=&mutinner.tasks[next].task_cxas*constTaskContext;drop(inner);unsafe{__switch(current_task_cx_ptr,next_task_cx_ptr);}println!("\nTask{}hasbeenswitchedout",next);}else{println!("Allapplicationscompleted!");shutdown(false);}}}这时候makerun,输出:
这个只需要略微明白mtime的作用就行,另外,如果你是一直看我博客到现在的,我现在提醒你可以去做第一章没有做完的作业了.
原本的打算是实现一个线程安全的数组,但是这样的写法说明我对OOP的理解学到了直肠里,主打一个脑子不好使.
//os/src/task/task.rs#[derive(Copy,Clone)]pubstructTaskControlBlock{pubtask_status:TaskStatus,pubtask_cx:TaskContext,pubtask_start_time:isize,}不要忘记初始化的时候初始化这个变量:
//os/src/task/mod.rslazy_static!{///全局单例的任务管理器pubstaticrefTASK_MANAGER:TaskManager={letnum_app=get_num_app();letmuttasks=[TaskControlBlock{task_status:TaskStatus::UnInit,task_cx:TaskContext::zero_init(),task_start_time:-1,};MAX_APP_NUM];for(i,task)intasks.iter_mut().enumerate(){task.task_cx=TaskContext::goto_restore(init_app_cx(i));task.task_status=TaskStatus::Ready;}TaskManager{num_app,inner:unsafe{UPSafeCell::new(TaskManagerInner{tasks,current_task:0,})}}};}这里注意要初始化为-1,判断有没有初始化过.
那么不仅没有区分用户态和内核态,而且还没有再任务不运行的时候停止计时.
这里再加一个成员就行:
//os/src/task/task.rs......pubstructTaskControlBlock{ ......pubtask_start_time:isize,pubtask_running_time:isize,}......在两种进行任务切换的情况下,计算任务运行时的差值:
//os/src/task/mod.rs......fnmark_current_suspended(&self){ ......inner.tasks[current].task_running_time+=get_time_us()asisize-inner.tasks[current].task_start_time;......}fnmark_current_exited(&self){......inner.tasks[current].task_running_time+=get_time_us()asisize-inner.tasks[current].task_start_time;println!("\nTask{}RunTime:{}us",current,inner.tasks[current].task_running_time);......}......这里注意要每次运行的时候都更新task_start_time:
fnrun_next_task(&self){ifletSome(next)=self.find_next_task(){ ......inner.tasks[next].task_start_time=get_time_us()asisize; ......}else{ ......}}也要注意初始化这个变量为0:
lazy_static!{///全局单例的任务管理器pubstaticrefTASK_MANAGER:TaskManager={ ......letmuttasks=[TaskControlBlock{ ......task_running_time:0,};MAX_APP_NUM];for(i,task)intasks.iter_mut().enumerate(){ ......}TaskManager{ ......}};}这时候进行运行:
这里是不发生任务切换的情况,这时候脑海中就一直想为什么参考答案要在mark_current_suspended和mark_current_exited中加停表呢:
如果发生任务切换,那么更复杂一些:
这是时候修改TaskManager的方法,这里refresh_stop_watch的位置没有加到mark_current_suspended和mark_current_exited中,这是因为理解不同,我认为发生了__switch之后才算真正完成了切换,而答案中则认为Task状态发生改变就是发生了切换:
implTaskManagerInner{fnrefresh_stop_watch(&mutself)->usize{letstart_time=self.stop_watch;self.stop_watch=get_time_us();self.stop_watch-start_time}}这里封装user_time_start和user_time_end两个函数:
#[no_mangle]///handleaninterrupt,exception,orsystemcallfromuserspacepubfntrap_handler(cx:&mutTrapContext)->&mutTrapContext{user_time_start();letscause=scause::read();//gettrapcauseletstval=stval::read();//getextravaluematchscause.cause(){Trap::Exception(Exception::UserEnvCall)=>{cx.sepc+=4;cx.x[10]=syscall(cx.x[17],[cx.x[10],cx.x[11],cx.x[12]])asusize;}Trap::Exception(Exception::StoreFault)|Trap::Exception(Exception::StorePageFault)=>{println!("[kernel]PageFaultinapplication,kernelkilledit.");exit_current_and_run_next();}Trap::Exception(Exception::IllegalInstruction)=>{println!("[kernel]IllegalInstructioninapplication,kernelkilledit.");exit_current_and_run_next();}Trap::Interrupt(Interrupt::SupervisorTimer)=>{println!("\nTimerinterrupt,timesliceusedup!");set_next_trigger();println!("Nexttimerset!");suspend_current_and_run_next();}_=>{panic!("Unsupportedtrap{:},stval={:#x}!",scause.cause(),stval);}}user_time_end();cx}这时候执行makerun:
那么我们知道的是浮点应用计算是分为好几步的,而不是原子操作,也就是进行任务切换的时候可能会存在浮点数计算计算到一半的情况.
那么回想之前的做法,我们需要进行保存上下文的操作.
RV32F和RV32D的浮点寄存器。单精度寄存器占用了32个双精度寄存器中最右边的一半。
可一看到这里的寄存器分为了:
这张图列出了寄存器的RISC-V应用程序二进制接口(ABI)名称和它们在函数调用中是否保留的规定。
这里我思考了是不是临时寄存器就不需要保存上下文,而保存寄存器就需要保存上下文这个问题,但是回头一想,在trap.S里我们选择了保存x0~x31(一部分特殊的寄存器暂时不用保存),这里吧也是有临时寄存器和保存寄存器的.
这时候的trap.S
此外,支持浮点指令可能还需要(包括但不限于)以下条件:
这时候就需要参阅linux或rCore的源码了.
那么这时候需要看Linux的上下文保存了.
那么其实对于我的能力而言,这里的内容有两个部分没有完成:
编写应用程序或扩展内核,能够统计任务切换的大致开销
这个其实上来是一脸懵的,首先先思考一下任务切换开销到底开销在哪了:
这题其实是没什么头绪的,但是看了答案豁然开朗,只需要统计__switch的总耗时即可.
这里注意,是所有的开销,而不是单纯的一个的开销
回顾trap.S:
__alltraps: ......calltrap_handler__restore: ......sret把trap_handler的内容中函数的内容更换为:
__alltraps: ......#一些代码逻辑goto__restore#实际上RISCV没有goto这个控制转移指令 #后续的代码逻辑__restore: ......sret这里一定要特别注意一点,这是汇编,也就是__alltraps不会执行到calltrap_handler就停止了,它会一直执行__restore到sret.
做出如下修改:
#TODO
#TODO似乎是一个看起来很简单但是执行起来很难的问题.
协作式调度与抢占式调度的区别是什么?
协作式调度是经由APP本身在执行耗时操作的时候主动执行yield释放CPU.
中断、异常和系统调用有何异同之处?
中断和异常都是Trap,而系统调用是一种异常.就是EnvironmentcallfromU-mode这个异常.
RISC-V支持哪些中断/异常?
这个第二章作业题其实已经解释过了,我直接把图贴过来.
但是这里发现ExceptionCode有很多是空着的.
如何判断进入操作系统内核的起因是由于中断还是异常
这里想到关于trap_handler的代码:
#[no_mangle]///handleaninterrupt,exception,orsystemcallfromuserspacepubfntrap_handler(cx:&mutTrapContext)->&mutTrapContext{letscause=scause::read();//gettrapcauseletstval=stval::read();//getextravaluematchscause.cause(){Trap::Exception(Exception::UserEnvCall)=>{cx.sepc+=4;cx.x[10]=syscall(cx.x[17],[cx.x[10],cx.x[11],cx.x[12]])asusize;}Trap::Exception(Exception::StoreFault)|Trap::Exception(Exception::StorePageFault)=>{println!("[kernel]PageFaultinapplication,badaddr={:#x},badinstruction={:#x},kernelkilledit.",stval,cx.sepc);exit_current_and_run_next();}Trap::Exception(Exception::IllegalInstruction)=>{println!("[kernel]IllegalInstructioninapplication,kernelkilledit.");exit_current_and_run_next();}Trap::Interrupt(Interrupt::SupervisorTimer)=>{set_next_trigger();suspend_current_and_run_next();}_=>{panic!("Unsupportedtrap{:},stval={:#x}!",scause.cause(),stval);}}cx}可以看到是处理了scause.cause,应该是scause寄存器的cause位.
和上一题对应起来就可以理解了.
在RISC-V中断机制中,PLIC和CLINT各起到了什么作用?
这里在我们已有的参考书里搜都找不到.
那么要回答这个问题只需要看手册的Introduction部分就行了.
关于PLIC:ThisdocumentcontainstheRISC-Vplatform-levelinterruptcontroller(PLIC)specification(wasremovedfromRISC-VPrivilegedSpecv1.11-draft),whichdefinesaninterruptcontrollerspecificallydesignedtoworkinthecontextofRISC-Vsystems.ThePLICmultiplexesvariousdeviceinterruptsontotheexternalinterruptlinesofHartcontexts,withhardwaresupportforinterruptpriorities.ThisspecificationdefinesthegeneralPLICarchitectureandtheoperationparameters.PLICsupportsup-to1023interrupts(0isreserved)and15872contexts,buttheactualnumberofinterruptsandcontextdependsonthePLICimplementation.However,theimplementmustadheretotheoffsetofeachregisterwithinthePLICoperationparameters.ThePLICwhichclaimedasPLIC-CompliantstandardPLICshouldfollowtheimplementationsmentionedinsectionsbelow.
机翻:这份文档包含了RISC-V平台级中断控制器(PLIC)的规格说明(该规格曾从RISC-V特权规格v1.11草案中移除),它定义了一个专门为RISC-V系统设计的中断控制器。PLIC将各种设备中断复用到Hart上下文的外部中断线上,并提供了对中断优先级的硬件支持。本规格说明定义了通用的PLIC架构和操作参数。PLIC支持最多1023个中断(0保留不用)和15872个上下文,但实际的中断数量和上下文数量取决于PLIC的具体实现。然而,实现必须遵循PLIC操作参数中每个寄存器的偏移量。声称符合PLIC标准的PLIC应当遵循下文中提到的实现。
关于CLINT:ThisRISC-VACLINTspecificationdefinesasetofmemorymappeddeviceswhichprovideinterprocessorinterrupts(IPI)andtimerfunctionalitiesforeachHARTonamulti-HARTRISC-Vplatform.TheseHART-levelIPIandtimerfunctionalitiesarerequiredbyoperatingsystems,bootloadersandfirmwaresrunningonamulti-HARTRISC-Vplatform.TheSiFiveCore-LocalInterruptor(CLINT)devicehasbeenwidelyadoptedintheRISC-Vworldtoprovidemachine-levelIPIandtimerfunctionalities.Unfortunately,theSiFiveCLINThasaunifiedregistermapforbothIPIandtimerfunctionalitiesanditdoesnotprovidesupervisor-levelIPIfunctionality.TheRISC-VACLINTspecificationtakesamoremodularapproachbydefiningseparatememorymappeddevicesforIPIandtimerfunctionalities.ThismodularityallowsRISC-VplatformstoomitsomeoftheRISC-VACLINTdevicesforwhentheplatformhasanalternatemechanism.Inadditiontomodularity,theRISC-VACLINTspecificationalsodefinesadedicatedmemorymappeddeviceforsupervisor-levelIPIs.TheTable1belowshowsthelistofdevicesdefinedbytheRISC-VACLINTspecification.
机翻:RISC-VACLINT规格说明定义了一组内存映射设备,这些设备为多HartRISC-V平台上每个Hart提供了处理器间中断(IPI)和定时器功能。这些Hart级别的IPI和定时器功能是多HartRISC-V平台上运行的操作系统、引导加载程序和固件所必需的。SiFive的Core-LocalInterruptor(CLINT)设备已在RISC-V领域被广泛采用,用于提供机器级别的IPI和定时器功能。不幸的是,SiFiveCLINT设备具有统一的寄存器映射,既用于IPI又用于定时器功能,并且不提供监督级别(supervisor-level)的IPI功能。RISC-VACLINT规格说明采取了更为模块化的方法,通过定义独立的内存映射设备来分别提供IPI和定时器功能。这种模块化允许RISC-V平台省略某些RISC-VACLINT设备,当平台有替代机制时。除了模块化之外,RISC-VACLINT规格说明还定义了一个专门的内存映射设备用于监督级别的IPI功能。下表1显示了RISC-VACLINT规格说明定义的设备列表。
基于RISC-V的操作系统支持中断嵌套?请给出进一步的解释说明。
~~这里根据本章学到的知识,得出的结论应该是支持中断嵌套.~~
模糊的记忆是:当RISC-V接收到中断后会修改对应寄存器的值以屏蔽和此中断同等级和低等级的所有中断.
RISC-V原生不支持中断嵌套。(在S态的内核中)只有sstatus的SIE位为1时,才会开启中断,再由sie寄存器控制哪些中断可以触发。触发中断时,sstatus.SPIE置为sstatus.SIE,而sstatus.SIE置为0;当执行sret时,sstatus.SIE置为sstatus.SPIE,而sstatus.SPIE置为1。这意味着触发中断时,因为sstatus.SIE为0,所以无法再次触发中断。
这里不知道是不是因为提了操作系统因此不考虑M态的中断会出现在操作系统中,因此是不支持中断嵌套的.
本章提出的任务的概念与前面提到的进程的概念之间有何区别与联系?
说实话前面似乎没有提到进程的概念.
感觉需要等到第五章.#TODO
这里直接抄答案了:
简单描述一下任务的地址空间中有哪些类型的数据和代码。
可参照user/src/linker.ld:
除此之外,在内核中为每个任务构造的用户栈os/src/loader.rs:USER_STACK也属于各自任务的地址。
任务控制块保存哪些内容?
这个直接照抄我们之前画的图就行了.
看最后的TaskControlBlock的结构.
任务上下文切换需要保存与恢复哪些内容?
这个同第九题,直接看TaskContext的内容.
特权级上下文和任务上下文有何异同?
任务上下文的内容同第九题即可,特权级上下文的内容直接照抄第二章的内容.
//os/src/trap/context.rs#[repr(C)]pubstructTrapContext{pubx:[usize;32],pubsstatus:Sstatus,pubsepc:usize,}可以看到TaskContext和TrapContext都保存了一些寄存器内容,而TrapContext保存了更多的寄存器.
特权级上下文切换可以发生在中断异常时,所以它不符合函数调用约定,需要保存所有通用寄存器。同时它又涉及特权级切换,所以还额外保留了一些CSR,在切换时还会涉及更多的CSR。
上下文切换为什么需要用汇编语言实现?
这题还真不知道,也是之前脑子里的疑惑了.
上下文切换过程中,需要我们直接控制所有的寄存器。C和Rust编译器在编译代码的时候都会“自作主张”使用通用寄存器,以及我们不知道的情况下访问栈,这是我们需要避免的。
切换到内核的时候,保存好用户态状态之后,我们将栈指针指向内核栈,相当于构建好一个高级语言可以正常运行的环境,这时候就可以由高级语言接管了。
有哪些可能的时机导致任务切换?
在设计任务控制块时,为何采用分离的内核栈和用户栈,而不用一个栈?
用户程序可以任意修改栈指针,将其指向任意位置,而内核在运行的时候总希望在某一个合法的栈上,所以需要用分开的两个栈。
此外,利用后面的章节的知识可以保护内核和用户栈,让用户无法读写内核栈上的内容,保证安全。
一些提示:
这里我们也可以完全不接受建议,使用github1s观看,虽然老登从来不接受github上的issue,但是确实在上边公开了自己的代码.
Linux正常运行的时候,stvec指向哪个函数?是哪段代码设置的stvec的值?
在源码的riscv文件夹中搜索stvec:
可以看到在\arch\riscv\kernel\kexec_relocate.S里有一段代码是写入CSR_STVEC的:
在\arch\riscv\kvm\vcpu_switch.S里有一个函数有操作是写入CSR_STVEC的:
SYM_FUNC_START(__kvm_riscv_switch_to) ... /*SaveHostSTVECandchangeittoreturnpath*/ csrrw t4,CSR_STVEC,t4 ... /*RestoreHostSTVEC*/ csrw CSR_STVEC,t1 ...SYM_FUNC_END(__kvm_riscv_switch_to) 这里需要注意的就是这里的神似魔法的宏展开操作:SYM_CODE_START,SYM_CODE_END,SYM_FUNC_START,SYM_FUNC_END.
大概意思是汇编代码也是需要区分代码和函数的,但是汇编器不强制要求这一点,这里通过宏操作把对这两方面的注解更改成了一键式.
SYM_FUNC_*代表类似于C语言的函数,有参数调用之类.
SYM_CODE_*使用特殊堆栈调用的特殊功能。无论是具有特殊栈内容的中断处理程序、中继机制,还是启动函数.
riscv_kexec_relocate只在如下函数中被调用,可以看到是一个初始化kexec的函数,而且非常巧思地直接把这段asm当作一段数组拷贝进一个地址,这样就可以实现动态函数:
这里大部分内容都是关于kexec的.
根据本题给出的线索,显然正常运行过程中是用不到这个工具的.
似乎搜索的结果很让人乐观.
在\arch\riscv\kernel\suspend.c中是对CSR的保存操作:
voidsuspend_save_csrs(structsuspend_context*context){ if(riscv_cpu_has_extension_unlikely(smp_processor_id(),RISCV_ISA_EXT_XLINUXENVCFG)) context->envcfg=csr_read(CSR_ENVCFG); context->tvec=csr_read(CSR_TVEC); context->ie=csr_read(CSR_IE); /* *Noneedtosave/restoreIPCSR(i.e.MIPorSIP)because: * *1.Forno-MMU(M-mode)kernel,thebitsinMIParesetby *externaldevices(suchasinterruptcontroller,timer,etc). *2.ForMMU(S-mode)kernel,thebitsinSIParesetby *M-modefirmwareandexternaldevices(suchasinterrupt *controller,etc). */#ifdefCONFIG_MMU context->satp=csr_read(CSR_SATP);#endif}voidsuspend_restore_csrs(structsuspend_context*context){ csr_write(CSR_SCRATCH,0); if(riscv_cpu_has_extension_unlikely(smp_processor_id(),RISCV_ISA_EXT_XLINUXENVCFG)) csr_write(CSR_ENVCFG,context->envcfg); csr_write(CSR_TVEC,context->tvec); csr_write(CSR_IE,context->ie);#ifdefCONFIG_MMU csr_write(CSR_SATP,context->satp);#endif}重点显然在\arch\riscv\kernel\head.S文件中:
先看第一部分_start:
这里暂时#TODO勉强可以确定是\arch\riscv\kernel\head.S里的setup_trap_vector把stval设置为了\arch\riscv\kernel\entry.S里的handle_exception.
Linux里进行上下文切换的函数叫什么?(对应rCore的__switch)
根据题目的提示应该在entry.S里找.
在没有提示的情况下,我们应该专注于LinuxKernel的启动流程才能达成.
根据注释很容易找到这个函数__switch_to.
既让上一小问找到了__switch_to,那么保存任务上下文只需要看那段代码段就行了,必然是和任务上下文有关的.
这里我们发现REG_L我们不认识,显然是用了"宏魔法".
#ifdef__ASSEMBLY__#define__ASM_STR(x) x#else#define__ASM_STR(x) #x#endif#if__riscv_xlen==64#define__REG_SEL(a,b) __ASM_STR(a)#elif__riscv_xlen==32#define__REG_SEL(a,b) __ASM_STR(b)#else#error"Unexpected__riscv_xlen"#endif#defineREG_L __REG_SEL(ld,lw)#defineREG_S __REG_SEL(sd,sw)只能说这里的宏还是非常有东西的,太有参考价值了.
__ASM_STR(x):
__REG_SEL(a,b):
REG_L和REG_S:
实际上实现的效果REG_S还是把寄存器内容存到内存中,REG_L还是把内存内容转存到寄存器中.
还有一个TASK_THREAD_*_*也是宏,我们一样可以按图索骥找到他们的实现,这里代码就不全贴出来了,原理都是一样的,在\arch\riscv\kernel\asm-offsets.c文件:
/**THREAD_{F,X}*mightbelargerthanaS-typeoffsetcanhandle,but*theseareusedinperformance-sensitiveassemblysowecan'tresort*toloadingthelongimmediateeverytime.*/DEFINE(TASK_THREAD_RA_RA, offsetof(structtask_struct,thread.ra) -offsetof(structtask_struct,thread.ra));这里用到的offsetof也是一个宏:
#defineoffsetof(TYPE,FIELD)((size_t)&((TYPE*)0)->FIELD)看到这个宏直接访问了NULL的成员我直接震惊了,这是什么操作
原来它也是一个宏魔法:
DEFINE本身也是一个宏:
#defineDEFINE(sym,val)\ asmvolatile("\n.ascii\"->"#sym"%0"#val"\""::"i"(val))这里难理解的原因是因为不懂asm关键字的操作:
asm(内嵌汇编指令:输出操作数:输入操作数:破坏描述);这时候发现我们看这段代码的时候看的方式错了,应该这样看:
DEFINE(TEST_A,offsetof(structtest,a));它会被展开为:
asmvolatile( "\n.ascii\"->""TEST_A""%0""offsetof(structtest,a)""\"" : :"i"(offsetof(structtest,a)));去掉最外边代表字符串的":\n.ascii\"->"TEST_A"%0"offsetof(structtest,a)"\"
这里发现TEST_A和offsetof(structtest,a)外层都套了一个",原因不是多此一举,因为如果不这样,它们会被识别成字符串的一部分.
最后得到的asm代码是:
.ascii"->TEST_A$xoffsetof(structtest,a)"这里注意:
利用预编译过程,就可以把.c文件预编译成.s文件,这样就可以实现动态获悉结构体大小的作用了.
并且上述资料为这个宏的由来提供了很好的背景:汇编器根本不认识C的语法,我们仍然需要C的计算结果.
这里发现.ascii是一个输出调试信息的命令,那么例如TASK_THREAD_RA_RA是什么呢
这里点明其中的重点:也就是先用.c文件生成.s文件,然后再用sed命令对其中特定的行进行替换,进而重定向到目标文件中,也就是asm-offset.h。
也就是这个.s文件也不重要,重要的是利用了汇编嵌入立即数的机制.(像我这种笨蛋还是一个个计算大小然后写入的命)
最后这些宏储存的是结构体structtask_struct的成员的偏移量.
那么我们可以看出上一题的汇编代码在保存的恰是structtask_struct的内容.
这个就难了,也没什么线索.只能尝试搜索一下trap.
没想到真搜到了,还是在\arch\riscv\kernel\entry.S:
只需要按图索骥找到PT_*寄存器对应的结构体的定义即可,例如:
OFFSET(PT_ORIG_R2,pt_regs,orig_r2);可见保存Trap上下文的结构体是pt_regs.
编译一遍LinuxKernel然后查看,这样是最妙的.
也可以像我一样使用这两个指令来解决:
gcc-S-oempty.Sempty.cgcc-E-oempty.iempty.c以empty.c为例,这两个指令分别是生成编译过程的汇编版本和预处理后的版本.
Linux在内核态运行的时候,tp寄存器的值有什么含义?sscratch的值是什么?
tp寄存器就是x4寄存器,代表Threadpointer.
在源码中搜索threadpointer找到arch\riscv\kernel\entry.S里的:
SYM_CODE_START(handle_exception) /* *Ifcomingfromuserspace,preservetheuserthreadpointerandload *thekernelthreadpointer.Ifwecamefromthekernel,thescratch *registerwillcontain0,andweshouldcontinueonthecurrentTP. */ csrrwtp,CSR_SCRATCH,tp bneztp,.Lsave_context根据注释,当在内核态的时候sscratch寄存器是0.
这时候再重点看csrrwtp,CSR_SCRATCH,tp这一句是交换tp和CSR_SCRATCH的值.与我脑中不同的是csrrw的用法csrrwrd,csr,zimm[4:0]:
那么这时候tp寄存器的值是什么呢明明搜的是tp结果的出来的结论是sscratch的.
发现就在这段代码里还有一句camefromthekernel.
/**Setthescratchregisterto0,sothatifarecursiveexception*occurs,theexceptionvectorknowsitcamefromthekernel*/csrwCSR_SCRATCH,x0但是内容只是印证了sscratch寄存器的值被设置为0.
这让我们对整段代码块产生了兴趣:
可以看到第一条就很容易得知了bneztp,.Lsave_context:
这段指令的作用是检查tp寄存器中的值是否非零。如果tp的值非零,则程序将跳转到标号.Lsave_context所指向的位置继续执行;否则,程序将继续按顺序执行下一条指令。
那么.Lsave_context的内容是:
REG_Ssp,TASK_TI_USER_SP(tp)REG_Lsp,TASK_TI_KERNEL_SP(tp)相当于分别储存了用户栈指针和载入了内核栈指针.
这两个宏还是神奇的,同上边讲的宏魔法是一样的方式,访问了tp为头指针的某个地址偏置的内容.
OFFSET(TASK_TI_KERNEL_SP,task_struct,thread_info.kernel_sp);OFFSET(TASK_TI_USER_SP,task_struct,thread_info.user_sp);这也证明了这时候tp指向的就是task_struct.那也就是当前任务的task_struct了.
至于为什么当前任务是储存在tp中的,我想我们需要更进一步的努力,这里我找了很多资料,实际上应该和tp寄存器在RISC-V里的规定和内核的启动流程有关系.
我们只能说,顾名思义,既然叫这个名字也被反复代码佐证调用,应该不会有异议,但是这样的碎片化学习也确实不扎实.
Linux在用户态运行的时候,sscratch的值有什么含义?
懂了第四问第五问也就很好了解,sscratch是暂存地当前任务的task_struct.
/* *Ifcomingfromuserspace,preservetheuserthreadpointerandload *thekernelthreadpointer.Ifwecamefromthekernel,thescratch *registerwillcontain0,andweshouldcontinueonthecurrentTP. */ csrrwtp,CSR_SCRATCH,tp bneztp,.Lsave_context...... /* *Setthescratchregisterto0,sothatifarecursiveexception *occurs,theexceptionvectorknowsitcamefromthekernel */ csrwCSR_SCRATCH,x0......可以看到,如果是来自于用户态sscratch不是0,那么把tp和sscratch调换后,tp不是0就进行上下文保存,如果是来自内核态,sscratch是0,tp也被调换为0,因此可以做分支操作.
这个也好说,还是看这段代码:
同样在CSR方面保存了SEPC,SSTATUS,SCAUSE,STVAL和SSCRATCH.
Linux在内核态的时候,被打断的用户态程序的寄存器值存在哪里?在C代码里如何访问?
和第六小问是同一个问题,是被保存在了sp指向的位置.
...... REG_Sx1,PT_RA(sp) REG_Sx3,PT_GP(sp) REG_Sx5,PT_T0(sp) save_from_x6_to_x31...... REG_Ss0,PT_SP(sp) REG_Ss1,PT_STATUS(sp) REG_Ss2,PT_EPC(sp) REG_Ss3,PT_BADADDR(sp) REG_Ss4,PT_CAUSE(sp) REG_Ss5,PT_TP(sp)而在代码的开头,
REG_Ssp,TASK_TI_USER_SP(tp) REG_Lsp,TASK_TI_KERNEL_SP(tp)实际上是存储在了内核栈里边.
Linux是如何根据系统调用编号找到对应的函数的?(对应rCore的syscall::syscall()函数的功能)
这个部分就很难说,查了半天,最后发现还是和汇编有关系,而且仍有部分代码是随编译生成.
搜索NR_chdir发现,在\include\uapi\asm-generic\unistd.h中有这样的注释:
/**Thisfilecontainsthesystemcallnumbers,basedonthe*layoutofthex86-64architecture,whichembedsthe*pointertothesyscallinthetable.**Asabasicprinciple,noduplicationoffunctionality*shouldbeadded,e.g.wedon'tuselseekwhenllseek*ispresent.Newarchitecturesshouldusethisfile*andimplementthelessfeature-fullcallsinuserspace.*/这里是列出了x86-64的syscall代码.而不是risc-v的.
查看题目中的提示,我们找到arch/riscv/kernel/syscall_table.c这个文件,发现这个数组似乎是动态生成的:
void*constsys_call_table[__NR_syscalls]={ [0...__NR_syscalls-1]=__riscv_sys_ni_syscall,#include
#include
Linux用户程序调用ecall的参数是怎么传给系统调用的实现的?系统调用的返回值是怎样返回给用户态的?
这个似乎和RISC-V有关系,和使用的什么操作系统是没关系的.
在rCore中,使用a0~a7寄存器来作为函数调用参数,返回值会给回a0.
在Linux中也是如此.
这里首先还是切换到~/App/rCore-Tutorial-v3,随后切换分支到要求的分支ch3-lab:
gitcheckoutch3-lab开幕雷击之上来就报错:
#[cfg(feature="board_k210")]pubconstCLOCK_FREQ:usize=403000000/62;#[cfg(feature="board_qemu")]pubconstCLOCK_FREQ:usize=12500000;这里的意思是必须实现board_qemu或者board_k210才能根据这两个进行定义CLOCK_FREQ.
这里我们只需要创建os/src/board_qemu.rs,然后在main.rs里边在modconfig之前通过modboard_qemu引用这个空的文件即可.
作业要求我们实现一个新的系统调用,实现这个接口的过程很简单,重点是实现功能.
这里越实现越觉得奇怪.
首先是发现APP层的文件里并没有调用我们需要的sys_task_info.
#TODO这里需要给rCore-Tutorial提一个issue.
切换到第三章作业,并且尝试运行:
gitcheckoutch3makerun运行成功:
我们通过观察rCore-Tutorial-Code-2024S/os/Makefile的内容:
......CHAPTER=$(shellgitrev-parse--abbrev-refHEAD|sed-E's/ch([0-9])/\1/')TEST=$(CHAPTER)BASE=1......kernel:@make-C../userbuildTEST=$(TEST)CHAPTER=$(CHAPTER)BASE=$(BASE)......可以看到如上指令是对APP层进行编译的指令.
这时候我们手动进行编译:
cd../usermakecleanmakebuildTEST=3CHAPTER=3BASE=1cd/build/binls发现编译结果确实是和我们猜想的只编译第三章的APP不同:
ch2b_bad_address.binch2b_bad_register.binch2b_power_3.binch2b_power_7.binch3b_yield1.binch2b_bad_instructions.binch2b_hello_world.binch2b_power_5.binch3b_yield0.binch3b_yield2.bin可以看到是编译了ch2~3所有的b结尾的APP.
我们去看user/Makefile:
......BASE=0CHAPTER=0TEST=$(CHAPTER)ifeq($(TEST),0)#Notest,deprecated,previouslyusedinv3 APPS:=$(filter-out$(wildcard$(APP_DIR)/ch*.rs),$(wildcard$(APP_DIR)/*.rs))elseifeq($(TEST),1)#Alltest APPS:=$(wildcard$(APP_DIR)/ch*.rs)else TESTS:=$(shellseq$(BASE)$(TEST)) ifeq($(BASE),0)#Normaltestsonly APPS:=$(foreachT,$(TESTS),$(wildcard$(APP_DIR)/ch$(T)_*.rs)) elseifeq($(BASE),1)#Basictestsonly APPS:=$(foreachT,$(TESTS),$(wildcard$(APP_DIR)/ch$(T)b_*.rs)) else#Basicandnormal APPS:=$(foreachT,$(TESTS),$(wildcard$(APP_DIR)/ch$(T)*.rs)) endifendif......好像确实是这么设计的(没绷住).
这里我们看到os/src/syscall/process.rs里的:
///YOURJOB:Finishsys_task_infotopasstestcasespubfnsys_task_info(_ti:*mutTaskInfo)->isize{trace!("kernel:sys_task_info");-1}同时加入编译条件LOG:
makerunLOG=TRACEBASE=0这时候发现日志信息太多了,稍微修改一下源码:
///YOURJOB:Finishsys_task_infotopasstestcasespubfnsys_task_info(_ti:*mutTaskInfo)->isize{info!("kernel:sys_task_info");-1}编译语句改为:
makerunLOG=INFOBASE=0这时候的输出:
这时候仔细看题目要求.
ch3中,我们的系统已经能够支持多个任务分时轮流运行,我们希望引入一个新的系统调用sys_task_info以获取当前任务的信息,定义如下:
fnsys_task_info(ti:*mutTaskInfo)->isize
structTaskInfo{status:TaskStatus,syscall_times:[u32;MAX_SYSCALL_NUM],time:usize}
├──os(内核实现)│├──Cargo.toml(配置文件)│└──src(所有内核的源代码放在os/src目录下)│├──main.rs(内核主函数)│└──...├──reports(不是report)│├──lab1.md/pdf│└──...├──...
#TODO这里还是没有想出来:虽然系统调用接口采用桶计数,但是内核采用相同的方法进行维护会遇到什么问题?是不是可以用其他结构计数?
这里直接从syscall模块开始看我写的实现:
//os/src/syscall/process.rs///YOURJOB:Finishsys_task_infotopasstestcasespubfnsys_task_info(_ti:*mutTaskInfo)->isize{info!("kernel:sys_task_info");unsafe{*_ti=TaskInfo{status:get_current_task_status(),syscall_times:get_current_task_syscall_times(),time:get_time_ms()-get_current_task_first_start_time()asusize,};}0}在os/src/syscall/mod.rs中,也要添加一个记录函数:
//os/src/syscall/mod.rs///handlesyscallexceptionwith`syscall_id`andotherargumentspubfnsyscall(syscall_id:usize,args:[usize;3])->isize{record_syscall_times(syscall_id);matchsyscall_id{SYSCALL_WRITE=>sys_write(args[0],args[1]as*constu8,args[2]),SYSCALL_EXIT=>sys_exit(args[0]asi32),SYSCALL_YIELD=>sys_yield(),SYSCALL_GET_TIME=>sys_get_time(args[0]as*mutTimeVal,args[1]),SYSCALL_TASK_INFO=>sys_task_info(args[0]as*mutTaskInfo),_=>panic!("Unsupportedsyscall_id:{}",syscall_id),}}四个函数是我在task模块里的新实现:
///Getthestatusofthecurrent'Running'task.pubfnget_current_task_status()->TaskStatus{TASK_MANAGER.get_current_task_status()}///Getthefirststarttimeofthecurrent'Running'task.pubfnget_current_task_first_start_time()->isize{TASK_MANAGER.get_current_task_first_start_time()}///Recordthesyscalltimesofthecurrent'Running'task.pubfnrecord_syscall_times(syscall_id:usize){TASK_MANAGER.record_syscall_times(syscall_id);}///Getthesyscalltimesofthecurrent'Running'task.pubfnget_current_task_syscall_times()->[u32;MAX_SYSCALL_NUM]{TASK_MANAGER.get_current_task_syscall_times()}这个也是仿照原来的实现对TASK_MANAGER的包裹.
那么TASK_MANAGER调用的属于TaskManager的方法,也是对于struct的成员的访问:
implTaskManager{ ......fnget_current_task_status(&self)->TaskStatus{letinner=self.inner.exclusive_access();inner.tasks[inner.current_task].task_status}fnget_current_task_first_start_time(&self)->isize{letinner=self.inner.exclusive_access();inner.tasks[inner.current_task].task_first_start_time}fnrecord_syscall_times(&self,syscall_id:usize){letmutinner=self.inner.exclusive_access();letcurrent=inner.current_task;inner.tasks[current].syscall_times[syscall_id]+=1;}fnget_current_task_syscall_times(&self)->[u32;MAX_SYSCALL_NUM]{letinner=self.inner.exclusive_access();letcurrent=inner.current_task;inner.tasks[current].syscall_times}......}其中比较重要的还是record_syscall_times和get_current_task_syscall_times.
因为这里涉及到了对TaskControlBlock添加的新成员.
//os/src/task/task.rs///Thetaskcontrolblock(TCB)ofatask.#[derive(Copy,Clone)]pubstructTaskControlBlock{///Thetaskstatusinit'slifecyclepubtask_status:TaskStatus,///Thetaskcontextpubtask_cx:TaskContext,///Thetimewhenthetaskfirststartspubtask_first_start_time:isize,///Thenumbersofsyscallcalledbytaskpubsyscall_times:[u32;MAX_SYSCALL_NUM],}这时候运行:
makerunLOG=INFOBASE=0输出:
运行得到的结果反而是会卡住.
//os/src/task/mod.rsimplTaskManager{fnget_current_task_block(&self)->TaskControlBlock{letinner=self.inner.exclusive_access();letcurrent=inner.current_task;inner.tasks[current]}}///Getthecurrenttaskblock.pubfnget_current_task_block()->TaskControlBlock{TASK_MANAGER.get_current_task_block()}这时候:
//os/src/syscall/process.rs///YOURJOB:Finishsys_task_infotopasstestcasespubfnsys_task_info(_ti:*mutTaskInfo)->isize{info!("kernel:sys_task_info");lettask_block=get_current_task_block();unsafe{*_ti=TaskInfo{status:task_block.task_status,syscall_times:task_block.syscall_times,time:get_time_ms()-task_block.task_first_start_timeasusize,};}0}重新执行打分系统:
cdci-usermaketestCHAPTER=3得到:
这个题在第二章作业已经有了,我们不再重复了吧.
这题在第一张的实验练习:问答作业这块是已经做过的,只需要理解是什么时候进入S态的即可.
这里文档中给出的链接定位非常准确,如下所示函数是rustsbi的入口: