LODY/정리

운영체제 / 프로세스와 CPU 가상화

프로세스와 CPU 가상화

🟣 CPU는 하나인데 프로그램은 여럿이다

노트북에서 브라우저, 에디터, 음악 재생기가 동시에 도는 것처럼 보인다. 그런데 코어가 하나라면 그 순간 실제로 CPU를 쓰는 프로그램은 딱 하나다. 나머지는 잠깐 멈춰 있다. 운영체제는 이 하나뿐인 CPU를 여러 프로그램이 아주 빠르게 번갈아 쓰게 만들어서, 모두가 자기만의 CPU를 가진 것처럼 보이게 한다. 이 착시가 CPU 가상화다.

이걸 가능하게 하는 기본 기법은 의외로 단순하다. 한 프로그램을 잠깐 돌리고, 멈추고, 다른 프로그램을 잠깐 돌리고, 다시 멈춘다. 이 시분할(time sharing)을 충분히 빠르게 반복하면 사람 눈에는 동시에 도는 것처럼 보인다. 문제는 "잠깐 돌리고 멈춘다"를 운영체제가 어떻게 강제하느냐다. 프로그램이 스스로 양보해 주기를 기다릴 수는 없다. 그 강제의 메커니즘을 이해하는 것이 이 장의 목표다.

먼저 무엇을 번갈아 실행하는지, 그 단위부터 정의해야 한다.


🟣 프로세스: 실행 중인 프로그램이라는 추상

프로그램은 디스크에 놓인 죽은 코드 덩어리다. 이 코드를 메모리에 올려 실제로 돌리기 시작하면 프로세스가 된다. 프로세스는 "실행 중인 프로그램"에 운영체제가 붙인 추상이고, 어느 순간 이 프로세스를 멈췄다가 나중에 이어서 실행할 수 있으려면 그 순간의 상태를 전부 붙잡을 수 있어야 한다.

프로세스의 상태는 크게 세 가지다. 코드와 데이터가 놓인 주소 공간, 지금 어느 명령을 실행 중이고 계산 값이 무엇인지 담은 레지스터, 그리고 열어 둔 파일 같은 입출력 정보다. 이 중에서도 프로그램 카운터(PC, 다음에 실행할 명령의 주소)와 스택 포인터(SP)가 핵심이다. 이 값들만 정확히 저장했다가 되돌리면, 멈춘 프로세스를 아무 일 없었다는 듯 이어서 돌릴 수 있다.

프로세스는 태어나서 죽을 때까지 몇 가지 상태를 오간다. CPU를 쓰고 있는 실행(running), 언제든 실행될 수 있지만 순서를 기다리는 준비(ready), 디스크 읽기 같은 사건을 기다리느라 당장은 실행될 수 없는 대기(blocked) 상태다.

준비 (Ready)실행 (Running)대기 (Blocked)스케줄됨타이머 만료I/O 요청I/O 완료

여기서 눈여겨볼 전이는 대기 상태다. 프로세스가 디스크나 네트워크를 기다리며 CPU를 놀리는 대신, 운영체제는 그동안 준비 상태의 다른 프로세스를 실행한다. 대기와 실행을 겹치는 이 오버랩이 CPU를 한가하지 않게 유지하는 핵심이고, 다음 장 스케줄링에서 다시 다룬다. 프로세스를 만들고 멈추고 없애는 시스템콜(fork, exec, wait)의 실제 동작은 리눅스 시스템 프로그래밍 쪽에 손으로 정리했으니, 여기서는 이론 골격만 짚고 넘어간다.


🟣 제한적 직접 실행: 빠르되 통제한다

프로세스를 빠르게 돌리는 가장 단순한 방법은 CPU에 프로그램을 그냥 올려서 곧장 실행하는 것이다. 이를 직접 실행(direct execution)이라 부른다. 빠르다. 운영체제가 중간에 끼지 않으니 오버헤드가 없다. 그런데 이대로 두면 두 가지 통제를 잃는다.

첫째, 프로그램이 디스크에 마음대로 쓰거나 남의 메모리를 읽는 것을 막을 수 없다. 둘째, 한 프로그램이 CPU를 붙잡고 놓지 않으면 운영체제가 되찾아올 방법이 없다. 그래서 운영체제는 직접 실행을 쓰되 결정적인 순간에만 개입하는 제한적 직접 실행(limited direct execution)을 택한다. 평소엔 프로그램이 직접 돌고, 위험하거나 통제가 필요한 지점에서만 운영체제가 끼어든다. 이 "제한"이 어떻게 걸리는지가 나머지 두 절의 내용이다.


🟣 위험한 연산은 커널에게 맡긴다

첫 번째 통제는 프로그램이 함부로 할 수 없는 일을 나누는 것이다. CPU는 두 가지 실행 모드를 하드웨어 차원에서 구분한다. 응용 프로그램이 도는 사용자 모드(user mode)에서는 디스크 접근이나 물리 메모리 직접 조작 같은 특권 연산이 금지된다. 운영체제가 도는 커널 모드(kernel mode)에서는 무엇이든 할 수 있다.

그러면 사용자 프로그램이 파일을 읽어야 할 때는 어떻게 하나. 자기가 직접 디스크를 건드릴 수 없으니, 커널에게 대신 해 달라고 부탁한다. 이 부탁이 시스템콜이고, trap이라는 특수 명령으로 이뤄진다. trap을 실행하면 CPU가 커널 모드로 올라가면서 미리 약속된 위치의 커널 코드로 점프한다. 커널이 요청을 처리하고 return-from-trap 명령으로 사용자 모드로 되돌아온다.

