用户空间和内核空间之间的切换通常称为trap

trap的三种形式
  1. 系统调用引发
  2. 异常发生
  3. 设备中断 (时间中断、IO中断、网络中断等)
supervise mode的权限

用户态和内核态之间的到底有什么区别?其实区别很小:

其中的一件事情是,你现在可以读写控制寄存器了。比如说,当你在supervisor mode时,你可以:读写SATP寄存器,也就是page table的指针;STVEC,也就是处理trap的内核指令地址;SEPC,保存当发生trap时的程序计数器;SSCRATCH等等。在supervisor mode你可以读写这些寄存器,而用户代码不能做这样的操作。

另一件事情supervisor mode可以做的是,它可以使用PTE_U标志位为0的PTE(页表项 详见页表)。当PTE_U标志位为1的时候,表明用户代码可以使用这个页表;如果这个标志位为0,则只有supervisor mode可以使用这个页表。

这也是supervise mode唯二可以做的事情了。

ecall指令

系统使用ecall指令来执行系统调用

第一,ecall将代码从user mode改到supervisor mode

第二,ecall将程序计数器的值保存在了SEPC寄存器。

第三,ecall会跳转到STVEC寄存器指向的指令 即trampoline.S

trap的全过程

exec系统调用为例。

首先,用户程序中调用exec函数,编译器解析exec函数时,并不会调用其源码,而是将exec系统调用的编号写入A7寄存器中,执行ecall指令,ecall指令将跳转到trampoline.S。

系统从用户态进入了内核态,但目前页表依旧是user page table。trampoline.S主要是使用trapFrame(附录部分)保存了系统的状态以及用户寄存器,用于恢复用户进程,通过这个操作也实现用户进程对于进程切换的无感知。在将需要存储的状态存入user page table后,系统将切换内核页表,执行相应的内核代码,即usertrap函数。

usertrap根据trap的不同情况(scause寄存器中存储着中断原因),调用相应的处理函数。

代码解析

下面的代码是操作系统的源代码,里面包含了很多的细节,你需要里了解的是整个流程、体系,不要过多的关注细节

trap的保存流程
  1. 首先,我们需要保存32个用户寄存器。因为很显然我们需要恢复用户应用程序的执行,尤其是当用户程序随机的被设备中断所打断时。(存储用户寄存器)
  2. 程序计数器也需要在某个地方保存,它几乎跟一个用户寄存器的地位是一样的,我们需要能够在用户程序运行中断的位置继续执行用户程序。(存储一些系统寄存器 如PC)
  3. 我们需要将mode改成supervisor mode,因为我们想要使用内核中的各种各样的特权指令。(修改运行模式)
  4. SATP(Supervisor Address Translation and Protection,存储了根页表的地址)寄存器现在正指向user page table,而user page table只包含了用户程序所需要的内存映射和一两个其他的映射,它并没有包含整个内核数据的内存映射。所以在运行内核代码之前,我们需要将SATP指向kernel page table。(修改SATP,获得内核的页表)
  5. 我们需要将堆栈寄存器指向位于内核的一个地址,因为我们需要一个堆栈来调用内核的C函数。(转换到内核堆栈)
  6. 一旦我们设置好了,并且所有的硬件状态都适合在内核中使用, 我们需要跳入内核的C代码usertrap函数。
trampoline.S
	#
