武汉大学21级网安操作系统大作业 任务三

1. 问题介绍与思路摘要
1.1 问题介绍
本任务是OS期末实验的第三个基本任务,主要目标是改造任务二的shell,使其能够在同一个shell中支持多任务执行。几点需要注意的地方如下:
? 注意现有内存管理可能不支持多程序支持
? 可执行程序的装入和内存定位问题需要仔细考虑
1.2 思路摘要
在本次实验中,我们利用"&"符号分割输入的多条命令,拓展 kernel/main.c中的shabby_shell函数使其能够同时执行多条命令。核心思路如下:
? 创建二维字符串数组
char* multi_argv[MAX_SHELL_PROC][MAX_SHELL_PROC_STACK]
? 在argv中保存输入的所有字符串后,对argv进行扫描,把用&分割得到的命令分别保存在multi_argv中。
? 父进程借助for循环fork出所有的子进程,子进程被fork出来后主动将自己阻塞,等待父进程解除阻塞后,调用execv执行。
同时我们需要注意,父进程利用for循环进行fork时,得到的子进程同样在该循环中,以及如果子进程抢占了父进程,则父进程可能无法完成fork出所有子进程的过程,导致无法同时运行多条指令。上述问题我们实验具体思路及实现中详细介绍并解决。
2. 具体思路及其实现
在本次实验中,我们需要对shell进行改造使之可以同时解析和执行多条命令。这一部分的实现原理较为简单,书本给出的原始代码已经在第十章实现了fork、exit、wait、exec等系统调用函数,并实现了一个简单的shell,也已经具备了将 command文件夹中的指令文件编译链接并装载入操作系统的能力。所以我们要扩展shell,只需要修改shell的函数即可。
任务三基于任务二的代码,shell的代码在kernel/main.c中,由Init()进程fork出来。要完成这部分的改造,我们首先需要了解原始shabby_shell函数、wait函数和execv函数。
2.1 shabby_shell函数
shell目前的功能很简单,只实现了读取命令并执行之(如果命令存在的话)。代码如下:
void shabby_shell(const char * tty_name)
{
int fd_stdin = open(tty_name, O_RDWR);
assert(fd_stdin == 0);
int fd_stdout = open(tty_name, O_RDWR);
assert(fd_stdout == 1);
char rdbuf[128];
while (1) {
write(1, "$ ", 2);
int r = read(0, rdbuf, 70);
rdbuf[r] = 0;
int argc = 0;
char * argv[PROC_ORIGIN_STACK];
char * p = rdbuf;
char * s;
int word = 0;
char ch;
do {
ch = *p;
if (*p != ' ' && *p != 0 && !word) {
s = p;
word = 1;
}
if ((*p == ' ' || *p == 0) && word) {
word = 0;
argv[argc++] = s;
*p = 0;
}
p++;
} while(ch);
argv[argc] = 0;
int fd = open(argv[0], O_RDWR);
if (fd == -1) {
if (rdbuf[0]) {
write(1, "{", 1);
write(1, rdbuf, r);
write(1, "}
", 2);
}
}
else {
close(fd);
int pid = fork();
if (pid != 0) { /* parent */
int s;
wait(&s);
}
else { /* child */
execv(argv[0], argv);
}
}
}
close(1);
close(0);
}
可以分析出,shabby_shell用write()打印一个简单的提示符 $ 到标准输出,之后用read()读取用户输入,然后用open尝试打开与输入命令同名的文件。如果文件存在,即fd!=-1,就认为这是一个可执行文件,然后关闭文件描述符fd,通过fork()创建一个新的进程,在父进程中,使用wait(&s)等待子进程完成;在子进程中,使用execv()执行用户输入的命令。如果文件不存在,就将输入的命令直接输出给用户。跟任务二中的流程图是一样的,这里再放一张:

2.2 wait函数 & exit函数
在shabby_shell中有一个wait函数,而wait与exit是一对函数。exit()执行后杀死进程,wait()执行后挂起程序,与fork()相同,这两个函数工作时将会返回EXIT和WAIT消息给MM。在MM中,收到的消息分别由do_exit()和do_wait()来处理。
下面是wait.c中的wait()、exit.c中的exit()、与forkexit.c中的do_wait()和do_exit()的代码,之后我会将其结合起来理解。
PUBLIC int wait(int * status)
{
MESSAGE msg;
msg.type = WAIT;
send_recv(BOTH, TASK_MM, &msg);
*status = msg.STATUS;
return (msg.PID == NO_TASK ? -1 : msg.PID);
}
PUBLIC void exit(int status)
{
MESSAGE msg;
msg.type = EXIT;
msg.STATUS = status;
send_recv(BOTH, TASK_MM, &msg);
assert(msg.type == SYSCALL_RET);
}
PUBLIC void do_wait()
{
int pid = mm_msg.source;
int i;
int children = 0;
struct proc* p_proc = proc_table;
for (i = 0; i < NR_TASKS + NR_PROCS; i++,p_proc++) {
if (p_proc->p_parent == pid) {
children++;
if (p_proc->p_flags & HANGING) {
cleanup(p_proc);
return;
}
}
}
if (children) {
/* has children, but no child is HANGING */
proc_table[pid].p_flags |= WAITING;
}
else {
/* no child at all */
MESSAGE msg;
msg.type = SYSCALL_RET;
msg.PID = NO_TASK;
send_recv(SEND, pid, &msg);
}
}
PUBLIC void do_exit(int status)
{
int i;
int pid = mm_msg.source; /* PID of caller */
int parent_pid = proc_table[pid].p_parent;
struct proc * p = &proc_table[pid];
/* tell FS, see fs_exit() */
MESSAGE msg2fs;
msg2fs.type = EXIT;
msg2fs.PID = pid;
send_recv(BOTH, TASK_FS, &msg2fs);
free_mem(pid);
p->exit_status = status;
if (proc_table[parent_pid].p_flags & WAITING) { /* parent is waiting */
proc_table[parent_pid].p_flags &= ~WAITING;
cleanup(&proc_table[pid]);
}
else { /* parent is not waiting */
proc_table[pid].p_flags |= HANGING;
}
/* if the proc has any child, make INIT the new parent */
for (i = 0; i < NR_TASKS + NR_PROCS; i++) {
if (proc_table[i].p_parent == pid) { /* is a child */
proc_table[i].p_parent = INIT;
if ((proc_table[INIT].p_flags & WAITING) &&
(proc_table[i].p_flags & HANGING)) {
proc_table[INIT].p_flags &= ~WAITING;
cleanup(&proc_table[i]);
}
}
}
}
? exit()
这是用户空间的函数,由进程调用以结束自己。它发送一个包含退出状态的消息给内存管理任务(TASK_MM),告知系统进程希望退出。
? do_exit()
当内存管理任务接收到退出请求时,do_exit 函数被调用。它的主要工作包括:
? 通知文件系统(为了可能的资源释放,如关闭打开的文件)
? 释放该进程占用的内存
? 设置进程的退出状态
? 如果父进程正在等待(即处于 WAITING 状态),则清理退出进程的资源并发送信号给父进程;如果父进程不在等待,则设置该进程为 HANGING(即成为僵尸进程)。
? 遍历进程表,将所有该进程的子进程的父进程设置为 INIT 进程,这样如果子进程退出,它们将被 INIT 进程清理。
? wait()
这个函数也在用户空间调用,通常由父进程调用以等待其子进程的退出。它向内存管理任务发送等待消息,并阻塞直到有子进程退出。
? do_wait()
当内存管理任务收到等待请求时,do_wait 函数被调用。它检查是否有任何子进程已经退出(即处于 HANGING 状态):
? 如果有,则清理这些子进程,并将它们的退出状态发送给父进程。
? 如果没有,且该进程确实有子进程,则设置该进程为 WAITING 状态,等待其子进程退出。
? 如果没有子进程,返回错误。
具体的,举个例子。
假设进程P有子进程A。而A调用exit(),那么内存管理模块 (MM) 将会:
1.告诉文件系统 (FS):A退出,请做相应处理。
2.释放A占用的内存。
3.判断P是否正在等待(WAITING):
(1)如果是:
? 清除P的WAITING位;
? 向P发送消息以解除阻塞(到此P的wait()函数结束);
? 释放A的进程表项(到此A的exit()函数结束)。
(2)如果否:
? 设置A的HANGING位。
4.遍历proc_table[],寻找A的子进程(如果有的话):
(1)将Init进程设置为A的子进程的父进程(换言之,将A的子进程过继给Init)。
(2)判断是否满足Init正在WAITING且A的子进程正在HANGING:
? 如果是:
i.清除Init的WAITING位;
ii.向Init发送消息以解除阻塞(到此Init的wait()函数结束);
iii.释放A的子进程的进程表项(到此A的子进程的exit()函数结束)。
? 如果否:
i.如果Init正在WAITING但A的子进程没有HANGING,那么“握手”会在将来A的子进程调用exit()时发生;
ii.如果A的子进程正在HANGING但Init没有WAITING,那么“握手”会在将来Init调用wait()时发生。
如果P调用wait(),那么MM将会:
1.遍历proc_table[],寻找P的子进程A:
(1)如果A正在HANGING,那么:
? 向P发送消息以解除阻塞(到此P的wait()函数结束);
? 释放A的进程表项(到此A的exit()函数结束)。
(2)如果A不在HANGING,那么:
? 设P的WAITING位。
2.如果P没有子进程:
向P发送消息,消息携带一个表示出错的返回值(到此P的wait()函数结束)。
2.3 execv函数
在shabby_shell中还有一个execv函数,而execv()所做的其实只是一件事,即向MM提供最终供调用exec的进程使用的堆栈。
下面是exec.c中的execv()和mm/do_exec()的代码。
PUBLIC int execv(const char *path, char * argv[])
{
char **p = argv;
char arg_stack[PROC_ORIGIN_STACK];
int stack_len = 0;
while(*p++) {
assert(stack_len + 2 * sizeof(char*) < PROC_ORIGIN_STACK);
stack_len += sizeof(char*);
}
*((int*)(&arg_stack[stack_len])) = 0;
stack_len += sizeof(char*);
char ** q = (char**)arg_stack;
for (p = argv; *p != 0; p++) {
*q++ = &arg_stack[stack_len];
assert(stack_len + strlen(*p) + 1 < PROC_ORIGIN_STACK);
strcpy(&arg_stack[stack_len], *p);
stack_len += strlen(*p);
arg_stack[stack_len] = 0;
stack_len++;
}
MESSAGE msg;
msg.type = EXEC;
msg.PATHNAME = (void*)path;
msg.NAME_LEN = strlen(path);
msg.BUF = (void*)arg_stack;
msg.BUF_LEN = stack_len;
send_recv(BOTH, TASK_MM, &msg);
assert(msg.type == SYSCALL_RET);
return msg.RETVAL;
}
PUBLIC int do_exec()
{
/* get parameters from the message */
int name_len = mm_msg.NAME_LEN; /* length of filename */
int src = mm_msg.source; /* caller proc nr. */
assert(name_len < MAX_PATH);
char pathname[MAX_PATH];
phys_copy((void*)va2la(TASK_MM, pathname),
(void*)va2la(src, mm_msg.PATHNAME),
name_len);
pathname[name_len] = 0; /* terminate the string */
/* get the file size */
struct stat s;
int ret = stat(pathname, &s);
if (ret != 0) {
printl("{MM} MM::do_exec()::stat() returns error. %s", pathname);
return -1;
}
/* read the file */
int fd = open(pathname, O_RDWR);
if (fd == -1)
return -1;
assert(s.st_size < MMBUF_SIZE);
read(fd, mmbuf, s.st_size);
close(fd);
/* overwrite the current proc image with the new one */
Elf32_Ehdr* elf_hdr = (Elf32_Ehdr*)(mmbuf);
int i;
for (i = 0; i < elf_hdr->e_phnum; i++) {
Elf32_Phdr* prog_hdr = (Elf32_Phdr*)(mmbuf + elf_hdr->e_phoff +
(i * elf_hdr->e_phentsize));
if (prog_hdr->p_type == PT_LOAD) {
assert(prog_hdr->p_vaddr + prog_hdr->p_memsz <
PROC_IMAGE_SIZE_DEFAULT);
phys_copy((void*)va2la(src, (void*)prog_hdr->p_vaddr),
(void*)va2la(TASK_MM,
mmbuf + prog_hdr->p_offset),
prog_hdr->p_filesz);
}
}
/* setup the arg stack */
int orig_stack_len = mm_msg.BUF_LEN;
char stackcopy[PROC_ORIGIN_STACK];
phys_copy((void*)va2la(TASK_MM, stackcopy),
(void*)va2la(src, mm_msg.BUF),
orig_stack_len);
u8 * orig_stack = (u8*)(PROC_IMAGE_SIZE_DEFAULT - PROC_ORIGIN_STACK);
int delta = (int)orig_stack - (int)mm_msg.BUF;
int argc = 0;
if (orig_stack_len) { /* has args */
char **q = (char**)stackcopy;
for (; *q != 0; q++,argc++)
*q += delta;
}
phys_copy((void*)va2la(src, orig_stack),
(void*)va2la(TASK_MM, stackcopy),
orig_stack_len);
proc_table[src].regs.ecx = argc; /* argc */
proc_table[src].regs.eax = (u32)orig_stack; /* argv */
/* setup eip & esp */
proc_table[src].regs.eip = elf_hdr->e_entry; /* @see _start.asm */
proc_table[src].regs.esp = PROC_IMAGE_SIZE_DEFAULT - PROC_ORIGIN_STACK;
strcpy(proc_table[src].name, pathname);
return 0;
}
? execv()
execv接收两个参数:要执行的文件路径和一个字符串数组,其中包含传递给新程序的参数。在您的代码中,execv 通过以下步骤实现:
1.复制参数到一个栈 (arg_stack):
(1)argv中的每个字符串都被复制到一个临时的栈arg_stack中。
(2)每个字符串的指针(在arg_stack中的位置)也被存储在arg_stack中。
2.构造MESSAGE结构:
(1)这个结构用于与内存管理模块(MM)进行通信,告知它要执行新的程序。
(2)消息包含了程序路径、路径长度、参数栈的地址和参数栈的长度。
3.发送消息给MM:
(1)通过send_recv调用,将消息发送给MM任务。
(2)assert确保调用成功。
? do_exec()
它处理从execv发来的执行请求。步骤如下:
1.获取并验证程序路径:
从消息中复制程序路径,并检查路径的合法性。
2.获取程序文件的大小并读取内容:
(1)使用 stat 系统调用获取文件大小。
(2)读取文件内容到mmbuf,这是一个临时缓冲区。
3.解析 ELF 格式的可执行文件:
(1)ELF (Executable and Linkable Format) 是一种常见的二进制文件格式。
(2)解析ELF头部,加载程序段到目标进程的地址空间。
4.设置参数栈:
将参数从execv发送的消息中复制到目标进程的栈空间。
5.设置新的指令指针 (eip) 和栈指针 (esp):
这些寄存器被设置为新程序的入口点和新栈的顶部。
6.更改进程名:
进程表中的名称被更新为新程序的路径。
总之,execv函数负责准备执行新程序所需的所有信息,并将其发送给内存管理模块。MM任务接收这些信息,加载新程序到内存,并更新进程表以反映这些更改。
需要注意的是,execv将arg_stack[]的首地址以及其中有效内容的长度等通过消息发送给了MM。
2.4 修改shell支持多任务执行
了解了上述基础知识后,我们就可以开始修改shell以实现支持多任务运行,我们采取的方法是令父进程fork出多个子进程,然后子进程去执行那些命令。
我们首先定义两个变量MAX_SHELL_PROC和MAX_SHELL_PROC_STACK,分别表示同时可执行的最多指令条数和一次输入的最大长度。
#define MAX_SHELL_PROC 5 //最多指令条数 echo pwd ls cat cp touch rm #define MAX_SHELL_PROC_STACK 128 //一次输入的最大长度
shabby_shell函数代码的前半部分不做修改,和源码是一样的,我们用0标志一条命令的结束。我们用&指令分割不同命令后,argv数组的内容可能是这样的:
argv = {test, &, ls, &, echo, dp'os, dp.txt, &, ls, &, cat, dp.txt}
利用 multi_argv 保存二维字符串数组,对命令进行处理后,它的内容可能是这样的:
multi_argv = {{test, 0},
{ls, 0},
{echo, dp'os, dp.txt, 0},
{ls, 0},
{cat, dp.txt, 0}
}
定义int类型变量num_proc表示有多少个命令,sec_count的作用与上面argc的作用类似。定义字符串数组multi_argv。error标记命令是否出错,初始值赋为0。
char* multi_argv[MAX_SHELL_PROC][MAX_SHELL_PROC_STACK]; //multi_argv保存二维字符串数组 int num_proc = 1; // 表示有多少个命令 int sec_count = 0; // 和上面argc的作用类似 int error = 0; // 标记命令是否出错
接着使用for循环顺序扫描argv数组。如果argv[i]处遇到的不是&,则把字符串放入multi_argv[num_proc - 1][sec_count++],否则,用0标记该命令结束,并将表示任务数量的变量num_proc加一,最后将sec_count重新置为0。
如果任务数量大于定义中的最大任务数MAX_SHELL_PROC,我们将error置为1。
int i;
for (i = 0; i < argc; i++)
{
if (strcmp(argv[i],"&"))
{
multi_argv[num_proc - 1][sec_count++] = argv[i];
} else {
multi_argv[num_proc - 1][sec_count] = 0;
num_proc++;
sec_count = 0;
if (num_proc > MAX_SHELL_PROC)
{
error = 1;
printf("Too many commands!
");
}
}
}
下面的代码只有在error为0,即没有错误时才会执行,出错则直接跳过。父进程fork出子进程后,父子进程均还在循环中,所以进行判断:如果当前进程是父进程,则执行wait(),是子进程,则调用execv执行。
if (!error)
{
for (i = 0; i < num_proc; i++)
{
int fd = open(multi_argv[i][0], O_RDWR);
if (fd == -1) {
if (rdbuf[0]) {
write(1, "{", 1);
write(1, rdbuf, r);
write(1, "}
", 2);
}
}
else {
close(fd);
int pid = fork();
if (pid != 0) { /* parent */
int s;
wait(&s);
}
else { /* child */
execv(multi_argv[i][0], multi_argv[i]);
}
}
}
}
至此对shell的一个最基本的改造就完成了。我们首先将这段新修改的代码放入kernel/main.c中,并编译连接,查看一下执行效果。
我们用之前在任务二中实现的算法指令进行测试,结果如下图。可以看到,改
造后的shell已经可以实现多任务的执行了。

3. 本部分小结
在本部分实验中,我们实现了对shell的改造,使它可以在同一个shell中支持多任务并行。我们首先对同时可执行的指令条数和一次输入的最大长度进行限制,然后修改源码中的shabby_shell函数,用"&"符号分割不同的指令,并定义multi_argv保存二维字符串数组。在for循环中顺序扫描argv数组对输入的指令字符串进行处理,然后父进程fork出所有的子进程以实现多任务的执行。