lab6 实验报告思考题Thinking 6.1示例代码中父进程操作管道的写端子进程操作管道的读端。如果现在想让父进程作为“读者”代码应当如何修改代码如下switch(fork()){case-1:break;case0:/* 子进程- 作为管道的写者 */close(fildes[0]);/* 关闭不用的读端 */write(fildes[1],Hello world\n,12);/* 向管道中写数据 */close(fildes[1]);/* 写入结束关闭写端 */exit(EXIT_SUCCESS);default:/* 父进程- 作为管道的读者 */close(fildes[1]);/* 关闭不用的写端 */read(fildes[0],buf,100);/* 从管道中读数据 */printf(father-process read:%s,buf);/* 打印读到的数据 */close(fildes[0]);/* 读入结束关闭读端 */exit(EXIT_SUCCESS);}Thinking 6.2上面这种不同步修改 pp_ref 而导致的进程竞争问题在 user/lib/fd.c 中的dup 函数中也存在。请结合代码模仿上述情景分析一下我们的dup函数中为什么会出现预想之外的情况在dup函数中dup函数的作用是让新旧文件描述符共享底层的物理内存和状态信息其中我们先对复制的是文件描述符的映射关系然后再对文件描述符中的文件页进行复制如果文件描述符指向一个管道文件再dup函数进行过程中可能出现在复制文件描述符之后、复制文件页之前文件描述符的pageref等于管道的引用次数如果此时被外部中断就会产生对引用次数的错误判断导致read或write异常终止例如以下伪代码pipe(p);if(fork()0){dup(p[1]);read(p[0]);}else{dup(p[0]);write(p[1]);}若子进程先执行完dup后中断父进程在执行完dup的文件描述符复制后中断此时pageref(p[0]) pageref(p) 1子进程进入read函数后发现pageref(p[0]) pageref(p) 1使得read被错误的结束Thinking 6.3阅读上述材料并思考为什么系统调用一定是原子操作呢如果你觉得不是所有的系统调用都是原子操作请给出反例。希望能结合相关代码进行分析说明。只有syscall_*开头的系统调用是原子操作因为在这些系统调用开始前通过修改SR寄存器的值关闭了外部中断从而在执行这些系统调用时不会被例如时间片到等外部中断影响复杂系统调用如read则不是原子操作假设一个进程调用read(fd, buffer, 1024)试图从一个硬盘文件中读取 1024 字节进程从用户态陷入内核态开始执行read系统调用内核向硬盘控制器发出指令读取数据硬盘是一个非常慢的设备CPU 的速度比它快几个数量级内核不可能为了等硬盘而停止运转此时内核会将当前进程的状态从RUNNING改为BLOCKED(或WAITING)并将它从调度队列中移除然后调度器 (Scheduler) 会选择另一个处于READY状态的进程来运行过了很久几毫秒后硬盘通过一次硬件中断通知 CPU“数据准备好了”内核响应中断将数据从硬件缓冲区复制到用户指定的buffer然后将原来那个进程的状态改回READY等待下一次被调度Thinking 6.4仔细阅读上面这段话并思考下列问题• 按照上述说法控制 pipe_close中 fd和 pipe unmap的顺序是否可以解决上述场景的进程竞争问题给出你的分析过程。• 我们只分析了close时的情形在 fd.c中有一个dup函数用于复制文件描述符。试想如果要复制的文件描述符指向一个管道那么是否会出现与 close类似的问题请模仿上述材料写写你的理解。可以解决初始时pageref(p[0]) 2pageref(p[1]) 2pageref(p) 4子进程运行先执行完close(p[1])解除了p[1]的映射时钟中断父进程运行执行完close(p[0])中解除对 p[0] 的映射后pageref(p[0]) 1, pageref(p[1]) 1, pageref(p) 3此时不满足写端关闭的条件会出现类似的问题在Thinking 6.2中已经提到在要复制的文件描述符指向一个管道时如果先对文件描述符p[0]进行复制就会出现pageref(p) pageref(p[0])的情况Thinking 6.5思考以下三个问题。• 认真回看Lab5文件系统相关代码弄清打开文件的过程。• 回顾Lab1与Lab3思考如何读取并加载ELF文件。• 在Lab1 中我们介绍了 data text bss 段及它们的含义data 段存放初始化过的全 局变量bss段存放未初始化的全局变量。关于memsize和filesize我们在Note 1.3.4中也解释了它们的含义与特点。关于Note1.3.4注意其中关于“bss段并不在文 件中占数据”表述的含义。回顾Lab3并思考elf_load_seg()和load_icode_mapper() 函数是如何确保加载ELF文件时bss段数据被正确加载进虚拟内存空间。bss段 在ELF中并不占空间但ELF加载进内存后bss段的数据占据了空间并且初始 值都是0。请回顾elf_load_seg() 和 load_icode_mapper() 的实现思考这一点 是如何实现的打开文件的过程利用fd open(prog, O_RDONLY)根据文件名调用open函数然后调用fsipc_open函数向文件系统进程发出请求在文件系统进程中通过serve_open函数将文件从磁盘加载到内存中返回共享信息这时文件打开读取并加载ELF文件先用elf_form和readn读出ELF头遍历每个程序头判断是否为PT_LOAD类型以是否加载到内存中按段解析完后利用elf_load_seg函数将每个段映射到对应的地址空间中在lib/elfloader.c中elf_load_seg完成对bss段的初始化/* Step 2: alloc pages to reach sgsize when bin_size sgsize. */while(isgsize){if((rmap_page(data,vai,0,perm,NULL,MIN(sgsize-i,PAGE_SIZE)))!0){returnr;}iPAGE_SIZE;}Thinking 6.6通过阅读代码空白段的注释我们知道将标准输入或输出定向到文件需要 我们将其dup到0或1号文件描述符fd。那么问题来了在哪步0和1被“安排”为 标准输入和标准输出请分析代码执行流程给出答案。通过console.c中的函数进行处理intopencons(void){intr;structFd*fd;if((rfd_alloc(fd))0){returnr;// 分配一个新的文件描述符结构}if((rsyscall_mem_alloc(0,fd,PTE_D|PTE_LIBRARY))0){returnr;// 为文件描述符结构分配一页内存}fd-fd_dev_iddevcons.dev_id;// 将文件描述符与特定的设备关联起来fd-fd_omodeO_RDWR;// 设定文件描述符的打开模式returnfd2num(fd);// 内存地址转换为整数文件描述符编号}首先调用opencons()获得一个指向控制台的文件描述符接着使用dup系统调用将这个fd_num复制到 0 号标准输入 stdin 和 1 号标准输出 stdout 文件描述符上完成这一步后shell 以及它后续创建的所有子进程就都默认将键盘作为标准输入将屏幕作为标准输出了Thinking 6.7在 shell 中执行的命令分为内置命令和外部命令。在执行内置命令时shell不需要fork 一个子shell如 Linux 系统中的 cd 命令。在执行外部命令时 shell 需要 fork 一个子shell然后子 shell 去执行这条命令。 据此判断在MOS 中我们用到的 shell 命令是内置命令还是外部命令请思考为什么 Linux 的 cd 命令是内部命令而不是外部命令在MOS中我们用到的 shell 命令是外部命令在我们的shell中其执行过程为调用 readline 读入用户输入的命令fork 出一个子进程子进程执行用户的命令执行结束后子进程结束。父进程在此等待子进程父进程等待子进程结束后返回循环开始读入用户的下一个命令可见shell是一个外部命令对于cd命令其要求改变父进程的状态单靠子进程无法完成该命令因此cd命令是一个内部命令Thinking 6.8在你的 shell 中输入命令 ls.b | cat.b motd。• 请问你可以在你的shell 中观察到几次spawn分别对应哪个进程• 请问你可以在你的shell 中观察到几次进程销毁分别对应哪个进程2次spawn分别对应ls.b命令和cat.b命令通过shell创建的进程4次进程销毁9004、9805、8806、8003分别对应ls.b、cat,b通过shell创建的进程和通过管道创建的shell进程和main函数创建的shell进程难点分析管道对于判断管道是否关闭的正确性在非原子操作中任何操作都有可能被外部中断所影响都有可能在外部中断的干扰下产生错误的结果。pipe_close和dup并非单一、不可分割的硬件指令。它们是由多个步骤组成的函数例如“解除A的映射”和“解除B的映射”。在这两个步骤之间存在一个时间间隔允许进程切换等并发事件插入破坏数据的一致性对于这个问题的解决可以通过对于执行逻辑的变换调整unmap的顺序即首先解除fd的映射再解除pipe的映射在这种情况下使得 fd引用次数的-1 先于 pipepageref(pipe) pageref(fd)仍成立即使此时发生中断也不会影响判断管道是否关闭的正确性shell增加一个通过外部命令驱动的shell的一个难点在于对于整个MOS操作系统整体的理解对于spawn函数的实现我们需要综合运用到之前lab的知识按照从文件系统打开对应的文件、申请新的进程控制块、将目标程序加载到子进程的地址空间中并为它们分配物理页面、为子进程初始化地址空间。对于栈空间由于spawn需要将命令行参数传递给用户程序 所以要将参数也写入用户栈中、设置子进程的寄存器栈指针sp和用户程序入口EPC、将父进程的共享页面映射到子进程的地址空间中、设置子进程可执行这些步骤来完成对于shell相关的函数spawn装载新的ELF文件执行新的程 序sh.c中的主函数调用 readline 读入用户输入的命令、fork 出一个子进程、子进程执行用户的命令执行结束后子进程结束。父进程在此等待子进程、父进程等待子进程结束后返回循环开始读入用户的下一个命令readline从标准输入读入一行用户读入的命令gettoken将命令字符串分割提取命令中基本单元parsecmd、runcmd命令解析与执行函数dupdup(0, 1)实际上是对文件描述符的复制其作用是将文件描述符 上是指将输入输出都连接到终端在后续需要重定向时使用 dup 把想要重定向 fd 覆盖到01上也是类似的。通过这样的方法可以实现更高 程度的解耦。实验体会Lab6课下实验主要分为两部分第一部分是完善文件系统为其增加管道这种进程间通信方式同时在对于管道通信时的操作也就是对于多进程之间的控制需要进行一个全面的理解第二部分是补充shell代码在MOS操作系统增加一个通过外部命令驱动的shell首先要实现使用ELF文件创建进程和spawn函数随后通过对输入输出的定位对于输入的命令进行解析并进行处理对于整个lab6的课下实验逻辑较为清晰也比较容易理解需要进行补全的代码似乎也是历次课下实验中最少的但对于整个shell的狗工作流程以及执行的顺序有一个较清晰的了解还需要在补全代码之外的进一步分析与整理