MIT6.828 Lab2 system call

简介

  • 实验:lab system cacall
  • In this lab you will add some new system calls to xv6, which will help you understand how they work and will expose you to some of the internals of the xv6 kernel.
  • Before you start coding, read Chapter 2 of the xv6 book, and Sections 4.3 and 4.4 of Chapter 4[[Lab-4:Traps]], and related source files:
    • The user-space “stubs” that route system calls into the kernel are in user/usys.S, which is generated by user/usys.pl when you run make. Declarations are in user/user.h
    • The kernel-space code that routes a system call to the kernel function that implements it is in kernel/syscall.c and kernel/syscall.h.
    • Process-related code is kernel/proc.h and kernel/proc.c.

一、参考讲义概述

  • 操作系统需满足: #多路复用 、 #隔离 、 #交互
  • 本章提供了实现这三点的概述
  • xv6是基于“LP64”C编写的,因此int为32位,long和pointer为64位

1.1 Abstracting physical resources

![[Pasted image 20240406202504.png]]
如果应用可以定制化自己的库函数,这些库函数可以直接作用在物理资源上,那么这些应用必须是良好运行的,然而更常见的是应用程序彼此不信任并且存在错误,因此需要更强的隔离性。将资源抽象为服务,有助于实现强隔离,禁止应用程序直接访问敏感硬件资源。
抽象物理资源:

  • 例如,Unix应用程序仅通过文件系统的open、read、write和close系统调用与存储交互,而不是直接读写磁盘。
  • Unix 进程使用 exec 来构建其内存映像,而不是直接与物理内存交互。
  • Unix 进程之间的许多交互形式都是通过文件描述符发生的。
  • Unix 接口并不是抽象资源的唯一方法,但它已被证明是一种非常好的方法。

1.2 User mode, supervisor mode, and system calls

强隔离需要应用程序和操作系统之间有硬边界。操作系统应该能够清理失败的应用程序并继续运行其他应用程序。为了实现强隔离,操作系统必须安排应用程序不能修改(甚至读取)操作系统的数据结构和指令,并且应用程序不能访问其他进程的内存。CPU为强隔离提供硬件支持,例如,RISC-V具有CPU执行指令的三种模式:机器模式、管理模式和用户模式。

  • CPU 以机器模式启动。机器模式主要用于配置计算机。
  • 在管理模式下,CPU 可以执行特权指令,运行在内核空间(或管理模式)的软件称为内核。
  • 应用程序只能执行用户模式指令(例如,加数字等),并且被称为在用户空间中运行。想要调用内核函数(例如 xv6 中的 read 系统调用)的应用程序必须转换到内核;应用程序不能直接调用内核函数

1.3 Kernel organization

操作系统的哪一部分应该在管理模式下运行?

  • 一种可能性是整个操作系统驻留在内核中,因此所有系统调用的实现都在管理程序模式下运行。这种组织称为 #整体内核。
    • 单一组织的缺点是操作系统不同部分之间的接口通常很复杂
    • 操作系统开发人员很容易犯错误。
    • 在整体内核中,一个错误是致命的,因为管理模式下的错误往往会导致内核失败。如果内核出现故障,计算机就会停止工作,因此所有应用程序也会失败。计算机必须重新启动才能重新启动。
  • 最大限度地减少在管理模式下运行的操作系统代码量,并在用户模式下执行操作系统的大部分内容。这种内核组织称为 #微内核。
    • 在微内核中,内核接口由一些用于启动应用程序的低级函数组成,发送消息、访问设备硬件等。
  • 与大多数 Unix 操作系统一样,Xv6 是作为整体内核实现的。由此可见,xv6内核接口对应于操作系统接口,内核实现了完整的操作系统。由于 xv6 不提供很多服务,因此它的内核比一些微内核要小,但从概念上讲 xv6 是整体的。

1.4 Code: xv6 organization

  • xv6 内核源代码位于 kernel/ 子目录中。源代码被分成文件,遵循模块化的粗略概念。
  • 模块间接口定义在defs.h (kernel/defs.h)
FileDescription
bio.c文件系统的磁盘块缓存
console.c连接到用户键盘和屏幕
entry.s首次启动说明
exec.cexec() 系统调用
file.c文件描述符支持
fs.c文件系统
kalloc.c物理页分配器
kernelvec.S处理来自内核的陷阱和定时器中断
log.c文件系统日志记录和崩溃恢复
mian.c启动时控制其他模块的初始化
pipe.c管道
plic.cRISC-V 中断控制器
printf.c格式化输出到控制台
proc.c流程和调度
sleeplock.c释放 CPU 的锁
spinlock.c不让出 CPU 的锁
start.c早期机器模式启动代码
string.cC 字符串和字节数组库
swtch.S线程切换
syscall.c将系统调用分派给处理函数
sysfile.c文件相关的系统调用
syspro.c进程相关的系统调用
tarmpoline.S在用户和内核之间切换的汇编代码
trap.c用于处理陷阱和中断并从中返回的 C 代码
uart.c串口控制台设备驱动程序
virtio_disk.c磁盘设备驱动程序
vm.c管理页表和地址空间

