首页 > 系统相关 >mit6.828笔记 - lab5(上)- Spawn and Shell

mit6.828笔记 - lab5(上)- Spawn and Shell

时间:2024-05-27 16:11:48浏览次数:23  
标签:Spawn sys Shell addr PTE mit6.828 spawn child tf

Spawning Process

有了文件系统了,我们终于可以方便地读取磁盘中的文件了。到目前为止,我们创建进程的方法一直都是在编译内核的时候将程序链接到数据段,在 i386_init 通过 ENV_CREATE 宏创建。
现在我们应该考虑通过文件系统直接将用户程序从硬盘中读取出来,spawn 就是这样的东西。
spawn和unix中的exec不同,spawn 在用户空间实现,不需要内核的特殊帮助,读取文件、创建进程完全通过 syscall。
spawn 已经实现好了,位于 lib/spawn.c 中。很有必要学习一下其中的代码。

spawn.c

spawn 很像 icode_load,但是他需要通过文件的方式读取数据。
而且栈的创建、子进程状态的设置,内存映射都需要以syscall的方式实现。

// 从文件系统加载的程序映像中生成一个子进程。
// prog:要运行的程序的路径名。
// argv: 字符串指针数组的空端指针,这些字符串将作为命令行参数传递给子进程。
// 成功时返回子程序 envid,失败时返回 <0。
int
spawn(const char *prog, const char **argv)
{
	unsigned char elf_buf[512];
	struct Trapframe child_tf;
	envid_t child;

	int fd, i, r;
	struct Elf *elf;
	struct Proghdr *ph;
	int perm;

	// 打开 elf 文件
	if ((r = open(prog, O_RDONLY)) < 0)
		return r;
	fd = r;

	// 读取 elf文件头
	elf = (struct Elf*) elf_buf;
	if (readn(fd, elf_buf, sizeof(elf_buf)) != sizeof(elf_buf)
	    || elf->e_magic != ELF_MAGIC) {
		close(fd);
		cprintf("elf magic %08x want %08x\n", elf->e_magic, ELF_MAGIC);
		return -E_NOT_EXEC;
	}

	// 创建子进程
	if ((r = sys_exofork()) < 0)
		return r;
	child = r;

	// Set up trap frame, including initial stack.
	// 将子进程的 eip 设置为 elf 的入口点
	child_tf = envs[ENVX(child)].env_tf;
	child_tf.tf_eip = elf->e_entry;

	// 为子进程设置栈
	if ((r = init_stack(child, argv, &child_tf.tf_esp)) < 0)
		return r;

	// Set up program segments as defined in ELF header.
	// 将 elf 的程序段载入内存
	ph = (struct Proghdr*) (elf_buf + elf->e_phoff);
	for (i = 0; i < elf->e_phnum; i++, ph++) {
		// 使用 Proghdr 中每个程序段的 p_flags 字段来确定如何映射程序段: 
		if (ph->p_type != ELF_PROG_LOAD)
			continue;
		perm = PTE_P | PTE_U;
		// 如果 ELF 标志不包括 ELF_PROG_FLAG_WRITE,则段包含文本和只读数据。
		// 如果 ELF 段标志包含 ELF_PROG_FLAG_WRITE,则该段包含读/写数据和 bss。
		if (ph->p_flags & ELF_PROG_FLAG_WRITE)
			perm |= PTE_W;
		if ((r = map_segment(child, ph->p_va, ph->p_memsz,
				     fd, ph->p_filesz, ph->p_offset, perm)) < 0)
			goto error;
	}
	close(fd);
	fd = -1;

	// Copy shared library state.
	if ((r = copy_shared_pages(child)) < 0)
		panic("copy_shared_pages: %e", r);

	child_tf.tf_eflags |= FL_IOPL_3;   // devious: see user/faultio.c
	if ((r = sys_env_set_trapframe(child, &child_tf)) < 0)
		panic("sys_env_set_trapframe: %e", r);

	if ((r = sys_env_set_status(child, ENV_RUNNABLE)) < 0)
		panic("sys_env_set_status: %e", r);

	return child;

error:
	sys_env_destroy(child);
	close(fd);
	return r;
}

