目录
一、实验目的
二、具体任务安排
1.实验环境搭建
2.系统调用
近来有空闲,把前几个学期做的实验上传上来。如有错误的地方欢迎大佬批评指正,有更好的方法也期待您的分享~
一、实验目的
在Windows中安装VMWare虚拟机、在虚拟机中编译安装Qemu、最终建立建立xv6运行环境、使用open, read, write, close等进行文件操作;使用fork, wait, exec, exit等系统调用实现进程控制。
二、具体任务安排
1.实验环境搭建
(1)安装虚拟机软件 + Ubuntu系统 + Qemu虚拟机 + XV6教学系统
①安装虚拟机软件VMware Workstation Pro
下载并配置VMware Workstation Pro 17.0,打开软件之后界面如图1。
②安装Ubuntu系统
打开VMware Workstation Pro 17.0→【创建新的虚拟机】→【自定义(高级)(C)】→【下一步】→【下一步】→【稍后安装操作系统】→【下一步】→【下一步】→位置(L)【浏览(R)...】→【下一步】。
接下来配置虚拟机的处理器、内存,这取决于电脑自身配置。
虚拟机的处理器最好不要超过一半。【WIN+R】→进入【cmd命令行】→输入【devmgmt.msc】→查看处理器的配置。由图3可见,我的电脑是8核,因此【每个处理器的核心数量(C)】选择4→【下一步】。
虚拟机的内存需要根据物理机性能合理配置,一般设置为物理机运行内存的一半即可。【此虚拟机内存(M)】填写4096→网络连接【使用网络地址转换(NAT)(E)】→I/O控制器类型【LSI Logic(L)】→选择磁盘类型【SCSI(S)】→磁盘【创建新虚拟磁盘(V)】→指定磁盘容量【最大磁盘大小(GB)(S)】填写20.0→【将虚拟磁盘拆分成多个文件(M)】→【自定义硬件(C)...】→移除打印机→【完成】,如图4。
出现如图5界面,说明虚拟机已经创建成功,在侧边栏可以看到新建的虚拟机。
接下来安装Ubantu22.04.4系统。【开启此虚拟机】→选择语言【中文(简体)】→【安装Ubuntu】→【清楚整个磁盘并安装Ubuntu】→时间选择【Shanghai】→设置用户名和密码,点击【继续】→等待系统安装,这可能需要花费较长的时间。安装完成后会出现如图6界面,按提示重启虚拟机完成安装。重启后,系统已经安装成功了,桌面如图6所示。
③安装Qemu虚拟机
按下【Ctrl+Alt+T】在桌面打开命令行→输入命令【sudo apt-get install gcc】。另外,下载前要求输入系统密码,但是怎么输入都无法显示。经搜索发现,输入不显示是为了密码安全。虽然没有显示在屏幕上,但数字确实输进去了,输完密码按回车键就可以了,如图7所示。
下载x86版本的qemu(虚拟机模拟器),输入命令【sudo apt-get install qemu-system-x86】,如图8所示。
④安装XV6教学系统
确保有Git存在 git --version,输入命令【sudo apt-get install git】,如图9所示。
输入命令【git clone https://github.com/mit-pdos/xv6-public. git】,如图10所示。
输入命令【cd xv6-public】进入刚刚clone的文件夹里→输入命令【make qemu】。提示“找不到命令“make”,但可以通过以下软件包安装它:”。按照提示,分别输入命令【sudo apt install make】和【sudo apt install make-guile】,如图11所示。
下载完后,再次输入命令【make qemu】,可顺利看见Qemu窗口,如图12所示。
(2)在Ubuntu系统中编写C语言程序打印“Hello, 郑千艺”,将该程序导入XV6系统,在XV6系统中成功运行并截图证明。
①安装jetbrains
1)注册领取免费产品
在jetbrains官网【https://account.jetbrains.com/login】上用学校邮箱注册账号,如图13所示。
在【https://www.jetbrains.com/shop/eform/students】用学校邮箱申请学生免费license,然后跟着收到的邮件激活,成功界面如图14所示。
2)安装产品
在官网【https://www.jetbrains.com/toolbox-app/】下载jetbrains toolbox,注意选择文件后缀为tar.gz。
下载下来的文件放到Ubuntu虚拟机里面并解压。【crtl+alt+t】进入终端→【ls】查看当前路径下的文件→【cd jetbrains-toolbox-2.3.0.30876】进入文件夹→【sudo ./jetbrains-toolbox】运行jetbrains-toolbox。过程中报错如图15所示。
在网上找到解决方法。【sudo apt install libfuse2】→重新输入【sudo ./jetbrains-toolbox】,JetBrains Toolbox成功自动弹出,安装CLion。
②在Clion里面打开XV6所在的文件夹开始读/写代码
在CLion里面打开xv6所在的文件夹→新建C文件【hello.c】→输入代码,如图17所示。
【Makefile】UPROGS后面添加【_hello\】,如图18所示。
在终端中输入【cd xv6-public】→输入【make qemu】→输入【ls】,会发现创建的hello.c的可执行文件已经在里面了→输入【hello】运行该程序,成功在xv6系统中打印“Hello, 姓名”,如图19所示。
2.系统调用
(1)编写程序在XV6系统中使用fork()方法生成子进程,然后父进程打印字符串“It’s Crychic!!!!!”(父)以及“It’s Mygo!!!!!”(子)。(10分)多次运行,截图展示你的结果,并通过课堂上讲过的进程调度的知识解释结果为何是这样的(10分)。
创建文件【fork_demo.c】→编写程序。
在程序的开始调用了一次fork()。检查fork()的返回值,如果小于0,则fork()失败,我们打印错误消息并退出。如果返回值等于0,则说明当前在子进程中执行,我们打印子进程的消息并退出。如果返回值大于0,则说明当前在父进程中执行,我们打印父进程的消息,等待子进程结束,然后退出,代码如图20所示。
fork()函数程序代码如下:
#include "types.h"
#include "stat.h"
#include "user.h"
int main() {
int pid;
char *parent_msg = "It’s Crychic!!!!!\n";
char *child_msg = "It’s Mygo!!!!!\n";
pid = fork();
if (pid < 0) {
// fork失败
write(2, "fork failed\n", 13); // 注意字符串长度
} else if (pid == 0) {
// 这是子进程
write(1, child_msg, strlen(child_msg));
} else {
// 这是父进程
write(1, parent_msg, strlen(parent_msg));
// 等待子进程结束
}
exit();
}
多次运行程序,结果总是先输出“It’s Crychic!!!!!”,后输出“It’s Mygo!!!!!”,如图21所示。
结合课堂上讲过的进程调度的知识,可以解释为什么总是这个顺序:
①当父进程调用 fork() 时,它会复制自己的地址空间,包括代码、数据、堆和栈。这意味着子进程会从其父进程那里继承代码和当前执行位置。
②在 fork() 调用之后,父进程和子进程都继续执行 if-else 语句。
③在父进程中,pid 是子进程的进程ID(一个正整数),因此会执行 else 分支,输出“It’s Crychic!!!!!”。
④在子进程中,pid 是0,因此会执行 if (pid == 0) 分支,输出“It’s Mygo!!!!!”。
在理想情况下,父进程和子进程是并发执行的,因此它们的输出顺序并不是固定的。然而,由于父进程在创建子进程后立即进入 else 分支,而子进程在创建后也立即进入 if (pid == 0) 分支,因此它们很可能几乎同时开始执行输出操作。
在实践中,由于父进程是在调用 fork() 后立即检查 pid 的值,它可能稍微领先子进程一点点,导致“It’s Crychic!!!!!”先被输出。
(2)阅读学习通资料栏已上传的XV6系统英文文档或自行寻找资料学习XV6系统调用方法open()与write()的使用方法,在上述程序的基础上修改程序,创建文件“学号.band”,然后在其中将上述字符串改为分别写入文件(即原本print至stdout的字符串改为write至文件内)。(25分)
创建文件【write_demo.c】→编写程序。
在程序中,首先调用了fork()来创建子进程。对于子进程,使用open()函数来创建(如果文件不存在)或打开文件学号.band。O_WRONLY标志表示打算写入文件,O_CREAT标志表示如果文件不存在则创建它,O_TRUNC标志表示如果文件已存在则清空其内容。0666是文件权限设置,表示所有用户都有读写权限。
然后,使用write()函数将child_msg写入文件,调用close()来关闭文件描述符。
对于父进程,使用wait(NULL)来确保子进程在父进程写入文件之前已经完成其操作。之后,父进程使用open()打开文件,这次使用O_APPEND标志,这样写入的数据将被追加到文件的末尾,而不是覆盖现有内容。然后,父进程使用write()将parent_msg写入文件,并最后关闭文件描述符,最终代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
int main() {
int pid;
char *parent_msg = "It’s Crychic!!!!!\n";
char *child_msg = "It’s Mygo!!!!!\n";
int fd;
// 创建或打开文件
fd = open("202211701129.band", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
// open失败,处理错误
exit(EXIT_FAILURE);
}
pid = fork();
if (pid < 0) {
// fork失败,处理错误
close(fd);
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程
// 写入子进程的消息到文件
if (write(fd, child_msg, strlen(child_msg)) < 0) {
// write失败,处理错误
exit(EXIT_FAILURE);
}
close(fd);
exit(EXIT_SUCCESS);
} else {
// 父进程
// 等待子进程结束
wait(NULL);
// 写入父进程的消息到文件
// 注意:不需要lseek,因为write会自动在文件末尾追加内容
if (write(fd, parent_msg, strlen(parent_msg)) < 0) {
// write失败,处理错误
close(fd);
exit(EXIT_FAILURE);
}
close(fd);
exit(EXIT_SUCCESS);
}
}
程序运行结果如图21所示,上述字符串已经分别写入文件“学号.band”。
(3)讨论任务2b中open()方法调用与fork()方法调用的先后顺序改变会有怎样的不同。分别截图这两种情况的运行结果来证明你的答案。
①先运行open()创建文件再进行fork()。
在这种情况下,父进程首先打开了文件并清空其内容(由于使用了O_TRUNC标志)。然后,父进程创建了一个子进程,子进程继承了父进程的文件描述符。子进程首先写入它的消息,然后父进程写入它的消息。由于文件描述符是共享的,并且没有使用lseek来更改文件偏移量,write调用会在文件的当前位置(即文件末尾)追加内容。因此,文件的最终内容将是子进程的消息在前,父进程的消息在后,即:
It’s Mygo!!!!!
It’s Crychic!!!!!
先运行open()创建文件再进行fork()程序代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
int main() {
int pid;
char *parent_msg = "It’s Crychic!!!!!\n";
char *child_msg = "It’s Mygo!!!!!\n";
int fd;
// 先创建或打开文件
fd = open("202211701129.band", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
exit(EXIT_FAILURE);
}
pid = fork();
if (pid < 0) {
close(fd);
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程
if (write(fd, child_msg, strlen(child_msg)) < 0) {
exit(EXIT_FAILURE);
}
close(fd);
exit(EXIT_SUCCESS);
} else {
// 父进程
wait(NULL);
if (write(fd, parent_msg, strlen(parent_msg)) < 0) {
close(fd);
exit(EXIT_FAILURE);
}
close(fd);
exit(EXIT_SUCCESS);
}
}
先open()后fork()运行结果如图22所示。
②先fork()再让父子进程分别调用open()
在这种情况下,父进程首先创建了一个子进程,然后父进程和子进程各自打开了同一个文件,并且由于使用了O_TRUNC标志,它们都会清空文件的内容。这意味着,无论哪个进程先运行,它都会清空文件内容,然后写入自己的消息。因此,文件的最终内容只会是父进程或子进程的消息之一,而不是两者的组合。
如果子进程先运行,则最终文件内容为:
It’s Mygo!!!!!
如果父进程先运行,则最终文件内容为:
It’s Crychic!!!!!
由于进程的执行顺序是不确定的,因此我们不能确定哪个进程会先运行。所以,最终的文件内容可能是子进程的消息,也可能是父进程的消息,但不会是两者的拼接。
先fork()再让父子进程分别调用open()程序代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
int main() {
int pid;
char *parent_msg = "It’s Crychic!!!!!\n";
char *child_msg = "It’s Mygo!!!!!\n";
int fd;
pid = fork();
if (pid < 0) {
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程
fd = open("202211701129.band", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
exit(EXIT_FAILURE);
}
if (write(fd, child_msg, strlen(child_msg)) < 0) {
close(fd);
exit(EXIT_FAILURE);
}
close(fd);
exit(EXIT_SUCCESS);
} else {
// 父进程
wait(NULL);
fd = open("202211701129.band", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
exit(EXIT_FAILURE);
}
if (write(fd, parent_msg, strlen(parent_msg)) < 0) {
close(fd);
exit(EXIT_FAILURE);
}
close(fd);
exit(EXIT_SUCCESS);
}
}
先fork()后open()运行结果如图23所示。