[UCB operating system CS162 project] Pintos Lab2: User Programs User Programs (Part 2)

In the previous section, we have completed the parameter passing required by Lab 2 and the halt, exit and write output to stdout in the system call, and finally stopped before the implementation of wait. This section will start with wait and exec.

Syscall wait + exec: realize parent-child process

The wait requirement in the handout is like this, which is quite long:

insert image description here

It is not difficult to read, in order to realize wait, we must first complete the design of the parent-child process. The parent process should hold a list that records the information of all child processes, but the members of this list cannot be the child process itself, because even if the child process has ended, the parent process should still be able to query its information . So add a new data structure to thread.hthe :

/** Information of a thread's child */
struct child_entry
{
    
    
  tid_t tid;                          /**< Child's tid. */
  struct thread *t;                   /**< Pointer to child thread. Set to NULL when no longer alive. */
  bool is_alive;                      /**< Whether the child is still alive (not exited). */
  int exit_code;                      /**< Child's exit code. */
  bool is_waiting_on;                 /**< Whether the parent is waiting on the child. */
  struct semaphore wait_sema;         /**< Semaphore to let parent wait on the child. */
  struct list_elem elem;
};

These members are the information about the child process that the parent process needs to be able to obtain. In threadclass :

struct thread
{
    
    
  ...
  struct thread *parent;              /**< Thread's parent. */
  struct list child_list;             /**< Thread's children. Member type is child_entry. */
  struct child_entry *as_child;       /**< Thread itself's child_entry. This will be added 
                                           to its parent's child_list and is heap-allocated 
                                           so that it lives after the thread dies. */
  ...
};

Add a pointer to the parent process parent, list child_listand a pointer as_child. thread_createThe information of a process as a child process is mallocgenerated when it is created ( ), and is added to the parent process child_listand stored in its own as_childpointer . In this way, a process can easily update the information reported to the parent process. initialization:

static void
init_thread (struct thread *t, const char *name, int priority)
{
    
    
  ...
  list_init(&t->child_list); // as_child initialization will be done later, see thread_create()
  ...
}

tid_t
thread_create (const char *name, int priority,
               thread_func *function, void *aux) 
{
    
    
  ...
  /* Initialize thread. */
  init_thread (t, name, priority);
  tid = t->tid = allocate_tid ();

  /* Initialize child entry here because we just got a tid. */
  t->as_child = malloc(sizeof(struct child_entry));
  t->as_child->tid = tid;
  t->as_child->t = t;
  t->as_child->is_alive = true;
  t->as_child->exit_code = 0;
  t->as_child->is_waiting_on = false;
  sema_init(&t->as_child->wait_sema, 0);

  /* Link child and parent thread. */
  t->parent = thread_current();
  list_push_back(&t->parent->child_list, &t->as_child->elem);
}

With this information, we can process_wait()write : to search for itself child_list, if the target sub-thread does not exist, return -1; if it exists and the sub-thread has not ended, and has not waited, use the semaphore to block itself and wait for the sub-thread to finish running ; If the wait is over, return -1; if it is over, return what the child thread left behind exit_code.

int
process_wait (tid_t child_tid) 
{
    
    
  struct thread *t_cur = thread_current();
  
  struct list_elem *e;
  for (e = list_begin (&t_cur->child_list); e != list_end (&t_cur->child_list);
       e = list_next (e))
  {
    
    
    struct child_entry *entry = list_entry(e, struct child_entry, elem);
    if (entry->tid == child_tid) {
    
    
      if (!entry->is_waiting_on && entry->is_alive) {
    
    
        entry->is_waiting_on = true;
        sema_down(&entry->wait_sema); // wait for child process to exit
        return entry->exit_code;
      }
      else if (entry->is_waiting_on) {
    
     // already waiting on child
        return -1;
      }
      else {
    
     // child has terminated, retrieve exit_code
        return entry->exit_code;
      }
    }
  }
  // child_tid is not a child of current process
  return -1;
}

When a process ends, consider its relationship with the parent-child process . On the one hand, as a parent process, it should tell all its surviving child processes that it has exited ( entry->t->parentmarked with NULL). On the other hand, as a child process, if it is found that the parent process has exited, as_childthe recorded information will no longer be accessed and should be released. Otherwise, it should be updated as_child->exit_code(copy from its own exit_codeproperty , because it will be threadset when interacting with threads externally exit_code); if the parent process is waiting for the child process with a semaphore, up the semaphore; set it is_aliveto false and tset the pointer to itself to NULL.

