[운영체제] 02. 프로세스 개념과 상태 전이
오늘은 운영체제에서 자주 듣는 단어인 프로세스를 정리했다. 처음엔 "프로그램이랑 프로세스가 뭐가 다르지? 그냥 실행 중인 프로그램 아닌가?" 정도로만 알고 있었는데, 운영체제가 한정된 CPU(Central Processing Unit, 중앙처리장치)를 누구한테 줄지 결정하는 그 "누구"가 바로 프로세스다. 이 개념을 대충 알고 넘어가면 뒤에 나오는 스케줄링이나 동기화가 전부 흐릿해진다.
이번 글에서는 내가 짠 코드 한 덩어리가 어떻게 실행 가능한 형태로 메모리에 올라가는지, 일단 올라간 뒤에는 어떤 상태들을 오가며 CPU를 받았다 뺏겼다 하는지를 따라가며 정리했다.
1. 시스템 소프트웨어 (번역·연결·적재)
내가 작성한 소스 코드는 그 자체로는 CPU한테 한 글자도 의미가 없다. 이 부분이 처음엔 잘 와닿지 않았는데, CPU가 직접 이해하는 건 0과 1로 된 기계어뿐이라서 그렇다. 내가 쓴 C 코드가 실행되기까지는 번역·연결·적재라는 단계가 차례로 끼어든다.
먼저 언어의 층위부터 정리했다.
| 언어 | 설명 |
|---|---|
| 기계어 | 0·1의 2진수. CPU가 직접 이해하는 유일한 언어. CPU 기종마다 명령어 체계가 달라 호환성이 없다 |
| 어셈블리어 | 기계어 명령과 1:1로 대응되는 기호(니모닉). 어셈블러로 번역 |
| 고급 언어 | C, Java 등 사람의 문법에 가까운 언어. 컴파일러·인터프리터로 번역 |
이 코드가 실행 파일이 되어 메모리에서 돌기까지의 흐름은 이렇다.
원시 모듈 → (어셈블러/컴파일러) → 목적 모듈 → (링커) → 적재 모듈 → (로더) → 주기억장치에서 실행
여기서 처음에 링커랑 로더를 헷갈렸는데, 둘의 역할을 정확히 갈라두니까 정리가 됐다.
- 링커(Linker): 따로따로 컴파일된 여러 목적 모듈과
printf같은 시스템 라이브러리들을 하나로 연결해 실행 가능한 적재 모듈을 만든다. 그래서 '연결 편집기'라고도 부른다. - 로더(Loader): 완성된 실행 파일을 주기억장치의 알맞은 위치에 적재하고 주소를 맞춰 실행을 준비한다.
로더는 필요에 따라 여러 방식으로 발전했다. 번역 직후 곧장 실행하는 Compile-and-go, 주소가 고정된 절대 로더, 연결과 적재를 함께 하는 링킹 로더, 그리고 실행 도중 실제로 필요한 모듈만 그때그때 메모리에 올리는 동적 로더(Dynamic Loader)가 있다. 핵심 차이는 주소를 확정하는 시점(바인딩, binding)이 언제냐다. 일찍 묶을수록 단순하지만 메모리 배치가 경직되고, 늦게 묶을수록 유연하지만 실행 시 처리가 늘어난다.
처음엔 이런 로더 종류를 왜 나눠서 외우나 싶었는데, 공유 라이브러리(.dll, .so)가 이 동적 적재에서 이어진 방식이라는 걸 알고 나니 납득이 됐다. 여러 프로그램이 같은 라이브러리 코드 하나를 메모리에서 공유할 수 있는 이유가 여기 있다.
2. 명령어 실행 과정
프로세스가 "실행된다"는 건 결국 CPU가 메모리에서 명령어를 하나씩 꺼내 해독하고 수행하는 사이클을 반복한다는 뜻이다. 이 한 바퀴를 명령어 사이클(instruction cycle)이라고 부른다.
① 명령어 인출(Fetch) → ② 해독(Decode) → ③ 데이터 인출 → ④ 실행(Execute)
- 명령어 인출(Fetch): PC가 가리키는 주소에서 다음 명령어를 CPU로 가져온다.
- 해독(Decode): 제어장치가 그 명령어가 무슨 연산인지 해석한다.
- 데이터 인출: 연산에 필요한 피연산자가 메모리에 있으면 레지스터로 가져온다.
- 실행(Execute): ALU(Arithmetic Logic Unit, 산술논리연산장치)가 실제 연산을 수행하고 결과를 저장한다.
이 사이클을 떠받치는 게 CPU 내부의 레지스터들이다. 이름만 외우려니 잘 안 외워졌는데, 각자 무슨 일을 하는지와 같이 보니 정리가 됐다.
- PC (Program Counter): 다음에 실행할 명령어의 주소. 한 사이클이 끝날 때마다 자동으로 증가하며, 이 값이 곧 "프로세스가 지금 코드의 어디까지 왔는가"를 가리킨다.
- IR (Instruction Register): 현재 인출해 실행 중인 명령어 자체를 담는다.
- MAR (Memory Address Register) / MBR (Memory Buffer Register): 메모리에 접근할 때 쓰는 주소(MAR)와 주고받는 데이터(MBR)를 임시로 담는다.
- ACC (Accumulator, 누산기): 연산의 중간 결과를 일시 보관한다.
여기서 PC와 레지스터 값들이 곧 "프로세스의 실행 흐름 그 자체"라는 점이 중요하다. CPU를 다른 프로세스에 넘겼다가 나중에 그 자리로 돌아오려면, 이 레지스터 값들을 어딘가에 통째로 저장해두면 된다. 이 발상이 뒤에 나오는 문맥 교환의 핵심이다.
3. 프로세스 개념
프로그램과 프로세스, 평소엔 그냥 섞어 썼는데 운영체제 관점에서는 분명히 구분해야 한다.
| 용어 | 설명 |
|---|---|
| 작업 (Job) | 아직 실행을 의뢰하기 전, 디스크에 파일로 존재하는 실행 프로그램 + 입력 데이터의 묶음 |
| 프로세스 (Process) | 메모리에 적재되어 실행 중인 프로그램. 커널에 등록되어 자원을 할당받는 활동의 주체 |
이 둘의 차이를 선명하게 보여주는 게 "같은 프로그램을 여러 번 실행할 수 있다"는 사실이다. 크롬 실행 파일(프로그램)은 디스크에 하나지만, 창을 여러 개 띄우면 각각 별도의 프로세스가 된다. 작업 관리자로 확인해보니 크롬 프로세스가 여러 개 떠 있어서 감이 잡혔다.
같은 코드에서 출발했어도 각 프로세스는 자기만의 메모리 공간과 실행 상태를 따로 갖는다. 프로세스가 메모리에 올라갈 때 이 공간은 보통 네 영역으로 나뉜다.
- 코드(Code): 실행할 명령어. 읽기 전용이라 같은 프로그램의 여러 프로세스가 공유하기도 한다.
- 데이터(Data): 전역 변수 등 프로그램 시작과 함께 정해지는 값들.
- 스택(Stack): 함수 호출 정보·지역 변수. 호출할 때 쌓이고 반환할 때 줄어든다.
- 힙(Heap): 실행 중 동적으로 할당하는 영역(
malloc,new).
스택과 힙이 보통 메모리의 양 끝에서 서로를 향해 자라도록 배치된다는 점도 짚어둘 만하다. 한정된 공간을 양쪽에서 나눠 쓰기 위한 설계인데, 둘이 중간에서 충돌하면 스택 오버플로가 발생한다. 그동안 에러 이름으로만 알던 게 이런 구조에서 나온 말이었다.
PCB (Process Control Block, 프로세스 제어 블록)
운영체제가 각 프로세스를 식별하고 관리하기 위한 핵심 정보를 모아둔 자료구조다. 커널 공간에 존재하며, 프로세스가 생성될 때 만들어지고 종료될 때 회수된다.
PCB에 담기는 정보는 대략 이렇다.
- 프로세스의 현재 상태(준비/실행/대기 등)
- 고유 식별자(PID, Process ID)와 스케줄링 우선순위
- 문맥 교환 시 복구에 쓰는 각종 레지스터 상태값(PC, 누산기 등)
- 할당된 메모리 영역과 열린 입출력·파일 정보
PCB가 왜 필요한지는 문맥 교환과 연결해서 보니 이해됐다. CPU를 A에서 B로 넘길 때, 운영체제는 A의 레지스터 값들을 A의 PCB에 저장해두고 B의 PCB에서 값을 꺼내 복원한다.
결국 PCB는 "지금 멈춰둔 프로세스를 나중에 그 자리에서 다시 살려내기 위한 스냅샷"인 셈이다. 그래서 할당되지 않은 주변장치 상태처럼 그 프로세스 복원과 무관한 정보는 PCB에 담기지 않는다.

