[pintos] Week2~3: User Program Part.5 - 파일 디스크립터

이전 포스팅들에서 exec, halt, exit, create, remove를 구현했다. 다음 단계로 넘어가기 위해 파일디스크립터가 무엇인지 어떻게 구현할지 알고 넘어가야 한다.

파일디스크립터는 운영체제 전반에 걸쳐 매우 자주 쓰이는 개념이다.


파일디스크립터(File Descriptor, FD)란?

파일 디스크립터는 열린 파일에 대한 추상적인 식별자이다.

  • 정수(int) 형태로 표현된다.
  • 유저 프로그램이 open() 같은 시스템 콜을 호출하면, 커널은 실제 파일 객체를 열고, 이 파일에 대한 핸들을 의미하는 FD 값을 유저에게 리턴한다.

예시:

int fd = open("foo.txt");
write(fd, "hello", 5);
close(fd);

여기서 fd는 유저 공간에서 foo.txt를 가리키는 핸들이다.


왜 FD가 필요한가?

추상화된 접근

  • 유저 프로세스가 직접 커널 내부 파일 객체에 접근할 수 없기 때문에, 정수형 FD를 통해 우회 접근하게 된다.
  • FD는 유저 프로그램과 커널이 파일을 공유할 수 있는 창구 역할을 한다.

다중 파일 관리

  • 하나의 프로세스가 여러 개의 파일을 동시에 열 수 있어야 하므로, 커널은 FD마다 실제 파일 구조체 포인터를 연결해서 관리한다.

FD는 어떻게 관리되는가?

1. 프로세스마다 별도의 FD 테이블:

  • 파일 디스크립터 테이블 구현
  • 각 프로세스는 자체 파일 디스크립터 테이블을 가진다(최대 크기: 64개 항목).
  • 파일 디스크립터 테이블은 구조체 file을 가리키는 포인터 배열이다.
  • FD는 파일 디스크립터 테이블의 인덱스이며, 순차적으로 할당된다.
  • FD 0과 1은 각각 stdin과 stdout에 할당된다.
  • open()은 fd를 반환한다.
  • close()는 인덱스 fd의 파일 디스크립터 항목에 0을 설정한다.

방법 설명
정적 배열 struct file *fdt[64]처럼 고정된 크기 배열을 직접 struct thread에 포함.
동적 할당 struct file **fdt로 포인터만 가지고 있고, FDT 자체는 palloc_get_page() 등으로 동적 할당. 더 유연하며 메모리 절약 가능.

실제 구현에서는 동적 할당 방식이 더 일반적이며 fork 시 복사도 용이하다.

  • FDT를 스레드 구조의 일부로 정의한다.
  • FDT를 커널 메모리 영역에 할당하고, 관련 포인터를 스레드 구조에 추가한다.

생성 시:

  • fdt = palloc_get_page(PAL_ZERO); 등으로 페이지 단위 할당
  • fdt[0], fdt[1]은 각각 stdin, stdout으로 예약
  • next_fd = 2로 초기화

종료 시:

  • 모든 열린 파일을 file_close(fdt[i])로 닫고
  • FDT 페이지를 palloc_free_page()로 해제

동기화 처리:

  • 파일 시스템은 전역적으로 공유되므로 경쟁 조건(Race Condition) 발생 가능
  • 해결 방법:
    • struct lock filesys_lock; (전역 락 선언)
    • syscall_init()에서 lock_init(&filesys_lock);

구현

1. thread 구조체 수정

페이지 할당

  • includ/threads/thread.h
#define FDT_PAGES     3                     
#define FDCOUNT_LIMIT FDT_PAGES * (1 << 9)
  • 한 페이지 크기 = 4KB = 4096 bytes
  • FDT_PAGES는 3페이지를 파일 디스크립터 테이블에 사용하겠다는 의미
  • → 3 * 4096 = 12,288 bytes 확보

multi-oom: 많은 자식 프로세스를 fork()해서 자원을 빠르게 소모시키는 테스트

  • 1 << 9 == 512
  • 즉, FDCOUNT_LIMIT = 3 * 512 = 1536
  • struct file *는 64비트 머신에서 8바이트
  • 한 페이지는 4096바이트 → 4096 / 8 = 512개의 포인터 저장 가능
  • 즉, 한 페이지에 FD 512개 저장 가능하다는 계산
  • FDT_PAGES = 3 → FDCOUNT_LIMIT = 1536개

위 두 코드는 추 후 multi-oom 테스트를 위해 미리 제한 값을 설정한 것이다.

fd_idx, fdt 선언

  • include/threads/thread.h
int exit_status;

int fd_idx;              // 파일 디스크립터 인덱스
struct file **fdt;       // 파일 디스크립터 테이블

thread_create() 수정

  • threads/thread.c
