상세 컨텐츠

본문 제목

PintOS | Project2. User Program | 시스템 콜 (프로세스 관련, 파일 관련)

Computer Science/운영체제 (Operating System)

by hyuga_ 2023. 12. 10. 17:21

본문

실제 리눅스 환경에서 제공되는 시스템 콜의 종류는 다음과 같이 다양하다. 

범주 설명 시스템 콜
프로세스 관리 프로세스 생성, 종료, 실행, 통신 등을 다룸 fork, exit, execve, wait, kill
파일 시스템 조작 파일 및 디렉토리 생성, 삭제, 읽기, 쓰기 등을 다룸 open, close, read, write, chmod
장치 관리 하드웨어 장치와의 상호작용 관련 기능을 다룸 ioctl, mmap
네트워크 관리 네트워크 통신 및 소켓 관리 관련 기능을 다룸 socket, bind, connect
메모리 관리 메모리 할당, 해제, 매핑 등을 다룸 brk, mmap, munmap
사용자 및 그룹 관리 사용자 및 그룹 식별, 관리 관련 기능을 다룸 getuid, setgid, getgid
시스템 정보 및 리소스 시스템 시간, 리소스 정보 조회 및 설정 관련 기능을 다룸 time, sysinfo, getrlimit
기타 로깅, 에러 처리 등 기타 기능을 다룸 syslog, errno

 

이 모든걸 다 구현하면 pint OS가 아니겠지??

 

핀토스에서 직접 구현하는 시스템 콜은 크게 두가지 분류로 나눌 수 있다:

1) 프로세스 관련 시스템 콜, 2) 파일 관련 시스템 콜.

 

핀토스에서 구현하게 되는 프로세스와 파일 시스템 조작 위주로 시스템 콜들이 어떻게 구현되는지 살펴보자.

 

 

프로세스 관련 시스템 콜

프로세스 관련 시스템 콜은 프로세스의 생성, 실행, 종료, 상태 관리 등 프로세스 관리와 관련된 작업을 수행한다. 

시스템 콜 용도 동작 방식
halt() 시스템 종료 power_off() 함수를 호출하여 전원을 끈다.
exit(status) 현재 실행 중인 프로세스를 종료 프로세스의 종료 상태를 status로 설정하고, thread_exit() 함수를 호출
fork(thread_name, interrupt frame) 현재 프로세스의 복사본을 생성 부모 프로세스의 상태를 복사하여 새로운 자식 프로세스를 생성
exec(cmd_line) 자식 프로세스 로드 후 실행 지정된 명령어 라인(cmd_line)에 따라 새 자식 프로그램을 로드하고 실행한다. 
wait(pid) 자식 프로세스의 종료를 기다린다. 지정된 프로세스 식별자(pid)를 가진 자식 프로세스가 종료될 때까지 현재 프로세스의 실행을 중단한다.

 

프로세스 관련 시스템 콜 중에서, 프로세스 계층 구조 관련 내용만 따로 정리해보자.

exec과 fork, wait이 관련 시스템 콜이다. 

 

부모 프로세스는 자식 프로세스 디스크립터를 리스트를 이용하여 관리한다.

또한 자식 프로세스는 자신의 부모 프로세스가 누구인지를 저장한다. 

 

핀토스에서는 이를 구현하기 위해 thread 구조체에 다음과 같은 필드를 추가한다. 

struct thread {
    struct thread *parent;       /* Parent thread. */
    struct list child_list;      /* List of child threads. */
    struct list_elem child_elem; /* List element for child_list. */

    /* 자식 프로세스가 load될 때까지 부모 프로세스를 block시키기 위한 semaphore */
    struct semaphore load_sema; 
    /* 자식 프로세스가 exit될 때까지 부모 프로세스를 block시키기 위한 semaphore */
    struct semaphore exit_sema; 
    struct semaphore wait_sema;
}

 

 

 

(부모, 자식 프로세스 실행흐름)

 

 

exec 함수는 파일 이름을 받아서, 해당 파일을 로드한다. 처음 시스템이 가동되어 데몬 프로세스가 실행될 때도 exec 함수가 사용된다. 

 

fork 함수는 부모 프로세스를 복제한 자식을 만드는 시스템 콜이다. 

