PintOS PJT2 - fork

김수환·2024년 11월 17일

PintOS

목록 보기
12/15

syscall.c

pid_t fork(const char *thread_name) {
    check_address(thread_name);

    return process_fork(thread_name, NULL);
}

process.c

/* Clones the current process as `name`. Returns the new process's thread id, or
 * TID_ERROR if the thread cannot be created. */
/** #Project 2: System Call - 현재 프로세스를 `name`으로 복제합니다. 새 프로세스의
 * 스레드 ID를 반환하거나 스레드를 생성할 수 없는 경우 TID_ERROR를 반환합니다.
 */

tid_t 
process_fork (const char *name, struct intr_frame *if_ UNUSED) {
    /* Clone current thread to new thread.*/

    thread_t *curr = thread_current();

    struct intr_frame *f = (pg_round_up(rrsp()) - sizeof(struct intr_frame));  // 현재 쓰레드의 if_는 페이지 마지막에 붙어있다.
    memcpy(&curr->parent_if, f, sizeof(struct intr_frame));                    // 1. 부모를 찾기 위해서 2. do_fork에 전달해주기 위해서

    /* 현재 스레드를 새 스레드로 복제합니다.*/
    tid_t tid = thread_create(name, PRI_DEFAULT, __do_fork, curr);

    if (tid == TID_ERROR)
        return TID_ERROR;

    thread_t *child = get_child_process(tid);

    sema_down(&child->fork_sema);  // 생성만 해놓고 자식 프로세스가 __do_fork에서 fork_sema를 sema_up 해줄 때까지 대기

    if (child->exit_status == TID_ERROR)
        return TID_ERROR;

    return tid;  // 부모 프로세스의 리턴값 : 생성한 자식 프로세스의 tid
}

process.c

static void 
__do_fork (void *aux) {
    struct intr_frame if_;
    struct thread *parent = (struct thread *) aux;
    struct thread *current = thread_current ();
    /* TODO: somehow pass the parent_if. (i.e. process_fork()'s if_) */
    struct intr_frame *parent_if = &parent->parent_if;
    bool succ = true;

    /* 1. Read the cpu context to local stack. */
    memcpy (&if_, parent_if, sizeof (struct intr_frame));
    if_.R.rax = 0;  // 자식 프로세스의 return값 (0)

    /* 2. Duplicate PT */
    current->pml4 = pml4_create();
    if (current->pml4 == NULL)
        goto error;

    process_activate (current);
#ifdef VM
    supplemental_page_table_init (&current->spt);
    if (!supplemental_page_table_copy (&current->spt, &parent->spt))
        goto error;
#else
    // Page Table 통째로 복제 
    if (!pml4_for_each (parent->pml4, duplicate_pte, parent))  
        goto error;
#endif

    /* TODO: Your code goes here.
     * TODO: Hint) To duplicate the file object, use `file_duplicate`
	 * TODO:       in include/filesys/file.h. Note that parent should not return
	 * TODO:       from the fork() until this function successfully duplicates
	 * TODO:       the resources of parent.
     * TODO: Hint) 파일 객체를 복제하려면 include/filesys/file.h에서 `file_duplicate`를 사용하세요.
         이 함수가 부모의 리소스를 성공적으로 복제할 때까지 부모는 fork()에서 반환되어서는 안 됩니다. */
    if (parent->fd_idx >= FDCOUNT_LIMIT)
        goto error;

    /** #Project 2: Extend File Descriptor - fd 복제 */
    struct dict_elem dup_file_dict[DICTLEN];
    int dup_idx = 0;

    current->fd_idx = parent->fd_idx;  // fdt 및 idx 복제
    struct file *file;
    for (int fd = 0; fd < FDCOUNT_LIMIT; fd++) {
        file = parent->fdt[fd];
        if (file == NULL)
            continue;

        bool is_exist = false;

        for (int i = 0; i <= dup_idx; i++) {
            if (dup_file_dict[i].key == file) {
                current->fdt[fd] = file_duplicate(file);
                is_exist = true;
                break;
            }
        }

        if (is_exist)
            continue;

        if (file > STDERR)
            current->fdt[fd] = file_duplicate(file);
        else
            current->fdt[fd] = file;

        if (dup_idx < DICTLEN) {
            dup_file_dict[dup_idx].key = file;
            dup_file_dict[dup_idx++].value = current->fdt[fd];
        }
        /** -------------------------------------------------------------- */
    }

    sema_up(&current->fork_sema);  // fork 프로세스가 정상적으로 완료됐으므로 현재 fork용 sema unblock

    process_init ();

    /* Finally, switch to the newly created process. */
    if (succ)
        do_iret (&if_);  // 정상 종료 시 자식 Process를 수행하러 감

error:
    sema_up(&current->fork_sema);  // 복제에 실패했으므로 현재 fork용 sema unblock
    exit(TID_ERROR);
}

