[CS] Jungle OS 특강 정리

Jungle OS 특강 정리

2025년 6월 2일 KAIST 정주원 코치님

1. 프로젝트 1 vs 2: 쓰레드와 프로세스

“쓰레드는 프로세스의 작업 단위인가?”
”왜 프로젝트 1에서 쓰레드를 먼저 구현하고, 프로젝트 2에서 프로세스를 구현하는가?”

  • 정확한 표현은 프로세스는 자원 단위이고, 쓰레드는 실행 단위이다. 하나의 프로세스는 여러 쓰레드를 가질 수 있다.
  • 프로세스(Process):
    • 컴퓨터의 추상화.
    • 프로그램의 실행 단위.
    • 메모리 보호 단위로, 서로 메모리를 공유하지 않음.
  • 쓰레드(Thread):
    • CPU 코어의 추상화.
    • CPU의 실행 단위.
    • 같은 프로세스 내 쓰레드들은 메모리를 공유함 (예: 세마포어 리스트).
  • 🔥 중요 포인트: 쓰레드가 프로세스의 하위 단위라는 표현은 계층 구조가 아니라 "무엇을 추상화하느냐"의 차이다.

2. 가상 주소 공간과 PML4

  • PML4: x86_64에서 사용하는 다단계 페이지 테이블의 루트. Radix Tree 구조로 구현되어 있음.
  • 이유: 주소 공간이 너무 커서, 단순 리스트보다 공간 절약 측면에서 트리가 유리함. (메모리 절약)
  • 단점: 탐색 시간이 리스트보다 길어짐.

3. Storage: 파일 시스템의 어려움

  • 데이터와 메타데이터는 동기화되어야 함.
    • 데이터 : 파일에 저장되는 실제 내용 (예: hello.txt 의 텍스트)
    • 메타데이터 : 파일의 이름, 크기, 수정 시간, 접근 권한 등
    • 문제 상황 : 파일을 쓸 때, 내용만 쓰고 메타데이터를 쓰지 않거나, 그 반대의 경우
  • 데이터를 주고 받는 객체(예: 앱, OS, 디스크) 간에도 동기화되어야 함
    • 문제 상황 : 앱이 write() 후 실제 OS의 page cache에만 반영되고, 디스크에 쓰지 않은 경우
    • 왜 중요한가? : OS는 성능 향상을 위해 파일을 쓸때 디스크에 바로 쓰지 않고 page cache에만 임시로 저장하는 경우가 많다. → 갑자기 시스템 크래시 또는 전원 off 되면 데이터 날아감
    • 디스크와 캐시 간 데이터 일관성을 확보하지 않으면 시스템 크래시 시 데이터 손실 발생
    • fsync() 호출을 통해 디스크와 실제 동기화(flush) 필요
  • 하지만 리눅스는 이를 보장하지 않음. (Persistence 보장 안 됨)
    • 즉, "뭐가 바뀌어도 OS 책임 아님 ㅅㄱ" → 개발자가 신경 써야 할 영역.
  • fsync 란 ?
    • 파일 디스크립터 fd에 대응되는 파일의 모든 변경 사항(데이터+메타데이터)을 디스크에 강제적으로 저장하는 시스템 호출
#include <unistd.h>
int fsync(int fd); 
// success -> return 0 
// false -> reuturn -1, errno 설정

4. OS 설계 초기 문제점

  • MSDOS처럼 모든 앱이 하드웨어를 직접 다루면 발생하는 문제:
    • 무한 루프 돌면서 CPU 점유
    • 다른 앱의 메모리 침범
    • 커널 침범
  • ✅ 문제 : 어플리케이션이 시스템을 망가트릴 수 있다
  • ➡️ 이런 문제를 막으려면 OS가 반드시 제어권을 가져와야 한다

