MIT 6.S081 Lab1: Xv6 and Unix utilities

写在前面

这周开始做 MIT 的 6.S801 操作系统实验,补一下本科时的基础,希望能一直做下去,二哥监督!

虽然2021年的实验已经出来了,但我还是选择做2020年的,实验地址在这里。主要考虑到自己太菜了,20年的网上有一些代码可供参考,有时候实在不会做硬看也不一定能做出来,能够读懂别人的也算收获,哈哈。

刚做完实验一,地址在 Lab: Xv6 and Unix utilities, 在这里做一下记录。

实验内容

实验环境(Boot xv6)

这次实验主要使用 MIT 提供的代码,将 xv6 系统在 qemu 上跑起来。git checkout util 是将分支切换到 util 上。

我在自己电脑上刷了一个 Ubuntu 20.04 的系统,然后按照实验指导书的过程一点点敲命令,就好了。中间需要安装一些必备的软件包和一些依赖,提示缺啥就安装啥就成,环境没啥大问题,不需要折腾太久。感觉折腾装环境啥的没什么意义,不如快点开始实验。

在安装好环境后就可以开始实验了。这部分的实验主要是写五个系统调用的命令,分别是 sleep, pingpong, primes, findxargs。建议先看一下xv6 book的第一章,主要看一下 1.1-1.3

任务一(sleep)

这是最简单的一个任务,直接调用提供的 sleep 方法即可,纯属为了好上手。头文件参考其他的 user 目录下的 .c 文件即可。

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[])
{
  if (argc < 2)
  {
	fprintf(2, "Usage: sleep time...\n");
	exit(1);
  }
  int time = atoi(argv[1]);
  sleep(time);
  exit(0);
}

在写完 .c 代码后,需要在 Makefile 文件中的 UPROGS 字段中添加 $U/_[xxx]\,然后才能使用 ./grade-lab-util 进行测试。

任务二(pingpong)

使用 pipe()fork() 实现父进程发送一个字符,子进程成功接收该字符后打印 received ping,再向父进程发送一个字符,父进程成功接收后打印 received pong

  • 文件描述符

这里需要懂得什么是文件描述符。在 Linux 操作系统中,一切皆文件,内核通过文件描述符访问文件。每个进程都会默认打开3个文件描述符,即0、1、2。其中0代表标准输入流(stdin)、1代表标准输出流(stdout)、2代表标准错误流(stderr)。可以使用 >>> 的方式重定向。

  • 管道

管道是操作系统进程间通信的一种方式,可以使用 pipe() 函数进行创建。有在管道创建后,就有两个文件描述符,一个是负责读,一个是负责写。两个描述符对应内核中的同一块内存,管道的主要特点:

  1. 数据在管道中以字节流的形式传输,由于管道是半双工的,管道中的数据只能单向流动。

  2. 管道只能用于具有亲缘关系的进程通信,父子进程或者兄弟进程。

  3. fork() 后,父进程和子进程拥有相同的管道描述符,通常为了安全起见,我们关闭一个进程的读描述符和另一个进程的写描述符,这样就可以让数据更安全地传输。

下面是代码实现:

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

int main(int argc, char *argv[]) {
  int p1[2], p2[2];
  char buffer[] = {'l'};
  int len = sizeof(buffer);
  pipe(p1);
  pipe(p2);
  if (fork() == 0) {
	close(p1[1]);
	close(p2[0]);
	if (read(p1[0], buffer, len) != len) {
	  printf("child read error!\n");
	  exit(1);
	}
	printf("%d: received ping\n", getpid());
	if (write(p2[1], buffer, len) != len) {
	  printf("child write error\n");
	  exit(1);
	}
	exit(0);
  } else {
	close(p1[0]);
	close(p2[1]);
	if (write(p1[1], buffer, len) != len) {
	  printf("parent write error!\n");
	  exit(1);
	}
	if (read(p2[0], buffer, len) != len) {
	  printf("parent read error!\n");
	  exit(1);
	}
	printf("%d: received pong\n");
	exit(0);
  }
  exit(0);
}

注:这里需要使用两个管道,我尝试改成一个,并没有成功…不太懂如何使用一个管道,感觉还是使用两个管道更加安全些。

任务三(primes)

任务是输出区间内的质数 [2, 35],如果一般的方法,直接挨个遍历计算就行,但由于这里需要充分使用 pipefork 方法,因此需要一些不一样的方法。

下面使用的方法是埃拉托斯特尼素数筛,简称筛法。简单地说就是,每次得到一个素数时,在所有小于 n 的数中,删除它的倍数,然后不断迭代,剩下的就全是素数了。