# code to switch between user and kernel space.
#
# this code is mapped at the same virtual address
# (TRAMPOLINE) in user and kernel space so that
# it continues to work when it switches page tables.
#
# kernel.ld causes this to be aligned
# to a page boundary.
#
.section trampsec
.globl trampoline
trampoline:
.align 4
.globl uservec
//trap状态保存
uservec:
#
# trap.c sets stvec to point here, so
# traps from user space start here,
# in supervisor mode, but with a
# user page table.
#
# sscratch points to where the process's p->trapframe is
# mapped into user space, at TRAPFRAME.
# # swap a0 and sscratch
# so that a0 is TRAPFRAME
csrrw a0, sscratch, a0 # save the user registers in TRAPFRAME
sd ra, 40(a0)
sd sp, 48(a0)
sd gp, 56(a0)
sd tp, 64(a0)
sd t0, 72(a0)
sd t1, 80(a0)
sd t2, 88(a0)
sd s0, 96(a0)
sd s1, 104(a0)
sd a1, 120(a0)
sd a2, 128(a0)
sd a3, 136(a0)
sd a4, 144(a0)
sd a5, 152(a0)
sd a6, 160(a0)
sd a7, 168(a0)
sd s2, 176(a0)
sd s3, 184(a0)
sd s4, 192(a0)
sd s5, 200(a0)
sd s6, 208(a0)
sd s7, 216(a0)
sd s8, 224(a0)
sd s9, 232(a0)
sd s10, 240(a0)
sd s11, 248(a0)
sd t3, 256(a0)
sd t4, 264(a0)
sd t5, 272(a0)
sd t6, 280(a0) # save the user a0 in p->trapframe->a0
csrr t0, sscratch
sd t0, 112(a0) # restore kernel stack pointer from p->trapframe->kernel_sp
ld sp, 8(a0) # make tp hold the current hartid, from p->trapframe->kernel_hartid
ld tp, 32(a0) # load the address of usertrap(), p->trapframe->kernel_trap
ld t0, 16(a0) # restore kernel page table from p->trapframe->kernel_satp
ld t1, 0(a0)
csrw satp, t1
sfence.vma zero, zero # a0 is no longer valid, since the kernel page
# table does not specially map p->tf. # jump to usertrap(), which does not return
jr t0
//trap状态恢复
.globl userret
userret:
# userret(TRAPFRAME, pagetable)
# switch from kernel to user.
# usertrapret() calls here.
# a0: TRAPFRAME, in user page table.
# a1: user page table, for satp. # switch to the user page table.
csrw satp, a1
sfence.vma zero, zero # put the saved user a0 in sscratch, so we
# can swap it with our a0 (TRAPFRAME) in the last step.
ld t0, 112(a0)
csrw sscratch, t0 # restore all but a0 from TRAPFRAME
ld ra, 40(a0)
ld sp, 48(a0)
ld gp, 56(a0)
ld tp, 64(a0)
ld t0, 72(a0)
ld t1, 80(a0)
ld t2, 88(a0)
ld s0, 96(a0)
ld s1, 104(a0)
ld a1, 120(a0)
ld a2, 128(a0)
ld a3, 136(a0)
ld a4, 144(a0)
ld a5, 152(a0)
ld a6, 160(a0)
ld a7, 168(a0)
ld s2, 176(a0)
ld s3, 184(a0)
ld s4, 192(a0)
ld s5, 200(a0)
ld s6, 208(a0)
ld s7, 216(a0)
ld s8, 224(a0)
ld s9, 232(a0)
ld s10, 240(a0)
ld s11, 248(a0)
ld t3, 256(a0)
ld t4, 264(a0)
ld t5, 272(a0)
ld t6, 280(a0) # restore user a0, and save TRAPFRAME in sscratch
csrrw a0, sscratch, a0 # return to user mode and user pc.
# usertrapret() set up sstatus and sepc.
sret
用户trap处理程序
void
usertrap(void)
{
int which_dev = 0; //判断系统mode状态
if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode"); // send interrupts and exceptions to kerneltrap(),
// since we're now in the kernel.
// 将中断处理程序设置为kernelvec
// 即将kernelvec函数地址写入到stvec寄存器
// 和user model下的中断对应,kernelvec用来处理内核态下的中断
// 两个流程很相似,kernelvec更简单一些,就不做解释了
w_stvec((uint64)kernelvec); struct proc *p = myproc(); //保存用户的PC值
// save user program counter.
p->trapframe->epc = r_sepc(); //根据scause寄存器的值判断异常原因
if(r_scause() == 8){
// system call if(p->killed)
exit(-1); // sepc points to the ecall instruction,
// but we want to return to the next instruction.
// 设置返回位置
p->trapframe->epc += 4; // an interrupt will change sstatus &c registers,
// so don't enable until done with those registers.
// 寄存器保存完成,开中断
intr_on();
// 执行系统调用
syscall();
} else if((which_dev = devintr()) != 0){
// 处理设备中断
// ok
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
} if(p->killed)
exit(-1); // give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield(); //trap处理完毕,恢复用户进程状态
usertrapret();
}

附:

系统调用函数 (根据A7寄存器存储的内容,调用不同的函数指针)

extern uint64 sys_chdir(void);
extern uint64 sys_close(void);
extern uint64 sys_dup(void);
extern uint64 sys_exec(void);
extern uint64 sys_exit(void);
extern uint64 sys_fork(void);
extern uint64 sys_fstat(void);
extern uint64 sys_getpid(void);
extern uint64 sys_kill(void);
extern uint64 sys_link(void);
extern uint64 sys_mkdir(void);
extern uint64 sys_mknod(void);
extern uint64 sys_open(void);
extern uint64 sys_pipe(void);
extern uint64 sys_read(void);
extern uint64 sys_sbrk(void);
extern uint64 sys_sleep(void);
extern uint64 sys_unlink(void);
extern uint64 sys_wait(void);
extern uint64 sys_write(void);
extern uint64 sys_uptime(void); static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
[SYS_wait] sys_wait,
[SYS_pipe] sys_pipe,
[SYS_read] sys_read,
[SYS_kill] sys_kill,
[SYS_exec] sys_exec,
[SYS_fstat] sys_fstat,
[SYS_chdir] sys_chdir,
[SYS_dup] sys_dup,
[SYS_getpid] sys_getpid,
[SYS_sbrk] sys_sbrk,
[SYS_sleep] sys_sleep,
[SYS_uptime] sys_uptime,
[SYS_open] sys_open,
[SYS_write] sys_write,
[SYS_mknod] sys_mknod,
[SYS_unlink] sys_unlink,
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
}; void
syscall(void)
{
int num;
struct proc *p = myproc(); num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}

