操作系统MIT6.S081:P5->Isolation & system call entry/exit

本文详细解析了MIT6.S081操作系统课程中关于用户空间与内核空间切换的过程,涵盖了ecall指令、trap机制、页表切换、寄存器保存等多个关键步骤。通过分析XV6操作系统,展示了从用户空间执行系统调用如何进入内核空间,执行内核代码,然后安全返回用户空间的完整流程。

本系列文章为MIT6.S081的学习笔记,包含了参考手册、课程、实验三部分的内容,前面的系列文章链接如下
操作系统MIT6.S081:[xv6参考手册第1章]->操作系统接口
操作系统MIT6.S081:P1->Introduction and examples
操作系统MIT6.S081:Lab1->Unix utilities
操作系统MIT6.S081:[xv6参考手册第2章]->操作系统组织结构
操作系统MIT6.S081:P2->OS organization and system calls
操作系统MIT6.S081:Lab2->System calls
操作系统MIT6.S081:[xv6参考手册第3章]->页表
操作系统MIT6.S081:P3->Page tables
操作系统MIT6.S081:Lab3->Page tables
操作系统MIT6.S081:P4->RISC-V calling conventions and stack frames
操作系统MIT6.S081:[xv6参考手册第4章]->Trap与系统调用


一、trap机制

trap是什么

掌握程序运行时如何完成用户空间和内核空间的切换是非常必要的。比如
①每当程序执行系统调用
②程序出现了类似page fault、运算时除以0的错误
③一个设备触发了中断,使得当前程序运行需要响应内核设备驱动
都会发生这样的切换 ,这里用户空间和内核空间的切换通常被称为trap。trap涉及了许多精心的设计和重要的细节,这些细节对于实现安全隔离和性能来说非常重要。很多应用程序因为系统调用、page fault等原因会频繁地切换到内核中,所以trap机制还需要要尽可能的简单。

trap的工作流程

trap的使用场景
初始的场景如下所示。我们有一些用户应用程序(例如Shell)运行在用户空间,同时我们还有内核空间。Shell可能会执行系统调用,将程序运行切换到内核运行。比如XV6启动之后Shell输出的一些提示信息,就是通过执行write系统调用来输出的。
在这里插入图片描述
我们需要清楚如何让程序的运行,从只拥有user权限并且位于用户空间的Shell,切换到拥有supervisor权限的内核。
寄存器状态
在这个过程中,硬件的状态将会非常重要,因为我们很多的工作都是将硬件从适合运行用户应用程序的状态改变到适合运行内核代码的状态。我们最关心的状态可能是32个用户寄存器,这在上节课中有介绍。RISC-V总共有32个比如a0a1这样的寄存器,用户应用程序可以使用全部的寄存器以获取高性能。
在这里插入图片描述
这里的很多寄存器都有特殊的作用,我们之后都会看到。
栈指针(Stack Pointer),也叫做栈寄存器,保存栈帧的地址。
程序计数器(Program Counter Register)
模式(Mode),表明当前模式的标志位。这个标志位表明了当前是supervisor mode还是user mode。当我们在运行Shell的时候,自然是在user mode。
还有一堆控制CPU工作方式的寄存器:
SATP(Supervisor Address Translation and Protection)寄存器,它包含了指向页表的物理内存地址。
还有一些对于今天讨论非常重要的寄存器:
STVEC(Supervisor Trap Vector Base Address Register)寄存器,它指向了内核中处理trap的指令的起始地址。
SEPC(Supervisor Exception Program Counter)寄存器,在trap的过程中保存程序计数器的值。
SSCRATCH(Supervisor Scratch Register)寄存器。
这些寄存器表明了执行系统调用时计算机的状态。
在这里插入图片描述
寄存器状态修改
可以肯定的是,在trap的最开始,CPU的所有状态都设置成运行用户代码而不是内核代码。在trap处理的过程中,我们需要更改一些这里的状态,或者对状态做一些操作,这样我们才可以运行系统内核中普通的C程序。接下来我们预览一下需要做的操作:
①首先,我们需要保存32个用户寄存器。因为很显然我们需要恢复用户应用程序的执行,尤其是当用户程序随机地被设备中断所打断时,我们希望内核能够响应中断,之后在用户程序完全无感知的情况下再恢复用户代码的执行。所以这意味着32个用户寄存器不能被内核弄乱。但是这些寄存器又要被内核代码所使用,所以在trap之前,你必须先在某处保存这32个用户寄存器。
②程序计数器也需要在某个地方保存,它几乎跟一个用户寄存器的地位是一样的,我们需要能够在用户程序运行中断的位置继续执行用户程序。
③我们需要将mode改成supervisor mode,因为我们想要使用内核中的各种各样的特权指令。
satp寄存器现在正指向用户页表,而用户页表只包含了用户程序所需要的内存映射和一两个其他的映射,它并没有包含整个内核数据的内存映射。所以在运行内核代码之前,我们需要将satp指向内核页表。
⑤我们需要将栈寄存器指向位于内核的一个地址,因为我们需要一个栈来调用内核的C函数。
一旦我们设置好了,并且所有的硬件状态都适合在内核中使用, 我们需要跳入内核的C代码。一旦我们运行在内核的C代码中,那就跟平常的C代码是一样的。