1.5 Process overview

进程作用

  • xv6 中的隔离单位(与其他 Unix 操作系统一样)是一个进程
  • 进程抽象可防止一个进程破坏或监视另一进程的内存、CPU、文件描述符等。
  • 可以防止进程破坏内核本身,从而使进程无法破坏内核的隔离机制。
  • 内核必须小心地实现进程抽象,因为有错误或恶意的应用程序可能会欺骗内核或硬件做一些坏事
  • 内核用于实现进程的机制包括用户/管理程序模式标志、地址空间和线程的时间分片。

进程虚拟地址空间的布局
![[Pasted image 20240406222111.png|400]]

  • Xv6 使用页表(由硬件实现)为每个进程提供自己的地址空间。
  • Xv6 为每个进程维护一个单独的页表,用于定义该进程的地址空间。
  • 限制进程地址空间的最大大小的因素:
    • RISC-V 上的指针是 64 bit
    • 在页表中查找虚拟地址时,硬件仅使用低 39 bit
    • xv6 仅使用这 39 位中的 38 位
    • 因此MAXVA=2^38 − 1 = 0x3fffffffff
  • 在地址空间的顶部,xv6 保留一个用于trampoline的页面和一个映射进程的 trapframe 的页面。 Xv6 使用这两个页面来转换内核并返回
  • Trampoline 页包含进出内核的代码,映射 trapframe 对于保存/恢复用户进程的状态是必要的
  • 在xv6中,一个进程由一个地址空间和一个线程组成。在实际操作系统中,一个进程可能有多个线程来利用多个 CPU。

进程和线程

  • 在xv6中,一个进程由一个地址空间和一个线程组成。在实际操作系统中,一个进程可能有多个线程来利用多个 CPU。
  • 每个 #进程 有两个堆栈: #用户堆栈 和 #内核堆栈 (p->kstack),当进程执行用户指令时,只有其用户堆栈在使用,其内核堆栈为空。当进程进入内核(进行系统调用或中断)时,内核代码在进程的内核堆栈上执行,此时,其用户堆栈仍然包含保存的数据,但没有被主动使用。
    • 进程捆绑了两种设计思想:地址空间,让进程有自己内存的错觉;线程,让进程有自己 CPU 的错觉
    • p->state 指示进程是否已分配、准备运行、正在运行、正在等待 I/O 或正在退出
    • p->pagetable 以 RISC-V 硬件期望的格式保存进程的页表,进程的页表还充当分配用于存储进程内存的物理页地址的记录
  • 每个进程都有一个执行 #线程 (或简称线程),用于执行进程的指令。线程的大部分状态(局部变量、函数调用返回地址)都存储在线程的堆栈中。线程主动交替使用其用户堆栈和内核堆栈。
  • 内核堆栈是独立的(并受到用户代码的保护),因此即使进程破坏了其用户堆栈,内核也可以执行。

1.6 Code: starting xv6, the first process and system call

概述内核如何启动并运行第一个进程

  • 当 RISC-V 计算机开机时,它会进行 #自身初始 化并运行存储在 #只读存储器 中的 #引导加载程序。
    • #bootloader 加载xv6内核进到内存
    • bootloader 将 xv6 内核加载到物理地址 0x80000000 处的内存中。因为地址范围 0x0:0x80000000 包含 I/O 设备
    • 进入到 #机器模式 ,CPU从_entry (kernel/entry.S:7)处开始执行xv6
    • RISC-V 在禁用分页硬件的情况下启动:虚拟地址直接映射到物理地址
  • 在_entry 处的指令设置了一个堆栈,以便 xv6 可以运行 C 代码。
    • Xv6 在文件 start.c (kernel/start.c:11) 中声明初始堆栈 stack0 的空间
    • 在_entry处的代码加载堆栈指针寄存器sp=stack0+4096(栈向下生长)
    • 现在内核有了一个堆栈,文件_entry 在 start (kernel/start.c:21)处调用 C 代码
  • 函数start执行一些仅在机器模式下允许的配置,然后切换到 #管理模式
    • start 不会从这样的调用中返回,而是像以前那样进行设置:
      • 它在寄存器 mstatus 中将先前的特权模式设置为 Supervisor
      • 它通过将 main 的地址写入寄存器 mepc 来将返回地址设置为 main
      • 通过将 0 写入页表寄存器 satp 来禁用管理模式下的虚拟地址转换
      • 将所有中断和异常委托给管理模式。
    • 在进入管理模式之前,start 还要执行一项任务:它对时钟芯片进行编程以生成定时器中断
    • start完成此内务处理后,通过调用 mret 开始“返回”到管理模式。这会导致程序计数器更改为 main (kernel/main.c:11)。
      • 为了进入管理模式,RISC-V 提供了 mret 指令。该指令最常用于从先前的调用从管理模式返回到机器模式
  • main (kernel/main.c:11) 初始化数个设备和子系统后:
    • main通过调用 userinit (kernel/proc.c:233) 创建第一个进程。
      • 第一个进程执行一个用 RISC-V 汇编编写的小程序,这为 xv6 中的第一个系统调用。
    • initcode.S (user/initcode.S:3) 将 exec 系统调用的编号 (SYS_EXEC (kernel/syscall.h:8) )加载到寄存器 a7 中,然后调用 ecall 重新进入内核。
  • 内核使用 寄存器 a7 中的编号在syscall (kernel/syscall.c:132)上来调用所需的系统调用。
    • 系统调用表 (kernel/syscall.c:107) 将 SYS_EXEC 映射到内核调用的 sys_exec。
    • exec 用新程序(在本例中为 /init)替换当前进程的内存和寄存器。
  • 一旦内核完成 exec,它就会返回到 /init 进程中的 #用户空间
    • init (user/init.c:15) 根据需要创建一个新的控制台设备文件,然后将其作为文件描述符 0、1 和 2 打开。
    • 然后它在控制台上启动一个 shell。
  • 系统已启动