This way of thinking is clear, child_entrythe information is recorded correctly, and it will not cause resource leakage.

void
thread_exit (void) 
{
    
    
  ...
  // as a parent, mark the parent of any child that hasn't exited as NULL
  for (e = list_begin (&t_cur->child_list); e != list_end (&t_cur->child_list);
       e = list_next (e))
  {
    
    
    struct child_entry *entry = list_entry(e, struct child_entry, elem);
    if (entry->is_alive) {
    
    
      entry->t->parent = NULL;
    }
  }
  // as a child, if the parent thread has exited, it's ok to free the as_child element
  if (t_cur->parent == NULL) {
    
    
    free(t_cur->as_child);
  } else {
    
     // otherwise, save our status as parent may visit it later (e.g., in wait())
    t_cur->as_child->exit_code = t_cur->exit_code;
    if (t_cur->as_child->is_waiting_on) {
    
    
      sema_up(&t_cur->as_child->wait_sema);
    }
    t_cur->as_child->is_alive = false;
    t_cur->as_child->t = NULL;
  }
  ...
}

In the syscall function of wait, process_waitjust :

static void 
syscall_wait(struct intr_frame *f)
{
    
    
  int pid = *(int *)(f->esp + ptr_size);
  f->eax = process_wait(pid);
}

With the basis of parent-child process, the implementation of exec is more natural. The lecture notes require:

insert image description here
Obviously the main body is the same as the creation of the main thread, call process_execute():

static void 
syscall_exec(struct intr_frame *f)
{
    
    
  char *cmd = *(char **)(f->esp + ptr_size);
  f->eax = process_execute(cmd);
}

However, there is a new requirement here that if the child process does not run normally, the parent process must return -1. In the current code, process_execute()the internal use returns after start_processcreating a new thread, without waiting for the new thread to run; and the reason why the child process may not run normally is start_processin : the executable program of the child process may fail because the file cannot be found, etc. Failed to load. So add another semaphore to let the parent process wait:

struct thread
{
    
    
  ...
  struct semaphore sema_exec;         /**< Semaphore for executing (spawning) a new process. 
                                           "UPed" after knowing whether the child has loaded 
                                           its executable successfully. */
  bool exec_success;                  /**< Whether new process successfully loaded its executable. */
  ...
};

tid_t
process_execute (const char *proc_cmd) 
{
    
    
  ...
  /* Create a new thread to execute PROC_CMD. */
  tid = thread_create (proc_name, PRI_DEFAULT, start_process, proc_cmd_copy2);
  ...

  sema_down(&thread_current()->sema_exec);
  if (!thread_current()->exec_success) {
    
    
    return -1;
  }
  thread_current()->exec_success = false; // reset the flag for next spawn

  return tid;
}

In fact, the previous code has already told us whether the loading of the child process is successful, which is successthe logo . The two cases of success and failure are to exec_successset the corresponding value and up the semaphore.

static void
start_process (void *proc_cmd_)
{
    
    
  ...
  success = load (proc_name, &if_.eip, &if_.esp);

  /* If load failed, quit. */
  if (!success) {
    
    
    ...
    thread_current()->as_child->is_alive = false;
    thread_current()->exit_code = -1;
    sema_up(&thread_current()->parent->sema_exec);
    thread_exit ();
  }
  ...
  /* load success. */
  thread_current()->parent->exec_success = 1;
  sema_up(&thread_current()->parent->sema_exec);
  ...
}

After this step, run a test and you will find that some tests related to exec and wait have been passed, but some fail, such as the following:
insert image description here
This is really too violent... so let's take a break and enter the next part of the implementation.

Belated Task 3: User memory access (pointer) check

Well, in fact, this part of the handout is before the System Call, but when I do it myself, the blogger is willing to do the logic of the normal case first so that the effect can be seen earlier, and then deal with special cases. Now is the time to do the latter.

In modern operating systems, in theory, no matter how messy the user is, his own program will crash, so that the operating system will not crash (blue screen warning doge). This requires the OS to have a checking function for user behavior, mainly memory access through pointers. The lecture notes tell us that when illegal memory access is found, the processing method is to immediately end the user process and release resources, and provides two optional detection methods:

insert image description here
The first is to do a legality check before accessing the memory of the user pointer: whether the address belongs to the user memory area (less than PHYS_BASE) and whether the address belongs to the memory area of ​​the current process; the second is to only do the former check and then access, if not Legal will cause a page fault, and then handle this exception. The latter is usually faster so it is more often used in actual systems, so we also take this path.

