운영체제의 역사와 발전
운영체제가 없던 시절부터 일괄 처리, 다중 프로그래밍, 시분할, 개인용 컴퓨터, 모바일과 클라우드까지, 시대마다 부딪힌 문제와 그 해결책을 따라가며 스케줄링, 메모리 보호, 동기화 같은 개념이 왜 생겨났는지 이해합니다. 입출력을 기다리는 동안 다른 작업을 실행하면 CPU 이용률이 어떻게 달라지는지 계산해 봅니다.
운영체제의 핵심 개념들은 한꺼번에 만들어진 것이 아닙니다. 컴퓨터를 쓰는 환경이 바뀔 때마다 새로운 문제가 생겼고, 그 문제를 풀면서 다중 프로그래밍, 시분할, 가상 메모리 같은 개념이 생겨났습니다. 이 글의 목적은 연도를 외우는 것이 아니라, 시대마다 어떤 문제가 있었고 그것을 풀려고 무엇이 등장했는지를 따라가는 것입니다. 그러면 운영체제의 원리가 왜 지금의 모양이 되었는지 자연스럽게 이해할 수 있습니다.
운영체제가 없던 시대
1940–50년대의 컴퓨터에는 운영체제가 없었고, 프로그래머가 하드웨어를 직접 다루었습니다. 프로그램을 천공 카드에 구멍을 뚫어 기록하고, 컴퓨터실에 들어가 카드를 읽힌 뒤 결과가 출력될 때까지 기다렸습니다. 컴퓨터는 1초에 수천 번 연산할 수 있었지만, 사람이 카드를 넣고 결과를 확인하고 다음 사람이 자기 카드를 넣는 데는 수십 분이 걸렸습니다. 당시 컴퓨터 한 대의 값이 수백만 달러였으므로, 이렇게 컴퓨터가 놀고 있는 시간은 엄청난 낭비였고, 이를 줄이려는 노력에서 운영체제가 시작되었습니다.
또 프로그래머마다 하드웨어를 직접 다루어야 했기 때문에, 새 프로그램을 실행하려면 카드 리더의 동작 방식, 프린터의 제어 코드, 메모리의 물리 주소까지 모두 알아야 했습니다. 하드웨어가 바뀌면 프로그램을 처음부터 다시 써야 했으며, 이 불편함이 나중에 하드웨어 추상화와 장치 드라이버라는 개념을 낳았습니다.
일괄 처리 시스템
일괄 처리 시스템(batch system)은 사람이 끼어드는 시간을 줄이려고 등장했습니다. 여러 사용자의 작업(job)을 모아 차례로 자동 실행하는 방식으로, 운영자가 사용자들의 카드 묶음을 모아 한꺼번에 넣으면, 상주 모니터(resident monitor)라는 초기 형태의 운영체제가 작업을 하나씩 읽어 실행하고 끝나면 다음 작업을 불러옵니다. 사람이 매번 오가지 않으니 컴퓨터가 노는 시간이 크게 줄었습니다.
상주 모니터는 최초의 운영체제라고 할 수 있으며, 단순하지만 이미 두 가지 핵심 기능을 갖추고 있었습니다. 한 작업이 끝나면 다음 작업을 자동으로 시작했고, 카드 리더와 프린터 같은 장치를 다루는 코드를 공통으로 두어 프로그램마다 장치를 직접 다루지 않게 했습니다.
하지만 일괄 처리 시스템에는 한계가 있었습니다. 프로그램이 디스크에서 데이터를 읽거나 프린터로 출력하는 동안, 곧 입출력(I/O)을 하는 동안 CPU는 아무 일도 하지 않고 기다렸습니다. 입출력 장치와 CPU의 속도가 수천 배에서 수만 배나 차이 났으므로, CPU 시간의 대부분이 기다리는 데 쓰였습니다. 여기서 "CPU가 노는 동안 다른 프로그램을 실행하면 되지 않을까?"라는 생각이 나옵니다.
다중 프로그래밍
다중 프로그래밍(multiprogramming)은 여러 프로그램을 메모리에 함께 올려 두고, 한 프로그램이 입출력을 기다리면 다른 프로그램에 CPU를 넘기는 방식입니다. 프로그램 A가 디스크 읽기를 요청하면 디스크 컨트롤러가 일하는 동안 CPU는 B를 실행하고, B가 네트워크 응답을 기다리면 C를 실행합니다. A의 디스크 읽기가 끝나면 A는 다시 실행될 수 있는 후보가 되며, 언제 CPU를 다시 받을지는 스케줄러가 정합니다. 실행할 수 있는 다른 작업이 있는 한 CPU가 노는 시간을 줄여 처리량을 높일 수 있지만, 모든 작업이 무언가를 기다리고 있다면 CPU는 여전히 쉬게 됩니다.
다중 프로그래밍을 구현하려 하자 새로운 문제들이 쏟아졌고, 이 문제들이 운영체제의 핵심 주제가 되었습니다. 여러 프로그램이 같은 물리 메모리에 함께 있으니 한 프로그램의 결함이 다른 프로그램의 메모리를 덮어쓰지 못하게 막는 메모리 보호(memory protection)가 필요해졌고, 이것이 베이스-한계 레지스터를 거쳐 가상 메모리로 발전했습니다. 여러 프로그램이 CPU를 원할 때 누구에게 먼저 줄지 정하는 문제는 스케줄링 알고리즘의 출발점이 되었고, 여러 프로그램이 프린터나 디스크 같은 공유 자원에 동시에 접근할 때 순서를 조율하는 문제는 동기화로 이어졌습니다. 1965년 무렵 다익스트라(Edsger Dijkstra)가 세마포어를 제안한 것도 이 시대입니다.
CPU가 실제로 일하는 시간을 계산해 보기
CPU가 하나인 컴퓨터에서 작업 A와 B가 시각 0에 준비되었다고 합시다. A는 CPU를 2ms 쓰고, 입출력을 4ms 기다린 뒤, 다시 CPU를 2ms 씁니다. B는 CPU만 4ms 필요합니다. 작업을 바꾸는 데 드는 시간은 0이고, A의 입출력은 CPU와 동시에 진행될 수 있으며, 다른 기다림은 없다고 가정합니다.
A를 끝낸 뒤 B를 시작하면, A는 0–2ms에 실행하고 2–6ms에 기다린 뒤 6–8ms에 실행하며, B는 8–12ms에 실행합니다. 반면 A가 기다리는 2–6ms에 B를 실행하면, B가 끝나는 6ms에 A의 입출력도 끝나 A를 6–8ms에 마칠 수 있습니다. 두 경우의 CPU 이용률을 다음 Python 코드로 계산해 봅니다.
def utilization(plan):
busy = sum(end - start for _, start, end in plan)
elapsed = max(end for _, _, end in plan)
return busy, elapsed
sequential = [("A", 0, 2), ("A", 6, 8), ("B", 8, 12)]
interleaved = [("A", 0, 2), ("B", 2, 6), ("A", 6, 8)]
for label, plan in [("차례로", sequential), ("겹쳐서", interleaved)]:
busy, elapsed = utilization(plan)
print(f"{label}: CPU 사용 {busy}ms, 경과 {elapsed}ms, 이용률 {busy / elapsed:.1%}")차례로: CPU 사용 8ms, 경과 12ms, 이용률 66.7%
겹쳐서: CPU 사용 8ms, 경과 8ms, 이용률 100.0%plan은 CPU를 쓰는 구간을 (작업, 시작, 끝)으로 적은 목록이고, utilization은 구간의 길이를 더한 CPU 사용 시간과 가장 늦게 끝나는 시각을 돌려줍니다. CPU가 일한 시간은 두 경우 모두 8ms이지만, 차례로 실행하면 전체가 12ms가 걸려 이용률이 66.7%이고, A가 기다리는 동안 B를 실행하면 8ms 만에 끝나 이 가정에서는 이용률이 100%가 됩니다. CPU를 두 개로 늘린 것이 아니라, 기다리는 시간에 준비된 다른 작업을 배치한 결과입니다.
이렇게 다중 프로그래밍이 등장하면서 운영체제는 작업의 순서만 관리하던 프로그램에서 자원을 관리하는 소프트웨어로 바뀌었습니다. 그리고 이때 생긴 메모리 보호, 스케줄링, 동기화의 문제가 오늘날 운영체제 교과서의 중심 내용이 되었습니다.
시분할 시스템과 UNIX
다중 프로그래밍은 처리량을 높였지만, 사용자는 자기 프로그램이 언제 실행될지 알 수 없었습니다. 작업을 제출하고 결과가 나오기까지 몇 시간을 기다리기도 했고, 오타 하나를 고쳐 다시 제출하면 또 몇 시간을 기다려야 했습니다. 시분할 시스템(time-sharing system)은 CPU 시간을 보통 수십 밀리초 정도의 작은 단위인 타임 퀀텀(time quantum 또는 time slice)으로 나누어 여러 사용자에게 번갈아 주는 방식으로 이 문제를 풀었습니다. 그래서 각 사용자는 자기만의 컴퓨터를 쓰는 것처럼 느끼고, 터미널에 명령을 입력하면 거의 바로 반응을 얻습니다.
시분할의 가장 큰 차이는 응답 시간(response time)에 관심을 둔다는 점입니다. 다중 프로그래밍이 전체 처리량을 높이는 것을 목표로 했다면, 시분할은 사용자마다의 응답 시간을 줄이는 것을 목표로 더했고, 이 목표의 차이가 스케줄링 알고리즘의 설계를 크게 바꾸어 놓았습니다.
UNIX가 바로 이런 시분할 시스템으로 태어났습니다. 1969년 AT&T 벨 연구소의 켄 톰프슨(Ken Thompson)과 데니스 리치(Dennis Ritchie)가 개발했으며, 처음에는 어셈블리어로 작성했다가 1973년에 커널을 C로 다시 작성하면서 이식성이 크게 높아졌습니다. 그 과정은 데니스 리치가 남긴 초기 UNIX의 역사에서 볼 수 있습니다. C로 쓴 부분은 새 하드웨어에서도 다시 쓰기 쉬웠지만, 커널을 옮기려면 컴파일러뿐 아니라 부팅 코드, 장치 드라이버, 하드웨어에 따라 달라지는 부분도 함께 맞추어야 했습니다.
이식성 덕분에 UNIX는 대학을 중심으로 빠르게 퍼졌고, BSD, System V, HP-UX, AIX, Solaris 같은 여러 갈래가 생겨났습니다. macOS는 BSD와 Mach 계열의 코드를 바탕으로 하고, Linux는 UNIX의 설계와 인터페이스에 영향을 받아 따로 개발된 UNIX 계열의 커널입니다. UNIX는 디스크와 키보드와 네트워크 소켓까지 파일처럼 다루는 "모든 것은 파일이다"라는 생각, 한 가지 일을 잘하는 작은 프로그램을 파이프(|)로 이어 쓰는 방식, 사람이 읽을 수 있는 텍스트로 설정을 적는 관습을 확립했으며, 이 생각들은 지금의 Linux 명령줄에도 그대로 남아 있습니다.
개인용 컴퓨터와 그래픽 인터페이스
하드웨어의 값이 크게 내리면서 개인용 컴퓨터(PC)가 널리 보급되었습니다. 한 사람이 컴퓨터 한 대를 쓰게 되자, 여러 사용자가 자원을 나누는 문제보다 한 사용자의 편리함이 더 중요해졌습니다. 1981년에 나온 MS-DOS는 IBM PC의 명령줄 운영체제로, 한 번에 프로그램 하나만 실행할 수 있었고 메모리 보호도 없었습니다. 기술만 보면 시분할과 다중 프로그래밍의 발전에서 잠시 뒤로 물러선 셈이지만, 누구나 컴퓨터를 가질 수 있게 된 변화가 훨씬 컸습니다.
Windows는 그래픽 사용자 인터페이스(graphical user interface, GUI)를 널리 퍼뜨렸습니다. 특히 1993년의 Windows NT는 하이브리드 커널, 선점형 멀티태스킹, 메모리 보호를 갖춘 본격적인 현대 운영체제였고, 이후 Windows 2000, XP, 7, 10, 11로 이어진 NT 계열이 오늘날 데스크톱 운영체제의 주류가 되었습니다. macOS는 2001년의 Mac OS X부터 Darwin을 바탕으로 하며 그 커널은 XNU인데, XNU라는 커널과 완성된 제품인 macOS가 받은 UNIX 인증은 서로 구별해야 합니다.
Linux는 1991년 핀란드의 대학생 리누스 토르발스(Linus Torvalds)가 시작한 오픈 소스 프로젝트로, UNIX와 호환되면서도 무료로 쓸 수 있는 운영체제를 목표로 했습니다. 처음에는 취미로 시작했지만 전 세계의 개발자가 함께 참여하며 빠르게 자랐고, 지금은 서버와 고성능 계산 환경을 대표하는 커널이 되었습니다. 2023년 기준으로 세계 상위 500대 슈퍼컴퓨터가 모두 Linux를 쓰며, 대규모 서버 인프라와 Android 스마트폰도 Linux 커널 위에 만들어졌습니다. 다만 슈퍼컴퓨터 목록에서의 비율과 전체 서버나 모바일 기기에서의 비율은 대상이 다르므로, 앞의 수치를 모든 기기의 점유율로 넓혀 읽지 않습니다.
Linux가 성공한 이유로는 누구나 소스 코드를 읽고 고치고 배포할 수 있는 오픈 소스 모델이 꼽힙니다. 이 개방성 덕분에 많은 조직과 개발자가 결함 수정과 기능 추가에 참여했고, Ubuntu, Debian, Fedora, CentOS Stream 같은 여러 배포판(distribution)이 용도에 맞게 다듬은 패키지를 제공합니다.
모바일과 클라우드
스마트폰은 운영체제에 새로운 과제를 던졌습니다. 2008년에 나온 Android는 Google이 주도하는 오픈 소스 모바일 운영체제로, Linux 커널 위에 Java와 Kotlin으로 앱을 만드는 프레임워크를 올렸습니다. 앱마다 샌드박스(sandbox)에 가두어 다른 앱의 비공개 데이터에 접근하지 못하게 하되, 허용된 프로세스 간 통신이나 권한, 공유 API로는 데이터를 주고받을 수 있게 합니다. 데스크톱 운영체제에도 사용자 권한과 접근 제어, 샌드박스가 있으므로, 둘의 차이는 보호가 있느냐가 아니라 무엇을 단위로 격리하고 권한을 어떻게 주느냐에 있습니다. iOS는 Apple이 macOS와 같은 Darwin을 바탕으로 만든 모바일 운영체제로, 하드웨어와 소프트웨어를 함께 맞추어 최적화합니다.
클라우드가 퍼지면서 운영체제를 쓰는 방식도 바뀌고 있습니다. 가상화(virtualization)는 물리 서버 한 대에 여러 가상 머신(virtual machine, VM)을 올려 각자 독립된 운영체제를 실행하게 하는 기술로, VMware, KVM, Hyper-V가 대표적입니다. 덕분에 서버를 사서 설치하는 대신 클라우드 화면에서 몇 번의 클릭으로 서버를 만들 수 있게 되었습니다.
컨테이너(container)는 가상 머신보다 가볍게 격리하는 방식입니다. 일반적인 컨테이너는 호스트의 커널을 함께 쓰면서 프로세스와 자원만 나누어 격리하므로, 게스트 커널을 따로 띄우지 않아 빠르게 시작하고 자원도 덜 쓰지만, 가상 머신만큼 강하게 격리된다고 볼 수는 없습니다. 2013년의 Docker가 컨테이너를 널리 퍼뜨렸고, Kubernetes가 많은 컨테이너를 관리하는 역할을 맡게 되었습니다. 서버리스(serverless)는 서버를 관리하는 일 자체를 감추어, 개발자는 함수 단위의 코드만 쓰고 기반 시설은 클라우드 제공자가 관리하는데, 안에서는 여전히 컨테이너나 작은 가상 머신이 그 함수를 실행합니다.
추상화의 단계가 높아질수록 배포와 운영은 쉬워지지만, 개발자가 직접 다룰 수 있는 운영체제의 영역은 줄어듭니다. 가상 머신에서는 게스트 운영체제를 직접 관리하며 문제를 하이퍼바이저, 게스트, 가상 장치로 나누어 살피고, 컨테이너에서는 공유 커널 위의 네임스페이스, cgroup, 마운트, 네트워크 설정을 확인하며, 서버리스에서는 런타임의 제한, 첫 호출이 느린 콜드 스타트, 제공자가 공개한 실행 조건을 먼저 확인합니다. 이 기술들은 모두 프로세스 격리, 메모리 관리, 네임스페이스, cgroup 같은 운영체제의 개념 위에 세워졌으므로, 문제가 생기면 결국 그 아래를 들여다보아야 합니다.
역사에서 읽는 운영체제의 원리
시대마다의 문제와 그 해결책을 이어 보면 운영체제의 핵심 개념이 왜 생겨났는지 분명해집니다.
| 시대와 문제 | 해결책과 이어진 개념 |
|---|---|
| 초기: 작업을 바꾸는 동안 컴퓨터가 놂 | 일괄 처리, 작업 대기열과 자동화 |
| 일괄 처리: 입출력을 기다리는 동안 CPU가 놂 | 다중 프로그래밍, CPU 스케줄링과 메모리 보호 |
| 시분할: 사용자의 응답이 늦음 | 타임 퀀텀, 라운드 로빈과 대화형 운영체제 |
| PC: 쓰기 편해야 함 | GUI와 플러그 앤 플레이, 윈도 시스템과 드라이버 |
| 모바일: 보안과 배터리 | 앱 격리와 전력 관리, 샌드박스와 권한 모델 |
| 클라우드: 자원과 배포의 비용 | 가상화와 컨테이너, VM과 Docker와 Kubernetes |
이처럼 운영체제의 개념은 모두 문제를 풀려는 과정에서 생겨났습니다. 앞으로 개념을 하나씩 배울 때마다 "이것은 어떤 문제를 풀려고 만들어졌는가?"를 함께 물으면 훨씬 깊이 이해할 수 있습니다.
정리
- 초기의 컴퓨터는 사람이 작업을 바꾸는 동안 놀았고, 이 낭비를 줄이려는 일괄 처리 시스템의 상주 모니터가 최초의 운영체제가 되었습니다.
- 다중 프로그래밍은 입출력을 기다리는 동안 다른 작업을 실행해 CPU 이용률을 높였고, 그 과정에서 메모리 보호, 스케줄링, 동기화의 문제가 생겼습니다.
- 시분할은 CPU 시간을 타임 퀀텀으로 나누어 사용자마다의 응답 시간을 줄였으며, UNIX가 이 시대에 태어나 C로 다시 쓰이며 이식성을 얻었습니다.
- PC 시대에는 편리함과 GUI가 중요해졌고, Windows NT, macOS, Linux가 오늘날의 주요 운영체제로 자랐습니다.
- 모바일은 앱 격리와 전력 관리를, 클라우드는 가상화와 컨테이너를 가져왔지만, 이들 역시 운영체제의 개념 위에 세워져 있습니다.
연습 문제
1. B가 짧아지면
본문의 예에서 B의 CPU 시간만 4ms에서 1ms로 줄이고, A가 기다리는 동안 B를 실행합니다. 두 작업이 모두 끝나는 시각과 CPU가 노는 구간, CPU 이용률을 구하세요. A의 입출력이 끝나면 A가 곧바로 실행된다고 할 수 있는지도 설명하세요.
해설 보기
A는 0–2ms, B는 2–3ms에 CPU를 씁니다. 3–6ms에는 A가 입출력을 기다리고 B는 이미 끝났으므로, 준비된 작업이 없어 CPU가 놉니다. A는 6–8ms에 마지막 CPU 구간을 쓰므로 모든 작업이 8ms에 끝나고, CPU 사용 시간은 5ms, 경과 시간은 8ms로 이용률은 62.5%입니다. 일반적으로 입출력이 끝나면 A는 실행될 수 있는 후보가 될 뿐이며, 여기서는 6ms에 다른 준비된 작업이 없고 작업을 바꾸는 시간도 0이라는 조건 때문에 A를 곧바로 실행할 수 있었던 것입니다.
2. 다중 프로그래밍과 시분할의 목표
다중 프로그래밍과 시분할은 모두 여러 프로그램을 번갈아 실행합니다. 두 방식이 무엇을 목표로 하는지 비교하고, 그 차이가 스케줄링에 어떤 영향을 주는지 설명하세요.
해설 보기
다중 프로그래밍은 입출력을 기다리는 동안 다른 작업을 실행해 CPU가 노는 시간을 줄이고 전체 처리량을 높이는 것이 목표입니다. 그래서 한 작업이 입출력을 요청할 때까지 계속 실행해도 목표에 어긋나지 않습니다. 시분할은 여기에 사용자마다의 응답 시간을 줄이는 목표를 더했으므로, 한 작업이 CPU를 오래 차지하지 못하도록 타임 퀀텀이 끝나면 강제로 다른 작업으로 바꾸어야 합니다. 이 차이가 라운드 로빈처럼 시간을 나누어 주는 스케줄링 방식으로 이어졌습니다.
3. 컨테이너는 가상 머신을 대신할까
"컨테이너가 가상 머신보다 가볍고 빠르니, 서로 믿을 수 없는 고객들의 프로그램도 컨테이너 하나씩에 넣어 같은 서버에서 돌리면 된다"는 의견을 평가하세요.
해설 보기
일반적인 컨테이너는 호스트의 커널을 함께 쓰므로, 커널의 취약점을 이용하면 한 컨테이너에서 다른 컨테이너나 호스트에 영향을 줄 수 있습니다. 그래서 게스트 커널을 따로 두는 가상 머신만큼 강하게 격리된다고 보기 어렵습니다. 서로 믿을 수 없는 고객의 프로그램을 함께 돌려야 한다면 가상 머신으로 나누거나, 가벼운 가상 머신으로 컨테이너를 감싸는 방식을 검토해야 합니다. 가볍고 빠르다는 장점과 격리의 강도는 따로 따져야 하며, 컨테이너의 격리는 13장에서 자세히 다룹니다.
다음 글 커널과 시스템 콜에서는 운영체제의 핵심인 커널이 무엇이고, 프로그램이 운영체제에 일을 요청하는 시스템 콜이 어떻게 동작하는지 살펴봅니다.