xv6实验记录(一)
友情链接: 友情链接
实验平台的搭建【VMWare + Ubuntu + qemu + xv6】
-
安装VMWare
官网在这:《VMWare的官网》。找寻CSDN其他文章,傻瓜式安装即可
-
安装Ubuntu
这里需要注意,装载的ubuntu镜像文件最好选择最新的20版本,如果选用老版本是无法直接安装qemu直接跑的,可能会有一些奇奇怪怪的问题,所以直接选用最新的版本,点击此处链接《 20版本的Ubuntu镜像文件》进去链接下载后缀为iso的文件即可。【配置镜像文件到VMWare还是找CSDN傻瓜式配置即可】
-
安装qemu
先检测当前Ubuntu的版本是否可用,输入
ctrl+alt+T
打开终端输入指令cat /etc/debian_version
如果显示的是,就说明没有问题
bullseye/sid
若是显示如下,则说明要去更新版本了
buster/...
1.升级apt并更换虚拟机的服务器镜像:输入如下指令
sudo apt update
2.换阿里的源:在Ubuntu20.04.3桌面版中,点击左下角“显示应用程序”,搜索“软件和更新”,点击进入。
3.在“软件与更新”界面中的“Ubuntu软件”一栏, 选择“下载自”下拉菜单中的“其他站点”。
然后点击“选择最佳服务器”,系统会自动测试下载的服务器,等待一段时间后,选择系统给出的镜像服务器,软件源修改完成。【使用阿里云的够了】
4. 使用命令下载前置相关包
这些包是要运行qemu不可或缺的前置组件[6],直接打开命令行终端复制粘贴运行即可安装。注意,这里安装包能否成功运行是和你的ubuntu版本有关系的
sudo apt-get install autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev
5. 安装riscv64-unknown-elf-gcc
sudo apt install riscv64-unknown-elf-gcc
6. 安装xv6
检查前面的组件是否安装成功…
riscv64-unknown-elf-gcc --version qemu-system-riscv64 --version
克隆xv6的源码仓库
git clone git://g.csail.mit.edu/xv6-labs-2021
7.使用
先打开上述步骤中git clone下来的库,名称应该是xv6-labs-2021,我改成了xv6-riscv,cd进去,然后输入指令:
make
输入以后你应该会看到一大堆输出,这里表示我们的xv6系统正在编译
在这些输出完成以后,输入指令:
make qemu
编译我们的qemu系统,输出如下:
最后如果看到终端显示出:init: starting sh,如下图:
这就说明我们的安装成功啦,可以愉快地完成后续的任务了,还可以再输入ls验证一下:
一、Sleep(easy)
编写前提示
- 开始编码之前,请阅读xv6手册第一章
- 查看文件夹
user/
中的一些其他程序(例如:user/echo.c
、user/grep.c
、user/rm.c
)了解如何将获得的命令行参数传递给程序 - 若用户忘记传递参数,sleep程序应该打印一条错误的提示信息
- 命令行参数作为字符串传递,应使用
atoi
函数将其转为整数(具体查看user/ulib.c中的atoi函数) - 参阅睡眠系统调用的xv6内核代码
kernel/sysproc.c
(sys_sleep函数) - 确保main中调用
exit()
以退出程序
编写后提示
假设你编写的程序名为“sleep.c”。在编写完成后:
- 请将**“sleep.c”拷贝至 xv6 源码目录下的 user/文件夹下**
- 然后在 xv6 源码根目录下的
Makefile
文件里,找到关键字UPROGS=\(可能
是第 179 行左右),在其下面列出的多个以&U
开头的字符串的下方(可能是
第 195 行左右),添加一行&U/_sleep\,并保存; - 回到 xv6 的源码目录的根目录下,敲
make qemu
,以让sleep.c
得以编译并
在 xv6 内部形成可执行程序; - 顺利进入到 xv6 的终端后,敲
“./sleep 5”
;
了解命令行如何传参
查看源文件 echo 了解一下如何传递参数
- 查看此文件了解如何进行传递参数
//echo.c源文件
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[]){
int i;
for(i = 1; i < argc; i++){
write(1, argv[i], strlen(argv[i]));
if(i + 1 < argc){
write(1, " ", 1);
} else {
write(1, "\n", 1);
}
}
exit(0);
}
通过上述echo.c
源码文件,了解到参数作用:
argc
:表示的是命令行总的参数个数argv
:数组类型,按序保存了接收的所有参数,其中第 0 个是程序的全名,之后的参数就是用户输入的参数write函数
:该函数接收三个参数- 第一个参数:0 表示 stdin,1 表示 stdout ,2 表示 stderr
- 第二个参数:输出的内容
- 第三个参数:输出内容的长度
在命令行使用echo程序得到结果:
[roc@roclinux ~]$ echo 'Hello World' //输入
Hello World //输出
字符串转为整数(atoi函数)
// atoi函数的源码
int atoi(const char *s){
int n;
n = 0;
while('0' <= *s && *s <= '9')
n = n*10 + *s++ - '0';
return n;
}
编写Sleep
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int
main(int argc, char *argv[]){
if(argc != 2){ //上述提到,这是参数总数,第一个是程序名字,第二个开始是用户输入的参数
fprintf(2, "Error: please input one parameter..\n");
exit(1);
}
int time = atoi(argv[1]);
sleep(time); //此处是xv内核 sys_sleep 函数的调用
exit(0);
}
二、pingpong(easy)
该实验是一个典型的进程间通信应用。它的运行逻辑是:父进程使用fork()
系统调用,创建一个子进程,然后使用管道pipe
,在两者之间进行通信。
编写前提示
- 实验之前,建议在网络上了解 fork、pipe、write、read、getpid函数的作用
- 使用pipe创建管道
- 使用fork创建子进程
- 使用read读取pipe,并使用write写入pipe
- 使用getpid查找调用进程的进程ID
- 程序编写完成后,参考上述Sleep实验的程序编译部分,把 pingpong 程序放到对应的 xv6
源码目录,然后修改 Makefile 文件,再编译运行
fork函数
- fork创建一个子进程并完全复制父进程,且子进程是从fork后面的指令开始执行的。
为已存在的进程创建一个子进程,而原进程称为父进程。调用该函数后,内核开始分配新的内存块和内核数据结构给子进程。
特点:
- 调用一次,返回两次并发执行
- 有独立使用的地址空间
- 函数fork返回值:向父进程返回子进程的ID,向子进程返回 0
- 在父进程中,fork() 返回新创建子进程的进程pid;
- 在子进程中,fork() 返回0;
- 如果出现错误,fork() 返回一个负值;
- 不同的进程有一个唯一的不同进程pid,通过 getpid() 函数可以知道当前进程pid,可以说我们就是通过这个pid知道当前所在进程。
fork 调用失败的原因:
- 系统中有太多进程。
- 实际用户的进程数超过限制。
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
pipe函数
pipe()
函数是一个用于创建无名管道
#include <unistd.h>
int pipe(int pipefd[2]); //函数原型
参数:
-
pipefd[2]
:一个整数数组,包含了两个文件描述符。文件描述符是操作系统中用于标识和操作文件或I/O资源的抽象概念。它是一个非负整数,通常作为索引或句柄,用于访问文件、套接字、管道和其他I/O设备。
-
pipefd[0]
用于读取,pipefd[1]
用于写入。
返回值:
- 成功时返回0。
- 失败时返回-1,并设置
errno
来指示错误的原因。
那么管道如何实现进程间的通信?
- 父进程创建管道,得到两个⽂件描述符指向管道的两端
- 父进程fork出子进程,⼦进程也有两个⽂件描述符指向同⼀管道。
- 《0-读,1-写》父进程关闭pipefd[0],子进程关闭pipefd[1],即⽗进程关闭管道读端,⼦进程关闭管道写端(因为管道只支持单向通信)。⽗进程可以往管道⾥写,⼦进程可以从管道⾥读,管道是⽤环形队列实现的,数据从写端流⼊从读端流出,这样就实现了进程间通信。
需要使用的函数已经在前置知识有介绍,知识略浅,需要在网络上了解更多详细知识,此处不在赘述
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc,char *argv[]){
int parent_fd[2];
int child_fd[2];
//调用函数创建管道,并判断是否创建成功
if(pipe(parent_fd) == -1)printf("Error: parernt_pipe not create...");
if(pipe(child_fd) == -1)printf("Error: child_pipe not create...");
//创建子进程,获取进程ID
int pid = fork();
//创建数据容器,保存数据
char buf[64];
if(pid == 0){//获取的PID == 0时为子进程
read(parent_fd[0],buf,4);
printf("%d: reacive %s\n",getpid(),buf);
write(child_fd[1],"ping",strlen("ping"));
}else{
//PID != 0 时为父进程
write(parent_fd[1],"pang",strlen("pang"));
read(child_fd[0],buf,4);
printf("%d: reacive %s\n",getpid(),buf);
}
close(parent_fd[0]);
close(parent_fd[1]);
close(child_fd[0]);
close(child_fd[1]);
exit(0);
}
注意进程死锁,上述if - else中的读和写语句要错位,如果两个进程一开始都是读文件,即会造成进程死锁
三、质数primes
实验步骤
- 父进程A生成【2-35】之间的所有数字,包括2和35
- 向A的子进程B进行输出(通过管道)
- B对[2-35]进行筛选,打印出第一个质数2
- B把剩下的数字(通过)通过管道传递给B的子进程C
- C打印出第二个质数,也就是3
- 重复(4)和(5)的过程,直到2 - 35 之间的质数全部在终端输出
一些提示
-
请小心关闭进程不需要的文件描述符,因为否则,您的程序将在第一个进程达到35之前在资源中运行xv6。
-
一旦第一个进程达到35,它应该等待直到整个管道终止,包括所有子代,孙代为止。因此,仅在打印完所有输出之后以及在所有其他准备工作都退出之后,才应退出主要准备工作。
-
提示:当管道的写侧关闭时,read返回零。
-
将32位(4字节)int直接写入管道是最简单的,而不是使用格式化的ASCII I / O。
-
您仅应在需要时才在管道中创建进程。
将程序添加到Makefile中的UPROGS。
此处选取算法埃拉托斯特尼筛法来实现快速筛选质数
建议在网络上了解上述算法的原理再回来看该程序会更容易理解一点
![image-20230918200904069](https://i-blog.csdnimg.cn/blog_migrate/61660016bd750796fdf0225cf9531c90.png)
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
//用指针的形式,让所有子进程能够读取同一块空间的数据,num记录目前还需要筛选数子的个数
void solution(int *array,int num){
if(num == 1){ // 筛选到最后一个数字的时候,该数字已经是质数了,所以程序也就结束了
printf("prime is %d\n",*array);
return;
}
int fd[2], i ,temp;
int prime = *array;
printf("prime is %d\n",prime);;
pipe(fd);
if(fork() == 0){ //父进程创建一个子进程(一),子进程循环写入剩余的每一个数字到管道
for(i = 0; i < num; i++){
temp = *(array + i);
write(fd[1], (char *)(&temp), 4);
}
exit(0);
}
close(fd[1]);
if(fork() == 0){ //父进程再创建一个子进程(二),子进程读取上面一个子进程写入管道的数据
int count = 0; //该变量记录本轮没有被筛掉的数字个数
char buffer[4];
while(read(fd[0], buffer, 4) != 0){
temp = *((int *)buffer);
if(temp % prime != 0){
*array = temp;
array = array + 1;
count ++;
}
}
solution(array - count,count);
exit(0)
}
}
int main(int argc,char argv[]){
int target[34];
int i = 0;
for(;i < 34;i++) target[i] = i + 2;
solution(target, 34);
exit(0);
}