01 — COMPILER & INTERPRETER

컴파일러 vs 인터프리터

소스 코드를 실행 가능한 기계어로 변환하는 방식은 크게 컴파일, 인터프리트, JIT 세 가지로 나뉩니다. 각 방식은 실행 속도와 유연성에서 서로 다른 트레이드오프를 가집니다.

컴파일 파이프라인
Source Code
.c / .py / .js
Lexer
토큰화
Parser
구문 분석
AST
추상 구문 트리
Code Gen
코드 생성
Machine Code
실행 가능
컴파일러
전체 소스를 미리 기계어로 번역 후 실행.
빠른 실행 속도, 느린 빌드.

예: C, C++, Go, Rust
인터프리터
소스 코드를 줄 단위로 읽어 즉시 실행.
빠른 개발 사이클, 느린 런타임.

예: Python, Ruby
JIT 컴파일러
바이트코드를 런타임에 기계어로 컴파일.
컴파일 + 해석 혼합.

예: Java (JVM), C# (.NET), JS V8
인터랙티브 토큰화 / 파싱
언어 분류표
유형 언어 메모리 관리 특징
컴파일 C, C++, Go, Rust 수동 / GC / Ownership 빠른 실행, 시스템 프로그래밍
인터프리트 Python, JS, Ruby GC (자동) 빠른 개발, 동적 타입
JIT Java, C#, Kotlin GC (JVM/.NET) 포터블 바이트코드, 크로스 플랫폼
GC (가비지 컬렉션) 전략 비교
수동 관리 (C/C++)
malloc/free 직접 호출. 가장 빠르나 메모리 누수, dangling pointer 위험.
자동 GC (Java/Python)
런타임이 자동으로 메모리 회수. 편리하나 GC pause 발생 가능.
소유권 (Rust)
컴파일 타임에 소유권 검사. GC 없이 안전한 메모리 관리 보장.
02 — SYSTEM CALL

시스템 콜 (System Call)

시스템 콜은 사용자 프로세스가 운영체제 커널의 서비스를 요청하는 인터페이스입니다. 하드웨어 보호를 위해 특권 모드 전환(trap)을 수반합니다.

유저 공간 ↔ 커널 공간
USER SPACE (Ring 3)
Application (프로세스)
C 표준 라이브러리 (libc)
syscall() 래퍼 함수
TRAP / INTERRUPT
KERNEL SPACE (Ring 0)
커널 진입점 (syscall handler)
syscall 번호 디스패치
하드웨어 드라이버 / 서비스
결과 반환 (iret)
시스템 콜 실행 흐름
앱 코드
write(1, buf, n)
trap
INT 0x80 / SYSCALL
커널 핸들러
번호 검증
하드웨어 I/O
드라이버 호출
결과 반환
레지스터 복원
주요 시스템 콜 — 클릭하면 상세 설명
syscall 번호 (Linux x86_64) 분류 간단 설명
위 표에서 시스템 콜을 클릭하면 상세 설명이 표시됩니다.
프로세스 생성: fork() & exec()
Parent Process
PID: 1234
fork()
Parent 계속
pid=1234
wait(&status)
Child Process
pid=1235
exec("/bin/ls")
exit(0)
fork(): 현재 프로세스를 복사하여 자식 프로세스 생성. 부모는 자식 PID, 자식은 0 반환.
exec(): 현재 프로세스 이미지를 새 프로그램으로 교체. fork 후 exec 패턴(fork-exec)이 일반적.
wait(): 부모가 자식 종료를 기다림. 좀비 프로세스 방지.
CPU 특권 링 (Privilege Rings)
Ring 0: 커널 (최고 권한) Ring 1/2: 드라이버 Ring 3: 사용자 앱 (최저 권한)
03 — IPC

IPC (프로세스 간 통신)

독립된 프로세스끼리 데이터를 주고받는 메커니즘입니다. 각 방식은 속도, 방향성, 범위에서 서로 다른 특성을 가집니다.

