前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从底层源码剖析操作系统如何切换用户态与内核态

从底层源码剖析操作系统如何切换用户态与内核态

原创
作者头像
潋湄
修改2024-10-17 16:10:14
1460
修改2024-10-17 16:10:14
举报
文章被收录于专栏:操作系统

在操作系统中,整个空间被划分为了两部分,分别为用户空间(user)内核空间(kernel),当用户执行程序时,由于程序的执行需要内核程序的辅助,因此会来回在用户空间与内核空间之间进行切换,而本篇文章旨在通过分析最简单的操作系统xv6来剖析操作系统如何在用户态与内核态之间进行切换

调度寄存器及基础概念介绍

在CPU进行用户态与内核态的上下文调度时,需要保存当前程序计数器(pc)、内核态或用户态的入口地址、具体上下文信息等,这里对这些基础概念进行介绍,方便后面展开:

32个寄存器:在内核区一共有32个寄存器保存转换状态时的上下文信息,从而保证恢复状态的安全性

pc:程序计数器,保存了当前程序执行的指令地址

stvec:内核在此处写入其trap处理程序的地址,RISC-V跳转到stvec中的地址来处理程序

sepc:当陷阱发生时,RISC-V会在此处保存原本状态对应的程序计数器(因为后续操作系统会转换状态,覆盖当前pc的值为新的状态入口地址stvec)。在最后返回时,会由sret(return from trap)指令将sepc复制到pc,从而确保切换回用户态后能够继续执行下一条指令,内核可以编写sepc来控制sret的去向

scause:RISC-V在此处放置了一个数字来描述当前程序发生异常的原因等信息

sscratch:内核在这里放置了一个值,一般用于临时存储上一个程序的返回地址,方便程序最后跳转回原来程序

trampoline:蹦床页面,保存了uservec(执行用户态到内核态操作)和userret(执行内核态到用户态操作)主要函数

trapframe:一个数据结构,保存了当前状态下要完成操作的必要信息(编号id、栈针、页表地址等信息)

操作系统切换状态流程介绍

用户态->内核态

用户态到内核态的转换可以由下图表示:

用户态到内核态的切换流程
用户态到内核态的切换流程
Step1:System call function

一般来说,计算机首先会将用户代码编译为计算机能够接受的.s文件,之后转换为.o等二进制文件进行调用,所以如果要看到用户态到内核态的调用,我们需要详细看计算机编译后的.s文件,这里以write函数为例,write函数编译后的.s代码如下:

用户态中的write调用代码
用户态中的write调用代码

这段代码是汇编代码,为了让小伙伴们更好理解,在这里解释一下:

.global write:汇编指令,生命全局符号write,使得其他汇编文件或链接器可以引用这个符号

write:funciton调用标签,作用是标记当前write函数的入口地址

li a7,SYS_write:加载系统调用号到寄存器a7中,li是"load immediate"的缩写,用于加载立即数到寄存器中,a7为存储函数调用系统接口的寄存器,计算机可以从这里获得系统接口信息进行调用跳转

ecall:环境调用指令,操作系统正是从这里执行从用户态转到内核态的操作

ret:从当前地方返回到调用它的地方,在系统调用的上下文中表示返回到用户空间的程序中

这段代码的核心点在于ecall,ecall是从用户态转到内核态的生命,当操作系统执行ecall指令后,主要会做以下几个操作:

(1)将当前状态从用户态切换为内核态,这个很重要,因为有些指令或者页表只有处于内核态的时候才可以访问

(2)将用户态原本要执行的下一条指令地址保存到sepc中,保证操作系统执行完内核态的代码后能够跳转回用户态继续执行

(3)跳转到stvec对应的地址执行内核处理程序

因此在执行完ecall后,当前便切换到了内核态中执行程序代码

Step2: Trampoline

trampoline意为蹦床,字面理解就是来回蹦,实际上也确实是这样,当操作系统切换状态时,总会执行这段程序来完成状态的切换,这里我们可以从trampoline.S对应的源码来分析:

首先是这段代码:

临时保存当前地址
临时保存当前地址

