前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MIT 6.S081 Lab One -- Util

MIT 6.S081 Lab One -- Util

作者头像
大忽悠爱学习
发布2023-10-11 08:40:45
4380
发布2023-10-11 08:40:45
举报
文章被收录于专栏:c++与qt学习
MIT 6.S081 Lab One -- Util

引言

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

解析

  • /user/echo.c函数代码如下:
代码语言:javascript
复制
#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):
代码语言:javascript
复制
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声明
代码语言:javascript
复制
int sleep(int);
  • user/usys.S中编写的关于sleep函数的汇编实现—通过ecall指令完成系统调用
代码语言:javascript
复制
.global sleep
sleep:
 li a7, SYS_sleep
 ecall
 ret
  • syscall.h头文件中,列举出了所有支持的系统调用号
代码语言:javascript
复制
// 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系统调用函数代码如下:
代码语言:javascript
复制
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函数代码如下:
代码语言:javascript
复制
// 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函数会在发生时钟中断时被调用
代码语言:javascript
复制
void
clockintr()
{
  //获取tickslock
  acquire(&tickslock);
  //记录当前时钟中断发生次数
  ticks++;
  //唤醒所有睡眠在ticks计数器上的任务
  wakeup(&ticks);
  //释放锁
  release(&tickslock);
}
  • kernel/proc.c中的wakeup函数代码如下:
代码语言:javascript
复制
// 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函数的参数单位也是毫秒
在这里插入图片描述
在这里插入图片描述

Lab代码实现

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

代码语言:javascript
复制
#include "kernel/types.h"
#include "user/user.h"

int main(int argc, char const *argv[])
{
  //参数错误--第一个参数默认为当前程序名
  if (argc != 2) { 
    fprintf(2, "usage: sleep <time>\n");
    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);
}
在这里插入图片描述
在这里插入图片描述

执行测试:

  • make clean
  • make qemu
在这里插入图片描述
在这里插入图片描述

pingpong(难度:Easy)

任务:

  • 编写一个使用UNIX系统调用的程序来在两个进程之间“ping-pong”一个字节,请使用两个管道,每个方向一个。
  • 父进程应该向子进程发送一个字节;
  • 子进程应该打印“<pid>: received ping”,其中<pid>是进程ID,并在管道中写入字节发送给父进程,然后退出;
  • 父级应该读取从子进程而来的字节,打印“<pid>: 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中。

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

代码语言:javascript
复制
$ make qemu
...
init: starting sh
$ pingpong
4: received ping
3: received pong
$

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


Lab代码实习

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

代码语言:javascript
复制
#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);
    }
}
在这里插入图片描述
在这里插入图片描述
  • 测试
在这里插入图片描述
在这里插入图片描述

小结

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-07-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • MIT 6.S081 Lab One -- Util
  • 引言
  • sleep(难度:Easy)
    • 解析
      • Lab代码实现
      • pingpong(难度:Easy)
        • Lab代码实习
        • 小结
        相关产品与服务
        腾讯云服务器利旧
        云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档