C
운영체제/기초/Lesson 02

OS 기초 + 프로세스·스레드

45분·theory

OS 기초 + 프로세스·스레드

🎯 이 lesson 을 읽고 나면

이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.

  • ✅ OS 의 4대 역할 (프로세스·메모리·파일·I/O)
  • ✅ User Mode vs Kernel Mode + syscall
  • ✅ Node.js 가 왜 단일 스레드인데 빠른지

학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.

OS가 하는 4가지 일

한 줄: 운영체제(OS) = 하드웨어 위에서 프로세스·메모리·파일·I/O 를 추상화하는 소프트웨어 층.

OS 4 대 책임:

영역의미
프로세스 관리실행 중 프로그램 생성·전환·종료. fork()·exec()·exit()
메모리 관리가상 메모리·페이징·할당. 각 프로세스가 독립된 메모리 환상
파일 시스템디스크 추상화·디렉토리·권한. inode 기반
I/O 관리키보드·네트워크·디스크 통신. 디바이스 드라이버

부팅 → 카톡 실행 흐름 (6단계):
1. 전원 ON — BIOS/UEFI → 부트로더 → 커널 로드
2. 커널 초기화 — 메모리 매핑·드라이버 로드·스케줄러 시작
3. init 프로세스 — PID 1, 모든 사용자 프로세스의 부모 (systemd / launchd)
4. 로그인 셸 — bash/zsh 시작, 환경 변수 로드
5. 앱 실행fork() + exec("/Applications/KakaoTalk.app/...") → 새 프로세스
6. 이벤트 루프 — 키보드·네트워크 입력 대기·처리

> 💡 카톡 한 번 실행 = OS 가 위 6 단계를 보이지 않게 수행. 매번 새로 시작 시 0.5 초 안에.

프로세스 vs 스레드 — 동시성의 두 모델

항목프로세스스레드
메모리완전 독립같은 프로세스 내 공유
생성 비용무거움 (수 ms)가벼움 (수 μs)
통신IPC (파이프·소켓·공유메모리)메모리 공유 + 동기화
격리성강함 (한 프로세스 죽어도 다른 영향 X)약함 (한 스레드 크래시 → 전체 죽음)
컨텍스트 스위치 비용높음 (메모리 맵·캐시 무효화)낮음

언제 무엇을:

  • 프로세스 분리: 안정성 우선 (Chrome 탭별 프로세스), 보안 격리 (Docker 컨테이너), 다른 언어 통합
  • 스레드 사용: 가벼운 동시성 (웹 서버 요청별 스레드), 공유 데이터 처리 (GUI 이벤트 루프)
  • 현대 트렌드: 가상 스레드(Java 21)·고루틴(Go)·async/await(Python·Node) — 스레드보다 더 가벼운 경량 동시성

흔한 실수:

  • ❌ 스레드 1000개 생성 → 컨텍스트 스위치 비용 폭발
  • ❌ 공유 데이터를 락 없이 수정 → race condition
  • ✅ 스레드 풀(ThreadPoolExecutor) + 락 또는 async/await 사용

컨텍스트 스위칭 — 동시성의 *진짜 비용*

컨텍스트 스위칭 6단계 (CPU 가 프로세스 A → B 로 전환):
1. 인터럽트 — 타이머·I/O 완료·시스템 콜
2. A 상태 저장 — 레지스터·PC·스택 포인터를 PCB(Process Control Block)에
3. 스케줄러 호출 — 다음 실행할 프로세스 선택 (CFS·O(1)·실시간)
4. B PCB 로드 — 레지스터·MMU 매핑 복구
5. 캐시 무효화 — L1/L2 캐시·TLB(Translation Lookaside Buffer) 일부 플러시
6. B 실행 — 멈췄던 곳부터 재개

비용:

자원비용
시간~1-10 μs (단순 스위치)
캐시 미스TLB 플러시 시 +수 μs
메모리 대역폭PCB I/O

오버헤드 함정:

  • ❌ 스레드 너무 많이 (1000+) → CPU 가 실제 작업보다 스위칭에 더 많은 시간
  • ❌ 비동기 코드인데 CPU bound 작업 → 메인 스레드 블록
  • ✅ CPU 코어 수와 비슷한 스레드 수 (대략 N + 1) 권장

> 💡 왜 비동기가 빠른가? = 컨텍스트 스위칭 없이 한 스레드에서 다중 작업 (이벤트 루프).

💻 📌 시나리오로 배우는 프로세스·스레드 명령어
# ============================================================
# 시나리오 1: "어? 서버가 느려졌어. 뭐가 CPU 다 먹지?"
# ============================================================
# 1) 실시간 모니터로 *CPU 많이 쓰는* 프로세스 찾기
htop                          # 컬러 인터랙티브 (top 의 발전형, 권장)
top -o %CPU                   # htop 없을 때

