Lab 1 Utilities

sleep(easy)

提示

  • 如果不传入参数,则打印error
  • 参数类型为string,使用stoi(user/ulib.c)转换为int
  • 使用系统调用sleep
  • 阅读 xv6 的 sys_sleep 函数,这个函数在 kernel/sysproc.c 中实现
  • user/user.h用于定义sleep;user/usys.S是用于从用户代码跳转到内核进行sleep的汇编代码。
  • 查看 user/echo.cuser/grep.cuser/rm.c 等程序,了解如何获取命令行参数和处理输入
  • 确保使用exit()退出程序
  • 将sleep添加至 Makefile的UPROGS中
  • 测试命令:./grade-lab-util sleepmake GRADEFLAGS=sleep grade

sleep.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 n\n");
        exit(1);
	}
    int sleepTime = atoi(argv[1]);
    if(sleepTime <= 0){
    	fprintf(2, "Error: Number must be positive.\n");
    }
    sleep(sleepTime);
    exit(0);
 }                                                                                  }

这里有个问题,明明sleep系统调用的原型如下:

MITS6.081 Lab1 Utilities_操作系统


然而在sleep.c中并没有使用sys_sleep而是直接使用sleep,这是因为之前框架编写者已经建立了sleep和sys_sleep调用编号之间的联系,在实验二添加新的系统调用就需要自己建立这种联系了,详细将在实验二中说明

运行测试:

MITS6.081 Lab1 Utilities_操作系统_02

pingpong (easy)

使用系统调用实现ping-pong程序

ping-pong程序要求父进程发送给子进程一个字节,子进程打印": received ping”

子进程发送给父进程一个字节然后exit,父进程打印": received pong”,然后exit

提示

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

遇到问题

MITS6.081 Lab1 Utilities_MIT6.S081_03

pipe()定义管道实际在调用函数,应该在main函数内部,哪怕是变量也应该定义在内部,编程习惯不好,改正

pingpong.c

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

int main(){
        char buf[5];
        int pf2c[2]; // the pipe from father to child
        int pc2f[2]; // the pipe from child to father
        pipe(pf2c);
        pipe(pc2f);

        write(pf2c[1], "a", 1); // write 1 byte to child

        if(fork() == 0)
        {
                if(read(pf2c[0], buf, 1) == 1) // child read from the pipe
                {
                        printf("%d: received ping\n", getpid());
                        write(pc2f[1], "b" , 1);
                        exit(0);
                }
                else
                {
                        exit(1);
                }
        }
        else
        {
				        //write(pf2c[1], "a", 1); // write 1 byte to child
                if(read(pc2f[0], buf, 1) == 1) // father read from the pipe
                {
                        printf("%d: received pong\n", getpid());
                        exit(0);
                }
                else
                {
                        exit(1);
                }
        }
}

测试:

MITS6.081 Lab1 Utilities_MIT6.S081_04

实验结束后的思考:

write(pf2c[1], "a", 1); 置于外部易读但也可以置于else中,因为父进程子进程并发进行,当子进程if(read(pf2c[0], buf, 1) == 1) 发现缓冲区内无数据时会进入阻塞等待,再父进程执行write(pf2c[1], "a", 1); 后子进程读入,此时父进程if(read(pc2f[0], buf, 1) == 1) 因缓冲区无数据进入阻塞等待,等待子进程写入数据

primes(moderate/hard)

使用管道和 fork 来建立管道。第一个进程将数字 2 到 35 送入管道。对于每一个质数,你将安排创建一个进程,通过管道从左邻右舍读取数据,并通过另一个管道向右邻右舍写入数据。由于 xv6 的文件描述符和进程数量有限,第一个进程可以在 35 处停止。

提示

  • 要小心关闭进程不需要的文件描述符,否则程序会在第一个进程运行到 35 时耗尽 xv6 的资源。
  • 第一个进程运行到 35 时,应等待整个管道(包括所有子进程、孙进程等)结束。因此,主 primes 进程只有在打印完所有输出并退出所有其他 primes 进程后才能退出。
  • 小提示:当管道的写入端关闭时,read 返回 0。
  • 最简单的方法是直接向管道写入 32 位(4 字节)int,而不是使用格式化的 ASCII I/O。
  • 只有在需要时才创建管道中的进程。
  • 将程序添加到 Makefile 的 UPROGS 中。
    首先理解质数筛选的原理,以下是伪代码和示意图
