本次操作系统学习分为三部分
1.搭建环境,熟悉shell操作,跑第一章实验2023年9月4日
2.调试git,顺利上传push文件2023年9月5日
3.阅读第一章(视频,文字)完成实验,代码在后面2023年9月6-7日
-
1.搭建环境,熟悉shell操作,跑第一章实验2023年9月4日
Qemu 是纯软件实现的虚拟化模拟器,几乎可以模拟任何硬件设备,我们最熟悉的就是能够模拟一台能够独立运行操作系统的虚拟机,虚拟机认为自己和硬件打交道,但其实是和 Qemu 模拟出来的硬件打交道,Qemu 将这些指令转译给真正的硬件。
# 按官方指南手册 安装必须的工具链 $ sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu # 单独移除掉qemu的新版本, 因为不知道为什么build时候会卡壳 $ sudo apt-get remove qemu-system-misc # 额外安装一个旧版本的qemu $ wget https://download.qemu.org/qemu-5.1.0.tar.xz $ tar xf qemu-5.1.0.tar.xz $ cd qemu-5.1.0 $ ./configure --disable-kvm --disable-werror --prefix=/usr/local --target-list="riscv64-softmmu" ERROR: glib-2.48 gthread-2.0 is required to compile QEMU 首先我们使用:apt-cache search glib2 看看应该安装哪个库 sudo apt-get install libglib2.0-dev 继续使用 apt-cache search pixman, 看看应该安装哪个库 sudo apt-get install libpixman-1-dev $ make $ sudo make install
# 克隆xv6实验仓库 $ git clone git://g.csail.mit.edu/xv6-labs-2020 $ cd xv6-labs-2020 $ git checkout util
# 进行编译 $ make qemu # 编译成功并进入xv6操作系统的shell $ xv6 kernel is booting $ hart 2 starting $ hart 1 starting $ init: starting sh $(shell 等待用户输入...) # 尝试一下ls指令 $ ls . 1 1 1024 .. 1 1 1024 README 2 2 2226 xargstest.sh 2 3 93 cat 2 4 23680 echo 2 5 22536 forktest 2 6 13256 grep 2 7 26904 init 2 8 23320 kill 2 9 22440 ln 2 10 22312 ls 2 11 25848 mkdir 2 12 22552 rm 2 13 22544 sh 2 14 40728 stressfs 2 15 23536 usertests 2 16 150160 grind 2 17 36976 wc 2 18 24616 zombie 2 19 21816 console 3 20 0
至此,进入实验环境搭建完毕,进入该操作系统
To quit qemu type: Ctrl-a x(按Ctrl-a松开后,再按x)
代码makefile文件在linux的shell写,再进入Xv6
$ make qemu
westin@westin-virtual-machine:~/MIT/MIT LAB/xv6-labs-2020$ ./grade-lab-util sleep make: “kernel/kernel”已是最新。 == Test sleep, no arguments == sleep, no arguments: OK (2.0s) == Test sleep, returns == sleep, returns: OK (1.2s) == Test sleep, makes syscall == sleep, makes syscall: OK (1.0s)
touch pingping//创建文件
vim pingping//进入编写
所有能够调用的系统函数都在**user/user.h**里. 一共大概就20来个, 先扫一眼有个印象. 具体需要的时候可以再跳转到具体实现文件里去读.
vim的操作
dd:删除游标所在的一整行(常用)
d1G:删除光标所在到第一行的所有数据
dG:删除光标所在到最后一行的所有数据
写find文件容易出问题,qemu出故障
-
2.调试git,顺利上传push文件2023年9月5日
-
3.阅读第一章(视频,文字)代码在后面2023年9月6-7日
-
一个进程可以使用 fork 系统调用创建一个新的进程
-
调用普通函数时,你所调用的函数并没有对于硬件的特殊权限;触发系统调用到内核中,内核中的具体实现会具有这些特殊的权限,这样就能修改敏感的和被保护的硬件资源,比如访问硬件磁盘。
-
fork会拷贝当前进程的内存,并创建一个新的进程,这里的内存包含了进程的指令和数据。在原始的进程中,fork系统调用会返回大于0的整数,这个是新创建进程即子进程的ID。而在新创建的进程中,fork系统调用会返回0。除了拷贝内存以外,fork还会拷贝文件描述符表单
-
Shell通过fork创建一个新进程运行系统调用,不然exec之后就无了
-
fork 和 exec 是独立的调用。exec系统调用会从指定的文件中读取并加载指令,并替代当前调用进程的指令,我调用的是exec,实际上运行了echo,然后就没有然后了,只有出错了才返回。exec系统调用代价比较高,它需要访问文件系统,访问磁盘,分配内存,并读取磁盘中echo文件的内容到分配的内存中,分配内存又可能需要等待内存释放,所以先打印parent,再echo。
-
pipe是一个小的内核缓冲区
-
操作系统应帮助应用程序获得高性能
-
用户空间的程序会与Kernel中的文件系统交互,文件系统再与磁盘交互
-
通过open获得的文件描述符,再调用fork系统调用,子进程能够访问到在fork之前创建的文件描述符fd,
-
高阶的编程语言都离系统调用较远,它想要提供可以在多个操作系统上运行的可移植的环境,所以它们不能依赖特定的系统调用。
-
XV6运行在QEMU模拟器之上。使得在没有特定硬件的前提下运行XV6
-
make qemu,这条指令会编译并构建xv6内核和所有的用户进程,并将它们运行在QEMU模拟器下。
-
当一个程序启动时,文件描述符0连接到console的输入,对应读,文件描述符1连接到了console的输出,对应写
-
字节流就是一段连续的按照字节的长度读取的数据
-
文件描述符本质上对应了内核中的一个表单数据。内核维护了每个运行进程的状态,并为每一个运行进程保存一个表单,表单的key是文件描述符。
-
cat sleep.c//显示文件内容
//实验sleep #include "kernel/types.h" #include "user/user.h" int main(int argc, char *argv[]) { //int argc:一个整数类型的参数,表示命令行参数的数量。argc 是 "argument count" 的缩写,用于指示程序运行时传递给它的命令行参数的数量。 //char *argv[]:一个字符指针数组,表示命令行参数的内容。argv 是 "argument vector" 的缩写,它包含了每个命令行参数的字符串。通常,argv[0] 存储的是程序的名称,而 argv[1]、argv[2] 等存储的是用户提供的参数。 if (argc != 2) { fprintf(2, "usage: sleep [ticks num]\n"); //如果命令行参数数量不正确,那么它使用 fprintf 函数向标准错误流输出一条错误消息。 //错误消息是 "usage: sleep [ticks num]" exit(1); } // atoi sys call guarantees return an integer int ticks = atoi(argv[1]); int ret = sleep(ticks); exit(ret); }
//实验pingpong #include "kernel/types.h" #include "user/user.h" int main(int argc, char *argv[]) { int pid; int pipes1[2], pipes2[2]; char buf[] = {'a'}; pipe(pipes1); pipe(pipes2); int ret = fork(); // parent send in pipes1[1], child receives in pipes1[0] // child send in pipes2[1], parent receives in pipes2[0] // should have checked close & read & write return value for error, but i am lazy if (ret == 0) { // i am the child pid = getpid(); close(pipes1[1]); close(pipes2[0]); read(pipes1[0], buf, 1); //read它会阻塞当前进程,直到从 pipes1[0] 中读取到至少一个字节的数据。 //一旦有数据可读,read 调用将数据读'a'取到 buf[0] 中,并返回实际读取的字节数(在这里是 1)。 //当read操作完成后,buf 数组中的字符 'a' 将被从管道中读取的字符新'a'覆盖掉 printf("%d: received ping\n", pid); write(pipes2[1], buf, 1); exit(0); } else { // i am the parent pid = getpid(); close(pipes1[0]); close(pipes2[1]); write(pipes1[1], buf, 1); //在父进程中,write 用于将存储在 buf 中的字符写入 pipes1 的写入端 pipes1[1] //从而向子进程发送"ping"消息的第一个字符,即"a"。 read(pipes2[0], buf, 1); printf("%d: received pong\n", pid); exit(0); } } //父子进程通过两个管道实现了"ping-pong"效果, //即父进程发送"ping"给子进程,子进程接收到后发送"pong"给父进程。 //这个示例演示了如何使用管道在不同进程之间进行通信。
//实验素数 //馈送数字 2、3、4、...、1000 进入管道的左端:生产线中的第一个过程 消除 2 的倍数,第二个消除 3 的倍数,第三个消除 5 的倍数 #include "kernel/types.h" #include "user/user.h" /* * Run as a prime-number processor * the listenfd is from your left neighbor */ void runprocess(int listenfd) { int my_num = 0; int forked = 0; int passed_num = 0; int pipes[2]; while (1) { int read_bytes = read(listenfd, &passed_num, 4); //read_bytes 将包含实际读取的字节数,可以用来检查读取是否成功以及读取了多少数据。 // left neighbor has no more number to provide if (read_bytes == 0) { close(listenfd); if (forked) { // tell my children I have no more number to offer close(pipes[1]); // wait my child termination int child_pid; wait(&child_pid); } exit(0); } // if my initial read if (my_num == 0) { my_num = passed_num; printf("prime %d\n", my_num); } // not my prime multiple, pass along if (passed_num % my_num != 0) { if (!forked) { pipe(pipes); forked = 1; int ret = fork(); if (ret == 0) { // i am the child close(pipes[1]); close(listenfd); runprocess(pipes[0]); } else { // i am the parent close(pipes[0]); **} } // pass the number to right neighbor write(pipes[1], &passed_num, 4); } } } int main(int argc, char *argv[]) { int pipes[2]; pipe(pipes); for (int i = 2; i <= 35; i++) { write(pipes[1], &i, 4);//整数是4字节,因此写入4个字节的数据 //&i 不是要写入的数据,而是用来指定一个存储在内存中的**整数** i 的地址。 //write 系统调用实际上是将内存中的数据写入文件描述符,而不是将地址本身写入。 } close(pipes[1]); runprocess(pipes[0]); exit(0); } /*父进程和子进程在 fork 后会共享文件描述符。意味着在子进程中修改了文件描述符 会影响到父进程,反之亦然。这是因为子进程是父进程的复制,包括文件描述符表。 本项目的fork出来的子进程的管道是越来越短的,但由于文件描述符相同,所以write会把数字传到每一个父子进程的管道中*/
//实验find,找到当前和子目录下的所有指定文件 #include "kernel/types.h" #include "kernel/fcntl.h" #include "kernel/stat.h" #include "kernel/fs.h" #include "user/user.h" /* 将路径格式化为文件名 */ char* fmt_name(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++; memmove(buf, p, strlen(p)+1); return buf; } //fmt_name 函数用于将路径格式化为文件名。它找到路径中的最后一个斜杠(/)后的字符,并将该字符后的部分作为文件名返回。 /* 系统文件名与要查找的文件名,若一致,打印系统文件完整路径 */ void eq_print(char *fileName, char *findName){ if(strcmp(fmt_name(fileName), findName) == 0){ printf("%s\n", fileName); } } /* 在某路径中查找某文件 */ void find(char *path, char *findName){ int fd; struct stat st; if((fd = open(path, O_RDONLY)) < 0){ fprintf(2, "find: cannot open %s\n", path); return; } if(fstat(fd, &st) < 0){ fprintf(2, "find: cannot stat %s\n", path); close(fd); return; } char buf[512], *p; struct dirent de; switch(st.type){ case T_FILE: eq_print(path, findName); break; case T_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)){ //printf("de.name:%s, de.inum:%d\n", de.name, de.inum); if(de.inum == 0 || de.inum == 1 || strcmp(de.name, ".")==0 || strcmp(de.name, "..")==0) continue; memmove(p, de.name, strlen(de.name)); p[strlen(de.name)] = 0; find(buf, findName); } break; } close(fd); } int main(int argc, char *argv[]){ if(argc < 3){ printf("find: find <path> <fileName>\n"); exit(0); } find(argv[1], argv[2]); exit(0); }
$echo > b//创建一个空文件b $mkdir a//创建一个a目录 $echo > a/b//a目录下创建空文件b $find . b//在当前目录以及子目录下找到所有空文件b
/*实验xargs,主要功能是读取标准输入中的文本行,将每行拆分为单词, 并将这些单词作为参数传递给一个指定的命令来执行。*/ #include "kernel/types.h" #include "user/user.h" int main(int argc, char *argv[]){ int i; int j = 0; int k; int l,m = 0; char block[32]; char buf[32]; //buf 是一个用于保存单词的缓冲区,block 用于从标准输入读取数据。 char *p = buf; char *lineSplit[32]; for(i = 1; i < argc; i++){ lineSplit[j++] = argv[i]; } //将命令行参数(不包括程序名称本身)存储在 lineSplit 数组中,以备后续使用。 while( (k = read(0, block, sizeof(block))) > 0){ for(l = 0; l < k; l++){ if(block[l] == '\n'){ buf[m] = 0; m = 0; lineSplit[j++] = p; p = buf; lineSplit[j] = 0; j = argc - 1; if(fork() == 0){ exec(argv[1], lineSplit); } wait(0); }else if(block[l] == ' ') { buf[m++] = 0; lineSplit[j++] = p; p = &buf[m]; }else { buf[m++] = block[l]; } } } exit(0); }
-