여기서 중요한 설계가 하나 있다. 프로그램이 점프할 주소를 직접 지정하게 두면 커널의 아무 코드로나 뛰어들 수 있으니, 그건 통제를 잃는 것이다. 대신 커널이 부팅 시점에 트랩 테이블(trap table)을 세팅해 둔다. 이 테이블은 "이런 사건이 생기면 커널의 이 주소로 가라"를 미리 정해 둔 표다. 프로그램은 시스템콜 번호만 넘기고, 실제 점프 주소는 커널이 정한 표를 따른다. 들어갈 수 있는 문을 커널이 미리 지정해 두는 셈이다.

사용자 모드(응용 프로그램)trap 명령커널 모드(트랩 핸들러)시스템콜 요청모드 상승 + 지정된 주소로 점프return-from-trap

부탁하고 돌려받는 이 왕복이 공짜는 아니다. 모드를 오르내리고 레지스터를 저장·복원하는 비용이 든다. 그래서 시스템콜을 너무 자주 부르면 느려지고, 이 비용을 줄이려는 여러 기법(버퍼링, mmap 같은 우회)이 뒤따라 나온다.


🟣 CPU를 되찾는 법: 타이머 인터럽트

두 번째 통제가 더 까다롭다. 한 프로세스가 시스템콜도 안 부르고 무한 루프를 돌면, 커널은 영영 CPU를 되찾지 못한다. 프로그램이 스스로 양보(yield)해 주기를 기다리는 협조적 방식은 이 한 줄 앞에서 무너진다. 악의가 없어도 버그 하나로 시스템 전체가 멈춘다.

해법은 커널이 아니라 하드웨어에 기댄다. 부팅 시 커널은 타이머 인터럽트(timer interrupt)를 걸어 둔다. 정해진 간격마다(예: 몇 밀리초) CPU가 지금 무엇을 하고 있든 강제로 멈추고 커널의 인터럽트 핸들러로 점프한다. 프로세스가 협조하든 안 하든 상관없다. 주기적으로 커널이 CPU를 돌려받을 기회를 하드웨어가 보장한다. 이 비협조적 방식이 있어야 시분할이 안정적으로 굴러간다.

커널이 CPU를 되찾으면 스케줄러가 판단한다. 이 프로세스를 계속 돌릴지, 다른 프로세스로 바꿀지 결정한다. 바꾸기로 하면 이제 문맥 교환(context switch)이 일어난다.


🟣 문맥 교환: 상태를 갈아 끼운다

문맥 교환은 지금 돌던 프로세스 A의 상태를 저장하고, 다음에 돌 프로세스 B의 상태를 복원하는 작업이다. 저장하고 복원할 대상은 앞서 본 레지스터, 특히 프로그램 카운터와 스택 포인터다. 커널은 A의 레지스터를 A의 커널 스택(또는 프로세스 구조체)에 저장하고, B의 레지스터를 CPU에 복원한다. 복원이 끝나고 return-from-trap으로 돌아가면, CPU는 B가 멈췄던 바로 그 명령부터 실행을 이어 간다.

프로세스 A커널프로세스 B실행 중레지스터 사용A 상태 저장B 상태 복원이어서 실행타이머 인터럽트return-from-trap

문맥 교환에도 비용이 든다. 레지스터를 옮기는 직접 비용에 더해, 캐시와 TLB(다음 장에서 볼 주소 변환 캐시)에 쌓아 둔 내용이 새 프로세스 때문에 무효화되는 간접 비용이 크다. 그래서 교환을 너무 자주 하면 정작 일할 시간보다 갈아 끼우는 시간이 늘어난다. 얼마나 자주 바꿀지, 다음에 누구를 실행할지를 정하는 것이 스케줄러의 몫이다.


🟣 정리

  • CPU 가상화는 하나뿐인 CPU를 여러 프로세스가 빠르게 번갈아 쓰게 해서 각자 CPU를 가진 착시를 만든다. 그 바탕은 시분할이다.
  • 프로세스는 실행 중인 프로그램의 추상이다. 주소 공간, 레지스터(PC·SP), 입출력 정보만 저장·복원하면 멈춘 실행을 이어 갈 수 있다.
  • 제한적 직접 실행은 평소엔 프로그램을 직접 돌리고 위험한 순간에만 커널이 개입한다. 사용자·커널 모드 구분과 트랩 테이블이 그 통제 장치다.
  • 스스로 양보하지 않는 프로그램에서 CPU를 되찾으려면 타이머 인터럽트가 필요하다. 하드웨어가 주기적 개입을 보장한다.
  • 문맥 교환은 레지스터 상태를 갈아 끼우는 작업이고, 캐시·TLB 무효화라는 숨은 비용이 있어 지나치게 자주 하면 손해다.

면접에서 "프로세스와 스레드의 차이"나 "문맥 교환 비용"을 물으면 대개 이 장의 내용으로 답이 갈린다. 특히 문맥 교환의 간접 비용(캐시·TLB)을 짚을 수 있으면 깊이 사다리의 한 단을 더 내려간 답이 된다. 이제 커널이 CPU를 되찾은 다음, 누구를 실행할지 정하는 문제로 넘어간다. 다음 장은 스케줄링이다.