2025. 3. 11. 21:45ㆍCS/OS
시스템 콜 | OS in 1,000 Lines
operating-system-in-1000-lines.vercel.app
애플리케이션이 커널 기능을 호출할 때 사용하는 시스템콜을 구현해 보자.
1. 시스템 콜 구현
User mode에서도 ecall
을 통한 시스템 콜 구현하기.
- user.c
int syscall(int sysno, int arg0, int arg1, int arg2) {
register int a0 __asm__("a0") = arg0;
register int a1 __asm__("a1") = arg1;
register int a2 __asm__("a2") = arg2;
register int a3 __asm__("a3") = sysno;
__asm__ __volatile__("ecall"
: "=r"(a0)
: "r"(a0), "r"(a1), "r"(a2), "r"(a3)
: "memory");
return a0;
}
: "=r"(a0)
: 커널에서 반환하는 값은a0
레지스터 저장.
ecall
명령어가 실행되면 예외 핸들러가 호출되어 제어권이 커널로 넘어갑니다.
- ECALL
: Environment CALL
: RISC-V 아키텍처에서 상위 권한 모드에 서비스를 요청하기 위해 예외를 발생시키는 명령어.
💡 시스템 콜 명령어는 언제 어디서 정의되는가?
시스템 콜 명령어는 주로 CPU 아키텍처에 따라 달라집니다.
아키텍처 | 시스템 콜 명령어 | 비고 |
x86 (32비트) | int 0x80, sysenter | sysenter가 더 빠름 |
x86-64 (64비트) | syscall | 표준 방식 |
ARM | svc | ARM 시스템 콜 방식 |
RISC-V | ecall | ecall로 트랩 발생 |
Windows (NT) | syscall | ntdll.dll을 통해 간접 호출 |
2. 시스템 콜과 예외
가. 시스템 콜을 요청하는데 예외가 발생하는 이유
ecall
은 RISC-V 아키텍처에서 예외(exception)로 처리된다.
RISC-V뿐만 아니라 x86 아키텍처에서는 syscall
(Linux) 또는 int 0x2E
or syscall
(Windows) 명령어를 사용하여 시스템 콜을 구현하는데 이러한 시스템 콜도 RISC-V의 ecall
과 유사하게 예외로 처리된다.
거의 모든 현대 운영체제에서 user mode의 시스템 콜은 더 상위 권한 모드(커널 모드)에서 처리되는 것이 일반적이다.
이는 보안과 시스템 안정성을 위한 기본적인 설계 원칙이다.
사용자 모드에서 실행되는 프로그램이 직접 커널의 자원에 접근하면 보안 문제가 발생할 수 있다.
이에 권한 상승이 동반되는 작업은 ‘예외’로 인식하고 인터럽트 벡터 테이블이나 트랩 핸들러와 같은 예외 처리 메커니즘을 이용하여 상위 모드로 전환된다.
나. RISC-V에서의 예외 처리 과정
RISC-V에서 예외가 발생하면 일반적으로…
발생 모드 | 처리 모드 | 진입점 레지스터 |
U-mode | S-mode | stvec |
S-mode | M-mode | mtvec |
이처럼 RISC-V에서는 예외가 발생하면 더 높은 권한을 가진 모드에서 이를 처리하게 됩니다.
- mtvec
: Machine Trap Vector
: M-mode (Machine mode)에서 발생하는 예외나 인터럽트를 처리하는 핸들러의 주소를 저장
: 부팅 시 OpenSBI가 M-Mode 트랩 핸들러 주소를 설정 - stvec
: Supervisor Trap Vector
: S-mode (Supervisor mode)에서 발생하는 예외나 인터럽트를 처리하는 핸들러의 주소를 저장
: 운영체제 커널에서 예외 처리 핸들러의 진입점을 설정
각 레지스터는 해당 권한 모드에서 예외가 발생했을 때 CPU가 점프할 주소를 지정하는 역할 수행.
3. 문자열 입출력 시스템 콜
가. putchar 시스템 콜
- common.h
#define SYS_PUTCHAR 1
- user.c
void putchar(char ch) {
syscall(SYS_PUTCHAR, ch, 0, 0);
}
kernel.c
에는 이미 putchar
를 구현했기 때문에 이는 넘어간다.
나. getchar 시스템 콜
- common.h
#define SYS_GETCHAR 2
- user.c
int getchar(void) {
return syscall(SYS_GETCHAR, 0, 0, 0);
}
- user.h
int getchar(void);
- kernel.c
long getchar(void) {
struct sbiret ret = sbi_call(0, 0, 0, 0, 0, 0, 0, 2);
return ret.error;
}
getchar
의 경우 putchar
와 다르게 kernel.c
에 구현한 적 없어서 추가해야 한다.
SBI는 입력이 없으면 -1
을 반환한다.
4. 커널에서 ecall 명령어 처리
커널의 예외 트랩 핸들러에서 getchar
, putchar
의 ecall
명령을 처리하도록 수정한다.
- kernel.h
#define SCAUSE_ECALL 8
- kernel.c
void handle_trap(struct trap_frame *f) {
uint32_t scause = READ_CSR(scause);
uint32_t stval = READ_CSR(stval);
uint32_t user_pc = READ_CSR(sepc);
if (scause == SCAUSE_ECALL) { // 추가
handle_syscall(f); // 추가
user_pc += 4; // 추가
} else { // 수정
PANIC("unexpected trap scause=%x, stval=%x, sepc=%x\n", scause, stval, user_pc); // 수정
} // 수정
WRITE_CSR(sepc, user_pc); // 추가
}
if (scause == SCAUSE_ECALL) {
:scause
가8
이면ecall
로 인한 예외다.user_pc += 4;
:sepc가
예외를 발생시킨 명령어(즉,ecall
)를 가리키기 때문, 만약 변경하지 않으면 커널이 동일한 위치로 돌아가ecall
명령어를 반복 실행하게 됨.
5. 시스템 콜 핸들러
- kernel.c
void handle_syscall(struct trap_frame *f) {
switch (f->a3) {
case SYS_GETCHAR:
while (1) {
long ch = getchar();
if (ch >= 0) {
f->a0 = ch;
break;
}
yield();
}
break;
case SYS_PUTCHAR:
putchar(f->a0);
break;
default:
PANIC("unexpected syscall a3=%x\n", f->a3);
}
}
struct trap_frame *f
: 예외가 발생했을 때 저장된 "레지스터 값들을 담은 구조체"if (ch >= 0) {
: SBI는 입력이 없으면-1
을 반환한다. 입력이 없으면 시스템 콜 종료.yield();
: 입력 중에 다른 프로세스들이 실행될 수 있도록, 다른 프로세스에게 CPU 양보.
6. shell 구현하기
- shell.c
void main(void) {
while (1) {
prompt:
printf("> ");
char cmdline[128];
for (int i = 0;; i++) {
char ch = getchar();
putchar(ch);
if (i == sizeof(cmdline) - 1) {
printf("command line too long\n");
goto prompt;
} else if (ch == '\r') {
printf("\n");
cmdline[i] = '\0';
break;
} else {
cmdline[i] = ch;
}
}
if (strcmp(cmdline, "hello") == 0)
printf("Hello world from shell!\n");
else
printf("unknown command: %s\n", cmdline);
}
}
} else if (ch == '\r') {
: 디버그 콘솔에서 줄 바꿈 문자는'\r'
다.
: 줄바꿈 문자가 입력될 때까지 문자를 읽어 들임.if (strcmp(cmdline, "hello") == 0)
: 쉘에hello
입력 시 "Hello world from shell!\n” 출력cmdline[i] = '\0';
: 문자열의 끝을 나타내는 널 문자(\0
)를 추가
: strcmp 함수로 명령어를 비교할 때 널 문자가 필요
$ ./run.sh
> hello
Hello world from shell!
shell 같은 거 완성.
7. 프로세스 종료 시스템 콜
이제 프로세스를 종료하는 exit
시스템 콜을 구현해 보자.
- common.h
#define SYS_EXIT 3
- user.c
__attribute__((noreturn)) void exit(void) {
syscall(SYS_EXIT, 0 ,0 ,0);
for(;;); // Just in case!
}
- kernel.h
#define PROC_EXITED 2
- kernel.c
void handle_syscall(struct trap_frame *f) {
switch (f->a3) {
case SYS_EXIT:
printf("process %d exited\n", current_proc->pid);
current_proc->state = PROC_EXITED;
yield();
PANIC("unreachable");
/* omitted */
}
}
최근 프로세스를 상태를 PROC_EXITED
로 변경.
yield
를 호출해서 스케줄링.
교안에서는 단순하게 프로세스 상태만 PROC_EXITED
로 변경했지만, 실제 OS에서는 페이지 테이블이나 할당된 메모리 영역과 같이 프로세스가 점유한 자원을 해제해야 한다.
- shell.c
if (strcmp(cmdline, "hello") == 0)
printf("Hello world from shell!\n");
else if (strcmp(cmdline, "exit") == 0) // 추가
exit(); // 추가
else
printf("unknown command: %s\n", cmdline);
shell에 exit
명령어를 추가한다.
$ ./run.sh
> hello
Hello world from shell!
> hello
Hello world from shell!
> asdf
Unkown command: asdf
> exit
process 2 exited
PANIC: kernel.c:357: unreachable
shell에 exit
를 입력하면 스케줄러가 idle
프로세스를 선택하며 PANIC
을 발생시키면 정상이다.
'CS > OS' 카테고리의 다른 글
[1000줄 OS 구현하기] 파일 시스템 (0) | 2025.03.17 |
---|---|
[1000줄 OS 구현하기] 디스크 I/O (0) | 2025.03.15 |
[1000줄 OS 구현하기] 유저 모드 (0) | 2025.03.11 |
[1000줄 OS 구현하기] 애플리케이션 (0) | 2025.03.09 |
[1000줄 OS 구현하기] 페이지 테이블 (0) | 2025.03.07 |