MIT 6.S081 Lab One

MIT 6.S081 Lab One

  • 引言
  • sleep(难度:Easy)
    • 解析
    • Lab代码实现
  • pingpong(难度:Easy)
    • Lab代码实习
  • 小结


引言

本文为 MIT 6.S081 2020 操作系统 实验一解析。

MIT 6.S081课程前置基础参考: 基于RISC-V搭建操作系统系列


sleep(难度:Easy)

任务:

  • 实现xv6的UNIX程序sleep:您的sleep应该暂停到用户指定的计时数。

一个滴答(tick)是由xv6内核定义的时间概念,即来自定时器芯片的两个中断之间的时间。您的解决方案应该在文件user/sleep.c中

Tips:

  • 在你开始编码之前,请阅读《book-riscv-rev1》的第一章
  • 看看其他的一些程序(如: /user/echo.c, /user/grep.c, /user/rm.c)查看如何获取传递给程序的命令行参数
  • 如果用户忘记传递参数,sleep应该打印一条错误信息
  • 命令行参数作为字符串传递; 您可以使用atoi将其转换为数字(详见/user/ulib.c
  • 使用系统调用sleep
  • 请参阅kernel/sysproc.c以获取实现sleep系统调用的xv6内核代码(查找sys_sleep),user/user.h提供了sleep的声明以便其他程序调用,用汇编程序编写的user/usys.S可以帮助sleep从用户区跳转到内核区。
  • 确保main函数调用exit()以退出程序。
  • 将你的sleep程序添加到Makefile中的UPROGS中;完成之后,make qemu将编译您的程序,并且您可以从xv6的shell运行它。
  • 看看Kernighan和Ritchie编著的《C程序设计语言》(第二版)来了解C语言。

运行效果:

  • 从xv6 shell运行程序:
$ make qemu
...
init: starting sh
$ sleep 10
(nothing happens for a little while)
$
  • 如果程序在如上所示运行时暂停,则解决方案是正确的。运行make grade看看你是否真的通过了睡眠测试。
  • 请注意,make grade运行所有测试,包括下面作业的测试。如果要对一项作业运行成绩测试,请键入(不要启动XV6,在外部终端下使用):
$ ./grade-lab-util sleep
  • 这将运行与sleep匹配的成绩测试。或者,您可以键入:
$ make GRADEFLAGS=sleep grade
  • 效果是一样的。

解析

  • /user/echo.c函数代码如下:
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

//argc是命令行参数个数
int main(int argc, char *argv[]){
  int i;
  // 依次处理每个命令行参数
  for(i = 1; i < argc; i++){
    // 默认情况下,文件描述符0对应标注输入,文件描述符1对应标准输出
    //文件描述符2对应标准错误
    write(1, argv[i], strlen(argv[i]));
    //每输出一个参数,就拼接一个换行符,如果是最后一个参数了,那么拼接一个" "
    if(i + 1 < argc){
      write(1, " ", 1);
    } else {
      write(1, "\n", 1);
    }
  }
  exit(0);
}
  • 字符串转整数的atoi函数代码如下(/user/echo.c):
int atoi(const char *s){
  int n=0;
  while('0' <= *s && *s <= '9')
    //每次处理一个字符,n每次乘10进一位,然后*s-'0'计算出当前字符代表数字几
    n = n*10 + *s++ - '0';
  return n;
}
  • user/user.h中的sleep声明
int sleep(int);
  • user/usys.S中编写的关于sleep函数的汇编实现—通过ecall指令完成系统调用
.global sleep
sleep:
 li a7, SYS_sleep
 ecall
 ret
  • syscall.h头文件中,列举出了所有支持的系统调用号
// System call numbers
#define SYS_fork    1
#define SYS_exit    2
#define SYS_wait    3
#define SYS_pipe    4
#define SYS_read    5
#define SYS_kill    6
#define SYS_exec    7
#define SYS_fstat   8
#define SYS_chdir   9
#define SYS_dup    10
#define SYS_getpid 11
#define SYS_sbrk   12
#define SYS_sleep  13
#define SYS_uptime 14
#define SYS_open   15
#define SYS_write  16
#define SYS_mknod  17
#define SYS_unlink 18
#define SYS_link   19
#define SYS_mkdir  20
#define SYS_close  21
  • kernel/sysproc.c中的sys_sleep系统调用函数代码如下:
uint64
sys_sleep(void)
{
  int n;
  uint ticks0;
  //从当前任务上下文中获取a0寄存器的值
  //a0寄存器作为系统调用参数寄存器,存放sleep(int)中int参数值
  if(argint(0, &n) < 0)
    return -1;
  //加锁
  acquire(&tickslock);
  //时钟中断每发生一次,ticks数加一 -- 此处是获取当前ticks数
  //ticks0保存进入睡眠的ticks数
  ticks0 = ticks;
  //进入sleep状态
  //每次都唤醒时,检查自身的sleep time是否到期,到期就停止sleep 
  while(ticks - ticks0 < n){
    //如果进程被杀掉了,直接释放锁,然后返回-1
    if(myproc()->killed){
      release(&tickslock);
      return -1;
    }
    //睡眠
    sleep(&ticks, &tickslock);
  }
  //释放锁
  release(&tickslock);
  return 0;
}
  • kernel/proc.c中的sleep函数代码如下:
// Atomically release lock and sleep on chan.
// Reacquires lock when awakened.
void
sleep(void *chan, struct spinlock *lk)
{
  //获取当前任务上下文 
  struct proc *p = myproc();
  
  // Must acquire p->lock in order to
  // change p->state and then call sched.
  // Once we hold p->lock, we can be
  // guaranteed that we won't miss any wakeup
  // (wakeup locks p->lock),
  // so it's okay to release lk.
  //获取当前任务锁
  //释放tickslock锁
  if(lk != &p->lock){  //DOC: sleeplock0
    acquire(&p->lock);  //DOC: sleeplock1
    release(lk);
  }

  // Go to sleep.
  //任务状态设置为SLEEPING状态,并且当前线程睡眠在ticks计数器上
  p->chan = chan;
  p->state = SLEEPING;
  //执行任务调度
  sched();
  
  // Tidy up.
  p->chan = 0;

  // Reacquire original lock.
  //释放任务锁,获取tickslock锁
  if(lk != &p->lock){
    release(&p->lock);
    acquire(lk);
  }
}

获取当前任务的lock,是为了改变当前任务状态时的并发安全性

  • kernel/trap.c中的clockintr函数会在发生时钟中断时被调用
void
clockintr()
{
  //获取tickslock
  acquire(&tickslock);
  //记录当前时钟中断发生次数
  ticks++;
  //唤醒所有睡眠在ticks计数器上的任务
  wakeup(&ticks);
  //释放锁
  release(&tickslock);
}
  • kernel/proc.c中的wakeup函数代码如下:
// Wake up all processes sleeping on chan.
// Must be called without any p->lock.
void
wakeup(void *chan)
{
  //遍历任务列表
  struct proc *p;
  for(p = proc; p < &proc[NPROC]; p++) {
    acquire(&p->lock);
    //唤醒所有睡眠在ticks计数器上的任务
    if(p->state == SLEEPING && p->chan == chan) {
      //设置对应任务状态为RUNNING,该任务会在之后的任务调度过程中被调度执行
      //然后执行时,检查自身sleep time是否到期,如果没有到期,则继续sleep
      //然后再次唤醒,再次检查,循环往复...
      p->state = RUNNABLE;
    }
    release(&p->lock);
  }
}

xv6中的sleep函数本质就是软件定时器的实现,但是其思路并不是在每次时钟中断发生时,唤醒所有到期的定时任务,而是直接唤醒所有睡眠的任务,让其自身去检查是否睡够了,如果没睡够,那么就继续接着睡。

这种实现方式的坏处就是定时任务的定时属性不够精准,而且唤醒了还未睡够的任务,造成资源浪费。

  • 在kernel/start.c的timerinit定时器中断初始化方法中我们可以看到,时钟中断的触发间隔大约为1毫秒,也就是说ticks大约是每毫秒累加一次,即: 我们sleep函数的参数单位也是毫秒
    MIT 6.S081 Lab One_第1张图片

Lab代码实现

经过上面的分析后,我们已经知道了sleep函数背后的原理,下面开始编写本lab的代码:

#include "kernel/types.h"
#include "user/user.h"

int main(int argc, char const *argv[])
{
  //参数错误--第一个参数默认为当前程序名
  if (argc != 2) { 
    fprintf(2, "usage: sleep );
    exit(1);
  }
  printf("sleep time=%s\n",argv[1]);
  int ticks = atoi(argv[1]);
  sleep(ticks);
  printf("(nothing happens for a little while)\n");
  exit(0);
}

MIT 6.S081 Lab One_第2张图片
执行测试:

  • make clean
  • make qemu
    MIT 6.S081 Lab One_第3张图片

pingpong(难度:Easy)

任务:

  • 编写一个使用UNIX系统调用的程序来在两个进程之间“ping-pong”一个字节,请使用两个管道,每个方向一个。
  • 父进程应该向子进程发送一个字节;
  • 子进程应该打印“: received ping”,其中是进程ID,并在管道中写入字节发送给父进程,然后退出;
  • 父级应该读取从子进程而来的字节,打印“: received pong”,然后退出。
  • 您的解决方案应该在文件user/pingpong.c中。

提示:

  • 使用pipe来创造管道
  • 使用fork创建子进程
  • 使用read从管道中读取数据,并且使用write向管道中写入数据
  • 使用getpid获取调用进程的pid
  • 将程序加入到Makefile的UPROGS
  • xv6上的用户程序有一组有限的可用库函数。您可以在user/user.h中看到可调用的程序列表;源代码(系统调用除外)位于user/ulib.c、user/printf.c和user/umalloc.c中。

运行程序应得到下面的输出:

$ make qemu
...
init: starting sh
$ pingpong
4: received ping
3: received pong
$

如果您的程序在两个进程之间交换一个字节并产生如上所示的输出,那么您的解决方案是正确的。


Lab代码实习

使用两个管道进行父子进程通信,需要注意的是如果管道的写端没有close,那么管道中数据为空时对管道的读取将会阻塞。因此对于不需要的管道描述符,要尽可能早的关闭。

#include "kernel/types.h"
#include "user/user.h"

#define RD 0 //pipe的read端
#define WR 1 //pipe的write端

int main(int argc, char const *argv[]) {
    char buf = 'P'; //用于传送的字节

    int fd_c2p[2]; //子进程->父进程
    int fd_p2c[2]; //父进程->子进程
    pipe(fd_c2p);
    pipe(fd_p2c);

    int pid = fork();
    int exit_status = 0;
   
    if (pid < 0) {
        fprintf(2, "fork() error!\n");
        close(fd_c2p[RD]);
        close(fd_c2p[WR]);
        close(fd_p2c[RD]);
        close(fd_p2c[WR]);
        exit(1);
    } else if (pid == 0) { //子进程
        close(fd_p2c[WR]);
        close(fd_c2p[RD]);
        
        if (read(fd_p2c[RD], &buf, sizeof(char)) != sizeof(char)) {
            fprintf(2, "child read() error!\n");
            exit_status = 1; //标记出错
        } else {
            fprintf(1, "%d: received ping\n", getpid());
        }

        if (write(fd_c2p[WR], &buf, sizeof(char)) != sizeof(char)) {
            fprintf(2, "child write() error!\n");
            exit_status = 1;
        }

        close(fd_p2c[RD]);
        close(fd_c2p[WR]);

        exit(exit_status);
    } else { //父进程
        close(fd_p2c[RD]);
        close(fd_c2p[WR]);

        if (write(fd_p2c[WR], &buf, sizeof(char)) != sizeof(char)) {
            fprintf(2, "parent write() error!\n");
            exit_status = 1;
        }

        if (read(fd_c2p[RD], &buf, sizeof(char)) != sizeof(char)) {
            fprintf(2, "parent read() error!\n");
            exit_status = 1; //标记出错
        } else {
            fprintf(1, "%d: received pong\n", getpid());
        }

        close(fd_p2c[WR]);
        close(fd_c2p[RD]);

        exit(exit_status);
    }
}

MIT 6.S081 Lab One_第4张图片

  • 测试

MIT 6.S081 Lab One_第5张图片


小结

实验一后续还有一些实验内容,留作后续慢慢补充

你可能感兴趣的:(#,MIT,6.S081,unix,linux,bash)