/* 
	`name`으로 현재 프로세스를 복제한다. 새 프로세스의 스레드 ID를 반환하거나,
	스레드를 생성할 수 없는 경우 TID_ERROR를 반환한다. 
*/
tid_t process_fork(const char *name, struct intr_frame *if_ UNUSED) {
    /* 현재 스레드를 새 스레드로 복제 */
    struct thread *parent = thread_current();
    tid_t tid = thread_create(name, PRI_DEFAULT, __do_fork, if_);

	if (tid == TID_ERROR) {
        return TID_ERROR;
    }
        
    /* 자식 프로세스 생성 대기를 위한 sema down */
    struct thread *child = get_child_process(tid);
    sema_down(&(child->load_sema));

    return tid;  // 자식프로세스 tid 반환
}

 

여기서 자식 프로세스 생성 시(thread_create)에, __do_fork 함수를 인자로 넣어주는데,

__do_fork 함수는 부모 프로세스의 실행 컨텍스트를 복사해서 자식에게 전달하는 역할을 수행한다. 

 

 

파일 관련 시스템 콜

파일 관련 시스템 콜은 파일 생성, 삭제, 읽기, 쓰기, 파일 속성 변경 등 파일 시스템과 상호작용하며 이와 관련된 작업을 수행한다. 

 

시스템 콜 용도 동작 방식
create(file, initial_size) 새 파일을 생성 주어진 이름(file)과 크기(initial_size)로 새 파일을 생성
remove(file) 파일을 삭제 주어진 이름의 파일을 삭제
open(file) 파일을 열고 파일 디스크립터를 반환 지정된 이름의 파일을 열고, 해당 파일에 대한 파일 디스크립터를 할당
filesize(fd) 열린 파일의 크기를 반환 지정된 파일 디스크립터에 해당하는 파일의 크기를 반환
seek(fd, position) 파일 내의 특정 위치로 이동 지정된 파일 디스크립터에 해당하는 파일의 읽기/쓰기 위치를 position으로 이동
tell(fd) 파일의 현재 위치를 반환 지정된 파일 디스크립터의 현재 위치를 반환
close(fd) 열린 파일을 닫는다.  지정된 파일 디스크립터에 해당하는 파일을 닫고, 파일 디스크립터를 해제
read(fd, buffer, size) 파일 또는 표준 입력에서 데이터를 읽는다. 지정된 파일 디스크립터에서 size만큼 데이터를 읽어 buffer에 저장
write(fd, buffer, size) 파일 또는 표준 출력에 데이터를 쓴다. 지정된 파일 디스크립터에 buffer의 데이터를 size만큼 쓴다.

 

 

파일 시스템 콜들을 보면 한가지 특징을 찾을 수 있다.

  • create, remove, open 은 file 이름을 자체를 인자로 받고, 
  • 그 외의 시스템 콜들은 fd(파일 디스크립터)를 인자로 받는다는 점이다.

 

어떤 차이점이 있을까?

  1. file 이름을 인자로 받는 경우 (예: create, remove, open)
    • 이러한 시스템 콜은 파일 작업의 초기 단계에 사용된다.
    • create는 새 파일을 생성하고, remove는 파일을 삭제하며, open은 파일을 열고 파일 디스크립터를 생성한다.
    • 이 단계에서는 파일이 시스템 내에서의 고유 위치(이름)에 의해 식별된다. 파일 이름은 파일 시스템 내에서 해당 파일을 찾는 데 사용된다.
    • 파일이 아직 열리지 않았거나 파일 시스템 내에서 직접 조작될 때 파일 이름을 사용한다.
  2. fd (파일 디스크립터)를 인자로 받는 경우
    • fd는 이미 열린 파일을 식별하는 데 사용되며, 파일에 대한 후속 작업(읽기, 쓰기, 위치 변경 등)을 수행할 때 사용된다.
    • 한 번 파일이 열리면, 시스템은 해당 파일에 대한 파일 디스크립터를 할당한다(open 시스템 콜의 역할). 파일 디스크립터는 열린 파일의 상태(예: 현재 읽기/쓰기 위치)를 관리한다.
    • read, write, seek 등의 시스템 콜은 이미 열린 파일에 대한 작업을 수행하므로 파일 디스크립터를 사용하는 것이다.

즉, 파일 이름을 사용하는 시스템 콜은 파일을 처음 찾고 조작하는 단계,

