操作系统实验大作业 吃水果_操作系统期中大作业18级(没错,我降级了

1.(20分)

完成xv6的安装和启动,并完成sleep的编写。

题干:完成Lab: Xv6 and Unix utilities中的sleep(easy)任务,即在user/下添加sleep.c文件。在报告中提供sleep.c的代码,并提供sleep运行的屏幕截图。提示:在vmware下安装ubuntu20,可以较为顺利完成xv6安装和编译。

这里我们不选择用vmware,基于巨硬提出了我死了(WSL)的优秀Linux解决方案(还出了2),我推荐使用WSL2来完成这次作业(如果你本身就是Linux系统那忽略WSL)。

首先,安装WSL,搜索"启用或关闭Windows功能",如图:

fef12ff0e15b328be1ca96233edf3c18.png

然后勾选中”适用于Linux的Windows子系统“:

1aa0b43840c657dce19dc860923bac5e.png

安装完毕之后,根据该网址的要求切换到WSL 2:https://docs.microsoft.com/zh-cn/windows/wsl/install-win10,从步骤2开始做起。

然后在Microsoft Store搜索Ubuntu 20.04 LTS并安装:

9ebda1aeba8dc534dd35cf38fedf66eb.png

安装完毕后,搜索Ubuntu 20.04并选中,第一次会需要一段时间安装,安装完成后会让你注册一个UNIX账户及密码。成功后,即可通过该账户登录Ubuntu。

你也可以在命令行输入wsl或输入wsl -d Ubuntu-20.04来启动Ubuntu 20.04 LTS。

P.S. 如果安装时遇到0xc03a001a错误怎么办?

解决方案:

53ba69f25d41a4e9b1dbcb58e39dcdc8.png

当我们安装完毕Ubuntu 20.04之后,第一步要做的就是换源,这里我推荐换清华源,如果还不会换源的请直接去http://mirrors.tuna.tsinghua.edu.cn上的使用帮助——Ubuntu查看如何换源,P.S.,WSL没有图形界面,所以习惯一下vim。

换源后,我们根据MIT 6.S081的提示,执行以下指令:

sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu

这个过程校园网环境下大概持续4~5分钟

安装完毕后,git clone xv6-riscv的源码:

git clone git://github.com/mit-pdos/xv6-riscv.git

切到xv6的源码目录下,输入make qemu。

正常情况下会出现如图界面:

30e632c0cf3cb22b7e0893ceade62df0.png

如果出现该界面,说明你的xv6已经在qemu成功启动了。

WSL同时还支持Visual Studio Code,只需要在插件市场搜索Remote WSL,就可以获得和Remote SSH一样的体验啦!

搞定了这些生产力工具之后,我们就要开始完成我们的sleep了。

首先在xv6-riscv/user下创建sleep.c,然后我们随便参照一个系统给的用户级应用是怎么写的,我这里选的是user/cat.c,因为它够短。

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
​
char buf[512];
​
void
cat(int fd)
{
  int n;
​
  while((n = read(fd, buf, sizeof(buf))) > 0) {
    if (write(1, buf, n) != n) {
      fprintf(2, "cat: write errorn");
      exit(1);
    }
  }
  if(n < 0){
    fprintf(2, "cat: read errorn");
    exit(1);
  }
}
​
int
main(int argc, char *argv[])
{
  int fd, i;
​
  if(argc <= 1){
    cat(0);
    exit(0);
  }
​
  for(i = 1; i < argc; i++){
    if((fd = open(argv[i], 0)) < 0){
      fprintf(2, "cat: cannot open %sn", argv[i]);
      exit(1);
    }
    cat(fd);
    close(fd);
  }
  exit(0);
}
​

我们可以看到,应用的退出不使用return而是使用exit,同时,kernel/stat.h定义了各种的变量别名,而user/user.h定义了系统调用接口和C的一些库函数。

我们依葫芦画瓢,马上就能写出我们的sleep.c:

#include "kernel/types.h"
#include "user/user.h"
​
int main(int argc, char **argv)
{
    // Return ASAP while there is no parameter.
    if (argc != 2) {
        fprintf(2,"sleep: wants 1 parameters, get %d parameter(s).n",argc-1);
        exit(1);
    }
    int second;
    second = atoi(argv[1]);
    // If second == 0, return ASAP.
    if (second <= 0) exit(1);
    sleep(second);
    exit(0);
}

我们这里判断了几种情况,1.参数数量不正确的时候,2.传入参数不能转化为数字的时候。3.成功调用的时候,使用系统提供的sleep系统调用接口。

没有事要做就赶快返回,这是编写高效应用的金科玉律。

2. (30分)

结合xv6 book 第1、2、7章,阅读xv6内核代码(kernel/目录下)的进程和调度相关文件,围绕swtch.S,proc.h/proc.c,理解进程的基本数据结构,组织方式,以及调度方法。提示:用source insight阅读代码较为方便。

  • a) 修改proc.c中procdump函数,打印各进程的扩展信息,包括大小(多少字节)、内核栈地址、关键寄存器内容等,通过^p可以查看进程列表,提供运行屏幕截图。
  • b) 在报告中,要求逐行对swtch.S,scheduler(void),sched(void),yield(void)等函数的核心部分进行解释,写出你对xv6中进程调度框架的理解。阐述越详细、硬件/软件接口部分理解越深,评分越高。
  • c) 对照Linux的CFS进程调度算法,指出xv6的进程调度有何不足;设计一个更好的进程调度框架,可以用自然语言(可结合伪代码)描述,但不需要编码实现。

