[TIL/크래프톤 정글] DAY 75

배재준·2025년 5월 23일

크래프톤 정글 - TIL

목록 보기
67/93
post-thumbnail

2025.05.23

TIL(TODAY I LEARN)


  • 오늘한 내용 : PintOS - Project2: UserProgram - System calls
    System calls 구현 (fork), 다시 전체 흐름 공부
  • WEEK 11 : 정글 끝까지(PintOS) - UserProgram

System calls

process_fork()

  • 동작 흐름
  1. 유저 모드 컨텍스트 복제

    • 부모의 struct intr_frame if_를 페이지 단위로 복사 (palloc_get_page)
    • 복사본의 R.rax = 0 으로 설정 → 자식 fork()에서 0 반환
  2. 자식 상태 관리 구조체 준비

    struct child_status {
      tid_t tid;            /* 자식 TID */
      int exit_status;      /* 자식이 exit()에서 넘긴 상태 코드 */
      bool has_exited;      /* exit() 호출 여부 */
      struct semaphore sema;/* 부모 대기를 위한 세마포어 */
      struct list_elem elem;/* 부모의 children 리스트 연결자 */
    };
    • sema_init(&c->sema, 0)으로 부모가 sema_down하면 블록
  3. 자식 스레드 생성

    • thread_create(name, PRI_DEFAULT, __do_fork, child_if) 호출
    • 시스템 콜로 뜯어낸 name 을 그대로 쓰레드 이름으로 사용
  4. 부모–자식 관계 설정

    c->tid = child_tid;
    thread_by_tid(child_tid)->parent_tid = parent->tid;
    • thread_by_tid()로 자식 스레드 구조체를 찾아 parent_tid 설정
    • 이 정보로 이후 process_exit()에서 부모를 깨울 수 있음
  5. 부모 동기화 (semaphore)

    • 부모는 sema_down(&c->sema)로 자식 복제 완료(__do_fork() 안에서 sema_up) 시점까지 블록
    • 이를 통해 주소 공간·파일 디스크립터 복제가 완전해지기 전 부모가 리턴하지 않음
  6. 리턴

    • process_fork()는 자식 TID를 반환
    • 자식은 __do_fork() 안의 do_iret(&child_if)를 통해 사용자 모드로 복귀하며 fork()에서는 0을 반환
  7. 자식 스레드 본체: __do_fork()

  • 새 PML4 생성
    current->pml4 = pml4_create();
    process_activate(current);
  • 페이지 테이블 복제
    • VM 꺼진 경우:
      pml4_for_each(parent->pml4, duplicate_pte, parent);
  • 부모 깨우기 sema_up(&c->sema);
  • 사용자 모드 복귀 do_iret(&child_if);
  1. 페이지 복제 핵심: duplicate_pte()

    duplicate_pte()가 부모 PML4를 한 페이지씩 순회하며 fork() 시 주소 공간이 부모와 완전 복제

    1. pml4_get_page(parent->pml4, va)로 부모 페이지 찾고
    2. palloc_get_page(PAL_USER)로 새 페이지 할당
    3. memcpy(newpage, parent_page, PGSIZE)
    4. 원래 페이지의 쓰기 가능 여부(pte & PTE_W)를 확인해서 writable 세팅
    5. pml4_set_page(child->pml4, va, newpage, writable)로 자식에게 매핑

process_create_initd()

  • fork-wait 경로와 똑같이
    • child_status 할당 및 초기화
    • 부모의 children 리스트에 push_back
    • thread_create() 로 initd 생성
    • c->tid = tid; parent_tid 설정
    • sema_down(&c->sema) 으로 부모 블록
  • 동작 흐름
  1. 명령줄 전체 복사
    • fn_copy"프로그램 [인자...]" 형태의 문자열 저장
  2. 실행할 프로그램 이름 분리
    • 첫 공백 전까지를 prog_name에 복사 → 스레드 이름으로 사용
  3. 부모–자식 동기화 구조 준비
    • child_status 생성: 자식 TID, exit 상태, 세마포어, 리스트 엘리먼트 등
    • 부모의 children 리스트에 c 추가
  4. initd 스레드 생성
    • thread_create(prog_name, ..., initd, fn_copy) 호출
  5. 관계 정보 설정
    • c->tid = tid;
    • 자식 스레드 구조체의 parent_tid = parent->tid;
  6. 부모 대기
    • sema_down(&c->sema)로 부모가 initd의 process_exit()까지 블록