The lecture notes kindly gave us a method to return an error after an exception is raised due to illegal memory access: use the following function, exception.cand EAXcopy the value of EIPto after throwing an exception in and set it to 0xffffffff(-1):

/* Reads a byte at user virtual address UADDR.
   UADDR must be below PHYS_BASE.
   Returns the byte value if successful, -1 if a segfault
   occurred. */
static int
get_user (const uint8_t *uaddr)
{
    
    
  int result;
  asm ("movl $1f, %0; movzbl %1, %0; 1:"
       : "=&a" (result) : "m" (*uaddr));
  return result;
}

/* Writes BYTE to user address UDST.
   UDST must be below PHYS_BASE.
   Returns true if successful, false if a segfault occurred. */
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;
}

The lecture notes don't tell us the specific judgment conditions under which we should do this. Look at the code exception.cfor :


/** Page fault handler.  This is a skeleton that must be filled in
   to implement virtual memory.  Some solutions to project 2 may
   also require modifying this code.

   At entry, the address that faulted is in CR2 (Control Register
   2) and information about the fault, formatted as described in
   the PF_* macros in exception.h, is in F's error_code member.  The
   example code here shows how to parse that information.  You
   can find more information about both of these in the
   description of "Interrupt 14--Page Fault Exception (#PF)" in
   [IA32-v3a] section 5.15 "Exception and Interrupt Reference". */
static void
page_fault (struct intr_frame *f) 
{
    
    
  bool not_present;  /**< True: not-present page, false: writing r/o page. */
  bool write;        /**< True: access was write, false: access was read. */
  bool user;         /**< True: access by user, false: access by kernel. */
  void *fault_addr;  /**< Fault address. */

  /* Obtain faulting address, the virtual address that was
     accessed to cause the fault.  It may point to code or to
     data.  It is not necessarily the address of the instruction
     that caused the fault (that's f->eip).
     See [IA32-v2a] "MOV--Move to/from Control Registers" and
     [IA32-v3a] 5.15 "Interrupt 14--Page Fault Exception
     (#PF)". */
  asm ("movl %%cr2, %0" : "=r" (fault_addr));

  /* Turn interrupts back on (they were only off so that we could
     be assured of reading CR2 before it changed). */
  intr_enable ();

  /* Count page faults. */
  page_fault_cnt++;

  /* Determine cause. */
  not_present = (f->error_code & PF_P) == 0;
  write = (f->error_code & PF_W) != 0;
  user = (f->error_code & PF_U) != 0;


  /* To implement virtual memory, delete the rest of the function
     body, and replace it with code that brings in the page to
     which fault_addr refers. */
  printf ("Page fault at %p: %s error %s page in %s context.\n",
          fault_addr,
          not_present ? "not present" : "rights violation",
          write ? "writing" : "reading",
          user ? "user" : "kernel");
  kill (f);
}

Obviously, the exception handling return should be placed kill(f)before , so how to judge? Considering the several flags given in the original code, it is not difficult to find the key point user: the syscall causes an exception to be in the kernel state, and theoretically if the kernel code logic is correct, the kernel state should not generate a page fault in other cases . Therefore, if userit is false, it must be caused by illegal memory access by syscall. In this way, the judgment condition is found. The completion code is as follows:

  // user赋值之后:
  
  // The only chance that a page fault happens in kernel context is when dealing 
  // with user-provided pointer through system call, because kernel code shouldn't 
  // produce page faults (if we're writing it right...)
  if (!user) {
    
    
    f->eip = (void (*) (void)) f->eax;
    f->eax = -1;
    return;
  }
  
  kill(f);

By the way, read kill()the function , when the exception is caused by the user (for example, direct access NULL), it does not belong to the syscall, and the branch that will be SEL_UCSEGtaken directly causes the thread to exit. Because I exit_codeinitialized to 0 (normal exit), I should add a sentence here to exit_codechange to -1.

/** Handler for an exception (probably) caused by a user process. */
static void
kill (struct intr_frame *f) 
{
    
    
  /* The interrupt frame's code segment value tells us where the
     exception originated. */
  switch (f->cs)
    {
    
    
    case SEL_UCSEG:
      /* User's code segment, so it's a user exception, as we
         expected.  Kill the user process.  */
      printf ("%s: dying due to interrupt %#04x (%s).\n",
              thread_name (), f->vec_no, intr_name (f->vec_no));
      intr_dump_frame (f);
      thread_current()->exit_code = -1;
      thread_exit (); 
    ...
    }
}