process_fork__do_fork의 차이점

1. 역할과 호출 관계

  • process_fork
    • fork 시스템 호출을 처리하기 위한 함수입니다.
    • 주로 부모 프로세스의 정보를 기반으로 자식 프로세스를 생성합니다.
    • __do_fork를 호출하여 실제로 프로세스 복제를 수행합니다.
    • process_fork는 주로 초기화와 데이터 전달의 역할을 담당합니다.
  • __do_fork
    • process_fork에 의해 호출되며, 실제 자식 프로세스의 생성 및 초기화 작업을 수행합니다.
    • 부모 프로세스의 주소 공간(Page Table) 복제, 파일 디스크립터 복제, CPU 상태 초기화 등을 처리합니다.

2. 주요 기능

  • process_fork
    • 부모 프로세스의 상태를 기반으로 새로운 프로세스를 준비합니다.

    • 자식 프로세스를 위한 스레드 생성 요청을 수행하고, 해당 스레드가 실행되도록 설정합니다.

    • 부모 프로세스의 CPU 상태(struct intr_frame)를 __do_fork에 전달하여 자식 프로세스에서 이어 실행될 수 있도록 준비합니다.

      주요 흐름:

      c
      코드 복사
      tid_t process_fork(const char *name, struct intr_frame *if_) {
          // 현재 스레드 정보를 읽고 초기화
          struct thread *parent = thread_current();
      
          // 새 스레드를 생성하고 __do_fork 호출
          tid_t tid = thread_create(name, PRI_DEFAULT, __do_fork, parent);
      
          // 실패 시 에러 반환
          if (tid == TID_ERROR)
              return TID_ERROR;
      
          // 부모는 자식이 fork 작업을 완료할 때까지 대기
          sema_down(&parent->fork_sema);
          return tid;
      }
      
  • __do_fork
    • 새 스레드에서 실행되는 함수로, 자식 프로세스를 위한 구체적인 복제 작업을 수행합니다.

    • 부모의 메모리 구조(Page Table), 파일 디스크립터 테이블, CPU 상태 등을 복사하여 자식 프로세스를 초기화합니다.

    • 자식 프로세스는 복제가 끝난 뒤 새로운 실행 경로로 진입합니다.

      주요 흐름:

      c
      코드 복사
      static void __do_fork(void *aux) {
          struct thread *parent = (struct thread *)aux;
          struct thread *current = thread_current();
      
          // 부모의 CPU 상태 복제
          memcpy(&if_, &parent->parent_if, sizeof(struct intr_frame));
          if_.R.rax = 0; // 자식 프로세스의 반환 값 설정
      
          // 부모의 페이지 테이블 복제
          current->pml4 = pml4_create();
          if (!pml4_for_each(parent->pml4, duplicate_pte, parent))
              goto error;
      
          // 파일 디스크립터 복제
          for (int fd = 0; fd < FDCOUNT_LIMIT; fd++) {
              if (parent->fdt[fd])
                  current->fdt[fd] = file_duplicate(parent->fdt[fd]);
          }
      
          // 프로세스 전환
          do_iret(&if_);
      error:
          exit(TID_ERROR);
      }
      

3. 책임 영역

기능process_fork__do_fork
스레드 생성새로운 스레드를 생성새로운 스레드로 호출되어 실행
부모 데이터 전달부모의 CPU 상태(struct intr_frame)와 스레드 구조 전달전달받은 데이터를 기반으로 자식 프로세스 초기화
주소 공간 복제직접 수행하지 않음부모의 페이지 테이블 및 메모리 구조를 복제
파일 디스크립터 복제직접 수행하지 않음부모의 파일 디스크립터 테이블을 복제
CPU 상태 초기화초기화 작업 없음CPU 상태를 복원하고 자식 프로세스의 실행 경로 진입
부모-자식 동기화sema_down을 통해 부모가 대기작업 완료 후 sema_up으로 부모에게 작업 완료 신호 전송

4. 차이점 요약

  • process_fork:
    • 상위 레벨 함수로 새 스레드를 생성하고, 부모의 상태를 자식에게 전달하는 역할.
    • 스레드 생성 및 초기화만 처리하며, 복제 작업은 수행하지 않음.
  • __do_fork:
    • 하위 레벨 함수로 실제 자식 프로세스의 복제 작업을 수행.
    • 주소 공간(Page Table), 파일 디스크립터, CPU 상태 등을 복사하여 자식 프로세스를 완전히 초기화.

do_fork를 나누어 놓은 이유