这里我们同样不使用Source Insight来看源码嗷,我们用Visual Studio Code。这里仅完成a)和b)的部分,你不会真打算看着我的教程全盘照抄吧。

a)的解答:

首先我们去看看procdump干了什么:

c955f3a48a2037e4e9c2d0c88412f30a.png

好我们看到procdump只输出了pid,状态和进程名字,这一点也不拉风,我们来看看struct proc有什么信息值得我们输出的:

enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
​
// Per-process state
struct proc {
  struct spinlock lock;
​
  // p->lock must be held when using these:
  enum procstate state;        // Process state
  struct proc *parent;         // Parent process
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  int xstate;                  // Exit status to be returned to parent's wait
  int pid;                     // Process ID
​
  // these are private to the process, so p->lock need not be held.
  uint64 kstack;               // Virtual address of kernel stack
  uint64 sz;                   // Size of process memory (bytes)
  pagetable_t pagetable;       // User page table
  struct trapframe *trapframe; // data page for trampoline.S
  struct context context;      // swtch() here to run process
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
};

哦很多东西啊,比如父进程,pid,是否被killed,内核栈地址等等的这些信息,因为这个进程可以输出pid,那么我们认为procdump是一定在持有p->lock的这个自旋锁的,所以我们可以放心输出需要p->lock的信息。

怎么输出看个人喜好吧,这里是我的一个例子:

695941582c38a589f2a1299ebb783ee3.png

%u是我新加的,至于怎么加你们自己去看printf源码改改就行了,那就是个依葫芦画瓢的玩意,没啥技术含量。

效果看一下:

862fcf0eba304fdf381d209d80110363.png

效果很不错呀,如果你还写了一些可以在后台挂着的应用,那么Ctrl-P就可以输出更多进程的东西了。

b)的解答:

首先我们来看看swtch.S:

# Context switch
#
#   void swtch(struct context *old, struct context *new);
# 
# Save current registers in old. Load from new. 
​
​
.globl swtch
swtch:
        sd ra, 0(a0)
        sd sp, 8(a0)
        sd s0, 16(a0)
        sd s1, 24(a0)
        sd s2, 32(a0)
        sd s3, 40(a0)
        sd s4, 48(a0)
        sd s5, 56(a0)
        sd s6, 64(a0)
        sd s7, 72(a0)
        sd s8, 80(a0)
        sd s9, 88(a0)
        sd s10, 96(a0)
        sd s11, 104(a0)
​
        ld ra, 0(a1)
        ld sp, 8(a1)
        ld s0, 16(a1)
        ld s1, 24(a1)
        ld s2, 32(a1)
        ld s3, 40(a1)
        ld s4, 48(a1)
        ld s5, 56(a1)
        ld s6, 64(a1)
        ld s7, 72(a1)
        ld s8, 80(a1)
        ld s9, 88(a1)
        ld s10, 96(a1)
        ld s11, 104(a1)
        
        ret

首先.globl表示这是swtch函数,然后开始swtch函数的汇编实现。

因为a0会被内核刷新成当前进程的结构体起始地址,所以要对a0(a0对应函数的第一个参数,也就是old)做偏移以存放13个寄存器(ra,sp和s0~s11),因为这些寄存器都是64位的,所以每次都要偏移8个字节。sd指的是将寄存器的内容存储到存储器中。

ld则相反,会从存储器a1(a1对应函数的第一个参数,也就是new)中读取13个寄存器,完成进程上下文的切换。

这个时候ret,因为ra已经被刷新了,所以会跳转到新进程的指定语句执行新进程。

关于RISC-V的汇编知识可以参考:

https://github.com/riscv/riscv-asm-manual/blob/master/riscv-asm.md​github.com

再来看看scheduler:

