System Calls

  • The second project significantly upgraded the PintOS by:
    • Enabling child processes to load and execute files.
    • Implementing argument passing to child processes.
    • Adding system call functionality with robust synchronization.
  • Below, I outlined the objectives, challenges, solutions, and implementations for each component.

Part 1: Executable Loading

Objective

  • The primary objective of this section is to reimplement the process_wait() function to enable a parent process to reliably synchronize with its child process.
    • Specifically, the parent must be able to wait until its child successfully loads and executes a specified file, or until the child terminates.

Current Problem

  • The original process_wait() function in PintOS was essentially non-functional, serving as a mere placeholder that always returned -1.
  • This complete lack of implementation meant that there was no mechanism for a parent process to synchronize with its children.

    Click to see the original code
    int
    process_wait (tid_t child_tid UNUSED) 
    {
      return -1;
    }
    

Original Grade

Description of image

Solution

  • To overcome these limitations, the process_wait() function has been comprehensively redesigned to:
    1. Maintain a Parent-Child Hierarchy:
      • A linked list structure within struct thread is used to maintain a clear hierarchy of child processes for each parent, allowing the parent to track its direct descendants.
    2. Enable Parent Waiting:
      • The parent process can now effectively suspend its execution until a specified child process completes its execution.
    3. Utilize Semaphores for Synchronization:
      • Semaphores are employed as the core synchronization primitive.
      • This ensures that the parent is reliably notified and can resume execution only after the child process has exited, or after the child has reported its loading status.

Implementation Details

  • The following components have been modified or introduced:
  • thread.h
    • struct thread
      • New members are added to struct thread to support child process management and synchronization:
        • struct list children: A list to hold the struct list_elem childelem of all child threads spawned by this thread.
        • struct list_elem childelem: A list element used to link a child thread into its parent’s children list.
        • struct semaphore sema_parent: A semaphore used by the parent to wait for the child’s exit status.
        • struct semaphore sema_child: A semaphore used by the child to signal its exit to the parent.
        • int exit_status: An integer to store the exit status of the child process.
      • These new members are properly initialized within the init_thread() function, similar to how donors were handled in the previous part.
        Click to see the refined code
          struct thread
          {
            /* Owned by thread.c. */
            tid_t tid;                          /* Thread identifier. */
            enum thread_status status;          /* Thread state. */
            char name[16];                      /* Name (for debugging purposes). */
            uint8_t *stack;                     /* Saved stack pointer. */
            int priority;                       /* Priority. */
            struct list_elem allelem;           /* List element for all threads list. */
        
            /* Shared between thread.c and synch.c. */
            struct list_elem elem;              /* List element. */
        
            int64_t tick_wakeup;                /* Tick till wake up */
            struct lock *lock_to_wait;          /* Address of the lock to wait */
            int original_priority;              /* Original Priority value */
            struct list donors;                 /* Doners for priority inversion */
            struct list_elem donelem;           /* List element for donors list */
        
        
          #ifdef USERPROG
            /* Owned by userprog/process.c. */
            uint32_t *pagedir;                  /* Page directory. */
        
            struct list children;               /* List of child processes */
            struct list_elem childelem;         /* List element for child processes */
            struct semaphore sema_parent;       /* Semaphore for waiting parent process*/
            struct semaphore sema_child;        /* Semaphore for waiting child process*/
            int exit_status;                    /* Exit status for exit() */
          #endif
        
            /* Owned by thread.c. */
            unsigned magic;                     /* Detects stack overflow. */
          };
        
  • thread.c
    • init_thread()
      • This function is updated to initialize the newly added data members for child process tracking and synchronization, specifically children, sema_parent, sema_child, and exit_status.
        Click to see the refined code
        static void
        init_thread (struct thread *t, const char *name, int priority)
        {
          // other codes
          list_init(&t->donors);
        
          #ifdef USERPROG
          list_init(&t->children);
          sema_init(&t->sema_parent, 0);
          sema_init(&t->sema_child, 0);
          t->exit_status = -1;
          #endif
        
          old_level = intr_disable ();
          list_push_back (&all_list, &t->allelem);
          intr_set_level (old_level);
        }
        
    • thread_create()
      • When a new thread (child process) is created, it is now added to the children list of the current (parent) thread, establishing the parent-child relationship.
        Click to see the refined code
        tid_t
        thread_create (const char *name, int priority,
                      thread_func *function, void *aux) 
        {
          struct thread *t;
          // other codes
          sf->ebp = 0;
        
          #ifdef USERPROG
          /* push child process to parent */
          list_push_back(&thread_current()->children, &t->childelem);
          #endif
        
          // other codes
        }
        
    • thread_exit()
      • Before a thread exits, it signals its parent using sema_up(&thread_current()->sema_child), notifying the parent that it has terminated.
      • It then waits on sema_parent to ensure the parent has retrieved its exit status before fully destroying its resources.
        Click to see the refined code
        void
        thread_exit (void) 
        {
          ASSERT (!intr_context ());
        
        #ifdef USERPROG
          sema_up(&thread_current()->sema_child);
          process_exit ();
          sema_down(&thread_current()->sema_parent);
        #endif
          /* Remove thread from all threads list, set our status to dying,
            and schedule another process.  That process will destroy us
            when it calls thread_schedule_tail(). */
          intr_disable ();
          list_remove (&thread_current()->allelem);
          thread_current ()->status = THREAD_DYING;
          schedule ();
          NOT_REACHED ();
        }
        
  • process.c
    • process_wait()
      • This function is responsible for the parent process waiting.
      • It iterates through the current thread’s children list to find the child with the matching child_tid.
      • Once found,
        • the child is removed from the list, and the parent calls sema_down(&child_thread->sema_child), effectively blocking until the child signals its exit.
      • After the child exits,
        • the parent retrieves the child’s exit_status and then signals the child (which is now in the THREAD_DYING state) via sema_up(&child_thread->sema_parent), allowing the child’s resources to be fully deallocated.
      • If the child_tid does not correspond to a direct child, or if the child has already been waited upon,
        • it simply returns -1.
        Click to see the refined code
        int
        process_wait (tid_t child_tid) 
        {
          struct thread *child_thread = NULL, *current_thread = thread_current();
          for(struct list_elem *current_element = list_begin(&current_thread->children), *end_element = list_end(&current_thread->children); current_element != end_element; current_element = list_next(current_element)) {
            struct thread *thread_current_element = list_entry(current_element, struct thread, childelem);
            if (thread_current_element->tid == child_tid) {
              child_thread = thread_current_element;
              list_remove(current_element);
              break;
            }
          }
          if(child_thread == NULL)
            return -1;
        
          sema_down(&child_thread->sema_child);
          int exit_status = child_thread->exit_status; 
          sema_up(&child_thread->sema_parent);
        
          return exit_status;
        }
        