do_fork를 분리하여 구현한 이유는 설계의 모듈화, 책임 분리, 그리고 코드의 가독성유지보수성을 높이기 위함입니다. 이러한 설계는 운영체제와 같은 복잡한 시스템에서 특히 중요합니다. 주요 이유를 다음과 같이 정리할 수 있습니다:


1. 책임 분리

  • process_fork:
    • 부모 프로세스의 상태를 확인하고, 새로운 스레드 생성 요청 및 동기화 작업을 담당합니다.
    • 시스템 호출의 인터페이스 역할을 하며, 상위 수준에서 자식 프로세스를 생성하는 관리적 역할에 초점을 맞춥니다.
  • __do_fork:
    • 부모 프로세스의 메모리, 파일 디스크립터, 주소 공간 등을 복사하고, 자식 프로세스 실행 준비를 수행합니다.
    • 구체적인 복제 작업과 자식 프로세스 초기화라는 하위 수준 작업을 처리합니다.

예:

  • process_fork는 부모 프로세스에서 호출하며 새로운 스레드 생성 요청을 합니다.
  • __do_fork는 새 스레드에서 호출되며 자식 프로세스의 주소 공간과 상태를 복제합니다.

이렇게 두 함수를 나눔으로써 각 함수가 단일 책임 원칙(Single Responsibility Principle)을 따를 수 있습니다.


2. 병렬 작업과 동기화 지원

  • 병렬 실행:
    • process_fork는 부모 프로세스에서 실행되고, __do_fork는 자식 프로세스에서 실행됩니다.
    • 이렇게 나누면 부모는 자식의 초기화가 완료될 때까지 기다릴 수 있고, 자식 프로세스의 복제 작업은 별도로 처리될 수 있습니다.
  • 동기화 지원:
    • sema_downsema_up과 같은 동기화 메커니즘을 활용하여 부모-자식 간의 작업 완료 여부를 명확히 처리할 수 있습니다.
    • process_fork는 부모가 자식이 준비될 때까지 대기하도록 설계됩니다.

3. 코드 가독성과 유지보수성

  • 복잡한 로직을 한 함수에 모두 포함하면 가독성이 떨어지고, 코드의 유지보수가 어려워집니다.
  • __do_fork를 별도로 분리함으로써:
    • 스레드 생성자식 프로세스의 복제 작업을 명확히 구분할 수 있습니다.
    • 각 함수의 역할과 기능을 이해하기 쉽게 만들고, 디버깅이나 확장이 용이해집니다.

4. 재사용성

  • process_fork와 *__do_fork는 서로 독립적으로 재사용될 수 있습니다.
    • 예를 들어, __do_fork는 다른 시스템 호출에서도 사용 가능하며, 다른 방식으로도 호출될 수 있습니다.
    • 프로세스 복제와 관련된 로직을 별도로 작성하면, 다른 기능에서 해당 로직을 재사용할 수 있어 중복 코드를 줄이고 유지보수를 쉽게 합니다.

5. 추상화 수준의 일치

  • process_fork:
    • 시스템 호출의 일환으로 부모 프로세스와의 상호작용을 담당하며, 고수준 추상화를 제공합니다.
  • __do_fork:
    • 구체적으로 메모리와 파일 디스크립터를 복제하고 CPU 상태를 초기화하며, 저수준 작업을 수행합니다.

예:

운영체제의 다른 부분에서 process_fork를 호출할 때 내부의 복잡한 구현을 알 필요 없이, 자식 프로세스를 생성하는 인터페이스로만 사용할 수 있습니다.


6. 확장성과 테스트 용이성

  • 확장성:
    • 새로운 기능(예: 메모리 할당 방식 변경, 파일 디스크립터 처리 변경)이 필요할 경우 __do_fork 내부만 수정하면 됩니다.
    • 시스템 호출 인터페이스(process_fork)는 영향을 받지 않으므로 안정성을 유지할 수 있습니다.
  • 테스트 용이성:
    • __do_fork는 독립적인 단위로 테스트할 수 있어, 프로세스 복제 로직에 문제가 없는지 검증하기 쉽습니다.

요약

process_fork__do_fork를 분리하면 다음과 같은 이점이 있습니다:

  1. 책임 분리: 각 함수가 명확한 역할을 수행.
  2. 동기화 및 병렬 작업 지원: 부모와 자식의 동작을 효과적으로 관리.
  3. 가독성과 유지보수성: 코드의 구조를 단순하고 이해하기 쉽게 유지.
  4. 재사용성: 복제 로직을 별도로 활용 가능.
  5. 확장성과 테스트 용이성: 변경과 검증이 쉽도록 설계.

이러한 설계는 운영체제와 같은 복잡한 소프트웨어 시스템에서 매우 유용합니다.

profile
juniorDev

0개의 댓글