1. 메모리 영역
메모리에는 컴퓨터가 실행되는 순간부터 다양한 프로세스들이 적재되어 실행된다. 프로세스는 사용자와 상호작용하며 실행되는 포그라운드 프로세스(foreground process)와 사용자가 보지 못하는 곳에서 실행되는 백그라운드 프로세스(background process)로 나뉜다. 백그라운드 프로세스 중 주어진 작업만 수행하는 서비스(service)라는 특별한 프로세스도 존재한다.
하지만 프로세스 종류 상관없이, 프로세스를 구성하는 메모리 정보는 크게 다르지 않다. 보통 커널 영역에는 프로세스 제어 블록(PCB: Process Control Block)이 있고, 사용자 영역에는 실행 중인 프로세스가 코드/데이터/힙/스택 영역으로 나뉘어 저장된다.
참고로 코드, 데이터, 힙, 스택 영역 외에 BSS 영역도 존재한다. 프로그램을 사용하는 동안 유지할 데이터 중 정적 변수나 전역 변수와 같이 초기값이 있는 데이터는 데이터 영역에 저장되고, 초기값이 없는 데이터는 BSS 영역에 저장된다.

- 코드 영역(code segment): 실행 가능한 명령어가 저장되는 공간. CPU가 읽고 실행할 명령어가 담겨 있는 공간으로, 쓰기가 금지된 읽기 전용(read-only) 공간이다.
- 데이터 영역(data segment): 프로그램이 실행되는 동안 유지할 데이터가 저장되는 공간. 정적 변수, 전역 변수가 대표적이다.
- 힙 영역(heap segment): 개발자가 프로그램 실행 도중 자유롭게 할당하여 사용 가능한 메모리 공간. 할당한 공간을 반환하지 않으면 메모리 누수(memory leak) 문제를 초래할 수 있어 주의해야 한다. 일부 언어에서는 이러한 문제를 해결하기 위해 쓰레기 수집(Garbage Collection) 기능을 제공하기도 한다.
- 스택 영역(stack segment): 일시적으로 사용할 값들이 저장되는 공간. 함수 실행이 끝나면 사라지는 매개변수, 지역 변수, 함수 복귀 주소 등이 저장된다. 또한 스택 트레이스(stack trace: 특정 시점에 스택 영역에 저장된 함수 호출 정보)가 저장될 수도 있다.
이 중 코드 영역과 데이터 영역은 프로그램 실행 도중 크기가 변하지 않아 정적 할당 영역이라 부르고, 크기가 변할 수 있는 힙 영역과 스택 영역을 동적 할당 영역이라 부른다.
1. PCB와 Context Switching
OS가 메모리에 적재된 다수의 프로세스를 관리하려면 프로세스를 식별할 수 있는 정보가 필요한데, 이것이 프로세스 제어 블록(PCB: Process Control Block)이다. PCB는 프로세스와 관련된 다양한 정보를 들고 있는 일종의 구조체로, 새로운 프로세스가 생성(메모리에 적재)되면 커널 영역에 만들어지고 실행이 끝나면 폐기된다.
운영체제마다 PCB의 정보에 차이가 있으나, 대표적으로 PID(프로세스 ID)와 실행 과정에서 사용한 레지스터 값, 현재 프로세스 상태, CPU 스케줄링(우선순위) 정보, 메모리 적채 위치, 파일 및 입출력장치 관련 정보가 명시된다. 아래는 리눅스에서 PCB를 나타내는 구조체의 일부 코드이다.
struct task_struct {
unsigned int __state; // 프로세스 상태
void *stack; // 스택 메모리
int prio; // 스케줄링 우선순위
struct mm_struct *mm; // 메모리 정보
pid_t pid; // 프로세스 id
struct files_struct *files; // 파일 정보
...
};
이런 PCB들은 보통 커널 내에 프로세스 테이블(process table) 형태로 관리된다. 프로세스 테이블은 곧 실행 중인 PCB의 모임을 의미하므로, 새롭게 실행되는 프로세스는 해당 프로세스의 PCB를 테이블에 추가하고 종료되는 프로세스는 사용 중이던 자원을 해제하고 PCB도 테이블에서 삭제된다.

