user program은 컴퓨터 전체를 소유한다는 환상 하에 쓰여진다. 예를 들어서 메모리, cpu 등 자원들을 사용하는데 우리는 이것을 추상화 하여 각 프로세스가 진짜로 자원을 다 사용하고 있다고 착각하게 만든다. 또는 이 환상을 만족시키는 방향으로 관리를 해야 한다. 따라서 프로세스가 하면 안되는 작업을 하기 위해 시스템 콜을 호출하게 된다.
시스템 콜이란 유저프로그램이 못하는 작업을 커널에게 부탁을 하는 것이라고 생각할 수 있다.
따라서 project 2 의 목표는 OS와 user program이 상호작용을 할 수 있도록 시스템 콜 함수들을 구현하는 것이다.
위의 그림을 간단히 설명해보자면,
- 유저프로그램이 exec() 함수를 실행한다.
- exec()는 유저프로그램이 실행하지 못하는 작업이므로 커널에게 요청하는 시스템 콜을 보낸다. 이 때 어떤 시스템 콜인지 알려주는 것이 있어야 한다.
- 커널은 시스템 콜이 어떤 경우인지를 확인하여 해당하는 작업을 처리한다. 위의 경우 exec 요청을 exec함수를 통해 처리해야 한다.
- 커널 함수인 process_exec()에서 로딩과 같은 작업을 진행하고 완료했으면 반환한다.
lib/syscall-nr.h 에는 어떤 시스템 콜이 몇 번의 숫자와 연결 되어있는지 확인할 수 있다.
코드구현
void syscall_handler (struct intr_frame *f UNUSED) {
switch (f->R.rax)
{
case SYS_HALT:
halt_syscall();
break;
case SYS_EXIT:
exit_syscall(f->R.rdi);
break;
case SYS_FORK:
f->R.rax =fork_syscall(f->R.rdi);
break;
case SYS_EXEC:
if (exec_syscall(f->R.rdi) == -1)
{
exit_syscall(-1);
}
break;
case SYS_WAIT:
f->R.rax =wait_syscall(f->R.rdi);
break;
case SYS_CREATE:
f->R.rax = create_syscall(f->R.rdi,f->R.rsi);
break;
case SYS_REMOVE:
f->R.rax = remove_syscall(f->R.rdi);
break;
case SYS_OPEN:
f->R.rax = open_syscall(f->R.rdi);
break;
case SYS_FILESIZE:
f->R.rax = filesize_syscall(f->R.rdi);
break;
case SYS_READ:
f->R.rax = read_syscall(f->R.rdi, f->R.rsi, f->R.rdx);
break;
case SYS_WRITE:
f->R.rax = write_syscall(f->R.rdi, f->R.rsi, f->R.rdx);
break;
case SYS_SEEK:
seek_syscall(f->R.rdi, f->R.rsi);
break;
case SYS_TELL:
f->R.rax = tell_syscall(f->R.rdi);
break;
case SYS_CLOSE:
close_syscall(f->R.rdi);
break;
default:
exit_syscall(-1);
break;
}
}
다양한 system call 경우들을 case로 관리하는 system call handler 함수.
여기서 인자들은 f->R.rdi ... 이러한 방식대로 넘겨주게 된다. 이는 user level application에서 인자들을 %rdi, %rsi, %rdx, %r10, %r9, %r8 순서대로 담아놓기 때문이다.또한 rax는 return 값을 담아놓는 레지스터이므로 시스템 콜의 번호가 담겨져 있다.
시스템 콜 기능을 구현하기 위해, 유저 가상 주소 공간에 데이터를 읽고 쓰기 위한 방법을 제공해야한다. 시스템 콜의 인자로 들어온 포인터로부터 데이터를 읽어야 할 때 가상주소공간에 데이터를 읽어야하는 방법을 알아야한다.
유효하지 않은 포인터는 예를들어 커널 메모리를 가르키는 포인터 등을 가리키면 유저프로세스를 종료시킴으로써 대응할 수 있어야 한다.
유효하지 않은 포인터 종류
- invalid pointer : NULL 등 mapping 되지 않은 주소
- kernel pointer : KERN_BASE 상위 주소
- block partially in one of those regions : 영역의 일부가 비정상 블록
정리하자면 시스템 콜에서 주어지는 인자가 유저 메모리에 포함되는 주소여야 한다는 의미이다.
void check_address(void *addr) {
struct thread *curr = thread_current();
if (is_kernel_vaddr(addr) || addr == NULL || pml4_get_page(curr->pml4, addr) == NULL)
exit_syscall(-1);
}
kernel 주소이거나 NULL 주소이거나 페이지 테이블에 매핑이 되지 않은 주소면 프로세스를 종료하는 작업을 하는 함수.
이를 통해 유저 메모리에 포함되는 주소를 확인한다.
thread struct에 추가해야 할 사항이 있다.
struct thread{
#ifdef USERPROG
/* Owned by userprog/process.c. */
uint64_t *pml4; /* Page map level 4 */
/** #Project 2: System Call */
int exit_status;
int fd_idx; // 파일 디스크립터 인덱스
struct file **fd_table; // 파일 디스크립터 테이블
struct file *running_file; // 실행중인 파일
struct intr_frame parent_if; // 부모 프로세스 if
struct list child_list;
struct list_elem child_elem;
struct semaphore fork_sema; // fork가 완료될 때 signal
struct semaphore exit_sema; // 자식 프로세스 종료 signal
struct semaphore wait_sema; // exit_sema를 기다릴 때 사용
#endif
};
------
init_thread() 에 추가
#ifdef USERPROG
/** #Project 2: System Call */
t->running_file = NULL;
list_init(&t->child_list);
sema_init(&t->fork_sema, 0);
sema_init(&t->exit_sema, 0);
sema_init(&t->wait_sema, 0);
/** ----------------------- */
#endif
------
thread_create() 에 추가 _ thread 초기화
#ifdef USERPROG
/** #Project 2: System Call - 구조체 초기화 */
t->fd_table = palloc_get_multiple(PAL_ZERO, FDT_PAGES);
if (t->fd_table == NULL)
return TID_ERROR;
t->exit_status = 0; // exit_status 초기화
t->fd_idx = 3;
t->fd_table[0] = STDIN; // stdin 예약된 자리 (dummy)
t->fd_table[1] = STDOUT; // stdout 예약된 자리 (dummy)
t->fd_table[2] = STDERR; // stderr 예약된 자리 (dummy)
/** ---------------------------------------- */
/** #Project 2: System Call - 현재 스레드의 자식 리스트에 추가 */
list_push_back(&thread_current()->child_list, &t->child_elem);
#endif
System Call functions
1. halt() : 프로그램 종료
void halt_syscall(){
power_off();
}
2. exit() : 프로세스 종료
void exit_syscall(int status){
struct thread *cur = thread_current();
cur->exit_status = status;
printf("%s: exit(%d)\n", cur->name, cur->exit_status);
thread_exit();
}
----------
void process_exit (void) {
struct thread *curr = thread_current ();
/* TODO: Your code goes here.
* TODO: Implement process termination message (see
* TODO: project2/process_termination.html).
* TODO: We recommend you to implement process resource cleanup here. */
for (int fd = 0; fd <curr->fd_idx; fd++){
close_syscall(fd);
}
palloc_free_multiple(curr->fd_table,FDT_PAGES);
file_close(curr->running_file);
process_cleanup ();
sema_up(&curr->wait_sema); // 자식 프로세스가 종료될 때 부모에게 signal
sema_down(&curr->exit_sema); // 부모가 종료시켜줄 때 까지 대기
}
3. fork() : 자식 프로세스 생성하고 부모 프로세스의 값을 복제
pid_t fork_syscall(const char *thread_name){
check_address(thread_name);
return process_fork(thread_name,NULL);
}
----------
tid_t process_fork (const char *name, struct intr_frame *if_ UNUSED) {
THREAD *curr = thread_current();
struct intr_frame *tf = (pg_round_up(rrsp()) - sizeof(struct intr_frame));
memcpy(&curr->parent_if, tf,sizeof(struct intr_frame));
tid_t tid = thread_create(name,PRI_DEFAULT,__do_fork,curr);
if (tid == TID_ERROR)
return TID_ERROR;
THREAD *child = get_thread(tid);
sema_down(&child->fork_sema); // waiting for child fork complete
if(child->exit_status == TID_ERROR)
return TID_ERROR;
return child->tid; // 부모 프로세스의 리턴값 : 생성한 자식 프로세스의 tid
}
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;
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; // 자식 프로세스의 return 값 0
/* 2. Duplicate PT */
// 0으로 만들어준 페이지 생성
current->pml4 = pml4_create();
if (current->pml4 == NULL)
goto error;
// 페이지 활성화 , kernel stack 연결
process_activate (current);
#ifdef VM
supplemental_page_table_init (¤t->spt);
if (!supplemental_page_table_copy (¤t->spt, &parent->spt))
goto error;
#else
// 페이지 테이블 4단계 확인하기
if (!pml4_for_each (parent->pml4, duplicate_pte, parent))
goto error;
#endif
// 부모가 가진 fd 복사
if(parent->fd_idx >= FDCOUNT_LIMIT)
goto error;
current->fd_idx = parent->fd_idx;
struct file *file;
for (int fd = 0; fd < FDCOUNT_LIMIT ; fd ++){
file = parent->fd_table[fd];
if (file == NULL)
continue;
if (file >STDERR){
current->fd_table[fd] = file_duplicate(file);
}else{
current->fd_table[fd] = file;
}
}
// signal to parent , fork complete
sema_up(¤t->fork_sema);
process_init ();
/* Finally, switch to the newly created process. */
if (succ)
do_iret (&if_);
error:
sema_up(¤t->fork_sema);
exit_syscall(-1);
}
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_ZERO);
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;
}
4. exec() : 명령을 실행하는 함수
int exec_syscall(const char *cmd_line){
check_address(cmd_line);
off_t size = strlen(cmd_line)+1;
char *cmd_copy = palloc_get_page(PAL_ZERO);
if(cmd_copy == NULL){
return -1;
}
memcpy(cmd_copy,cmd_line,size);
return process_exec(cmd_copy);
}
----------
int process_exec (void *f_name) {
char *file_name = f_name;
bool success;
/* 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 ();
// ==== argument parsing ====
// argv_list , argc_num 만들기
char *argv_list[64]; // pintos에서 command line의 길이에는 128바이트 제한이므로 인자를 담을 배열을 64로 지정
char *token, *save_ptr;
int agrc_num = 0;
token = strtok_r(file_name, " ", &save_ptr);
argv_list[agrc_num] = token; // arg_list[0] = file_name_first
while (token != NULL)
{
token = strtok_r(NULL, " ", &save_ptr);
agrc_num++;
argv_list[agrc_num] = token;
}
// ==== argument parsing ====
/* And then load the binary */
success = load (file_name, &_if);
if (!success){
palloc_free_page (file_name);
return -1;
}
setup_argument(argv_list,agrc_num,&_if);
/* If load failed, quit. */
palloc_free_page (file_name);
/* Start switched process. */
do_iret (&_if);
NOT_REACHED ();
}
5. wait() : 부모 프로세스가 자식 프로세스의 작업 완료와 프로세스 종료를 기다리는 함수
int wait_syscall(pid_t tid){
return process_wait(tid);
}
-----------
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_thread(child_tid);
if(child == NULL)
return -1;
// 현재 부모프로세스 시점
sema_down(&child->wait_sema); // 자식 프로세스가 종료될 때 까지 대기
list_remove(&child->child_elem);
sema_up(&child->exit_sema); // 자식 프로세스가 죽을 수 있도록 signal
return child->exit_status;
}
'PintOS' 카테고리의 다른 글
PintOS Project 3 : Virtual Memory (1) - supplemetal page table 보조 페이지 테이블 (0) | 2024.10.01 |
---|---|
PintOS Project 2 : system call (2) (1) | 2024.09.18 |
PintOS Project 2 : argument passing (0) | 2024.09.18 |
PintOS Project 1 : Thread (Priority) (0) | 2024.09.01 |
PintOS Project 1 : Thread(Alarm Clock) (0) | 2024.08.27 |