trap机制的一些注意事项

之后我们会讨论内核通过C代码做了什么工作,但是今天讨论的是如何将将程序执行从用户空间切换到内核的一个位置,这样我们才能运行内核的C代码。
①操作系统的一些high-level的目标限制了这里的实现,其中一个目标是安全和隔离。我们不想让用户代码干扰到这里的user/kernel切换,否则有可能会破坏安全性。所以这意味着,trap中涉及到的硬件和内核机制不能依赖任何来自用户空间东西。比如说我们不能依赖32个用户寄存器,它们可能保存的是恶意的数据。所以,XV6的trap机制不会查看这些寄存器,而只是将它们保存起来。
②在操作系统的trap机制中,我们想保留隔离性并防御来自用户代码的可能的恶意攻击。但另一方面,我们想要让trap机制对用户代码是透明的。也就是说,我们想要执行trap,然后在内核中执行代码,之后再恢复代码到用户空间。这个过程中,用户代码并不会注意到发生了什么,这样也就更容易编写用户代码。
③需要注意的是,虽然我们这里关心隔离和安全,但是今天我们只会讨论从用户空间切换到内核空间相关的安全问题。当然,系统调用的具体实现(如write在内核的具体实现)以及内核中任何的代码也必须小心并安全地写好。因此,即使从用户空间到内核空间的切换十分安全,整个内核的其他部分也必须非常安全,并时刻小心用户代码可能会尝试欺骗它。
④在前面介绍的寄存器中,保存mode标志的寄存器需要讨论一下。当我们在用户空间时,这个标志位对应的是user mode,当我们在内核空间时,这个标志位对应supervisor mode。但是有一点很重要:当这个标志位从user mode变更到supervisor mode时,我们能得到什么样的权限。实际上,这里获得的额外权限实在是有限。也就是说,并不是像你可以在supervisor mode完成但是不能在user mode完成一些工作那么有特权。我们接下来看看supervisor mode可以控制什么。
----可以读写控制寄存器。比如说,当你在supervisor mode时,你可以:读写SATP寄存器,也就是页表的指针;STVEC,也就是处理trap的内核指令地址;SEPC,保存当发生trap时的程序计数器;SSCRATCH等等。在supervisor mode你可以读写这些寄存器,而用户代码不能做这样的操作。
----可以使用PTE_U标志位为0的PTE。当PTE_U标志位为1时,表明用户代码可以使用这个页表。如果这个标志位为0,则只有supervisor mode可以使用这个页表。
这两点就是supervisor mode可以做的事情,除此之外就不能再干别的事情了。
在这里插入图片描述
注意: 需要特别指出的是,supervisor mode中的代码并不能读写任意物理地址。在supervisor mode中,就像普通的用户代码一样,也需要通过页表来访问内存。如果一个虚拟地址并不在当前由SATP指向的页表中,又或者SATP指向的页表中PTE_U=1,那么supervisor mode不能使用那个地址。所以,即使我们在supervisor mode,我们还是受限于当前页表设置的虚拟地址。


二、trap代码执行流程

trap代码执行流程

我们跟踪如何在Shell中调用write系统调用。从Shell的角度来说,这就是个Shell代码中的C函数调用。但是实际上,write通过执行ecall指令来执行系统调用,ecall指令会切换到具有supervisor mode的内核中。
①在这个过程中,内核中执行的第一个指令是一个由汇编语言写的函数,叫做uservec。这个函数是内核代码trampoline.s文件的一部分。
在这里插入图片描述
之后,在这个汇编函数中,代码执行跳转到了由C语言实现的函数usertrap中,这个函数在trap.c中。
在这里插入图片描述
现在代码运行在C中,所以代码更加容易理解。在usertrap这个C函数中,我们执行了一个叫做syscall的函数。
在这里插入图片描述
这个函数会在一个表单中,根据传入的代表系统调用的数字进行查找,并在内核中执行具体实现了系统调用功能的函数。对于我们来说,这个函数就是sys_write
在这里插入图片描述
sys_write会将要显示数据输出到console上。当它完成了之后,它会返回到syscall函数,然后syscall函数返回到用户空间。
在这里插入图片描述
因为我们现在相当于在ecall之后中断了用户代码的执行,为了用户空间的代码恢复执行,需要做一系列的事情。在syscall函数中,会调用一个函数叫做usertrapret,它也位于trap.c中,这个函数完成了部分在C代码中实现的返回到用户空间的工作。
在这里插入图片描述
除此之外,最终还有一些工作只能在汇编语言中完成。这部分工作通过汇编语言实现,并且存在于trampoline.s文件中的userret函数中。
在这里插入图片描述
最终,在这个汇编函数中会调用机器指令返回到用户空间,并且恢复ecall之后的用户程序的执行。
在这里插入图片描述