Process A
write(fd[1],...)
단방향 →
H
e
l
l
o
바이트 스트림
Process B
read(fd[0],...)
장점
간단한 API (POSIX)
커널이 동기화 처리
부모-자식 간 쉬운 통신
단점
단방향만 지원
부모-자식 프로세스만 가능
구조화된 메시지 없음
pipe(fd[2]) → fd[0]: 읽기 끝, fd[1]: 쓰기 끝. 셸에서 ls | grep txt가 파이프의 대표 예시입니다.
Client
connect()
request →
← response
Server
listen() / accept()
TCP: 연결 지향 · 신뢰성 보장  |  UDP: 비연결 · 빠른 전송
장점
양방향 통신
네트워크 너머 통신 가능
다양한 프로토콜 지원
단점
설정 복잡 (addr, port)
네트워크 오버헤드
연결 관리 필요
Unix Domain Socket은 같은 호스트 내 IPC에 최적화된 소켓입니다. TCP보다 빠르며, Docker 데몬 통신 등에 사용됩니다.
Process A
shmget/shmat
공유 메모리 영역
Shared Memory
뮤텍스/세마포어 필요
Process B
shmget/shmat
장점
가장 빠른 IPC
데이터 복사 없음
대용량 데이터 공유 적합
단점
동기화 직접 구현 필요
같은 호스트만 가능
경쟁 조건 주의
두 프로세스가 동일한 물리 페이지를 각자의 가상 주소 공간에 매핑합니다. 복사 없이 직접 접근하므로 가장 빠른 IPC이지만, 뮤텍스/세마포어로 동시 접근을 제어해야 합니다.
Producer A
msgsnd()
MSG1
type:1
MSG2
type:2
MSG3
type:1
메시지 큐
(커널 관리)
Consumer B
msgrcv()
장점
비동기 통신 가능
구조화된 메시지
타입별 수신 선택
단점
커널 메모리 제한
공유 메모리보다 느림
큐 크기 제한 존재
POSIX 메시지 큐(mq_open) 또는 System V IPC(msgget)를 사용합니다. 메시지에 타입을 붙여 선택적 수신이 가능합니다.
시그널 (Signal) — 클릭하여 설명 보기
시그널을 클릭하면 설명이 표시됩니다.
04 — GC & MEMORY

가비지 컬렉션 & 메모리 관리

GC는 더 이상 사용되지 않는 메모리를 자동으로 회수하는 기법입니다. 참조 카운팅, Mark & Sweep, 세대별 GC 등 다양한 알고리즘이 존재합니다.

각 객체의 참조 횟수를 추적. 참조 수가 0이 되면 즉시 회수.
배지의 숫자가 참조 카운트입니다. 0이 되면 즉시 회수됩니다.
단점: 순환 참조(A→B→A)는 카운트가 절대 0이 되지 않아 누수 발생.
루트(root)에서 도달 가능한 객체를 마킹 후, 마킹되지 않은 객체를 일괄 제거.
루트(Root): 스택/전역 변수에서 직접 참조되는 객체.
Mark 단계: 루트에서 DFS/BFS로 도달 가능한 모든 객체 마킹.
Sweep 단계: 마킹되지 않은 객체 회수. GC pause 발생.
대부분의 객체는 금방 죽는다는 약한 세대 가설에 기반. Young/Old 세대를 나눠 GC 빈도를 다르게 적용.
Eden
새로운 객체 할당
Young Gen
Survivor 0
1회 생존
Young Gen
Survivor 1
2회 생존
Young Gen
Old Gen
장기 생존 객체
Old Gen
Minor GC: Young 세대만 GC. 빠르고 자주 발생 (짧은 pause).
Major GC: Old 세대 포함 전체 GC. 느리지만 드물게 발생 (긴 pause).
임계값(Tenuring Threshold) 이상 생존하면 Old 세대로 승격(Promotion).
Rust 소유권 (Ownership) & 이동 시맨틱스
// Rust 소유권 이동 시뮬레이션
let s1 = String "hello" [소유자: s1];
// s2 = s1 실행 전
Rust에서 값에는 단 하나의 소유자(owner)만 존재합니다. 소유권이 이동(move)되면 이전 변수는 무효화됩니다. 빌리기(&)는 소유권 없이 임시 접근을 허용합니다.
메모리 누수 vs 올바른 관리
메모리 누수 (C)
void leak() {
  char *p = malloc(1024);
  // ... 사용 ...
  // free(p) 누락!
  return; // 1024 bytes 누수
}
// 반복 호출 → 메모리 고갈
올바른 관리 (Rust)
fn no_leak() {
  let v = Vec::new(); // 힙 할당
  // ... 사용 ...
  // 스코프 종료 → Drop 자동 호출
} // ← 여기서 자동 해제!
// 컴파일러가 보장 → 런타임 0 비용