운영체제
시스템 콜(system call)의 동작 과정
joonior
2025. 3. 10. 00:25
1. 시스템 콜이란 무엇인가?
기본적으로 사용자 어플리케이션은 컴퓨터의 자원에 직접 접근할 수 없다. 컴퓨터의 자원을 어떻게 쓸지에 대해서 관린해주는 것이 운영체제이고, 운영체제는 흔히 우리가 많이 사용하는 Windows, Mac OS, Linux 등이 있다.
시스템 콜이란,
"사용자가 특정 자원 (CPU, 메모리, 디스크 등)에 직접 접근할 수 없기 때문에, 운영체제를 통해 대신 요청하는 것이다."
2. 시스템 콜의 종류
시스템 콜은 크게 프로세스 관리, 파일 관리, 메모리 관리, 네트워크 통신 등 다양한 기능으로 분류할 수 있다.
종류 | 시스템 콜 | 설명 |
1. 프로세스 관리 (Process Management) | fork() | 새로운 프로세스를 생성 (부모-자식 프로세스) |
execve() | 실행 중인 프로세스를 새로운 프로그램으로 교체 | |
exit() | 현재 프로세스 종료 | |
wait() | 자식 프로세스가 종료될 때까지 부모 프로세스 대기 | |
getpid() | 현재 프로세스의 ID(PID) 가져오기 | |
getppid() | 부모 프로세스의 ID 가져오기 | |
2. 파일 시스템 관리 (File System Management) | open() | 파일 열기 |
close() | 파일 닫기 | |
read() | 파일에서 데이터 읽기 | |
write() | 파일에 데이터 쓰기 | |
lseek() | 파일의 읽기/쓰기 위치 이동 | |
unlink() | 파일 삭제 | |
stat() | 파일 정보 가져오기 (크기, 권한 등) | |
3. 메모리 관리 (Memory Management) | brk() | 힙 메모리 크기 변경 |
mmap() | 파일 또는 메모리를 매핑하여 공유 메모리 사용 | |
munmap() | mmap()으로 매핑된 메모리 해제 | |
sbrk() | 힙 영역 크기 변경 (과거에 많이 사용됨) | |
4. 네트워크 및 IPC (Inter-Process Communication) | socket() | 소켓 생성 (네트워크 통신 시작) |
bind() | 소켓과 특정 IP/포트를 연결 | |
listen() | 클라이언트 연결을 대기 | |
accept() | 클라이언트의 연결 요청 수락 | |
connect() | 서버에 연결 요청 | |
send(), recv() | 네트워크 데이터를 송수신 | |
pipe() | 부모-자식 프로세스 간 파이프(IPC) 생성 | |
shmget() | 공유 메모리 생성 | |
shmat() | 공유 메모리를 현재 프로세스에 연결 | |
5. 시스템 정보 및 관리 (System Control) | gettimeofday() | 현재 시간 가져오기 |
settimeofday() | 시스템 시간 설정 | |
uname() | OS 및 시스템 정보 가져오기 | |
sysinfo() | 시스템 메모리, 부팅 시간 등 정보 가져오기 | |
6. 사용자 및 권한 관리 (User & Permission Management) | getuid() | 현재 사용자 ID 가져오기 |
setuid() | 프로세스의 사용자 ID 변경 | |
chmod() | 파일 권한 변경 | |
chown() | 파일 소유자 변경 |
3. 시스템 콜의 동작 과정
- 사용자 애플리케이션이 시스템 콜을 요청한다.
- ex. 프로그램이 파일을 연다. 네트워크 패킷을 수신하다.
- C 라이브러리에서 제공하는 래퍼(wrapper) 함수 호출한다.
가령, 위 코드에서 open은 아래와 glib 내에 다음과 같이 구현되어 있음#include <fcntl.h> #include <unistd.h> int main() { int fd = open("test.txt", O_RDONLY); if (fd == -1) { perror("open failed"); return 1; } close(fd); return 0; }
int open(const char *pathname, int flags, ...) { va_list args; mode_t mode = 0; if (flags & O_CREAT) { va_start(args, flags); mode = va_arg(args, mode_t); va_end(args); } return syscall(SYS_open, pathname, flags, mode); }
우리는 syscall을 직접 사용해서 파일을 open하거나, 어셈블리어를 통해 호출할 수도 있다.#include <sys/syscall.h> #include <unistd.h> #include <fcntl.h> int main() { int fd = syscall(2, "test.txt", O_RDONLY); if (fd == -1) { perror("open failed"); return 1; } syscall(3, fd); return 0; }
mov rax, 2 ; SYS_open (2번 시스템 콜) mov rdi, filename ; 파일 경로 mov rsi, O_RDONLY ; 읽기 전용 플래그 syscall ; 시스템 콜 실행
다만, 이렇게 직접 호출하는건 이식성이 떨어지며, 직관적이지 못하다.- 이식성이 떨어지는 이유: 운영체제마다 CPU 아키텍쳐마다 system call의 번호가 다르다. 가령 Linux (x86_64)에서 open은 2번인 반면, Linux (ARM), macOS (x86_64)에서는 5번이다.
- 사실, 이 부분은 SYS_open, SYS_close 등을 사용하면 해결할 수 있다.
- 다만, 리눅스 최신 버전에서는 open의 구현이 보안 이슈로 인해 내부 구현이 openat으로 변경되었고, 이런 점들을 반영하지 못한다.
- 커널 모드로 전환한다.
- 시스템 콜은 커널에서 처리된다.
- syscall(SYS_openat, dirfd, pathname, flags, mode); 등 시스템 콜 함수를 호출하면, 이 함수가 처리되는데 아키텍쳐에 따라 구현이 상이하다.
- 현대 시스템에서는 syscall이라는 instruction이 존재한다.(x86_64기준)
x86에는 sysenter가 ARM에서는 svc(supervisor call)가 있다.mov rax, 257 ; SYS_openat (시스템 콜 번호) mov rdi, -100 ; AT_FDCWD (디렉터리 FD) mov rsi, pathname ; 파일 경로 mov rdx, flags ; 파일 열기 옵션 mov r10, mode ; 파일 모드 syscall ; 시스템 콜 실행
- 과거 시스템에서는 소프트웨어 인터럽트를 통해서 커널 모드로 전환했다.
이게 흔히 우리가 운영체제 수업시간에 배우는 방식일거다 (혹시 바뀌었나)mov eax, 5 ; SYS_open (open 시스템 콜 번호) mov ebx, pathname ; 첫 번째 인자 (파일 경로) mov ecx, flags ; 두 번째 인자 (파일 열기 옵션) mov edx, mode ; 세 번째 인자 (파일 모드) int 0x80 ; 시스템 콜 호출
- 현대 시스템에서는 syscall이라는 instruction이 존재한다.(x86_64기준)
- 커널은 시스템 콜 번호를 확인하고, 매핑되어 있는 시스템 콜 핸들러를 호출한다.
- 커널은 작업 수행을 한 후에 결과를 반환한다.
- 일반적으로 작업이 성공한 경우에는 0을 리턴하고 오류가 발생하면 오류에 맞는 정수(음수)를 리턴한다.
- 사용자 모드로 다시 복귀한다.
- sysret (x86_64), sysexit (x86), iret (x86 구형) eret (ARM) 등 명령어를 통해 복귀한다.
- CPU가 저장된 사용자 모드의 EIP, ESP로 복귀한다. (기타 레지터도 복원)