운영체제 프로세스: 메모리 구조와 상태 전이, PCB와 IPC
운영체제 프로세스의 메모리 구조, 상태 전이, PCB, 생성·종료 과정과 IPC 및 쓰레드와의 차이를 정리합니다.
2026-08-14 · 최초 발행 2026-01-03
실행 파일이 프로세스가 되는 시점
프로세스(Process)는 메모리에 올라와 CPU가 실행 중인 프로그램 인스턴스다. 디스크에 있는 프로그램은 정적인 실행 파일이지만, 실행되면 자원과 실행 상태를 가진 동적인 개체가 된다. 운영체제는 이 프로세스를 CPU 스케줄링 대상으로 삼고, 메모리·파일 디스크립터·네트워크 소켓·I/O 장치를 프로세스 단위로 배분한다.
하나의 프로그램에서 여러 프로세스가 만들어질 수 있다. Chrome.exe를 실행했을 때 여러 Chrome 프로세스가 존재하는 경우가 이에 해당한다.
프로세스가 끝나면 운영체제는 프로세스에 할당했던 자원을 회수한다. 이 단위성은 자원 관리의 기준이 되는 동시에, 각 프로세스를 서로 분리하는 기반이기도 하다.
독립 주소 공간과 전환 비용
각 프로세스에는 독립적인 가상 주소 공간이 주어진다. 다른 프로세스의 메모리를 직접 접근할 수 없으며, 이 격리는 MMU와 페이지 테이블로 구현된다. 메모리 보호는 Read, Write, Execute 권한으로 적용된다.
한 프로세스의 오류가 다른 프로세스로 바로 번지지 않는다는 점은 안정성과 보안 측면의 장점이다. 반대로 프로세스끼리 데이터를 주고받으려면 IPC(Inter-Process Communication)가 필요하다.
CPU가 실행 대상을 바꿀 때는 Context Switching이 수행된다. 현재 프로세스의 레지스터 값을 PCB에 저장하고, 스케줄러가 다음 프로세스를 고른 뒤, 해당 PCB에서 레지스터 값을 복원하여 실행을 이어간다.
- 현재 프로세스 레지스터 값을 PCB에 저장한다.
- 스케줄러가 다음 프로세스를 선택한다.
- 다음 프로세스 PCB에서 레지스터 값을 복원한다.
- 실행을 재개한다.
이 과정에는 레지스터 저장·복원, Cache Flush, TLB 플러시가 포함되며 수 마이크로초 ~ 수십 마이크로초의 오버헤드가 발생한다. 타임 슬라이스 만료, I/O 요청(Blocking), 인터럽트 발생, 우선순위가 높은 프로세스의 Ready 상태 진입이 전환을 유발할 수 있다.
스케줄러가 다루는 프로세스 상태
기본적인 5-State 모델은 New, Ready, Run, Sleep(Wait), Terminated 상태로 프로세스의 생명주기를 표현한다.
- New: 프로세스가 생성되는 중인 상태다. 메모리 할당과 PCB 생성 후 Ready 상태로 전이할 준비를 한다.
- Ready: 실행 준비는 끝났지만 CPU 할당을 기다리는 상태다. Ready Queue에서 대기한다.
- Run: CPU를 배정받아 명령어를 수행하는 상태다. 단일 CPU에서는 한 번에 하나의 프로세스만 Run 상태가 된다.
- Sleep(Wait): I/O 완료 또는 이벤트를 기다리는 상태다. CPU를 할당받아도 실행할 수 없으며 Wait Queue에 머문다.
- Terminated: 실행을 마친 상태다. 자원 해제와 PCB 삭제가 뒤따른다.
메모리 상황까지 포함해 다루는 7-State 모델은 Suspend Ready와 Suspend Wait를 추가한다. Ready 또는 Wait 상태의 프로세스가 Swap Out된 상태이며, 메모리 부족, 낮은 우선순위, 시스템 부하 감소가 Suspend의 이유가 된다.
New에서 Ready로의 전이는 메모리 할당이 끝나는 Admit 시점에 일어난다. Ready에서 Run으로는 스케줄러의 Dispatch가 필요하다. 타임 슬라이스가 만료되면 Run은 Ready로 돌아가고, I/O 요청이나 System Call은 Run을 Sleep으로 옮긴다. I/O 완료나 이벤트 발생 후에는 다시 Ready가 되며, 정상 또는 비정상 종료는 Run에서 Terminated로 이어진다.
주소 공간 안에서 Code, Data, Heap, Stack이 맡는 일
프로세스 메모리는 Code, Data, Heap, Stack 영역으로 나뉜다.
Code 영역에는 기계어 형태의 실행 코드와 함수 코드가 들어간다. 읽기 전용이며, 같은 프로그램을 실행하는 여러 프로세스가 공유할 수 있다.
Data 영역은 프로그램 시작부터 종료까지 유지되는 읽기·쓰기 영역이다. 초기화된 전역/static 변수는 .data, 초기화되지 않은 전역/static 변수는 .bss에 배치된다.
Heap은 malloc, new와 같은 동적 메모리 할당에 사용된다. 낮은 주소에서 높은 주소 방향으로 성장하고 런타임에 크기가 바뀐다. C에서는 malloc()과 free(), C++에서는 new와 delete로 관리하며, Java에서는 new와 GC 자동 관리가 사용된다.
Stack에는 지역 변수와 함수 매개변수, Return Address, Frame Pointer 같은 함수 호출 정보가 저장된다. 높은 주소에서 낮은 주소 방향으로 성장하며 LIFO(Last In First Out) 방식으로 동작한다. 함수마다 Stack Frame이 만들어지고, 함수가 끝나면 지역 변수·매개변수·반환 주소를 포함한 해당 Frame이 자동 해제된다. 재귀 함수가 무한 호출되거나 Stack과 Heap이 충돌하면 Stack Overflow가 발생할 수 있다.
PCB는 프로세스의 실행 문맥을 보관한다
PCB(Process Control Block)는 운영체제가 프로세스 상태를 관리하기 위해 유지하는 정보 구조다. 프로세스 식별 정보로 PID(Process ID), PPID(Parent Process ID), User ID, Group ID를 담는다.
실행을 재개하기 위한 정보도 PCB에 있다. Process State, 다음 명령어 주소인 Program Counter(PC), Context Switching 때 저장하는 CPU 레지스터 값이 여기에 포함된다. 우선순위, CPU 사용 시간, 대기 시간 같은 스케줄링 정보와 페이지 테이블·세그먼트 테이블 같은 메모리 관리 정보도 관리 대상이다.
파일 디스크립터 테이블로 표현되는 열린 파일 목록, I/O 요청 큐, 누적 CPU 사용 시간은 자원 관리와 종료 시 자원 회수에 쓰인다. Context Switching에서는 현재 프로세스 상태를 저장하고 다음 프로세스 상태를 복원하며, 스케줄러는 PCB의 우선순위와 CPU 시간 정보를 참고한다.
Linux에서는 task_struct 구조체가 PCB 역할을 하며 /include/linux/sched.h에 있다. Windows에서는 Kernel 모드에서 관리되는 EPROCESS 구조체가 이 역할을 맡는다.
fork(), execve(), exit()로 이어지는 생명주기
Unix/Linux에서 fork()는 부모 프로세스를 바탕으로 자식 프로세스를 생성한다.
pid_t pid = fork();
if (pid == 0) {
// 자식 프로세스
printf("Child PID: %d\n", getpid());
} else if (pid > 0) {
// 부모 프로세스
printf("Parent PID: %d, Child PID: %d\n", getpid(), pid);
} else {
// fork 실패
perror("fork failed");
}
fork()는 부모 프로세스 PCB를 복사하고, 새 PID를 할당한 뒤, Copy-On-Write 방식으로 메모리를 복사한다. 만들어진 자식 프로세스는 Ready Queue에 추가된다.
execve()는 현재 프로세스 이미지를 새 프로그램으로 교체한다. 일반적으로 fork() 뒤에 execve()를 호출하며 PID는 유지된다.
char *argv[] = {"/bin/ls", "-l", NULL};
execve("/bin/ls", argv, NULL);
정상 종료는 exit()으로 처리할 수 있다.
exit(0); // 종료 코드 0 (성공)
Segmentation Fault(메모리 접근 위반), Abort(프로그램 오류), kill 시그널에 의한 강제 종료는 비정상 종료에 해당한다. 종료 시에는 메모리·파일·소켓을 포함한 모든 자원을 해제하고 열린 파일을 닫는다. 이후 부모에게 exit code를 전달하며, 부모가 wait()를 호출할 때까지 Zombie 상태로 PCB가 남고 그 뒤 삭제된다.
Unix/Linux의 프로세스 계층은 트리 구조다. Windows는 부모-자식 관계가 약한 평등 구조를 가진다. 부모가 wait()를 호출하지 않아 종료했지만 PCB가 남아 있는 프로세스가 Zombie 프로세스다. 부모가 먼저 종료된 Orphan 프로세스는 init(PID 1) 프로세스가 입양하며, init이 wait()를 호출해 정리한다.
격리된 프로세스가 데이터를 교환하는 방법
독립 주소 공간은 프로세스 간 직접 통신을 막는다. 협력 작업, 데이터 교환, 동기화, 이벤트 알림이 필요할 때 IPC를 선택한다.
| IPC 방법 | 속도 | 복잡도 | 용도 |
|---|---|---|---|
| Pipe | 중간 | 낮음 | 부모-자식 간 단순 데이터 |
| Named Pipe | 중간 | 중간 | 무관한 프로세스 간 |
| Message Queue | 중간 | 중간 | 비동기 메시지 전달 |
| Shared Memory | 빠름 | 높음 | 대용량 데이터 공유 |
| Socket | 느림 | 높음 | 네트워크 통신 |
| Signal | 빠름 | 낮음 | 이벤트 알림 |
파이프는 부모-자식 프로세스 간 단방향 FIFO(First In First Out) 통신에 사용한다.
int pipefd[2];
pipe(pipefd); // pipefd[0]: 읽기, pipefd[1]: 쓰기
Named Pipe(FIFO)는 파일 시스템에 생성되며 무관한 프로세스 간 통신과 양방향 통신에 쓸 수 있다. 메시지 큐는 메시지 단위의 비동기 통신을 제공하고 여러 프로세스 간에 사용할 수 있다.
공유 메모리는 동일한 메모리 영역을 공유하므로 가장 빠른 IPC지만 세마포어와 뮤텍스를 통한 동기화가 필요하다. 소켓은 로컬 소켓으로 동일 시스템 안에서, 원격 소켓으로 네트워크 사이에서 통신한다. 시그널은 SIGINT(Ctrl+C), SIGKILL처럼 비동기 이벤트를 알리는 데 사용한다.
격리와 공유 사이에서 프로세스와 쓰레드를 고르기
| 구분 | 프로세스 | 쓰레드 |
|---|---|---|
| 정의 | 실행 중인 프로그램 | 프로세스 내 실행 흐름 |
| 메모리 | 독립적 (Code, Data, Heap, Stack) | 공유 (Code, Data, Heap), 독립 (Stack) |
| 자원 할당 | 프로세스 단위 | 쓰레드는 최소한 자원 |
| Context Switch | 오버헤드 큼 (페이지 테이블 전환) | 오버헤드 작음 (레지스터만) |
| 통신 | IPC 필요 (복잡) | 직접 메모리 접근 (간단) |
| 생성 비용 | 높음 (fork) | 낮음 (pthread_create) |
| 안정성 | 높음 (격리) | 낮음 (공유로 인한 충돌) |
| 병렬성 | 멀티프로세스 | 멀티쓰레드 |
한 프로세스의 오류가 다른 프로세스에 영향을 주지 않아야 하거나, 작업의 독립성과 보안이 우선이면 프로세스를 선택한다. 데이터 공유, 빠른 Context Switch, GUI 애플리케이케이션의 응답성이 핵심이면 쓰레드가 맞는다.