[PintOS-KAIST]Project2: SYSTEMCALL(1)
project2의 핵심내용인 systemcall에 대해서 작성을 해보도록 하겠다.
systemcall이란 운영체제의 커널이 제공하는 서비스에 대해, 응용 프로그램의 요청에 따라 커널에 접근하기 위한 인터페이스이다. 보통 c나 c++과 같은 고급 언어로 작성된 프로그램들은 직접 시스템 호출을 사용할 수 없기 때문에 고급 API를 통해 시스템 호출에 접근하게 하는 방법이다. 즉, 운영체제 서비스에 접근하기 위한 유일한 수단인 소리이다.
그럼 이러한 systemcall이 필요한 이유는 무엇일까?
- 사용자가 직접적으로 하드웨어 장치를 제어한다면 큰 문제가 발생할 수 있다. 따라서 사용자 어플리케이션은 System Call 을 통해 직접적인 H/W 요청이나 중요한 시스템 요청을 하는 것이다.
- 우리가 일반적으로 사용하는 프로그램은 응용프로그램이다. 유저레벨의 프로그램은 유저레벨의 함수들 만으로는 많은 기능을 구현하기 힘들기 때문에 커널의 도움을 반드시 받아야 한다.
- 커널에 관련된 것은 커널모드로 전환된 후에야 해당 작업을 수행할 권한이 생긴다. 커널모드를 통한 이러한 작업은 반드시 시스템 콜을 통해 수행하도록 설계되어 있다.
여기서 사용자모드와 커널모드란 무엇일까?
- 유저레벨, 즉 사용자 모드는 프로그램 실행, 코드 작성 등을 하는 상태를 말한다.
- 커널 모드는 시스템 콜을 처리하는 상태를 말한다.
- 사용자 모드와 커널 모드는 H/W 내부에 존재하는 Mode bit를 통해 구분하는데, 모든 비트가 0 이면 커널 모드, 1 이면 사용자 모드가 된다.
- 사용자가 만들어내는 프로그램은 불안정하고 보안이 부족할 수 있기 때문에, 시스템에 큰 영향을 미칠 수 있는 연산들은 커널 모드에서 이루어진다. 즉, H/W, S/W 추상화를 통해 컴퓨터 전체의 보안을 유지할 수 있게 되는 것이다.
systemcall의 유형에는 어떤것이 있을까?
- 프로세스 제어
프로세스 생성, 중지(fork, exit, exec, ...) - 파일 조작
파일 생성, 삭제, 속성 관리 등(create, open, close, lseek, ...) - 장치 관리
장치 연결, 방출, 속성 관리 등(open, close, ioctl, ...) - 정보 유지
시간, 날짜 설정, 프로세스와 연결된 파일, 장치 관리 등(time, data, dump, pid, ...) - 통신
파일, 사용자 권한 등(chmod, umask, ...)
systemcall의 호출은 어떻게 진행될까?
1. 유저프로그램에서 fork(스레드이름); 으로 fork 호출
2. fork() : syscall1(fork의 시스템콜 번호, 스레드이름); 호출
/* lib/user/syscall.c 파일 */
pid_t
fork (const char *thread_name){
return (pid_t) syscall1 (SYS_FORK, thread_name);
}
3. syscall1() : syscall(fork의 시스템 콜 번호, 스레드 이름); 호출
#define syscall1(NUMBER, ARG0) ( \
syscall(((uint64_t) NUMBER), \
((uint64_t) ARG0), 0, 0, 0, 0, 0))
4. syscall(): CPU 레지스터 %rax에 시스템 콜 번호, %rdi 에 스레드 이름을 넣고, syscall 실행
__attribute__((always_inline))
static __inline int64_t syscall (uint64_t num_, uint64_t a1_, uint64_t a2_,
uint64_t a3_, uint64_t a4_, uint64_t a5_, uint64_t a6_) {
int64_t ret;
register uint64_t *num asm ("rax") = (uint64_t *) num_;
register uint64_t *a1 asm ("rdi") = (uint64_t *) a1_;
register uint64_t *a2 asm ("rsi") = (uint64_t *) a2_;
register uint64_t *a3 asm ("rdx") = (uint64_t *) a3_;
register uint64_t *a4 asm ("r10") = (uint64_t *) a4_;
register uint64_t *a5 asm ("r8") = (uint64_t *) a5_;
register uint64_t *a6 asm ("r9") = (uint64_t *) a6_;
__asm __volatile(
"mov %1, %%rax\n"
"mov %2, %%rdi\n"
"mov %3, %%rsi\n"
"mov %4, %%rdx\n"
"mov %5, %%r10\n"
"mov %6, %%r8\n"
"mov %7, %%r9\n"
"syscall\n" /* syscall 실행 */
: "=a" (ret)
: "g" (num), "g" (a1), "g" (a2), "g" (a3), "g" (a4), "g" (a5), "g" (a6)
: "cc", "memory");
return ret;
}
5. syscall : syscall-entry 실행 (핀토스 부팅시 init.c에서 실행되는 syscall_init에서 syscall 호출시 syscall-entry가 실행되도록 이미 설정되어있기 때문)
#define MSR_STAR 0xc0000081 /* Segment selector msr */
#define MSR_LSTAR 0xc0000082 /* Long mode SYSCALL target */
#define MSR_SYSCALL_MASK 0xc0000084 /* Mask for the eflags */
void
syscall_init (void) {
write_msr(MSR_STAR, ((uint64_t)SEL_UCSEG - 0x10) << 48 |
((uint64_t)SEL_KCSEG) << 32);
write_msr(MSR_LSTAR, (uint64_t) syscall_entry); /* syscall ->syscall_entry 실행 */
/* The interrupt service rountine should not serve any interrupts
* until the syscall_entry swaps the userland stack to the kernel
* mode stack. Therefore, we masked the FLAG_FL. */
write_msr(MSR_SYSCALL_MASK,
FLAG_IF | FLAG_TF | FLAG_DF | FLAG_IOPL | FLAG_AC | FLAG_NT);
lock_init(&file_lock);
}
6. syscall-entry.S :
- CPU의 rsp 값에 tss에 저장된 커널스택의 스택포인터를 저장
→ 이제 rsp는 유저 스택이 아닌 커널 스택을 가리킴 = 커널모드 시작/* syscall-entry.S 부분 */ movabs $tss, %r12 movq (%r12), %r12 movq 4(%r12), %rsp /* Read ring0 rsp from the tss */
- CPU 레지스터 값들을 커널 스택에 push
: push 되는 순서는 마지막 들어간 데이터부터 interrupt frame 구조체 순서가 되도록 하는 순서로 push/* syscall-entry.S 부분 */ push $(SEL_UDSEG) /* if->ss */ push %rbx /* if->rsp */ push %r11 /* if->eflags */ push $(SEL_UCSEG) /* if->cs */ push %rcx /* if->rip */ subq $16, %rsp /* skip error_code, vec_no */ push $(SEL_UDSEG) /* if->ds */ push $(SEL_UDSEG) /* if->es */ push %rax movq temp1(%rip), %rbx push %rbx pushq $0 push %rdx push %rbp push %rdi push %rsi push %r8 push %r9 push %r10 pushq $0 /* skip r11 */ movq temp2(%rip), %r12 push %r12 push %r13 push %r14 push %r15 movq %rsp, %rdi
- 커널 스택에 인터럽트 프레임 구조체 형태의 데이터가 다 들어가 있고, rsp는 이 시작점을 가리키는 형태
→ syscall_handler() 호출 시 인자로 rsp를 넘겨주면 핸들러는 *if 를 받았다고 생각할 수 있음/* syscall-entry.S 부분 */ movabs $syscall_handler, %r12 call *%r12
7. syscall_handler (struct intr_frame *f):
인자로 들어온 f는 커널스택의 rsp이고, rsp부터는 intr_frame 형태의 데이터들이 순차적으로 들어가 있기 때문에 이 자체를 인터럽트 프레임처럼 사용할 수 있는 것 → f의 rax 값을 통해 필요한 시스템 콜을 수행하는 함수를 호출한다.
void syscall_handler(struct intr_frame *f UNUSED) {
uint64_t syscall_num = f->R.rax;
// TODO: Your implementation goes here.
struct thread *curr = thread_current();
switch (syscall_num) {
case SYS_HALT:
power_off(); /* Halt the operating system. */
break;
case SYS_EXIT: /* Terminate this process. */
exit(f->R.rdi);
break;
case SYS_FORK:
f->R.rax = fork(f->R.rdi, f);
break; /* Clone current process. */
case SYS_EXEC:
f->R.rax = exec(f->R.rdi);
break; /* Switch current process. */
case SYS_WAIT:
f->R.rax = wait(f->R.rdi);
break; /* Wait for a child process to die. */
case SYS_CREATE:
f->R.rax = create(f->R.rdi, f->R.rsi);
break; /* Create a file. */
case SYS_REMOVE:
f->R.rax = remove(f->R.rdi);
break; /* Delete a file. */
case SYS_OPEN:
f->R.rax = open(f->R.rdi);
break; /* Open a file. */
case SYS_FILESIZE:
f->R.rax = filesize(f->R.rdi);
break; /* Obtain a file's size. */
case SYS_READ:
f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
break; /* Read from a file. */
case SYS_WRITE:
f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
break; /* Write to a file. */
case SYS_SEEK:
seek(f->R.rdi, f->R.rsi);
break; /* Change position in a file. */
case SYS_TELL:
f->R.rax = tell(f->R.rdi);
break; /* Report current position in a file. */
case SYS_CLOSE:
close(f->R.rdi);
break;
case SYS_DUP2:
f->R.rax = dup2(f->R.rdi, f->R.rsi);
break;
default:
break;
}
}