# 2) 의심 프로세스 PID 찾기
pgrep -fl node                # 'node' 가 들어간 명령 + PID 출력
# 결과: 28391 /usr/bin/node /app/server.js

# 3) 자세히 조사 — 메모리·상태·시작 시각
ps -p 28391 -o pid,ppid,user,stat,%cpu,%mem,start,cmd
# STAT 컬럼:
#   R = 실행 중   S = 대기 중   D = I/O 대기 (못 죽임)
#   Z = 좀비      T = 정지됨    < = 높은 우선순위

# 4) 한 프로세스가 *몇 개 스레드* 쓰는지
ps -T -p 28391                # 스레드 목록
# 또는: cat /proc/28391/status | grep Threads

# ============================================================
# 시나리오 2: "프로세스가 멈췄어. 어떻게 죽이지?"
# ============================================================
# *항상* 정상 종료 시도부터 — 데이터 저장·정리 기회 부여
kill 28391                    # = kill -TERM (SIGTERM, 15)
                              # 앱이 graceful shutdown 가능 (DB 연결 정리·진행 중 요청 완료)

# 5초 기다려도 안 죽으면 강제
kill -9 28391                 # SIGKILL — OS 가 *즉시* 메모리에서 제거
                              # ⚠️ 데이터 손상 위험. 최후 수단

# 설정 다시 읽기 (재시작 X)
kill -HUP 28391               # SIGHUP — nginx·systemd 서비스 reload 표준

# 이름으로 일괄
killall -TERM nginx           # 모든 nginx 프로세스 정상 종료
pkill -f 'node.*server.js'    # 패턴 매칭

# ============================================================
# 시나리오 3: "백그라운드 무거운 작업이 *다른 작업* 방해해"
# ============================================================
# 우선순위 조정 — nice 값 -20(높음) ~ +19(낮음). 기본 0.
nice -n 10 python heavy_batch.py    # 낮은 우선순위로 새로 실행
                                     # 다른 작업 양보, 자기는 천천히

renice -n 5 -p 28391                # *실행 중인* 프로세스 우선순위 변경
                                     # 양수 = 양보. ↑ 값 = ↓ 우선순위 (직관과 반대!)

# ============================================================
# 시나리오 4: "프로세스가 죽었는데 ps 에 Z(좀비)로 남아있다"
# ============================================================
# 좀비 = 자식 종료 → 부모가 wait() 안 부름 → PCB 만 남음
ps aux | awk '$8=="Z"'        # 좀비만 추출
# 또는: ps -eo stat,pid,ppid,cmd | grep -w Z

# 좀비는 *부모 죽이면* init(PID 1) 이 입양 → 정리
# 부모 PID 확인 후
kill -CHLD <부모PID>          # 부모에게 자식 정리하라 신호
# 안 되면 부모 재시작 권장

# ============================================================
# 시나리오 5: "프로세스가 어떤 파일을 열고 있는지 보고 싶다"
# ============================================================
lsof -p 28391                 # 그 PID 가 연 모든 파일·소켓·라이브러리
lsof -i :3000                 # *3000 포트* 점유 프로세스 (반대 방향)
lsof -i TCP:443 -sTCP:LISTEN  # 443 LISTEN 중인 것만

# fd 제한 확인 (자주 만나는 'too many open files')
cat /proc/28391/limits | grep 'open files'
ulimit -n                     # 현재 셸 제한
ulimit -n 65536               # 늘리기 (영구는 /etc/security/limits.conf)

# ============================================================
# 시나리오 6: "백그라운드로 돌리면서 시그널 무시"
# ============================================================
nohup python server.py &      # 로그아웃해도 안 죽음, nohup.out 에 출력
disown -h %1                  # 이미 실행 중인 job 에 nohup 효과
# 또는 systemd 서비스로 등록 (장기 운영 표준)

시스템 콜 · 인터럽트 · 컨텍스트 스위칭 — 한 페이지

사용자 공간 vs 커널 공간

OS 메모리는 2개 영역:

  • 사용자 공간 (User Space) — 우리 앱이 실행되는 곳. 권한 제한적. 메모리·디스크·네트워크에 직접 접근 불가.
  • 커널 공간 (Kernel Space) — OS 자체가 사는 곳. 모든 하드웨어 권한.
code
[User Space]    [내 앱]
     ↓ syscall
[Kernel Space]  [OS 커널]
     ↓
[Hardware]      [디스크, NIC, ...]

사용자 앱이 파일을 열려면 — 직접 디스크에 접근 X. 반드시 OS 에 요청 해야 함. 이 요청이 시스템 콜.

