1. 과제 목표: 시스템 콜 핸들러 및 시스템 콜 구현
Gitbook 설명을 보기 전에, 먼저 시스템 콜의 개념부터 살펴보자. (참고로, user memory access 파트는 시스템 콜의 일부로 check_address()를 구현하는 파트이다. 따라서 시스템 콜 구현에서 함께 설명한다.)
시스템 콜이란?
시스템 콜은, 위 이미지에서 얘기하는 것처럼 운영 체제가 제공하는 서비스에 대한 프로그래밍 인터페이스이다. 유저 프로그램이 함부로 OS에 접근하다가는 보안부터 시작해서 여러 문제를 야기할 수 있다. 또한 이전에 얘기했듯, 유저 프로그램이 하드웨어 단까지 내려가서 작업하는 게 여간 복잡한 게 아니다. 이를 위해서 운영체제를 제공한 것인데 운영체제 내부도 여간 복잡한 게 아니지 않겠나.
따라서 OS의 중요한 부분은 숨기고, 인터페이스만 제공해 유저 프로그램이 OS에 쉽게 접근하면서도 OS의 보안을 유지할 수 있도록 하는 장치 인터페이스가 바로 시스템 콜이다. 쉽게 말해 유저 프로그램이 커널 기능을 쉽게 이용할 수 있도록 하는 인터페이스이다. 유저 프로그램은 평상시에는 커널 영역에 접근하지 못하다가 커널 영역에 접근해야 할 일이 있을 때(I/O 접근이라던지 등) 시스템 콜을 호출한다. 그러면 커널 영역이 작업을 수행하고 반환값을 유저 프로그램에 전달한다.
보다 디테일하게 사용자 모드에서 커널 모드로 전환되는 과정을 살펴보자. 아래는 유저 프로그램에서 운영체제에게 write() 시스템 콜을 요청한 상황이다. 유저 프로그램에서 무언가 작성을 할 때, 이 작업을 사용자 혼자서 처리하는 게 아니다. 작성한 값을 출력 장치(모니터)에 띄워야 할 수도 있고, 하드 디스크로 접근해 저장해야 할 수도 있다. 이 두 과정 모두 커널(운영체제)의 도움이 필요하다. 따라서 write() 자체가 시스템 콜이며, write에 넣은 인자가 syscall로 넘어간다.
1. 유저 프로그램을 실행한다. 유저 프로그램에서는 write()을 호출한다.
2. write() 함수는 시스템 콜로, 인자를 유저 스택에 넣고서 커널로 진입한다. 이때 스택 포인터는 인자가 들어간 영역을 가리킨다.
3. 인터럽트 벡터 테이블에 가면 주소별로 어떤 종류의 인터럽트를 실행해야 하는지가 맵핑되어 있다. 이 중 0x30은 시스템 콜 핸들러를 호출하는 주소이다. 따라서 syscall_handler()를 호출한다.
4. 이제 우리가 구현해야 할 함수인 syscall_handler()에 도달했다. 유저 스택에 보면 넘버가 있는데, 이 넘버는 system call number이다.
이 작업을 수행할 수 있도록 시스템 콜 핸들러를 구현하는 게 이번 과제의 목표이다. 이어서 Gitbook 설명을 확인한다.
우리는 첫 번째 프로젝트 Alarm clock에서 운영체제가 사용자 프로그램으로부터 제어권을 다시 획득하는 방법 중 하나로 Timer & I/O interrupt를 처리했다. 이 timer & I/O는 모두 외부 인터럽트이다. (프로세스에 할당해준 시간이 만료되거나 I/O와 같은 입출력 장치로 인한 인터럽트는 CPU 바깥에서 이뤄지기에 외부 인터럽트)
운영체제는 프로그램 코드에서 발생하는 이벤트인 소프트웨어 exception도 처리한다. 이러한 exception에는 page fault 혹은 0으로 나누기 등으로 인한 에러가 있을 수 있다. exception은 유저 프로그램이 OS에게 서비스(시스템 콜에 해당)를 요청하는 수단이기도 하다.
syscall
명령은 x86-64에서 system call을 호출하는 가장 일반적인 수단이다.(이때, syscall 명령은 어셈블리어임! x86-64에서 업데이트된 요소) Pintos에서 역시 사용자 프로그램은 system call을 호출하기 위해 syscall을 수행한다. system call number와 추가 인자 어떤 것이든 두 가지 점을 제외하고 일반적인 방식으로 레지스터에 설정되어야 하는데, 그 이후에 syscall
을 호출한다. 이때 두 가지는
1. %rax
는 the system call number이다. (이따가 syscall_number() 구현할 때 쓴다)
2. 네 번째 인자는 %rcx
가 아닌 %r10
이다.
따라서 system call handler syscall_handler()
가 제어권을 얻을 때 system call number는 rax
에 있고 인자는 %rdi
, %rsi
, %rdx
, %r10
, %r8
, %r9
순서로 전달된다.
(*주의: %rdi, %rsi, ... => 얘네 레지스터에 들어가는 인자 값은 당연히 인자 순서대로 들어오는 애들과 매핑되는 값이며 어떤 함수에서 특정지어지는 값이 아니다. 예컨대 %rdi 는 반드시 스레드를 가리킨다던지. 그냥 해당 함수에서 들어오는 인자 순서대로 하나씩 연결해주면 됨! 순서만 기억하자.)
*이따 아래 과제를 해결할 때 이 파트가 중요하다. syscall_handler()를 구현할 때 인자로 인터럽트 프레임 포인터가 들어오는데, 이 인터럽트 프레임 안에 우리가 원하는 정보가 모두 들어있기 때문이다. 아래에서 다시 설명할 것.
caller(함수를 호출한 애. 호출당한 함수를 callee라고 함. 여기 링크 참고)의 레지스터는 인터럽트 프레임 구조체 struct intr_frame에 인자를 전달하기 위해 엑세스할 수 있다. (저번에 다뤘듯 intr_frame은 커널 스택에 존재한다.)
아래 system call을 구현하라
아래 코드는 사용자 프로그램에서 볼 수 있는 코드이다. 이 코드는 include/lib/user/syscall.h
에서 호출한다. 각 system call에 대한 system call number는 include/lib/syscall-nr.h
에서 정의한다.
(이하 코드 설명은 Gitbook에도 있으니 구태여 여기서 반복해서 적지 않겠다. 너무 길다..)
2. 과제 풀이
앞서 설명했듯, Gitbook에서 시스템 콜 과제 이전에 나오는 User memory access는 이후 시스템 콜 구현할 때 메모리에 접근할 텐데, 이때 접근하는 메모리 주소가 유저 영역인지 커널 영역인지를 체크하라는 과제이다. 이는 한양대 핀토스 pdf 시스템 콜 과제에 있는 check_address()에 해당하므로 이 함수부터 구현하고 시작한다.
check_address()
void check_address(void *addr) {
struct thread *t = thread_current();
/* --- Project 2: User memory access --- */
// if (!is_user_vaddr(addr)||addr == NULL)
//-> 이 경우는 유저 주소 영역 내에서도 할당되지 않는 공간 가리키는 것을 체크하지 않음. 그래서
// pml4_get_page를 추가해줘야!
if (!is_user_vaddr(addr)||addr == NULL||
pml4_get_page(t->pml4, addr)== NULL)
{
exit(-1);
}
}
함수는 간단하다. 해당 주소값이 유저 가상 주소(user_vaddr)에 해당하는지 아닌지 체크하고(is_user_vaddr()은 이미 주어져 있다.) 유저 영역이 아니면 종료해준다. 끝.
(*수정: 위에서 코드 하나를 더 추가해줬다(pml4_get_page(t->pml4, addr)== NULL)
포인터가 가리키는 주소가 유저 영역 내에 있지만 페이지로 할당하지 않은 영역일 수도 있다. pml4_get_page()는 유저 가상 주소와 대응하는 물리주소를 확인해서 해당 물리 주소와 연결된 커널 가상 주소를 반환하거나 만약 해당 물리 주소가 가상 주소와 매핑되지 않은 영역이면 NULL을 반환한다. 따라서 NULL인지 체크할 필요가 있다.)
그리고 한양대 핀토스 pdf에서는 Gitbook에서 모든 과제를 시스템 콜로 퉁치는 것과 달리 시스템 콜 / process hierachy로 구분하고 있다. 아무래도 시스템 콜 하나로 퉁치면 양이 너무 많기에 이 글에서는 한양대 핀토스 흐름을 따라가겠다. 따라서 여기서 구현할 함수는 아래와 같다.
위 4개 함수에 더해 syscall_handler()의 뼈대를 잡고 check_address(), get_argument()까지 구현한다. check_address()는 이미 했으니 syscall_handler()를 하고 위 4개 함수를 구현하겠다.
주의!!! 한양대 핀토스 자료에 있는 get_argument()는 구현할 필요 X!
32비트 x86에서는 프로그래머가 아래와 같이 직접 함수를 구현해서 스택에 쌓인 인자를 커널로 옮겨주는 작업을 수행해야 했다. 하지만 x86-64부터는 아까 위에 설명한 것처럼 syscall이라는 어셈블리어 명령이 추가되어 알아서 밑단에서 스택 인자를 커널로 옮겨준다. system call number를 R->rax에, 인자는 %rdi, %rsi, %rdx, %r10, %r8, %r9 에 전달한다. 그러므로 여기서는 get_argument()를 구현하지 않는다. 이외에도 32비트와 64비트 간에 약간씩 차이가 있으니 유념해서 구현해야 한다. 한양대 핀토스 자료를 100% 참고하지는 말 것.
이외에 여기서 syscall_handler()의 뼈대만 구현한다. 디테일한 건 다음 챕터에서.
syscall_handler()
void
syscall_handler (struct intr_frame *f UNUSED) {
/* 유저 스택에 저장되어 있는 시스템 콜 넘버를 가져와야지 일단 */
int sys_number = f->R.rax; // rax: 시스템 콜 넘버
/*
인자 들어오는 순서:
1번째 인자: %rdi
2번째 인자: %rsi
3번째 인자: %rdx
4번째 인자: %r10
5번째 인자: %r8
6번째 인자: %r9
*/
// TODO: Your implementation goes here.
switch(sys_number) {
case SYS_HALT:
halt();
case SYS_EXIT:
exit(f->R.rdi);
case SYS_FORK:
fork(f->R.rdi);
case SYS_EXEC:
exec(f->R.rdi);
case SYS_WAIT:
wait(f->R.rdi);
case SYS_CREATE:
create(f->R.rdi, f->R.rsi);
case SYS_REMOVE:
remove(f->R.rdi);
case SYS_OPEN:
open(f->R.rdi);
case SYS_FILESIZE:
filesize(f->R.rdi);
case SYS_READ:
read(f->R.rdi, f->R.rsi, f->R.rdx);
case SYS_WRITE:
write(f->R.rdi, f->R.rsi, f->R.rdx);
case SYS_SEEK:
seek(f->R.rdi, f->R.rsi);
case SYS_TELL:
tell(f->R.rdi);
case SYS_CLOSE:
close(f->R.rdi);
}
printf ("system call!\n");
thread_exit ();
}
앞서 설명한 것처럼, syscall_handler를 호출할 때 이미 인터럽트 프레임에 해당 시스템 콜 넘버에 맞는 인자 수만큼 들어있다. 그러니 각 함수별로 필요한 인자 수만큼 인자를 넣어준다. 이때 rdi, rsi, ...얘네들은 특정 값이 있는 게 아니라 그냥 인자를 담는 그릇의 번호 순서이다. 어떤 특정 인자와 매칭되는 게 아니라 첫번째 인자면 rdi, 두번째 인자면 rsi 이런 식이니 헷갈리지 말 것.
halt()
halt()는 호출 시 핀토스를 종료시키는 함수이다.
/* pintos 종료시키는 함수 */
void halt(void){
power_off();
}
exit()
exit()은 핀토스 전체가 아닌 현재 돌고 있는 프로세스만 종료시킨다.
/* 현재 프로세스를 종료시키는 시스템 콜 */
void exit(int status)
{
struct thread *t = thread_current();
printf("%s: exit%d\n", t->name, status); // Process Termination Message
/* 정상적으로 종료됐다면 status는 0 */
/* status: 프로그램이 정상적으로 종료됐는지 확인 */
thread_exit();
}
create()
파일을 생성하는 시스템 콜이다.
/* 파일 생성하는 시스템 콜 */
bool create (const char *file, unsigned initial_size) {
/* 성공이면 true, 실패면 false */
check_address(file);
if (filesys_create(file, initial_size)) {
return true;
}
else {
return false;
}
}
remove()
파일을 제거하는 함수이다. 이때, 파일을 제거하더라도 그 이전에 파일을 오픈했다면 해당 오픈 파일은 close되지 않고 그대로 켜진 상태로
남아있는다.
bool remove (const char *file) {
check_address(file);
if (filesys_remove(file)) {
return true;
} else {
return false;
}
}
일단 여기서 한 번 끊고 시스템 콜 핸들러 함수 내에서 호출하는 다른 함수들을 다음 글에서 마저 구현해보자.