• 欢迎大家分享资料!前往留言板评论即可!

MIT 6.S081 操作系统课程系列2 System calls

合宙 模组资料网 7个月前 (05-15) 93次浏览 已收录 0个评论 扫描二维码
文章目录[隐藏]

MIT 6.S081 操作系统课程系列2 System calls


https://pdos.csail.mit.edu/6.828/2020/labs/syscall.html
本次实验需要读xv6手册第二章、4.3、4.4。看相关kernel代码。
理解system call的流程并实现一些system call。


物理资源的抽象

  • 为什么要有system call。因为为了安全等因素,需要有一层隔离,不能让应用程序直接访问敏感资源。
  • 为一些关键资源做一系列system call,供应用程序使用,是比较好的办法,既安全又分层化方便开发。比如文件的open/read/write等等。
    比如exec,会做一系列内存操作,不需要应用程序自己处理。

user模式和supervisor模式

  • 应用程序与内核是“隔离”的,应用程序的崩溃绝不能影响内核的正常运行。所以不允许一个应用程序访问内核本身的数据和内存等。
    而且应用程序之间也不允许相互访问内存。
  • cpu对这种隔离有硬件上的支持。比如risc-v有三种模式:machine mode/supervisor mode/user mode。
    machine模式下执行的指令有所有权限,通常是机器启动时配置各种资源时使用。
    xv6在machine模式下执行少数代码后会切换到supervisor模式。
  • supervisor模式下cpu可以执行一些需要权限的指令。比如中断的开关,读写寄存器等等。
    如果在user模式下执行这些指令,cpu会忽略。
    普通应用程序只能运行在user模式下,所谓的user space。
    supervisor模式下运行的程序在所谓的kernel space里。
  • 应用程序想调用system call时必须转到kernel模式。cpu会提供指令从user模式转到supervisor模式。
    收到system call后,kernel会检查其参数。没有问题才运行。

kernel的规划

  • 一个大问题是操作系统的哪些部分要运行在supervisor模式。
  • 一种方法是整个操作系统都放进去,即monolithic kernel。
    这样的好处是好实现,少费脑筋,可共用资源比如各种buff。
    一个坏处是各个模块之间的接口会过于复杂,开发人员容易犯错。
    monolithic kernel里的错误是致命的,因为supervisor模式下的错误通常会造成整个kernel崩溃。
  • 为了缓解这个问题,可尽量减少运行在supervisor模式下的代码,而转到user模式下执行。成为micro kernel微内核。
  • 例如用户程序想访问文件,操作系统会在user空间跑一个文件服务器,用户程序还是向kernel请求system call,kernel把请求再发给user空间的文件服务器。这样内核实际跑的代码就很少,只是转发了一下命令。
  • xv6是monolithic kernel,体积小,功能少。

进程

  • xv6中隔离的粒度是进程。进程间无法相互访问各自内存、cpu信息、文件等资源。
    进程也不能访问kernel的资源。
  • kernel代码必须很小心地实现,防止用户程序的各种恶意操作。
    进程有自己的地址空间,仿佛自己占用是一套独立完整的机器资源。
  • xv6用分页表(硬件实现)实现每个进程的地址空间。
    risc-v分页表会把虚拟地址转换成实际的物理内存地址。

进程的虚拟地址空间示意:

_____________   
| trampoline |  <- maxva
|____________|
| trapframe  |
|____________|
|            |
|            |
|            |
|            |
|   heap     |
|            |
|            |
|            |
|            |
|____________|
| user stack |
|____________|
|            |
| user text  |
| user data  |
|____________|  <- 0

  • riscv的指针为64bit。会用低39位来查分页表中的地址。xv6又会用其中的38位,所以寻址空间就是pow(2, 38)-1=0x3fffffffff(kernel/riscv.h里又定义)。
  • 每个进程有user和kernel两个stack。程序在user空间时就用user栈,进入kernel是就用kernel栈。
  • 进程调用system call时先发送rsicv的ecall指令,获取硬件权限然后把程序接到kernel指定的入口,在入口处会转换到kernel栈然后执行system call的代码。
    当system call执行完,发送sret指令取消硬件权限,切换回user栈和user空间。

