시스템 콜, 예외적 제어흐름에 대한 글입니다.
- 유저 프로그램 시스템 콜 ‘핸들러’ 작성
- 우리가 만든 건 시스템 콜 그 자체라기 보단 사실상 '핸들러'.
- ‘exit()’과 같은 시스템 콜을 호출하면 기본적으로 lib/user/syscall.c 안의 syscall 호출
- 이는 우리가 작성한 시스템 콜 핸들러(userprog/syscall.c)로 점프하고, 커널모드에서 시스템콜을 처리할 수 있도록 해줌.
- 기존에 구현되어 있는 시스템콜은 특별한 기능이 없으나, 우리가 직접 작성한 시스템콜로 라우팅시켜 필요한 기능을 수행하게 하겠다는 것.
- 핀토스는 User Program - Kernel간 상호작용할 수 있는 여러 장치를 만들어두었음.
- 기본적으로 Stack, file descriptor Table은 프로세스마다 독립적으로 할당(per-process)
- 동시에 커널은 System wide 장치가 있음. 시스템적으로 메모리가 어떻게 관리되고 있는지, 어떤 파일이 열려있는지 트래킹하기 위함
- 커널 가상주소
- Open file descripter Table
- 시스템 콜은 대부분 이러한 System wide *주소를 통해 구현하게 되어있으며
- ptov (physical to virtual address) 등 페이지 테이블 복제시 계속해서 등장하는 함수가 리턴하는 건 User VA(virtual address)가 아닌 Kernel VA.
- filesys_open()과 같이 커널단에서 실제 file path & inode를 리턴하는 함수는 핀토스 내부에 구현되어 있어 중간다리만 놓아주면 되며, 유저 프로그램단 file descriptor Table인덱스 및 file object만 업데이트 해주면 됨.
- 일은 커널에서 하고, 우리는 유저 프로그램 환경을 셋팅한다고 보면 됨
- 가장 까다로웠던 부분은 fork()의 Context Switching
- 운영체제로 문맥전환을 할 때 Trap을 발생시키는데, 이 때 하드웨어(CPU)와 운영체제간 교류가 있음.
- fork의 경우 자식 프로세스로 context를 복제하기 위해 CPU 레지스터 값을 push, pop 하는 과정 필요.
- 시스템 콜은 어셈블리어를 모르면 결코 이해했다고 할 수 없다.
- syscall_handler : 시스템 콜을 미리 정의된 핸들러로 보내주는 명령어
- intr_entry : intr_frame 생성 명령어
- iret() : 인터럽트 처리 후 반환할 때 사용하는 CPU 명령어
- schedule(): 프로세스간 문맥전환 → thread_yield()
- 시스템콜은 커널로 문맥전환 시 하드웨어(레지스터)를 이용한다. 훨씬 빠르기 때문이다.
/* Clones the current process as `name`. Returns the new process's thread id, or TID_ERROR if the thread cannot be created. */
tid_t
process_fork (const char *name, struct intr_frame *if_ UNUSED) {
/* Clone current thread to new thread.*/
/* ------------------------ Project 2 --------------------*/
struct thread *curr = thread_current();
memcpy(&curr->parent_if, if_, sizeof(struct intr_frame));
tid_t tid = thread_create(name, curr->priority, __do_fork, curr);
if (tid == TID_ERROR) {
return TID_ERROR;
}
struct thread *child = get_child(tid);
sema_down(&child->fork_sema);
if (child->exit_status == -1) {
return TID_ERROR;
}
return tid;
}
static void
__do_fork (void *aux) {
struct intr_frame if_;
struct thread *parent = (struct thread *) aux;
struct thread *current = thread_current ();
printf("jisu: child's tid is %d\n", current->tid);
/* TODO: somehow pass the parent_if. (i.e. process_fork()'s if_) */
struct intr_frame *parent_if;
bool succ = true;
parent_if = &parent->parent_if;
/* 1. Read the cpu context to local stack. */
memcpy (&if_, parent_if, sizeof (struct intr_frame));
if_.R.rax = 0; /* child's return value = 0 */
current->pml4 = pml4_create();
if (current->pml4 == NULL)
goto error;
process_activate (current);
#ifdef VM
supplemental_page_table_init (¤t->spt);
if (!supplemental_page_table_copy (¤t->spt, &parent->spt))
goto error;
#else
if (!pml4_for_each (parent->pml4, duplicate_pte, parent))
goto error;
#endif
if (parent->fd_idx == FDCOUNT_LIMIT)
goto error;
current->fd_table[0] = parent->fd_table[0];
current->fd_table[1] = parent->fd_table[1];
for (int i=2; i<FDCOUNT_LIMIT; i++){
struct file *f = parent->fd_table[i];
if (f == NULL) {
continue;
}
current->fd_table[i] = file_duplicate(f);
}
current->fd_idx = parent->fd_idx;
/* if child load success, wake up parent in process_fork */
sema_up(¤t->fork_sema);
/* Finally, switch to the newly created process. */
if (succ)
do_iret (&if_);
error:
current->exit_status = TID_ERROR;
sema_up(¤t->fork_sema);
exit(TID_ERROR);
// thread_exit ();
}
parent_if
에 넣어주고, do_iret
을 호출해 실제 child process 유저 프로그램으로 전환
intr_frame
structure를 인자로 넘겨줌유저 → 커널
- 인터럽트 발생 → 인터럽트된 프로그램 상태 저장 → 인터럽트 루틴 실행(처리)
- intr_entry에 의해 컨텍스트를 intr_frame에 저장
- intr_handler()를 호출해 실제 인터럽트 수행
커널 → 유저
- iret() 호출시 저장된 상태 복원, 중단된 프로그램 재개
/* Switch the current execution context to the f_name.
* Returns -1 on fail.
*/
int
process_exec (void *f_name) {
char *file_name = f_name; // void -> char
bool success;
int argc = 0;
char *argv[128];
char *token, *save_ptr;
token = strtok_r(file_name, " ", &save_ptr);
while (token) {
argv[argc++] = token;
token = strtok_r(NULL, " ", &save_ptr);
}
/* We cannot use the intr_frame in the thread structure.
* This is because when current thread rescheduled,
* it stores the execution information to the member. */
/* 인터럽트 프레임 구조체 선언 */
struct intr_frame _if;
_if.ds = _if.es = _if.ss = SEL_UDSEG;
_if.cs = SEL_UCSEG;
_if.eflags = FLAG_IF | FLAG_MBS;
/* We first kill the current context */
process_cleanup ();
/* And then load the binary */
success = load (file_name, &_if);
/* If load failed, quit. */
if (!success) {
palloc_free_page (file_name);
return -1;
}
/* set up (User's) stack */
argument_stack(argc, argv, &_if);
// hex_dump(_if.rsp, _if.rsp, USER_STACK-_if.rsp, true);
/* Start switched process. */
do_iret (&_if);
NOT_REACHED ();
}
wait\_sema
: 자식 프로세스 종료를 기다림free\_sema
: 부모가 exit_status 회수할 때까지 기다림process\_cleanup()
init.c 실행 로직을 다음과 같이 그려보았다.
자식 스레드를 생성하면서 process_exec()를 시키는데,
가장 중요한 부분인 load()가 커널단에서 작업하고 파일 로딩, 페이지 할당 등 모든 셋팅이 끝나고 나면 iret
을 통해 실제 유저 프로그램으로 전환된다.
즉, 자식 스레드는 파일을 실행하기 위해 시스템콜을 호출해서 커널모드로 전환 후 다시 돌아와야 한다.
Fork 와 execve 차이
- fork : 같은 프로그램을 child가 복제해서 실 (두 개의 프로세스가 같은 프로그램)
- execve : 한 프로세스내 새로운 프로그램을 실행
- execve는 기존 주소공간을 덮어써버림
-process_cleanup()
후 다시 셋팅. 현재 프로세스의 유저스택 뿐만 아니라 페이지 디렉토리도 비움
- load 함수에서 페이지 디렉토리를 새롭게 생성하고 활성화process_activate()
- 그러나 PID, open files는 그대로임.
- 새롭게 실행하는 프로그램은 해당 프로세스가 돌리고 있는 여러 open file 중 하나이므로, fd_index가 + 1 늘어날 뿐.
- 다만 execve는 새로운 프로세스에서 파일을 실행하는 경우도 많으며, 핀토스에서 구현한 것도 새로운 스레드를 생성하여 process_exec()를 호출하였음.
void argument_stack (int argc, char **argv, struct intr_frame *if_)
{
char *arg_address[128];
/* Insert argument value */
for (int i = argc-1; i>=0; i--){ // right to left, (n-1)~0
int argv_len = strlen(argv[i]);
if_->rsp = if_->rsp - (argv_len + 1);
memcpy(if_->rsp, argv[i], argv_len + 1);
arg_address[i] = if_->rsp;
}
/* Insert padding for word-alignment 8 byte (64bit) */
while (if_->rsp % 8 != 0){
if_->rsp--;
*(uint8_t *)(if_->rsp) = 0;
}
/* Insert addresses of strings including word-padding */
for (int i = argc; i>=0; i--){
if_->rsp = if_->rsp -8;
if (i == argc){
memset(if_->rsp, 0, sizeof(char **));
}
else
memcpy(if_->rsp, &arg_address[i], sizeof(char **));
}
/* Fake return address */
// it's just newly created thread process, so it doesn't have return address. just put zero.
if_->rsp = if_->rsp - 8;
memset(if_->rsp, 0, sizeof(void *));
/* 레지스터 값 설정 */
/* main으로 가는 세 개의 인자
1) argc
2) argv[] 배열 첫 항목으로의 포인터 (**argv)
3) envp[] 전역변수 배열 첫 항목에 대한 포인터
*/
if_->R.rdi = argc;
if_->R.rsi = if_->rsp + 8;
}
static bool
load (const char *file_name, struct intr_frame *if_) {
struct thread *t = thread_current ();
struct ELF ehdr;
struct file *file = NULL;
off_t file_ofs;
bool success = false;
int i;
/* Allocate and activate page directory. */
t->pml4 = pml4_create ();
process_activate (thread_current ());
/* Open executable file. */
file = filesys_open (file_name);
/* 스레드의 파일 구조체 포인터 running이 *file을 가리키게 함 */
t->running = file;
file_deny_write(file);
/* Read and verify executable header. */
if (file_read (file, &ehdr, sizeof ehdr) != sizeof ehdr
...
}
/* Read program headers. */
file_ofs = ehdr.e_phoff;
for (i = 0; i < ehdr.e_phnum; i++) {
struct Phdr phdr;
/* Sets the current position in FILE to NEW_POS bytes from the start of the file. */
file_seek (file, file_ofs);
file_read (file, &phdr, sizeof phdr)
file_ofs += sizeof phdr;
...
/* Set up stack. */
if (!setup_stack (if_))
goto done;
/* Start address. */
if_->rip = ehdr.e_entry;
success = true;
done:
/* We arrive here whether the load is successful or not. */
file_close (file);
return success;
}
t->pml4 = pml4_create
: 페이지 디렉토리 생성 process_activate
: 페이지 테이블 활성화 filesys_open
: 프로그램 파일 Openfile_deny_write(file)
: 해당 파일에 쓰기 금지 설정file_read
: ELF 파일 헤더를 읽음 file_seek
: setting the file position to the beginning of each program header. 알맞은 위치에서 데이터를 읽어오기 위해 현재 파일을 가리키는 포인터를 ‘프로그램 헤더’ 위치로 옮김. setup_stack (if_)
:setup_stack)
wait_sema
: 자식이 종료할 때까지 기다림으로써 asynchronous한 작업을 보장fork_sema
: 자식이 부모의 맥락을 완전히 복제할 때까지 기다림free_sema
: 자식이 완전히 죽기 전 부모가 exit_status를 받을 때까지 잠시 기다려줌.process_cleanup()
or do_schedule(THREAD_DYING)
로 완전히 terminate 하기 전에 sema_down(&curr->free_sema)
로 잠시 락을 걸어둔다.
/* Exit the process. This function is called by thread_exit (). */
void
process_exit (void) {
struct thread *curr = thread_current ();
/* 열려있는 모든 파일 종료 */
for (int i=0; i<FDCOUNT_LIMIT; i++){
close(i) // file_close -> allow_write()
}
/* Destroy the current process's page directory */
palloc_free_multiple(curr->fd_table, FDT_PAGES); // fd_table 해제
/* Current executalbe file 종료 */
file_close(curr->running);
/* Wake up blocked parent */
sema_up(&curr->wait_sema);
/* Postpone child termination until parents receive exit status with 'wait' */
sema_down(&curr->free_sema);
/* Switch to Kernel-only page directory */
process_cleanup ();
}
/* close() system call ---------------------------*/
void
file_close (struct file *file) {
if (file != NULL) {
file_allow_write (file);
inode_close (file->inode);
free (file);
}
}
void close(int fd) {
struct file *fileobj = find_file_by_fd(fd);
if (fileobj == NULL)
return;
remove_file_from_fdt(fd);
}
void remove_file_from_fdt(int fd) {
struct thread *cur = thread_current();
if (fd < 0 || fd >= FDCOUNT_LIMIT)
return;
cur->fd_table[fd] = NULL;
}
close
: 열려있는 모든 파일 종료. close 안에는 file_allow_write()로 쓰기 권한을 풀어줌palloc_free_multiple
: 페이지 테이블 해제file_close(curr->running)
: ‘현재’ 파일 종료sema_up(wait_sema)
: 부모 프로세스를 Unblocksema_down(free_sema)
: 부모가 exit_status 반환받을 때까지 기다림sema_up(free_sema)
를 하면process_cleanup
:Destroy the current process's page directory and switch back to the kernel-only page directory