보호를 위한 3가지 핵심 요구 사항

  1. Privileged Instructions (특권 명령 제한)
    • 예 : in, out, hlt, cli, sti
    • 유저 모드에서 실행 불가. 커널 모드만 실행 가능
    • 하드웨어가 이를 구분하도록 한다 (mode bit)
  2. Memory Protection
    • 프로세스가 다른 프로세스 메모리를 읽거나 쓰는 것을 막는다
    • MMU + 페이지 테이블을 통해 달성
  3. Timer Interrupt
    • 무한 루프 도는 앱이 있어도, 일정 시간이 지나면 인터럽트를 발생시켜 OS가 제어권을 회수

해결 → Dual Mode (User vs Kernel Mode)

  • 커널 모드: 특권 명령 실행 가능 (e.g., hlt, in/out)
  • 유저 모드: 제한된 명령만 실행 가능
  • 하드웨어에서 Mode Bit로 유저/커널을 구분
  • syscall 명령으로 유저 → 커널로 전환

명령 실행의 흐름

CPU는 현재 실행 중인 코드가 “커널 수준 권한인지”, “유저 수준 권한인지” 구분해야 한다. 이를 구현하는 방식이 mode bit 또는 ring level (x86에서는 Ring 0~3)

 

1. CPU가 명령을 실행하면서 메모리에 접근하려 한다

  • int a = ptr;
  • ptr 은 논리 주소 : logical address = [segment selector] + offset

2. 세그먼트 디스크립터를 참조하여 선형 주소 생성

  • segment selector는 세그먼트 테이블(GDT or LDT)의 인덱스
  • 거기서 해당 세그먼트의 base, limit, DPL을 읽고
  • offset을 더해서 선형 주소(linear address)를 만든다
  • ➡️ 이 때, CPU가 현재 실행 중인 코드의 권한 레벨(CPL)과 세그먼트의 권한 레벨(DPL)을 비교한다
    • CS 레지스터에 대한 개념 혼동 : 커널의 virtual memory 안의 code/text/bss/heap 이 부분이 아니다!
    • CS 레지스터는 현재 실행 중인 코드가 속한 세그먼트를 가리키는 하드웨어 레지스터
    • 출처 : http://www.c-jump.com/CIS77/ASM/Memory/lecture.html
    • 세그먼트 레지스터
      • CS : Code Segment
      • SS : Stack Segment → 스택이 저장되는 위치
      • DS/ES/FS/GS : Data Segment → 일반 데이터 접근용
    • 세그먼트 디스크립터
      • ACCESS : 접근 권한 및 타입 (read/write, execute, privilege level 등)
      • LIMIT : 세그먼트의 크기
      • BASE ADDRESS : 해당 세그먼트의 시작 주소
      • 이 디스크립터는 GDT(Global Descriptor Table) or LDT(Local Descriptor Table)에 위치
    • 실제 물리 메모리에 매핑
      • 세그먼트 디스크립터의 BASE + OFFSET 의 조합으로 4번에서 해석된다
      • CODE : 실행 코드 (.text)
      • STACK : 함수 호출/지역 변수 등 (.stack)
      • DATA : 전역/정적 변수, 힙 등 (.data, .bss)CPL (Current Privilege Level)은 현재 실행되고 있는 테스크의 특권 레벨. CS (Code Segment) 레지스터의 0, 1번째 비트에 있는 수

 

Segment Register

3. 권한 체크

  • CPL ≤ DPL 이어야 접근 허용
    • 커널 코드 (CPL = 0)는 모든 접근 가능
    • 유저 코드 (CPL = 3)는 DPL이 3인 세그먼트만 접근 가능
  • 이 조건이 맞지 않으면 → CPU가 General Protection Fault 발동

 

4. 권한 통과 → MMU 가 페이지 테이블로 물리 주소 변환

  • 선형 주소 → MMU → 페이지 테이블 → 물리 주소
  • 물리 주소를 실제 메모리에 접근할 때 사용한다


5. 디바이스 성능 문제와 우회

  • Dual Mode 의 단점은 디바이스의 성능을 극대화하여 사용하지 못한다는 점이다.
  • 커널을 통하지 않고 디바이스를 빠르게 다루고 싶다 → DirectX, DPDK 등
  • 하드웨어 성능을 제대로 활용하려면, 어떻게 추상화했는지가 관건

