728x90
  • 과제 관련 설명
 

[Krafton Jungle | TIL_22.12.27] Project 2. System Calls 구현 (1) - 개요

1. [Git book] User Memory 시스템 콜을 구현하기 위해서는 가상 주소 공간에 데이터를 읽고 쓰는 방법을 제공해야한다. 인수를 가져올 때는 이러한 기능이 필요하지 않다. 그러나 시스템 콜의 인수로

olive-su.tistory.com

 

  • Process related: halt, exit, exec, wait, fork
  • File related: create, remove, open, filesize, read, write, seek, tell, close

 


🎯 Goal

  • Modified files
    • threads/thread.h
    • threads/thread.c
    • userprog/syscall.h
    • userprog/syscall.c
    • userprog/process.c

 

📝 Functions List

  1. check_address
  2. fdt_add_fd
  3. fdt_get_fd
  4. fdt_remove_fd

 

  1. create
  2. remove
  3. open
  4. filesize
  5. read
  6. write
  7. seek
  8. tell
  9. close

 


 

Ⅰ. Help Functions(추가 함수)

1. check_address

👉 시스템 콜의 인자로 들어오는 모든 포인터 값을 검사한다.

 

  • 전체 시스템 콜 관련 함수
/* userprog/syscall.c */

static void 
check_address(void *addr) {
     struct thread *curr = thread_current();
    if (!is_user_vaddr(addr) || pml4_get_page(curr -> pml4, addr) == NULL || addr == NULL) // 유저 영역인지  NULL 포인터인지 확인
        exit(-1);
}
  • 시스템 콜의 인자로 들어오는 주소 값이 유효한 주소 영역인지 확인한다.
  • ‘USER_STACK’ -> is_user_vaddr 내장 함수 응용
  • [Hanyang Univ p.70] exit(-1)로 처리하라.
  • 예외 처리 경우
    • Null 포인터
    • 매핑되지 않은 가상 메모리에 대한 포인터
    • 커널 가상 주소 공간에 대한 포인터(KERN_BASE)

 

 

2. fdt_add_fd

👉 fd table에 인자로 들어온 파일 객체를 저장하고 fd를 생성한다.

  • ⭐ 파일 디스크립터 관련 함수

 

/* userprog/syscall.c */

static int 
fdt_add_fd(struct file *f) {
    struct thread *curr = thread_current();
    struct file **fdt = curr->fdt;

    // fd가 제한 범위를 넘지 않고 fdt의 인덱스 위치와 일치 시
    while (curr->next_fd < FDCOUNT_LIMIT && fdt[curr->next_fd]) {
        curr->next_fd++;
    }

    // fdt가 가득 찼을 때 return -1
    if (curr->next_fd >= FDCOUNT_LIMIT)
        return -1;

    fdt[curr->next_fd] = f; // fdt에 해당 fd 새로 넣어줌
    return curr->next_fd;
}
  • [Hanyang Univ] ‘process_add_file’ 함수 → 의미적 명확성을 위해 이름 변경

 

 

3. fdt_get_fd

👉 fd table에서 인자로 들어온 fd를 검색하여 찾은 파일 객체를 리턴한다.

  • ⭐ 파일 디스크립터 관련 함수

 

/* userprog/syscall.c */

static struct file *
fdt_get_file(int fd) {
    struct thread *curr = thread_current();
    if (fd < STDIN_FILENO || fd >= FDCOUNT_LIMIT) { // 실패
        return NULL;
    }
    return curr->fdt[fd]; // 성공
}
  • [Hanyang Univ] ‘process_get_file’ 함수 → 의미적 명확성을 위해 이름 변경

 

 

4. fdt_remove_fd

👉 fd table에서 인자로 들어온 fd를 제거한다.

  • ⭐ 파일 디스크립터 관련 함수

 

/* userprog/syscall.c */

