MIT_6.s081_Lab1:Xv6 and Unix utilities
于2022年3月1日2022年3月1日由Sukuna发布
运行环境:Ubuntu 20.04 qemu
在做6.s081的实验之前我们首先要先下载Xv6操作系统以及qemu虚拟机:
代码语言:javascript复制sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu
调试的gbd工具使用方法:在Ubuntu的终端输入这个命令即可
记住端口号,是tcp::26000
另起一个窗口,输入下面命令:
输入 file ./kernel/kernel载入符号表,然后target remote loaclhost:26000即可:
Lab1_1:Boot xv6
运行并安全退出xv6系统:
运行的方法很简单:cd进xv6的文件夹里面,然后输入`make qemu`即可,输入完之后就是进入了xv6的系统里面了
退出的方法就是,先Ctrl A,再输入x即可.
Lab1_2 sleep
本实验要为 xv6 实现 UNIX 程序 sleep; 您的睡眠应暂停用户指定的滴答数。 滴答是 xv6 内核定义的时间概念,即来自定时器芯片的两次中断之间的时间。
一些提示:
- 在开始编码之前,请阅读xv6 书籍第 1 章。(中文版xv6 书籍)
- 查看
user/
中的其他一些程序 (例如,user/echo.c
、user/grep.c
和user/rm.c
)以了解如何获取传递给程序的命令行参数。 - 如果用户忘记传递参数, sleep 应该打印错误消息。
- 命令行参数作为字符串传递;您可以使用
atoi
将其转换为整数(请参阅 user/ulib.c)。 - 使用系统调用
sleep
。 - 确保
main
调用exit()
以退出您的程序。 - 将你的
睡眠
程序添加到Makefile 中的UPROGS
;完成后,make qemu
将编译您的程序,您将能够从 xv6 shell 运行它。 - 查看 Kernighan 和 Ritchie 的书*The C programming language (second edition)*(K&R)以了解 C。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc,char *argv[]){
for(int i=0;!argv[i];i ){
if(argv[1][i]>'9'||argv[1][i]<'0'){
write(1, "errorn", 6);
exit(-1);
}
}
int times=atoi(argv[1]);
sleep(times);
exit(0);
}
首先我们知道在C语言中argc代表传入的参数数,argv是传入的参数.
第一个for循环就是判断这个是不是一个正常的数字(什么时候结束循环呢?),第二步用atoi函数转化成数字,最后执行系统调用sleep.
Lab1_3 pingpong
编写一个程序,使用 UNIX 系统调用在两个进程之间通过一对管道“乒乓”一个字节,每个管道一个。 父母应该向孩子发送一个字节; 子进程应该打印“: received ping”,其中 是它的进程 ID,将管道上的字节写入父进程,然后退出; 父母应该从孩子那里读取字节,打印“: received pong”,然后退出。
一些提示:
- 使用管道创建管道。
- 使用 fork 创建一个孩子。
- 使用 read 从管道读取,并使用 write 写入管道。
- 使用 getpid 查找调用进程的进程 ID。
- 将程序添加到 Makefile 中的 UPROGS。
- xv6 上的用户程序有一组有限的库函数可供它们使用。 可以在 user/user.h 中看到列表; 源(系统调用除外)位于 user/ulib.c、user/printf.c 和 user/umalloc.c。
我们可以认为pipe是一个Linux进程间通讯的一种方式,一个管道以一个两位的int类型数组构成,其中第一个元素是读端的接口编号,第二个元素是写端的接口编号.然后可以使用read和write来进行读取,注意管道的端口有读端口和写端口之分,读的时候只能从读端口读数据,写的时候只能从写的端口写数据.
有点像我们之前操作系统课上面学到的缓冲区的结构,缓冲区有两端,一端读一端写.
系统调用: 可以使用pipe(一个二位的数组)来初始化一个管道.经过pipe了之后,第一个元素就是一个读取的端口,第二个元素就是对应写入的端口, 可以使用read(读端口,读出来的元素写在哪里,长度)来从一个读的端口读出元素 可以使用write(写端口,写出来的元素写在哪里,长度)来把元素写进一个端口.
fork函数就是一次调用,两次返回,调用之后父进程和子进程都从获得函数的返回值开始继续往下运行,就像一条河流,遇到了一个分叉口,河流分成了两叉,这两叉都从分叉口开始继续往下流.在这里分叉口就像fork()调用,调用生成了两个分叉,两个分叉都从fork()调用结束后继续往下走.
代码语言:javascript复制#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(){
int p_filedes[2],s_filedes[2];
pipe(p_filedes);
pipe(s_filedes);
char buf[4];
if(fork()==0){
read(p_filedes[0],buf,4);
printf("%d: received %sn",getpid(),buf);
write(s_filedes[1],"pong",4);
}else{
write(p_filedes[1],"ping",4);
read(s_filedes[0],buf,4);
printf("%d: received %sn",getpid(),buf);
}
exit(0);
}
函数的思路就是先初始化两根管道,接着父进程先read再写,子进程先写再read.
Lab1_4 prime
编写一个程序,完成一个并发版本的prime初筛,第一个进程负责把2-35范围内的素数输入进管道,对于每一个素数,我们都需要fork一个进程来接受管道的数字然后输出出来.
- 关闭进程不需要的文件描述符,否则你的程序将在第一个进程达到 35 之前耗尽 xv6 的资源。
- 一旦第一个进程达到 35,它应该等到整个管道终止,包括所有子、孙等。因此,主素数进程应该只在所有输出都被打印出来,并且在所有其他素数进程都退出之后才退出。
- 当管道的写端关闭时,read 返回零。
- 将 32 位(4 字节)整数直接写入管道是最简单的,而不是使用格式化的 ASCII I/O。
- 您应该仅在需要时在管道中创建流程。
- 将程序添加到 Makefile 中的 UPROGS。
基本的思路在下面,每一个进程对应一个素数,主进程负责传输2-34的数据给子进程们,每个进程对应一个素数,如果这个数%这个素数不为0的话就可以传给下一级的进程,如果没有下一级的进程那么fork一个新的进程,这个数一定是素数,这个进程就会接着处理来自左边邻居的数据,处理的方式.这样子每一个进程就像一个筛子,筛选不可能是素数的数.
总的来说主进程的数据首先从左到右到第一个子进程,判断能不能被2除,不可以就继续从左到右交给下一个子进程,判断能不能被3除…,如果下一个子进程是不存在的,那么新建一个进程,这个进程就代表对应数.
Lab1_5 find
编写一个简单版本的 UNIX 查找程序:查找目录树中具有特定名称的所有文件。给定对应的文件名以及文件名在目录,找到文件名的位置.
- 查看 user/ls.c 以了解如何读取目录。
- 使用递归允许 find 访问到子目录。
- 不要递归到“.” 和 ”..”。
- 对文件系统的更改在 qemu 运行中持续存在; 要获得一个干净的文件系统,请运行 make clean 然后 make qemu。
struct dirent {
ushort inum;
char name[DIRSIZ];
};
上面是文件系统中关于目录文件内容的说明,inum说明这个文件占用了几个inode.该操作系统类似于Linux系统,磁盘分成磁盘块,磁盘块的一些相关控制信息就储存在存放在内存中的inode.文件系统获得文件,先找到文件的file结构,根据file结构找到inode,再根据inode找到磁盘块.
说白了目录文件存储的就是一堆dirent类型的结构体.
下面就是stat信息,stat信息存放了文件的一些控制信息,比如说链接信息,大小和类型之类的.在我们利用open打开文件后,open函数会返回一个数字,我们再利用fstat这个调用找到stat控制块.
代码语言:javascript复制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
};
我们先来看看xv6是如何完成对ls指令的支持的?
首先第一个函数:根据文件的的路径名提取出文件的名字,就是从后往前遍历,找到第一个‘/’,这之间的那一部分就是文件的名字.
接着就是ls函数,ls函数中只需要提供当前的path,找到path里面的所有文件即可.首先先打开当前path对应的文件(Linux内部目录文件和普通的文件都是文件),再利用fstat系统调用找到stat的值.由于目录文件里面就是连续地存储了一堆dirent类型的结构体,那我们可以把目录文件的内容当成一个struct dirent[MAX](结构体数组,一个结构体一个结构体地去读)
最后就是main函数,就是看看输入的指令罢了
代码语言:javascript复制#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 %sn", path);
return;
}
if(fstat(fd, &st) < 0){
fprintf(2, "ls: cannot stat %sn", path);
close(fd);
return;
}
switch(st.type){
case T_FILE:
printf("%s %d %d %ln", fmtname(path), st.type, st.ino, st.size);
break;
case T_DIR:
if(strlen(path) 1 DIRSIZ 1 > sizeof buf){
printf("ls: path too longn");
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("ls: cannot stat %sn", buf);
continue;
}
printf("%s %d %d %dn", 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指令稍微修改修改,ls只用找当前目录第一层的所有文件,那么我们也是找当前目录的所有文件,对于目录文件,我们继续往下搜索这个目录,对于普通文件,我们只用看名字是否匹配即可.
Lab1_6 xargs
这个指令就是我们要把若干条指令合并在一块进行执行.其中前面指令的standard out会作为下一条的指令一个输入来进行执行.
举个例子:前面指令的hello too作为standard out作为下一条指令的输入.
代码语言:javascript复制$ echo hello too | xargs echo bye
$ bye hello too
- 使用 fork 和 exec 对每一行输入调用命令。 在父级中使用 wait 等待子级完成命令。
- 要读取单行输入,请一次读取一个字符,直到出现换行符 (‘n’)。
- kernel/param.h 声明了 MAXARG,如果您需要声明 argv 数组,这可能很有用。
- 对文件系统的更改在 qemu 运行中持续存在; 要获得一个干净的文件系统,请运行 make clean 然后 make qemu。
- 将程序添加到 Makefile 中的 UPROGS。
首先第一步把指令xargs给删除掉:然后把标准输出(来源:0)获取下来,放入最后一个参数中进行执行.
代码语言:javascript复制#include "kernel/types.h"
#include "kernel/param.h"
#include "user/user.h"
int main(int argc, char *argv[]){
char* argvs[MAXARG];
for(int i=1;i<argc;i ){
argvs[i-1]=argv[i];
}
char buf[512];
int index;
int read_len;
while(1){
index = -1;
do{
index ;
read_len=read(0,&buf[index],sizeof(char));
}while(read_len>0&&buf[index]!='n');
if(read_len==0&&index==0){
break;
}
buf[index]='