实现思想是,只要还没获取到所有的素数,便不断遍历/递归。每次递归时:

  1. 先在父进程中创建一个子进程。
  2. 利用子进程将剩下的所有数全都写到管道中。
  3. 在父进程中,将数不断读出来,管道中第一个数一定是素数,然后删除它的倍数(如果不是它的倍数,就继续更新数组,同时记录数组中目前已有的数字数量)。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

void printPrime(int *input, int count)
{
  if (count == 0) {
    return;
  }
  int p[2], i = 0, prime = *input;
  pipe(p);
  char buff[4];
  printf("prime %d\n", prime);
  if (fork() == 0) {
	close(p[0]);
	for (; i < count; i++) {
	  write(p[1], (char *)(input + i), 4);
	}
	close(p[1]);
	exit(0);
  } else {
	close(p[1]);
	count = 0;
	while (read(p[0], buff, 4) != 0) {
	  int temp = *((int *)buff);
	  if (temp % prime) {
	    *input++ = temp;
		count++;
	  }
	}
	printPrime(input - count, count);
	close(p[0]);
	wait(0);
  }
}

int main(int argc, char *argv[]) {
  int input[34], i = 0;
  for (; i < 34; i++) {
    input[i] = i + 2;
  }
  printPrime(input, 34);
  exit(0);
}

无论是使用迭代的方式还是递归的方法,大致思想都是相同的。这道题我想了好久,。。

任务四(find)

在路径中查找特定文件名的文件。

基本直接复制 user/ls.c 文件内容。有几点需要注意的:

  1. 在获取文件名的函数(fmtname )中,需要设置 buf[strlen(p)] = 0; 否则比较函数 strcmp 会失效。
  2. memmove()memcpy() 的改进版,实现的功能相同,但更安全,具体分析见这篇博客,大致是说,一个是从头部开始复制,一个是从尾部开始复制。
  3. 结构体 statdirent 分别存储文件的信息和目录的信息。没有很理解,留着以后再看。
  4. 不太懂 de.inum 是干嘛的,有的代码还把它等于 1 的情况省略了,不懂为啥。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"

char* fmtname(char *path) {
  static char buf[DIRSIZ + 1];
  char *p;
  for (p = path + strlen(path); p >= path && *p != '/';p--);
  p++;
  if (strlen(p) >= DIRSIZ) return p;
  memmove(buf, p, strlen(p));
  memset(buf+strlen(p), ' ', DIRSIZ-strlen(p));
  buf[strlen(p)] = 0;
  return buf;
}

void find(char *path, char *fileName) {
  char buf[512], *p;
  int fd;
  struct stat st;
  struct dirent de;
  if ((fd = open(path, 0)) < 0 ) {
    fprintf(2, "find: cannot open %s\n", path);
	return;
  }
  if (fstat(fd, &st) < 0) {
    fprintf(2, "find: cannot stat %s\n", path);
	close(fd);
	return;
  }
  //printf("%s %s\n",path, fmtname(path));
  switch(st.type) {
  case T_FILE:
	//printf("%s\n", fmtname(path));
	if (strcmp(fmtname(path), fileName) == 0) {
	  printf("%s\n", path);
	}
	break;
  case T_DIR:
	//printf("%s %s\n", path, fmtname(path));
	if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf) {
	  printf("find: path too long\n");
	  break;
	}
	strcpy(buf, path);
	p = buf + strlen(buf);
	*p++ = '/';
	while (read(fd, &de, sizeof(de)) == sizeof(de)) {
	  if (de.inum == 0 || strcmp(de.name, ".") == 0 || strcmp(de.name, "..") == 0) {
		continue;
	  }
	  memmove(p, de.name, DIRSIZ);
	  p[DIRSIZ] = 0;
	  find(buf, fileName);
	}
	break;
  }
  close(fd);
}

int main(int argc, char *argv[]) {
  if (argc < 3) {
    printf("error\n");
	exit(1);
  }
  find(argv[1], argv[2]);
  exit(0);
}

任务五(xargs)

没咋用过这个命令,还是先查的它是啥功能。主要作用是读取标准输出中的信息,然后将其作为标准输入,拼接到下一个命令后面来执行。神奇!

注意的一点是,利用 fork() 让命令在子进程中执行后,函数并没有结束,还需要继续读取,因此一些变量需要继续设置新的值。

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