6. 메모리 보호와 타이머 인터럽트

  • 각 프로세스는 자신의 Page Table과 가상 메모리를 갖고, 실행될 때마다 그걸 스위칭함
  • 메모리 침범 방지 및 CPU 점유 문제 해결 위해 인터럽트 기반 타이머 사용
    • 요즘은 PIT(Programmable Interval Timer) 대신, CPU 내장 하드웨어 타이머 사용

7. 파일 보호

  • 파일 접근 시 Reference Monitor를 두고 사용자 권한에 따라 접근 제어 (user/group/other)
  • 어플리케이션 단에서는 Role-Based Access Control도 사용 가능

8. IPC (Inter-Process Communication)

“서로 격리된 프로세스(어플리케이션)들이 어떻게 데이터나 상태를 주고 받을까?”

  • Shared Memory 방식: 두 프로세스가 같은 메모리 주소를 매핑
  • Message Passing 방식: 커널을 경유하여 데이터를 주고받음 (예: socket, pipe)
    • pipe는 단방향, socket은 양방향

Shared Memory
Pipe


9. Sharing: CPU & Memory

  • 결국 컴퓨터의 모든 문제는 CPU의 시간과 메모리 공간을 얼마나 효율적으로 사용하는지로 귀결된다.
  • CPU: Time Sharing (스케줄링 필요)
  • Memory: Space Sharing (페이지 교체 필요 → swap)
  • 페이지 교체 정책: LRU, Clock, Optimal

10. 스케줄링

  • 선점형 (Preemptive): OS가 CPU 제어권을 강제로 회수 가능
  • 비선점형 (Non-Preemptive): 작업자가 자발적으로 양보해야 함
  • 스케줄링 알고리즘
    • FIFO: 응답시간 안좋음, 콘보이 효과
    • SJF (Shortest Job First): 응답 빠름, 기아 발생
    • RR (Round Robin): 응답 빠름, 턴어라운드 타임 낮음
    • 여기서 주목할 차이점은, FIFO는 프로세스를 종료될 때까지 연속으로 실행하는 반면, Round-Robin은 정해진 시간만큼만 실행하고 다음 프로세스로 전환한다.

FIFO vs RR


11. FYI : 자원 공유의 예 – 네트워크 스택 최적화

구글은 리눅스 커널에 CPU별 TCP 수신 버퍼를 도입하여, 수신된 네트워크 패킷이 처리되는 CPU에 바로 저장되도록 최적화했다.

이는 자원 공유(네트워크)를 하되, 캐시 친화성과 CPU 간 간섭 최소화를 통해 성능을 극대화한 사례다.

 

✅ 전통적 방식의 문제

  • 네트워크 패킷이 들어오면, 네트워크 인터페이스 카드(NIC)가 임의의 CPU 코어에서 인터럽트를 발생시켜 수신 처리함
  • 근데 해당 패킷을 처리해야 할 애플리케이션은 다른 CPU에서 실행 중일 수 있음
  • 그러면 캐시 미스, lock contention, cross-core data movement 등으로 성능 손실 발생

✅ 해결책 : 해당 코어(Local CPU)에서 데이터 버퍼를 유지하도록 최적화

“TCP 수신 버퍼가 현재 패킷을 처리하는 CPU에 맞춰 배정되도록 한다”

  • NIC가 수신 패킷을 특정 CPU로 전달하도록 설정한다 → RSS
  • 리눅스 커널이 각 CPU마다 별도 소켓 큐를 유지 → per-CPU socket buffer
  • 해당 CPU의 softirq 에서 처리, 같은 CPU에서 애플리케이션 처리 유도 → L1/L2 캐시 히트 증가, 컨텍스트 스위치 최소화

➡️ 구글이 적용한 기술: SO_INCOMING_CPU, SO_REUSEPORT, TCP_FASTOPEN, BPF redirect

특히: