Chapter 08

운영체제: 컴퓨터의 관리자

컴퓨터에서 음악을 들으며 웹 서핑을 하고, 메신저 알림을 받으면서 문서를 쓴다. CPU 코어는 몇 개뿐인데 어떻게 수십 개의 프로그램이 동시에 돌아갈까? 서로 메모리를 침범하지 않고, 같은 SSD를 함께 쓰면서도 파일이 뒤섞이지 않는 이유는 뭘까? 그 뒤에는 운영체제라는 부지런한 주방장이 있다.

운영체제가 없다면

운영체제가 없는 세상을 상상해 보자. 사진 앱을 만드는 회사는 SSD 제조사마다 다른 명령어로 블록을 읽는 코드를, 화면 칩마다 다른 방식으로 픽셀을 찍는 코드를 직접 짜야 한다. 사진 앱과 음악 앱이 동시에 실행되면 둘 다 CPU와 메모리를 독차지하려고 싸운다. 악성 프로그램은 다른 앱의 메모리를 마음대로 읽을 수 있다.

운영체제(OS, Operating System)는 하드웨어와 응용 프로그램 사이에 앉아 이 문제들을 해결한다. 윈도우, macOS, 리눅스, 안드로이드, iOS가 모두 운영체제다.

역할하는 일주방장이라면
자원 관리CPU 시간, 메모리, 저장장치, 네트워크를 여러 프로그램에 나눠 준다.누가 언제 화구를 쓰고 조리대 어디를 쓸지 정한다.
추상화“블록 5번, 6번” 대신 “파일”, “CPU 코어” 대신 “프로세스”처럼 쉬운 개념을 제공한다.요리사에게 “냉장고 3번 칸 왼쪽 아래” 대신 “양파 가져와”라고 말할 수 있게 한다.
보호프로그램끼리 서로의 메모리와 파일을 침범하지 못하게 막는다.다른 요리사의 조리대에 손대지 못하게 한다.

커널과 시스템 콜: 창구를 통해서만

운영체제의 핵심 부분을 커널(kernel, 알맹이)이라 한다. CPU에는 두 가지 실행 모드가 있다. 커널은 모든 하드웨어를 직접 만질 수 있는 커널 모드에서, 일반 앱은 권한이 제한된 사용자 모드에서 실행된다. 사용자 모드에서 하드웨어를 직접 건드리는 명령을 실행하면 CPU가 거부하고 커널에 알린다.

그래서 앱이 파일을 저장하거나 네트워크로 데이터를 보내려면 커널에게 정식으로 부탁해야 한다. 이 부탁을 시스템 콜(system call)이라 한다. 은행 금고에 손님이 직접 들어갈 수 없고 창구 직원에게 요청해야 하는 것과 같다.

SIMULATOR

시스템 콜 따라가기: “파일 저장” 버튼을 누르면

메모장 앱사용자 모드
라이브러리 (표준 함수)사용자 모드
시스템 콜 창구여기서 모드가 바뀐다
커널: 파일 시스템커널 모드
커널: SSD 드라이버커널 모드
하드웨어: SSD실제 기록
“다음”을 눌러 저장 요청이 내려가는 길을 따라가 보세요.
드라이버

SSD, 프린터, 그래픽 카드는 제조사마다 조작 방법이 다르다. 각 장치를 다루는 방법을 담은 커널의 부품을 드라이버(device driver)라 한다. 새 장치를 꽂았을 때 “드라이버를 설치하세요”라는 말이 나오는 이유다. 드라이버 덕분에 앱은 장치의 종류를 몰라도 된다.

부팅: 운영체제가 깨어나기까지

전원 버튼을 누르면 메모리는 텅 비어 있다. 운영체제도 SSD에 파일로 잠들어 있을 뿐이다. 그렇다면 누가 운영체제를 메모리로 불러올까? 자기 자신을 끌어올린다는 뜻의 부팅(booting, bootstrap)은 다음 순서로 진행된다.

  1. 전원 공급: 전원부가 안정되면 CPU가 깨어나 미리 정해진 주소의 명령부터 실행한다.
  2. 펌웨어(BIOS/UEFI): 메인보드의 작은 플래시 칩에 들어 있는 프로그램이 메모리, 키보드, 저장장치를 점검한다.
  3. 부트로더: 펌웨어가 저장장치에서 부트로더라는 작은 프로그램을 찾아 실행한다.
  4. 커널 적재: 부트로더가 운영체제 커널 파일을 메모리에 올리고 실행을 넘긴다.
  5. 초기화: 커널이 드라이버를 불러오고, 메모리 관리를 시작하고, 첫 번째 프로세스를 띄운다.
  6. 로그인 화면: 서비스와 화면 관리 프로그램이 차례로 실행되고, 마침내 로그인 화면이 나타난다.

프로세스와 스레드

SSD에 저장된 앱은 그저 명령어와 데이터가 담긴 파일, 즉 프로그램이다. 레시피 책에 비유할 수 있다. 이 프로그램을 실행하면 운영체제가 메모리에 올리고 실행 상태를 관리하는데, 이렇게 실행 중인 프로그램을 프로세스(process)라 한다. 레시피를 보고 실제로 요리가 진행 중인 상태다. 같은 브라우저를 두 번 실행하면 프로그램은 하나지만 프로세스는 두 개다.

운영체제는 프로세스마다 다음 정보를 기록해 둔다: 지금 어디까지 실행했는지(PC 값), 레지스터 값들, 사용 중인 메모리 영역, 열어 둔 파일, 상태(실행 중/대기 중/준비됨). 이 기록 덕분에 프로세스를 잠시 멈췄다가 정확히 그 자리에서 다시 이어 갈 수 있다.

한 프로세스 안에서도 여러 일을 동시에 하고 싶을 때가 있다. 웹 브라우저는 페이지를 그리면서 동시에 파일을 내려받고 동영상을 재생한다. 한 프로세스 안에서 메모리를 공유하며 따로 실행되는 흐름을 스레드(thread, 실)라 한다. 한 주방(프로세스) 안의 여러 요리사(스레드)가 같은 조리대(메모리)를 함께 쓰는 셈이다. 함께 쓰니 빠르고 편하지만, 나중에 보겠지만 부딪칠 위험도 있다.

CPU 스케줄링: 누구에게 먼저 CPU를 줄까

준비된 프로세스는 수십 개인데 CPU 코어는 몇 개뿐이다. 운영체제는 아주 짧은 시간(수 밀리초)씩 번갈아 CPU를 나눠 줘서 모두가 동시에 실행되는 것처럼 보이게 한다. 누구에게 언제, 얼마나 줄지 정하는 규칙을 스케줄링(scheduling)이라 한다.

은행 창구 줄

창구가 하나인 은행에 손님들이 온다. 먼저 온 순서대로 처리할까(선착순)? 금방 끝나는 손님부터 처리할까(짧은 일 먼저)? 아니면 한 사람당 2분씩만 처리하고 아직 안 끝났으면 줄 맨 뒤로 보낼까(돌아가며)? 어떤 방식이냐에 따라 손님들의 평균 대기 시간과 불만이 크게 달라진다.

SIMULATOR

CPU 스케줄러 비교

스케줄링 방식
도착한 작업들
평균 대기 시간—
평균 첫 응답 시간—
작업 전환 횟수—
가로축은 시간(ms)이다. 해볼 것: “긴 작업이 먼저 옴”에서 선착순은 짧은 작업들이 긴 작업 뒤에서 오래 기다린다(호위 효과). 짧은 일 먼저는 평균 대기가 가장 짧지만, 작업 길이를 미리 알아야 하고 긴 작업이 계속 밀릴 수 있다. 라운드 로빈은 모두가 금방 첫 응답을 받아 반응이 빠르게 느껴지지만 전환이 잦다.

실제 운영체제는 이들을 섞은 정교한 방식을 쓴다. 키보드 입력을 기다리는 대화형 프로그램은 우선순위를 올려 반응을 빠르게 하고, 오래 계산만 하는 프로그램은 뒤로 돌린다. 작업을 바꿀 때마다 레지스터와 PC를 저장하고 다른 프로세스의 것을 불러오는 문맥 교환(context switch) 비용이 들기 때문에 너무 자주 바꾸는 것도 손해다.

인터럽트: “잠깐만요!”

CPU가 계산에 몰두하는 동안 키보드가 눌렸다. CPU는 이를 어떻게 알까? 계산하다 말고 1초에 수천 번 “키보드 눌렸어?” 하고 물어보는 방식(폴링)은 낭비가 크다. 대신 키보드가 CPU에게 직접 신호를 보낸다. 이것이 인터럽트(interrupt, 끼어들기)다.

인터럽트를 받은 CPU는 하던 일의 위치(PC와 레지스터)를 저장하고, 운영체제에 미리 등록된 처리 코드(인터럽트 처리기)를 실행한 뒤, 원래 하던 일로 정확히 돌아간다. 요리하다가 초인종이 울리면 불을 줄이고 택배를 받은 뒤 다시 요리를 이어 가는 것과 같다.