프로세스가 비정상 종료되어 사용한 자원이 회수되었음에도 테이블에 종료된 PCB가 남아있을 수 있다. 이러한 비정상 종료 상태를 좀비 프로세스(zombie process)라고 한다.
일반적으로 메모리에 적재된 프로세스들은 한정된 시간 동안 번갈아 가며 실행된다. 여기서 프로세스가 실행된다는 말은 운영체제에 의해 CPU의 자원을 할당받았다란 말과 같다. CPU가 프로세스를 구성하는 명령어와 데이터를 인출하여 실행하고, 운영체제가 CPU 자원을 할당하기 때문이다.
프로세스의 CPU 사용 시간은 타이머 인터럽트(timer interrupt)에 의해 제한된다. 자신의 차례가 되면 정해진 시간만큼 CPU를 이용하고, 인터럽트가 발생하면 차례를 양보하고 다음 차례가 올 때까지 기다린다. 예를 들어, 다음과 같이 프로세스 A가 운영체제로부터 CPU를 할당받아 실행되다가, 인터럽트가 발생해 프로세스 B로 CPU 사용을 양보한다고 가정해 보자.

프로세스 A는 프로그램 카운터(PC)를 비롯한 각종 레지스터 값과 메모리 정보, 파일, 입출력 장치 등 지금까지의 중간 정보를 백업해야 한다. 그래야 이후에 다시 A를 실행할 때 이전까지 실행했던 내용을 이어서 재개할 수 있기 때문이다. 이렇게 프로세스 수행을 재개하기 위해 기억해야 하는 정보를 문맥(context)이라 하며, 문맥은 프로세스의 PCB에 명시된다. 프로세스가 CPU를 사용할 수 있는 시간이 다 되거나 인터럽트가 발생하면 운영체제는 해당 프로세스의 PCB에 문맥을 백업하고, 뒤이어 실행할 프로세스의 문맥을 복구한다.

이처럼 기존 프로세스 문맥을 PCB에 백업하고, PCB에서 문맥을 복구해 새로운 프로세스를 실행하는 것을 문맥 교환(Context Switching)이라 한다. 이것이 여러 프로세스가 끊임없이 빠르게 번갈아 가며 실행되는 원리이다.

프로세스 간 문맥 교환이 자주 발생하는 것은 좋은 상황이 아니다. 캐시 미스가 발생할 가능성이 높아져 메모리로부터 실행할 프로세스의 내용을 가져오는 작업이 빈번해지고, 이는 곧 큰 오버헤드로 이어질 수 있기 때문이다.
2. 프로세스의 상태
하나의 프로세스는 여러 상태를 거치며 실행되고, 운영체제는 PCB를 통해 프로세스 상태를 인식하고 관리한다. 운영체제 종류마다 약간의 차이는 있지만, 대표적으로 생성 / 준비 / 대기 / 종료 등이 있다.

- 생성 상태(New): 프로세스를 생성 중인 상태. 메모리에 적재되어 PCB를 할당받은 상태이다. 실행 준비가 완료되면 Ready 상태로 변경되어 CPU 할당을 기다린다.
- 준비 상태(Ready): 자신의 차례를 기다리는 상태. CPU를 할당받으면 Running 상태로 전환되며, 이를 디스패치(Dispatch)라 한다.
- 실행 상태(Running): CPU를 할당받아 실행 중인 상태. 일정 시간 동안 CPU를 사용할 수 있으며, 타이머 인터럽트가 발생해 프로세스가 할당된 시간을 모두 사용하면 다시 Ready 상태가 된다. 또는 실행 도중 입출력 작업 요청이 들어와 해당 요청이 끝날 때까지 기다려야 하면 Blocked 상태가 된다.
- 대기 상태(Blocked): 입출력 작업을 요청하거나 즉시 확보할 수 없는 자원을 요청하는 등 즉시 실행 불가능한 조건에 놓일 때 되는 상태. 입출력 작업이 완료되는 등 실행 가능한 상태가 되면 다시 Ready 상태가 되어 CPU 할당을 기다린다.
- 종료 상태(Terminated): 프로세스가 종료된 상태. 운영체제는 PCB와 프로세스가 사용한 메모리를 정리한다.
참고
실행 도중 입출력 작업 요청 등으로 Blocked 상태가 되고, 작업이 완료가 되면 Ready 상태가 되어 실행을 재개하는 방식을 블로킹 입출력(blocking I/O)이라고 한다. 반대로 입출력 장치에게 작업을 맡기고, 곧바로 이어질 명령어를 실행하는 방식을 논블로킹 입출력(non-blocking I/O)이라고 한다.
예를 들어, 한 프로세스가 네트워크를 통해 메시지를 보내는 시스템 콜을 호출했다고 가정해 보자. 메시지가 올바르게 보내졌는지(Send 완료) 확인할 때까지 Blocked 상태로 접어드는 경우 블로킹 입출력이고, 실행 결과를 기다리지 않고 곧바로 다음 명령을 수행하는 경우 논블로킹 입출력이다.

