시스템 콜과 사용자 모드·커널 모드 전환
시스템 콜의 역할과 호출 흐름, 사용자 모드·커널 모드 전환, 시스템 콜 테이블 및 성능 오버헤드를 정리한다.
2026-08-14 · 최초 발행 2026-01-12
사용자 공간에서 커널 서비스로 들어가는 통로
응용 프로그램은 하드웨어에 직접 명령을 내릴 수 없다. 파일을 읽고 쓰거나 프로세스를 만들고, 메모리를 확보하려면 운영체제 커널에 작업을 요청해야 한다. 이 요청의 인터페이스가 시스템 콜(System Call)이다.
시스템 콜은 응용 프로그램과 운영체제 사이의 통신 메커니즘이자 커널에 대한 controlled entry point다. 커널은 이 경로에서 하드웨어 접근을 중재하고, 권한을 확인하며, 프로세스 간 자원 충돌을 막는다. 중앙에서 자원을 할당하고 우선순위를 관리할 수 있는 이유도 여기에 있다.
시스템 콜은 보통 Trap 또는 Exception을 일으키는 소프트웨어 인터럽트 형태로 진입한다. x86 Linux 구형에서는 INT 0x80, x86-64에서는 SYSCALL/SYSRET, ARM에서는 SVC (Supervisor Call)가 사용된다. 프로그램이 명시적으로 호출하고 처리 완료를 기다리는 동기적 호출이며, 이때 User Mode에서 Kernel Mode로 바뀌어 특권 명령어를 실행할 수 있게 된다.
호출 순간에 일어나는 모드 전환
사용자 모드에서는 일반 애플리케이션과 사용자 라이브러리가 실행된다. 권한은 제한되고 특권 명령어를 실행할 수 없으며, 일반 메모리 영역만 접근한다.
커널 모드에서는 운영체제 커널과 장치 드라이버가 실행된다. 모든 명령어와 전체 메모리에 접근할 수 있고 하드웨어를 직접 다룰 수 있다. CPU 상태 레지스터의 1비트인 Mode Bit는 현재 실행 모드를 나타내며, 0은 Kernel Mode, 1은 User Mode를 뜻한다.
시스템 콜, 인터럽트, 예외가 발생하면 User Mode에서 Kernel Mode로 전환한다. 처리가 끝나 반환할 때는 Kernel Mode에서 User Mode로 돌아온다.
open() 요청이 커널에 도달하는 과정
다음 호출은 사용자 프로그램에서 시작한다.
// C 프로그램
int fd = open("/tmp/file.txt", O_RDONLY);
먼저 libc의 open() wrapper 함수가 시스템 콜 번호를 설정하고 매개변수를 레지스터에 저장한다. 이어 소프트웨어 인터럽트가 발생하면 CPU는 Kernel Mode로 전환하고 System Call Handler로 이동한다. 핸들러는 시스템 콜 번호를 확인해 System Call Table에서 함수를 찾고, 파일 열기나 메모리 할당 같은 요청 작업을 수행한다.
처리 결과는 반환값 레지스터에 저장된다. CPU가 User Mode로 복귀하면 라이브러리 함수가 그 결과를 호출자에게 돌려준다.
모드 전환 중에는 사용자 프로그램의 레지스터 상태와 스택 포인터, 프로그램 카운터 등을 저장한다. 사용자 스택에서 커널 스택으로 바꾸고, 서비스가 끝나면 레지스터를 복원해 다시 사용자 스택으로 전환한다.
커널이 제공하는 요청 범주
시스템 콜은 요청 대상에 따라 나눠 볼 수 있다.
프로세스 제어에는 fork()로 프로세스를 만들고, exec()로 프로그램을 실행하며, exit()와 wait()로 종료와 자식 프로세스 대기를 처리하는 기능이 포함된다. getpid(), getppid(), nice()는 프로세스 ID나 우선순위를 다룬다.
파일 관리에서는 open(), close(), creat()로 파일을 열고 닫거나 생성한다. read(), write(), lseek()는 데이터와 파일 포인터를 다루며, stat(), chmod(), chown()은 파일 정보·권한·소유자를 조회하거나 바꾼다.
장치 관리는 ioctl()과 read()/write()를 통한 장치 제어 및 입출력을 포함한다. 장치를 요청하거나 해제하는 인터페이스로는 request_device()와 release_device()가 있다.
정보 유지 범주에서는 time(), stime(), gethostname()으로 시스템 시간과 호스트 이름을 다루고, getuid(), getgid(), setuid()로 사용자 및 그룹 정보를 처리한다.
통신에는 pipe(), msgget(), msgsnd(), msgrcv() 같은 메시지 전달 기능이 있다. 공유 메모리는 shmget(), shmat(), shmdt()로 생성·연결·분리한다. 네트워크 통신은 socket(), bind(), listen(), accept(), connect(), send()/recv()로 구성된다.
보호 기능에서는 chmod(), chown(), setuid()가 권한과 소유자를 관리하고, access()가 접근 권한을 확인한다.
운영체제별 호출 형태
UNIX/Linux에서는 프로세스, 파일, 메모리 작업을 다음과 같은 인터페이스로 요청한다.
pid_t fork(void); // 프로세스 복제
int execve(const char *filename, char *const argv[], char *const envp[]); // 프로그램 실행
void exit(int status); // 프로세스 종료
pid_t wait(int *status); // 자식 프로세스 대기
pid_t getpid(void); // 프로세스 ID 조회
int open(const char *pathname, int flags, mode_t mode); // 파일 열기
int close(int fd); // 파일 닫기
ssize_t read(int fd, void *buf, size_t count); // 파일 읽기
ssize_t write(int fd, const void *buf, size_t count); // 파일 쓰기
off_t lseek(int fd, off_t offset, int whence); // 파일 포인터 이동
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); // 메모리 매핑
int munmap(void *addr, size_t length); // 메모리 매핑 해제
void *brk(void *addr); // 힙 크기 변경
Windows에서는 프로세스와 파일 작업을 다음 인터페이스로 제공한다.
HANDLE CreateProcess(...); // 프로세스 생성
BOOL TerminateProcess(HANDLE hProcess, UINT uExitCode); // 프로세스 종료
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds); // 대기
HANDLE CreateFile(...); // 파일 생성/열기
BOOL CloseHandle(HANDLE hObject); // 핸들 닫기
BOOL ReadFile(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, ...); // 파일 읽기
BOOL WriteFile(HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, ...); // 파일 쓰기
시스템 콜 번호와 테이블이 연결하는 것
각 시스템 콜에는 고유 번호가 있으며, 이 번호는 System Call Table의 인덱스로 쓰인다. 테이블은 시스템 콜 번호를 커널 함수 포인터에 매핑하는 배열 또는 테이블 구조다.
Linux x86-64에서 read는 0, write는 1, open은 2, close는 3, fork는 57, execve는 59다.
커널 내부에서는 다음처럼 테이블 항목이 함수에 연결된다.
// 커널 내부
sys_call_table[0] = sys_read;
sys_call_table[1] = sys_write;
sys_call_table[2] = sys_open;
...
x86-64 Linux는 시스템 콜 번호를 RAX에 넣고, 인자 1부터 인자 6까지 RDI, RSI, RDX, R10, R8, R9에 전달한다. 반환값 역시 RAX에 저장한다. 인자가 많으면 스택을 사용할 수 있으며, 구조체 포인터처럼 메모리 블록을 전달하는 방식도 사용한다.
라이브러리 함수와 구분해야 하는 이유
시스템 콜은 커널 서비스를 직접 요청하므로 Kernel Space에서 실행되고 모드 전환이 발생한다. read(), write(), fork(), open()이 이에 속한다.
반면 라이브러리 함수는 User Space에서 실행된다. 시스템 콜을 감싼 wrapper일 수도 있고 독립 함수일 수도 있다. printf()는 write() System Call을 사용하고, malloc()은 brk() 또는 mmap() System Call을 사용한다. strlen()은 System Call 없이 실행되는 순수 라이브러리 함수다.
| 특성 | System Call | Library Function |
|---|---|---|
| 실행 위치 | Kernel Space | User Space |
| 모드 전환 | 발생 | 없거나 간접적 |
| 오버헤드 | 높음 | 낮음 |
| 이식성 | OS 종속적 | 비교적 높음 |
| 예시 | read(), write() | printf(), malloc() |
호출 비용을 줄이는 방식
시스템 콜에는 User Mode와 Kernel Mode의 전환, 레지스터 저장·복원, 경우에 따른 캐시 flush 비용이 따른다. 모드 전환에는 약 수십수백 CPU 사이클이 들며, 페이지 테이블이 바뀌는 경우에는 Context Switching 과정에서 TLB flush도 발생할 수 있다. 총 비용은 함수 호출 대비 10100배 느리다.
그래서 시스템 콜 횟수 자체를 줄이는 방식이 중요하다. 버퍼링으로 여러 번의 write()를 한 번으로 묶고, Batching으로 여러 작업을 한 번의 System Call로 처리할 수 있다. vDSO(Virtual Dynamic Shared Object)는 gettimeofday(), time() 등을 포함한 일부 System Call을 User Space에서 처리한다.
Fast System Call도 비용을 줄이는 방법이다. x86-64의 SYSCALL/SYSRET은 INT 0x80보다 빠르고, x86의 SYSENTER/SYSEXIT은 Pentium II 이후 사용된다. 블로킹 대신 비동기 I/O를 활용하는 방법으로는 Linux의 io_uring이 있다.