
운영체제(OS)란: 하드웨어와 사용자 사이의 중재자
OS가 없으면 우리는 하드디스크의 몇 번째 섹터에 0과 1을 써야 할지 직접 계산해야 합니다. 리눅스와 윈도우가 대신 해주는 일들.

OS가 없으면 우리는 하드디스크의 몇 번째 섹터에 0과 1을 써야 할지 직접 계산해야 합니다. 리눅스와 윈도우가 대신 해주는 일들.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

처음 코딩을 배울 땐 "OS는 그냥 윈도우나 맥 같은 거"라고만 생각했습니다. 컴퓨터를 켜면 바탕화면이 나오고, 폴더를 클릭하면 파일이 보이고, 프로그램을 실행하면 작동하는 것. 그게 전부였습니다.
그런데 첫 서버 배포를 하려고 AWS EC2에 접속했을 때 멘탈이 깨졌습니다. 검은 화면에 흰 글자만 깜빡이는 리눅스 터미널. 마우스도 없고, 폴더 아이콘도 없고, 시작 버튼도 없었습니다. "이게 무슨 운영체제야? 윈도우랑 완전 다른데?"
그때 깨달았습니다. 내가 알던 건 GUI(그래픽 껍데기)일 뿐이었고, 진짜 OS의 본질은 눈에 보이지 않는 깊은 곳에서 작동하고 있었다는 걸. 그 순간부터 OS를 제대로 이해해야겠다고 다짐했습니다.
제가 OS를 이해하는 데 가장 와닿았던 건 "OS가 없는 컴퓨터"를 상상해본 것입니다. 이걸 Bare Metal(맨쇠)이라고 부릅니다. CPU, 메모리, 하드디스크만 덩그러니 있는 컴퓨터죠.
여기서 단순히 화면에 "Hello World"를 출력하려면 어떻게 해야 할까요?
파이썬으로 print("Hello")라고 한 줄만 쓰면 되는 걸, OS 없이는 수백 수천 줄의 어셈블리 코드를 작성해야 합니다. 이 과정에서 "아, 결국 OS는 이 모든 지저분한 하드웨어 제어를 대신 해주는 존재구나"라고 받아들였습니다.
OS를 한 문장으로 정의하자면 "한정된 하드웨어 자원을 여러 프로그램이 효율적으로 나눠 쓸 수 있게 관리하는 정부(Government)"입니다. 이 비유가 정말 와닿았던 게, 정부도 땅, 돈, 인력 같은 제한된 자원을 여러 국민/기업에게 배분하고 감독하잖아요. OS도 똑같습니다.
제 맥북에는 CPU 코어가 8개 있지만, 동시에 실행 중인 프로세스는 300개가 넘습니다. 어떻게 8개 코어로 300개 작업을 처리할까요?
답은 시분할(Time Sharing)입니다. OS는 각 프로세스에게 아주 짧은 시간(보통 수 ms)씩 CPU를 할당합니다. 너무 빨라서 우리는 모든 게 "동시에" 실행된다고 느끼지만, 실제로는 빠르게 번갈아 가며 실행되는 거죠.
터미널에서 htop 명령어를 처음 쳐봤을 때 충격이었습니다.
$ htop
실시간으로 CPU 사용률이 춤추고, 프로세스들이 생겼다 사라졌다 하는 걸 보면서 "OS가 지금 이 순간에도 이렇게 바쁘게 일하고 있구나"라고 느꼈습니다. 크롬이 CPU를 80% 먹고 있으면 다른 프로그램은 느려질 수밖에 없는 이유도 이제 이해했습니다.
메모리(RAM)는 유한합니다. 제 노트북은 16GB인데, 크롬 탭을 20개만 열어도 메모리 경고가 뜹니다. 왜 각 프로그램은 마음대로 메모리를 쓰지 못할까요?
OS는 각 프로세스에게 가상 주소 공간(Virtual Address Space)을 줍니다. 프로그램 입장에서는 "난 0x00000000부터 시작하는 메모리를 혼자 쓴다"고 착각하지만, 실제로는 OS가 물리 메모리의 다른 위치에 매핑해놓은 겁니다.
$ free -m
total used free shared buff/cache available
Mem: 16384 12000 1200 800 3184 3000
Swap: 8192 2048 6144
처음 이 명령어를 봤을 때 "Swap이 뭐지?"라고 궁금했습니다. 알고 보니 RAM이 부족하면 OS가 덜 쓰는 데이터를 하드디스크로 옮기는(Swap Out) 기술이더군요. 느리긴 하지만 프로그램이 죽는 것보단 낫습니다. 이걸 가상 메모리(Virtual Memory)라고 부릅니다.
어떤 프로그램이 할당받지 않은 메모리에 접근하려 하면? OS가 즉시 "Segmentation Fault"를 띄우고 프로그램을 강제 종료시킵니다. 이게 바로 정부가 불법 점유를 단속하는 것과 같다고 이해했습니다.
하드디스크는 물리적으로는 그냥 거대한 자석 원판입니다. 데이터는 트랙(Track)과 섹터(Sector)라는 기계적 단위로 저장됩니다. 하지만 우리는 "내 문서/프로젝트/report.docx"라는 경로로 파일을 찾죠.
이 마법 같은 일을 해주는 게 파일 시스템(File System)입니다. OS는 raw 디스크 위에 파일/폴더라는 추상화 계층을 얹어줍니다.
$ ls -lh /var/log/
total 1.2G
-rw-r----- 1 root adm 52M Feb 6 10:23 syslog
-rw-r----- 1 root adm 120M Feb 5 23:59 syslog.1
drwxr-xr-x 2 root root 4.0K Jan 15 06:25 apt/
ls 명령어를 쓰면 OS가 디스크의 inode 테이블을 뒤져서 파일 크기, 권한, 수정 시간까지 깔끔하게 정리해줍니다. 이 작업을 직접 하려면 파일 시스템 포맷(ext4, NTFS, APFS)을 공부하고 바이너리 구조를 파싱해야 합니다. 생각만 해도 끔찍합니다.
OS는 크게 두 부분으로 나뉩니다.
커널은 하드웨어를 직접 제어하는 핵심 코드입니다. CPU 레지스터에 값을 쓰고, 메모리 컨트롤러를 조작하고, 디스크 I/O를 처리합니다. 보안상 일반 사용자 프로그램은 커널 영역에 접근할 수 없습니다.
리눅스 커널 버전을 확인하면 이렇게 나옵니다.
$ uname -a
Linux my-server 5.15.0-89-generic #99-Ubuntu SMP x86_64 GNU/Linux
이 5.15.0-89-generic이 바로 커널 버전입니다. 리누스 토르발스가 1991년에 처음 만든 코드가 지금도 계속 발전하고 있다는 게 신기했습니다.
쉘은 사용자가 커널과 대화할 수 있게 해주는 인터페이스입니다. 우리가 터미널에 ls, cd, rm 같은 명령어를 치면, 쉘이 이걸 해석해서 커널에게 "파일 목록 좀 가져와", "디렉토리 바꿔", "파일 삭제해"라고 요청합니다.
쉘의 종류도 다양합니다: Bash, Zsh, Fish, PowerShell... 각자 문법과 기능이 조금씩 다르지만, 결국 하는 일은 같습니다. 커널과의 통역.
OS가 처음부터 이렇게 똑똑했던 건 아닙니다. 1950년대 초기 컴퓨터는 배치 처리(Batch Processing) 방식이었습니다. 프로그램을 천공 카드에 적어서 한꺼번에 넣으면 순차적으로 실행되는 거죠. 프로그램 하나 돌리는 데 몇 시간이 걸려도 기다려야 했습니다.
1960년대에 멀티태스킹(Multitasking)이 등장했습니다. 여러 프로그램을 메모리에 올려놓고 CPU를 번갈아 쓰게 한 거죠. Unix가 이 시기에 탄생했고, 지금 우리가 쓰는 대부분의 개념(프로세스, 파일 시스템, 셸)이 이때 정립됐습니다.
1980~90년대엔 개인용 컴퓨터(PC)가 보급되면서 Windows, macOS 같은 GUI 기반 OS가 나왔습니다. 그리고 2000년대 이후엔 서버용 Linux, 모바일용 Android/iOS, 임베디드용 RTOS(Real-Time OS)까지 OS가 분화했습니다.
이 역사를 보면서 "결국 OS는 하드웨어 발전에 맞춰 계속 진화한다"는 걸 받아들였습니다.
일반 사용자용입니다. GUI가 잘 되어 있고, 멀티미디어 지원이 좋습니다. 개발자 입장에서 차이를 정리해본다면:
저는 맥북으로 개발하다가 서버는 Ubuntu Linux를 쓰는데, 처음엔 혼란스러웠지만 지금은 "결국 둘 다 Unix 계열이니 명령어가 비슷하구나"라고 이해했습니다.
Ubuntu, CentOS, Debian, Alpine... 전부 리눅스 커널을 쓰지만 패키지 관리자와 기본 설정이 다릅니다. AWS EC2에서 Ubuntu를 고른 이유는 "문서화가 잘 되어 있고 커뮤니티가 크다"는 이유였습니다.
자동차, 로봇, 의료기기 같은 곳엔 RTOS(Real-Time OS)가 들어갑니다. FreeRTOS, VxWorks 같은 것들인데, "10ms 안에 반드시 응답해야 한다"는 식의 시간 보장이 핵심입니다. 일반 OS는 "언젠가는 처리해준다"지만, RTOS는 "정확히 언제까지 처리한다"고 약속합니다.
Android는 리눅스 커널을 씁니다. 하지만 일반 리눅스와 달리 터치스크린, 센서, 배터리 관리에 특화되어 있습니다. iOS는 macOS와 같은 Darwin 커널을 쓰지만 모바일에 최적화되어 있죠.
프로그램이 OS에게 뭔가를 요청할 때 쓰는 게 시스템 콜(System Call)입니다. 파일 열기, 메모리 할당, 프로세스 생성... 이 모든 게 시스템 콜입니다.
예를 들어 파이썬에서 파일을 열면:
f = open("test.txt", "r")
data = f.read()
f.close()
내부적으로는 이렇게 됩니다:
fopen() 호출fopen()이 커널에게 open() 시스템 콜 요청read() 시스템 콜로 디스크에서 데이터 읽기close() 시스템 콜로 자원 해제처음 strace라는 도구로 이걸 봤을 때 "와, 한 줄 코드 뒤에 이렇게 많은 일이 벌어지는구나"라고 감탄했습니다.
$ strace python3 -c "open('test.txt').read()"
...
openat(AT_FDCWD, "test.txt", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=1024, ...}) = 0
read(3, "Hello World\n", 4096) = 12
close(3) = 0
...
OS에서 실행 중인 프로그램을 프로세스(Process)라고 부릅니다. 각 프로세스는 고유한 ID(PID)를 받고, 독립된 메모리 공간을 갖습니다.
$ ps aux | grep python
user 12345 2.3 1.5 /usr/bin/python3 app.py
여기서 12345가 프로세스 ID입니다. 프로그램이 죽지 않고 계속 돌아갈 때 kill -9 12345로 강제 종료하는데, 이게 바로 OS에게 "이 프로세스 좀 죽여줘"라고 요청하는 겁니다.
프로세스는 생명주기가 있습니다:
처음 이 개념을 배울 때 "왜 Running 상태가 아니라 Ready 상태가 있지?"라고 궁금했는데, CPU보다 프로세스가 훨씬 많으니 대기 줄이 필요하다는 걸 받아들였습니다.
물리 메모리가 16GB인데, 프로그램들이 쓰는 가상 주소 공간을 다 합치면 100GB가 넘습니다. 어떻게 가능할까요?
페이징(Paging)이라는 기술 덕분입니다. OS는 메모리를 4KB 단위의 페이지로 나누고, 실제로 쓰는 페이지만 물리 메모리에 올립니다. 안 쓰는 페이지는 디스크(Swap 영역)에 저장해뒀다가 필요할 때 불러옵니다.
이게 왜 좋냐면:
물론 Swap을 과도하게 쓰면 디스크 I/O 때문에 엄청 느려집니다(Thrashing). 그래서 서버 튜닝할 때 vm.swappiness 값을 조절한다는 걸 나중에 알게 됐습니다.
컴퓨터 전원을 켜면 무슨 일이 벌어질까요? 이 과정을 정리해본다면:
리눅스에서 부팅 로그를 보면:
$ dmesg | head -20
[ 0.000000] Linux version 5.15.0-89-generic
[ 0.000000] Command line: BOOT_IMAGE=/vmlinuz root=/dev/sda1
[ 0.000000] Kernel memory protection enabled
[ 0.012345] CPU: Intel Core i7-9750H
...
컴퓨터가 켜지는 몇 초 사이에 이렇게 많은 일이 벌어진다는 게 놀라웠습니다.
코드만 짜면 되지, OS를 왜 알아야 할까요? 실제 개발하면서 느낀 이유들:
"왜 내 파이썬 스크립트가 갑자기 느려지지?" → htop으로 보니 다른 프로세스가 CPU를 독점하고 있었음.
"왜 서버가 멈추지?" → free -m으로 보니 메모리가 꽉 차서 Swap을 과도하게 쓰고 있었음.
멀티스레딩을 쓸지, 멀티프로세싱을 쓸지 결정하려면 OS의 스케줄링을 이해해야 합니다. Python GIL(Global Interpreter Lock) 때문에 CPU-bound 작업은 멀티스레딩이 소용없다는 것도 OS 지식이 있어야 이해됩니다.
Docker 컨테이너가 뭔지 이해하려면 "프로세스 격리", "네임스페이스", "cgroups" 같은 리눅스 커널 기능을 알아야 합니다.
Docker를 처음 배울 때 "컨테이너는 가벼운 가상머신"이라고만 알았습니다. 하지만 본질은 다릅니다.
VM(Virtual Machine)은 OS를 통째로 복제합니다. 게스트 OS 전체가 메모리에 올라가므로 무겁습니다.
컨테이너는 호스트 OS의 커널을 공유하고, 프로세스만 격리합니다. 그래서 빠르고 가볍습니다.
$ docker run -it ubuntu bash
root@abc123:/# uname -a
Linux abc123 5.15.0-89-generic #99-Ubuntu x86_64 GNU/Linux
Ubuntu 컨테이너 안에서 커널 버전을 보면, 호스트 머신과 같습니다. "아, 컨테이너는 결국 같은 커널 위에서 돌아가는 고립된 프로세스구나"라고 이해했습니다.
이게 왜 중요하냐면, 맥북에서 Windows 컨테이너를 직접 돌릴 수 없다는 뜻입니다. 커널이 다르니까요. (Docker Desktop은 내부적으로 VM을 써서 우회합니다.)
apt, yum)제가 Ubuntu를 서버로 쓰는 이유는 "개발 환경과 프로덕션 환경을 최대한 비슷하게 유지하려고"입니다. 로컬에서 되는데 서버에서 안 되는 상황을 줄일 수 있습니다.
저는 맥북으로 개발하는데, Homebrew로 패키지 설치하고 iTerm2로 터미널 쓰는 게 편해서입니다. 그리고 iOS 앱 개발은 맥에서만 가능합니다.
요즘은 WSL(Windows Subsystem for Linux) 덕분에 Windows에서도 리눅스 환경을 쓸 수 있습니다. "윈도우 안에서 리눅스 커널을 돌린다"는 발상이 신기했습니다.
OS를 공부하면서 가장 크게 깨달은 건 "추상화의 힘"입니다.
개발자인 우리는 malloc(), open(), fork() 같은 함수만 부르면 됩니다. 그 뒤에서 커널이 페이지 테이블을 조작하고, 디스크 컨트롤러에 명령을 보내고, CPU 레지스터를 바꾸는 수천 줄의 코드가 돌아갑니다.
이 추상화 덕분에 우리는 비즈니스 로직에 집중할 수 있습니다. "사용자가 버튼을 누르면 데이터베이스에 저장한다"는 로직만 생각하면 되고, "하드디스크의 몇 번째 섹터에 바이트를 쓴다"는 건 신경 쓰지 않아도 됩니다.
결국 OS는 "복잡함을 숨기고, 단순함을 제공하는" 시스템입니다. 그리고 이 시스템을 이해하면 내가 작성한 코드가 실제로 어떻게 작동하는지, 왜 느린지, 어떻게 최적화할 수 있는지 보이기 시작합니다.
지금도 제 맥북 안에서 수백 개의 프로세스가 밀리초 단위로 CPU를 나눠 쓰고, 메모리를 주고받고, 디스크를 읽고 쓰고 있습니다. 그 모든 혼돈을 질서정연하게 만드는 게 바로 OS입니다.
이 사실을 받아들이고 나니, 컴퓨터가 조금 더 친근하게 느껴졌습니다.