spawn的步骤:

  • 打开程序文件。
  • 像以前一样读取 ELF 头文件,并检查其神奇数字是否正确。 (检查你的 load_icode!)。
  • 使用 sys_exofork() 创建一个新环境。
  • 将 child_tf 设置为子程序的初始 struct Trapframe。
  • 调用上面的 init_stack() 函数,为子环境设置初始堆栈页面。
  • 将所有 p_type ELF_PROG_LOAD 类型的程序段映射到新环境的地址空间。

init_stack 则是先在父进程的 UTMP 上将子进程的用户栈布局好,然后通过 sys_page_map 将物理页映射到子进程中。布局情况如下:

//下面的argv[n]指的是字符串首地址,也是这个栈中对应条目的虚拟地址
		argv_2 -->			|		"initarg2"		| 	<--  USTACKTOP 
		argv_1 -->			|		"initarg1"		|
		argv_0 -->			|		"init"			|
							|		 0(NULL)		|
							|		 &argv_2		|
							|		 &argv_1		|
		&argv  -->			|		 &argv_0		|
							|	  	 &argv		    |
 child->esp(往上是出栈) -->  |		   3		  	|
————————————————

练习7

练习 7. `spawn` 依靠新的系统调用 `sys_env_set_trapframe` 来初始化新创建环境的状态。在 `kern/syscall.c` 中实现 `sys_env_set_trapframe`(别忘了在 `syscall()` 中调度新的系统调用)。

运行 `kern/init.c` 中的 `user/spawnhello` 程序来测试代码,该程序将尝试从文件系统中生成 `/hello`。

使用 `make grade` 测试代码。
// 将 envid 的陷阱框架设置为 “tf”。
// 修改 tf 是为了确保用户环境始终运行在代码
// 保护级别 3(CPL 3),启用中断,IOPL 为 0。
//
// 成功时返回 0,错误时返回 <0。 错误是
// -E_BAD_ENV 如果环境 envid 当前不存在、
// 或调用者没有权限更改 envid。
static int
sys_env_set_trapframe(envid_t envid, struct Trapframe *tf)
{
	// LAB 5: Your code here.
	// Remember to check whether the user has supplied us with a good
	// address!
	// panic("sys_env_set_trapframe not implemented");
	struct Env * e;
	if(envid2env(envid, &e, true) < 0)
		return -E_BAD_ENV;
	tf->tf_eflags = FL_IF;				//允许中断
	tf->tf_eflags &= ~FL_IOPL_MASK;		//IOPL为0
	tf->tf_cs = GD_UT | 3;				//保护级别 3
	e->env_tf = *tf;
	return 0;
}

然后在 kern/syscall.c : syscall 中补充:

case SYS_env_set_trapframe:
			ret = sys_env_set_trapframe((envid_t) a1, (struct Trapframe *)a2);
			return ret;

为了测试效果,在 kern/init.c : i386_init 中补充:

#if defined(TEST)
	// Don't touch -- used by grading script!
	ENV_CREATE(TEST, ENV_TYPE_USER);
#else
	// Touch all you want.
	// ENV_CREATE(user_icode, ENV_TYPE_USER);
	ENV_CREATE(user_spawnhello, ENV_TYPE_USER);
#endif // TEST*

测试效果 make qemu:

image.png

跨 fork 和 spawn 共享库状态

我们希望在 forkspawn 之间共享文件描述符状态,但文件描述符状态保存在用户空间内存中。
现在,fork 时,内存将被标记为写入时复制,因此状态将被复制而非共享(这意味着进程无法在自己未打开的文件中寻址,管道也无法在 fork 时工作)。
spawn时,内存将被留下,根本不会被复制。(实际上,生成(spawned)的进程一开始并没有打开文件描述符)。

我们将修改 fork,使其知道某些内存区域被 "库操作系统 "使用,并应始终共享。
我们将在页表项中设置一个未使用的位,而不是在某个地方硬编码一个区域列表(就像我们在 fork 中设置 PTE_COW 位一样)。

