시스템 콜이 무엇인지 설명해 주세요.
사용자 프로세스가 커널의 기능(파일 I/O, 프로세스 생성, 메모리 할당 등)을 안전하게 사용하기 위해 운영체제에 요청하는 프로그래밍 인터페이스입니다.
왜 필요한가?
운영체제는 사용자 모드(User Mode)와 커널 모드(Kernel Mode)를 분리하여 시스템을 보호합니다. 일반 프로세스는 하드웨어나 커널 자원에 직접 접근할 수 없으므로, 시스템 콜을 통해 커널에 작업을 위임합니다. 만약 시스템 콜 없이 직접 접근을 허용하면 악의적인 프로그램이 디스크를 포맷하거나 다른 프로세스의 메모리를 훼손하는 등의 심각한 문제가 발생할 수 있습니다.
동작 흐름 (상세)
① 사용자 프로세스가 라이브러리 함수 호출 (예: read())
② 라이브러리가 시스템 콜 번호를 레지스터에 저장
③ 소프트웨어 인터럽트(Trap) 발생 (x86: int 0x80 또는 syscall 명령어)
④ CPU가 커널 모드로 전환 (모드 비트 1 → 0)
⑤ IDT/시스템 콜 테이블에서 해당 번호의 핸들러 탐색
⑥ 커널이 요청된 작업 수행
⑦ 결과를 레지스터에 저장하고 사용자 모드로 복귀 (모드 비트 0 → 1)
⑧ 라이브러리가 결과를 사용자 프로세스에 반환
┌─────────────────────────────────────────────┐
│ 사용자 모드 (User Mode) │
│ │
│ 응용 프로그램 → C 라이브러리 (glibc) │
│ │ read() 호출 │
│ ▼ │
│ syscall 명령어 실행 │
├─────────────── Trap (모드 전환) ──────────────┤
│ 커널 모드 (Kernel Mode) │
│ │ │
│ 시스템 콜 테이블에서 핸들러 조회 │
│ │ │
│ sys_read() 실행 │
│ │ │
│ 결과 반환 + 모드 복귀 │
└─────────────────────────────────────────────┘
시스템 콜 테이블
커널은 각 시스템 콜에 고유 번호를 부여하고 테이블로 관리합니다.
| 번호 (x86-64 Linux) | 이름 | 설명 |
|---|---|---|
| 0 | sys_read | 파일 읽기 |
| 1 | sys_write | 파일 쓰기 |
| 2 | sys_open | 파일 열기 |
| 39 | sys_getpid | 현재 PID 조회 |
| 57 | sys_fork | 프로세스 복제 |
| 59 | sys_execve | 프로그램 실행 |
| 60 | sys_exit | 프로세스 종료 |
주요 분류
| 분류 | 예시 | 설명 |
|---|---|---|
| 프로세스 제어 | fork(), exec(), exit(), wait() | 프로세스 생성·실행·종료·대기 |
| 파일 관리 | open(), read(), write(), close() | 파일 읽기·쓰기·열기·닫기 |
| 장치 관리 | ioctl(), read(), write() | 디바이스 제어 |
| 정보 유지 | getpid(), time(), gettimeofday() | 시스템·프로세스 정보 조회 |
| 통신 | socket(), send(), recv(), pipe() | 프로세스 간 통신(IPC) |
| 메모리 관리 | mmap(), brk(), munmap() | 메모리 매핑·할당·해제 |
사용자 모드 vs 커널 모드
| 항목 | 사용자 모드 | 커널 모드 |
|---|---|---|
| 권한 | 제한된 명령어만 실행 | 모든 명령어 실행 가능 |
| 메모리 접근 | 자신의 주소 공간만 | 전체 메모리 접근 가능 |
| 하드웨어 접근 | 불가 (시스템 콜 필요) | 직접 접근 가능 |
| 모드 비트 | 1 | 0 |
| 전환 계기 | 시스템 콜, 인터럽트, 예외 | - |
| 실행 주체 | 응용 프로그램 | OS 커널 |
Java에서의 시스템 콜
Java는 JVM 위에서 동작하므로 시스템 콜을 직접 호출하지 않지만, JVM의 네이티브 메서드를 통해 내부적으로 시스템 콜이 발생합니다.
// ── 파일 I/O ──
// 내부적으로 open() → read() → close() 시스템 콜 호출
FileInputStream fis = new FileInputStream("/tmp/test.txt");
int data = fis.read(); // sys_read
fis.close(); // sys_close
// ── 프로세스 생성 ──
// 내부적으로 fork() + execve() 시스템 콜 호출
Process p = Runtime.getRuntime().exec("ls -al");
// ── 네트워크 통신 ──
// 내부적으로 socket() → connect() → write() → read() 시스템 콜 호출
Socket socket = new Socket("example.com", 80);
OutputStream out = socket.getOutputStream();
out.write("GET / HTTP/1.1\r\n".getBytes());
// ── 메모리 ──
// JVM이 내부적으로 mmap() 또는 brk()를 통해 힙 메모리 확보
byte[] largeArray = new byte[1024 * 1024]; // 1MB 할당
Java 코드 → JVM (Java Native Interface) → OS 라이브러리 (glibc) → 시스템 콜 → 커널
시스템 콜의 오버헤드
시스템 콜은 모드 전환이 수반되므로 일반 함수 호출보다 비용이 큽니다.
| 항목 | 일반 함수 호출 | 시스템 콜 |
|---|---|---|
| 모드 전환 | 없음 | 사용자 → 커널 → 사용자 |
| 컨텍스트 저장 | 일부 레지스터 | 전체 레지스터 + 스택 전환 |
| 소요 시간 | ~수 ns | ~수백 ns ~ 수 μs |
| 캐시 영향 | 거의 없음 | TLB/캐시 플러시 가능 |
이 때문에 성능이 중요한 애플리케이션에서는 시스템 콜 횟수를 줄이는 것이 중요합니다. 예를 들어 BufferedReader나 BufferedOutputStream 같은 버퍼링 기법은 여러 번의 read()/write() 시스템 콜을 하나로 모아 오버헤드를 줄입니다.
// Bad: 1바이트마다 시스템 콜 발생
FileInputStream fis = new FileInputStream("file.txt");
while ((b = fis.read()) != -1) { /* ... */ }
// Good: 버퍼 단위로 시스템 콜 발생 (기본 8KB 버퍼)
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("file.txt"));
while ((b = bis.read()) != -1) { /* ... */ }