project2是在src/userprog中进行代码修改,主要分为参数分离和系统调用者两大任务。
用到的相干目录:pintos/src/userprog/process.c thread.h synch.h
在这个任务中,我们主要修改"process.c"和处理字符串。同时,为了测试我们的算法在此任务中的正确性,我们还必须在'syscall.c'中实现其他功能
预期pass:aggs-xxxx
修改
修改函数process_execute ()
修改函数 start_process ()
添加函数push_argument(void **esp,int argc, int argv[])
修改函数process_wait ()
修改
添加函数sys_write(int fd, const void *buffer, unsigned size, int* ret)
process_execute()
把传入的file_name用strtok_r
()函数分离字符串,获得当前的线程名,为实现参数传递做准备
fn_copy2 = strtok_r (fn_copy2, " ", &save_ptr);
以此为线程名创建一个新线程,然后新线程转去执行start_process
函数
tid = thread_create (fn_copy2, PRI_DEFAULT, start_process, fn_copy);
保证父进程与子进程的返回顺序,用同步操纵使得父进程在子进程之后返回。
sema_down(&thread_current()->sema);//降低父进程的信号量,等待子进程结束
通过线程的success变量值判断线程是否运行成功,如果子进程加载可执行文件的过程没有问题,就返回新建线程的tid;如果子进程加载可执行文件失败报错,则返回TID_ERROR。
if (!thread_current()->success) return TID_ERROR;
return tid;
start_process()
原本的函数中,如果线程加载不成功,则直接执行进程退出函数。
修改后:
①做与process_execute
()相同的操作,先把传入的file_name用strtok_r
()函数分离字符串,获得当前的线程名
file_name = strtok_r (file_name, " ", &save_ptr);
②如果调用load成功:将命令行输入的参数分离后得到的数组(调用push_argument
()函数)
for (token = strtok_r (fn_copy, " ", &save_ptr); token != NULL; token = strtok_r (NULL, " ", &save_ptr)){
if_.esp -= (strlen(token)+1);
memcpy (if_.esp, token, strlen(token)+1);//栈指针退后来存放token
argv[argc++] = (int) if_.esp;//argv数组的末尾存放栈顶地址
}
push_argument (&if_.esp, argc, argv);//新加函数
thread_current ()->parent->success = true;
sema_up (&thread_current ()->parent->sema);
③如果调用不成功,那么久保存父进程的执行状态(失败),提升信号量并退出
thread_current ()->parent->success = false;
sema_up (&thread_current ()->parent->sema);
thread_exit ();
process_argument()
这是新添加的函数,为了实现将start_process
()中命令行的参数分离,将得到的数组压入栈中,具体代码如下
void
push_argument (void **esp, int argc, int argv[]){
*esp = (int)*esp & 0xfffffffc;
*esp -= 4;
*(int *) *esp = 0;
/*下面这个for循环的意义是:按照argc的大小,循环压入argv数组,这也符合argc和argv之间的关系*/
for (int i = argc - 1; i >= 0; i--)
{
*esp -= 4;//每次入栈后栈指针减4
*(int *) *esp = argv[i];
}
*esp -= 4;
*(int *) *esp = (int) *esp + 4;
*esp -= 4;
*(int *) *esp = argc;
*esp -= 4;
*(int *) *esp = 0;
}
process_wait()
获得当前父进程的子进程,遍历查询每个子进程,记录并判断退出状态。如果子进程已经结束,则减少子进程信号量以唤醒父进程,从子进程列表中删除该子进程并返回退出状态。下面是该函数内的遍历子进程循环。
while (temp != list_end (l))
{
temp2 = list_entry (temp, struct child, child_elem);
if (temp2->tid == child_tid)
{
if (!temp2->isrun)//
{
temp2->isrun = true;
sema_down (&temp2->sema);
break;
}
else
{
return -1;//子进程还在运行,没有退出,则返回-1
}
}
temp = list_next (temp);
}
sys_write()
写入情况两种:
①往缓冲区写入,调用putbuf
函数完成写入。
②往文件中写入调用find_file_id
获取要写入的文件的标识符,调用file_write
往文件写入数据。
涉及到的文件目录 :pintos/src/userprog/syscall.h syscall.c
pintos/src/threads/thread.h
预期pass:halt exec-xxxx multi-xxx rox-xxxx
具体修改:
创建新的一个结构 child
struct child
{
tid_t tid; /* 标识*/
bool isrun;/* 运行是否成功 */
struct list_elem child_elem; /* 子进程 */
struct semaphore sema; /* 控制等待的信号量 */
int store_exit;/* 子线程的退出状态 */
};
向 thread
添加新的属性
struct list childs; /* 子进程 */
struct child * thread_child;
int st_exit; /* 退出状态 */
struct semaphore sema; /* 实现父进程等待子进程 */
bool success; /* 子进程是否运行成功 */
struct thread* parent; /* 父线程*/
struct list files; /* 打开的文件列表 */
int file_fd; /* 文件描述符 */
struct file * file_owned; /* 打开的文件*/
添加函数get_user (const uint8_t *uaddr)
修改函数syscall_handler ()
修改函数syscall_init ()
添加函数sys_halt (intr_frame* f)
, sys_exit (intr_frame* f)
, sys_exec (intr_frame* f)
, sys_write(intr_frame* f)
, int sys_wait(intr_frame* f);
syscall_handler()
功能:对于任务二,只需将1添加到其第一个参数,并打印其结果。弹出用户栈参数,将这一类去除,在按照这个类型去查找syscall_init
中定义的syscalls数组,找到对应的系统调用,在执行系统调用之前检查地址是否指向有效地址,之后并执行它。如果地址无效,那么我们需要释放内存页,并在退出之前释放该进程中的所有锁或信号量。
static void
syscall_handler (struct intr_frame *f UNUSED)
{
int * p = f->esp;
check_ptr2 (p + 1);//检查有效性
int type = * (int *)f->esp;//记录在栈顶的系统调用类型type
if(type <= 0 || type >= max_syscall){
exit_special ();//类型错误,退出
}
syscalls[type](f);//类型正确,查找数组调用对应系统调用并调用执行
}
syscall_init ()
功能:初始化系统调用,通过syscall数组来存储13个系统调用,syscall_handler里通过识别数组的序号决定调用哪一个系统调用。
syscalls[SYS_HALT] = &sys_halt;
syscalls[SYS_EXIT] = &sys_exit;
syscalls[SYS_EXEC] = &sys_exec;
syscalls[SYS_WAIT] = &sys_wait;
syscalls[SYS_CREATE] = &sys_create;
syscalls[SYS_REMOVE] = &sys_remove;
syscalls[SYS_OPEN] = &sys_open;
syscalls[SYS_WRITE] = &sys_write;
syscalls[SYS_SEEK] = &sys_seek;
syscalls[SYS_TELL] = &sys_tell;
syscalls[SYS_CLOSE] =&sys_close;
syscalls[SYS_READ] = &sys_read;
syscalls[SYS_FILESIZE] = &sys_filesize;
sys_halt()
功能:调用shutdown_power_off
让pintos关机。
void
sys_halt (struct intr_frame* f)
{
shutdown_power_off();
}
sys_exit()
功能:结束当前的用户程序,并返回状态给内核kernel.
sys_exec ()
功能:检查由file_name指向的文件是否有效(调用check_ptr2
)。若有效,则调用process_execute
来去执行它。
void
sys_exec (struct intr_frame* f)
{
uint32_t *user_ptr = f->esp;
check_ptr2 (user_ptr + 1);
check_ptr2 (*(user_ptr + 1));
*user_ptr++;
f->eax = process_execute((char*)* user_ptr);
}
sys_wait()
功能:检查传入参数f是否有效。若有效则调用process_wait
来完成系统调用,等待一个子进程的结束。
在进程的执行过程中,execute()函数将返回-1,如果流程失败,则无法返回。为了解决这个问题,将'success'添加到结构'thread',以记录线程是否成功执行。此外,使用'parent'获取其父线程,并根据加载结果设置其状态。创建子进程时,它将关闭'sema'以阻止父进程。当子进程结束时,就会唤醒父进程。
在等待进程的过程中通过信号量实现等待进程),当父进程需要等待子进程时,调用'sema_down'来阻止父进程。
涉及到的文件:process.c thread.h synch.h 预期pass:sc-xxx create-xxx open-xxx close-xxx read-xxx write-xxx bad-xxx lg-xxx sm-xxx syn-xxx multi-xxx
这里详细介绍如何实现剩余10个系统调用。实际上,系统调用操作的所有关键部分都由filesys/filesys.c提供。但是project2并不需要修改filesys目录。所以任务三中重要的方面是正确弹出和获取栈中的数据,并注意不要同时进行写操作。
对 thread
添加新属性 与上述syscall.c相同
struct list files; /* 打开的文件列表 */
int file_fd; /*文件描述符 */
struct file * file_owned; /* 打开的文件 */
创建新数据结构thread_file
struct thread_file{
int fd; /* 文件描述数字 */
struct file* file; /* 文件指针 */
struct list_elem file_elem;
};
修改函数syscall_handler (struct intr_frame *)
修改函数syscall_init (void)
将初始化'syscalls',以存储此任务中的syscalls函数。
增加如下函数
void sys_create(struct intr_frame* f); //创建文件
void sys_remove(struct intr_frame* f); //删除文件
void sys_open(struct intr_frame* f); //打开文件
void sys_wait(struct intr_frame* f); //等待打开
void sys_filesize(struct intr_frame* f); //文件大小
void sys_read(struct intr_frame* f); //读文件`
void sys_write(struct intr_frame* f);//printf和写文件
void sys_seek(struct intr_frame* f); //移动文件指针`
void sys_tell(struct intr_frame* f); //文件指针位置
void sys_close(struct intr_frame* f); //关闭文件
增加函数is_valid_pointer(void* esp,uint8_t argc)
验证esp是否是一个虚拟地址并且是否已经加载到当前页表
增加函数 find_file_id(int file_id)
得到文件的标识符
增加函数 check_ptr2(const void *vaddr)
检查每个系统调用是否存在内存无效、页面无效和页面内容错误等错误。
添加全局文件锁,保证线程安全
static struct lock c;//执行文件操作时用锁来锁定线程
添加文件锁定功能
void acquire_lock_f(){
lock_acquire(&lock_f);
}
void release_lock_f(){
lock_release(&lock_f);
}
在这个任务中,我们需要实现另外9个与文件操作系统调用相关的系统调用:创建create
、删除remove
、打开open
、文件大小filesize
、读取read
、写入write
、查找seek
、通知tell
和关闭close
。当用户程序运行时,我们必须确保没有人可以修改磁盘上的可执行文件。对于这个任务,我们使用一个全局锁来确保文件syscalls是线程安全的。当调用每个系统调用时,它必须获取锁,然后释放锁。
此外,Pintos的文件系统不是线程安全的,文件操作syscalls不能同时调用多个文件系统函数。为了确保这一点,我们在'thread
'中添加了一个新变量'int file_fd
',以使文件描述符大于'STDIN_FILENO
'和'STDOUT_FILENO
','file_u owned
'用于保持线程打开的文件。
当系统调用出现时,由syscall_handler
()函数来判断进程如何继续执行。当用户程序使用 lib/user/syscall.c执行系统操作时,所有参数都被压入栈中。所以,程序只需要从栈中取出参数。从栈中弹出系统调用编号由栈指针esp来实现。
sys_create()
调用check_ptr2()
检查文件地址是否有效,通过acquire_lock_f()
获得锁,调用filesys_create()
创建文件,完成后通过release_lock_f()
释放锁。锁定过程将在其他文件系统操作中使用。
sys_remove()
检查当前指针是否有效(调用check_ptr2
),通过acquire_lock_f()
获得锁,调用filesys_remove()
删除当前文件,释放锁
sys_open()
检查当前指针是否有效(调用check_ptr2
),调用filesys_open()
打开当前文件,将数据结构为thread_file
的文件push到线程打开的文件列表中。关键代码如下:
acquire_lock_f ();//获得锁
struct file * file_opened = filesys_open((const char *)*user_ptr);//打开文件
release_lock_f ();//释放锁
struct thread * t = thread_current();
if (file_opened)
{
struct thread_file *thread_file_temp = malloc(sizeof(struct thread_file));
thread_file_temp->fd = t->file_fd++;
thread_file_temp->file = file_opened;
list_push_back (&t->files, &thread_file_temp->file_elem);
//push到线程打开的文件列表中
f->eax = thread_file_temp->fd;
}
else
{
f->eax = -1;
}
sys_wait()
等待,调用process_wait()
函数
sys_filesize()
调用file_length()
来得到文件长度
sys_read()
调用is_valid_pointer()
来验证地址的正确性。
若fd=0
,则使用input_getc()
从键盘中获取数据,送入缓冲区
若fd=1
,则表示读文件,调用file_read()
来实现。
sys_write()
设置一个变量 temp2
,判断是往缓冲区写入还是文件中写入。
temp2=1
,则表示写入到缓冲区中,调用putbuf()
函数实现。
temp2
不为1,则表示写入到文件中去。具体实现步骤,通过当前线程id,调用find_file_id()
找到文件,然后通过调用函数file_write()
来实现
sys_seek()
通过线程id,调用find_file_id()
找到文件,在调用 file_seek()
来移动文件指针。
sys_tell()
调用file_tell ()
哈数实现
sys_close()
调用file_close()
来实现,最后还要删除线程列表中的文件并释放文件
①所有文件操作都要先通过acquire_lock_f()
获得锁,执行文件操作之后,通过release_lock_f()
释放锁。
②文件操作都受到全局文件系统锁的保护,全局文件系统锁可以同时防止同一个fd上的I/O。
③当使用thread_current()->parent->children_list
或者thread_current()->opened_files
禁止中断,以防止不必要的错误产生
本次pintos实验,我完成了project1和project2。
project1实验难度较大,尤其是优先级捐赠这一部分,一开始的虚拟机配置环境就走了许多弯路,但是一整个实验做下来也让我对操作系统有了更深入的理解。实验一的线程(一开始以为看上去pintos并不支持多线程,这里的进程直接充当调度单位了,可能是因为核心态线程由内核直接管理)中虽然没有实用的进程在运行,但是让我看到了线程在操作系统中的调度方式,让我看到了同步的重要用途,让我看到了操作系统为了提高运行效率的一些手段。
project2主要让我们实现参数分离和系统调用这两大任务。参数分离需要对pintos的栈机制有一定的了解:用户空间是从高向低生长的,因此在参数压栈时要遵循“反压”的规则。
pintos这个项目让我们脱离了书本上刻板的知识,通过实践更好地理解OS这门课中的许多概念。比如项目中涉及到“优先级调度”、“信号量”、“锁”、“用户栈”、“中断”等等知识点,它们在pintos中串成了一个整体出现,这些概念都以看得见、摸得着的c代码的形式展现于眼前,是一个理解OS非常好的途径。
进程的基本状态反映了进程执行过程的变化。包括就绪状态、执行状态、阻塞状态、终止状态,分别对应了thread状态中的thread_ready、thread_running、thread_block、thread_dying。贯穿进程调度的整个过程,是进程调度的基础。
CPU调度算法-BSD,较好平衡了现场的不同需求,其中priority的根据recent_cpu、nice解决。其中recent_cpu是线程最近使用的CPU时间的估计值。近期recent_cpu越大优先级越低。
首先要分析问题,方案设计过程中要考虑实际数据要求,需要有创新精神与实践精神。
学习操作系统,可以提高CPU运行的效率,缩短完成任务的时间和成本,在一定程度上可以减少建造的材料费(处理机数量减少),节约成本,对社会和环境具有可持续发展。
结果: all pass 代码可以私我