Trap (陷入/中断) 源码解析的更多相关文章

  1. Java并发之ThreadPoolExecutor源码解析(二)

    ThreadPoolExecutor ThreadPoolExecutor是ExecutorService的一种实现,可以用若干已经池化的线程执行被提交的任务.使用线程池可以帮助我们限定和整合程序资源 ...

  2. Java并发之ReentrantLock源码解析(二)

    在了解如何加锁时候,我们再来了解如何解锁.可重入互斥锁ReentrantLock的解锁方法unlock()并不区分是公平锁还是非公平锁,Sync类并没有实现release(int arg)方法,这里会 ...

  3. Java并发之ReentrantLock源码解析(四)

    Condition 在上一章中,我们大概了解了Condition的使用,下面我们来看看Condition再juc的实现.juc下Condition本质上是一个接口,它只定义了这个接口的使用方式,具体的 ...

  4. Java并发之Semaphore源码解析(一)

    Semaphore 前情提要:在学习本章前,需要先了解笔者先前讲解过的ReentrantLock源码解析,ReentrantLock源码解析里介绍的方法有很多是本章的铺垫.下面,我们进入本章正题Sem ...

  5. Java并发之Semaphore源码解析(二)

    在上一章,我们学习了信号量(Semaphore)是如何请求许可证的,下面我们来看看要如何归还许可证. 可以看到当我们要归还许可证时,不论是调用release()或是release(int permit ...

  6. 源码解析Synchronous Queue 这种特立独行的队列

    摘要:Synchronous Queue 是一种特立独行的队列,其本身是没有容量的,比如调用者放一个数据到队列中,调用者是不能够立马返回的,调用者必须等待别人把我放进去的数据消费掉了,才能够返回. 本 ...

  7. jQuery2.x源码解析(设计篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 这一篇笔者主要以设计的角度探索jQuery的源代 ...

  8. Python2 基本数据结构源码解析

    Python2 基本数据结构源码解析 Contents 0x00. Preface 0x01. PyObject 0x01. PyIntObject 0x02. PyFloatObject 0x04. ...

  9. Heritrix 3.1.0 源码解析(三十七)

    今天有兴趣重新看了一下heritrix3.1.0系统里面的线程池源码,heritrix系统没有采用java的cocurrency包里面的并发框架,而是采用了线程组ThreadGroup类来实现线程池的 ...

随机推荐

  1. js中date类型的格式转化为yyyy-MM-dd HH:mm:ss的String类型

    在vue中或其他框架中可以在Date的原型链中添加Format的方法,如ruoyi可以写在main.js中更好,如果写在utils还需要去导入包. 正常的js直接放到utils.js就好 Date.p ...

  2. Docker-Compose的一些常用命令

    一.Docker-Compose简介 1.Docker-Compose简介 Docker-Compose项目是Docker官方的开源项目,负责实现对Docker容器集群的快速编排. Docker-Co ...

  3. Redis新旧复制

    在Redis中,用户可以通过执行SALVEOF命令,让一个服务器去复制另一个服务器. 127.0.0.1:12345> SLAVEOF 127.0.0.1 6379 OK 6379的奴隶是123 ...

  4. amber模拟kcl水溶液

    最近刚开始学习amber软件,看网上的教程勉强知道怎么操作这个amber了.就暂时跑了个分子动力学,其他的啥也没处理.先把我的操作过程记录下来吧,免得日后忘记. 一.构建kcl.pdb结构 利用Gau ...

  5. dbus客户端使用指南

    DBus是Linux使用的进程间通信机制,允许各个进程互相访问,而不需要为每个其他组件实现自定义代码.即使对于系统管理员来说,这也是一个相当深奥的主题,但它确实有助于解释linux的另一部分是如何工作 ...

  6. Java秘诀!Java赋值运算符介绍

    运算符丰富是 Java 语言的主要特点之一,它提供的运算符数量之多,在高级语言中是少见的. Java 语言中的运算符除了具有优先级之外,还有结合性的特点.当一个表达式中出现多种运算符时,执行的先后顺序 ...

  7. Java标识符和关键字的区别!java基础 java必学

    任何计算机语言都离不开标识符和关键字,那我们就来简单讲一下他们两者的区别,希望有助于大家的的理解!本篇文章干货满满,如果你觉得难懂的话可以看下高淇老师讲的Java300集的教学视频,分选集,深度剖析了 ...

  8. 题解 2020.10.24 考试 T2 选数

    题目传送门 题目大意 见题面. 思路 本来以为zcx.pxj变强了,后来发现是SPJ出问题了...考试的时候感觉有点人均啊...结果自己还是只想出来一半. 我们假设 \(f(x)=(\lfloor\f ...

  9. bzoj3262陌上花开 (CDQ,BIT)

    题目大意 给定n朵花,每个花有三个属性,定义\(f[i]\)为满足\(a_j \le a_i\)且\(b_j \le b_i\)且\(c_j \le c_i\)的j的数量, 求\(d \in [0,n ...

  10. Java:修饰符小记

    Java:修饰符小记 对 Java 中的 修饰符,做一个微不足道的小小小小记 Java 语言提供了很多修饰符,大概分为两类: 访问权限修饰符 非访问权限修饰符 访问权限修饰符 修饰符 说明 publi ...