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가지 핵심 요구 사항
- Privileged Instructions (특권 명령 제한)
- 예 :
in
,out
,hlt
,cli
,sti
등 - 유저 모드에서 실행 불가. 커널 모드만 실행 가능
- 하드웨어가 이를 구분하도록 한다 (mode bit)
- 예 :
- Memory Protection
- 프로세스가 다른 프로세스 메모리를 읽거나 쓰는 것을 막는다
- MMU + 페이지 테이블을 통해 달성
- 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번째 비트에 있는 수
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
은 양방향
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은 정해진 시간만큼만 실행하고 다음 프로세스로 전환한다.
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
특히:
- 구글은 리눅스에 SO_INCOMING_CPU 소켓 옵션을 추가했는데,
- 이걸 사용하면 수신된 TCP 연결이 수신 당시의 CPU에 고정됨.
- 멀티 프로세스 TCP 서버에서 SO_REUSEPORT + BPF로 요청을 CPU별로 분산시킬 수 있음.
- [net-next] net: introduce SO_INCOMING_CPU 공식 문서
- https://patchwork.ozlabs.org/project/netdev/patch/1415393472.13896.119.camel@edumazet-glaptop2.roam.corp.google.com/
'IT 성장기 (교육이수) > 크래프톤정글 (2025.03-07)' 카테고리의 다른 글
[크래프톤정글] Week14 Toy Project - 개발 일지 (TIL) (3) | 2025.06.13 |
---|---|
[PintOS] Project 3 : Virtual Memory 이해 (2) | 2025.06.04 |
[PintOS] Project 2 : Argument Passing (0) | 2025.05.30 |
[PintOS] Project 1 : Priority Donation 구현 (0) | 2025.05.30 |
[PintOS] GDB Debugging Tool 사용 방법 (1) | 2025.05.29 |