글을 쓰고 있는 현재 시점(24.5.30)에서 프로젝트 2 테스트를 모두 통과하지는 못했지만 그래도 큰 흐름과 개념을 잡는 것이 더 중요하기 때문에 간단하게라도 정리를 해본다!!
User Memory Access
시스템 콜 과제 구현에 앞서서 먼저 주소 유효성 검사를 하는 함수를 만들어야 한다.
시스템 콜을 구현할 때 사용자 가상 주소 공간에서 데이터를 쓰게끔 하는 것이 중요하기 때문
만약 유저 영역을 벗어난 경우 프로세스를 종료시켜야 한다. -> 유효하지 않은 포인터나 커널 메모리로 접근을 시도하는 경우 예외 처리를 하게끔 먼저 함수를 구현해야 한다!
void check_address(void *addr) {
/*포인터가 가리키는 주소가 유저 영역의 주소인지 확인
잘못된 접근일 경우 프로세스 종료*/
struct thread *cur = thread_current();
if (!(is_user_vaddr(addr)) || pml4_get_page(cur-> pml4, addr) == NULL || addr == NULL) {
exit(-1);
}
/*is_user_vaddr(addr): 사용자 영역의 가상 메모리 주소인지 확인하는 함수
pml4_get_page(t->pml4, addr): 가상 메모리 주소의 유효성 판단 -> 해당 주소에 매핑된 페이지가 있는지 확인
=> NULL 반환하면 가상 주소가 유효하지 않거나 접근할 수 없는 영역*/
}
사용자 프로그램의 system call 호출 - 인터럽트를 발생시키는 명령 실행 -> 커널 모드로 진입
-> interrupt vector table에서 해당 인터럽트 번호에 매핑된 핸들러를 찾아서 실행
-> user stack에서 시스템 콜 번호와 해당 번호에 맞는 시스템 콜 함수 실행
-> 시스템 콜 처리
-> 각 시스템 콜 함수는 필요한 작업 수행후에 결과를 사용자 프로그램에 return
/* The main system call interface */
void
syscall_handler (struct intr_frame *f UNUSED) {
// TODO: Your implementation goes here.
/*User Stack에 저장되어 있는 시스템 콜 넘버를 이용해서 시스템 콜 핸들러 구현
스택 포인터가 유저 영역인지 확인
저장된 인자 값이 포인터일 경우 유저 영역의 주소인지 확인
0: halt, 1: exit . . . */
int syscall_num = f->R.rax; //rax: system call number
/*
인자 들어오는 순서:
1번째 인자: %rdi
2번째 인자: %rsi
3번째 인자: %rdx
4번째 인자: %r10
*/
switch (syscall_num) {
case SYS_HALT:
halt();
break;
case SYS_EXIT:
exit(f->R.rdi);
break;
case SYS_FORK:
f->R.rax = fork(f->R.rdi, f);
break;
case SYS_EXEC:
if (exec(f->R.rdi)== -1) {
exit(-1);
}
// f->R.rax = exec(f->R.rdi);
break;
case SYS_WAIT:
f->R.rax = wait(f->R.rdi);
break;
case SYS_CREATE:
f->R.rax = create(f->R.rdi, f->R.rsi);
break;
case SYS_REMOVE:
f-> R.rax = remove(f->R.rdi);
break;
case SYS_OPEN:
f->R.rax = open(f->R.rdi);
break;
case SYS_FILESIZE:
f->R.rax = filesize(f->R.rdi);
break;
case SYS_READ:
f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
break;
case SYS_WRITE:
f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
break;
case SYS_SEEK:
seek(f->R.rdi, f->R.rsi);
break;
case SYS_TELL:
f->R.rax = tell(f->R.rdi);
break;
case SYS_CLOSE:
close(f->R.rdi);
break;
default:
thread_exit();
break;
}
}
시스템 콜 핸들러는 운영체제의 커널 내부에 구현되어 있다. 사용자 프로그램이 시스템 콜을 요청하면 운영체제는 인터럽트나 트랩을 통해 커널모드로 전환한다.
이때 시스템 콜 핸들러가 활성화되어서 user stack에서 시스템 콜 번호와 인자들을 읽어서 해당하는 시스템 콜 함수를 호출하고 이를 통해 사용자 프로그램은 안전하게 커널이 제공하는 서비스를 활용할 수 있게 된다.
intr_frame
: 시스템 콜이 호출될 때 생성된 구조체 -> 시스템 콜이 완료된 후 사용자 프로그램으로 복귀할 때 필요한 정보를 담음!
1. 새로운 프로세스를 생성하는 가장 일반적인 방법은 기존 프로세스가 fork와 같은 시스템 콜을 통해 자식 프로세스를 만드는 것이다.
fork()
는 현재 실행 중인 프로세스를 복제해서 새로운 프로세스를 만든다. 초기 설정, 자원 할당 과정이 상대적으로 더 효율적이고 빠르다!
예시
- 부모 프로세스 - 클라이언트 요청을 수신하고 새로운 연결이 들어오면 fork 통해서 자식 프로세스 생성
- 새로운 자식 프로세스는 특정 클라이언트의 요청을 처리하고 요청 처리 끝나면 종료
exec()
시스템 콜핀토스에서의 우리의 과제)
- 프로세스 간의 부모 자식 관계를 구현하고, 부모가 자식 프로세스의 종료를 대기하는 기능을 구현해야 한다.
지금의 핀토스는 프로세스 구조체에서 부모/자식 관계를 명시하는 정보가 없다.
그래서 자식의 시작/종료 전에 부모 프로세스가 종료되고 있다.
부모 프로세스와 자식 프로세스, 프로세스 간의 계층 구조를 우리는 어떻게 구현할 것인가?
: 부모와 자식 관계를 나타낼 수 있는 필드를
thread
구조체 내에 포함시킨다.
struct intr_frame tf; /* Information for switching */
unsigned magic; /* Detects stack overflow. */
struct intr_frame parent_if; /*부모 프로세스의 인터럽트 프레임*/
struct list child_list; /*자식 프로세스 리스트*/
struct list_elem child_elem; /*자식 프로세스 리스트의 element*/
struct file *running; //현재 실행 중인 파일
int exit_status; //프로세스의 종료 유무 확인
struct semaphore wait_sema; //자식 프로세스가 종료될 때까지 대기 - 종료 상태 저장
struct semaphore free_sema; //자식 프로세스가 종료될 떄까지
struct semaphore fork_sema; //fork 완료 될 때 sema_up
참고)
핀토스에서는 프로세스는 1개의 쓰레드로 구성되어 있다. 그러니까 pid, tid를 여기서는 동일
현재 쓰레드를 복제하여 새로운 쓰레드 생성
-name
:새로운 쓰레드(자식 프로세스) 이름
-if_
: 인터럽트 프레임 주소, 현재 쓰레드의 상태를 자식 쓰레드에 복사하기 위해 사용한다.
tid_t
process_fork (const char *name, struct intr_frame *if_ UNUSED) {
/* Clone current thread to new thread.*/
struct thread *parent = thread_current(); //부모 쓰레드 - 현재 쓰레드
if (if_ != NULL) {
//부모 쓰레드의 인터럽트 프레임 복사
memcpy(&parent->parent_if, if_, sizeof(struct intr_frame));
} else {
return TID_ERROR;
}
//새로운 쓰레드(자식 프로세스) 생성
tid_t tid = thread_create(name, PRI_DEFAULT, __do_fork, parent);
//새로운 쓰레드 생성 실패 시 return TID_ERROR
if (tid == TID_ERROR) {
return TID_ERROR;
}
//생성된 자식 쓰레드를 tid를 통해서 찾음
struct thread *child = get_child_process(tid);
// 자식 쓰레드의 fork semaphore를 대기 상태로 만들어서 실행 일시 중지
//__do_fork 함수가 실행되어 로드가 완료될 때까지 부모는 대기한다.
if (child == NULL ){
return TID_ERROR;
}
sema_down(&child->fork_sema);
if (child->exit_status == TID_ERROR) {
return TID_ERROR;
}
return tid;
}
thread_current
=> 현재 쓰레드는 부모 쓰레드
memcpy(&parent->parent_if, if_, sizeof(struct intr_frame));
=> 부모 쓰레드의 인터럽트 프레임을 새로운 자식 쓰레드에 복사한다.
intr_frame: cpu 레지스터, 스택 포인터, 프로그램 카운터 등 현재 쓰레드의 실행 상태를 포함함
=> 자식 쓰레드가 생성될 떄 부모 쓰레드의 실행 상태 그대로 가져갈 수 있게끔. 자식 쓰레드가 동일한 실행 흐름을 가질 수 있게.
그리고
sema_down(&child->fork_sema);
해당 자식 프로세스가 로드될 때까지 부모프로세스가 기다릴 수 있도록 semaphore를 사용한다. 세마포어의 값이 0이 될 때까지 부모 프로세스는blocked
상태가 된다.자식 프로세스가
__do_fork
함수 안에서sema_up
으로 신호를 보내면 부모 쓰레드는 깨어나서 Ready 상태로 전환된다.
struct thread *cur = thread_current();
list_push_back(&cur->child_list, &new_t->child_elem);
thread_create
함수 안에서 자식 프로세스를 현재 쓰레드의 자식 리스트에 추가해준다.
: 현재 실행 중인 자식 쓰레드가 부모 쓰레드에서 페이지를 복제.
메모리 공간에서 부모 프로세스의 페이지를 자식 프로세스로 복제하는 함수이다.
-> Page Table Entry?
가상 메모리를 물리 메모리로 매핑하는 과정에서 사용되는 자료구조. 각 엔트리는 해당 페이지가 물리 메모리의 어느 위치에 있는지, 페이지가 읽기 전용인지 쓰기 가능한지 등의 정보가 포함되어 있음
- 물리 메모리 주소
- valid bit
- 읽기/쓰기 권한
- 캐싱 정책
- 참조 비트, 수정 비트: 참조되거나 수정된 적이 있는지
=> 부모 프로세스의 페이지가 쓰기 가능한지 확인하고, 자식 프로세스에게 동일한 권한을 부여하기 위해 필요하다.
/* Duplicate the parent's address space by passing this function to the
* pml4_for_each. This is only for the project 2. */
// 부모 프로세스의 주소 공간을 자식 프로세스로 복사하는 기능
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. */
//만약 va가 커널 주소라면 즉시 return true
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); //부모의 pml4에서 va에 해당하는 페이지 가져옴
//만약 부모 페이지가 null이면 return false
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 | PAL_ZERO);//자식 프로세스를 위해 새로운 PAL_USER 페이지 할당
//만약 페이지 할당에 실패한다면 false return
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); //페이지 테이블 엔트리(pte)를 통해서 부모 페이지가 쓰기 가능한지 확인
// writable = (*pte & PTE_W) != 0; //비트 연산(&)을 통해 페이지 쓰기가 가능한지 확인
/* 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. */
palloc_free_page(newpage); //할당한 페이지 해제
return false;
}
return true;
}
부모 프로세스의 상태를 자식 프로세스에 복제하고 필요한 자원을 복제해서 자식 프로세스가 독립적으로 실행할 수 있도록
__do_fork()
: 부모 프로세스로부터 자식 프로세스를 생성하는 작업 수행 - 인자 aux는 부모 쓰레드이다.
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; //부모 프로세스의 intr_frame 포인터
/* 1. Read the cpu context to local stack. */
/* 부모 프로세스의 CPU 컨텍스트를 자식 프로세스의 로컬 스택에 복사*/
memcpy (&if_, parent_if, sizeof(struct intr_frame));
/* 2. Duplicate PT
페이지 테이블 복제*/
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
/* TODO: Your code goes here.
* TODO: Hint) To duplicate the file object, use `file_duplicate`
* TODO: in include/filesys/file.h. Note that parent should not return
* TODO: from the fork() until this function successfully duplicates
* TODO: the resources of parent.*/
if (parent->next_fd_idx == FDT_COUNT_LIMIT) //파일 디스크립터 테이블이 가득 찬 경우 에러 처리
goto error;
//표준 파일 디스크립터(0,1)을 제외하고 복제
for (int fd = 2; fd < FDT_COUNT_LIMIT; fd++){
struct file *file = parent->fdt[fd]; //부모의 파일 디스크립터를 가져옴
if (file == NULL)
continue;
current->fdt[fd] = file_duplicate(file); //파일 복제
}
current ->next_fd_idx = parent->next_fd_idx; // 다음 파일 디스크립터의 인덱스 설정
sema_up(¤t->fork_sema); //자식 프로세스가 준비되었음을 부모에게 알림
if_.R.rax = 0; //자식 프로세스의 리턴값 0으로 설정
process_init ();
/* Finally, switch to the newly created process.
새로 생성된 프로세스로 전환*/
if (succ)
do_iret (&if_);
error:
// succ = false;
sema_up(&parent->fork_sema); //에러 발생시 세마포어를 올려, 부모가 기다리지 않도록 한다.
exit(TID_ERROR); //에러 발생 시 자식 프로세스 종료시킴
// thread_exit ();
}
자식 프로세스에 부모의 인터럽트 프레임을 복사해서 넣어주고, 부모 프로세스의 페이지 테이블을 자식 프로세스의 페이지 테이블로 복사해준다.
memcpy(newpage, parent_page, PGSIZE);
이후에 부모 프로세스에서 자식 프로세스의 복사가 다 완료되면
sema_up(&parent->fork_sema)
을 통해서 부모 프로세스에게 알려준다.
이후 do_iret
을 통해서 새로운 자식 프로세스의 인터럽트 프레임으로 전환되어 자식 프로세스가 실행된다.
부모 프로세스의 상태를 복사해서 독립적으로 자식이 실행흐름이 나아갈 수 있도록.
//현재 쓰레드의 자식 쓰레드 중에서(child_list) 주어진 pid와 일치하는 자식 쓰레드를 찾아서 return
//int pid => 찾고자 하는 자식 프로세스 식별자
// 주어진 자식 프로세스 식별자(pid)에 해당하는 쓰레드 구조체 검색 - 존재하지 않으면 NULL return
struct thread *get_child_process (int pid){
struct thread *cur = thread_current();
struct list *child_list = &cur->child_list; //현재 쓰레드의 자식 쓰레드 목록
//자식 쓰레드 목록을 순회하면서 주어진 pid와 일치하는 자식 쓰레드가 있는지 찾는다.
for (struct list_elem *e = list_begin(child_list); e != list_end(child_list); e = list_next(e)){
struct thread *t = list_entry(e, struct thread, child_elem);
//일치하는 자식 프로세스 반환
if (t->tid == pid){
return t;
}
}
return NULL; //일치하는 자식 쓰레드가 없을 경우 return NULL
: 자식 프로세스가 종료될 때까지 부모 프로세스가 기다리는 함수
대기하려는 자식의 tid를 인자로 받는다.
int
process_wait (tid_t child_tid UNUSED) {
//자식 프로세스의 쓰레드 구조체 child
struct therad *cur = thread_current();
int result = -1;
struct thread *child = get_child_process(child_tid);
//자식 프로세스가 존재하지 않는다면 return -1
if (child == NULL) {
return -1;
}
//자식 프로세스가 종료될 때까지 대기
sema_down(&child->wait_sema);
result = child->exit_status;
//자식 프로세스의 exit_status를 가져옴.
// int exit_status = child->exit_status;
//자식 프로세스를 child_list에서 제거
list_remove(&child->child_elem);
//자식 프로세스를 해제할 수 있도록 신호를 보냄
sema_up(&child->free_sema);
//자식 프로세스의 exit_status를 return
// return exit_status;
return result;
}
자식 프로세스가 성공적으로 종료될 때까지 부모 프로세스는 대기한다.
자식 프로세스가 process_exit
함수를 통해 종료되고 wait_sema up이 되면 부모 프로세스는 ready 상태가 된다.
그리고 부모 프로세스는 child_list에서 해당 자식을 제거하고
free_sema
down을 통해 자식 프로세스의 리소스를 완전히 해제할 수 있게 된다. -> 부모 프로세스는 자식 프로세스의 종료 상태를 확실하게 알 수 있다.
void
process_exit (void) {
struct thread *cur = thread_current ();
file_close(cur->running);
cur->running = NULL;
//프로세스에 열린 모든 파일을 닫음
for (int i = 0; i < FDT_COUNT_LIMIT; i++){
close(i);
}
palloc_free_multiple(cur->fdt, FDT_PAGES);
process_cleanup ();
//부모 프로세스가 프로세스가 종료되었음을 알 수 있게 wait_sema up
sema_up(&cur->wait_sema);
//부모 프로세스가 free_sema 자원을 해제할 때까지 대기
sema_down(&cur->free_sema);
}