p = get a number from left neighbor
print p
loop:
    n = get a number from left neighbor
    if (p does not divide n)
        send n to right neighbor

MITS6.081 Lab1 Utilities_操作系统_05

思路 第一个进程不断向下一个进程写入数字 其他进程:不断读取上一个进程写入的数字,筛选操作后向下一个进程写入数字

fork后,如何实现父进程写入,子进程读出 当一个进程创建了一个新的子进程,该子进程拥有与父进程相同的fd,包括管道fd,实现父进程写入子进程读出:

  1. fork()
  2. 关闭父进程的读端
  3. 关闭子进程的写端

primes.c

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

static void primeSieve(int read_fd){ // read numbers from upper proc, deal with it and create new proc
	int p;
	if(read(read_fd, &p, sizeof(int)) == 0)
	{
		return;
	}

	printf("prime %d\n", p);

	int n;
	int new_pipefd[2];
	pipe(new_pipefd);

	if(fork() == 0)
	{
		close(new_pipefd[1]); // close child proc's write fd
		primeSieve(new_pipefd[0]); // recursive call
		close(new_pipefd[0]); // close child proc's read fd after reading all the numbers
		exit(0);
	}
	else
	{
		close(new_pipefd[0]); // close father proc's read fd
		while(read(read_fd, &n, sizeof(int)) != 0) // loop: read from upper proc
		{
			if(n % p != 0)
			{
				write(new_pipefd[1], &n , sizeof(int));
			}
		}
		close(new_pipefd[1]); // close father proc's write fd after passing all numbers from upper proc
		while(wait(0) > 0){}
	}
	return;
}

int main(){
	int pipefd[2];
	pipe(pipefd);
	if(fork() == 0) // the other process
	{
		close(pipefd[1]); // close child proc's write fd
		primeSieve(pipefd[0]);
		close(pipefd[0]); // close child proc's read fd
		exit(0);
	}
	else // the first process
	{
		close(pipefd[0]); // close first proc's read fd
		for(int i = 2; i <= 35; i++)
		{
			write(pipefd[1], &i, sizeof(int));
		}
		close(pipefd[1]); // close first proc's write fd after passing all numbers
		
		while(wait(0) > 0){} // wait all child procs exit
		exit(0);
		
	}
	return 0;
}

运行结果:

MITS6.081 Lab1 Utilities_MIT6.S081_06

测试结果

MITS6.081 Lab1 Utilities_操作系统_07

实验小结:

这是我第一次接触并行计算,还是很有意思的 在做实验时在想前一个进程是否需要等待后一个进程处理完再传下一个数据,当时我想建立一个双向管道,子进程处理完一个数字给父进程发送一条信息,父进程再传下一数字,这样就与并行计算背道而驰了。实际上父进程可以不断向子进程传如数字,原因是管道的阻塞机制,当父进程传入数字发现管道缓冲区内的数字还没被子进程读出时会进入堵塞状态,直到子进程读出才会传入下一数字,子进程的读出阻塞同理

find(moderate)

提示

  • 请参阅 user/ls.c 了解如何读取目录。
  • 使用递归功能让查找进入子目录。
  • 不要递归到". "和"..."。
  • 文件系统的更改会在运行 qemu 时持续存在;要获得干净的文件系统,请运行 make clean,然后再运行 make qemu。
  • 你需要使用 C 字符串。请参阅 K&R(C 语言书籍),例如第 5.5 节。
  • 注意 == 不能像 Python 那样比较字符串。
  • 将程序添加到 Makefile 的 UPROGS 中。

代码阅读

// stat.h
#define T_DIR     1   // Directory
#define T_FILE    2   // File
#define T_DEVICE  3   // Device

