环境搭建
在Ubuntu虚拟机中执行如下命令, 升级软件包
sudo apt update
复制官网给出的命令,执行.
sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu
在一个新目录中,执行如下,拉取xv6源码
git clone git://g.csail.mit.edu/xv6-labs-2021
进入源码目录,并切换分支:
cd xv6-labs-2021
git checkout util
实验概要
本次实验将围绕操作系统的用户态与内核态、进程创建和进程间通信等核心概念进行实践。首先,通过实现一个用户态的sleep程序并补全编译文件,我们将初步理解用户态与内核态之间的区别。其次,我们将通过编写一个pingpong示例程序来深入了解子进程的创建和进程间通信的管道方法。最后,我们将利用所学的子进程和管道知识,编写一个primes程序,并通过多进程的方式解决素数筛选问题。通过这三个任务,我们将对操作系统的核心概念有更深入的理解和实践。
任务1: sleep
解题思路
该任务需要实现一个名为sleep
的程序。该程序应该接受用户指定的tick数作为命令行参数,并暂停程序的执行相应的tick数。如果用户未提供参数,则应输出错误信息。可以使用atoi
函数将命令行参数转换为整数,并使用系统调用sleep
实现功能。在main
函数中调用exit()
以正确退出程序,并在Makefile的UPROGS中添加sleep
程序。最后,通过make qemu
命令编译并在xv6中运行程序。
步骤
- 在源码目录下, 使用如下命令创建
sleep.c
vim ./usr/sleep.c
- 根据要求编写代码
#include "kernel/types.h" // 引入类型定义,例如int32_t等
#include "kernel/stat.h" // 引入文件状态定义,例如文件的访问权限、大小等
#include "user/user.h" // 引入用户级函数,例如打印信息、退出程序等
int
main(int argc, char *argv[])
{
if(argc != 2){ // 检查参数个数是否足够
fprintf(2, "参数数量错误\n"); // 打印提示信息
exit(1); // 退出程序,返回错误码
}
int ticks = atoi(argv[1]);
sleep(ticks);
exit(0);
}
- 使用以下命令打开
Makefile
文件, 使用:196
快速定位196行, 添加$U_sleep\
vim Makefile
- 使用命令
make qemu
编译生成可执行文件,在xv6
终端输入sleep 10
查看效果
任务2: pingpong
解题思路
该任务需要编写一个程序,使用系统调用在两个进程之间的一对管道上进行“pingpong
”,即交替发送和接收一个字节。 程序应该创建一对用于两个方向的管道,并在父进程和子进程之间进行通信。父进程应该向子进程发送一个字节;子进程应该打印出“<pid>:received ping
”,其中<pid>
是它自己的进程ID,将字节写入管道并退出;父进程应该从子进程读取字节,打印“<pid>:received pong
”,然后退出。我们可以使用pipe
创建管道,使用fork
创建子进程,使用read
读取管道,使用write
向管道中写入数据,使用getpid
获取进程ID。
步骤
- 在源码目录下, 使用如下命令创建
pingpong.c
vim ./usr/pingpong.c
- 根据要求编写代码
#include "kernel/types.h" // 引入类型定义,例如int32_t等
#include "kernel/stat.h" // 引入文件状态定义,例如文件的访问权限、大小等
#include "user/user.h" // 引入用户级函数,例如打印信息、退出程序等
int main(int argc, char* argv[]) {
if (argc > 1) { // 检查命令行参数数量
fprintf(2, "参数数量错误\n"); // 输出错误信息到标准错误输出
exit(1); // 退出程序并返回1
}
int p1[2]; // 定义两个整型数组作为管道p1
int p2[2]; // 定义两个整型数组作为管道p2
if (pipe(p1) == -1) { // 创建第一个管道
fprintf(2, "bad: 分配管道结构体失败\n");
exit(1);
}
if (pipe(p2) == -1) { // 创建第二个管道
fprintf(2, "bad: 分配管道结构体失败\n");
exit(1);
}
int pid = fork(); // 创建子进程
if (pid == -1) { // 子进程创建失败
fprintf(2, "创建子进程失败\n");
exit(1);
} else if (pid == 0) { // 子进程中
char buf;
close(p1[1]); // 关闭管道p1的写端
close(p2[0]); // 关闭管道p2的读端
read(p1[0], &buf, 1); // 从管道p1的读端读取一个字符到buf中
close(p1[0]); // 关闭管道p1的读端
printf("%d: received ping\n", getpid()); // 输出进程ID,并打印"received ping"
write(p2[1], "O", 1); // 向管道p2的写端写入一个字符'O'
close(p2[1]); // 关闭管道p2的写端
} else { // 父进程中
close(p1[0]); // 关闭管道p1的读端
close(p2[1]); // 关闭管道p2的写端
write(p1[1], "I", 1); // 向管道p1的写端写入一个字符'I'
close(p1[1]); // 关闭管道p1的写端
char buf;
read(p2[0], &buf, 1); // 从管道p2的读端读取一个字符到buf中
printf("%d: received pong\n", getpid()); // 输出进程ID,并打印"received pong"
close(p2[0]); // 关闭管道p2的读端
}
exit(0); // 退出程序并返回0
}
- 使用以下命令打开
Makefile
文件, 在$U_sleep\
下添加
$U_pingpong\
- 使用命令
make qemu
编译生成可执行文件,在xv6
终端输入pingpong
查看效果
任务3: primes
解题思路
该任务需要使用子进程和管道实现并发素数筛选算法, 每个进程输出它得到的数据中的最小素数, 并忽略数据中最小素数的倍数, 将其他数通过管道写入下一个子进程中进行处理, 处理至35即可.
步骤
- 在源码目录下, 使用如下命令创建
primes.c
vim ./usr/primes.c
- 根据要求编写代码
#include "kernel/types.h" // 引入类型定义,例如int32_t等
#include "kernel/stat.h" // 引入文件状态定义,例如文件的访问权限、大小等
#include "user/user.h" // 引入用户级函数,例如打印信息、退出程序等
void process(int * p);
int main(int argc, char *argv[])
{
int p[2]; // 定义一个整型数组p,用于存储管道的读写文件描述符
if (pipe(p) == -1) { // 创建管道,将读写文件描述符存储在p数组中
fprintf(2, "bad: 分配管道结构体失败\n");
exit(1);
}
for (int i = 2; i <= 35; i++) { // 循环从2到35,依次写入数据到管道中
write(p[1], &i, sizeof(int)); // 向管道的写入端写入整数i的值
}
close(p[1]); // 关闭管道的写入端
process(p); // 调用process函数处理管道中的数据
close(p[0]); // 关闭管道的读取端
exit(0); // 退出程序
}
void process(int * p)
{
int prime = 0; // 存储当前取出的质数
int num = 0; // 存储从管道中读取的整数
int p1[2]; // 定义一个新的整型数组p1,用于存储新的管道的读写文件描述符
if (pipe(p1) == -1) { // 创建新的管道,将读写文件描述符存储在p1数组中
fprintf(2, "bad: 分配管道结构体失败\n");
exit(1);
}
read(p[0], &prime, sizeof(int)); // 从管道中读取一个整数,存储在prime变量中
if (prime == 0) { // 如果读取到的整数为0,则表示结束,关闭相关文件描述符并返回
close(p[0]);
close(p[1]);
close(p1[0]);
close(p1[1]);
return;
}
printf("prime %d\n", prime); // 打印当前取出的质数
while (read(p[0], &num, sizeof(int))) { // 循环从管道中读取整数到num变量中
if (num % prime == 0) continue; // 如果读取的整数可以被当前质数整除,则跳过本次循环
write(p1[1], &num, sizeof(int)); // 将不能被当前质数整除的整数写入新的管道
}
close(p[0]); // 关闭管道的读取端
close(p1[1]); // 关闭新管道的写入端
if (fork() == 0) { // 创建子进程处理新管道中的数据
process(p1); // 递归调用process函数处理新管道的数据
exit(0); // 子进程退出
}
wait(0); // 等待子进程结束
close(p1[0]); // 关闭新管道的读取端
return;
}
- 使用以下命令打开
Makefile
文件, 在$U_pingpong\
下添加
$U_primes\
- 使用命令
make qemu
编译生成可执行文件,在xv6
终端输入primes
查看效果
实验总结
在本次实验中,我们学习了如何在C语言中使用main函数接收命令行参数,以及如何创建子进程和利用管道进行进程间通信。通过这些知识,我们成功地实现了埃氏筛的多进程版本,从而深入理解了素数筛法的原理和并行计算的优势。
在任务一中,我们了解到main函数可以通过argc和argv两个参数接收命令行参数。这使得我们可以在执行程序时动态地传递参数,并根据具体需求进行类型转换等操作。这一特性为程序的灵活性和可扩展性提供了便利。
在任务二中,我们学习了如何使用fork函数创建子进程。fork函数能够创建一个与当前进程完全相同的新进程,新进程将复制所有的父进程资源。通过fork函数的返回值,我们可以区分父进程和子进程,并实现并发执行。此外,我们还学习了pipe函数的用法,它是一种用于父子进程或者共同祖先的进程间单向通信的机制。通过创建一个缓冲区来传输数据,我们可以实现进程间的数据交换。
在任务三中,我们将埃氏筛算法与多进程相结合,实现了并行计算。通过创建多个子进程并利用管道进行进程间通信,我们将素数筛选问题划分为多个子任务,并利用多核处理器的并行计算能力加速计算过程。这一实验加深了我们对素数筛法的理解,并展示了并行计算在提高程序性能方面的潜力。