파일 디스크립터(fd)를 사용하는 시스템 콜은 이미 열린 파일에 대한 작업을 수행할 때 사용된다.

  • fd의 경우 정수형 숫자 형태인데, 예전에 살펴봤듯이 0은 해당 파일에 대한 표준 입력, 1은 표준 출력, 2는 표준 에러를 의미하고, 3 이상의 번호에는 그 외의 다양한 조작을 임의로 매칭할 수 있다. 
  • 각 스레드 별로 fd table에 대한 포인터를 가지고 있는데, 여기서 해당 스레드에서 열려있는 파일들의 번호를 관리하게 된다. 
  • 해당 fd table은 사용자 영역이 아니라 커널 영역에 할당된다. (핀토스 기준으로, palloc_get_page 인자로 PAL_USER 옵션을 주면 사용자 영역에 할당할 수 있다.)

 

 

 

file 이름을 인자로 받는 함수 중 open을 자세히 살펴보자. 

 

sys_open(file) 시스템 콜이 호출되면 다음과 같은 일을 수행한다. 

1) 루트 디렉토리에서 주어진 파일 이름을 찾고,
2) 해당 파일에 대한 인덱스 노드를 기반으로 파일 객체를 생성하여 반환한다.
3) 해당 파일 객체는 이후 파일 읽기, 쓰기, 닫기 등의 작업에 사용된다.

 

 

(userprog/syscall.c)

int sys_open(const char *file) {
    if (file == NULL) sys_exit(-1);

    struct file *f = filesys_open(file);

    if (f == NULL) {
        return -1;
    }
    if (thread_current()->next_fd_idx >= 128) {
        file_close(f);
        return -1;
    }

    thread_current()->fd_table[thread_current()->next_fd_idx] = f;
    return thread_current()->next_fd_idx++;
}

 

 

  1. 파일 이름 확인: sys_open 시스템 콜은 파일 이름을 문자열 형태(file)로 받으며, 이를 통해 열고자 하는 파일을 식별한다. 이를 filesys_open()의 인자로 전달해준다. 그리고 file 구조체 타입의 포인터를 반환받고있다.  

 

 

(filesys/filesys.c)

struct file *filesys_open(const char *name) {
    struct dir *dir = dir_open_root();
    struct inode *inode = NULL;

    if (dir != NULL) dir_lookup(dir, name, &inode);
    dir_close(dir);

    return file_open(inode);
}

 

  1. 루트 디렉토리 열기: filesys_open 함수는 먼저 파일 시스템의 루트 디렉토리를 연다. 이는 dir_open_root() 함수를 호출하여 수행된다. 
  2. 루트 디렉토리에서 파일 검색: 열린 루트 디렉토리 내에서 주어진 파일 이름을 가진 파일의 존재 여부를 확인한다. 이는 dir_lookup(dir, name, &inode)를 호출하여 수행되며, 파일이 존재하면 해당 파일의 인덱스 노드(inode)를 가져온다.
  3. 파일 객체 생성: file_open(inode) 함수를 호출하여, 찾아낸 inode를 가지고 파일 객체를 생성한다.  

 

i-node는 물리 디스크에 적재된 데이터에 대한 위치 정보와 메타데이터를 지닌 일종의 포인터같은 역할을 수행한다.

(출처: 상기님 블로그)

 

 

(filesys/file.c)

/* Opens a file for the given INODE, of which it takes ownership,
 * and returns the new file.  Returns a null pointer if an
 * allocation fails or if INODE is null. */
struct file *file_open(struct inode *inode) {
    struct file *file = calloc(1, sizeof *file);
    if (inode != NULL && file != NULL) {
        file->inode = inode;
        file->pos = 0;
        file->deny_write = false;
        return file;
    } else {
        inode_close(inode);
        free(file);
        return NULL;
    }
}

 

  1. calloc을 사용하여 struct file 타입의 새로운 파일 객체를 메모리에 할당하고, 이 파일 객체의 필드(인덱스 노드, 현재 위치, 쓰기 금지 여부)를 초기화한다.
  2. 생성된 파일 객체를 반환한다. 파일 객체는 파일의 inode, 현재 읽기/쓰기 위치, 파일에 대한 쓰기 작업이 금지되었는지의 여부를 추적한다.
  3. 만약 파일이 존재하지 않거나 메모리 할당에 실패한 경우, 함수는 NULL을 반환한다. 이 과정에서 할당된 inode나 메모리 자원은 적절히 해제된다.

관련글 더보기