이제 run 'foo [args]' 경로에서 부모는 initd가 완전히 종료(exit)돼 process_exit()에서 sema_up(&c->sema) 호출될 때까지 올바르게 기다리고, 그 후에 "Execution of 'foo ...' complete." 메시지와 함께 종료.


  • process_create_initd() – 커널이 처음 한 번 “initd”라는 사용자 프로세스를 띄우기 위해 쓰는 함수. – 내부적으로는 thread_create(initd…)process_exec() → ELF 로딩 경로로 들어가기 때문에, fork가 아니라 exec 기반으로 새 프로세스를 시작. – 이걸로 “메인 커널”과는 완전히 다른 첫 사용자 프로세스 컨텍스트를 만들고, 부모(커널 스레드)는 그 프로세스가 끝날 때까지 기다림.
  • process_fork() – 그 이후 userland 에서 fork() 시스템 콜을 호출할 때마다, 이미 실행 중인 프로세스를 복제(clone) 하기 위해 쓰는 함수. – SYS_FORKsyscall_handlerprocess_fork(name, &if_) 로 진입해서, 부모의 메모리·파일 디스크립터·실행 컨텍스트를 그대로 복사한 “자식” 프로세스를 만들어 줌. – fork()가 리턴한 뒤 부모에는 자식 TID, 자식 코드 경로에서는 0이 돌아가도록 레지스터를 세팅해 주는 전형적인 Unix‐style fork 구현.
    forkexec
    생성 시점이미 실행 중인 프로세스를 복제커널(메인 스레드)이 디스크에서 새 프로그램을 로드
    주소 공간부모와 똑같은 가상 메모리(코드·데이터)를 공유·복제완전히 새로운 ELF 바이너리를 읽어서 덮어씀
    스레드 이름원래 프로세스 이름(또는 인자가 넘긴 이름) 유지process_create_initd()에서 짠 이름(prog_name)
    반환값부모: 자식 TID, 자식: 0 반환호출한 곳으로 돌아가지 않음(성공 시 곧바로 사용자 코드 진입)
    내부 동기화세마포어로 부모·자식간 sema_down/sema_up부모는 process_create_initd()에서 대기, exec 자체에는 wait 없음

다시보는 전체 흐름

1. 부팅 단계

  1. main() (threads/init.c) 진입
    • BSS 영역 초기화 (bss_init)
    • 물리 메모리·페이지 테이블 설정 (palloc_init, paging_init)
    • 스레드 시스템 초기화 (thread_init) → all_list, ready_list 등 세팅
    • 콘솔·입출력 장치 초기화 (console_init, timer_init, kbd_init …)
    • 예외·시스템콜 핸들러 등록 (exception_init, syscall_init)
  2. 스케줄러 기동
    • thread_start() 호출 → idle 스레드 생성 → 인터럽트 허용 → 스케줄러 시작
    • 곧바로 idle 또는 main 스레드 중 우선순위에 따라 실행

2. 첫 사용자 프로세스(“initd”) 생성

  1. run_actions()run_task()
    • run '프로그램 [인자]' 옵션을 해석
    • process_create_initd(cmdline) 호출
  2. process_create_initd()
    • cmdline 전체를 복사해 두고(palloc_get_page + strlcpy)
    • prog_name(첫 번째 토큰)만 뽑아 스레드 이름으로 사용
    • 부모(메인 스레드)의 children 리스트에 child_status 추가
    • thread_create(prog_name, initd, fn_copy)
    • 생성된 initd 스레드의 TID와 부모 TID 연결, 부모는 sema_down으로 블록
  3. initd() (커널 스레드)
    • process_init() 호출(빈 껍데기)
    • process_exec(fn_copy) → 유저 ELF 로드 루틴(load()) 진입
    • ELF 헤더 파싱 → 각 PT_LOAD 세그먼트 매핑 → 스택 셋업 → 인자( argv ) 복사
    • do_iret()로 사용자 모드로 전환 → 유저 프로그램의 main(int, char**) 진입
  4. 부모(메인 스레드)
    • initd가 exit()할 때까지 sema_down에서 블록
    • initd(=유저프로세스) 종료 시 process_exit()sema_up → 부모 깨어남
    • process_create_initd() 리턴 → run_task()에서 “Execution complete.” 출력 → VM 종료