struct stat {
  int dev;     // File system's disk device
  uint ino;    // Inode number
  short type;  // Type of file
  short nlink; // Number of links to file
  uint64 size; // Size of file in bytes
};
//fs.h
// Directory is a file containing a sequence of dirent structures.
// filename's max length
#define DIRSIZ 14  
// 目录项的数据结构
struct dirent {
  ushort inum; // inode
  char name[DIRSIZ]; // filename
};
// ls.c
#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;

  // Find first character after last slash.
  for(p=path+strlen(path); p >= path && *p != '/'; p--)
    ;
  p++;

  // Return blank-padded name.
  if(strlen(p) >= DIRSIZ)
    return p;
  memmove(buf, p, strlen(p));
  memset(buf+strlen(p), ' ', DIRSIZ-strlen(p));
  return buf;
}

void
ls(char *path)
{
  char buf[512], *p;
  int fd;
  struct dirent de;
  struct stat st;

  if((fd = open(path, 0)) < 0){
    fprintf(2, "ls: cannot open %s\n", path);
    return;
  }

  if(fstat(fd, &st) < 0){
    fprintf(2, "ls: cannot stat %s\n", path);
    close(fd);
    return;
  }

  switch(st.type){
  case T_FILE:
    printf("%s %d %d %l\n", fmtname(path), st.type, st.ino, st.size);
    break;

  case T_DIR:
    if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){
      printf("ls: path too long\n");
      break;
    }
    strcpy(buf, path); // buf = path
    
    // 在buf最后添加'/'
    p = buf+strlen(buf);
    *p++ = '/'; 
    
    while(read(fd, &de, sizeof(de)) == sizeof(de)){ // 不断读取路径下的目录项
      if(de.inum == 0) // 忽略无效目录项
        continue;
      memmove(p, de.name, DIRSIZ); // 复制de.name的DIRSIZ个字节到p,.../(p)
      p[DIRSIZ] = 0;
      if(stat(buf, &st) < 0){ // 获取新路径文件的详细信息,存入st
        printf("ls: cannot stat %s\n", buf);
        continue;
      }
      printf("%s %d %d %d\n", fmtname(buf), st.type, st.ino, st.size);
    }
    break;
  }
  close(fd);
}

int
main(int argc, char *argv[])
{
  int i;

  if(argc < 2){
    ls(".");
    exit(0);
  }
  for(i=1; i<argc; i++)
    ls(argv[i]);
  exit(0);
}

代码分析 通过阅读ls.c的代码可以看出,fmtname函数获取path路径最后一个\后的字符内容,如/home/user/abc,将这个path传入fmtname返回abc。

一个目录中的所有文件和子目录都以目录项的形式保存,如果abc是一个目录,通过语句while(read(fd, &de, sizeof(de)) == sizeof(de)),不断读取目录项保存到de中。

通过memmove(p, de.name, DIRSIZ)语句将该目录项的名字接到path后组成新的path(保存在buf中),新的path为path/de

有了新的path就可以调用stat(buf, &st)将目录项的详细信息存入st中,随后通过st打印详细信息

要点分析 ls不需要打印子目录中的内容,find在遇到子目录时需要递归调用find函数进入子目录搜索 由于find不需要打印整齐的内容并且需要进行字符串比较,所以修改fmt函数直接返回无空格填充的字符串 Linux系统的文件中体系中,一个目录中除了包含其中的文件和子目录外还包含当前目录:"."和上级目录"..",当遍历目录中的目录项时需忽略这两个目录

find.c

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

char* fmtname(char *path)
{
  char *p;
  // Find first character after last slash.
  for(p=path+strlen(path); p >= path && *p != '/'; p--);
  p++;
  // Return no blank-padded name.
	return p;
}

void find(char *path, char *target)
{
    char buf[512], *p;
    int fd;
    struct dirent de;
    struct stat st;

    // open path get fd
    if ((fd = open(path, 0)) < 0) {
        fprintf(2, "find: cannot open %s\n", path);
        return;
    }

    // get st
    if (fstat(fd, &st) < 0) {
        fprintf(2, "find: cannot stat %s\n", path);
        close(fd);
        return;
    }

    switch (st.type) {
    case T_FILE: // st is a file
        if (strcmp(fmtname(path), target) == 0) {
            printf("%s\n", path);
        }
        break;

    case T_DIR: // st is a dir
        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)
                continue;

            memmove(p, de.name, DIRSIZ);
            p[DIRSIZ] = 0;

            if (stat(buf, &st) < 0) {
                printf("find: cannot stat %s\n", buf);
                continue;
            }

            if (st.type == T_DIR && strcmp(de.name, ".") != 0 
                && strcmp(de.name, "..") != 0) 
            {
				find(buf, target); // find in the subdir
            } 
			else if (st.type == T_FILE) {
                if (strcmp(fmtname(buf), target) == 0) { 
                    // compare the dir's all files
                    printf("%s\n", buf);
                }
            }
        }
        break;
    }

    close(fd);
}