SIMULATOR

인터럽트 타임라인

사건 일으키기
회색·파랑 띠는 사용자 프로그램 A, B가 CPU를 쓰는 시간, 주황 띠는 커널의 인터럽트 처리 시간이다. 타이머 인터럽트는 운영체제가 주기적으로 CPU를 되찾아 다음 프로그램으로 바꾸는 장치다. 이것이 없으면 무한 반복에 빠진 프로그램 하나가 컴퓨터를 독차지할 수 있다.

경쟁 상태: 동시에 고치면 생기는 일

두 스레드가 같은 메모리에 있는 숫자 count를 각각 1씩 늘린다고 하자. 결과는 당연히 2가 늘어야 한다. 그런데 “1 늘리기”는 CPU에게 한 번의 동작이 아니라 세 단계다: ① 메모리에서 레지스터로 읽기, ② 레지스터에서 더하기, ③ 메모리에 쓰기(4장의 LOAD, ADD, STORE). 스케줄러가 그 사이 아무 때나 스레드를 바꿀 수 있다면?

SIMULATOR

두 스레드, 하나의 계좌

공유 메모리100count
누구를 한 단계 실행할까? (스케줄러 역할)
두 스레드가 각각 count를 1씩 늘린다. 정답은 102다. 해볼 것: 스레드 1 → 스레드 2 → 스레드 1 → … 처럼 번갈아 실행해 보자.
무작위 스케줄로 1,000번 실험

번갈아 실행하면 두 스레드가 모두 100을 읽고, 각자 101을 계산해 쓴다. 1이 사라졌다! 이처럼 실행 순서에 따라 결과가 달라지는 버그를 경쟁 상태(race condition)라 한다. 은행 앱에서 이런 일이 생기면 돈이 사라진다. 더 무서운 점은 이 버그가 대부분의 실행에서는 드러나지 않다가 아주 가끔만 터진다는 것이다.

해결책은 잠금(lock, mutex)이다. 공유 데이터를 고치는 구간에 들어가기 전에 잠금을 걸고, 끝나면 푼다. 다른 스레드는 잠금이 풀릴 때까지 기다린다. 화장실 문을 잠그는 것과 같다. 위에서 잠금을 켜고 다시 해 보자. 단, 잠금을 너무 많이 쓰면 스레드들이 서로 기다리느라 느려지고, 서로 상대의 잠금을 기다리며 영원히 멈추는 교착 상태(deadlock)가 생길 수도 있다.

핵심 정리

  1. 운영체제는 자원 관리, 추상화, 보호를 맡는 하드웨어와 앱 사이의 관리자다.
  2. 커널은 커널 모드에서, 앱은 사용자 모드에서 돈다. 앱은 시스템 콜로 커널에 부탁해야 하드웨어를 쓸 수 있다.
  3. 프로세스는 실행 중인 프로그램, 스레드는 프로세스 안에서 메모리를 공유하는 실행 흐름이다.
  4. 스케줄러는 CPU를 짧게 나눠 여러 프로세스에 준다. 방식에 따라 대기 시간과 반응성이 달라진다.
  5. 인터럽트는 장치가 CPU에 사건을 알리는 신호다. 타이머 인터럽트로 OS가 CPU를 되찾는다.
  6. 여러 스레드가 공유 데이터를 동시에 고치면 경쟁 상태가 생긴다. 잠금으로 막는다.

확인 퀴즈

앱이 SSD에 파일을 쓰고 싶을 때 하는 일은?

사용자 모드의 앱은 하드웨어를 직접 만질 수 없다. 시스템 콜이라는 정해진 창구로만 커널에 부탁한다.

“프로그램”과 “프로세스”의 차이로 옳은 것은?

레시피 책(프로그램)과 지금 진행 중인 요리(프로세스)의 관계다. 한 프로그램을 여러 번 실행하면 여러 프로세스가 된다.

라운드 로빈 스케줄링의 가장 큰 장점은?

시간 조각마다 돌아가며 CPU를 주므로 누구도 오래 굶지 않는다. 대신 전환 비용이 늘어난다.

타이머 인터럽트가 없다면 생길 수 있는 문제는?

타이머 인터럽트는 OS가 주기적으로 CPU를 되찾을 수 있게 보장한다. 협조하지 않는 프로그램도 강제로 쉬게 할 수 있다.

두 스레드가 잠금 없이 같은 count를 1씩 늘렸더니 2가 아니라 1만 늘었다. 원인은?

경쟁 상태다. 두 스레드가 모두 옛 값을 읽고 각자 +1을 써서 하나의 갱신이 덮어써졌다.