static void 
fdt_remove_fd(int fd) {
    struct thread *curr = thread_current();

    if (fd < STDIN_FILENO || fd >= FDCOUNT_LIMIT) // 실패
        return;

    curr->fdt[fd] = NULL; // 성공
}
  • [Hanyang Univ] ‘process_close_file’ 함수 → 의미적 명확성을 위해 이름 변경

 

 


Ⅱ. 파일 디스크립터 구현

  • 📢 file descriptor 필요!
    • open, filesize, read, write, seek, tell, close
    • open, read, write 구현 시에는 파일에 대한 동시접근이 발생할 수 있으니, Lock을 사용한다!

 

👀 파일 디스크립터 관련 개념!

Ref. CS110: Principles of Computer Systems

 

  • 파일 디스크립터는 리소스(대부분 파일에 해당)간에 상호작용(시스템 콜을 통한)하기 위한 식별자이다.
  • 식별자의 별다른 의미는 없다.
  • 새로운 파일 디스크립터를 할당할 때, 커널은 가장 작은 숫자를 할당한다.
  • 파일 디스크립터의 진입점은 단지 하나의 파일 테이블에 대한 포인터이다.

 

  • i-node 🆚 v-node
    • i-node : 파일에 대한 메타 데이터(커널 x)
    • v-node : 파일에 대한 실질적 객체(커널 o)
    • Ref. Inode vs Vnode

 

  • 파일 디스크립터를 통한 파일 참조의 구조(큰 그림)

 

  • 파일 디스크립터는 프로세스 당 하나 존재하며, 열려 있는 파일에 대한 파일 테이블 엔트리는 커널에 존재한다. 각 프로세스 당의 작업이 다르므로(작업 현황 R/W) 파일 테이블 엔트리도 각각 생기게 되는 것 같다.
    (⭐ 정확하진 않으니까 레퍼런스 더 찾아보기!)
  • But, 파일 자체는 똑같으므로 i-node는 동일하다.

 

  • 같은 프로세스에서 같은 파일 참조

  • open 시스템 콜을 통해서 파일 디스크립터가 생성된다는 걸 뜻하는 것 같다.
    그래서 같은 파일임에도 다른 ‘fd flags(idx)’를 갖게 된다.

 

  • 다른 프로세스에서 같은 파일 참조

 

  • 핀토스의 파일 디스크립터
    • Project 3 이전의 할당은 모두 palloc 으로 구현한다.
    • [KAIST p.59] 파일 구조체를 가진 파일 디스크립터 테이블을 만든다.
      fd는 파일 디스크립터 테이블의 인덱스이다. (순차적 할당)
    • fd 0, 1 은 stdin, stdout에 할당되어져 있다.
    • [KAIST p.60] FDT를 커널 메모리 공간에 할당한다.

 

 

1. thread

/* threads/thread.h */

#define FDT_PAGES 3 // fdt 할당시 필요한 페이지 개수
#define FDCOUNT_LIMIT FDT_PAGES *(1<<9) // 3(테이블 개수) * 512(한 테이블 당 전체 엔트리 개수)
  • fdt 페이지 할당을 위해 페이지 크기를 정의한다.

 

/* threads/thread.h */

struct thread {

    ...

#ifdef USERPROG
    /* Owned by userprog/process.c. */
    uint64_t *pml4;                     /* Page map level 4 */
    struct file **fdt; // 파일 디스크립터 테이블(프로세스당 개별적으로 존재)
    int next_fd; // 다음 fd 인덱스

    ...

};
  • fd table을 가리키는 포인터 값 fdt과 fdt의 다음 fd 인덱스를 가리키는 next_fd를 선언한다.
  • ⚠️ 이때, fd table에는 파일 객체 자체가 들어가므로 struct file 타입의 포인터로 선언해준다!

 

/* threads/thread.c */

