学校的pintos project2实验大作业记录
GitHub:完成代码
获得更好的阅读体验以及更新,请阅读原博客
目录
安装Pintos
References:
https://zhuanlan.zhihu.com/p/104497182
https://blog.csdn.net/geeeeeker/article/details/108104466
YouTube教程(口音迷人,但是看视频还是能看懂的)
我的环境:vmware虚拟机 Ubuntu 16.04
Install QEMU Simulator
sudo apt-get install qemu
下载pintos源码
从https://github.com/WyldeCat/pintos-anon中下载zip压缩包(Windows下载完后,往vmware直接拖进去就行),在虚拟机中解压。
Edit GDBMACROS
Open pintos/src/utils/pintos-gdb. Make the variable GDBMACROS point to pintos/src/misc/gdb-macros i.e. GDBMACROS=/home/....../pintos/src/misc/gdb-macros
. Note that it should point to the full path.
就是将如上图橙色部分将$PINTOS_DIR
替换即可
编辑Makefile
编辑Makefile文件,把其中的LOADLIBES
改成LDLIBS
编译utils
在xxx/pintos-anon-master/src/utils下执行make
命令
编辑Make.vars文件
在src/threads/Make.vars的第七行把bochs
改成qemu
编译threads
在threads文件夹下执行make
命令
注意:执行完后最后一句会有make[1]: Leaving directory '/home/ddd/Desktop/pintos-anon-master/src/threads/build'
(每个人都不一样,后面有用)
编辑pintos
在src/utils/pintos文件中做以下更改:
- Line 103:
bocs
改成qemu
- (存在bug) Line 257:
kernel.bin
改成/home/.../pintos/src/threads/build/kernel.bin
(即之前的路径) - Line 621:把
qemu-system-i386
改成qemu-system-x86_64
编辑Pintos.pm
- (存在bug)Line 362:
loader.bin
改成/home/.../pintos/src/threads/build/loader.bin
(同上)
把utils路径加入PATH变量
打开~/.bashrc
最后一行加上export PATH=/home/.../pintos/src/utils:$PATH
重新加载terminals
source ~/.bashrc
运行pintos
pintos run alarm-multiple
验证配置成功
- 方法1:在utils文件夹下运行
pintos run alarm-multiple
,成功时会出现 - 方法2:在threads文件夹下运行
make check
,执行时间比较长,最后会出现结果提示通过7个测试
文件系统
创建磁盘(不确定需不需要做,之前在debug的时候做了,一团乱麻了属于是)
the current directory is userprog/build
:
pintos-mkdisk filesys.dsk --filesys-size=2
pintos -- -f -q
pintos -p ../../examples/echo -a echo -- -q
pintos -- -q run 'echo x'
bug修复
出现报错:Kernel panic in run: PANIC at ../../threads/init.c:264 in parse_options(): unknown option '-f' (use -h for help) Call stack: 0xc00285bf
把前面标了bug的部分threads
改成userprog
,再次在userprog
, utils
文件夹下执行make
工具
由于在虚拟机中编程实在太过于阴间,我找到了vscode通过ssh连接虚拟机的方法,成功在windows中使用vscode编程。
配置方法:https://blog.csdn.net/qq_40300094/article/details/114639608
另外虚拟机ip最好设置固定或者默认时间久一些
Project2 背景
参考:
https://github.com/Wang-GY/pintos-project2/blob/master/project_report.md
http://www.cs.jhu.edu/~huang/cs318/fall18/project/project2.html
https://zhuanlan.zhihu.com/p/343328700
https://zhuanlan.zhihu.com/p/340428650
背景
到目前为止,您在 Pintos 下运行的所有代码都已成为操作系统内核的一部分。这意味着,例如,上次分配的所有测试代码都作为内核的一部分运行,可以完全访问系统的特权部分。一旦我们开始在操作系统之上运行用户程序,就不再是这样了,这个项目就处理接下来的情况。
我们允许一次运行多个进程。每个进程有一个线程(不支持多线程进程)。用户程序是在他们拥有整台机器的错觉下编写的。这意味着,当您一次加载和运行多个进程时,您必须正确管理内存、调度和其他状态,以保持这种错觉。
在之前的项目中,我们将测试代码直接编译到您的内核中,因此我们必须在内核中要求某些特定的功能接口。从现在开始,我们将通过运行用户程序来测试您的操作系统,这给了你更大的自由。您必须确保用户程序接口满足此处描述的规范,但即使考虑到该限制,您仍可以随意重构或重写内核代码。
userprog文件夹下文件的用处
- 加载 ELF 二进制文件并启动进程:process.c/h
- 一个简单的 80 x 86 硬件页表管理器。尽管您可能不想为此项目修改此代码,但您可能希望调用其中的一些函数:pagedir.c/h
- 每当用户进程想要访问某些内核功能时,它都会调用系统调用。这是一个骨架系统调用处理程序。目前,它只是打印一条消息并终止用户进程。在本项目的第 2 部分中,您将添加代码以执行系统调用所需的所有其他操作:syscall.c/h
- 当用户进程执行特权或禁止操作时,它会作为“异常”或“错误”进入内核。(3) 这些文件处理异常。目前,所有异常都只是打印一条消息并终止进程。项目 2 的一些(但不是全部)解决方案需要
page_fault()
在此文件中进行修改:exception.c/h - 80 x 86 是一种分段架构。全局描述符表 (GDT) 是一个描述正在使用的段的表。这些文件设置了全局描述符表。您不需要为任何项目修改这些文件。如果您对 GDT 的工作方式感兴趣,可以阅读代码:gdt.c/h
- 任务状态段 (TSS) 用于 80 x 86 架构任务切换。Pintos 仅在用户进程进入中断处理程序时使用 TSS 来切换堆栈,Linux 也是如此。您不需要为任何项目修改这些文件。如果您对 TSS 的工作方式感兴趣,可以阅读代码:tss.c/h
简而言之,我们需要改变的文件只有:process.c/h、syscall.c/h、exception.c/h(是不是感觉好多了
API
//filesys.h
#ifndef FILESYS_FILESYS_H
#define FILESYS_FILESYS_H
#include <stdbool.h>
#include "filesys/off_t.h"
/* Sectors of system file inodes. */
#define FREE_MAP_SECTOR 0 /* Free map file inode sector. */
#define ROOT_DIR_SECTOR 1 /* Root directory file inode sector. */
/* Block device that contains the file system. */
struct block *fs_device;
void filesys_init (bool format);
void filesys_done (void);
bool filesys_create (const char *name, off_t initial_size);
struct file *filesys_open (const char *name);
bool filesys_remove (const char *name);
#endif /* filesys/filesys.h */
//file.h
#ifndef FILESYS_FILE_H
#define FILESYS_FILE_H
#include "filesys/off_t.h"
struct inode;
/* Opening and closing files. */
struct file *file_open (struct inode *);
struct file *file_reopen (struct file *);
void file_close (struct file *);
struct inode *file_get_inode (struct file *);
/* Reading and writing. */
off_t file_read (struct file *, void *, off_t);
off_t file_read_at (struct file *, void *, off_t size, off_t start);
off_t file_write (struct file *, const void *, off_t);
off_t file_write_at (struct file *, const void *, off_t size, off_t start);
/* Preventing writes. */
void file_deny_write (struct file *);
void file_allow_write (struct file *);
/* File position. */
void file_seek (struct file *, off_t);
off_t file_tell (struct file *);
off_t file_length (struct file *);
#endif /* filesys/file.h */
虚拟内存布局
Pintos 中的虚拟内存分为两个区域:用户虚拟内存和内核虚拟内存。用户虚拟内存范围从虚拟地址 0 到PHYS_BASE
,在threads/vaddr.h
中定义 ,默认为0xc0000000
(3 GB)。内核虚拟内存占用剩余的虚拟地址空间, PHYS_BASE
最多 4 GB。
用户虚拟内存是每个进程的。当内核从一个过程到另一个切换,它也通过改变处理器的页面目录基址寄存器(见切换用户的虚拟地址空间 userprog / pagedir.c
中pagedir_activate()
)。 struct thread
包含一个指向进程页表的指针。
内核虚拟内存是全局的。无论用户进程或内核线程正在运行什么,它总是以相同的方式映射。在 Pintos 中,内核虚拟内存一对一映射到物理内存,从PHYS_BASE
. 即虚拟地址 PHYS_BASE
访问物理地址 0,虚拟地址PHYS_BASE
+ 0x1234
访问物理地址0x1234
,依此类推,直到机器物理内存的大小。
用户程序只能访问自己的用户虚拟内存。访问内核虚拟内存的尝试导致页面错误,通过在userprog / exception.c
中的page_fault()
处理,且过程将被终止。内核线程可以访问内核虚拟内存,如果用户进程正在运行,还可以访问正在运行的进程的用户虚拟内存。但是,即使在内核中,尝试访问未映射的用户虚拟地址的内存也会导致page fault。
建议实现顺序
-
参数传递:每个用户程序都会立即出现页面错误,直到实现参数传递之后。
现在,您可能只想改变
setup_stack()
中的*esp = PHYS_BASE;
至
*esp = PHYS_BASE - 12;
这适用于任何不检查其参数的测试程序,尽管其名称将打印为
(null)
.在实现参数传递之前,您应该只运行不传递命令行参数的程序。尝试向程序传递参数将在程序名称中包含这些参数,这可能会失败。
-
用户内存访问:所有系统调用都需要读取用户内存。很少有系统调用需要写入用户内存。
-
系统调用基础结构:实现足够的代码以从用户堆栈中读取系统调用号并根据它分派给处理程序。
-
系统调用
exit
。每个以正常方式完成的用户程序都会调用exit
. 即使是一个从main()
返回的程序,它也间接调用exit
(见lib/user/entry.c
中_start()
)。 -
系统调用
write
写入 fd 1(系统控制台)。我们所有的测试程序都写到控制台(用户进程版本printf()
就是这样实现的),所以write
在可用之前它们都会出现故障。 -
现在,把
process_wait()
改为无限循环(永远等待)。现在提供的实现会立即返回,因此 Pintos 将在任何进程实际运行之前关闭。您最终需要提供正确的实现。
实现上述后,用户进程应该最少工作。至少,他们可以写入控制台并正确退出。然后您可以优化您的实现,以便一些测试开始通过。
任务
-
处理终止信息
每当用户进程终止时,因为它调用
exit
或出于任何其他原因,打印进程的名称和退出代码,格式为printf ("%s: exit(%d)\n", ...);
打印的名称应该是传递给process_execute()
的全名,省略命令行参数。当不是用户进程的内核线程终止或调用halt
系统调用时,不要打印这些消息。当进程加载失败时,该消息是可选的。练习 2.1
当进程终止时, 打印
"%s: exit(%d)\n"
格式为进程名称和退出状态的退出消息。 -
参数传递
目前,
process_execute()
不支持向新进程传递参数。通过扩展process_execute()
来实现此功能, 而不是简单地将程序文件名作为其参数,而是将其以空格分隔成单词。第一个词是程序名称,第二个词是第一个参数,依此类推。也就是说,process_execute("grep foo bar")
应该运行grep
传递两个参数foo
和bar
。练习 2.2
为
process_execute()
.添加参数传递支持。在命令行中,多个空格等价于一个空格,所以这
process_execute("grep foo bar")
相当于我们原来的例子。您可以对命令行参数的长度施加合理的限制。例如,您可以将参数限制为适合单个页面 (4 kB) 的参数。(pintos
实用程序可以传递给内核的命令行参数有 128 字节的无关限制。)您可以按您喜欢的任何方式解析参数字符串。如果你迷路了,看看
strtok_r()
,在lib/string.h 中
原型化并在lib/string.c
中用完整的注释实现。您可以通过查看手册页(man strtok_r
在提示符下运行)找到有关它的更多信息。 -
访问用户内存
作为系统调用的一部分,内核必须经常通过用户程序提供的指针访问内存。内核在这样做时必须非常小心,因为用户可以传递一个空指针、一个指向未映射虚拟内存的指针或一个指向内核虚拟地址空间的指针(上图PHYS_BASE)。通过终止违规进程并释放其资源,必须拒绝所有这些类型的无效指针,而不会对内核或其他正在运行的进程造成损害。
练习 2.3
支持读取和写入用户内存以进行系统调用。
至少有两种合理的方法可以正确地做到这一点。
第一种方法是验证用户提供的指针的有效性,然后取消引用它。如果您选择这条路线,您将需要查看
userprog/pagedir.c
和threads/vaddr.h
中的函数。这是处理用户内存访问的最简单方法。第二种方法是只检查用户指针是否指向下方
PHYS_BASE
,然后取消引用它。无效的用户指针将导致“页面错误”,你可以通过修改代码的处理page_fault()
在userprog / exception.c
。这种技术通常更快,因为它利用了处理器的 MMU,所以它往往用于实际内核(包括 Linux)。无论哪种情况,您都需要确保不会“泄漏”资源。例如,假设您的系统调用使用
malloc()
. 如果之后遇到无效的用户指针,您仍然必须确保释放锁定或释放内存页面。如果您选择在取消引用之前验证用户指针,这应该很简单。如果无效指针导致页面错误,则更难处理,因为无法从内存访问中返回错误代码。因此,对于那些想要尝试后一种技术的人,我们将提供一些有用的代码:/* 在用户虚拟地址 UADDR 读取一个字节。 UADDR 必须低于 PHYS_BASE。 如果成功则返回字节值,如果 发生段错误则返回 -1 。*/ static int get_user (const uint8_t *uaddr) { int result; asm ("movl $1f, %0; movzbl %1, %0; 1:" : "=&a" (result) : "m" (*uaddr)); return result; } /* 将 BYTE 写入用户地址 UDST。 UDST 必须低于 PHYS_BASE。 如果成功则返回真,如果发生段错误则返回假。*/ static bool put_user (uint8_t *udst, uint8_t byte) { int error_code; asm ("movl $1f, %0; movb %b2, %1; 1:" : "=&a" (error_code), "=m" (*udst) : "q" (byte)); return error_code != -1; }
这些函数中的每一个都假定用户地址已经被验证为低于
PHYS_BASE
。他们还假设您已经进行了修改,page_fault()
以便内核中的页面错误仅设置eax
为0xffffffff
并将其以前的值复制到eip
. -
系统调用
练习 2.4.1
在
userprog/syscall.c
实现系统调用处理程序。我们提供的框架实现通过终止进程来“处理”系统调用。它将需要检索系统调用号,然后是任何系统调用参数,并执行适当的操作。练习 2.4.2
实现以下系统调用。列出的原型是包含
lib/user/syscall.h
的用户程序看到的原型。(这个头文件,以及lib/user
中的所有其他头文件,仅供用户程序使用。)每个系统调用的系统调用号在lib/syscall-nr.h
中定义 -
拒绝写入可执行文件
练习 2.5
添加代码以拒绝写入用作可执行文件的文件。许多操作系统这样做是因为如果进程试图运行正在磁盘上更改的代码,则会产生不可预测的结果。一旦在项目 3 中实现了虚拟内存,这一点尤其重要,但即使现在也不会受到影响。
您可以使用
file_deny_write()
来防止写入打开的文件。调用file_allow_write()
,文件将重新启用它们(除非文件被另一个打开程序拒绝写入)。关闭文件也将重新启用写入。因此,要拒绝写入进程的可执行文件,只要进程仍在运行,您就必须保持它处于打开状态。
实现过程
orz!
本项目均为参考https://github.com/NicoleMayer/pintos_project2的代码和文档,根据实验过程一步步“推演”的结果,代码和源代码基本一致。
由于并非原作者,理解可能不尽正确,还望谅解以及指正
参数传递
strtok_r函数
需要用到strtok_r函数
char *
strtok_r (char *s, const char *delimiters, char **save_ptr) ;
函数的返回值是 排在前面的被分割出的字串,或者为NULL
s是传入的字符串。需要注意的是 :第一次使用strtok_r之后,要把str置为NULL,
delim指向依据分割的字符串,常见的空格“ ” 逗号“,”等。
saveptr保存剩下待分割的字符串。
注意:strtok_r会改变s的值,所以需要复制后再进行操作
数据结构
数据结构thread(thread.h)修改(这些修改在参数传递部分非必须,暂且提一嘴,后面会有详细的讨论):
/* Structure for Task2 */
struct list childs; /* The list of childs */
struct child * thread_child; /* Store the child of this thread */
int st_exit; /* Exit status */
struct semaphore sema; /* Control the child process's logic, finish parent waiting for child */
bool success; /* Judge whehter the child's thread execute successfully */
struct thread* parent; /* Parent thread of the thread */
线程的同步操作是依靠struct thread里的success变量以及信号量sema的增减实现的。success记录了线程是否成功执行,而通过创建子进程时父进程信号量减少、子进程结束时父进程信号量增加来实现父进程等待子进程的效果,并保证子进程结束唤醒父进程。
信号量在操作系统课上有讲过, sema_up()和sema_down()类似于signal()和wait()
这些量的初始化和改变在代码里都可以找到,就不赘述了
完成函数
参数传递的任务是重写process_execute()以及相关函数,使得传入的filename分割成文件名、参数,并压入栈中
就是…我们在开始执行的时候要把它(可执行文件)从硬盘load(用load函数)到内存里,然后根据用户在命令行输入的参数初始化程序的栈(这里栈指针用esp来表示),也就是把参数按照某种方式一个个压进栈里。然后才跳转到这个程序的start处让它自己运行去。
tid_t
process_execute (const char *file_name)
{
char *fn_copy0, *fn_copy1;
tid_t tid;
/* Make a copy of FILE_NAME.
Otherwise strtok_r will modify the const char *file_name. */
fn_copy0 = palloc_get_page(0);//palloc_get_page(0)动态分配了一个内存页
if (fn_copy0 == NULL)//分配失败
return TID_ERROR;
/* Make a copy of FILE_NAME.
Otherwise there's a race between the caller and load(). */
fn_copy1 = palloc_get_page (0);
if (fn_copy1 == NULL)
{
palloc_free_page(fn_copy0);
return TID_ERROR;
}
//把file_name 复制2份,PGSIZE为页大小
strlcpy (fn_copy0, file_name, PGSIZE);
strlcpy (fn_copy1, file_name, PGSIZE);
/* Create a new thread to execute FILE_NAME. */
char *save_ptr;
char *cmd = strtok_r(fn_copy0, " ", &save_ptr