4. 프로세스 상태와 전이
프로세스는 생성되어 소멸하기까지 여러 상태를 오간다. 처음엔 상태가 많아 복잡해 보였는데, 핵심은 가운데의 세 활동 상태다.
제출 → 접수 → 준비(Ready) → 실행(Run) → 대기(Wait/Block) → 종료
이 중 준비·실행·대기를 활동 상태라 하며, 모두 주기억장치를 할당받은 상태다.
- 준비 (Ready): 메모리에 올라와 CPU 할당만 기다리는 상태. 당장 실행될 수 있지만 차례가 아직 안 온 것.
- 실행 (Run): CPU를 점유해 실제로 명령어를 수행 중인 상태. 단일 CPU에선 어느 순간이든 단 하나의 프로세스만 이 상태에 있다.
- 대기 (Wait/Block): 입출력 완료 같은 이벤트를 기다리며 CPU를 내려놓은 상태.
상태 전이
| 전이 | 이름 | 발생 시점 |
|---|---|---|
| 접수 → 준비 | Job 스케줄러 | 실행할 작업으로 선정됨 |
| 준비 → 실행 | Dispatch | CPU 스케줄러가 선택 |
| 실행 → 준비 | Timer runout | 할당 시간 만료 |
| 실행 → 대기 | Block | 입출력 등 이벤트 요청 |
| 대기 → 준비 | Wake up | 입출력 완료 |