3. 일반적인 스레드 생성 흐름 (thread_create)

  1. API 호출

    tid_t tid = thread_create("name", priority, function, aux);
  2. 새 커널 스레드 구조체 할당

    • palloc_get_page(PAL_ZERO)
    • init_thread(t, name, priority)
  3. 초기 컨텍스트 설정

    t->tf.rip = (uintptr_t)kernel_thread;
    t->tf.R.rdi = (uint64_t)function;
    t->tf.R.rsi = (uint64_t)aux;
    /* cs, ds, ss, eflags 등 설정 */
  4. READY 상태에 추가

    • 우선순위별로 ready_list에 삽입
    • 필요 시 thread_preempt()로 즉시 선점
  5. 스케줄러

    • schedule()next_thread_to_run()thread_launch()do_iret()
    • kernel_thread(function, aux)로 진입 → intr_enable()function(aux) 실행

4. fork()exec() 흐름 비교

fork()exec()
호출 위치유저 모드 C 코드 안에서유저 모드 C 코드 안에서
진입 경로SYS_FORKsyscall_handlerprocess_fork(name, &if_)SYS_EXECsyscall_handlerprocess_exec(fn_copy)
동작부모 컨텍스트(주소 공간, 파일 등) 복제 → 자식 생성현재 스레드 주소 공간 버리고 ELF 로드
반환값부모: 자식 TID, 자식: 0호출 지점으로 복귀하지 않음
  • process_fork()
    1. 부모의 intr_frame 복사 + RAX=0 세팅
    2. child_status 생성 → 부모 리스트 추가 → 부모 sema_down 블록
    3. thread_create(name, __do_fork, child_if) → 복제 쓰레드 생성
    4. 자식 스레드는 __do_fork()에서 페이지 테이블·파일 디스크립터·메모리 복제 후 sema_up → 부모 깨어남
  • process_exec()
    1. process_cleanup() → 기존 주소 공간 해제
    2. load() → ELF 로드(프로그램 헤더별 페이지 매핑, 스택/argv 구성)
    3. do_iret() → 새 프로그램 진입(main)

추가) sema 의 위치

  • fork 동기화
    • 부모: sema_down(&c->sema)
    • 자식: sema_up(&c->sema) (__do_fork() 안)
  • wait/exit 동기화
    • 부모: sema_down(&c->sema) (process_wait())
    • 자식: sema_up(&c->sema) (process_exit() 안)

추가) do_fork는 어떤걸 복제해야하는가?

  • 사용자 프로세스가 메모리에 올려둔 모든 가상 메모리 페이지를 복제.
  • Pintos 는 userland 프로그램을 ELF 로더(process_exec)를 통해
    • 코드(text) 세그먼트 → 읽기 전용 페이지에 올려 놓고
    • 데이터·BSS·힙·스택 세그먼트 → 읽기·쓰기 페이지에 올려 놓음.
  • process_fork()__do_fork() 안에서
    1. 자식용 빈 PML4(페이지 디렉토리) 생성
    2. pml4_for_each(parent->pml4, duplicate_pte, parent) 호출
    3. duplicate_pte() 가상 주소 va 하나하나마다
      • parent_page = pml4_get_page(parent->pml4, va) 로 부모의 물리 페이지 찾아오고
      • newpage = palloc_get_page(PAL_USER) 로 자식 쪽 새 페이지 할당
      • memcpy(newpage, parent_page, PGSIZE) 로 내용(기계어 코드+데이터)을 복사
      • pml4_set_page(child->pml4, va, newpage, writable) 로 자식 PML4 에 매핑

결국 “fork” 를 호출하면

  • 텍스트(기계어 코드) 페이지
  • 데이터·BSS·힙·스택 페이지
  • (그리고 여러분이 추가 복제해 준) 열린 파일 디스크립터 테이블 등

모두 새로 할당한 페이지에 동일하게 복사되기 때문에, 자식은 부모와 메모리 컨텐츠가 완전히 똑같은 프로세스가 됨.

중요한 차이: “소스코드(.c)” 자체가 복사되는 게 아니라, 메모리에 올라가서 실행되는 컴파일된 기계어 코드와 그 외 데이터 구조가 통째로 복제된다고 이해하면 된다.

0개의 댓글