tid_t
thread_create (const char *name, int priority, thread_func *function, void *aux) {

    ...

    t->fdt = palloc_get_multiple(PAL_ZERO, FDT_PAGES); // fdt 공간 할당
    if (t->fdt == NULL) {
        return TID_ERROR;
    }
    t->next_fd = 2; // 0 : stdin, 1 : stdout
    t->fdt[0] = 1; // STDIN_FILENO -> dummy value
    t->fdt[1] = 2; // STDOUT_FILENO -> dummy value

    ...

    return tid;
}
  • 스레드 생성 시, fdt 공간을 새로 할당해준다.
  • STDIN(0), STDOUT(1) 은 미리 콘솔을 위해 예약된 fd이므로 next_fd 는 2부터 시작하도록 초기화한다.
  • fdt[0]fdt[1] 에는 0(NULL)이 아닌 값으로 채워준다.

 

/* userprog/process.c */

#include "lib/user/syscall.h" // need to Calling syscall_close.

...

void
process_exit (void) {

    ...

    for (int i = 0; i < FDCOUNT_LIMIT; i++) { // 프로세스 종료 시, 해당 프로세스의 fdt의 모든 값을 0으로 만들어준다.
        close(i);
    }
    palloc_free_multiple(curr->fdt, FDT_PAGES); // fd table 메모리 해제

    ...    

}
  • close system call 설명에 대한 구현이다.
  • [Hanyang Univ p.132] “프로세스가 종료될 때, 메모리 누수 방지를 위해 프로세스에 열린 모든 파일을 닫는다.”
  • 프로세스 종료 시, 해당 프로세스 fdt에 열려있는 모든 파일에 대한 fd를 0으로 만들어준다.

 

  • thread_create의 메모리 할당 부분에서 페이지를 모두 0으로 초기화한 뒤, 할당 하지만 혹시 모를 메모리 값에 대한 충돌을 막기 위해 for문을 돌면서 fdt의 모든 엔트리를 0으로 만들어주는 작업을 수행한다.
    t->fdt = palloc_get_multiple(PAL_ZERO, FDT_PAGES);
  • 💡 실제로 코드 내 for문 부분을 지워도 TC는 통과된다!

 

🙋‍♂️ Q1. palloc_get_multiple 로 할당하는 이유

/* threads/palloc.h */

void *palloc_get_page (enum palloc_flags flags) // 하나의 페이지
void *palloc_get_multiple (enum palloc_flags flags, size_t page_cnt) // page_cnt개의 연속된 페이지
  • fdt 할당 시 페이지 개수를 3으로 해주는 이유는 페이지 개수를 3미만으로 할당하게 되면  "multi-oom" TC는 불통한다...
    • 아마 fd개수가 512 * 2(1024)를  넘게 넣어주는듯
  • 따라서 공간을 넉넉히 할당해주는 것!

 

🙋‍♂️ Q2. 페이지 하나 당 엔트리 개수가 1<<9 (512개)인 이유?

  • 핀토스 기본 페이지 하나의 사이즈 : 1 << 12(PGSIZE)
    • threads/vaddr.h 에 선언되어있다.

 

  • fd table에 저장하는 데이터 타입 : 파일 객체의 포인터(struct file**)
    • sizeof(struct file**) : 8(byte)

 

  • 1<<12 / 1<<3 = 512
    4096(byte) / 8(byte) = 512

 

 

2. lock

/* userprog/syscall.h */

...

void close (int fd); // "userprog/process.c"에서 close 시스템 콜 호출을 위해 필요

struct lock filesys_lock; // 파일 점유시 필요한 락

...
  • [Hanyang Univ p.133]
  • read, write 시 파일에 대한 동시 접근이 일어날 수 있으므로 lock을 사용한다.

 

/* userprog/syscall.c */

void
syscall_init (void) {

    ...

    lock_init(&filesys_lock); // 파일 읽고 쓰기에 필요한 락 초기화 _defined "userprog/syscall.h"
}

 

 


Ⅲ. 파일 관련 시스템 콜 구현

해당 프로젝트의 초점은 파일 시스템이 아니므로 filesys 디렉토리에 간단하게 구현된 파일 시스템을 이용한다.
(ref. Gitbook - Introduction)