uservec:表明这是用户态到内核态切换时的预先执行模块,在这个模块中,第一条执行的指令是:csrrw a0, sscratch a0,这行代码的意思是在内核中临时保存用户态信息的首个地址,保证最后跳转到用户态后,能够继续正确从用户态的首地址中取出正确信息

下一段代码:

保存用户态的信息
保存用户态的信息

我们看到是32条sd指令,每条sd指令根据用户态首地址信息的偏移量,将用户态对应寄存器的值保存到内核态的寄存器中,保存了上下文信息

下一段代码:

保存其他信息以及跳转到内核程序
保存其他信息以及跳转到内核程序

下面依次解释一下每行代码作用:

csrr t0, sscratch | sd t0, 112(a0):保存a0寄存器信息到内核态,a0存储用户态地址,方便返回

ld sp, 8(a0):加载内核态的栈指针(stack pointer),方便从内核对应的栈中取出寄存器信息

ld tp, 32(a0):设置当前线程id,保存任务执行的唯一信息

ld t0, 16(a0):加载用户处理程序(usertrap)的地址到寄存器t0中

ld t1, 0(a0) | csrw satp, t1 | sfence.vma zero, zero:恢复内核中的页表,方便从内核中取出数据处理程序信息

jr t0:跳转到对应的usertrap执行地址t0中,执行接下来的程序

实际上其实操作系统就是在将当前内核需要的信息保存到内核态对应的trapframe中,也就是下图展示的这些字段,方便后续执行程序取出对应值,

trapframe字段展示
trapframe字段展示
Step 3: Usertrap

调用trap.c代码中的usertrap函数进行用户程序的处理:

trap.c代码的usertrap函数
trap.c代码的usertrap函数

我们看到,这里系统会通过w_stvec将对usertrap地址进行保存,之后将sepc(用户态程序计数器pc)保存到当前进程的p->trapframe->epc中,因为返回到用户态后要执行用户态对应的下一条程序,所以对epc进行+4,指向下一条地址,完了打开中断(intr_on),之后调用syscall()函数执行相应方法

Step 4:System call's code

这里会根据SYS_write的值进行匹配调用具体内部代码sys_write,这部分代码在sysproc.c中,至此,用户态到内核态的执行就结束了

调用系统代码
调用系统代码

内核态->用户态

如果对用户态到内核态的转变清楚了,其实内核态到用户态的转变就是将上述流程反过来,因为在切换状态时,已经保存了之前的上下文信息,所以只需要恢复对应的寄存器、程序首地址、程序计数器等信息,再跳转回用户程序即可,这里通过具体代码再来理解一下:

首先在trap.c中,我们执行完syscall()对应的编译代码后,会跳转到usertrapret()函数中,来完成跳转到用户态的操作

trap.c内核态到用户态的切换
trap.c内核态到用户态的切换

接下来在usertrapret()函数中,我们会将用户态对应的页表地址(satp)、栈针地址(sp)、编号(hartid)等信息恢复,之后通过计算trampoline中的userret地址,再次返回到trampoline中,执行接下来的userret操作

usertrapret()函数返回到用户态
usertrapret()函数返回到用户态

在接下来trampoline的userret中,将原本的用户态首地址信息恢复,之后恢复用户态的寄存器信息,恢复用户态的返回,最后返回到用户态中,至此,整个程序的执行流程就结束了

userret返回到用户态
userret返回到用户态
userret返回到用户态
userret返回到用户态

最后附上一张操作系统内核态与用户态切换的完整流程图片,有兴趣的小伙伴可以自行追踪一下xv6底层源码:

内核态与用户态切换完整流程
内核态与用户态切换完整流程

如果你看到了这里,一定说明你对操作系统的状态切换有浓厚的兴趣,创作不易,希望能对你有帮助,祝好!!!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 调度寄存器及基础概念介绍
  • 操作系统切换状态流程介绍
    • 用户态->内核态
      • Step1:System call function
      • Step2: Trampoline
      • Step 3: Usertrap
      • Step 4:System call's code
    • 内核态->用户态
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档