🔑
1. 모든 함수, 전문용어들은 영어로 표현
(ex: 쓰레드 -> thread, 프로세스 -> process) ( 이유 : 엄밀성을 위해. 프로세스 vs 스레드 구분도 명확히)
2. 단원 제목이 아닌 경우, 모든 영어는 소문자 유지
(ex : Process -> process, PintOS-> pintOS)
1. Passing Arguments
Goal 🔍
user program을 적재하고 실행할 때, command line argument가 stack에 쌓여 올바르게 전달될 수 있도록 해야한다.
process_exec() 실행을 위해 command line argument를 ‘parsing’하는 것이 목표
이를 위해서 user program이 load될 수 있는 기반 구조를 작성해야한다.
process를 생성 및 시작하는 process_exec(void *f_name)함수가 명령어를 받도록 해야한다.
pintOS 에서는 하나의 process는 하나의 thread에 mapping되어 있는데, 따라서 user process를 load하고 시작하는 thread function인 thread_create()에서 f_name에서 명령어를 파싱하여 filename과 argument를 알아내고, 해당 파일을 load(적재) 후 argument를 전달해주면 된다.
이에 대해 우리는 arguments parsing을 load()함수 내부에서 진행해야 하는것인지, load()함수를 호출하기 직전에 한 뒤 load()함수 성공여부에 따라서 process_exec()종료 시점에 argument를 전달해야 할 지 고민했고, 둘 다 구현했다. 결과적으로, 전부 정상적으로 test를 통과했다.
strtok_r를 이용해서 공백을 구분자로 끊을 수 있었다. 이 함수를 이용해서 모든 인자들을 띄어쓰기를 기준으로 끊어 버퍼에 저장한다. 페이지 영역은 4KB로 2048개의 인자를 저장할 수 있는데, buffer overflow등을 처리하지는 않았다.
load()를 성공적으로 수행하면, 매개변수들을 (if가 지정하는 주소 영역) 스택에 넣는 함수를 호출한다.
스택을 넣는 함수 구현은 깃북에서 제시한 calling convention에 따라 (rsp가 높은 순서에서 낮은 순서대로) 다음과 같이 작성했다.
- 가장 먼저 argument 들의 string pool을 스택에 쌓는다. memcpy로 스택 영역에 토큰들의 스트링을 전부 복사한다. 이 때 각 토큰별로, 스트링이 시작하는 스택의 시작 주소들을 기억해 둔다.
- word align : string pool이 끝났으므로, rsp를 8의 배수로 맞추어 준다.(padding값을 넣어준다.)
- argv의 마지막 (argv[argc])은 0을 셋팅한다.
- argv[argc-1], … , argv[0]을 셋팅한다. ( 1번에서 기억해둔 각 스택의 시작 주소를 집어넣는다.)
- argv의 주소를 세팅한다.
- argc를 세팅한다.
- return addr (0) 을 세팅한다. (fake address를 세팅한다.)
2. System Call
Goal 🔍
user program이 요청하는 system call을 처리해주는 system call handler를 구현하는 것이 목표
pintOS에서 user program이 사용하는 system call들은 모두 interrupt로 구현이 됨. system call handler가 해당 interrupt를 처리하는 구조.
system call 의 number, arguments는 모두 stack에 쌓여서 전달되고 user program의 stack을 참조하여 접근 가능. calling convention에 따르면 system call number는 rsp가 가리키는 8byte 값이므로, 이를 참조하여 적절한 kernel 쪽의 system call 함수들이 실행되도록 하는 방식이다. 예를 들어 syscall_number가 SYS_WAIT이면 wait(pid) 함수가 호출된다.각 system call마다 필요한 argument들은 user stack을 참조하여 (ex : rsp+8, rsp+16) 얻을 수 있다.
pintOS는 일반적인 OS와 달리, thread가 곧 user program인 간단한 모델을 사용하고 있다. 프로세스를 관리하는 적절한 자료 구조의 설계가 필요했고, parent-child process관계를 고려하여 잘 설계해야 한다.
이를 위해 두 가지 방안이 팀 내에서 논의됐다.
- struct thread 의 ‘멤버’로 자료 구조를 추가하는 방법.
- struct process_control_block 구조체를 생성하여 관리하는 방법.
두 가지 방법의 가장 큰 차이점은 exec() 함수를 구현할때 볼 수 있었다.
전자의 경우 명령어를 인자로 process_exec()을 호출했지만, 후자의 경우 pcb의 멤버를 수정하여 pcb를 인자로 process_exec()을 호출했다.
int exec(const *cmd_line) {
int size = strlen(cmd_line) + 1;
char *fn_copy = palloc_get_page(PAL_ZERO);
if(fn_copy == NULL){
exit(-1);
}
strlcpy(fn_copy, cmd_line, size);
lock_acquire(&filesys_lock);
if (process_exec(fn_copy) == -1){
lock_release(&filesys_lock);
return -1;
}
lock_release(&filesys_lock);
NOT_REACHED();
return 0;
}
(전자) struct thread의 멤버로 자료구조를 추가
int exec (const char *cmd_line) {
struct thread *cur = thread_current();
check_address((const uint8_t*)cmd_line);
int siz = strlen(cmd_line) + 1;
char *fn_copy = palloc_get_page(PAL_USER);
if (fn_copy == NULL)
exit(-1);
strlcpy(fn_copy, cmd_line, siz);
cur->pcb->cmdline = fn_copy;
if (process_exec(cur->pcb) == -1)
return -1;
}
(후자) struct process_control_block 구조체를 생성하여 관리하는 방법.
3. File Descriptor
Goal 🔍
시스템 콜 중에서 파일과 관련된 create, remove, open, filesize, seek, tell, close, read, write 등을 구현한다.
파일 관련 시스템 콜을 구현하기 위해서는 , 프로세스별로 열고 있는 파일들을 관리할 수 있도록 하는 자료 구조를 도입해야 한다.
이를 위해 두 가지 방안이 팀 내에서 논의됐다.
- struct thread 의 멤버로 배열( 이중 포인터 )를 활용하는 방법.
- struct file_desc 구조체를 생성하여 연결 리스트로 관리하는 방법.
두 가지 방법의 가장 큰 차이점은 open()함수에서 볼 수 있었다.
전자의 경우 file을 open할 때마다 이중배열로 구현된 file descriptor table의 가장 마지막 자리에 file descriptor를 할당했다. (file descriptor table **fdTable의 형태는 ‘index : fd, value : file’) file descriptor table의 가장 뒤에 file을 삽입하기 위해, 마지막 index를 찾는 보조함수(add_file_to_fdt)도 구현하였다.
int
open(const char *file){
lock_acquire(&filesys_lock);
struct file *f_open = filesys_open(file);
lock_release(&filesys_lock);
if (f_open == NULL){
return -1;
}
int new_fd = add_file_to_fdt(f_open);
/* full fdt */
if (new_fd == -1){
file_close(f_open);
}
return new_fd;
}
(전자) struct thread 의 멤버로 배열( 이중 포인터 )를 활용하는 방법
int add_file_to_fdt(struct file *file){
/* create fd for *f */
struct thread *cur = thread_current();
struct file **fdt = cur->fdTable;
while (cur->next_fd < FDCOUNT_LIMIT && fdt[cur->next_fd] != NULL ){
cur->next_fd++;
}
if (cur->next_fd >= FDCOUNT_LIMIT){
return -1;
}
fdt[cur->next_fd] = file;
return cur->next_fd;
}
(후자) index를 찾는 보조함수(add_file_to_fdt)
후자의 경우 file을 open할 때마다 palloc_get_page()로 file_desc 구조체를 위한 메모리를 할당했다. 이후 file데이터를 구조체에 바인딩 하고, thread 구조체의 file_descriptors 리스트에 삽입했다.
int open (const char *file) {
check_address((const uint8_t*) file);
struct file* file_opened;
struct file_desc* desc = palloc_get_page(PAL_USER);
if (!desc) {
palloc_free_page (desc);
return -1;
}
lock_acquire (&filesys_lock);
file_opened = filesys_open(file);
if (!file_opened) {
palloc_free_page (desc);
lock_release (&filesys_lock);
return -1;
}
desc->file = file_opened;
struct list* fd_list = &thread_current()->file_descriptors;
if (list_empty(fd_list)) {
desc->id = 3;
}
else {
desc->id = (list_entry(list_back(fd_list), struct file_desc, elem)->id) + 1;
}
list_push_back(fd_list, &(desc->elem));
lock_release (&filesys_lock);
return desc->id;
}
(후자) struct file_desc 구조체를 생성하여 연결 리스트로 관리하는 방법
4. 각종 예외 처리
정상적인 실행 결로 말고도 유저 프로그램이 잘못된 argument를 전달하거나 잘못된 메모리를 접근하는 경우, 커널에 리소스가 부족한 경우 등에 여러가지 예외 케이스에 대해 올바른 처리를 하는 것이 중요했다. 진행하며 생긴 대부분의 fail들이 예외 처리에서 발생했다.
예외처리를 위해 감안해야 한 부분은 크게 아래와 같다.
- palloc_get_page() 등을 사용하여 메모리 영역을 할당받는 경우, allocation에 실패하면 NULL이 리턴될 수 있다. 이 때에는 깃북에 명시된 각 함수의 요구사항에 따라 특정 리턴값 (주로 -1을 반환) 을 반환할 수 있도록 처리해야 한다.
- 잘못된 메모리 영역을 접근하는 경우 등 프로세스가 비정상적으로 종료되어야 하는 경우 exit(-1)를 통해 프로세스가 종료하도록 처리한다. exit()함수는 해당 프로세스가 점유하고 있던 모든 자원 ( 잡고있는 lock을 포함 ) 을 해제하고 부모 프로세스가 자신을 기다리는 경우 등에는 적절한 동기화 처리가 일어날 수 있도록 한다.
5. Synchronization and Lock
현재 구현되어 있는 file system은 내부 동기화가 없기 때문에, 최대 하나의 프로세스만 파일 시스템에 접근하는 것을 보장하기 위해서 파일 접근을 하는 시스템 콜의 전 후로 lock을 잡아주었다. 이 때 사용한 lock은 syscall.c에 선언되어 있으며, 파일 시스템 관련된 시스템 콜을 수행하기 전에는 lock을 acquire, 이후에는 (실패한 경우에도) lock을 release 하도록 구현한다.
struct thread의 멤버로 자료구조를 추가한 pintOS 결과
FAIL tests/filesys/base/syn-read
FAIL tests/filesys/base/syn-write
2 of 95 test failed.
struct pcb, struct file_desc 추가한 pintOS 결과
FAIL tests/userprog/fork-read
FAIL tests/userprog/fork-close
FAIL tests/userprog/exec-read
FAIL tests/userprog/multi-child-fd
FAIL tests/userprog/rox-child
FAIL tests/userprog/rox-multichild
FAIL tests/userprog/no-vm/multi-oom
7 of 95 tests failed.
회고
- pintOS 프로젝트를 진행하며, 이전에는 경험해보지 못한 방대한 양의 코드를 읽고 이해했다. 이런 작업은 많은 스트레스를 받게했다. 하지만, 실무에서는 비교도 못할 양을 접하게 될 것이라고 생각한다. 이번 프로젝트는 실무에서 발생가능한 스트레스에 대해 미리 경험했다고 느꼈다.
- 실제 OS와 pintOS의 구조가 다르다는 사실을 되새겨야겠다는 생각을 했다. pintOS의 작동원리에 따라 기능을 구현해야 한다. gitbook을 최대한 활용해야 한다는 것을 이번 프로젝트에서도 느꼈다.
- 함수의 기능을 구현하는 작업에서, 논리적으로 코드를 작성하는 것도 중요하지만 해당 함수를 어디에서 호출해야 하는지도 중요하다는 것을 느꼈다. ( ex. 구조체 초기화, 자원 할당 및 해제 등 )
'Krafton_Jungle' 카테고리의 다른 글
[WEEK FINAL] 크래프톤 정글 1기 (2) | 2023.04.12 |
---|---|
[WEEK 18] 크래프톤 정글 1기 (3) | 2023.03.14 |
[WEEK 10] 크래프톤 정글 1기 (2) | 2023.01.02 |
[WEEK 07] 크래프톤 정글 1기 (0) | 2022.12.13 |
[WEEK 06] 크래프톤 정글 1기 (1) | 2022.12.06 |
댓글