我们在 inc/lib.h 中定义了一个新的 PTE_SHARE 位。
该位是 Intel 和 AMD 手册中标明 "可用于软件 "的三个 PTE 位之一。
我们将建立一个惯例,即如果页表项设置了该位,则 PTE 应在 forkspawn 中直接从父节点复制到子节点。
请注意,这与 "写入时复制 "不同:如第一段所述,我们要确保共享页面更新。

练习 8. 修改 `lib/fork.c` 中的 `duppage`,以遵循新的约定。如果页表项设置了 `PTE_SHARE` 位,则直接复制映射即可。(您应该使用 `PTE_SYSCALL`,而不是 0xfff 来屏蔽掉页表项中的相关位。0xfff 还会拾取访问位和脏位)。

同样,在 `lib/spawn.c` 中实现 `copy_shared_pages`。它应该循环查看当前进程中的所有页表项(就像 fork 所做的),将任何设置了 `PTE_SHARE` 位的页面映射复制到子进程中。

lib/fork.c : duppage

// 将当前进程(父进程)的内存映射(页表)复制给子进程,同时标记COW
static int
duppage(envid_t envid, unsigned pn)
{
	int r;

	// LAB 4: Your code here.
	// panic("duppage not implemented");
	void *addr = (void *)(pn * PGSIZE);
	//如果页表项设置了 `PTE_SHARE` 位,则直接复制映射即可。
	if(uvpt[pn] & PTE_SHARE){
		sys_page_map(0, addr, envid, addr, PTE_SYSCALL);	
	}
	//对父进程所有可写页或COW页,标记COW
	else if ((uvpt[pn]&PTE_W)|| (uvpt[pn] & PTE_COW)){
		if ((r = sys_page_map(0, addr, envid, addr, PTE_COW|PTE_U|PTE_P)) < 0)
			panic("duppage:sys_page_map:%e", r);
		if ((r = sys_page_map(0, addr, 0, addr, PTE_COW|PTE_U|PTE_P)) < 0)
			panic("duppage:sys_page_map:%e", r);
	}
	//对于父进程的只读页不标记COW
	else{
		sys_page_map(0, addr, envid, addr, PTE_U|PTE_P);	
	}
	return 0;
}

lib/spawn.c : copy_shared_pages