xv6的启动流程

  1. 上电。bootloader加载kernel进内存。地址为0x80000000。因为0x0到0x80000000包含io设备,所以放到0x80000000。
  2. machine模式。cpu执行entry.S里的_entry。此时硬件分页关闭,虚拟地址直接映射到物理地址。
  3. _entry起一个stack0让xv6能够跑c代码。sp指针指向stack0+4096。开始跑start.c里的start()函数。
  4. start()做一些machine模式特许的操作。
  5. 切换到supervisor模式。写main函数(kernel/main.c)的地址到mepc,直接返回到main函数。
  6. main()初始化各种设备和子系统。调用userinit()创建第一个进程。执行user/initcode.S。
  7. initcode.S执行exec(system call),启动init程序(user/init.c)。进入shell等待用户输入。

system call的c代码解析

  • exec为例,用户程序代码会把参数放进a0和a1寄存器,exec的代码7(syscall.h)放进a7。
  • 见syscall()函数,从a7取出代号调用相应的system call函数。
  • system call函数里需要从寄存器取参数(使用argaddr/argint/argfd等等)。
    返回值存入a0。
  • 结束后exec就会返回a0里的值。
  • 有时需要返回较多的数据,需要用copyout从kernel复制数据到用户地址。

实战

trace

  • 做一个trace程序(system call),供user层的trace调用。
  • 第一个参数是一个bit设置,对应syscall.h里的定义。比如传32,就是1<<5,5对应的就是SYS_read。可以同时打开多项。
  • 调用system call后,检查对应的bit是否打开,打开的话就打印信息。
  1. makefile里UPROGS加入trace
  2. user/user.h里加入trace的定义
  3. user/usys.pl里加入trace的定义
  4. kernel/syscall.h里加入trace新号
  5. kernel/proc.h的进程结构体里添加int类型trace_flag
  6. kernel/sysproc.c里加入sys_trace()
    sys_trace这个system call。获取参数用argint。给myproc()的trace_flag赋值即可。
  7. 修改fork函数。把parent的trace_flag传给child。
  8. 初始化进程的地方把trace_flag置0。
  9. 修改kernel/syscall.c。调用syscall后判断num和trace_flag,如果bit被打开,打印相关信息。

sysinfo

  • 做一个sysinfo程序(system call)。打印系统信息。
    测试程序为user/sysinfotest.c
  1. 修改makefile
  2. user/user.h里加入sysinfo的定义(参照sysinfotest.c的使用方法)
  3. 参照trace的实现,添加其他定义。
  4. 实现sysinfo函数。
  • 参考sysfile.c里的sys_fstat。会接收一个指针,要用copyout把kernel模式下的内存数据copy到这个指针的地址。
    要获取系统可用内存和当前进程总数这两个数据。

  • 如何获取系统的空闲内存?需要看一下内存操作的实现。
    memlayout.h里定义了KERNBASE和PHYSTOP,从0x80000000L开始的128M字节为可用内存。
    riscv.h里定义了页的大小PGSIZE为4字节。

  • 看一下kalloc.c里对内存的操作
    有一个run结构类型包含一个指向同类型结构的指针,就是一个简单的list。
    有一个kmem结构实体包含一个run的指针freelist,和一个spinlock。对kmem操作时都要锁这个spinlock。

  • 先看kfree如何释放或初始化内存。
    kfree接收一个指针pa,pa的值是要释放的内存的地址比如0x80000008L,往0x80000008L开始填4字节垃圾(也可以不管),然后把pa插到freelist头部。
    kalloc先检查freelist头部,如果不是0就往头部填充一些垃圾返回(这里没有硬性规定,所以分配内存后一般需要memset置零。),把freelist往后挪。

  • 这样就很清晰了,freelist实际就是个可用内存的链表。每次free一个页就把这个地址做成指针插到头部,每次alloc一个页就拿出头部,之前的第二项变成新的新的头部。

  • 懂了内存分配的原理,获取可用内存就非常简单,遍历一下freelist并计数即可。

  • 统计当前进程总数,遍历proc数组,统计不为UNUSED的数量即可。



喜欢 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址