Project 2: USER-PROGRAMS
Argument Passing
System Call
과제 목표
process_exec() 내에 사용자 프로그램을 위한 인자를 셋업해라.
유저 프로그램을 실행하기 전에, 커널은 레지스터에다가 맨 처음 function의 argument를 저장해야 한다. process_exec()은 유저가 입력한 명령어를 수행할 수 있도록 프로그램(=process)을 메모리에 적재하고 실행하는 함수이다. 해당 프로그램은 f_name에 문자열로 저장되어 있으나 현재 상태에서 process_exec() 은 새로운 프로세스에 대한 인자 passing을 제공하지 않는다. 이 기능을 구현하는 것이 이번 과제이다. process_exec() 에 코드를 추가해서 간단히 프로그램 파일 이름을 인자로 넣는것 대신에, space가 올 때마다 단어를 parsing하도록 만들어야 한다. 이때, 첫 번째 단어는 프로그램 이름이고 두세 번째 단어는 각각 첫 번째, 두 번째 인자이다.
명령어 라인과 함께, 여러 개 space는 하나의 space와 동일하게 취급해야 한다. 이때 명령어 인자의 길이에 제한을 둘 수 있다.
구현
process_create_initd() 수정
원본 코드
tid_t
process_create_initd (const char *file_name) {
char *fn_copy;
tid_t tid;
/* Make a copy of FILE_NAME.
* Otherwise there's a race between the caller and load(). */
fn_copy = palloc_get_page (0);
if (fn_copy == NULL)
return TID_ERROR;
strlcpy (fn_copy, file_name, PGSIZE);
/* Create a new thread to execute FILE_NAME. */
tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
if (tid == TID_ERROR)
palloc_free_page (fn_copy);
return tid;
}
현재 process_create_initd()
함수는 커맨드 라인의 첫 번째 토큰을 thread_create()
함수의 첫 인자로 전달 되도록 프로그램을 수정해야 한다. 현재는 커맨드 라인 전체가 thread_create()
에 전달되고 있다.
tid_t
process_create_initd (const char *file_name) {
char *fn_copy;
tid_t tid;
/* Make a copy of FILE_NAME.
* Otherwise there's a race between the caller and load(). */
fn_copy = palloc_get_page (0);
if (fn_copy == NULL)
return TID_ERROR;
strlcpy (fn_copy, file_name, PGSIZE);
/* Create a new thread to execute FILE_NAME. */
char *save_ptr;
strtok_r(file_name, " ", &save_ptr);
tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
if (tid == TID_ERROR)
palloc_free_page (fn_copy);
return tid;
}
strtok_r()
: strtok_r 함수는 지정된 문자(이를 delimiters라고 한다)를 기준으로 문자열을 자른다.
예를 들어 위 코드에서 token = strtok_r(file_name, " ", &save_ptr);이라고 하면 file_name의 가장 첫번째 문자열이 나온다. 긴 말 할 것 없이 아래 예시를 보자. "The little prince"를 공백을 기준으로 잘라서 출력한다고 하자. 그러면 출력값은 "The", "Little", "Prince"일 것이다. strtok_r은 한글자씩 이동하다가(T, h, e, ...) 공백을 만나면 그곳에 NULL 값을 넣은 다음, 그 앞까지의 문자열(=The)을 반환한다. 여기서 &the_last는 그 뒤의 문자열("\nLittie Prince\n")에서 가장 첫번째 문자의 주소값을 나타낸다. 이렇게 한 글자씩 잘라서 반환하는 개념이기에 while문을 돌리면서 한글자씩 뽑아내는 것으로 이해하면 된다.
load() 수정
원본 코드
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 ();
if (t->pml4 == NULL)
goto done;
process_activate (thread_current ());
/* Open executable file. */
file = filesys_open (file_name);
if (file == NULL) {
printf ("load: %s: open failed\n", file_name);
goto done;
}
•••
원래 코드 이후에는 load()를 실행하는 코드가 나온다. load()함수는 실행파일의 file_name을 적재해 실행하는 함수이다. load()를 부른 caller인 process_exec()에서 입력한 커맨드 전체가 file_name인자로 넘어온다.
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 ();
if (t->pml4 == NULL)
goto done;
process_activate (thread_current ());
char *token, *save_ptr;
char *argv[64];
uint64_t cnt = 0;
for (token = strtok_r(file_name, " ", &save_ptr); token != NULL; token = strtok_r(NULL, " ", &save_ptr)) {
argv[cnt++] = token;
}
/* Open executable file. */
file = filesys_open (argv[0]);
if (file == NULL) {
printf ("load: %s: open failed\n", file_name);
goto done;
}
•••
for 문을 보면 매개변수로 받은 인자 file_name을 NULL을 기준으로 문자열을 잘라서 argv 배열에 하나씩 넣어준다.
•••
/* Set up stack. */
if (!setup_stack (if_))
goto done;
/* Start address. */
if_->rip = ehdr.e_entry;
/* TODO: Your code goes here.
* TODO: Implement argument passing (see project2/argument_passing.html). */
argument_stack(argv, cnt, &if_->rsp);
if_->R.rdi = cnt;
if_->R.rsi = if_->rsp + 8;
success = true;
done:
/* We arrive here whether the load is successful or not. */
file_close (file);
return success;
}
다음은 인자값을 스택에 올리는 함수 argument_stack()이 필요하다. 위에서 parsing한 다음 한 문자씩 넣어준 배열 arg_list와 count값인 token_count, 그리고 인터럽트 프레임도 인자로 넣는다. 이 함수 자체에서 인터럽트 프레임을 스택에 올리는 것은 아니고, 인터럽트 프레임 내 구조체 중 특정값(rsp)에 인자를 넣어주기 위함이다. 이후에 do_iret()에서 이 인터럽트 프레임을 스택에 올린다.
argument_stack() 추가
argument_stack()
함수는 유저 스택에 프로그램 이름과 인자들을 저장하는 함수이다.
parse : 프로그램 이름과 인자가 저장되어 있는 메모리 공간, count : 인자의 개수, esp : 스택 포인터를 가리키는 주소
void argument_stack(char **parse, int count, void **esp) {
char *argv_address[count];
uint8_t size = 0;
// * argv[i] 문자열
for (int i = count - 1; -1 < i; i--) {
*esp -= (strlen(parse[i]) + 1);
memcpy(*esp, parse[i], strlen(parse[i]) + 1);
size += strlen(parse[i]) + 1;
argv_address[i] = *esp;
}
if (size % 8) {
for (int i = (8 - (size % 8)); 0 < i; i--) {
*esp -= 1;
**(char **)esp = 0;
}
}
*esp -= 8;
**(char **)esp = 0;
// * argv[i] 주소
for (int i = count - 1; -1 < i; i--) {
*esp = *esp - 8;
memcpy(*esp, &argv_address[i], strlen(&argv_address[i]));
}
// * return address(fake)
*esp = *esp - 8;
**(char **)esp = 0;
}
argument_stack()
코드를 뜯어보자. 배열 argaddress[128]은 아래 for문에서 스택에 담을 각 인자의 주소값을 저장하는 배열이다. 이후 for문을 돌면서 process_exec()
에서 넣어주는 arg_list로부터 값을 하나하나씩 꺼내서 if->rsp에 하나씩 넣어준다. 이때 if->rsp는 user stack에서 현재 위치를 가리키는 스택 포인터이자 인터럽트 프레임 내 멤버이다. 여기서 작업이 Gitbook에 나오는 테이블에 값을 채워넣는 것과 같다.
이때, 각 인자에서 인자 크기(argv_len)을 읽는데 이때 각 인자마다는 실제로 sentinel(\n)이 포함되어 있는데 여기서 역시 strlen은 sentinel을 읽지 않으니 +1을 해주는 것이다. for문 순서를 자세히 보면
이후에는 while문을 돌면서 패딩을 삽입한다.
이번에는 주소값 자체를 삽입한다. 이래서 위에 arg_address[] 배열을 따로 만들어 여기에 주소값을 저장한 이유이다. 똑같이 for문들 돌면서 넣는데, 처음 for문에서는(int i = argc-1)로 선언된 반면 여기 for문에서는 (int i = argc)로 선언된다. 이는 앞서 process_exec()에서 while문을 돌며 token을 받아오는 것을 보면, 맨 마지막 인자값으로 NULL을 받아 arg_list에 저장하게 된다. 따라서 argv[-1]= '\n'이 된다. 근데 이 인자 값을 스택에 저장할 때는 맨 끝에 있는 NULL 값을 저장하지 않은 반면 여기서는 NULL값 가리키는 포인터를 저장한다.
for문을 돌고 나면 fake address를 넣어준다. 해당 영역의 메모리는 0으로 초기화해준다.
마지막으로 인터럽트 프레임 if_의 멤버로 있는 레지스터 구조체의 rdi에 인자 count값인 argc, 그리고 rsi에는 fake address바로 위인 arg_address의 맨 앞을 가리키는 주소값을 넣는다.
Interrupt Frame
인터럽트 프레임 (struct Intr_frame)
인터럽트 프레임은 인터럽트가 들어왔을 때, 이전에 레지스터에 작업하던 context를 switching하기 위해 이 정보를 담아놓는 구조체이다. 그래서 구조체 intr_frame에 가보면 엄청 복잡하게 나와 있고, 그안에 멤버 구조체 gp_registers R을 들고 있다. 이 R은 기존 스레드가 작업하고 있을 때의 레지스터 값을 인터럽트가 들어오면 switching하기 위해 이 구조체에다가 정보를 담는다. 그래서 1주차에 do_schedule() 을 보면do_iret()이 나오고, 이 do_iret()는 어셈블리어로 되어 있는데, 여기가 기존까지 작업했던 context를 intr_frame에 담는 과정이라고 보면 되겠다.
즉, 인터럽트 프레임은 인터럽트와 같은 요청이 들어와서 기존까지 실행 중이던 context(레지스터 값 포함)를 스택에 저장하기 위한 구조체이다.
결과
결과(중간 결과)를 확인하기 위해서는 process_exec()
함수에 hex_dump()
함수를 추가해줘야 한다.
hex_dump(_if.rsp, _if.rsp, KERN_BASE - _if.rsp, true);
process_exec()
함수 중간에 load가 끝난 후 다음 위에 함수를 추가해준다.
또한 process_wait()
함수에 무한루프를 추가해줘야 결과를 제대로 볼 수 있다.
while (1){}
다음 구문을 함수 내 return 전에 추가해준다. 밑에 사진은 현재 pintos의 상태를 나타내는 사진이다.
결과
과제 목표
이번 과제에서는 시스템 콜 핸들러 및 시스템 콜을 구현하는 것이다. 여기서 시스템 콜이란 사용자가 커널 영역에 접근하고 싶을 때, 원하는 목적을 대신해서 작업하는 프로그래밍 인터페이스이다. 그렇기 때문에 시스템 콜은 커널 모드에서 실행되고, 작업 후 사용자 모드로 복귀한다. pintos에서는 이를 시스템 콜 핸드러를 통해 시스템 콜을 호출한다.
시스템 콜을 호출할 때, 원하는 기능에 해당하는 시스템 콜 번호를 rax에 담는다. 그리고 시스템 콜 핸들러는 rax의 숫자로 시스템 콜을 호출하고, 해당 콜의 반환값을 다시 rax에 담아서 intr frame(인터럽트 프레임)에 저장한다.
구현
halt();
case SYS_HALT:
halt();
break;
void halt(void) {
// * power_off()를 사용하여 pintos 종료
power_off();
}
power_off()
함수를 호출하면서 pintos를 종료시키는 시스템 콜이다exit();
case SYS_EXIT:
exit(f->R.rdi);
break;
void exit(int status) {
/*
* 실행중인 스레드 구조체를 가져옴
* 프로세스 종료 메시지 출력
* 출력 양식: "프로세스 이름: exit(종료상태)"
* thread 종료
*/
struct thread *cur = thread_current();
cur->exit_status = status;
printf("%s: exit(%d)\n", cur->name, status);
thread_exit();
}
※ 곧 사라질 thread의 exit_status에 status를 저장하는 것은 만약 현재 프로세스가 자식 프로세스여서 부모 프로세스가 wait 상태일 경우, 자식 프로세스가 사라지면서 종료 상태(exit_status)를 부모에게 알려주기 위함이다. 정상적 종료라면 0을 저장한다.
create();
case SYS_CREATE:
f->R.rax = create(f->R.rdi, f->R.rsi);
break;
bool create (const char *file, unsigned initial_size) {
/*
* 파일 이름과 크기에 해당하는 파일 생성
* 파일 생성 성공 시 true 반환, 실패 시 false 반환
*/
check_address(file);
return filesys_create(file, initial_size);
}
file을 만드는 시스템 콜이다. 인자로 받은 file 이름과 크기(initial_size)에 해당하는 파일을 생선한다. 파일 생성에 성공하면 true를 반환하고 실패하면 false를 반환한다. filesys_create가 실질적으로 작업을 수행하므로 크게 볼 것은 없다. (파일을 만들고 바로 열지 않는다. 여는 것은 open() 시트템 콜을 사용해서 따로 실행한다.)
create를 실행하기 전에 해당 file이 유저 영역에 있는 file인지 확인해야한다. 이는 check_address()로 확인할 수 있다.
remove();
case SYS_REMOVE:
f->R.rax = remove(f->R.rdi);
break;
bool remove (const char *file) {
/*
* 파일 이름에 해당하는 파일을 제거
* 파일 제거 성공 시 true 반환, 실패 시 false 반환
*/
check_address(file);
return filesys_remove(file);
}
open();
case SYS_OPEN:
f->R.rax = open(f->R.rdi);
break;
int open (const char *file) {
check_address(file);
struct thread *cur = thread_current();
struct file *fd = filesys_open(file);
if (fd) {
for (int i = 2; i < 128; i++) {
if (!cur->fdt[i]) {
cur->fdt[i] = fd;
cur->next_fd = i + 1;
return i;
}
}
file_close(fd);
}
return -1;
}
open()
함수는 파일을 열 때 사용하는 시스템 콜이다. 성공 시 fd를 생성하고 반환, 실패 시 -1을 반환한다.filesize();
case SYS_FILESIZE:
f->R.rax = filesize(f->R.rdi);
break;
int filesize (int fd) {
struct file *file = thread_current()->fdt[fd];
if (file)
return file_length(file);
return -1;
}
filesize()
함수는 파일의 크기를 알려주는 시스템 콜이다read();
case SYS_READ:
f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
break;
int read (int fd, void *buffer, unsigned size) {
check_address(buffer);
if (fd == 1) {
return -1;
}
if (fd == 0) {
lock_acquire(&filesys_lock);
int byte = input_getc();
lock_release(&filesys_lock);
return byte;
}
struct file *file = thread_current()->fdt[fd];
if (file) {
lock_acquire(&filesys_lock);
int read_byte = file_read(file, buffer, size);
lock_release(&filesys_lock);
return read_byte;
}
return -1;
}
write();
case SYS_WRITE:
f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
break;
int write (int fd UNUSED, const void *buffer, unsigned size) {
check_address(buffer);
if (fd == 0) // STDIN일때 -1
return -1;
if (fd == 1) {
lock_acquire(&filesys_lock);
putbuf(buffer, size);
lock_release(&filesys_lock);
return size;
}
struct file *file = thread_current()->fdt[fd];
if (file) {
lock_acquire(&filesys_lock);
int write_byte = file_write(file, buffer, size);
lock_release(&filesys_lock);
return write_byte;
}
}
write()
함수는 열린 파일의 데이터를 기록하는 시스템 콜이다. 역시 fd값이 1일 때는 표준 출력이기 때문에 1일 시 putbuf() 함수를 사용하여 버퍼에 저장된 데이터를 화면에 출력한다.seek();
case SYS_TELL:
f->R.rax = tell(f->R.rdi);
break;
unsigned tell (int fd) {
struct file *curfile = thread_current()->fdt[fd];
if (curfile)
return file_tell(curfile);
}
tell()
함수는 열린 파일의 위치를 알려주는 시스템 콜이다.close();
case SYS_CLOSE:
close(f->R.rdi);
break;
void close (int fd) {
struct file * file = thread_current()->fdt[fd];
if (file) {
lock_acquire(&filesys_lock);
thread_current()->fdt[fd] = NULL;
file_close(file);
lock_release(&filesys_lock);
}
}
close()
함수는 열린 파일을 닫는 시스템 콜이다. 파일을 닫고 fd를 제거한다.check_addres();
void check_address(void *addr) {
struct thread *cur = thread_current();
if (addr == NULL || is_kernel_vaddr(addr) || pml4_get_page(cur->pml4, addr) == NULL)
exit(-1);
}
구현
exec();
case SYS_EXEC:
exec(f->R.rdi);
break;
int exec (const char *file_name) {
check_address(file_name);
int file_size = strlen(file_name) + 1;
char *fn_copy = palloc_get_page(PAL_ZERO);
if (!fn_copy) {
exit(-1);
return -1;
}
strlcpy(fn_copy, file_name, file_size);
if (process_exec(fn_copy) == -1) {
exit(-1);
return -1;
}
}
인자로 받은 실행 파일을 실행시킨다. 현재 실행 중인 프로세스의 이미지를 이 실행 파일 프로세스의 이미지로 바꿔치기한다. 새로운 프로세스를 생성하는 것은 아니다. fork()가 자신의 복사본을 생성해 실행한다면, 자신의 복사본이 아닌 아예 다른 프로그램을 실행해야 하는 경우에 exec()을 사용한다.
file_name을 copy를 해야 하는가?
copy를 하지 않으면, process_exec()
에서 process_cleanup()
을 할 때 해당 file_name
의 문자열의 물리적 메모리와의 매핑 정보를 담은 Page Table도 같이 지워지기 때문이다.
여기서 strlcpy()
하여 복사된 파일 이름 문자열은 커널 스택에 저장될 것이다. 따라서 process_cleanup()
후에도 커널 스택과 연결된 페이지 테이블을 통해 물리적 메모리와 매핑되어 활용될 수 있으므로, 애초에 fn_copy
문자열을 process_exec()
의 인자로 넣어준다.
실제로 파일 이름을 복사하지 않고 그대로 사용해서 process_cleanup()
후에 참조하려 시도하면 page fault가 뜬다.
wait();
case SYS_WAIT:
f->R.rax = wait(f->R.rdi);
break;
int wait (tid_t pid) {
return process_wait(pid);
}
userprog/process.c
int process_wait (tid_t child_tid UNUSED) {
/* XXX: Hint) The pintos exit if process_wait (initd), we recommend you
* XXX: to add infinite loop here before
* XXX: implementing the process_wait. */
struct thread *child = get_child_process(child_tid);
if (child == NULL)
return -1;
sema_down(&child->sema_wait);
int exit_status = child->exit_status;
list_remove(&child->child_elem);
sema_up(&child->sema_exit);
return exit_status;
}
wait_sema
를 DOWN해주고 BLOCK된다. 이제 자식 프로세스가 sema_up()
으로 명시적으로 풀어주지 않는 이상 부모 프로세스는 계속 잠들어 있는다.exit_status
등.exit_status
를 얻어온다. 그리고 자신의 child_list
에서 자식 프로세스를 지운다.exit_status
를 반환한다.userprog/process.c
void process_exit (void) {
•••
sema_up(&curr->sema_wait);
sema_down(&curr->sema_exit);
palloc_free_page(table);
process_cleanup ();
}
process_exit()
함수에 세마포어를 추가해 자식 프로세스가 종료되었을 때, 부모를 깨울수 있도록 다음과 같이 코드를 추가해준다.fork();
case SYS_FORK:
memcpy(&thread_current()->ptf, f, sizeof(struct intr_frame));
f->R.rax = fork(f->R.rdi);
break;
int fork (const char *thread_name) {
check_address(thread_name);
return process_fork(thread_name, &thread_current()->ptf);
}
userprog/process.c
tid_t process_fork (const char *name, struct intr_frame *if_ UNUSED) {
/* Clone current thread to new thread.*/
struct thread *cur = thread_current();
tid_t ctid = thread_create (name, PRI_DEFAULT, __do_fork, cur);
if (ctid == TID_ERROR)
return TID_ERROR;
struct thread *child = get_child_process(ctid);
sema_down(&cur->sema_fork);
return ctid;
}
현재 실행되고 있는 부모 프로세스를 자식 프로세스에게 복제한다 __do_fork()
. 자식이 fork를 완료할 때까지 BLOCK해 있다가, 자식이 fork를 완료하면 새롭게 생성된 자식 프로세스의 pid
를 반환한다.
이 때 인자로 받는 if_
는 시스템 콜 핸들러 함수에 인자로 들어가는, 시스템 콜을 부른 부모 프로세스의 인터럽트 프레임이다.
어차피 부모 프로세스의 인터럽트 프레임이면 그냥 parent->tf
를 하면 되지 뭐하러 parent->parent_if
로 if
를 복사해서 쓰냐?
if_ = parent->parent_if
이다.userprog/process.c
static void
__do_fork (void *aux) {
struct intr_frame if_;
struct thread *parent = (struct thread *) aux;
struct thread *current = thread_current ();
/* TODO: somehow pass the parent_if. (i.e. process_fork()'s if_) */
struct intr_frame *parent_if = &parent->ptf;
bool succ = true;
/* 1. Read the cpu context to local stack. */
memcpy (&if_, parent_if, sizeof (struct intr_frame));
if_.R.rax = 0;
•••
•••
int cnt = 2;
struct file **table = parent->fdt;
while (cnt < 128) {
if (table[cnt]) {
current->fdt[cnt] = file_duplicate(table[cnt]);
} else {
current->fdt[cnt] = NULL;
}
cnt++;
}
current->next_fd = parent->next_fd;
sema_up(&parent->sema_fork);
process_init ();
/* Finally, switch to the newly created process. */
if (succ)
do_iret (&if_);
error:
sema_up(&parent->sema_fork);
exit(TID_ERROR);
}
파일을 복사한다.
fdt
와 그에 매핑되는 파일도 모조리 똑같이 복사한다.자식이 fork가 끝날 때까지 잠들어있던 부모 프로세스를 깨운다.
if_의 값들을 모두 레지스터에 넣음으로서 자식 프로세스를 실행시킨다.
userprog/process.c
static bool
duplicate_pte (uint64_t *pte, void *va, void *aux) {
struct thread *current = thread_current ();
struct thread *parent = (struct thread *) aux;
void *parent_page;
void *newpage;
bool writable;
/* 1. TODO: If the parent_page is kernel page, then return immediately. */
if (is_kernel_vaddr(va))
return true;
/* 2. Resolve VA from the parent's page map level 4. */
parent_page = pml4_get_page (parent->pml4, va);
if (parent_page == NULL)
return false;
/* 3. TODO: Allocate new PAL_USER page for the child and set result to
* TODO: NEWPAGE. */
newpage = palloc_get_page(PAL_USER);
if (newpage == NULL)
return false;
/* 4. TODO: Duplicate parent's page to the new page and
* TODO: check whether parent's page is writable or not (set WRITABLE
* TODO: according to the result). */
memcpy(newpage, parent_page, PGSIZE);
writable = is_writable(pte);
/* 5. Add new page to child's page table at address VA with WRITABLE
* permission. */
if (!pml4_set_page (current->pml4, va, newpage, writable)) {
/* 6. TODO: if fail to insert page, do error handling. */
return false;
}
return true;
}
결과