int main(int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(2, "Usage: find <path> <filename>\n");
        exit(1);
    }
    find(argv[1], argv[2]);
    exit(0);
}

补充说明:

在遍历子目录时,其实可以不判断目录项类型直接形成新的path调用find,因为我们已经在find函数中定义了fmtname返回类型为文件的情况。但分开目录与文件能够提高可读性并且提高性能,chatgpt是这样建议的

xargs(moderate)

从标准输入中读取行数,并为每一行运行一条命令,将行数作为参数提供给命令。

MITS6.081 Lab1 Utilities_操作系统_08

请注意,这里的命令是 "echo bye",而附加参数是 "hello too",因此命令是 "echo bye hello too",输出结果是 "bye hello too"。

提示

  • 使用 fork 和 exec 在每一行输入上调用命令。 在父进程中使用 wait 等待子进程完成命令。
  • 要读取单行输入,每次读取一个字符,直到出现换行符 ('\n')。
  • kernel/param.h 声明了 MAXARG,这在需要声明 argv 数组时可能有用。
  • 将程序添加到 Makefile 的 UPROGS 中。
  • 文件系统的更改会在运行 qemu 时持续存在;要获得干净的文件系统,运行 make clean,然后再运行 make qemu。

解决思路

主函数不断从标准输入中读取字符,遇到\n调用子函数

子函数中:创建子进程执行命令,父进程等待

遇到的问题:

结果正确但引发了usertrap

MITS6.081 Lab1 Utilities_MIT6.S081_09

这里犯了一个很sb的错误,main最后写了return 0;改为exit(0)即可….,之前sleep的要求没记住。结合我的猜测和chatgpt的分析造成usertrap的原因大概是:

  • return 0 是从 main 函数中返回的标准 C 语言语句。它只是简单地退出 main 函数,并将控制权返回给操作系统,但它不会执行额外的进程终止处理,如清理资源或通知操作系统进程已正常终止。
  • 在某些操作系统中,特别是在低层次的系统调用和操作中,return 0 可能不会触发完整的进程终止流程。操作系统可能会认为进程以不正确的方式终止,从而导致异常处理机制(如 usertrap)被触发。

xargs.c

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

int main(int argc , char* argv[]){
	char* newargv[MAXARG];	
	char buf[512]; // store the arg read from std input
	char ch;
	int index = 0;

	if(argc < 2){
		printf("Usage: xargs commend <arg>");
		exit(1);
	}
	else if(argc >= MAXARG){
		printf("xargs's arg too long\n");
		exit(1);
	}

	for(int i = 0; i < argc-1; i++){
		newargv[i] = argv[i+1];
	}
	newargv[argc] = 0;

	while(read(0, &ch, sizeof(char)) > 0){
		if(ch == '\n'){
			buf[index] = '\0';
			newargv[argc-1] = buf;

			// execute with new arg
			//for(int i = 0; i < argc; i++) printf("argv[%d]: %s\n", i, newargv[i]);
			if(fork() == 0){
				exec(newargv[0], newargv);
			}
			else{
				wait(0);
			}
			
			// reset
			index = 0;
			memset(buf, 0, 512);
		}
		else{
			if(index < 512 - 1)
				buf[index++] = ch;
			else{
				printf("New argument is too long\n");
				exit(1);
			}
		}
	}
	exit(0);
	//return 0;
}

运行测试脚本

MITS6.081 Lab1 Utilities_操作系统_10

最后make grade测试全部

MITS6.081 Lab1 Utilities_MIT6.S081_11

实验总结

sleep实验学习使用系统调用,pingpong、primes实验学习管道使用,find实验学习文件结构遍历,xargs实验学习exec使用

做完实验回头看实验并不难,然而由于这是第一次实验对实验整体风格不熟悉,以及对管道、系统结构等原理不了解走了许多弯路,实际完成时间到达了官方分析时间的2~3倍