논블로킹 소켓 프로그래밍을 이전에 한 번 다룬 적이 있다. 궁금한 사람은 [논블로킹 소켓] 포스팅을 확인해 봐도 좋겠다.
2. 멀티프로세스와 멀티스레드
어떤 한 프로세스를 구성하는 코드를 동시에 실행할 수 있는 방법이 있는데, 그중 하나는 같은 프로그램을 각기 다른 여러 프로세스로 생성하여 실행하는 방법이다. 가장 대표적인 예시가 웹 브라우저 탭이다.
일반적으로 웹 브라우저는 탭 하나당 하나의 프로세스로 동작한다. Windows 운영체제의 경우 작업 관리자를 통해 확인할 수 있다. 아래 상황의 경우 웨일 브라우저에서 구글 탭을 하나 열었을 때 20개의 프로세스가 실행되고 있다.


그런데 여기서 네이버 탭을 하나 추가했더니 프로세스가 1개 추가된 것을 확인할 수 있다.


이렇게 동시에 여러 프로세스가 실행되는 것을 멀티프로세스(multi-process)라고 한다.
프로세스들은 기본적으로 자원을 공유하지 않고 독립적으로 실행된다는 점에 주의해야 한다. 같은 작업을 수행하지만 각각의 프로세스 ID(PID) 값을 가지고 있고, 파일과 입출력장치 등의 자원이 독립적으로 할당되어 다른 프로세스에 거의 영향을 끼치지 않는다. 그래서 한 프로세스의 실행 과정에서 문제가 발생해도 다른 프로세스에 직접적인 영향을 거의 끼치지 않는다.

한 프로세스를 구성하는 코드를 동시에 실행할 수 있는 다른 방법으로는 여러 스레드를 이용하는 것이다. 이렇게 프로세스를 동시에 실행하는 여러 스레드를 멀티스레드(multi-thread)라고 한다. 하나의 스레드는 스레드를 식별할 수 있는 고유 정보인 스레드 ID와 프로그램 카운터, 레지스터 값, 스택 등으로 구성된다. 스레드마다 각각의 프로그램 카운터 값과 스택을 가지기 때문에 스레드마다 다음에 실행할 주소를 가질 수 있고, 연산 과정의 임시 저장 값을 가질 수 있다.

멀티프로세스와 멀티스레드의 가장 큰 차이점은 자원의 공유 여부에 있다. 서로 다른 프로세스들은 기본적으로 자원을 공유하지 않기 때문에 독립적으로 실행되는 반면, 같은 프로세스를 실행하는 여러 스레드들은 프로세스의 자원을 공유한다. 스레드들은 동일한 주소 공간의 코드, 데이터, 힙 영역을 공유하고, 파일 등의 자원을 공유하기 때문에 쉽게 협력하고 통신할 수 있다. 다만 멀티프로세스 환경과 달리 멀티스레드 환경에서는 한 스레드에 생긴 문제가 프로세스 전체의 문제로 번질 수 있다.

3. 프로세스 간 통신: IPC(Inter-Process Communication)
프로세스는 기본적으로 자원을 공유하지 않는다. 하지만 필요에 따라 프로세스 간에도 자원을 공유하고 데이터를 주고받아야 할 때가 있는데, 이때 프로세스 간 통신(IPC:Inter-Process Communication)이라는 방법이 사용된다. 크게 2가지 방법이 있는데, 각각에 대해 자세히 알아보자.
1. 공유 메모리(Shared Memory)
프로세스 간에 공유하는 메모리 영역을 토대로 데이터를 주고받는 통신 방식
프로세스는 기본적으로 자원을 공유하지 않으므로 특정 프로세스가 다른 프로세스의 메모리 공간을 임의로 수정할 수 없다. 대신 별도의 메모리 공간을 할당해 해당 공간을 공유하여 읽고 쓰게 하는 방식이 공유 메모리 기법이다.

공유 메모리 기반 IPC는 프로세스가 공유하는 메모리 영역을 확보하는 시스템 콜을 기반으로 수행될 수도 있고, 간단하게 프로세스가 공유하는 변수나 파일을 활용할 수도 있다. 가령 'a.txt'라는 파일을 프로세스 A는 수정하고 프로세스 B는 읽는다면 두 프로세스는 'a.txt'라는 파일을 매개로 프로세스 간에 통신을 주고받은 셈이다.

공유 메모리 기반 IPC는 통신을 주고받는 각 프로세스가 마치 자신의 메모리 영역을 읽고 쓰는 것처럼 통신한다. 또한 이 과정에서 커널의 개입이 거의 없다. 이러한 특징으로 메시지 전달 방식보다 통신 속도가 빠른 대신, 공유 메모리 영역을 동시에 읽고 쓸 경우 데이터의 일관성이 훼손될 수 있다는 문제가 있다. 이를 레이스 컨디션이라 한다.
2. 메시지 전달
프로세스 간에 주고받을 데이터가 커널을 거쳐 송수신되는 통신 방식
메시지 전달 기반 IPC는 메시지를 보내는 수단과 받는 수단이 명확하게 구분되어 있다. 메시지 전송은 send()라는 시스템 콜로, 메시지 수신은 recv()라는 시스템 콜로 정해져 있다.