int main(int argc, char *argv[]) {
  int i, count = 0, k, m = 0;
  char* lineSplit[MAXARG], *p;
  char block[32], buf[32];
  p = buf;
  for (i = 1; i < argc; i++) {
	lineSplit[count++] = argv[i];
  }
  while ((k = read(0, block, sizeof(block))) > 0) {
    for (i = 0; i < k; i++) {
	  if (block[i] == '\n') {
		buf[m] = 0;
		lineSplit[count++] = p;
		lineSplit[count] = 0;
		m = 0;
		p = buf;
		count = argc - 1;
		if (fork() == 0) {
		  exec(argv[1], lineSplit);
		}
		wait(0);
	  } else if (block[i] == ' ') {
		buf[m++] = 0;
		lineSplit[count++] = p;
		p = &buf[m];
	  } else {
		buf[m++] = block[i];
	  }
	}
  }
  exit(0);
}

命令中的 | 表示管道(我一直以为这是与或非的或,。。惭愧)。

总结

这个实验算是熟悉一下 xv6 的环境和操作吧,确实感觉学到了很多东西,大赞!

代码地址在 github,文章同步在知乎

参考文章

  1. Mit6.S081-实验1-Xv6 and Unix utilities
  2. 操作系统实验Mit6.S801笔记 Lab1: Util
  3. MIT-6.s081-OS lab util Unix utilities
  4. 【MIT-6.S081-2020】Lab1 Util
  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: :xv6是一个基于Unix操作系统,它是一个教学用途的操作系统,旨在教授操作系统的基本概念和实现。它是在MIT的x86架构上开发的,包括了Unix的一些基本功能,如进程管理、文件系统、内存管理等。xv6的源代码是公开的,可以用于学习和研究。 Unix utilitiesUnix操作系统中的一些基本工具,如ls、cd、cp、mv、rm等。这些工具可以帮助用户管理文件和目录,执行各种操作。这些工具的实现是基于Unix的系统调用,可以通过编写C程序来调用这些系统调用实现相应的功能。这些工具是Unix操作系统的基础,也是其他操作系统的参考。 ### 回答2: lab: xv6 and unix utilities 实验是一项旨在帮助学生深入理解操作系统Unix 工具使用的实验。该实验分为两个部分,第一部分教授学生如何构建和运行 xv6 操作系统;第二部分则重点教授 Unix 工具的使用。 在 xv6 操作系统部分,学生将学习到操作系统内核的基本结构和实现原理。实验将引导学生理解内存管理、进程调度、系统调用等关键操作系统概念。此外,学生还将学习如何编写简单的 shell 以及如何通过修改 xv6 内核代码来实现新的系统调用和功能。 在 Unix 工具部分,学生将探索 Unix 系统中广泛使用的常见工具。这些工具包括 vi 编辑器、grep、awk、sed 等。实验将介绍这些工具的基本使用方法以及它们在处理文本和数据时的实际应用。这部分实验还将让学生深入了解 shell 和 shell 脚本的编写,帮助他们在 Unix 环境中轻松地编写脚本和自动化任务。 lab: xv6 and unix utilities 实验对计算机科学专业的学生具有重要意义。通过完成这个实验,学生将建立起对操作系统Unix 工具的深入理解,为他们成为一名优秀的软件工程师奠定坚实的基础。同时,这个实验还将为学生提供实践经验,让他们能够将所学知识应用到真实的软件开发和运维中。 ### 回答3: Lab: xv6 and Unix Utilities是一个计算机科学领域的实验,旨在让学生深入了解Unix操作系统以及操作系统本身的自我管理机制。在这个实验中,学生需要从零开始构建一个类似于Unix操作系统,在这个操作系统中,学生需要设计一些基本命令,例如ls,cat,grep等等,并且将它们与系统的底层API结合起来,以实现各种功能。此外,学生还需要了解和探索xv6这个开发工具,它是一个轻量级基于Unix操作系统实现,具有一定的可移植性和简洁性,因此,它可以作为一个基础框架来实现一个完整的Unix操作系统。 这个实验的目标是让学生了解Unix的基本命令结构和API,以及操作系统内部的一些基本机制,例如进程管理,文件系统交互以及进程通信等等。此外,通过实现这些命令,学生还可以学到一些基本的C语言编程技能,例如文件操作,字符串处理以及进程管理等等。还可以学习到如何使用Git等版本控制工具,以及如何进行调试和测试代码的技巧。 在整个实验过程中,学生需要有较强的自我管理能力和综合运用能力,因为在实现这些命令的同时,他们还需要和其他团队成员进行交流和合作,以及不断改进和完善他们的代码。总之,这个实验是一个非常有趣且富有挑战性的计算机科学课程,通过完成这个实验,学生可以更好地了解操作系统的构造和运作机制,以及如何设计和开发高效的系统级应用程序。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值