问答

学生提问vm.c运行在什么mode下?
Robert教授vm.c中的所有函数都是内核的一部分,所以运行在supervisor mode。
学生提问:为什么这些函数叫这些名字?
Robert教授:现在的函数命名比较乱,明年我会让它们变得更加合理一些。(助教说:我认为命名与寄存器的名字有关)。
学生提问:难道vm.c里的函数不是要直接访问物理内存吗?
Robert教授:是的,这些函数能这么做的原因是:内核在页表中精心地设置好直接映射关系,这样当内核收到了一个读写虚拟内存地址的请求,会通过内核页表将这个虚拟内存地址翻译成与之等价物理内存地址,再完成读写。所以,一旦使用了内核页表,就可以非常方便的在内核中使用所有这些直接的映射关系。但是直到trap机制切换到内核之前,这些映射关系都不可用。直到trap机制将程序运行切换到内核空间之前,我们使用的仍然是用户页表。
学生提问:这个问题或许并不完全相关,readwrite系统调用相比内存的读写,它们的代价都高的多,因为它们需要切换模式并来回捣腾。有没有可能当你执行打开一个文件的系统调用时, 直接得到一个页表映射,而不是返回一个文件描述符?这样只需要向对应于设备的特定的地址写数据,程序就能通过页表访问特定的设备。你可以设置好限制,就像文件描述符只允许修改特定文件一样,这样就不用像系统调用一样在用户空间和内核空间来回捣腾了。
Robert教授:这是个很好的想法,实际上很多操作系统都提供这种叫做内存映射文件访问(MMAP, Memory-mapped file access)的机制。在这个机制里面通过页表可以将用户空间的虚拟地址空间对应到文件内容,这样你就可以通过内存地址直接读写文件。实际上,你们将在mmap实验中完成这个机制。对于许多程序来说,这个机制的确会比直接调用read/write系统调用要快的多。


三、ecall指令之前的状态

系统调用大致流程

接下来我们将切换到gdb中,跟踪一个XV6的系统调用,也就是Shell将它的提示信息通过write系统调用走到操作系统再输出到console的过程。你们可以看到,用户代码sh.c初始了这一切。
在这里插入图片描述
上图中选中的行,是一个write系统调用,它将“$”写入到文件描述符2。接下来我将打开gdb并启动XV6。
在这里插入图片描述
作为用户代码的Shell调用write时,实际上调用的是关联到Shell的一个库函数。你可以查看这个库函数的源代码,在usys.s中。

当使用 `docker start elasticsearch` 启动容器时出现 `iptables: No chain/target/match by that name` 错误,通常是由于 Docker 网络配置与 iptables 规则不匹配导致的,以下是一些可能的解决办法: ### 重启 Docker 引擎服务 重启 Docker 引擎服务可能会将必要的规则重新添加到 iptables 列表中。可以使用以下命令来重启 Docker 服务: ```bash sudo systemctl restart docker ``` ### 检查防火墙状态 如果使用的是 CentOS 7 服务器,在部署 Docker 的过程中启停过 `firewalld` 服务,可能会影响 iptables 规则。因为在 CentOS 7 里使用 `firewalld` 代替了 `iptables`,但启动 `firewalld` 后,`iptables` 仍会被使用。可以尝试停止 `firewalld` 服务并禁用它: ```bash sudo systemctl stop firewalld sudo systemctl disable firewalld ``` 或者配置 `firewalld` 允许 Docker 相关的网络流量。 ### 检查网关状态 如果在启动 Docker 服务时网关是关闭的,而之后网关重新启动,可能会导致 Docker 网络无法对新容器进行网络配置。可以尝试重启 Docker 服务以更新网络配置: ```bash sudo systemctl restart docker ``` ### 手动创建缺失的链 可以手动创建 Docker 所需的 iptables 链。以下是一些示例命令: ```bash sudo iptables -t nat -N DOCKER sudo iptables -t filter -N DOCKER sudo iptables -t filter -N DOCKER-ISOLATION-STAGE-1 sudo iptables -t filter -N DOCKER-ISOLATION-STAGE-2 sudo iptables -t filter -N DOCKER-USER ``` ### 重建 iptables 规则 可以尝试清除 iptables 规则并重新加载 Docker: ```bash sudo iptables -F sudo iptables -t nat -F sudo systemctl restart docker ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知初与修一

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值