시스템 콜 (System Call)

c
// C 에서 파일 열기
int fd = open("data.txt", O_RDONLY);    // syscall
read(fd, buf, 100);                       // syscall
close(fd);                                 // syscall

open / read / write / close / fork / exec — Linux 기준 약 300개. 모든 I/O · 프로세스 생성 이 syscall.

고수준 언어 (Python, Java) 의 모든 파일/네트워크 호출 이 내부적으로 syscall 을 부릅니다.

syscall 의 비용

사용자 공간 → 커널 공간 전환 자체가 비쌉니다 (마이크로초 단위). 그래서:

  • 버퍼링 — 매번 syscall 안 하고 모아서 한 번에. BufferedReader, console.log 의 내부 버퍼.
  • 비동기 I/O — Node.js, async/await — syscall 대기 중 다른 작업 처리.

인터럽트 vs 폴링

폴링 (Polling) — 계속 묻기

python
while not done():
    pass    # 끝났나? 끝났나? 끝났나? — CPU 100%

CPU 시간을 낭비. 바쁜 대기.

인터럽트 (Interrupt) — 알려줘

하드웨어가 "준비됐어!" 신호를 보내면 CPU 가 즉시 처리:

  • 키보드 입력 — 키 누르는 순간 IRQ 발생 → 커널이 이벤트로 변환 → 앱에 전달
  • 네트워크 패킷 도착 — NIC 가 인터럽트 → 커널이 버퍼에 저장 → 앱 깨움
  • 타이머 만료setTimeout 의 기반

"폴링은 비효율, 인터럽트가 표준" — 현대 OS 의 거의 모든 I/O 는 인터럽트 기반.

컨텍스트 스위칭 (Context Switching)

CPU 가 프로세스/스레드 사이를 바꿀 때 — 현재 상태 (레지스터·PC·메모리 맵핑) 를 저장하고 다음 작업의 상태를 복원.

비용

  • 프로세스 스위칭 — 메모리 맵핑까지 바꿔야 함. 비쌈 (~수 ㎲).
  • 스레드 스위칭 — 같은 메모리 공유. 프로세스보다 5~10배 가벼움.
  • 함수 호출 — 단순 스택 푸시. 나노초 단위.

그래서 — 멀티스레드가 멀티프로세스보다 빠릅니다.

왜 Node.js 가 단일 스레드인데 빠른가

Node.js 의 "단일 스레드 + 이벤트 루프":

code
[메인 스레드 — JS 실행]
     ↑
   이벤트 큐
     ↑
[libuv 스레드풀] ←— 비동기 I/O 시 사용
[커널 — async syscall (epoll, kqueue)]

핵심 아이디어:

1. I/O 가 99% 의 시간을 잡아먹는다 (DB·API·디스크)
2. I/O 대기 동안 메인 스레드가 다른 요청 처리
3. 컨텍스트 스위칭 비용 거의 0 — 스레드 자체가 1개

Apache 같은 "요청당 스레드" 모델: 1만 요청 = 1만 스레드 = 컨텍스트 스위칭 폭발 + 메모리 폭발.

Node 모델: 1만 요청 = 1 메인 스레드 + 4~8 워커 스레드 + 이벤트 큐. 비교 불가의 효율.

정리 — 바이브 코딩 연결

  • fetch / 파일 읽기 = syscall → 비싸다 → 최소화 (배치·캐싱)
  • Node.js·async/await = 이벤트 기반I/O 많은 서버에 강함
  • CPU 무거운 작업은 Worker Thread → 메인 이벤트 루프 막지 마세요

면접에서 "Node.js 가 왜 빠르냐" 물으면 이 페이지의 내용을 그대로 말하면 합격 입니다.

🤖 AI 에게 이렇게 요청해보세요

이 lesson 의 개념을 알면 AI 에게 구체적으로 지시할 수 있습니다. 막연한 "고쳐줘" 가 아니라 어휘를 가진 요청 — 그게 토큰 절약의 출발점입니다.

  • "이 작업의 syscall 추적을 strace (Linux) 로 실행하는 명령어 알려줘"
  • "이 코드가 사용자 공간 / 커널 공간 어느 쪽에서 비용 큰지 진단해줘"

왜 이게 토큰을 줄이나

개념을 모를 땐 AI 답변을 받고도 "그게 뭐예요?" 를 다시 물어야 합니다. 그 "다시 물음" 이 토큰을 잡아먹습니다. 개념 한 번 익혀두면 대화가 한 번에 끝납니다.

먼저 읽으면 좋은 개념: 운영체제
OS 기초 + 프로세스·스레드 - 운영체제