jun-wiki

View My GitHub Profile

Posts (Latest 10 updated) :
Read all
Contents:
  1. 시스템 콜 처리
    1. syscall_init
    2. syscall_handler
    3. 헬퍼 함수 (메모리 검증)
      1. assert_user_range(const void *uaddr, size_t size)
      2. copy_in(void *kdst, const void *usrc, size_t n)
      3. copy_out(void *udst, const void *ksrc, size_t n)
      4. copy_in_string(char *kdst, const char *usrc, size_t max_len)
    4. 파일 디스크립터 테이블 fd_table
      1. fd_alloc()
    5. 공유 파일 관리
      1. fdref_inc(struct file *fd)
      2. fdref_dec(struct file *fp)
    6. system_dup2(int oldfd, int newfd)
    7. system_fork(const char *thread_name, struct intr_frame *parent_if)
      1. __do_fork(void *aux)
    8. process_exit()
      1. 열린 파일들 닫기
      2. exec 전용 파일 닫기
      3. 파일 디스크립터 테이블 메모리 해제
      4. 자식/부모 관계 정리

시스템 콜 fork 부터는 TIL에 정리하지 않아 몰아서 정리하려 한다

그치만 그건 어렵기에 걍 완성된 코드랑

어떻게 흐름이 진행되는지 다루겠다

그리고난 extra 과제인 dup2까지 클리어했기에 이를 포함한 흐름이다

extra 까지 하지 않았을 경우와 흐름에서 차이가 있음을 알아두자


시스템 콜 처리

일단 초기화부터 해야 한다

초기 설정도 같이 해주며 말이다

syscall_init

초기화하고 설정 해주는 역할을 맡고 있다

기본적으로 부팅할때에 한번만 실행된다

실행시, 모델별 레지스터(MSR)들을 설정한다

이건 어차피 기본적으로 구현되어 있어 신경 쓸 거 아니다

이곳에서 해줘야 할건 파일시스템 동기화를 위한 전역 락 초기화와

파일 디스크립터 관리 위해 추가한 자료구조 초기화 해주면 된다

난 전역 락과 파일 디스크립터 관리 자료구조와 그 보호락을 초기화 해주었다

그리고 이곳에서 시스템 콜 핸들러를 호출한다


syscall_handler

switch문을 통해 들어오는 번호를 확인하고 해당하는 함수 호출해준다

ARG0(f), ARG1(f)… 등으로 레지스터 값 매크로로 받게 해놓았다
(첫번째 인자, 두번째 인자, …)

RET()는 시스템 콜 반환값을 f->R.rax에 넣어주는 역할을 한다

  switch (SC_NO(f)) {
    case SYS_HALT: system_halt(); /* 전원 종료 */
    case SYS_EXIT: system_exit((int)ARG0(f)); /* 현재 프로세스 종료 */

    case SYS_FORK: RET(f, system_fork((const char *)ARG0(f), f)); /* 프로세스 복제 */
    case SYS_EXEC: RET(f, system_exec((const char *)ARG0(f))); /* 새 프로그램 실행 */ break;

    case SYS_WAIT: RET(f, system_wait((tid_t)ARG0(f))); /* 자식 프로세스 대기 */
 
    case SYS_CREATE: RET(f, system_create((const char *)ARG0(f), (unsigned)ARG1(f))); break;
    case SYS_REMOVE: RET(f, system_remove((const char *)ARG0(f))); break;

    case SYS_OPEN:   RET(f, system_open((const char *)ARG0(f))); break;
    case SYS_CLOSE:  system_close((int)ARG0(f)); break;

    case SYS_FILESIZE: RET(f, system_filesize((int)ARG0(f))); break;

    case SYS_READ:   RET(f, system_read((int)ARG0(f), (void *)ARG1(f), (unsigned)ARG2(f))); break;
    case SYS_WRITE:  RET(f, system_write((int)ARG0(f), (const void *)ARG1(f), (unsigned)ARG2(f))); break;
  
	  case SYS_SEEK:   system_seek((int)ARG0(f), (unsigned)ARG1(f)); break;
    case SYS_TELL:   RET(f, system_tell((int)ARG0(f))); break;

    /* dup2 extra 과제 */
    case SYS_DUP2:   RET(f, system_dup2((int)ARG0(f), (int)ARG1(f))); break;

    default:         system_exit(-1); __builtin_unreachable();
  }

위와 같이 각각에 맞춰 시스템콜 함수 호출해준다



헬퍼 함수 (메모리 검증)