프로세스 입장에서 메시지 기반 IPC는 커널의 도움을 적극적으로 받을 수 있어 레이스 컨디션, 동기화 등의 문제에서 상대적으로 자유롭다. 다만 커널을 통해 데이터가 송수신되므로 공유 메모리 기반 IPC보다 속도가 느리다.
1. 파이프(pipe)
메시지 전달 기반 IPC를 위한 수단 중 하나로, 단방향 프로세스 간 통신 도구이다. 아래 그림과 같이 프로세스 A가 파이프의 한쪽 방향에서 데이터를 쓰면 프로세스 B는 파이프 반대쪽으로 데이터를 읽을 수 있다. 데이터 관점에서 보면 파이프에 먼저 삽입된 데이터가 먼저 읽히는 것이다. 만약 단방향 파이프로 양방향 통신을 수행하고 싶다면, 읽기용 파이프와 쓰기용 파이프 2개를 이용해 통신할 수 있다.

이런 단방향 통신 수단인 전통적인 파이프는 익명 파이프(unnamed pipe)라고도 부른다. 익명 파이프는 양방향 통신을 지원하지 않고, 부모 프로세스와 자식 프로세스 간에만 통신이 가능하다는 한계가 있다.
익명 파이프를 확장한 네임드 파이프(named pipe)도 존재한다. 양방향 통신을 지원하며, 부모 - 자식뿐만 아니라 임의의 프로세스 간에도 사용할 수 있다.
2. 시그널(signal)
메시지 전달 기반 IPC를 위해 시그널을 활용할 수 있다. 시그널은 프로세스에게 특정 이벤트(event)가 발생했음을 알리는 비동기적 신호이다.
프로세스는 시그널이 발생하면 여느 인터럽트 처리 과정과 유사하게 하던 일을 중단하고, 시그널 처리를 위한 시그널 핸들러(signal handler)를 실행한 뒤 실행을 재개한다. 이때 프로세스는 특정 시그널을 발생시킬 수도 있고, 직접 일부 시그널 핸들러를 (재)정의할 수 있다. 시그널이 발생했을 때의 동작을 정의해 프로세스에게 해당 시그널을 보냄으로써 프로세스 간 통신을 수행할 수 있다.
시그널을 발생시키는 이벤트 종류는 다양하다. 대부분 인터럽트 관련 이벤트지만, 사용자가 직접 정의할 수 있는 시그널도 있다.
시그널을 이용하는 방법은 직접적으로 메시지를 주고받지는 않지만, 비동기적으로 원하는 동작을 수행할 수 있는 좋은 수단이다.

대부분의 시그널은 프로세스를 종료하거나, 무시하거나, 코어 덤프를 생성하는 기본 동작을 수행한다. 여기서 코어 덤프(core dump)는 주로 비정상 종료 시 생성되는 파일로, 프로그램이 특정 시점에 작업하던 메모리 상태가 기록되어 있다. 디버깅 용도로 자주 확인하게 될 파일이다.
3. 기타
이 외에도 원격 프로시저 호출(RPC: Remote Procedure Call)이나 네트워크 소켓을 통해 IPC를 수행할 수도 있다. RPC는 원격 코드를 실행하는 IPC 기술이다. 한 프로세스 내 특정 코드 실행이 로컬 프로시저 호출이라면, 다른 프로세스의 원격 코드 실행이 원격 프로시저 호출인 셈이다. RPC를 통해 프로그래밍 언어나 플랫폼과 무관하게 성능 저하를 최소화하고, 메시지 송수신이 가능하기 때문에 대규모 트래픽 처리 환경, 특히 서버 간 통신 환경에서 사용되는 경우가 많다.
4. 마치며
프로세스와 스레드에 대한 전반적인 내용을 살펴보았다. 다음 포스팅에서는 프로세스 간 통신이 이루어지는 상황에서 발생할 수 있는 문제와, 이를 해결하는 동기화 기법 등에 대해 알아보겠다.

'Computer Science > Operating System' 카테고리의 다른 글
| 가상 메모리 (0) | 2025.10.17 |
|---|---|
| Stack 메모리 vs Heap 메모리 (0) | 2025.09.25 |
| 운영체제의 큰 그림 (0) | 2025.05.09 |