이 다이어그램에서 눈에 들어온 지점은 "대기에서 곧장 실행으로 가는 화살표가 없다"는 사실이다. 처음엔 입출력 끝났으면 바로 다시 실행하면 되는 거 아닌가 싶었는데, 입출력이 끝난 프로세스(Wake up)는 실행 상태로 직행하지 못하고 반드시 준비 상태를 거친다.
이유는 단순하다. 그 사이 CPU는 이미 다른 프로세스가 쓰고 있을 가능성이 높고, 누가 다음 CPU를 차지할지는 스케줄러가 다시 판단해야 한다. 입출력이 끝났다고 새치기로 CPU를 돌려주는 게 아니라, 동등하게 줄 뒤에 다시 세우는 것이다.
전이의 주체도 구분해두니 흐름이 또렷해진다.
- Block(실행→대기)은 프로세스가 스스로 일으킨다. "디스크 읽기를 기다려야 하니 CPU를 붙잡고 있어봐야 시간 낭비다" 하고 자발적으로 내려놓는 것이다.
- Timer runout(실행→준비)은 운영체제가 강제로 일으킨다. 한 프로세스가 CPU를 독점하지 못하도록, 하드웨어 인터럽트 클록이 일정 시간마다 신호를 보내 제어권을 회수한다. 이 강제 회수 장치가 없으면 무한 루프 하나가 시스템 전체를 멈출 수 있다.
디스패치(Dispatch)는 준비 큐 맨 앞의 프로세스를 골라 CPU에 배당하는 것, 디스패처(Dispatcher)는 그 디스패치를 실제로 수행하는 커널 코드다. 디스패처가 매번 호출되므로 이 코드가 빠를수록 시스템 전체 오버헤드가 줄어든다.
오늘 느낀 점
- 프로그램과 프로세스의 차이는 "디스크에 있는 정적인 파일"과 "메모리에서 자원을 받아 도는 활동 주체"로 잡으니 뒤에 나올 개념들의 출발점이 정리됐다.
- PCB와 문맥 교환을 따로 외우려다 잘 안 됐는데, "레지스터 값 = 실행 흐름 그 자체, 그걸 스냅샷으로 저장하는 게 PCB"로 엮으니 한 줄기로 정리됐다. 개념은 따로 외우기보다 인과로 엮는 편이 오래 남는다.
- 상태 전이도 화살표를 통째로 외우기보다, 누가(프로세스 자신 vs 운영체제) 왜 일으키는지를 따지니 자연스럽게 그려졌다.
한 걸음 더
- 이 글의 5상태 모델에는 준비-보류(suspended ready)·대기-보류(suspended blocked)라는 상태가 더 붙기도 한다. 메모리가 부족할 때 활동 중인 프로세스 일부를 디스크로 통째로 내쫓는(swap out) 경우인데, 가상 메모리·스와핑과 직결된다. 상태 다이어그램이 교재마다 다른 건 이 보류 상태를 포함하느냐의 차이인 경우가 많다.
- 현대 운영체제는 CPU 할당의 실제 단위가 프로세스가 아니라 스레드(thread)다. 한 프로세스 안에 여러 실행 흐름(스레드)이 코드·데이터·힙은 공유하되 스택과 레지스터만 따로 갖는 구조인데, 이 글의 상태 전이 논리는 스레드 단위로도 거의 그대로 성립한다.
- "프로세스는 비동기적 행위의 주체"라는 표현을 가끔 만난다. 여기서 비동기적이라는 건 프로세스들이 서로의 진행 속도와 무관하게 제각각 인터럽트·이벤트에 따라 상태가 바뀐다는 뜻이다. 코드를 위에서 아래로 순차적으로 읽는 감각과 실제 실행이 진행되는 양상이 다를 수 있다는 점을 일러주는 말이다.