시스템 콜 다룰때에는 직접 접근하고 역참조하고 이런건 오류도 그렇고 상당히 위험하다

그렇기에 중간에서 메모리 검증하고 버퍼에 대신 받아서 복사하게 하는 등의 안정장치가 필요하다

이를 위해 만든 헬퍼 함수들이다

assert_user_range(const void *uaddr, size_t size)

uaddr 부터 size만큼의 바이트를 검사한다

총 두가지를 검사하는데

주소 영역이 유저 영역이 맞는지(is_user_vaddr) 확인하고

해당 범위내의 페이지가 현제 프로세스의 페이지테이블에 매핑되어 있는지(pml4_get_page) 확인한다

만약 잘못된 주소라면 system_exit(-1)로 프로세스 종료한다


copy_in(void *kdst, const void *usrc, size_t n)

이름처럼 복사해온다

사용자 영역 메모리 usrc로부터 커널 버퍼 kdst로 말이다

pml4_get_page로 물리주소 얻고 페이지 경계까지 복사한다

이후 usrckdst의 포인터 증가시켜가며 남은 바이트 이어 복사하다가 잘못된 주소 나오면 system_exit(-1)해준다


copy_out(void *udst, const void *ksrc, size_t n)

이름 보면 추측 가능하다

커널 데이터 ksrc로부터 사용자 버퍼 udst로 복사한다

똑같이 페이지 단위로 진행한다


copy_in_string(char *kdst, const char *usrc, size_t max_len)

사용자 영역의 문자열을 커널 버퍼로 복사한다

문자열 길이가 오버하지 않게 최대 max_len바이트까지 복사한다 (어지간해선 넘을 수가 없다)

그 전까지 문자열 돌면서 pml4_get_page로 유효성 검사해준다

성공 조건은 중간에 NUL문자 \0을 만나는 것이다

만약 max_len안에 이를 찾지못하면 그냥 \0추가하고 false반환한다



파일 디스크립터 테이블 fd_table

각 프로세스에서 참조할 파일들을 위해 fd table이 존재한다

이를 관리하기위해 process.c헤더의 스레드 구조체에 관련 변수를 넣어줬다

  struct file **fd_table;    // 파일 포인터 배열
  int fd_cap;                // 한계
  bool fd_table_from_palloc; // exec 누수 관리용

주석 보면 알 수 있듯이

배열 저장용이랑 테이블 용량, 그리고 누수 관리용이다

테이블 palloc으로 받았는지 확인해서 메모리 해제한다

그리고 파일 디스크립터 숫자 0 = STDIN, 1 = STDOUT으로 사용해야 한다

이를 위해 매크로도 지정해주었다

#define STDIN_FD  ((struct file*)-1)
#define STDOUT_FD ((struct file*)-2)

값은 의미 없고 일관성만 지키면 된다

그래도 0이나 1은 양수이고 NULL로도 쓰일 수 있는 값이기에 혼동 방지용으로 음수 사용했다

음수 순서를 저장할 일은 없으니 훨 안전하다


각 스레드가 실행될때에 파일 테이블을 준비해준다

    cur->fd_table = (struct file **)palloc_get_page(PAL_ZERO);
    cur->fd_cap = PGSIZE / (int)sizeof(cur->fd_table[0]);
    cur->fd_table_from_palloc = true;
    cur->fd_table[0] = (struct file *)-1; /* stdin */
    cur->fd_table[1] = (struct file *)-2; /* stdout */

0으로 채운 페이지 할당받고 fd크기로 나눠 용량도 정해준다

이후 현재 테이블 palloc으로 받았음을 체크해주고

0번과 1번 테이블을 stdin, stdout으로 설정해주면 준비 끝이다


fd_alloc()

새 FD 할당해준다

fd_alloc(struct file *f) 현재 스레드의 fd_table에서 빈 칸을 찾아

그 위치에 파일 포인터 f를 넣고 FD 번호를 리턴한다

좀 더 구체화 된 동작이다 ↓

  1. fd_ensure_table()호출

    • 현재 스레드에 fd_table이 없다면 위에 처럼 파일 테이블 준비

    • 만약을 대비한 함수다

  2. fd_cap - 1까지 순회하며 NULL을 찾는다

  3. 찾으면 거기에 f저장 후 그 인덱스 반환

  4. 만약 자리가 없으면 -1 호출

이렇게 할당된 FD는 프로세스 내부에서 유일한 핸들로 사용되고, 실제 파일을 가리키는 포인터(struct file *) 와 연결된다



공유 파일 관리

