Pintos project2 实验报告

project2是在src/userprog中进行代码修改,主要分为参数分离和系统调用者两大任务。

任务一 参数传递

用到的相干目录:pintos/src/userprog/process.c thread.h synch.h

在这个任务中,我们主要修改"process.c"和处理字符串。同时,为了测试我们的算法在此任务中的正确性,我们还必须在'syscall.c'中实现其他功能

预期pass:aggs-xxxx

具体修改如下:

  • 修改<process.c> :

    • 修改函数process_execute ()

    • 修改函数 start_process ()

    • 添加函数push_argument(void **esp,int argc, int argv[])

    • 修改函数process_wait ()

  • 修改<syscall.c>

    • 添加函数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

具体修改:

数据结构修改

  • <thread.h>

    • 创建新的一个结构 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;  /* 父线程*/

  • <syscall.c>  添加syscalls 结构,有如下属性

struct list files;                  /* 打开的文件列表 */
int file_fd;                        /* 文件描述符 */
struct file * file_owned;           /* 打开的文件*/

函数修改

  • <syscall.c>

    • 添加函数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.h>

  • thread添加新属性 与上述syscall.c相同

struct list files; /* 打开的文件列表  */
int file_fd; /*文件描述符 */
struct file * file_owned; /* 打开的文件 */

<syscall.h>

  • 创建新数据结构thread_file

struct thread_file{
    int fd;               /*  文件描述数字   */
    struct file* file;    /* 文件指针  */
    struct list_elem file_elem;
};

具体函数修改

  • <syscall.c>

    • 修改函数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)

      检查每个系统调用是否存在内存无效、页面无效和页面内容错误等错误。

  • <thread.c>

    • 添加全局文件锁,保证线程安全

    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来实现。

10个文件操作系统调用解释

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禁止中断,以防止不必要的错误产生

实验收获与心得

1)对专业知识基本概念、基本理论和典型方法的理解

本次pintos实验,我完成了project1和project2。

project1实验难度较大,尤其是优先级捐赠这一部分,一开始的虚拟机配置环境就走了许多弯路,但是一整个实验做下来也让我对操作系统有了更深入的理解。实验一的线程(一开始以为看上去pintos并不支持多线程,这里的进程直接充当调度单位了,可能是因为核心态线程由内核直接管理)中虽然没有实用的进程在运行,但是让我看到了线程在操作系统中的调度方式,让我看到了同步的重要用途,让我看到了操作系统为了提高运行效率的一些手段。

project2主要让我们实现参数分离和系统调用这两大任务。参数分离需要对pintos的栈机制有一定的了解:用户空间是从高向低生长的,因此在参数压栈时要遵循“反压”的规则。

2)怎么建立模型

pintos这个项目让我们脱离了书本上刻板的知识,通过实践更好地理解OS这门课中的许多概念。比如项目中涉及到“优先级调度”、“信号量”、“锁”、“用户栈”、“中断”等等知识点,它们在pintos中串成了一个整体出现,这些概念都以看得见、摸得着的c代码的形式展现于眼前,是一个理解OS非常好的途径。

3)如何利用基本原理解决复杂工程问题

进程的基本状态反映了进程执行过程的变化。包括就绪状态、执行状态、阻塞状态、终止状态,分别对应了thread状态中的thread_ready、thread_running、thread_block、thread_dying。贯穿进程调度的整个过程,是进程调度的基础。

CPU调度算法-BSD,较好平衡了现场的不同需求,其中priority的根据recent_cpu、nice解决。其中recent_cpu是线程最近使用的CPU时间的估计值。近期recent_cpu越大优先级越低。

4)具有实验方案设计的能力

首先要分析问题,方案设计过程中要考虑实际数据要求,需要有创新精神与实践精神。

5)如何对环境和社会的可持续发展

学习操作系统,可以提高CPU运行的效率,缩短完成任务的时间和成本,在一定程度上可以减少建造的材料费(处理机数量减少),节约成本,对社会和环境具有可持续发展。

结果: all pass 代码可以私我

猜你喜欢

转载自blog.csdn.net/m0_53112875/article/details/124790478
今日推荐