After dealing with this, let's go back syscall.cand first add the two functions provided by the lecture notes. The function of these two functions is to check whether the reading and writing of a Byte is legal. The pointer provided by the user may point to data of various sizes , so write the following two functions to realize the pointer check of data of any size (if it is illegal, call it directly, terminate_processso that the thread exits with a return value of -1):

/** Check if a user-provided pointer is safe to read from. Return the pointer itself if safe, 
 * or call terminate_process() (which do not return) to kill the process with exit_code -1. */
static void * 
check_read_user_ptr(const void *ptr, size_t size)
{
    
    
  if (!is_user_vaddr(ptr)) {
    
    
    terminate_process();
  }
  for (size_t i = 0; i < size; i++) {
    
     // check if every byte is safe to read
    if (get_user(ptr + i) == -1) {
    
    
      terminate_process();
    }
  }
  return (void *)ptr; // remove const
}

/** Check if a user-provided pointer is safe to write to. Return the pointer itself if safe, 
 * or call terminate_process() (which do not return) to kill the process with exit_code -1. */
static void * 
check_write_user_ptr(void *ptr, size_t size)
{
    
    
  if (!is_user_vaddr(ptr)) {
    
    
    terminate_process();
  }
  for (size_t i = 0; i < size; i++) {
    
    
    if (!put_user(ptr + i, 0)) {
    
     // check if every byte is safe to write
      terminate_process();
    }
  }
  return ptr;
}

When parsing system call parameters, it can be used like this:

static void 
syscall_wait(struct intr_frame *f)
{
    
    
  int pid = *(int *)check_read_user_ptr(f->esp + ptr_size, sizeof(int));
  f->eax = process_wait(pid);
}

The same applies to other types. Don't forget that the entry's syscall number is also obtained by dereferencing a user pointer, also check:

static void
syscall_handler (struct intr_frame *f) 
{
    
    
  int syscall_type = *(int *)check_read_user_ptr(f->esp, sizeof(int));
  ...
}

A character string is a special case, its size cannot be obtained sizeof(char *)through , but \0ends with a character. Write a dedicated function for it:

/** Check if a user-provided string is safe to read from. Return the string itself if safe, 
 * or call terminate_process() (which do not return) to kill the process with exit_code -1. */
static char * 
check_read_user_str(const char *str)
{
    
    
  if (!is_user_vaddr(str)) {
    
    
    terminate_process();
  }

  uint8_t *_str = (uint8_t *)str;
  while (true) {
    
    
    int c = get_user(_str);
    if (c == -1) {
    
    
      terminate_process();
    } else if (c == '\0') {
    
     // reached the end of str
      return (char *)str; // remove const
    }
    ++_str;
  }
  NOT_REACHED();
}

When using, pay attention to check the pointer of the string first, and then check the string body:

static void 
syscall_exec(struct intr_frame *f)
{
    
    
  char *cmd = *(char **)check_read_user_ptr(f->esp + ptr_size, ptr_size);
  check_read_user_str(cmd);
  f->eax = process_execute(cmd);
}

So far, task 3 user fetch check task is completed.

Syscall: create, remove, open...: file system calls

Tears, Pintos conscientiously gave us a set of file system implementations, and did not let us write from scratch, so we mainly manage the thread file resources in this part, and the specific file operations are easy and pleasant package transfers (in filesys.hand middlefile.h ). Some special requirements mentioned in the handouts, such as the file system can also be deleted when opened, have been done for us, and no special judgment is required.

One of the requirements that needs to be implemented is that the file system does not have a synchronization mechanism for the time being, and needs to be locked by itself. I put locks filesys.hin all file system related calls to be guarded, for example:

static void 
syscall_create(struct intr_frame *f)
{
    
    
  char *file_name = *(char **)check_read_user_ptr(f->esp + ptr_size, ptr_size);
  check_read_user_str(file_name);
  unsigned file_size = *(unsigned *)check_read_user_ptr(f->esp + 2 * ptr_size, sizeof(unsigned));

  lock_acquire(&filesys_lock);
  bool res = filesys_create(file_name, file_size);
  f->eax = res;
  lock_release(&filesys_lock);
}

The process needs to manage the files it opens, and each file is described by an integer file descriptor and a file pointer. FD=0 logo STDIN, FD=1 logo STDOUT, other files start from No. 2, the allocation rules are arbitrary, I will make it grow naturally here. Define the structure file_entryand add the following members to threadthe class :

