시스템 콜이, 운영체제에서 어떤 과정으로 실행되는지 설명해 주세요.
사용자 프로세스가 시스템 콜을 호출하면 라이브러리 → 트랩 → 커널 모드 전환 → 핸들러 실행 → 복귀의 과정을 거쳐 커널 기능을 수행합니다.
전체 흐름 요약
사용자 프로세스 → C 라이브러리(glibc) → 트랩(syscall 명령어)
→ 커널 모드 전환 → 시스템 콜 디스패처 → sys_call_table 조회
→ 커널 함수 실행 → 결과 반환 → 사용자 모드 복귀
단계별 상세 설명
1단계: 사용자 프로세스가 API 호출
// Java 코드
FileInputStream fis = new FileInputStream("/tmp/test.txt");
int data = fis.read();
Java API (fis.read())
│
▼
JVM native 메서드 (read0())
│
▼
C 라이브러리 (glibc의 read() 함수)
사용자는 시스템 콜을 직접 호출하지 않고, 라이브러리 함수(Wrapper Function)를 통해 간접 호출합니다.
2단계: 레지스터에 인자 세팅
C 라이브러리가 시스템 콜 번호와 인자를 CPU 레지스터에 저장합니다.
read(3, buf, 100) 호출 시:
┌──────────┬─────────────────────────┐
│ 레지스터 │ 값 │
├──────────┼─────────────────────────┤
│ RAX │ 0 (sys_read 번호) │
│ RDI │ 3 (파일 디스크립터) │
│ RSI │ 0x7fff1000 (버퍼 주소) │
│ RDX │ 100 (읽을 바이트 수) │
└──────────┴─────────────────────────┘
3단계: 트랩 발생 (syscall 명령어 실행)
glibc가 syscall 명령어 실행
│
▼
CPU가 소프트웨어 인터럽트(트랩) 발생
│
├── 현재 상태 저장
│ ├── RCX ← 복귀할 사용자 코드 주소 (RIP)
│ └── R11 ← 현재 플래그 레지스터 (RFLAGS)
│
├── 모드 비트 1 → 0 (사용자 모드 → 커널 모드)
│
└── MSR 레지스터에 저장된 커널 진입점으로 점프
(LSTAR MSR → system_call 핸들러 주소)
4단계: 커널 진입 — 시스템 콜 디스패처
system_call 핸들러 진입 (arch/x86/entry/entry_64.S)
│
├── ① 사용자 레지스터를 커널 스택에 저장 (pt_regs 구조체)
│
├── ② RAX 값 유효성 검사
│ ├── RAX < NR_syscalls → 정상 진행
│ └── RAX >= NR_syscalls → -ENOSYS 에러 반환
│
├── ③ sys_call_table[RAX]에서 커널 함수 주소 조회
│
└── ④ 해당 커널 함수 호출
sys_call_table (시스템 콜 테이블)
┌────────┬──────────────────┐
│ RAX=0 │ → sys_read() │ ← 이것이 호출됨
│ RAX=1 │ → sys_write() │
│ RAX=2 │ → sys_open() │
│ RAX=3 │ → sys_close() │
│ ... │ ... │
└────────┴──────────────────┘
5단계: 커널 함수 실행
sys_read(fd=3, buf=0x7fff1000, size=100)
│
├── ① 파일 디스크립터 테이블에서 fd=3에 해당하는 파일 객체 조회
│
├── ② 권한 검사 (읽기 권한이 있는지)
│
├── ③ 파일 시스템 계층 호출 (VFS → ext4 등)
│
├── ④ 페이지 캐시 확인
│ ├── 캐시 Hit → 캐시에서 바로 복사
│ └── 캐시 Miss → 디스크 I/O 요청
│ ├── 디바이스 드라이버에 요청
│ ├── DMA로 디스크 → 메모리 전송
│ └── 디스크 인터럽트로 완료 통보
│
└── ⑤ 사용자 버퍼(0x7fff1000)에 데이터 복사
(copy_to_user 함수 사용)
6단계: 결과 반환 및 사용자 모드 복귀
커널 함수 완료
│
├── ① 반환값을 RAX에 저장
│ ├── 성공: 읽은 바이트 수 (예: 100)
│ └── 실패: 음수 에러 코드 (예: -ENOENT)
│
├── ② 커널 스택에서 사용자 레지스터 복원
│
├── ③ sysret 명령어 실행
│ ├── RCX → RIP (사용자 코드 복귀 주소)
│ └── R11 → RFLAGS (플래그 복원)
│
└── ④ 모드 비트 0 → 1 (커널 모드 → 사용자 모드)
사용자 프로세스가 RAX에서 결과 수신
│
├── RAX >= 0 → 성공, 값을 그대로 반환
└── RAX < 0 → glibc가 errno 설정 후 -1 반환
전체 과정 한눈에 보기
┌─────────────────── 사용자 모드 ────────────────────┐
│ │
│ ① Java: fis.read() │
│ ② JVM native: read0() │
│ ③ glibc: read(3, buf, 100) │
│ ④ 레지스터 세팅: RAX=0, RDI=3, RSI=buf, RDX=100 │
│ ⑤ syscall 명령어 실행 │
│ │ │
├──────────────── 트랩 (모드 전환) ───────────────────┤
│ ▼ │
│ ⑥ CPU: 상태 저장 (RCX=복귀주소, R11=RFLAGS) │
│ ⑦ CPU: 커널 진입점으로 점프 (LSTAR MSR) │
│ ⑧ 디스패처: 사용자 레지스터 커널 스택에 저장 │
│ ⑨ 디스패처: sys_call_table[0] → sys_read() 호출 │
│ ⑩ sys_read(): 파일 시스템 → 디스크 I/O → 데이터 복사 │
│ ⑪ RAX = 100 (읽은 바이트 수) │
│ ⑫ 사용자 레지스터 복원 │
│ ⑬ sysret 명령어 실행 │
│ │ │
├──────────────── 모드 복귀 ─────────────────────────┤
│ ▼ │
│ ⑭ glibc: RAX 값 확인 → 결과 반환 │
│ ⑮ JVM: Java 변수에 결과 저장 │
│ ⑯ Java: data = 읽은 값 │
│ │
└───────────────────────────────────────────────────┘
핵심 정리
| 단계 | 위치 | 핵심 동작 |
|---|---|---|
| ① API 호출 | 사용자 모드 | Java API → JVM → glibc |
| ② 레지스터 세팅 | 사용자 모드 | RAX에 시스템 콜 번호, RDI~R9에 인자 |
| ③ 트랩 발생 | 모드 전환 | syscall 명령어 → 상태 저장 → 커널 진입 |
| ④ 디스패처 | 커널 모드 | sys_call_table[RAX]에서 핸들러 조회 |
| ⑤ 커널 함수 실행 | 커널 모드 | 실제 작업 수행 (I/O, 프로세스 관리 등) |
| ⑥ 결과 반환 | 모드 복귀 | RAX에 결과 저장 → sysret → 사용자 모드 |