dup2 같은 경우는 fd table 번호만 다르고 실제로는 같은 파일을 가리킨다

여러 파일 디스크립터가 동일한 파일을 가리킬때 이 파일을 관리하는게 핵심이라 볼 수 있다

기본적으로 pintos에서는 file_close() 하면 걍 메모리 해제하고 사용 해제 처리해버린다

그치만…

dup2를 통해 여러 파일 디스크립터가 파일 하나 참조 중인데 그걸 그냥 닫아버리면

남은 디스크립터들이 상당히 곤란해진다

이를 막기 위해 파일 참조 카운트를 사용했다

file_reaf 구조체와 해시 테이블을 사용해 관리했는데

해시 테이블말고 연결리스트도 좋다


  • struct file_ref 구조체

      struct file_ref {
          struct file *fp;
          int refcnt;
          struct hash_elem elem;
      };
    

    열린 파일 포인터 *fp와 그 포인터를 참조하는 파일 디스크립터들의 참조 개수 refcnt를 기록한다
    hash_elem elem은 해시테이블의 원소 중 하나라는 뜻이다
    list_elem elem이면 리스트 원소였을 터다

  • 해시 테이블 file_ref_ht
    file_ref들을 해시 테이블이다
    syscall_init할때 초기화 되고 전용 락(file_ref_lock)도 쥐어줬다


fdref_inc(struct file *fd)

파일 포인터 fp의 참조 카운터 증가시킨다

파일 새로 열거나 fork, dup2할때에 쓰인다

fpSTDIN_FD (-1) 또는 STDOUT_FD (-2) 인 경우는 참조 카운터 필요 없다

그냥 센티널값이라 바로 true리턴해주면 된다

이후 ref_find(fp)로 해세 테이블에 fp키 유무 확인한다

  • 없을 경우
    malloc으로 struct file_ref할당하고 fp 설정하고 refcnt 1로 설정해준다
    그리고 해시 테이블에 삽입하면 끝
    malloc 실패시 false반환

  • 있을 경우
    해당 file_ref 가져와 refcnt++로 값 올려준다

작업이 끝났으면 true 반환한다


fdref_dec(struct file *fp)

파일 포인터 fp의 참조 카운터 감소시킨다

똑같이 STDIN_FD (-1) 또는 STDOUT_FD (-2) 인 경우는 그냥 반환한다

void 타입이라 반환값도 필요없다

해시 테이블에서 fp에 해당하는 file_ref를 찾아온다

refcnt를 1감소시킨다

만약, 이로 인해 refcnt0이 되었다면 이제 누구도 참조하지 않는다는 거다

  • 0이 되었을때

    • 해시 테이블에서 삭제

    • 실제 파일 닫기 file_close(fp)

    • free(r)file_ref 구조체도 메모리 해제

    • 함수 종료

물론, refcnt가 0보다 크다면 그냥 종료하면 된다

이를 통해 요구사항을 명백히 충족할 수 있었다

이 dup2의 요구사항을 지키기가 어려운 과제였다

여기서 CS:APP과의 차이가 존재하는데

기본적으로 POSIX 방식의 dup2를 책에서는 서술하는데

우리가 구현해야 하는 것은 이와는 좀 다르다

POSIX에서는 inode table을 참조하는 open file table을 fork해도 자식 desciptor table은 부모와 같은 open file을 참조한다

그치만 Pintos에서는 fork 시 자식은 별개의 open file table(부모와 똑같은 inode table을 참조)을 참조한다

[POSIX]
Parent FD table --> [open file desc X] --> [inode A]
Child  FD table --> [open file desc X] --> [inode A]
(오프셋/플래그 공유)

[Pintos(보편 구현)]
Parent FD table --> [struct file P] --> [inode A]
Child  FD table --> [struct file C] --> [inode A]
(P와 C는 오프셋 독립, inode는 동일)

작게 도식화

이를 구현하기 위해 fdref_inc과정에서 해시 테이블을 사용하는 것이다

아무튼 여기까지 구현하면 테스트 케이스는 무리 없이 통과 가능한 dup2 로직을 완성해낸 것이다



system_dup2(int oldfd, int newfd)

위에서 사실상 다 구현했기에 여기서는 조건만 간략히 검사한다

oldfdnewfd가 범위 내인지 확인하고, 둘이 같은 지 확인한 뒤에

oldfd가 가리키는 파일의 유무를 확인하고 이미 newfd가 열려 있는지 확인한다

범위 밖이거나 없으면 실패 반환

둘이 같으면 이미 되어있으니 바로 성공 처리