Conclusion

  • With these modifications, the parent process can now reliably wait for its child’s termination and retrieve its exit status, establishing a crucial synchronization mechanism.
  • However, the child’s actual ability to load and execute an executable, along with the passing of arguments, remains to be addressed in subsequent parts of the project.

Part 2: Argument Passing

Objective

  • The objective of this section is to enhance the start_process() function to correctly parse and pass command-line arguments from the parent process to its newly loaded child executable.
  • This enables user programs to receive and interpret arguments, similar to how command-line applications work in conventional operating systems.

Current Problem

  • The original start_process() function was designed with a fundamental limitation:
    • it assumed that the file_name argument contained only the name of the executable.
  • Any additional arguments provided by the user (e.g., myprog arg1 arg2) were entirely ignored, making it impossible for user programs to receive and process command-line input.

    Click to see the original code
    static void
    start_process (void *file_name_)
    {
      char *file_name = file_name_;
      struct intr_frame if_;
      bool success;
    
      /* Initialize interrupt frame and load executable. */
      memset (&if_, 0, sizeof if_);
      if_.gs = if_.fs = if_.es = if_.ds = if_.ss = SEL_UDSEG;
      if_.cs = SEL_UCSEG;
      if_.eflags = FLAG_IF | FLAG_MBS;
      success = load (file_name, &if_.eip, &if_.esp);
    
      /* If load failed, quit. */
      palloc_free_page (file_name);
      if (!success) 
        thread_exit ();
    
      /* Start the user process by simulating a return from an
        interrupt, implemented by intr_exit (in
        threads/intr-stubs.S).  Because intr_exit takes all of its
        arguments on the stack in the form of a `struct intr_frame',
        we just point the stack pointer (%esp) to our stack frame
        and jump to it. */
      asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (&if_) : "memory");
      NOT_REACHED ();
    }
    

Solution

  • To enable robust argument passing, start_process() has been reimplemented to perform the following key steps:
    1. Argument Parsing:
      • The input string containing the executable name and its arguments is parsed to extract individual arguments.
    2. User Stack Manipulation:
      • The parsed arguments are then carefully pushed onto the user stack of the new process.
      • This adheres to the 80x86 Calling Convention, which dictates a specific stack layout for command-line arguments
        • which is to push the arguments (argv[i]) from right to left, followed by their starting addresses, the number of arguments (argc), and return address (usually 0).
    3. Stack Alignment and Padding:
      • It is crucial to ensure that the arguments on the stack are properly aligned to a 4-byte boundary.
      • This often involves adding padding bytes to maintain correct memory alignment, which is critical for system stability and performance on x86 architectures.

Implementation Details

  • The following components have been modified or introduced:
  • syscall.c
    • collapse_spaces()
      • A new helper function, collapse_spaces(), has been introduced.
        • Its purpose is to normalize the input argument string by replacing multiple consecutive spaces with a single space.
      • This ensures consistent parsing and prevents issues caused by variable spacing in the command line.
        Click to see the refined code
        void collapse_spaces(char *str) {
          char *source = str, *destination = str;
          bool in_space = false;
        
          while (*source) {
            if (*source == ' ') {
              if (in_space == false) {
                *destination++ = ' ';
                in_space = true;
              }
            } else {
              *destination++ = *source;
              in_space = false;
            }
            source++;
          }
        
          *destination = '\0';
        }
        
    • start_process()
      • This function undergoes significant modifications to handle argument parsing and stack setup.
      • It first calls collapse_spaces() on the input file_name_.
      • It then uses strtok_r to tokenize the string, separating the executable name from its arguments and storing them in a local argv array. argc tracks the number of arguments.
      • The original load() function is called with the executable name (argv[0]).
      • Stack Construction: If load is successful, the function proceeds to construct the argument stack frame from bottom to top (highest address to lowest address):
        • Argument Strings: Each argument string from argv is copied onto the user stack. The stack pointer (if_.esp) is decremented accordingly. The starting address of each copied argument string is stored in arguments_starting_points.
        • Padding: Padding bytes are added to ensure that the stack pointer is 4-byte aligned after the argument strings. This is calculated as (4 - sum_bits % 4) % 4, where sum_bits is the total length of the argument strings.
        • Null argv[argc]: A null pointer (0) is pushed onto the stack, marking the end of the argv array for the user program.
        • argv Pointers: The addresses stored in arguments_starting_points (pointers to the actual argument strings on the stack) are pushed onto the stack in reverse order (from argc - 1 down to 0).
        • argv Base Pointer: The address of the first argv pointer (arguments_starting_points[0]) is pushed, which will become the char** argv argument to main.
        • argc: The count of arguments is pushed.
        • Fake Return Address: A 0 is pushed as a fake return address.
      • Finally, an assembly instruction is used to jump to the intr_exit stub, which will then use the constructed struct intr_frame (including the modified if_.esp) to set up the user process’s initial stack and registers.
        Click to see the refined code
        static void
        start_process (void *file_name_)
        {
          /* Parse the first argument */
          collapse_spaces(file_name_);
          const int MAX_COUNT_ARGUMENT = 32;
          const int MAX_LENGTH_ARGUMENT = 64;
          char argv[MAX_COUNT_ARGUMENT][MAX_LENGTH_ARGUMENT];
          int argc = 0;
          char *saveptr, *token = strtok_r(file_name_, " ", &saveptr);
                
          while (token != NULL && argc < MAX_COUNT_ARGUMENT) {
            strlcpy(argv[argc], token, MAX_LENGTH_ARGUMENT);
                  
            ++argc;
            token = strtok_r(NULL, " ", &saveptr);
          }
          char *file_name = argv[0];
        
          /* Initialize interrupt frame and load executable. */
          struct intr_frame if_;
          bool success;
          struct thread *current_thread = thread_current();
        
        
          memset (&if_, 0, sizeof if_);
          if_.gs = if_.fs = if_.es = if_.ds = if_.ss = SEL_UDSEG;
          if_.cs = SEL_UCSEG;
          if_.eflags = FLAG_IF | FLAG_MBS;
          success = load (file_name, &if_.eip, &if_.esp);
        
        
          /* If load failed, quit. 
            it should be file_name_ not file_name, because file_name_ points to the allocated page */
          palloc_free_page (file_name_);    
          if (!success) 
            thread_exit ();
        
          /* Push arguments to interrupt frame with the 80x86 calling convention. 
            The registers in interrupt frame will be restored to user stack of the loaded executable  */
          int sum_bits = 0;
          int64_t arguments_starting_points[MAX_COUNT_ARGUMENT];
          for(int length, current_count = 0; current_count < argc; ++current_count) {
            length = strlen(argv[current_count]) + 1;
            if_.esp -= length;                 // decrement is needed because stack should be moved downward
            memcpy(if_.esp, argv[current_count], length);
            arguments_starting_points[current_count] = if_.esp;
            sum_bits += length;
          }  
        
          uint8_t padding_value = (4 - sum_bits % 4) % 4;
          if(padding_value != 0) {
            if_.esp -= padding_value;
            memset(if_.esp, 0, padding_value);      // use memset instead of memcpy
          }
                
          if_.esp -= sizeof(char*);
          memset(if_.esp, 0, sizeof(char*));
          for(int current_count = argc - 1; current_count > -1; --current_count) {
            if_.esp -= sizeof(char*);
            memcpy(if_.esp, &arguments_starting_points[current_count], sizeof(char*));        // store the actual physical memory address, not the address of local variable
          }
        
          int64_t start_point = if_.esp;
          if_.esp -= sizeof(char**);
          memcpy(if_.esp, &start_point, sizeof(char**));
          if_.esp -= sizeof(int);
          memcpy(if_.esp, &argc, sizeof(int));
                
          if_.esp -= sizeof(char*);
          memset(if_.esp, 0, sizeof(char*));
                
          /* Start the user process by simulating a return from an
            interrupt, implemented by intr_exit (in
            threads/intr-stubs.S).  Because intr_exit takes all of its
            arguments on the stack in the form of a `struct intr_frame',
            we just point the stack pointer (%esp) to our stack frame
            and jump to it. */
        
          asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (&if_) : "memory");
          NOT_REACHED ();
        }
        

Conclusion

  • This comprehensive modification to start_process() successfully enables child processes to receive and process command-line arguments, significantly enhancing their functionality and interaction capabilities.
  • While this resolves the argument passing problem, the full utility of these arguments, particularly for executing system calls based on them, still depends on the implementation of a robust system call handler (which will be addressed in Part 4).

Part 3: Synchronization Issues

Objective

  • Before proceeding with the implementation of comprehensive system call functionality, it is crucial to address potential race conditions that can arise during concurrent file operations.
  • The primary objective here is to ensure the integrity and consistency of the file system by introducing robust synchronization mechanisms.
    • This involves implementing a global file system lock (filesys_lock) and strategically utilizing file_deny_write() to prevent unauthorized or conflicting modifications to executable files.

Current Problem

  • The existing PintOS kernel suffered from a critical vulnerability:
    • it lacked a dedicated filesys_lock to protect shared file system resources.
  • Furthermore, the load() function, responsible for loading executables, did not explicitly call file_deny_write() after a file was loaded.
    • This oversight meant that a loaded executable could potentially be modified or deleted by another process while it was still being used, leading to unpredictable behavior, crashes, or security vulnerabilities due to concurrent file access.
    Click to see the original code
    bool
    load (const char *file_name, void (**eip) (void), void **esp) 
    {
      struct thread *t = thread_current ();
      // other codes
    
    done:
      /* We arrive here whether the load is successful or not. */
      file_close (file);
      return success;
    }
    

Solution

  • To resolve these critical synchronization issues, the following solutions have been implemented:
    1. Introduction of filesys_lock:
      • A global mutex, filesys_lock, has been introduced.
      • All operations that access or modify the file system are now protected by this lock, ensuring mutual exclusion and preventing race conditions.
    2. Modified load() Function:
      • The load() function has been updated to:
        • Acquire filesys_lock before attempting to open and read the executable file.
        • Call file_deny_write() immediately after successfully loading an executable.
          • This prevents other processes from writing to the file while it is still in use by the current process.
        • The file_close() call for the loaded executable has been delayed.
          • Instead of closing the file immediately after loading, the struct file object representing the loaded executable is now stored in the thread’s structure (current_running_file) and explicitly closed only when the process exits (process_exit()).
          • This ensures that the deny-write status persists for the entire lifetime of the process using the executable.
        • filesys_lock is released after the loading process, whether successful or not.
    3. Enhanced process_execute():
      • Before attempting to create a new process, process_execute() now performs an initial check to verify the existence and accessibility of the specified file_name.
      • This prevents the creation of threads for non-existent or unopenable files, returning TID_ERROR early and avoiding unnecessary resource allocation.

Implementation Details

  • The following components have been modified or introduced:
  • syscall.h
    • filesys_lock
      • A declaration for the struct lock filesys_lock has been added to syscall.h.
        • This makes the lock accessible to all parts of the userprog subsystem that interact with the file system.
      • It is explicitly initialized in syscall_init().
        Click to see the refined code
        #ifndef USERPROG_SYSCALL_H
        #define USERPROG_SYSCALL_H
        
        void syscall_init (void);
        struct lock filesys_lock;
        
        #endif /* userprog/syscall.h */
        
  • syscall.c
    • syscall_init()
      • The syscall_init() function, which is called early in the kernel’s initialization, now includes a call to lock_init(&filesys_lock) to properly initialize the file system lock.
        Click to see the refined code
        void
        syscall_init (void) 
        {
          intr_register_int (0x30, 3, INTR_ON, syscall_handler, "syscall");
          lock_init(&filesys_lock);
        }
        
  • process.c
    • load()
      • lock_acquire(&filesys_lock) is added before filesys_open() to protect the file system during the file opening and loading process.
      • If the file is successfully opened and loaded,
        • file_deny_write(file) is called to prevent modifications to the executable.
      • The file_close(file) call at the done label is removed.
        • Instead, the file pointer is stored in t->current_running_file for later closing.
      • lock_release(&filesys_lock) is called before returning from the function.
        Click to see the refined code
        bool
        load (const char *file_name, void (**eip) (void), void **esp) 
        {
          // other codes
        
          /* Open executable file. */
          lock_acquire(&filesys_lock);
          file = filesys_open (file_name);
          // other codes
        
        done:
          /* We arrive here whether the load is successful or not. */
          if(file != NULL)
            file_deny_write(file);      // call this instead of file_close()
          lock_release(&filesys_lock);
          t->current_running_file = file;
        
          return success;
        }
        
    • process_exit()
      • When a process exits, file_close(cur->current_running_file) is now explicitly called.
      • This releases the deny-write lock on the executable file, allowing it to be modified or deleted once the process is no longer using it.
        Click to see the refined code
        void
        process_exit (void)
        {
          struct thread *cur = thread_current ();
          uint32_t *pd;
        
          file_close(cur->current_running_file);
        
          /* Destroy the current process's page directory and switch back
            to the kernel-only page directory. */
          pd = cur->pagedir;
          if (pd != NULL) 
            {
              /* Correct ordering here is crucial.  We must set
                cur->pagedir to NULL before switching page directories,
                so that a timer interrupt can't switch back to the
                process page directory.  We must activate the base page
                directory before destroying the process's page
                directory, or our active page directory will be one
                that's been freed (and cleared). */
              cur->pagedir = NULL;
              pagedir_activate (NULL);
              pagedir_destroy (pd);
            }
        }
        
    • process_execute()
      • Before creating the new thread, ``process_execute() now acquires filesys_lock and attempts to filesys_open() the executable specified by token (the parsed program name).
      • If filesys_open() returns NULL (indicating the file doesn’t exist or can’t be opened), the lock is released, the allocated page is freed, and TID_ERROR is returned immediately.
        • This prevents the creation of invalid processes.
      • The opened current_file is immediately closed after this check, as its purpose was merely to verify existence.
        Click to see the refined code
        tid_t
        process_execute (const char *file_name) 
        {
          char *fn_copy;
          tid_t tid;
        
          /* Make a copy of FILE_NAME.
            Otherwise there's a race between the caller and load(). */
          fn_copy = palloc_get_page (0);
          if (fn_copy == NULL)
            return TID_ERROR;
          strlcpy (fn_copy, file_name, PGSIZE);
        
          /* Create a new thread to execute FILE_NAME. */
          const unsigned MAX_PROCESS_NAME_COUNT = 32;
          char new_process_name[MAX_PROCESS_NAME_COUNT];
          strlcpy(new_process_name, file_name, MAX_PROCESS_NAME_COUNT);
          char *saveptr, *token = strtok_r(new_process_name, " ", &saveptr);       // parse the first argument only to make it as name of the thread
        
          lock_acquire(&filesys_lock);
          struct file* current_file = filesys_open (token);
          if (current_file == NULL) {
            lock_release(&filesys_lock);
            palloc_free_page (fn_copy); 
            return TID_ERROR;
          }
          file_close(current_file);
          lock_release(&filesys_lock);
        
        
          tid = thread_create (token, PRI_DEFAULT, start_process, fn_copy);
          if (tid == TID_ERROR)
            palloc_free_page (fn_copy); 
                  
          return tid;
        }
        

Conclusion

  • These crucial synchronization enhancements ensure that file operations are performed safely and consistently, preventing race conditions and maintaining the integrity of the file system.
  • By protecting shared resources with filesys_lock and managing executable writes with file_deny_write(), PintOS is now far more robust against concurrent access issues.
  • This foundation is essential for the reliable implementation of system calls, which will be the focus of the next part.

Part 4: System Call Implementation

Objective

  • The primary objective of this section is to fundamentally enhance the syscall_handler() function to provide comprehensive support for a wide range of system calls.
  • These system calls are essential for user processes to interact with the kernel, enabling operations such as process creation, termination, and various file management tasks.

Current Problem

  • The original syscall_handler() in PintOS was extremely rudimentary.
    • It merely printed a debugging message "system call!" and then immediately terminated the calling thread.
    • This placeholder implementation rendered user programs largely incapable of performing any meaningful interactions with the operating system beyond basic execution and termination.
  • It lacked any mechanism to interpret system call numbers or arguments, let alone dispatch to specific handling routines.

    Click to see the original code
      static void
      syscall_handler (struct intr_frame *f UNUSED) 
      {
        printf ("system call!\n");
        thread_exit ();
      }
    

Solution

  • The syscall_handler() has been completely reimplemented to transform PintOS into a more functional operating system.
  • The revised approach includes:
    1. Argument Validation and Parsing:
      • The handler now rigorously validates addresses on the user stack (f->esp) to ensure they fall within the user’s address space and are mapped.
      • It then parses the system call number and its corresponding arguments directly from the interrupt frame’s stack:
        • esp[0] is interpreted as the system call number, which acts as a unique identifier for the requested kernel service.
        • Subsequent stack locations (esp[1], esp[2], esp[3], etc.) are treated as the arguments for the specific system call.
    2. System Call Dispatching:
      • Based on the extracted system call number, the handler dispatches control to the appropriate, dedicated system call function
        • e.g., halt(), exit(), read(), write(), open(), etc.
    3. Return Value Handling:
      • The return value from the system call function is carefully stored in the eax register of the interrupt frame (f->eax), allowing the user process to retrieve the result of its kernel request.
    4. Comprehensive File Closure on Exit:
      • A crucial modification has also been made to process_exit() to ensure that all open file descriptors belonging to a process are properly closed when that process terminates.
      • This prevents resource leaks and maintains file system integrity.

Implementation Details

  • The following components have been modified or introduced:
  • thread.h
    • A macro FD_TABLE_MAX_SLOT has been defined with a value of 32.
      • This sets the maximum number of file descriptors a single process can simultaneously have open.
    • struct thread
      • New members fd_table and next_fd have been added to manage the file descriptor table.
        • struct file* fd_table[FD_TABLE_MAX_SLOT]:
          • This array serves as the file descriptor table for each thread, mapping integer file descriptors to their corresponding kernel file objects.
        • int next_fd:
          • An integer that tracks the next available (lowest numbered) file descriptor for a given thread, optimizing the allocation of new FDs.
      • These new members are properly initialized within init_thread().
        Click to see the refined code
        #define FD_TABLE_MAX_SLOT 32
        
        struct thread
        {
            // other codes
            struct file* fd_table[FD_TABLE_MAX_SLOT];              /* File Descriptor table*/
            int next_fd;                                           /* Empty fd value for next file object */
            // other codes
        };
        
  • thread.c
    • init_thread()
      • The init_thread() function has been updated to initialize the new file descriptor related fields.
        • Specifically, t->next_fd is set to 3 (reserving 0, 1, and 2 for standard input, output, and error, respectively), and all entries in t->fd_table are initialized to NULL.
        Click to see the refined code
        static void
        init_thread (struct thread *t, const char *name, int priority)
        {
          // other codes
          list_init(&t->donors);
        
          #ifdef USERPROG
          list_init(&t->children);
          sema_init(&t->sema_parent, 0);
          sema_init(&t->sema_child, 0);
          t->exit_status = -1;
          t->next_fd = 3;
          for(unsigned current_index = 0; current_index < FD_TABLE_MAX_SLOT; ++current_index)
            t->fd_table[current_index] = NULL;
          t->current_running_file = NULL;
          #endif
        
          old_level = intr_disable ();
          list_push_back (&all_list, &t->allelem);
          intr_set_level (old_level);
        }
        
  • syscall.c
    • is_valid_address()
      • A new helper function, is_valid_address(), has been implemented.
      • This function performs critical validation by checking two conditions:
        • Whether the given ptr is within the user virtual address range (using is_user_vaddr()).
          • It’s worth noting that the inclusion of threads/vaddr.h is needed to call is_user_vaddr().
        • Whether the virtual address ptr is actually mapped to a physical page in the current process’s page directory (using pagedir_get_page()).
      • This function is crucial for preventing segmentation faults and other memory access violations by user programs.
        Click to see the refined code
        #include "threads/vaddr.h"
        // other codes
        bool is_valid_address(void *ptr)
        {
          if(ptr != NULL && is_user_vaddr(ptr) == true) {
            if(pagedir_get_page(thread_current()->pagedir, ptr) != NULL)
              return true;
          }
          return false;
        }
        
    • syscall_handler()
      • This function is the central dispatcher for all system calls.
      • It first validates the f->esp (stack pointer) to prevent access to invalid memory.
      • A switch statement then dispatches to the appropriate system call handler based on the value of current_esp[0] (the system call number).
      • Arguments for the system calls are retrieved directly from current_esp[1], current_esp[2], current_esp[3], etc.
      • The return value from the system call function is assigned to f->eax, which will be visible to the user program upon return from the interrupt.
        Click to see the refined code
          static void syscall_handler(struct intr_frame* f)
          {
              int *current_esp = f->esp;
        
              if(is_valid_address(current_esp) == false)
                  return;
        
              switch (current_esp[0])
              {
                  case SYS_HALT: halt(); break;
                  case SYS_EXIT: exit(current_esp[1]); break;
                  case SYS_EXEC: f->eax = exec(current_esp[1]); break;
                  case SYS_WAIT: f->eax = wait(current_esp[1]); break;
                  case SYS_CREATE: f->eax = create(current_esp[1], current_esp[2]); break;
                  case SYS_REMOVE: f->eax = remove(current_esp[1]); break;
                  case SYS_OPEN: f->eax = open(current_esp[1]); break;
                  case SYS_FILESIZE: f->eax = filesize(current_esp[1]); break;
                  case SYS_READ: f->eax = read(current_esp[1], current_esp[2], current_esp[3]); break;
                  case SYS_WRITE: f->eax = write(current_esp[1], current_esp[2], current_esp[3]); break;
                  case SYS_SEEK: seek(current_esp[1], current_esp[2]); break;
                  case SYS_TELL: f->eax = tell(current_esp[1]); break;
                  case SYS_CLOSE: close(current_esp[1]); break;
              }
          }
        
  • System Call Handlers
    • halt()
      • Shuts down PintOS.
        Click to see the refined code
        void halt(void) {
          shutdown_power_off();
        }
        
    • exit()
      • Terminates the user program with the exit_status.
        Click to see the refined code
        void exit(int status) {
          if(status < 0)
            status = -1;
          struct thread *current_thread = thread_current();
          printf("%s: exit(%d)\n", current_thread->name, status);
          current_thread->exit_status = status;
        
          thread_exit();
        }
        
    • exec()
      • Runs a new executable with arguments.
        Click to see the refined code
        int exec(const char *cmd_line) {
          if(is_valid_address(cmd_line) == false) 
            return -1;
        
          const unsigned MAX_PROCESS_NAME_COUNT = 32;
          char new_process_name[MAX_PROCESS_NAME_COUNT];
          strlcpy(new_process_name, cmd_line, MAX_PROCESS_NAME_COUNT);
          char *saveptr, *token = strtok_r(new_process_name, " ", &saveptr);  
          lock_acquire(&filesys_lock);
          struct file *temp_file = file_open(token);
          if(temp_file == NULL) {
            lock_release(&filesys_lock);
            return -1;
          }
          file_close(temp_file);
          lock_release(&filesys_lock);
        
          return process_execute(cmd_line);
        }
        
    • wait()
      • Waits for a child process and retrieves its exit_status.
        Click to see the refined code
        int wait(int pid) {
          return process_wait(pid);
        }
        
    • create()
      • Creates a file of specified size.
      • Returns true if successful; false otherwise.
        Click to see the refined code
        bool create(const char *file, unsigned initial_size) {
          if(is_valid_address(file) == false)
            exit(-1);
        
          lock_acquire(&filesys_lock);
          int return_value = filesys_create(file, initial_size);
          lock_release(&filesys_lock);
        
          return return_value;
        }
        
    • remove()
      • Deletes a file.
      • Returns true if successful; false otherwise.
        Click to see the refined code
        bool remove(const char *file) {
          lock_acquire(&filesys_lock);
          int return_value = filesys_remove(file);
          lock_release(&filesys_lock);
                
          return return_value;
        }
        
    • open()
      • Opens a file.
      • Returns a current_fd if successful; -1 otherwise.
        Click to see the refined code
        int open(const char *file) {
          if(is_valid_address(file) == false)
            exit(-1);
        
          struct thread *current_thread = thread_current();
          if(current_thread->next_fd == -1)
            return -1;
        
          lock_acquire(&filesys_lock);
          struct file *current_file = filesys_open(file);
          if(current_file == NULL) {
            lock_release(&filesys_lock);
            return -1;
          }
          lock_release(&filesys_lock);
        
          current_thread->fd_table[current_thread->next_fd] = current_file;
          int current_fd = current_thread->next_fd;
        
          while(true) {
            if(current_thread->fd_table[current_thread->next_fd] == NULL)
              break;
            ++current_thread->next_fd;
            if(current_thread->next_fd == FD_TABLE_MAX_SLOT) {
              current_thread->next_fd = -1;
              break;
            }
          }
        
          return current_fd;
        }
        
    • filesize()
      • Returns the size, in bytes, of the file open as fd.
        Click to see the refined code
        int filesize(int fd) {
          if(fd < 0 || fd >= FD_TABLE_MAX_SLOT || thread_current()->fd_table[fd] == NULL)
            exit(-1);
        
          lock_acquire(&filesys_lock);
          int return_value = file_length(thread_current()->fd_table[fd]);
          lock_release(&filesys_lock);
        
          return return_value;
        }
        
    • read()
      • Reads size bytes from the file open as fd into buffer.
      • Returns the number of bytes actually read (0 at end of file), or -1 if the file could not be read (due to a condition other than end of file).
      • fd 0 reads from the keyboard using input_getc().
        Click to see the refined code
        int read(int fd, void *buffer, unsigned size) {
          if(fd < 0 || fd >= FD_TABLE_MAX_SLOT || thread_current()->fd_table[fd] == NULL || is_valid_address(buffer) == false || size < 0)
            exit(-1);
        
          if(fd == STDIN_FILENO)
            return input_getc();
          else {
            lock_acquire(&filesys_lock);
            int return_value = file_read(thread_current()->fd_table[fd], buffer, size);
            lock_release(&filesys_lock);
        
            return return_value;
          }
        }
        
    • write()
      • Writes size bytes from buffer to the open file fd.
      • Returns the number of bytes actually written, which may be less than size if some bytes could not be written.
        Click to see the refined code
        int write(int fd, const void *buffer, unsigned size) {
          if(is_valid_address(buffer) == false || size < 0)
            exit(-1);
        
          if(fd == STDOUT_FILENO)
            putbuf(buffer, size);
          else {
            if(fd < 0 || fd >= FD_TABLE_MAX_SLOT || thread_current()->fd_table[fd] == NULL)
              exit(-1);
        
            lock_acquire(&filesys_lock);
            int return_value = file_write(thread_current()->fd_table[fd], buffer, size);
            lock_release(&filesys_lock);
        
            return return_value;
          }
          return size;
        }
        
    • seek()
      • Changes the next byte to be read or written in open file fd to position, expressed in bytes from the beginning of the file.
        Click to see the refined code
        void seek(int fd, unsigned poisiton) {
          if(fd < 0 || fd >= FD_TABLE_MAX_SLOT || thread_current()->fd_table[fd] == NULL)
            exit(-1);
        
          lock_acquire(&filesys_lock);
          file_seek(thread_current()->fd_table[fd], poisiton);
          lock_release(&filesys_lock);
        }
        
    • tell()
      • Returns the position of the next byte to be read or written in open file fd, expressed in bytes from the beginning of the file.
        Click to see the refined code
        unsigned tell(int fd) {
          if(fd < 0 || fd >= FD_TABLE_MAX_SLOT || thread_current()->fd_table[fd] == NULL)
            exit(-1);
        
          lock_acquire(&filesys_lock);
          int return_value = file_tell(thread_current()->fd_table[fd]);
          lock_release(&filesys_lock);
          return return_value;
        }
        
    • close()
      • Closes file descriptor fd.
        Click to see the refined code
        void close(int fd) {
          struct thread *current_thread = thread_current();
          if(fd < 0 || fd >= FD_TABLE_MAX_SLOT || current_thread->fd_table[fd] == NULL)
            exit(-1);
        
          lock_acquire(&filesys_lock);
          file_close(current_thread->fd_table[fd]);
          lock_release(&filesys_lock);
        
          current_thread->fd_table[fd] = NULL;
          current_thread->next_fd = fd;
        }
        
  • process.c
    • process_execute()
      • To ensure correct behavior for the oom test (out of memory), process_execute() now waits for the child process to complete its load() operation.
      • It uses sema_down(&child_thread->sema_child) to block until the child signals its loading status.
      • The child’s loading success or failure is communicated via child_thread->exit_status, allowing process_execute() to return TID_ERROR if the child fails to load.
      Click to see the refined code
        tid_t
        process_execute (const char *file_name) 
        {
          // other codes
      
          tid = thread_create (token, PRI_DEFAULT, start_process, fn_copy);
          if (tid == TID_ERROR)
            palloc_free_page (fn_copy); 
      
          // respect the result of load()
          struct thread *child_thread = list_entry(list_back(&thread_current()->children), struct thread, childelem);
          sema_down(&child_thread->sema_child);
          if(child_thread->exit_status == TID_ERROR)
            return TID_ERROR;
      
          return tid;
        }
      
    • start_process()
      • This function, executed by the child thread, now explicitly updates its loading status (success ? 0 : TID_ERROR) in current_thread->exit_status after calling load().
      • It then signals its parent calling sema_up(&current_thread->sema_child), notifying the parent of the loading outcome.
      • If load fails, it immediately calls thread_exit().
      Click to see the refined code
        static void
        start_process (void *file_name_)
        {
          // other codes
          success = load (file_name, &if_.eip, &if_.esp);
      
      
          /* If load failed, quit. 
            it should be file_name_ not file_name, because file_name_ points to the allocated page */
          palloc_free_page (file_name_);    
              
          // Update loading status to parent thread
          current_thread->exit_status = success ? 0 : TID_ERROR;
          sema_up(&current_thread->sema_child);
          if (!success)
            thread_exit();
              
          // other codes
        }
      
    • process_exit()
      • This function has been significantly expanded to perform comprehensive resource cleanup upon process termination:
        • Donors List Cleanup:
          • It iterates through and removes all remaining thread objects from the donors list, ensuring proper deallocation and preventing memory leaks.
        • Child Process Waiting:
          • It iterates through its children list and calls process_wait() for each remaining child.
          • This ensures that the parent properly clears all its children before exiting, preventing orphaned processes.
        • File Descriptor Table Cleanup:
          • It iterates through the fd_table and calls file_close() for every open struct file* object, explicitly closing all files opened by the process.
        • It also explicitly closes the current_running_file (the executable itself) as discussed in Part 3.
      • filesys_lock is acquired before closing files and released after all file-related cleanup is done, ensuring atomicity of file system operations during exit.
        Click to see the refined code
        void
        process_exit (void)
        {
          struct thread *cur = thread_current ();
          uint32_t *pd;
        
          for(struct list_elem *current_element = list_begin(&cur->donors), *end_element = list_end(&cur->donors), *next_element; current_element != end_element;) {
            next_element = list_next(current_element);
            struct thread *thread_current_element = list_entry(current_element, struct thread, donelem);
            list_remove(current_element);
            current_element = next_element;
          }
        
          for(struct list_elem *current_element = list_begin(&cur->children), *end_element = list_end(&cur->children), *next_element; current_element != end_element;) {
            next_element = list_next(current_element);
            struct thread *thread_current_element = list_entry(current_element, struct thread, childelem);
            process_wait(thread_current_element->tid);
            current_element = next_element;
          }
        
          if(lock_held_by_current_thread(&filesys_lock) == false)
            lock_acquire(&filesys_lock);
        
          /* deallocate opened file objects */
          for(--cur->next_fd; cur->next_fd > -1; --cur->next_fd)
            file_close(cur->fd_table[cur->next_fd]);
        
          file_close(cur->current_running_file);
        
          /* Destroy the current process's page directory and switch back
            to the kernel-only page directory. */
          pd = cur->pagedir;
          if (pd != NULL) 
          {
            /* Correct ordering here is crucial.  We must set
                cur->pagedir to NULL before switching page directories,
                so that a timer interrupt can't switch back to the
                process page directory.  We must activate the base page
                directory before destroying the process's page
                directory, or our active page directory will be one
                that's been freed (and cleared). */
            cur->pagedir = NULL;
            pagedir_activate (NULL);
            pagedir_destroy (pd);
          }
          lock_release(&filesys_lock);
        }
        
  • exception.c
    • It’s worth noting that this modification is not related to the OS feature but is necessary to pass the tests.
    • kill()
      • By default, when a critical error occurs, PintOS calls kill() to panic the system.
      • However, for now, kill() simply calls exit(-1) instead of triggering a system panic.
        Click to see the refined code
        static void
        kill (struct intr_frame *f) 
        {
          /* This interrupt is one (probably) caused by a user process.
            For example, the process might have tried to access unmapped
            virtual memory (a page fault).  For now, we simply kill the
            user process.  Later, we'll want to handle page faults in
            the kernel.  Real Unix-like operating systems pass most
            exceptions back to the process via signals, but we don't
            implement them. */
                  
          /* 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_exit (); 
        
            case SEL_KCSEG:
              // Kernel's code segment, which indicates a kernel bug.
              // Kernel code shouldn't throw exceptions.  (Page faults
              // may cause kernel exceptions--but they shouldn't arrive
              // here.)  Panic the kernel to make the point.  
              intr_dump_frame (f);
              PANIC ("Kernel bug - unexpected interrupt in kernel"); 
        
            default:
              // Some other code segment?  Shouldn't happen.  Panic the kernel. 
              printf ("Interrupt %#04x (%s) in unknown segment %04x\n",
                    f->vec_no, intr_name (f->vec_no), f->cs);
              thread_exit ();
            }
              */
          exit(-1);
        }
        
    • page_fault()
      • Similarly, the printf() statement has been commented out to pass the tests.
        Click to see the refined code
        static void
        page_fault (struct intr_frame *f) 
        {
          // other codes
          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);
        }
        
    • exception_print_stats()
      • Likewise, the printf() statement has been commented out to pass the tests.
        Click to see the refined code
        void
        exception_print_stats (void) 
        {
          //printf ("Exception: %lld page faults\n", page_fault_cnt);
          ;
        }
        

Conclusion

  • PintOS now supports a robust and functional set of system calls, enabling user programs to perform complex operations like process management, file creation, reading, writing, and various other interactions with the kernel.
  • These implementations, coupled with proper argument validation and synchronization, significantly enhance the capabilities and stability of the operating system, allowing for the execution of more sophisticated user applications.

Improved Grade

Description of image

Final Thoughts

  • These enhancements fundamentally transformed PintOS into a significantly more capable and robust operating system by:
    • Establishing multitasking through revised process management.
    • Enabling command-line input.
    • Ensuring file system integrity with robust synchronization mechanisms and a comprehensive suite of system calls.
  • This second project significantly expanded PintOS’s features, optimizing resource utilization and thread coordination, bringing it closer to a fully functional operating system environment.

Top

Leave a comment