tid_t
thread_create (const char *name, int priority,
		thread_func *function, void *aux) {
	struct thread *t;
	tid_t tid;

	ASSERT (function != NULL);

	/* Allocate thread. */
	t = palloc_get_page (PAL_ZERO);
	if (t == NULL)
		return TID_ERROR;

	/* Initialize thread. */
	init_thread (t, name, priority);
	tid = t->tid = allocate_tid ();

#ifdef USERPROG
    t->fd_table = palloc_get_multiple(PAL_ZERO, FDT_PAGES);
    if (t->fd_table == NULL)
        return TID_ERROR;

    t->exit_status = 0;  // exit_status 초기화

    t->fd_idx = 3;
    t->fd_table[0] = 0;  // stdin 예약된 자리 (dummy)
    t->fd_table[1] = 1;  // stdout 예약된 자리 (dummy)
    t->fd_table[2] = 2;  // stderr 예약된 자리 (dummy)

    list_push_back(&thread_current()->child_list, &t->child_elem);
#endif	

	/* Call the kernel_thread if it scheduled.
	 * Note) rdi is 1st argument, and rsi is 2nd argument. */
	t->tf.rip = (uintptr_t) kernel_thread;
	t->tf.R.rdi = (uint64_t) function;
    .
    .
    .
}

프로세스마다 자신만의 File Descriptor Table (FDT)이 있어야 하고, 이는 struct thread 안에서 fdt 포인터로 관리된다.

t->fdt = palloc_get_multiple(PAL_ZERO, FDT_PAGES);

새로 생성되는 쓰레드(프로세스)의 FDT를 동적 메모리 영역에 할당한다. 이 때 0으로 모든 페이지를 초기화한다.

유닉스 계열 시스템에서의 표준 입출력 FD 예약 관례

FD 번호 용도 의미
0 stdin 표준 입력 (keyboard)
1 stdout 표준 출력 (screen)
2 stderr 표준 에러 출력 (screen)

2. File Descriptor 구현

함수 선언

  • include/userprog/process.h
int process_add_file(struct file *f);
struct file *process_get_file(int fd);
int process_close_file(int fd);

process_add_file() 함수

  • userprog/process.c
int process_add_file(struct file *f) 
{
    struct thread *curr = thread_current();
    struct file **fdt = curr->fdt;

    if (curr->fd_idx >= FDCOUNT_LIMIT)
        return -1;

    fdt[curr->fd_idx++] = f;

    return curr->fd_idx - 1;
}
struct file **fdt = curr->fdt;
  • 해당 스레드가 가진 파일 디스크립터 테이블의 포인터를 가져오고.
  • 현재의 fd_idx 위치에 struct file *을 저장하고, fd_idx를 증가시킨다.

3. process_get_file() 함수 구현

  • userprog/process.c
struct file *process_get_file(int fd) 
{
    struct thread *curr = thread_current();    // 현재 스레드 가져오기

    if (fd < 0 || fd >= FDCOUNT_LIMIT)         // 유효하지 않은 fd 검사
        return NULL;

    return curr->fdt[fd];                      // FDT에서 해당 fd 위치의 file 포인터 반환
}

4. process_close_file() 함수 구현

  • userprog/process.c

이 함수 process_close_file()는 현재 스레드의 파일 디스크립터 테이블(FDT)에서 특정 파일 디스크립터 fd를 닫는(close) 역할을 한다.

int process_close_file(int fd) 
{
    struct thread *curr = thread_current();  // 현재 실행 중인 스레드 가져오기

    if (fd >= FDCOUNT_LIMIT)                 // fd가 너무 크면 잘못된 접근 → 에러 반환
        return -1;

    curr->fdt[fd] = NULL;                    // 해당 fd를 NULL로 설정 (닫은 것으로 처리)
    return 0;                                // 성공 반환
}

5. process_exit() 함수 구현

  • userprog/process.c
void
process_exit (void) {
    struct thread *curr = thread_current ();
    /* TODO: Your code goes here.
     * TODO: Implement process termination message (see
     * TODO: project2/process_termination.html).
     * TODO: We recommend you to implement process resource cleanup here. */

    // 1. 파일 디스크립터 정리
    for (int fd = 0; fd < curr->fd_idx; fd++)
        close(fd);

    // 2. 실행 중인 바이너리 파일 해제
    file_close(curr->runn_file);

    // 3. FDT 메모리 해제
    palloc_free_multiple(curr->fdt, FDT_PAGES);

    // 4. 기타 프로세스 정리
    process_cleanup();

    // 5. 부모-자식 동기화
    sema_up(&curr->wait_sema);   // 부모의 wait() unblock
    sema_down(&curr->exit_sema); // 부모가 정보를 수집할 때까지 대기

}
for (int fd = 0; fd < curr->fd_idx; fd++) 
	close(fd);
  • 파일 디스크립터 테이블(FDT)에서 열린 파일들을 모두 닫는다.
file_close(curr->runn_file);
  • 현재 실행 중인 유저 프로세스의 실행 파일을 닫는다.
palloc_free_multiple(curr->fdt, FDT_PAGES);
  • 파일 디스크립터 테이블 메모리(FDT)를 해제한다.
process_cleanup();
  • 사용자 주소 공간 (pagedir 또는 pml4, spt) 정리
  • 프로세스와 관련된 파일 시스템, 주소 공간, 커널 구조체 등을 해제
sema_up(&curr->wait_sema);
  • 부모가 process_wait()에서 자식의 종료를 기다릴 수 있도록 시그널 전송
  • 자식이 먼저 종료되면 sema_up() 호출하여 부모 대기 해제
sema_down(&curr->exit_sema);
  • 자식은 여기서 부모가 정보를 수거할 때까지 대기
  • 부모는 process_wait()에서 정보를 가져간 후 sema_up()을 호출해야 자식이 이 블로킹에서 깨어나 thread_exit()으로 완전히 종료됨