// 将共享页面的映射复制到子地址空间。
static int
copy_shared_pages(envid_t child)
{
	// LAB 5: Your code here.
	uintptr_t addr;
	for (addr = 0; addr < UTOP; addr += PGSIZE) {
		if ((uvpd[PDX(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_P) &&
				(uvpt[PGNUM(addr)] & PTE_U) && (uvpt[PGNUM(addr)] & PTE_SHARE)) {
            sys_page_map(0, (void*)addr, child, (void*)addr, (uvpt[PGNUM(addr)] & PTE_SYSCALL));
        }
	}
	return 0;
}

话说,我们是在什么时候将文件描述符标记为 PTE_SHARE 的呢?vscode搜索一下:
答案是在 serve_open 的末尾,文件的主循环在处理open请求时,调用serve_open,然后serve_open 申请一个新的openfile,代表打开的文件。然后将该openfile关联的 FD 所在物理页,以及该物理页权限返回给serve,如下图

image.png

紧接着 serve 调用 ipc_send 将 FD 物理页发送给客户端,并以带有 PTE_SHARE 的权限,将FD安装在客户端调用 ipc_recv 时指定的地址。

因此,所有通过open打开的文件描述符,都是 PTE_SHARE 的。经过 fork 或 spawn 后,父子进程共享。

键盘接口

为了让 shell 正常工作,我们需要一种输入方式。QEMU 一直在显示我们写入 CGA 显示屏和串行端口的输出,但到目前为止,我们只在内核监视器中输入了内容。在 QEMU 中,在图形窗口中输入的内容会以键盘输入的形式显示在 JOS 上,而输入到控制台的内容则会以串行端口上的字符形式显示。kern/console.c 已经包含了内核监视器从实验一开始就使用的键盘和串行驱动程序,但现在你需要将它们连接到系统的其他部分。

练习 9. 在 kern/trap.c 中,调用 kbd_intr 处理陷阱 IRQ_OFFSET+IRQ_KBD,调用 serial_intr 处理陷阱 IRQ_OFFSET+IRQ_SERIAL。

我们在 lib/console.c 中为您实现了控制台输入/输出文件类型。kbd_intr 和 serial_intr 会将最近读取的输入内容填入缓冲区,而控制台文件类型则会耗尽缓冲区(除非用户重定向,否则默认情况下控制台文件类型用于 stdin/stdout)。

运行 make run-testkbd 并键入几行代码,测试你的代码。当你输入完毕时,系统会回声提示。如果控制台和图形窗口都可用,请尝试同时在控制台和图形窗口中键入。

kern/trap.c : trap_dispatch 中添加:

// Handle keyboard and serial interrupts.
	// LAB 5: Your code here.
	if (tf->tf_trapno == IRQ_OFFSET + IRQ_KBD){
		kbd_intr();
  		return;
	}
	if (tf->tf_trapno == IRQ_OFFSET + IRQ_SERIAL){
		serial_intr();
  		return;
	}

然后 make run-testkbd

image.png

看起来只是简单的回显了输入,来看看代码
user/testkbd

#include <inc/lib.h>

void
umain(int argc, char **argv)
{
	int i, r;

	// Spin for a bit to let the console quiet
	for (i = 0; i < 10; ++i)
		sys_yield();

	close(0);
	// 打开一个文件,这个文件的设备类型是终端
	if ((r = opencons()) < 0)
		panic("opencons: %e", r);
	// 由于是第一个打开的,fd应该是0
	if (r != 0)
		panic("first opencons used fd %d", r);
	// 复制一个文件描述符
	if ((r = dup(0, 1)) < 0)
		panic("dup: %e", r);

	for(;;){
		char *buf;

		buf = readline("Type a line: ");
		if (buf != NULL)
			// fprintf 最终会调用write向fd1写入数据
			// 此时会将内容显示在终端上
			fprintf(1, "%s\n", buf);
		else
			fprintf(1, "(end of file received)\n");
	}
}

首先打开了文件描述符0,其设备类型为终端devcons(可以通过 opencons 看到)。
然后复制了文件描述符0得到文件描述符1,并向文件描述符1写入我们输入的字符串。
从而使得终端显示了我们的输入。
image.png

The Shell

运行 make run-icode 或 make run-icode-nox。这将运行内核并启动用户/icode。icode 会执行 init,将控制台设置为文件描述符 0 和 1(标准输入和标准输出)。然后会生成 shell sh。你应该可以运行以下命令:

	echo hello world | cat
	cat lorem |cat
	cat lorem |num
	cat lorem |num |num |num |num |num
	lsfd

请注意,用户库例程 cprintf 直接打印到控制台,而不使用文件描述符代码。这非常适合调试,但不适合在其他程序中使用。printf("...", ...) 是打印到 FD 1 的快捷方式。有关示例,请参见 user/lsfd.c。

练习 10.

shell 不支持 I/O 重定向。如果能运行 sh <script 就好了,而不必像上面那样手写输入脚本中的所有命令。将 < 的 I/O 重定向添加到 user/sh.c。

在 shell 中键入 sh <script 测试你的实现

运行 make run-testshell 测试你的 shell。testshell 只需将上述命令(也可在 fs/testshell.sh 中找到)输入 shell,然后检查输出是否与 fs/testshell.key 一致。

熟悉 linux 的 bash 的话,应该知道IO重定向的概念。 < 用于重定向标准输入。比如说 [命令a] < [文件b] 的含义就是,将命令a的标准输入改为文件b。
标准输入的文件描述符编号是0,所以我们要做的就是将,打开 文件b,然后将 文件描述符0 改为文件b:


// LAB 5: Your code here.
// panic("< redirection not implemented");
// t是gettoken得到的当前短语,即文件b的文件名,打开文件b
if ((fd = open(t, O_RDONLY)) < 0) {
	cprintf("open %s for read: %e", t, fd);
	exit();
}
if (fd != 0) {
	// 将文件描述符0 变为文件b的副本
	dup(fd, 0);
	// 关闭文件b
	close(fd);
}
break;

image.png

关于 /lib/sh.c
sh.c 实现了一个 shell,其核心函数式 runcmd。逻辑其实也很简单,通过循环调用gettoken 解析命令。然后根据重定向的需求修改输入输出,最后通过 spawn 运行相关程序。

image.png

管道部分比较有意思:
image.png

管道左侧直接跳转到 runit 运行命令,右侧则重新解析命令。

管道右侧需要等待管道左侧运行完毕后,再运行:

image.png

标签:Spawn,sys,Shell,addr,PTE,mit6.828,spawn,child,tf
From: https://www.cnblogs.com/toso/p/18215788

相关文章

  • shell脚本的简单初识
     脚本相信大家都不陌生,平时玩游戏遇到的各种辅助软件;你可能要敲上一会的命令,大佬发给你一个文本,运行一下一秒解决。脚本确实帮助了我们很多,今天就来简单的了解一下在Linux中的一个shell脚本。什么是shell脚本?作用,或者是好处shell脚本简单来说就是将平时使用的指令按照顺序......
  • 【SHELL】命令使用笔记
    按行拼接两个文件awk'NR==FNR{a[NR]=$0;next}{print$0,a[FNR]}'B.txtA.txt>C.txt注:文件格式须为unix,dos格式拼接后会跨行 在指定格式的文件中查找字符串在指定格式的文件中查找字符串grep-nr"string"--include=*.{c,cpp,h}在排除指定格式的文件中查找字符串grep......
  • 40道Bash Shell高频题整理(附答案背诵版)
    1.简述如何调试Shell脚本?调试Shell脚本是一个帮助开发者识别和修正脚本中错误的过程。Bash提供了多种方式来调试脚本,其中包括:使用-x选项:通过在运行脚本时使用-x选项,Bash会在执行每一行命令之前打印该命令。这有助于查看脚本的执行流程和变量的值变化。例如,如果有......
  • shell编程之循环语句与函数
    一、for循环语句        在实际工作中,经常会遇到某项任务需要多次执行的情况,而每次执行时仅仅是处理的对象不一样,其他命令相同。例如,根据通讯录中的姓名列表创建系统账号,根据服务器清单检查各主机的存活状态,根据IP地址黑名单设置拒绝访问的防火墙策略等。    ......
  • Shell编程规范与变量
    一、Shell脚本概述        在一些复杂的Linux维护工作中,大量重复性的输入和交互操作不仅费时费力,而且容易出错,而编写一个恰到好处的Shell脚本程序,可以批量处理、自动化地完成一系列维护任务,大大减轻管理员的负担。1.1 Shell的作用        Linux系统......
  • Shell 编程之条件语句
    条件测试操作        Shell环境根据命令执行后的返回状态值($?)来判断是否执行成功,当返回值为0时表示成功,否则(非0值)表示失败或异常。使用专门的测试工具——test命令,可以对特定条件进行测试,并根据返回值来判断条件是否成立(返回值为0表示条件成立)。使用test测......
  • linux shell中移除文件的后缀、前缀
     001、[root@PC1test2]#a="a.csv.map.txt"[root@PC1test2]#echo$aa.csv.map.txt[root@PC1test2]#echo${a%.*}a.csv.map[root@PC1test2]#echo${a%%.*}a 。 002、[root@PC1test2]#ls[root@PC1test2]#a="a.csv.map.txt"[root@......
  • Linux shell 变量中何时需要使用花括号
     001、简单测试[root@PC1test2]#ls[root@PC1test2]#a="abc"##生成一个测试变量[root@PC1test2]#echo$a##输出变量方式1abc[root@PC1test2]#echo${a}##输出变量方式2abc[root@PC1test2]#echo$axxx##......
  • shell中的命令
    shell中的特殊字符1.通配符:    *:匹配任意长度的字符串   ?:匹配任意一个字符   []:匹配方括号内任意一个字符   [1-4]:匹配方括号内范围内的一个字符   [^]:匹配除了方括号内的任意一个字符   2.管道:    |:将一条命令的输出作为另外一条命令......
  • 常用于管理的shell命令
    常用于管理的shell命令一、进程管理命令    1.ps:查看进程的信息   选项:    -aux:查看所有用户的进程的详细信息       进程ID:在操作系统中标识唯一进程        进程状态:        运行态R+表示在后台运行   ......