// Per-CPU process scheduler.
// Each CPU calls scheduler() after setting itself up.
// Scheduler never returns.  It loops, doing:
//  - choose a process to run.
//  - swtch to start running that process.
//  - eventually that process transfers control
//    via swtch back to the scheduler.
void
scheduler(void)
{
  struct proc *p;
  // mycpu会返回当前的CPU状态,这个时候中断必须关闭以防止有抢占现象发生(也就是操作必须是原子性atomic的)
  struct cpu *c = mycpu();
  
  c->proc = 0;
  for(;;){
    // Avoid deadlock by ensuring that devices can interrupt.
    // 这个时候要开启中断,因为调度程序有可能会死锁,这个时候必须允许外部中断
    intr_on();
​
    for(p = proc; p < &proc[NPROC]; p++) {
      // 简单轮询当前所有的进程,并挑选第一个合适的进程装载入CPU
      // 首先要获得进程自旋锁,不然就不能调用swtch,会有竞争条件(你没有获得自己的自旋锁而放弃了所有其他锁,你是否还在执行?)
      acquire(&p->lock);
      // 发现了一个可以被装载的进程
      if(p->state == RUNNABLE) {
        // Switch to chosen process.  It is the process's job
        // to release its lock and then reacquire it
        // before jumping back to us.
        // 设置状态为RUNNING,更改CPU当前进程为p,调用swtch切换进程上下文。
        p->state = RUNNING;
        c->proc = p;
        swtch(&c->context, &p->context);
​
        // Process is done running for now.
        // It should have changed its p->state before coming back.
        // 这个时候已经执行完了进程,设置当前CPU进程为空,继续轮询,直到有就绪态的进程被发现。
        c->proc = 0;
      }
      // 释放该进程自旋锁
      release(&p->lock);
    }
  }
}

再来看看sched:

// Switch to scheduler.  Must hold only p->lock
// and have changed proc->state. Saves and restores
// intena because intena is a property of this
// kernel thread, not this CPU. It should
// be proc->intena and proc->noff, but that would
// break in the few places where a lock is held but
// there's no process.
void
sched(void)
{
  // intena我这里没有一个很好的翻译,这里要去看proc.h和spinlock.c的实现,这里是一个关于自旋锁的控制函数push_off的辅助变量,push_off/pop_off和incr_off/incr_on最大的差别就是,push_off是一个有计数的incr_off,当第一次调用push_off的时候,intena会保持关闭CPU中断前的CPU中断状态;如果之前已经调用过push_off,intena不会被刷新。
  // 如果当push_off的次数和pop_off对应上之后,调用最后一次pop_off会查看intena记录的调用push_off前的CPU中断状态是怎样的,如果是开着的,那就把CPU中断打开。也就是说intena是一个维持push_off发生前的CPU中断状态的一个变量。具体的实现可以看kernel/spinlock.c的L84-L110
  // sched只有当进程就绪要被切换的时候才会被执行
  int intena;
  // 获得当前运行进程
  struct proc *p = myproc();
  // 这个时候p必须要持有自身的自旋锁
  if(!holding(&p->lock))
    panic("sched p->lock");
  // noff只有当被执行第一次push_off的时候会为1,这个时候中断是关闭的。
  if(mycpu()->noff != 1)
    panic("sched locks");
  // 还在RUNNING状态
  if(p->state == RUNNING)
    panic("sched running");
  // 二次检查中断是否被正常关闭了
  if(intr_get())
    panic("sched interruptible");
  // 保存当前cpu的intena
  intena = mycpu()->intena;
  // 切换当前进程到scheduler
  swtch(&p->context, &mycpu()->context);
  // 执行完scheduler,还原intena
  mycpu()->intena = intena;
}

最后我们来看看yield:

// Give up the CPU for one scheduling round.
void
yield(void)
{
  // yield是进程主动放弃CPU占用的一个手段。
  // 获得调用yield的进程
  struct proc *p = myproc();
  // 请求自旋锁以确认它没有在运行
  acquire(&p->lock);
  // 设置为就绪态
  p->state = RUNNABLE;
  // 切换到内核的切换函数
  sched();
  // 进程已经被成功切换,释放该进程自旋锁
  release(&p->lock);
}

当然,进程调度肯定还不止这些,xv6是怎么把进程组织到proc数组里的?这个就是进程调度策略的核心。sched/scheduler/yield都只是内核用于切换当前CPU运行进程的手段,我们可以看到scheduler是在proc数组里线性搜索第一个RUNNABLE的进程的,当然,在这看起来就是根据pid搜进程了,因为proc数组的元素还真就是按照pid排序的。

总结一下:进程执行完了或者时间片到了,会由内核执行yield剥夺该进程的执行,设置进程为就绪态。然后该进程进入休眠,由sched判断该进程是否已经未在执行,判断完成后切换到调度程序scheduler进行进程调度。然后新进程继续执行工作直到时间片到或者时间片前执行完毕,内核继续调用yield,如此循环。

yield的执行时机可以去kerneltrap.c里面看一下,我们也知道,要yield那肯定是产生了trap,所以方向往这边找就对了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值