关于我
欢迎来到我的博客!这里汇集了我对编程和技术的洞见和总结。本站内容分为几个主要类别,涵盖从具体技术实现到编程理念的广泛话题。
主要内容分类
- 项目工程:深入探讨技术的实现细节和理解。
- C/C++:围绕C/C++语言的技术点和编程技巧进行详细总结。
- 程序员哲学:分享程序员在职业生涯中应该具备的哲学理念和思考方式。
想要了解更多具体内容,您可以访问文章分类页面。
联系我
如果您有任何问题或想要交流,欢迎通过关于页面与我联系。
感谢您的阅读和支持,希望我的博客能为您的技术旅程带来帮助!
概述
文档中描述的内容涉及操作系统中多任务管理的关键技术,包括任务的上下文初始化、任务切换、栈的管理以及用户栈与内核栈的处理。每个任务维护独立的栈空间以隔离执行状态,内核与用户模式间切换需谨慎处理栈指针,确保安全。初始任务切换通常在系统启动时进行,而后续的任务调度则通过系统调用或中断触发。这些机制共同支持了操作系统的并发执行和资源管理功能
rt_hw_stack_init和kcontext机制
这里对上下文初始化,是按照给的kstack,这里是将kstack当作栈顶.
内部,需要移动kstack来给Context腾位置的.
这里的init函数,会针对给定的stack_addr进行8对齐.
然后将tentry,parameter,texit依次入栈.
然后将此时的栈顶位置作为kcontext的第三参数.
此时将栈顶,包装函数,和栈顶传入kcontext函数.
kcontext函数,会依次初始化上下文,并且设置了包装函数入口
在包装函数入口,会根据指针参数,来获取三个函数相关参数.
此时,在一系列的传递,调用过程中,在rt_hw_stack_init中押入栈的三个参数内存是没有被破坏的.
rt_hw_context_switch_to以及rt_hw_context_swtich
前者,是在第一次切换执行流,没有旧执行流的时候调用的函数.
调用rt_thread_self函数获取线程信息,然后将from变量初始化,然后将to变量赋值.
用于在深层异常处理函数中使用.
然后调用yield自陷.
后者,同样的原理,就是也将from变量记录.
在yield自陷中,会识别出这个,然后通过rt_thread_self获取线程相关信息.
来对from和to作出对应的处理.
参数传递
kcontext中的函数参数,我们是需要放入到栈的a0寄存器位置上.
将来在trap.S中出栈,跳转函数之后,函数会从a0拿到参数.
如果是在汇编中,我们依然需要参数放置在a0寄存器中.
包装函数需要的三个变量,是直接将三个变量依次保存在栈中,然后将地址作为包装函数的参数
pcb的union定义
这里使用了union,那么内部所有的变量的首地址都是一样的.可以说,都在栈的栈空间的最下面.
之前以为是在最后面的地址共用.
用户栈与内核栈的切换分析与实现
之前一直都是在运行内核程序.
每一个程序注册在内核栈上,使用的是pcb的栈.
现在加入了用户程序概念,用户栈,获取一个heap.end作为用户栈的sp切向.
用户陷入,在用户sp上执行了一系列的上下文入栈,包括当前的sp,可以用MAP宏进行映射,但是我并没有这么做,而是使用的LOAD宏来单独保存sp指针.否则,在恢复上下文的时候,切换对应栈时,到最后会出现重复赋值,容易发生错误.
获取了这个上下文指针,在shedule中,保存在当前的pcb的上下文管理中,sp的值也会被保存在这里,下次切换的时候,可以将sp切换到目标栈.
通过一定的策略,返回目标上下文,这个上下文一定是在pcb中的上下文,其中包括了用户栈的sp指针.
这里就涉及到了sp到底切到pcb中的上下文恢复,然后切回用户栈,还是直接切到用户栈sp,然后恢复.如果是在pcb中,那么再切到用户栈,那么sp就会发生调整问题,如果,直接切到用户栈,那么就自然的,sp会被调整到入栈前的位置.应该尽可能少改动这里的汇编代码,以保持向前兼容.
所以在注册的时候,会初始化两个上下文信息,一个在用户栈中,一个在pcb中,并且将pcb的cp指针初始化为pcb的上下文地址.
用户栈和内核栈的同步
使用context_xload进行上下文初始化时,都是在用户栈上操作.
每一次调用该函数,就会创建一个内核程序,准确说是内核程序,而不是用户程序.
注册的时候,会在pcb中,写入初始化的上下文信息,用于在第一次进入内核程序的时候使用.
这里,当自陷发生的时候.trap.S中的保存信息行为,是保存在自陷发生程序的sp指针指向的地方.
然后传入深层的异常处理函数,传入的参数就是这里的上下文保存地址,这个地址不是pcb中的上下文地址.
一般来说,要将pcb中的上下文内容,和自陷发生程序的上下文内容保持同步,所以,应该做一个内存拷贝.
但是之前一直都是运行内核程序,自陷发生地的sp指向,其实就是pcb中的内核栈,所以不需要拷贝,并且,甚至都不需要保存.
但是现在有了用户程序,需要作出一些理念更改.
可以明确的是,我们在进入用户程序的时候,使用的栈应该是用户栈.
所以,我们在初始化用户上下文的时候,sp寄存器应该保存用户栈的地址.
每一次自陷操作,执行流会从用户程序回到内核处理程序.
同时呢,在自陷的时候,我们的sp值也是保存在了用户栈的上下文sp寄存器中.
同步上下文:我们在schedule中,做了同步操作,但是,这里的同步操作,并没有做拷贝,而是将cp指针指向了这里的传入的上下文
地址,也就是说,pcb中的cp在刚进入用户程序的时候,指向的是pcb的上下文地址,再后续的用户程序自陷操作中,会被保存为用户栈中的上下文地址.
然后切换上下文.到另一个程序,这个程序可以是用户程序也可以是内核程序.
如果是内核程序,那么返回cp指针,此时,在trap.S中,会首先获得sp指针,切换到对应的保存上下文地址,然后做一系列的退栈操作,然后恢复sp值.
这里都是在pcb的内核栈中操作的.
如果内核程序再次自陷,切换到用户程序,那么此时,shedule返回的就是用户栈的上下文信息保存地址.
然后sp会跳转到这个地址,做一系列的退栈操作和更改sp值.
这样的行为,会发现,多次自陷之后,内核程序的上下文信息一直在更新,并且cp指针指向的就是内核栈的上下文地址,
而用户程序的内核栈上下文信息,不会再次更新,保持的是第一次调用时的初始化值,但是其pcb栈中的cp指针会一直保持更新,指向的是用户栈中的上下文地址.
用户栈初始化
包装了loader和ucontext函数,用于在context_uload函数中调用.
在uer_loader程序中,对用户栈进行了初始化,pcb中的内核栈,是在ucontext中进行的.
而对于_start程序中,我并没有做对sp指针的初始化.
_start程序是navy在编译用户程序时候,的跳转入口.用户程序的堆栈,应该在nanos中分配,并且是在nanos中指定.
当跳转到一个用户程序的时候,一般是使用了yield程序进行自陷,然后在schedule中调度.
这里实现的策略就是,操作系统自陷,然后通过schedule进行调度.后续,应该会添加一些机制,能够指定向运行某些程序.
如果要验证关于用户程序使用的是用户栈还是内核栈,在调度器中打印对应的cp值和sp值就可以了.
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !