English

시스템 호출 (System Calls)

  • 두 번째 프로젝트는 PintOS를 다음과 같이 대폭 업그레이드했습니다:
    • 자식 프로세스가 파일을 로드하고 실행할 수 있도록 허용.
    • 자식 프로세스로 인자 전달 기능 구현.
    • 견고한 동기화를 갖춘 시스템 호출 기능 추가.
  • 아래에서는 각 구성 요소에 대한 목표, 도전 과제, 해결책 및 구현을 간략하게 설명했습니다.

    overview

Part 1: 실행 파일 로딩 (Executable Loading)

목표

  • 이 섹션의 주요 목표는 process_wait() 함수를 재구현하여 부모 프로세스가 자식 프로세스와 안정적으로 동기화할 수 있도록 하는 것입니다.
    • 특히, 부모는 자식이 지정된 파일을 성공적으로 로드하고 실행할 때까지, 또는 자식이 종료될 때까지 기다릴 수 있어야 합니다.

현재 문제

  • PintOS의 원래 process_wait() 함수는 본질적으로 기능이 없었으며, 항상 -1을 반환하는 단순한 자리 표시자(placeholder) 역할만 했습니다.
  • 이러한 구현의 완전한 부재는 부모 프로세스가 자식과 동기화할 수 있는 메커니즘이 전혀 없었음을 의미했습니다.

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

원래 점수

project2_original

해결책

  • 이러한 한계를 극복하기 위해, process_wait() 함수는 다음과 같이 포괄적으로 재설계되었습니다:
    1. 부모-자식 계층 구조 유지:
      • struct thread 내의 연결 리스트 구조가 사용되어 각 부모에 대한 자식 프로세스의 명확한 계층 구조를 유지함으로써, 부모가 자신의 직계 자손을 추적할 수 있도록 합니다.
    2. 부모 대기 가능:
      • 이제 부모 프로세스는 지정된 자식 프로세스가 실행을 완료할 때까지 실행을 효과적으로 일시 중단할 수 있습니다.
    3. 동기화를 위한 세마포어 활용:
      • 세마포어가 핵심 동기화 기본 요소로 사용됩니다.
      • 이는 부모가 자식 프로세스가 종료된 후 또는 자식이 로딩 상태를 보고한 후에만 안정적으로 알림을 받고 실행을 재개할 수 있도록 보장합니다.

    part1

구현 세부 사항

  • 다음 구성 요소들이 수정되거나 도입되었습니다:
  • thread.h
    • struct thread
      • 자식 프로세스 관리 및 동기화를 지원하기 위해 struct thread에 새로운 멤버들이 추가되었습니다:
        • struct list children: 이 스레드에 의해 생성된 모든 자식 스레드의 struct list_elem childelem을 담는 리스트입니다.
        • struct list_elem childelem: 자식 스레드를 해당 부모의 children 리스트에 연결하는 데 사용되는 리스트 요소입니다.
        • struct semaphore sema_parent: 부모가 자식의 종료 상태를 기다리는 데 사용되는 세마포어입니다.
        • struct semaphore sema_child: 자식이 부모에게 자신의 종료를 알리는 데 사용되는 세마포어입니다.
        • int exit_status: 자식 프로세스의 종료 상태를 저장하는 정수입니다.
      • 이 새로운 멤버들은 이전 파트에서 donors가 처리된 것과 유사하게 init_thread() 함수 내에서 적절하게 초기화됩니다.
      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()
      • 이 함수는 자식 프로세스 추적 및 동기화를 위해 새로 추가된 데이터 멤버들, 특히 children, sema_parent, sema_child, 그리고 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()
      • 새 스레드(자식 프로세스)가 생성될 때, 해당 스레드는 이제 현재 스레드(부모)의 children 리스트에 추가되어 부모-자식 관계가 설정됩니다.
        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()
      • 스레드가 종료되기 전에, sema_up(&thread_current()->sema_child)를 사용하여 자신의 부모에게 신호를 보내, 자신이 종료되었음을 알립니다.
        • 그런 다음, 부모가 자신의 종료 상태를 완전히 회수할 때까지 sema_parent에서 대기하여 리소스를 완전히 파괴하기 전에 부모의 회수 완료를 보장합니다.
        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()
      • 이 함수는 부모 프로세스 대기를 담당합니다.
      • 현재 스레드(부모)의 children 리스트를 반복하여 일치하는 child_tid를 가진 자식을 찾습니다.
      • 일단 찾으면,
        • 자식은 리스트에서 제거되고, 부모는 sema_down(&child_thread->sema_child)를 호출하여 자식이 종료를 알릴 때까지 효과적으로 블록됩니다.
      • 자식이 종료된 후,
        • 부모는 자식의 exit_status를 회수하고 sema_up(&child_thread->sema_parent)를 통해 자식(이제 THREAD_DYING 상태)에게 신호를 보내 자식의 리소스가 완전히 할당 해제될 수 있도록 합니다.
      • 만약 child_tid가 직계 자식에 해당하지 않거나 이미 해당 자식에 대해 대기(wait)가 완료된 경우,
        • 단순히 -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;
      }
      

결론

  • 이러한 수정으로 부모 프로세스는 이제 자식의 종료를 안정적으로 대기하고 종료 상태를 회수할 수 있게 되어, 중요한 동기화 메커니즘이 확립되었습니다.
  • 하지만, 자식이 실제로 실행 파일을 로드하고 실행하는 능력인자 전달 기능은 프로젝트의 이후 파트에서 다루어질 예정입니다.

Part 2: 인자 전달 (Argument Passing)

목표

  • 이 섹션의 목표는 start_process() 함수를 향상시켜, 부모 프로세스로부터 새로 로드된 자식 실행 파일로 명령줄 인자를 올바르게 파싱하고 전달할 수 있도록 하는 것입니다.
  • 이를 통해 사용자 프로그램이 일반적인 운영체제의 명령줄 애플리케이션처럼 인자를 받고 해석할 수 있게 됩니다.

현재 문제

  • 원래의 start_process() 함수는 근본적인 한계점을 가지고 설계되었습니다:
    • file_name 인자가 오직 실행 파일의 이름만 포함한다고 가정했습니다.
  • 사용자(예: myprog arg1 arg2)가 제공한 추가 인자들은 완전히 무시되어, 사용자 프로그램이 명령줄 입력을 받고 처리하는 것을 불가능하게 만들었습니다.

    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 ();
    }
    

해결책

  • 견고한 인자 전달을 가능하게 하기 위해, start_process() 함수는 다음과 같은 핵심 단계들을 수행하도록 재구현되었습니다:
    1. 인자 파싱 (Argument Parsing):
      • 실행 파일 이름과 해당 인자들을 포함하는 입력 문자열을 파싱하여 개별 인자들을 추출합니다.
    2. 사용자 스택 조작 (User Stack Manipulation):
      • 파싱된 인자들은 새로 생성된 프로세스의 사용자 스택에 신중하게 푸시됩니다.
      • 이는 명령줄 인자에 대한 특정 스택 레이아웃을 규정하는 80x86 호출 규약(Calling Convention)을 준수합니다.
        • 이 규약은 인자들($argv[i]$)을 오른쪽에서 왼쪽으로 푸시하고, 그 뒤에 인자들의 시작 주소, 인자의 개수($argc$), 그리고 반환 주소(일반적으로 $0$)를 푸시하도록 요구합니다.
    3. 스택 정렬 및 패딩 (Stack Alignment and Padding):
      • 스택 상의 인자들이 4바이트 경계에 적절하게 정렬되도록 보장하는 것이 중요합니다.
      • 이는 x86 아키텍처에서 시스템 안정성과 성능에 필수적인 올바른 메모리 정렬을 유지하기 위해 종종 패딩 바이트를 추가하는 것을 포함합니다.

    part2

구현 세부 사항

  • 다음 구성 요소들이 수정되거나 도입되었습니다:
  • syscall.c
    • collapse_spaces()
      • 새로운 헬퍼 함수인 collapse_spaces()가 도입되었습니다.
        • 그 목적은 입력 인자 문자열에서 연속된 여러 개의 공백을 단일 공백으로 대체하여 문자열을 정규화하는 것입니다.
      • 이는 일관된 파싱을 보장하고 명령줄의 다양한 간격으로 인해 발생하는 문제를 방지합니다.
      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()
      • 이 함수는 인자 파싱 및 스택 설정을 처리하기 위해 상당한 수정을 거칩니다.
      • 먼저 입력 file_name_에 대해 collapse_spaces()를 호출합니다.
      • 그런 다음 strtok_r을 사용하여 문자열을 토큰화하고, 실행 파일 이름을 인자들과 분리하여 로컬 argv 배열에 저장합니다. argc는 인자의 개수를 추적합니다.
      • 원래의 load() 함수는 실행 파일 이름(argv[0])과 함께 호출됩니다.
      • 스택 구성: load가 성공하면, 함수는 아래(가장 높은 주소)에서 위(가장 낮은 주소)로 인자 스택 프레임을 구성하는 작업을 진행합니다.
        • 인자 문자열: argv의 각 인자 문자열은 사용자 스택에 복사됩니다. 스택 포인터(if_.esp)는 이에 따라 감소됩니다. 복사된 각 인자 문자열의 시작 주소는 arguments_starting_points에 저장됩니다.
        • 패딩: 인자 문자열 뒤에 스택 포인터가 4바이트 정렬되도록 패딩 바이트가 추가됩니다. 이는 sum_bits(인자 문자열의 총 길이)를 사용하여 (4 - sum_bits % 4) % 4로 계산됩니다.
        • Null argv[argc]: 사용자 프로그램을 위한 argv 배열의 끝을 표시하는 널 포인터(0)가 스택에 푸시됩니다.
        • argv 포인터: arguments_starting_points에 저장된 주소(스택 상의 실제 인자 문자열을 가리키는 포인터)들이 역순(즉, $argc - 1$부터 $0$까지)으로 스택에 푸시됩니다.
        • argv 기준 포인터: 첫 번째 argv 포인터(arguments_starting_points[0])의 주소가 푸시되며, 이는 main 함수로 전달될 char** argv 인자가 됩니다.
        • argc: 인자의 개수가 푸시됩니다.
        • 가짜 반환 주소: 가짜 반환 주소로 0이 푸시됩니다.
      • 마지막으로, 어셈블리 명령어가 사용되어 intr_exit 스텁으로 점프하며, 이는 구성된 struct intr_frame(수정된 if_.esp 포함)을 사용하여 사용자 프로세스의 초기 스택과 레지스터를 설정하게 됩니다.
        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 ();
        }
        

결론

  • start_process()에 대한 이러한 포괄적인 수정은 자식 프로세스가 명령줄 인자를 성공적으로 수신하고 처리할 수 있도록 하여, 그 기능과 상호작용 능력을 크게 향상시킵니다.
  • 이로써 인자 전달 문제는 해결되었지만, 이 인자들의 완전한 유용성, 특히 인자를 기반으로 시스템 호출을 실행하는 것은 여전히 견고한 시스템 호출 핸들러의 구현에 달려 있습니다 (Part 4에서 다룰 예정).

Part 3: 동기화 문제 (Synchronization Issues)

목표

  • 포괄적인 시스템 호출 기능 구현을 진행하기 전에, 동시 파일 작업 중에 발생할 수 있는 잠재적인 경쟁 조건(race condition)을 해결하는 것이 중요합니다.
  • 여기서의 주요 목표는 견고한 동기화 메커니즘을 도입하여 파일 시스템의 무결성일관성을 보장하는 것입니다.
    • 이는 전역 파일 시스템 락(filesys_lock)을 구현하고, 실행 파일에 대한 무단 또는 충돌하는 수정을 방지하기 위해 file_deny_write()를 전략적으로 활용하는 것을 포함합니다.

현재 문제

  • 기존 PintOS 커널은 치명적인 취약점을 안고 있었습니다:
    • 공유 파일 시스템 리소스를 보호하기 위한 전용 filesys_lock이 부족했습니다.
  • 또한, 실행 파일을 로드하는 역할을 하는 load() 함수는 파일이 로드된 후 file_deny_write()를 명시적으로 호출하지 않았습니다.
    • 이러한 간과는 로드된 실행 파일이 사용 중인 상태에서 다른 프로세스에 의해 잠재적으로 수정되거나 삭제될 수 있음을 의미했으며, 이로 인해 동시 파일 접근으로 인한 예측 불가능한 동작, 충돌 또는 보안 취약점이 발생할 수 있었습니다.
    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;
    }
    

해결책

  • 이러한 중요한 동기화 문제를 해결하기 위해 다음과 같은 해결책들이 구현되었습니다:
    1. filesys_lock 도입:
      • 전역 뮤텍스filesys_lock이 도입되었습니다.
      • 파일 시스템에 접근하거나 수정하는 모든 작업은 이제 이 락으로 보호되어 상호 배제를 보장하고 경쟁 조건을 방지합니다.
    2. load() 함수 수정:
      • load() 함수는 다음과 같이 업데이트되었습니다:
        • 실행 파일을 열고 읽기 전에 filesys_lock획득합니다.
        • 실행 파일을 성공적으로 로드한 직후 file_deny_write()를 호출합니다.
          • 이는 다른 프로세스가 현재 프로세스에서 사용 중인 파일에 쓰기 작업을 하는 것을 방지합니다.
        • 로드된 실행 파일에 대한 file_close() 호출이 지연되었습니다.
          • 로딩 직후 파일을 닫는 대신, 로드된 실행 파일을 나타내는 struct file 객체는 이제 스레드 구조체(current_running_file)에 저장되며, 프로세스가 종료될 때(process_exit())만 명시적으로 닫힙니다.
          • 이는 deny-write 상태가 실행 파일을 사용하는 프로세스의 전체 수명 동안 지속되도록 보장합니다.
        • 로딩 과정 후에는 성공 여부와 관계없이 filesys_lock해제합니다.
    3. process_execute() 강화:
      • process_execute()는 새 프로세스를 생성하기 전에 이제 지정된 file_name의 존재 여부와 접근 가능성을 확인하는 초기 검사를 수행합니다.
      • 이를 통해 존재하지 않거나 열 수 없는 파일에 대한 스레드 생성을 방지하고, 불필요한 자원 할당을 피하며 조기에 TID_ERROR를 반환합니다.

구현 세부 사항

  • 다음 구성 요소들이 수정되거나 도입되었습니다:
  • syscall.h
    • filesys_lock
      • struct lock filesys_lock에 대한 선언syscall.h에 추가되었습니다.
        • 이는 파일 시스템과 상호 작용하는 userprog 서브시스템의 모든 부분에서 이 락에 접근할 수 있도록 합니다.
      • 이 락은 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()
      • 커널 초기 단계에서 호출되는 syscall_init() 함수는 이제 lock_init(&filesys_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()
      • 파일 열기 및 로드 프로세스 동안 파일 시스템을 보호하기 위해 filesys_open() 전에 lock_acquire(&filesys_lock)이 추가되었습니다.
      • file이 성공적으로 열리고 로드되면,
        • 실행 파일 수정을 방지하기 위해 file_deny_write(file)이 호출됩니다.
      • done 레이블에 있던 file_close(file) 호출이 제거되었습니다.
        • 대신, 파일 포인터는 나중에 닫기 위해 t->current_running_file에 저장됩니다.
      • 함수에서 반환하기 전에 lock_release(&filesys_lock)이 호출됩니다.
        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()
      • 프로세스가 종료될 때, file_close(cur->current_running_file)가 이제 명시적으로 호출됩니다.
      • 이는 실행 파일에 대한 쓰기 금지(deny-write) 락을 해제하여, 프로세스가 더 이상 사용하지 않을 경우 파일이 수정되거나 삭제될 수 있도록 합니다.
        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()
      • 새 스레드를 생성하기 전에, process_execute()는 이제 filesys_lock을 획득하고 토큰(파싱된 프로그램 이름)으로 지정된 실행 파일에 대해 filesys_open()을 시도합니다.
      • 만약 filesys_open()NULL을 반환하면(파일이 존재하지 않거나 열 수 없음을 나타냄), 락이 해제되고, 할당된 페이지가 해제되며, 즉시 TID_ERROR가 반환됩니다.
        • 이는 유효하지 않은 프로세스의 생성을 방지합니다.
      • 열었던 current_file은 단순히 존재 여부를 확인하는 것이 목적이었으므로, 이 검사 직후 즉시 닫힙니다.
        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;
        }
        

결론

  • 이러한 중요한 동기화 강화는 파일 작업이 안전하고 일관되게 수행되도록 보장하여, 경쟁 조건을 방지하고 파일 시스템의 무결성을 유지합니다.
  • 공유 리소스를 filesys_lock으로 보호하고 실행 파일 쓰기 작업을 file_deny_write()로 관리함으로써, PintOS는 이제 동시 접근 문제에 대해 훨씬 더 견고해졌습니다.
  • 이러한 토대는 다음 파트의 핵심이 될 시스템 호출의 안정적인 구현을 위해 필수적입니다.

Part 4: 시스템 호출 구현 (System Call Implementation)

목표

  • 이 섹션의 주요 목표는 syscall_handler() 함수를 근본적으로 개선하여 광범위한 시스템 호출에 대한 포괄적인 지원을 제공하는 것입니다.
  • 이러한 시스템 호출은 사용자 프로세스가 커널과 상호 작용하는 데 필수적이며, 프로세스 생성, 종료, 다양한 파일 관리 작업과 같은 기능을 가능하게 합니다.

현재 문제

  • PintOS의 원래 syscall_handler()는 극도로 초보적이었습니다.
    • 단지 "system call!"이라는 디버깅 메시지를 출력한 다음 호출한 스레드를 즉시 종료시켰습니다.
    • 이 자리 표시자 구현은 사용자 프로그램이 기본 실행 및 종료 이상의 의미 있는 운영체제 상호 작용을 수행할 수 없도록 만들었습니다.
  • 시스템 호출 번호나 인자를 해석하는 메커니즘은 물론, 특정 처리 루틴으로 디스패치(dispatch)하는 기능조차 없었습니다.

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

해결책

  • PintOS를 더욱 기능적인 운영체제로 변모시키기 위해 syscall_handler()가 완전히 재구현되었습니다.
  • 수정된 접근 방식은 다음을 포함합니다:
    1. 인수 유효성 검사 및 구문 분석 (Argument Validation and Parsing):
      • 핸들러는 이제 사용자 스택(f->esp)의 주소를 엄격하게 검증하여 해당 주소가 사용자 주소 공간 내에 있고 매핑되었는지 확인합니다.
      • 그런 다음 인터럽트 프레임의 스택에서 시스템 호출 번호와 해당 인수를 직접 구문 분석합니다:
        • esp[0]은 요청된 커널 서비스에 대한 고유 식별자 역할을 하는 시스템 호출 번호로 해석됩니다.
        • 이후의 스택 위치(esp[1], esp[2], esp[3] 등)는 특정 시스템 호출에 대한 인수로 처리됩니다.
    2. 시스템 호출 디스패칭 (System Call Dispatching):
      • 추출된 시스템 호출 번호를 기반으로, 핸들러는 제어를 적절하고 전용인 시스템 호출 함수로 분배합니다.
      • 예: halt(), exit(), read(), write(), open()
    3. 반환 값 처리 (Return Value Handling):
      • 시스템 호출 함수의 반환 값은 인터럽트 프레임의 eax 레지스터(f->eax)에 신중하게 저장되어, 사용자 프로세스가 커널 요청 결과를 검색할 수 있도록 합니다.
    4. 종료 시 포괄적인 파일 닫기 (Comprehensive File Closure on Exit):
      • 프로세스가 종료될 때 해당 프로세스에 속한 모든 열린 파일 디스크립터가 올바르게 닫히도록 process_exit()에도 중요한 수정이 이루어졌습니다.
      • 이는 리소스 누수를 방지하고 파일 시스템 무결성을 유지합니다.

    part34

구현 세부 사항

  • 다음과 같은 구성 요소들이 수정되거나 도입되었습니다:
  • thread.h
    • FD_TABLE_MAX_SLOT 매크로가 32의 값으로 정의되었습니다.
      • 이는 단일 프로세스가 동시에 열 수 있는 최대 파일 디스크립터 수를 설정합니다.
    • struct thread
      • 파일 디스크립터 테이블을 관리하기 위해 새로운 멤버인 fd_tablenext_fd가 추가되었습니다.
        • struct file* fd_table[FD_TABLE_MAX_SLOT]:
          • 이 배열은 각 스레드(프로세스)의 파일 디스크립터 테이블 역할을 하며, 정수형 파일 디스크립터를 해당 커널 파일 객체에 매핑합니다.
        • int next_fd:
          • 주어진 스레드에 대해 다음에 할당 가능한 (가장 낮은 번호의) 파일 디스크립터를 추적하는 정수형 변수로, 새로운 FD 할당을 최적화합니다.
      • 이 새로운 멤버들은 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()
      • init_thread() 함수가 새로운 파일 디스크립터 관련 필드를 초기화하도록 업데이트되었습니다.
        • 구체적으로, t->next_fd3으로 설정되고 (표준 입력, 출력, 오류를 위해 0, 1, 2를 예약), t->fd_table의 모든 항목은 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()
      • 새로운 헬퍼 함수 is_valid_address()가 구현되었습니다.
      • 이 함수는 다음 두 가지 조건을 확인하여 핵심적인 유효성 검사를 수행합니다:
        • 주어진 포인터 ptr이 사용자 가상 주소 범위 내에 있는지 확인합니다 (is_user_vaddr() 사용).
          • is_user_vaddr()를 호출하기 위해서는 threads/vaddr.h를 포함해야 한다는 점에 주목할 필요가 있습니다.
        • 가상 주소 ptr이 현재 프로세스의 페이지 디렉터리에서 실제로 물리 페이지에 매핑되어 있는지 확인합니다 (pagedir_get_page() 사용).
      • 이 함수는 사용자 프로그램에 의한 세그먼테이션 오류 및 기타 메모리 접근 위반을 방지하는 데 결정적인 역할을 합니다.
        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()
      • 이 함수는 모든 시스템 호출(system calls)을 위한 중앙 디스패처(central dispatcher)입니다.
      • 먼저, 유효하지 않은 메모리 접근을 방지하기 위해 f->esp (스택 포인터)의 유효성을 검사합니다.
      • 이후, switch 문은 current_esp[0] (시스템 호출 번호)의 값에 따라 적절한 시스템 호출 핸들러로 디스패치합니다.
      • 시스템 호출을 위한 인자들은 current_esp[1], current_esp[2], current_esp[3] 등에서 직접 검색됩니다.
      • 시스템 호출 함수의 반환 값은 f->eax에 할당되며, 이는 인터럽트에서 복귀할 때 사용자 프로그램에 의해 확인될 수 있습니다.
        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()
      • PintOS를 종료합니다.
        Click to see the refined code
        void halt(void) {
          shutdown_power_off();
        }
        
    • exit()
      • 사용자 프로그램을 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()
      • 인자와 함께 새로운 실행 파일(executable)을 실행합니다.
        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()
      • 자식 프로세스를 기다리고 해당 프로세스의 exit_status를 검색합니다.
        Click to see the refined code
        int wait(int pid) {
          return process_wait(pid);
        }
        
    • create()
      • 지정된 크기의 파일생성합니다.
        • 성공 시 true를, 그렇지 않으면 false를 반환합니다.
        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()
      • 파일삭제합니다.
        • 성공 시 true를, 그렇지 않으면 false를 반환합니다.
        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).
        • 성공 시 current_fd를 반환하며, 그렇지 않으면 -1을 반환합니다.
        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()
      • 파일 디스크립터 fd로 열린 파일의 크기(size)를 바이트 단위로 반환합니다.
        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()
      • 파일 디스크립터 fd로 열린 파일에서 size 바이트만큼을 buffer읽습니다(Reads).
      • 실제로 읽은 바이트 수(파일의 끝에서는 0)를 반환하며, 파일을 읽을 수 없는 경우(파일의 끝 이외의 조건으로 인한 경우) -1을 반환합니다.
      • fd 0input_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()
      • 파일 디스크립터 fd로 열린 파일에 buffer로부터 size 바이트만큼을 씁니다(Writes).
        • 실제로 쓰인 바이트 수를 반환하며, 일부 바이트를 쓸 수 없었던 경우 size보다 작을 수 있습니다.
        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()
      • 열린 파일 fd에서 다음에 읽거나 쓸 바이트를 파일의 시작부터 position 바이트로 표현된 위치로 변경합니다.
        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()
      • 열린 파일 fd에서 다음에 읽거나 쓸 바이트의 위치를 파일의 시작부터 바이트로 표현하여 반환합니다.
        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()
      • 파일 디스크립터 fd닫습니다(Closes).
        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()
      • oom (메모리 부족) 테스트에 대해 정확한 동작을 보장하기 위해, process_execute()는 이제 자식 프로세스load() 작업을 완료할 때까지 기다립니다.
      • 자식이 자신의 로딩 상태를 알릴 때까지 차단하기 위해 sema_down(&child_thread->sema_child)를 사용합니다.
      • 자식의 로딩 성공 또는 실패는 child_thread->exit_status를 통해 전달되며, 이를 통해 자식이 로드에 실패하면 process_execute()TID_ERROR를 반환할 수 있습니다.
        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()
      • 자식 스레드에 의해 실행되는 이 함수는 이제 load()를 호출한 후 current_thread->exit_status에 자신의 로딩 상태 (success ? 0 : TID_ERROR)를 명시적으로 업데이트합니다.
      • 그런 다음 sema_up(&current_thread->sema_child)를 호출하여 부모에게 신호를 보내 로딩 결과를 알립니다.
      • 로드에 실패하면 즉시 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()
      • 이 함수는 프로세스 종료 시 포괄적인 자원 정리(resource cleanup)를 수행하도록 크게 확장되었습니다:
        • donors 리스트 정리:
          • donors 리스트에 남아 있는 모든 스레드 객체를 반복하여 제거함으로써, 적절한 할당 해제를 보장하고 메모리 누수를 방지합니다.
        • 자식 프로세스 대기:
          • 자신의 children 리스트를 반복하며 남아 있는 각 자식에 대해 process_wait()을 호출합니다.
          • 이는 부모가 종료하기 전에 모든 자식을 적절히 정리하도록 보장하여 고아 프로세스(orphaned processes)를 방지합니다.
        • 파일 디스크립터 테이블 정리:
          • fd_table을 반복하며 열려 있는 모든 struct file* 객체에 대해 file_close()를 호출하여, 프로세스가 열었던 모든 파일을 명시적으로 닫습니다.
        • 또한, 파트 3에서 논의된 대로 current_running_file (실행 파일 자체)도 명시적으로 닫습니다.
      • 파일 정리와 관련된 모든 작업이 수행되는 동안 filesys_lock을 획득하고 모든 정리가 완료된 후 해제하여, 종료 중 파일 시스템 작업의 원자성(atomicity)을 보장합니다.
        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
    • 이 수정 사항은 OS 기능 자체와는 관련이 없지만, 테스트를 통과하기 위해 필수적이라는 점에 주목할 필요가 있습니다.
    • kill()
      • 기본적으로 치명적인 오류가 발생하면 PintOS는 시스템을 패닉(panic)시키기 위해 kill()을 호출합니다.
      • 그러나 현재는 kill()이 시스템 패닉을 유발하는 대신 단순히 exit(-1)을 호출합니다.
      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()
      • 마찬가지로, 테스트를 통과하기 위해 printf() 문이 주석 처리되었습니다.
        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()
      • 마찬가지로, 테스트를 통과하기 위해 printf() 문이 주석 처리되었습니다.
        Click to see the refined code
        void
        exception_print_stats (void) 
        {
          //printf ("Exception: %lld page faults\n", page_fault_cnt);
          ;
        }
        

결론

  • PintOS는 이제 견고하고 기능적인 시스템 호출 집합을 지원하여, 사용자 프로그램이 프로세스 관리, 파일 생성, 읽기, 쓰기 및 커널과의 다양한 기타 상호 작용과 같은 복잡한 작업을 수행할 수 있게 되었습니다.
  • 이러한 구현은 적절한 인자 유효성 검사동기화와 결합되어 운영체제의 기능안정성을 크게 향상시키며, 더욱 정교한 사용자 응용 프로그램의 실행을 가능하게 합니다.

향상된 점수

project2_improved

최종 의견

  • 이러한 개선 사항들은 다음을 통해 PintOS를 근본적으로 더욱 유능하고 견고한 운영체제로 변모시켰습니다:
    • 수정된 프로세스 관리를 통한 다중 작업(multitasking) 환경 구축.
    • 명령줄 입력 활성화.
    • 견고한 동기화 메커니즘포괄적인 시스템 호출 집합을 통한 파일 시스템 무결성 보장.
  • 이 두 번째 프로젝트는 PintOS의 기능을 크게 확장하고, 자원 활용스레드 조정을 최적화하여, PintOS를 완전한 기능을 갖춘 운영체제 환경에 더욱 가깝게 만들었습니다.

Top

Leave a comment