/** Information of a thread's opened file */
struct file_entry
{
    
    
  int fd;                             /**< File descriptor. */
  struct file *f;                     /**< Pointer to file. */
  struct list_elem elem;
};

struct thread
{
    
    
  ...
  struct file *exec_file;             /**< The executable file loaded by the thread. Opened upon*/
  struct list file_list;              /**< Files opened by the thread. Member type is file_entry. */
  int next_fd;                        /**< Next file descriptor to be allocated.
  ...
};

exec_fileExplained below. When opening a file, create a new file record and add it file_listto :

static void 
syscall_open(struct intr_frame *f)
{
    
    
  char *file_name = *(char **)check_read_user_ptr(f->esp + ptr_size, ptr_size);
  check_read_user_str(file_name);
  
  lock_acquire(&filesys_lock);
  struct file *opened_file = filesys_open(file_name);
  lock_release(&filesys_lock);

  if (opened_file == NULL) {
    
    
    f->eax = -1;
    return;
  }
  struct thread *t_cur = thread_current();
  struct file_entry *entry = malloc(sizeof(struct file_entry));
  entry->fd = t_cur->next_fd++;
  entry->f = opened_file;
  list_push_back(&t_cur->file_list, &entry->elem);
  f->eax = entry->fd;
}

Many of the parameters of file-related system calls are fd, and these operations must be performed on the opened file, so write a function that uses fd to find out whether the file is open:

/** Get pointer to a file entry owned by current process by its fd. 
 * Returns NULL if not found. */
static struct file_entry *
get_file(int fd)
{
    
    
  struct thread *t_cur = thread_current();
  struct list_elem *e;
  for (e = list_begin (&t_cur->file_list); e != list_end (&t_cur->file_list);
       e = list_next (e))
  {
    
    
    struct file_entry *entry = list_entry(e, struct file_entry, elem);
    if (entry->fd == fd) {
    
    
      return entry;
    }
  }
  return NULL;
}

Each system call can be processed according to the flow of parsing parameters to obtain fd → obtain file pointer according to fd → call file system function → write return value. Pay attention to the handling of and special cases in read and writeSTDINSTDOUT .

When close is called, the is removed file_listfrom file_entryand the resource of is released file_closeafter entry. When the thread exits, the files that still exist (unclosed) in the list should also be closed and entryblocks to avoid resource leaks.

void
thread_exit (void) 
{
    
    
  ...
  struct list_elem *e;
  // close remaining opened files
  while (!list_empty(&t_cur->file_list))
  {
    
    
    e = list_pop_front(&t_cur->file_list);
    struct file_entry *entry = list_entry(e, struct file_entry, elem);
    lock_acquire(&filesys_lock);
    file_close(entry->f);
    lock_release(&filesys_lock);
    free(entry);
  }
  ...
}

So far, the calls related to the file system are generally OK, but the last requirement is put forward in Task 5:

  • Add code to deny writes to files in use as executables.
    • Many OSes do this because of the unpredictable results if a process tried to run code that was in the midst of being changed on disk.
    • This is especially important once virtual memory is implemented in project 3, but it can’t hurt even now.

It makes sense that a process's own running executable should not be able to be modified. So add a special file pointer for each thread exec_file, open and store your own executable file start_processin the pointer, and call to file_deny_write()deny writing, process_exitand close it in (will automatically allow writing):

static void
start_process (void *proc_cmd_)
{
    
    
  ...
  lock_acquire(&filesys_lock);
  struct file *f = filesys_open(proc_name);
  file_deny_write(f);
  lock_release(&filesys_lock);
  thread_current()->exec_file = f;
  ...
}

void
process_exit (void)
{
    
    
  ...
  // close the executable file
  lock_acquire(&filesys_lock);
  file_close(t_cur->exec_file);
  lock_release(&filesys_lock);
  ...
|

Congratulations on reaching the end of Lab 2! The only thing to pay attention to is that multi-oomthis test point will attack your system as much as possible until its resources are exhausted (the execution will also be stuck for a while). If you are prompted that the execution depth is not enough, please search mallocand palloc_get_pageto check whether there is a possibility of resource leaks in the code you wrote, including the command line string allocated when starting the process .

insert image description here
The 100th blog post ~

insert image description here

Guess you like

Origin blog.csdn.net/Altair_alpha/article/details/127177624