🔅 파일 관련 시스템 콜에서 사용하는 함수 모두가 “filesys/filesys.h” 또는 "filesys/file.h" 에 있으므로 적절히 가져다 쓰면 된다.

 

1. create

  • file 을 이름으로 하고 크기가 initial_size 인 새로운 파일을 생성한다.
  • 파일 생성은 파일 열기를 의미하지 않는다.
    파일을 여는 것은 open 시스템 콜이 수행한다.
  • return
    • 성공 시, true
    • 실패 시, false

 

/* userprog/syscall.c */

bool 
create(const char *file, unsigned initial_size){
    check_address(file);
    return filesys_create(file, initial_size);
}

 

 

2. remove

  • file 이름을 가진 파일을 삭제한다.
  • 파일은 열려있는 지 닫혀있는지와 관계없이 삭제될 수 있다.
    • ⭐ 삭제 전 파일 닫기 작업 필요!!
  • return
    • 성공 시, true
    • 실패 시, false

 

/* userprog/syscall.c */

bool 
remove(const char *file){
    check_address(file);
    return filesys_remove(file);
}

 

 

3. open

  • 파일(file : 파일 포인터)을 연다.
  • 콘솔용으로 예약 되어있는 파일 디스크립터
    • 0 : 표준 입력(STDIN_FILENO)
    • 1 : 표준 출력(STDOUT_FILENO)

 

  • 각 프로세스는 독립적인 파일 디스크립터들의 세트를 가지고 있다.
    파일 디스크립터 테이블 구현 필요
  • 파일 디스크립터는 자식 프로세스들에게 상속된다.
  • 하나의 파일이 한번 이상 열릴 때(단일 프로세스 또는 다중 프로세스로 부터), 각 open 프로세스는 새로운 파일 디스크립터를 반환한다.
    open 을 통해 새로운 파일 디스크립터가 생성된다.→ 💥 주의

 

  • close를 하면서 각각의 닫힌 파일 위치는 공유하지 않는다.

 

  • return
    • 성공 시, fd
    • 실패 시, -1

 

/* userprog/syscall.c */

int 
open (const char *file){
    check_address(file);
    struct file *target_file = filesys_open(file);

    if (target_file == NULL) {
        return -1;
    }
    int fd = fdt_add_fd(target_file); // fdt : file data table

    // fd table이 가득 찼다면
    if (fd == -1) {
        file_close(target_file);
    }
    return fd;
}

 

 

4. filesize

  • filesys 의 내장 함수 file_length 를 이용한다.

 

  • ⚠️ 이때, file_length 의 파라미터가 struct file* 임을 유의!
  • ⚠️ 따로 테스트 코드 x

 

/* userprog/syscall.c */

int 
filesize (int fd){
    struct file *target_file = fdt_get_file(fd);
    if (target_file == NULL)
        return -1;
    return file_length(target_file);
}

 

 

5. read

  • fd로 열린 파일에서 size바이트를 buffer로 읽는다.
  • 실제로 읽은 바이트 수를 리턴하거나 만약 파일을 읽을 수 없다면(EOF로 인한) -1을 리턴한다.
  • fd 0은 input_getc() 를 이용해서 키보드에서 읽는다.

 

  • input_getc
/* devices/input.c */

/* Retrieves a key from the input buffer.
   If the buffer is empty, waits for a key to be pressed. */
/* 입력 버퍼로부터 누른 키 값을 가져온다.
     만약 입력 버퍼가 비어있으면 키를 누를 때까지 기다린다. */

uint8_t
input_getc (void) {
    enum intr_level old_level;
    uint8_t key;

    old_level = intr_disable ();
    key = intq_getc (&buffer);
    serial_notify ();
    intr_set_level (old_level);

    return key;
}

 

/* userprog/syscall.c */