newfd가 열려있으면 조용히 닫아준다 (system_close호출)

그리고 나서 fdref_inc호출해준다



system_fork(const char *thread_name, struct intr_frame *parent_if)

copy_in_string으로 검사 한번 해주고 process_fork (const char *name, struct intr_frame *if_ UNUSED) 호출한다

process_forkthread_create로 자식 스레드 생성 전 자식 상태 추적을 위해 child_status구조체 만든다

이 구조체에 자식 tid, 종료 코드, 로드 성공 여부 등등 다양한 정보 기록해 놓는다

그리고 부모의 children 리스트에 연결해준다

thread_create를 호출시 새 스레드가 실행할 함수로 __do_fork를 지정하고 인자로 fork_args 구조체를 전달

자식 스레드는 __do_fork부터 실행 시작

__do_fork(void *aux)

__do_fork에서 실행 컨텍스트 복사하고 자원 복제

  • fork_args로부터 부모 스레드 포인터와 레지스터 상태 parent_if등을 받아온다

  • 페이지 테이블 복제
    새 페이지 테이블(pml4_create())을 만들고 해당 페이지 테이블로 주소공간 전환(pml4_activate)
    이후, 부모의 페이지 테이블 순회하며 사용자 메모리 영역 모두 복사

  • 파일 디스크립터 테이블 복제
    파일 포인터가 STDIN이나 STDOUT일 경우 동일한 값을 복사
    일반 파일인 경우 file_duplicate로 복사하고 해시테이블에 넣은 뒤 참조 1로 설정한다
    만약 해시테이블에 존재하는 경우 기존에 만든 파일 구조체 가리키고 하고 참조++ 한다

만약 위 과정 중 어느 지점에서라도 실패할 경우
fork_rollback을 통해 복제된 자원 정리하고 프로세스 종료한다

자식의 fd_table을 훑으며 전부 fdref_dec 호출해 닫고 테이블 해제한다

이후, error로 이동하여 로드 실패 처리하고 부모가 대기 중이었다면 load_sema up시켜 알리고
자식스레드는 thread_exit()로 종료

실패가 아닌 성공일 경우, 로드 성공 처리하고 load_sema up 시킨다

이 경우 부모 쪽에서 fork 호출 마무리하고 자식 tid 반환해준다

유의할 점은 자식 쪽에서 RAX레지스터를 0으로 수정해야 한다는 거다

fork 완료후 부모쪽은 자식의 tid를, 자식은 0을 반환해야 한다



process_exit()

이름 보면 알수 있듯이 프로세스 종료다

이 경우 자원 정리가 중요하다

열린 파일들 닫기

if (cur->fd_table) {
    for (int i = 0; i < cur->fd_cap; i++) {
        struct file *p = cur->fd_table[i];
        if (!p) continue;
        cur->fd_table[i] = NULL;
        fdref_dec(p);
    }
}

현재 프로세스의 fd_table 검사하며 유효한 파일 포인터 전부 닫는다


exec 전용 파일 닫기

process_exec에서 실행 중인 프로그램 파일 열 때 file_deny_write()를 걸어 실행 중 수정 당하지 않게 해놨다

현재 스레드의 exec_file 포인터에 이를 저장하고 있으니 프로세스 종료 시 모두 해제해야 한다

if (cur->exec_file) {
    file_allow_write(cur->exec_file);
    file_close(cur->exec_file);
    cur->exec_file = NULL;
}

file_allow_write로 쓰기 허용 풀고 파일을 닫아 다른데서 수정, 삭제 가능케 한다


파일 디스크립터 테이블 메모리 해제

fd_tablepallocmalloc으로 할당했기에 이를 해제해줘야 한다

fd_table_from_palloc이라는 플래그로 palloc으로 통해 할당했다는 것이 확인되면 palloc_free_page

malloc이면 그냥 free로 해제해주면 된다

fd_table 포인터는 NULL로 fd_cap도 0으로 설정해주면 끝이다


자식/부모 관계 정리

현재 프로세스가 부모라면 child_status 구조체 정리한다

부모와 자식 간에도 ref_cnt가 존재하기에 이를 1씩 깍아주는 것도 잊지말고 말이다

당연하게 0이되면 메모리 해제해주고

현재 프로세스가 자식일 경우 ref_cnt 줄이고 스스로의 상태를 exited = true로 설정해

부모에서 이를 확인 가능케 해준다


이렇게 다해주고 나서 process_cleanup()을 호출해주면 나머지도 다 처리해준다

여기까지가 프로젝트 2 구현 fork와 dup2의 핵심이었다