1.7 Security Model

操作系统如何处理有缺陷或恶意代码?
操作系统的假定

  • 进程的用户级代码将尽最大努力破坏内核或其他进程。
  • 用户代码可能会尝试取消引用其允许的地址空间之外的指针;
  • 它可能会尝试执行任何 RISC-V 指令,甚至是那些不用于用户代码的指令;
  • 它可能会尝试读写任何RISC-V控制寄存器;
  • 它可能会尝试直接访问设备硬件;
  • 它可能会向系统调用传递聪明的值,试图欺骗内核崩溃或做一些愚蠢的事情。
    内核的目标
  • 限制每个用户进程,使其只能:
    • 读/写/执行自己的用户内存
    • 使用 32 个通用 RISC-V 寄存器,并以系统调用的方式影响内核和其他进程。
  • 内核必须阻止任何其他操作。这通常是内核设计中的绝对要求。
    内核代码要求
  • 内核代码应该没有错误,并且肯定不包含任何恶意内容。这个假设影响我们分析内核代码的方式。
    • 例如,有许多内部内核函数(例如,自旋锁),如果内核代码不正确地使用它们,就会导致严重的问题。当检查任何特定的内核代码片段时,我们希望说服自己它的行为正确。然而,我们假设内核代码总体上是正确编写的,并且遵循有关使用内核自身函数和数据结构的所有规则。在硬件层面,假设 RISC-V CPU、RAM、磁盘等按照文档中宣传的方式运行,没有硬件错误。

二、实验部分

MIT6.828 Lab2-1 Using gdb

MIT6.828 Lab2-3 Sysinfo

  • 43
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实验概述 本次实验是MIT 6.828操作系统课程的第一次实验,主要内容是编写一个简单的操作系统内核,并在QEMU虚拟机上运行。本次实验共有9个练习,其中练习9要求实现一个简单的用户程序并运行。 练习9要求我们实现一个简单的用户程序,该程序能够在屏幕上输出一些信息,并等待用户输入,输入结束后将输入内容输出到屏幕上。用户程序的具体要求如下: - 输出一些信息,例如“Hello World!”。 - 等待用户输入,可以使用getchar()函数实现。 - 将用户输入内容输出到屏幕上。 实验过程 1. 编写用户程序 我们首先在lab1目录下创建一个user文件夹,用于存放用户程序。然后创建一个名为“test.c”的文件,编写用户程序的代码如下: ``` #include <stdio.h> int main() { printf("Hello World!\n"); char c = getchar(); printf("You entered: %c\n", c); return 0; } ``` 这段代码的功能是输出“Hello World!”并等待用户输入,输入结束后将输入内容输出到屏幕上。 2. 修改Makefile文件 为了能够编译用户程序,我们需要修改Makefile文件。具体修改如下: ``` UPROGS=\ _cat\ _echo\ _forktest\ _grep\ _init\ _kill\ _ln\ _ls\ _mkdir\ _rm\ _sh\ _stressfs\ _usertests\ _wc\ _test\ # 添加用户程序的名称 $(OBJDIR)/_test: $(OBJDIR)/test.o $(LIBDIR)/ulib.o | $(OBJDIR) $(LD) $(LDFLAGS) -N -e main -Ttext 0 -o $@ $^ $(OBJDIR)/test.o: test.c | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< ``` 在UPROGS变量中添加上刚刚编写的用户程序的名称“_test”,然后在Makefile文件的末尾添加如上代码。 3. 编译内核和用户程序 在终端运行命令“make”,编译内核和用户程序。 4. 运行QEMU虚拟机 在终端运行命令“make qemu”,启动QEMU虚拟机。 5. 运行用户程序 在QEMU虚拟机中,输入“test”,即可运行刚刚编写的用户程序。运行结果如下: ``` Hello World! This is a test. You entered: T ``` 可以看到,程序首先输出了“Hello World!”这个信息,然后等待用户输入。我们输入了“This is a test.”这个字符串,然后按下回车键,程序将输入内容输出到了屏幕上。 实验总结 本次实验要求我们实现一个简单的用户程序并运行。通过编写代码、修改Makefile文件、编译内核和用户程序、启动虚拟机以及运行用户程序等步骤,我们成功地完成了本次实验,并学会了如何在操作系统内核中运行用户程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值