int 
read(int fd, void *buffer, unsigned size) {
    check_address(buffer);
    int read_bytes = -1;

    if(fd == STDIN_FILENO){ // fd 0 reads from the keyboard using input_getc()._gitbook
        int i;
        unsigned char *buf = buffer;

        for (i = 0; i < size; i++)
        {
            char c = input_getc();
            *buf++ = c;
            if (c == '\0')
                break;
        }
        return i;

    }
    else{
        struct file *file = fdt_get_file(fd);
        if (file != NULL && fd != STDOUT_FILENO){ // STDOUT_FILENO 
            lock_acquire(&filesys_lock); // 파일을 읽는 동안은 접근 못하게 락 걸어줌
            read_bytes = file_read(file, buffer, size);
            lock_release(&filesys_lock); // 락 해제
        }
    }
    return read_bytes;
}
  • 파일에 대한 읽기 작업을 수행하므로 lock을 걸어준다.

 

 

6. write

  • buffer 에서 열린 파일 fdsize 만큼의 바이트를 쓴다.
  • 실제로 쓴 바이트 수를 리턴한다.
  • 일부의 바이트를 쓸 수 없는 경우에는 리턴이 size 보다 작을 수 있다.
  • 일반적으로 파일의 끝을 지나서 쓰면 파일이 확장되지만 핀토스 파일 시스템으로는 불가하다.
  • 파일 끝까지 가능한 한 많은 바이트를 쓰고, 실제 바이트 수를 리턴하거나 쓸 수 없는 경우 0 을 리턴한다.
  • fd 1은 putbuf() 를 이용해서 콘솔에 쓴다.

 

/* userprog/syscall.c */

int
write (int fd, const void *buffer, unsigned size) {
    check_address(buffer);
    int write_bytes = -1;

    if (fd == STDOUT_FILENO){
        putbuf (buffer, size);
        return size;
    }
    else {
        struct file *file = fdt_get_file(fd);
        if (file != NULL && fd != STDIN_FILENO){ // STDIN_FILENO
            lock_acquire(&filesys_lock); // 파일을 쓰는 동안은 접근 못하게 락 걸어줌
            write_bytes = file_write(file, buffer, size);
            lock_release(&filesys_lock); // 락 해제
        }
    }
    return write_bytes;
}

 

 

7. seek

  • 열린 파일 fd 에서 읽거나 쓸 다음 바이트를 position 으로 바꾼다.
    • 즉, position 0 은 파일의 시작 위치이다.
  • 현재 파일의 끝을 지나서 읽는 것은 오류가 아니다.
    • 파일의 끝을 지나서 read를 실행하면 0(byte)를 반환한다.
  • But, 파일의 끝을 지나서 쓰는 건(write) 불가능하다.
    • 프로젝트 4 이전까지는 파일의 크기가 고정되어 있기 때문

 

/* userprog/syscall.c */

void 
seek (int fd, unsigned position){
    struct file *target_file = fdt_get_file(fd);

    if (fd <= STDOUT_FILENO || target_file == NULL)
        return;

    file_seek(target_file, position);
}

 

 

8. tell

  • 열린 파일 fd 에서 읽거나 쓸 다음 바이트(파일의 시작 지점 부터)를 반환한다.

 

/* userprog/syscall.c */

unsigned 
tell (int fd){
    struct file *target_file = fdt_get_file(fd);

    if (fd <= STDOUT_FILENO || target_file == NULL)
        return;

    file_tell(target_file);
}

 

 

9. close

  • fd를 닫는다.
  • 프로세스를 종료하거나 제거하는 것은 묵시적으로 해당 프로세스에 열려있는 모든 fd를 닫는다.
    • fd 각각에 대해 호출한다.

 

/* userprog/syscall.c */

void
close (int fd){
	struct file *target_file = fdt_get_file(fd);

	if (fd <= STDOUT_FILENO || target_file == NULL || target_file <= 2)
		return;
	
	fdt_remove_fd(fd); // fd table에서 해당 fd값을 제거한다.

	file_close(target_file); // 열었던 파일을 닫는다.
}

 

 

 

728x90