프로세스 개념
프로세스 개념¶
개요¶
프로세스(Process)는 실행 중인 프로그램을 의미합니다. 이 레슨에서는 프로세스의 메모리 구조, 프로세스 제어 블록(PCB), 프로세스 상태 전이, 그리고 컨텍스트 스위치에 대해 학습합니다.
목차¶
1. 프로세스란?¶
프로그램 vs 프로세스¶
┌────────────────────────────────────────────────────────┐
│ 프로그램 vs 프로세스 │
├────────────────────┬───────────────────────────────────┤
│ 프로그램 │ 프로세스 │
├────────────────────┼───────────────────────────────────┤
│ 정적인 개체 │ 동적인 개체 │
│ 디스크에 저장 │ 메모리에 적재 │
│ 실행 파일 │ 실행 중인 파일 │
│ 수동적 (passive) │ 능동적 (active) │
│ 변하지 않음 │ 상태가 계속 변함 │
└────────────────────┴───────────────────────────────────┘
프로그램 ──(로드)──▶ 프로세스
메모리에
적재하면
프로세스의 구성 요소¶
프로세스 = 코드 + 데이터 + 스택 + 힙 + PCB
┌─────────────────────────────────────┐
│ 프로세스 │
├─────────────────────────────────────┤
│ ┌───────────────────────────────┐ │
│ │ 텍스트 (코드) 섹션 │ │ 실행할 명령어
│ ├───────────────────────────────┤ │
│ │ 데이터 섹션 │ │ 전역/정적 변수
│ ├───────────────────────────────┤ │
│ │ 힙 │ │ 동적 할당 메모리
│ ├───────────────────────────────┤ │
│ │ 스택 │ │ 지역변수, 함수 호출
│ └───────────────────────────────┘ │
│ │
│ ┌───────────────────────────────┐ │
│ │ PCB (커널에 저장) │ │ 프로세스 메타데이터
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
2. 프로세스 메모리 구조¶
메모리 레이아웃¶
높은 주소 (0xFFFFFFFF)
┌─────────────────────────────────────┐
│ 커널 영역 │ OS 전용 (사용자 접근 불가)
├─────────────────────────────────────┤ ← 0xC0000000 (Linux 32-bit)
│ │
│ 스택 (Stack) │ 지역 변수, 함수 매개변수
│ ↓ 아래로 성장 │ 복귀 주소
│ │
├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
│ │
│ ↑ 위로 성장 │
│ 힙 (Heap) │ malloc, new
│ │
├─────────────────────────────────────┤
│ BSS │ 초기화되지 않은 전역/정적 변수
├─────────────────────────────────────┤
│ Data │ 초기화된 전역/정적 변수
├─────────────────────────────────────┤
│ Text (Code) │ 프로그램 코드 (읽기 전용)
└─────────────────────────────────────┘
낮은 주소 (0x00000000)
각 섹션 상세¶
#include <stdio.h>
#include <stdlib.h>
// BSS 섹션: 초기화되지 않은 전역 변수
int uninit_global;
// Data 섹션: 초기화된 전역 변수
int init_global = 42;
// Data 섹션: 정적 변수
static int static_var = 100;
void example_function(int param) { // param: 스택
int local_var = 10; // 스택
static int func_static = 0; // Data 섹션
int *heap_ptr;
heap_ptr = malloc(sizeof(int)); // 힙에 메모리 할당
*heap_ptr = 20;
printf("local: %d, heap: %d\n", local_var, *heap_ptr);
free(heap_ptr); // 힙 메모리 해제
}
// Text 섹션: 이 코드 자체
int main() {
example_function(5);
return 0;
}
메모리 영역 특성¶
┌──────────┬──────────┬──────────┬────────────────────────┐
│ 섹션 │ 읽기 │ 쓰기 │ 용도 │
├──────────┼──────────┼──────────┼────────────────────────┤
│ Text │ O │ X │ 프로그램 코드 │
│ Data │ O │ O │ 초기화된 전역/정적 변수 │
│ BSS │ O │ O │ 미초기화 전역/정적 변수 │
│ Heap │ O │ O │ 동적 할당 메모리 │
│ Stack │ O │ O │ 지역 변수, 함수 호출 │
└──────────┴──────────┴──────────┴────────────────────────┘
스택 프레임¶
함수 호출 시 스택 구조:
int add(int a, int b) {
int result = a + b;
return result;
}
int main() {
int x = add(3, 5);
return 0;
}
┌─────────────────────────────┐ ← 높은 주소
│ ... │
├─────────────────────────────┤
│ main() 스택 프레임 │
│ ┌───────────────────────┐ │
│ │ x (지역 변수) │ │
│ │ 이전 프레임 포인터 │ │
│ │ 복귀 주소 │ │
│ └───────────────────────┘ │
├─────────────────────────────┤
│ add() 스택 프레임 │
│ ┌───────────────────────┐ │
│ │ result (지역 변수) │ │
│ │ 이전 프레임 포인터 │ │
│ │ 복귀 주소 │ │
│ │ b = 5 (매개변수) │ │
│ │ a = 3 (매개변수) │ │
│ └───────────────────────┘ │
├─────────────────────────────┤ ← Stack Pointer (SP)
│ ... │
└─────────────────────────────┘ ← 낮은 주소
3. 프로세스 제어 블록 (PCB)¶
PCB란?¶
PCB (Process Control Block) = 프로세스를 관리하기 위한 모든 정보를 담은 자료구조
= 커널이 유지
= 프로세스 테이블에 저장
┌───────────────────────────────────────────────────────┐
│ PCB 구조 │
├───────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 프로세스 식별자 (PID) │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ 프로세스 상태 (Ready, Running, Waiting...) │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ 프로그램 카운터 (PC) - 다음 실행 명령어 주소 │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ CPU 레지스터 (범용, 스택 포인터, 플래그...) │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ CPU 스케줄링 정보 (우선순위, 스케줄링 큐) │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ 메모리 관리 정보 (페이지 테이블, 세그먼트 테이블) │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ 회계 정보 (CPU 사용 시간, 시작 시간...) │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ I/O 상태 정보 (열린 파일 목록, I/O 장치...) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────┘
Linux task_struct (간략화)¶
// Linux 커널의 프로세스 구조체 (간략화)
struct task_struct {
// 프로세스 식별
pid_t pid; // 프로세스 ID
pid_t tgid; // 스레드 그룹 ID
// 프로세스 상태
volatile long state; // TASK_RUNNING, TASK_INTERRUPTIBLE...
// 스케줄링 정보
int prio; // 동적 우선순위
int static_prio; // 정적 우선순위
struct sched_entity se; // 스케줄링 엔티티
// CPU 컨텍스트
struct thread_struct thread; // CPU 레지스터 상태
// 메모리 관리
struct mm_struct *mm; // 메모리 디스크립터
// 파일 시스템
struct files_struct *files; // 열린 파일 테이블
struct fs_struct *fs; // 파일 시스템 정보
// 프로세스 관계
struct task_struct *parent; // 부모 프로세스
struct list_head children; // 자식 프로세스 리스트
struct list_head sibling; // 형제 프로세스 리스트
// 시그널
struct signal_struct *signal;
// 타이밍 정보
u64 utime, stime; // 사용자/시스템 CPU 시간
u64 start_time; // 시작 시간
};
프로세스 테이블¶
┌─────────────────────────────────────────────────────────┐
│ 프로세스 테이블 │
├─────┬──────────────────────────────────────────────────┤
│ PID │ PCB │
├─────┼──────────────────────────────────────────────────┤
│ 1 │ init: state=Running, priority=20, mem=4MB... │
├─────┼──────────────────────────────────────────────────┤
│ 2 │ kthreadd: state=Sleeping, priority=10... │
├─────┼──────────────────────────────────────────────────┤
│ 100 │ bash: state=Ready, priority=20, mem=8MB... │
├─────┼──────────────────────────────────────────────────┤
│ 101 │ vim: state=Waiting, priority=20, mem=12MB... │
├─────┼──────────────────────────────────────────────────┤
│ ... │ ... │
└─────┴──────────────────────────────────────────────────┘
4. 프로세스 상태 전이¶
5-상태 모델¶
생성 (New)
│
│ 승인 (Admitted)
▼
┌──────────────┐ 디스패치 ┌──────────────┐
│ │─────────▶│ │
│ 준비 │ │ 실행 │──────┐ 종료
│ (Ready) │◀─────────│ (Running) │ │
│ │ 인터럽트 │ │ │
└──────────────┘(타임아웃) └──────────────┘ │
▲ │ ▼
│ │ ┌──────────┐
│ I/O 또는 │ │ 종료 │
│ 이벤트 완료 │ │(Terminated)│
│ │ └──────────┘
│ │ I/O 또는
│ │ 이벤트 대기
│ ▼
│ ┌──────────────┐
└──────────────│ 대기 │
│ (Waiting) │
└──────────────┘
상태 설명¶
┌────────────────┬────────────────────────────────────────┐
│ 상태 │ 설명 │
├────────────────┼────────────────────────────────────────┤
│ New (생성) │ 프로세스가 생성 중 │
├────────────────┼────────────────────────────────────────┤
│ Ready (준비) │ CPU 할당을 기다리는 중 │
│ │ 실행할 준비가 완료됨 │
├────────────────┼────────────────────────────────────────┤
│ Running (실행) │ CPU에서 명령어를 실행 중 │
│ │ 한 번에 하나의 프로세스만 (단일 CPU) │
├────────────────┼────────────────────────────────────────┤
│ Waiting (대기) │ I/O 완료나 이벤트를 기다리는 중 │
│ │ I/O 완료 시 Ready 상태로 전환 │
├────────────────┼────────────────────────────────────────┤
│ Terminated │ 실행 완료, 자원 반환 중 │
│ (종료) │ │
└────────────────┴────────────────────────────────────────┘
상태 전이 조건¶
┌─────────────────┬─────────────────────────────────────────┐
│ 전이 │ 조건 │
├─────────────────┼─────────────────────────────────────────┤
│ New → Ready │ OS가 프로세스를 승인 │
├─────────────────┼─────────────────────────────────────────┤
│ Ready → Running │ 스케줄러가 CPU 할당 (dispatch) │
├─────────────────┼─────────────────────────────────────────┤
│ Running → Ready │ 타임 슬라이스 만료 (타임아웃) │
│ │ 더 높은 우선순위 프로세스 도착 (선점) │
├─────────────────┼─────────────────────────────────────────┤
│ Running → Wait │ I/O 요청, 이벤트 대기 │
├─────────────────┼─────────────────────────────────────────┤
│ Wait → Ready │ I/O 완료, 이벤트 발생 │
├─────────────────┼─────────────────────────────────────────┤
│ Running → Term │ exit() 호출, 정상/비정상 종료 │
└─────────────────┴─────────────────────────────────────────┘
7-상태 모델 (Swapping 포함)¶
┌──────────────────────────────────┐
│ │
│ 스왑 아웃 │
▼ │ │
┌─────────┐ ┌─────────┐ ┌───┴─────┐ │
│ New │──────────▶│ Ready │◀────────▶│ Ready │ │
│ │ │ │ 스왑인 │ Suspend │ │
└─────────┘ └─────────┘ └─────────┘ │
│ │
디스패치 │
│ │
▼ │
┌─────────┐ ┌─────────┐ │
│ Term │◀──────────│ Running │ │
│ │ │ │ │
└─────────┘ └─────────┘ │
│ │
I/O 요청 │
│ │
▼ 스왑 아웃 │
┌─────────┐ ┌─────────┐ │
│ Waiting │◀────────▶│ Waiting │────────┘
│ │ 스왑인 │ Suspend │
└─────────┘ └─────────┘
Suspend 상태: 메모리에서 디스크로 스왑 아웃된 프로세스
5. 컨텍스트 스위치¶
컨텍스트 스위치란?¶
컨텍스트 스위치 (Context Switch)
= CPU를 다른 프로세스로 전환하는 과정
= 현재 프로세스의 상태를 저장하고, 새 프로세스의 상태를 복원
┌────────────────────────────────────────────────────────────┐
│ 컨텍스트 스위치 과정 │
└────────────────────────────────────────────────────────────┘
프로세스 P0 운영체제 프로세스 P1
│ │ │
│ 실행 중 │ │ 대기 중
│ │ │
│──인터럽트/시스템콜───▶│ │
│ │ │
│ PCB0에 상태 저장 │
│ │ │
│ PCB1에서 상태 복원 │
│ │ │
│ │──────────────────────▶│
│ 대기 중 │ │ 실행 중
│ │ │
│ │◀──인터럽트/시스템콜────│
│ │ │
│ PCB1에 상태 저장 │
│ │ │
│ PCB0에서 상태 복원 │
│ │ │
│◀─────────────────────│ │
│ 실행 중 │ │ 대기 중
컨텍스트 스위치에서 저장/복원하는 정보¶
┌────────────────────────────────────────────────────────┐
│ 컨텍스트 (Context) │
├────────────────────────────────────────────────────────┤
│ │
│ CPU 레지스터: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ • 프로그램 카운터 (PC) │ │
│ │ • 스택 포인터 (SP) │ │
│ │ • 베이스 포인터 (BP) │ │
│ │ • 범용 레지스터 (RAX, RBX, RCX, RDX...) │ │
│ │ • 상태 레지스터 (FLAGS) │ │
│ │ • 부동소수점 레지스터 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 메모리 관리 정보: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ • 페이지 테이블 베이스 레지스터 │ │
│ │ • 세그먼트 레지스터 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────┘
컨텍스트 스위치 비용¶
┌────────────────────────────────────────────────────────┐
│ 컨텍스트 스위치 비용 │
├────────────────────────────────────────────────────────┤
│ │
│ 직접 비용: │
│ • 레지스터 저장/복원: ~수백 나노초 │
│ • 커널 모드 전환: ~수백 나노초 │
│ │
│ 간접 비용 (더 큼): │
│ • TLB 플러시: 수백~수천 사이클 │
│ • 캐시 미스 증가 (캐시 오염) │
│ • 파이프라인 플러시 │
│ │
│ 일반적인 총 비용: 1~10 마이크로초 │
│ │
└────────────────────────────────────────────────────────┘
시간축에서 보는 컨텍스트 스위치:
P0 실행 │ 컨텍스트 스위치 │ P1 실행 │ 컨텍스트 스위치 │ P0 실행
━━━━━━━━│ 오버헤드 │━━━━━━━━│ 오버헤드 │━━━━━━━
│← ~1-10 μs →│ │← ~1-10 μs →│
↑ 이 시간 동안 유용한 작업 불가
6. 프로세스 생성과 종료¶
fork()를 이용한 프로세스 생성¶
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid;
int x = 10;
printf("부모 프로세스 시작, PID: %d\n", getpid());
pid = fork(); // 프로세스 분기점
if (pid < 0) {
// fork 실패
perror("fork failed");
return 1;
}
else if (pid == 0) {
// 자식 프로세스
printf("자식: PID=%d, 부모 PID=%d\n", getpid(), getppid());
x = x + 10;
printf("자식: x = %d\n", x);
}
else {
// 부모 프로세스
printf("부모: PID=%d, 자식 PID=%d\n", getpid(), pid);
wait(NULL); // 자식 종료 대기
printf("부모: x = %d\n", x); // 여전히 10
}
return 0;
}
/*
출력:
부모 프로세스 시작, PID: 1234
부모: PID=1234, 자식 PID=1235
자식: PID=1235, 부모 PID=1234
자식: x = 20
부모: x = 10
*/
fork() 동작 과정¶
fork() 호출 전:
┌─────────────────────────────────┐
│ 부모 프로세스 (PID: 100) │
│ ┌─────────────────────────┐ │
│ │ x = 10 │ │
│ │ 코드/데이터/스택/힙 │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
fork() 호출 후:
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ 부모 프로세스 (PID: 100) │ │ 자식 프로세스 (PID: 101) │
│ ┌─────────────────────────┐ │ │ ┌─────────────────────────┐ │
│ │ x = 10 │ │ │ │ x = 10 (복사됨) │ │
│ │ 코드/데이터/스택/힙 │ │ │ │ 코드/데이터/스택/힙 │ │
│ │ fork() 반환값: 101 │ │ │ │ fork() 반환값: 0 │ │
│ └─────────────────────────┘ │ │ └─────────────────────────┘ │
└─────────────────────────────────┘ └─────────────────────────────────┘
│ │
│ 두 프로세스는 독립적 │
│ (별도의 메모리 공간) │
▼ ▼
exec()를 이용한 프로그램 실행¶
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 자식 프로세스: ls 명령어 실행
printf("자식: ls 실행\n");
// execl: exec with list of arguments
execl("/bin/ls", "ls", "-l", NULL);
// exec 성공 시 이 코드는 실행되지 않음
perror("exec failed");
return 1;
}
else {
// 부모 프로세스
wait(NULL);
printf("부모: 자식 종료됨\n");
}
return 0;
}
exec() 동작 과정¶
exec() 호출 전:
┌─────────────────────────────────┐
│ 자식 프로세스 │
│ ┌─────────────────────────┐ │
│ │ 원래 프로그램 코드 │ │
│ │ 원래 데이터 │ │
│ │ 원래 스택/힙 │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
exec("/bin/ls") 호출 후:
┌─────────────────────────────────┐
│ 자식 프로세스 │
│ ┌─────────────────────────┐ │
│ │ ls 프로그램 코드 │ │ ← 새 프로그램으로
│ │ ls 데이터 │ │ 완전히 교체
│ │ 새로운 스택/힙 │ │
│ └─────────────────────────┘ │
│ │
│ PID는 그대로 유지 │
└─────────────────────────────────┘
프로세스 종료¶
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
printf("자식 프로세스 종료\n");
exit(42); // 종료 코드 42로 종료
}
else {
int status;
wait(&status); // 자식의 종료 상태 수집
if (WIFEXITED(status)) {
printf("자식 종료 코드: %d\n", WEXITSTATUS(status));
}
}
return 0;
}
/*
출력:
자식 프로세스 종료
자식 종료 코드: 42
*/
좀비 프로세스와 고아 프로세스¶
좀비 프로세스 (Zombie Process):
- 자식이 종료되었지만 부모가 wait()를 호출하지 않은 상태
- PCB가 남아있음 (종료 상태 저장)
- 자원은 해제되었지만 프로세스 테이블 엔트리 유지
┌──────────────────────────────────────────────┐
│ 부모 프로세스 (PID: 100) │
│ - wait() 호출 안 함 │
└──────────────────────────────────────────────┘
│
│ (관계 유지)
▼
┌──────────────────────────────────────────────┐
│ 좀비 프로세스 (PID: 101) │
│ - 상태: Z (Zombie) │
│ - 코드/데이터/스택 해제됨 │
│ - PCB만 남아있음 │
└──────────────────────────────────────────────┘
고아 프로세스 (Orphan Process):
- 부모가 자식보다 먼저 종료된 상태
- init(PID 1) 또는 systemd가 새 부모가 됨
┌──────────────────────────────────────────────┐
│ init (PID: 1) │
│ - 고아 프로세스의 새 부모 │
│ - 주기적으로 wait() 호출 │
└──────────────────────────────────────────────┘
│
│ (입양)
▼
┌──────────────────────────────────────────────┐
│ 고아 프로세스 (PID: 102) │
│ - 원래 부모 (PID: 100)가 종료됨 │
│ - PPID가 1로 변경됨 │
└──────────────────────────────────────────────┘
7. 연습 문제¶
문제 1: 메모리 영역 식별¶
다음 변수들이 저장되는 메모리 영역을 고르세요.
int global_var = 100; // ( )
int uninitialized; // ( )
const char* str = "hello"; // ( )
void func() {
int local = 10; // ( )
static int stat = 20; // ( )
int* ptr = malloc(4); // ptr: ( ), *ptr: ( )
}
보기: Text, Data, BSS, Stack, Heap
정답 보기
int global_var = 100; // (Data)
int uninitialized; // (BSS)
const char* str = "hello"; // str: Data, "hello": Text (읽기전용)
void func() {
int local = 10; // (Stack)
static int stat = 20; // (Data)
int* ptr = malloc(4); // ptr: (Stack), *ptr: (Heap)
}
문제 2: 프로세스 상태 전이¶
다음 상황에서 프로세스 상태 전이를 설명하세요.
- 프로세스 A가 CPU를 사용 중, 타임 슬라이스 만료
- 프로세스 B가 파일 읽기 요청
- 프로세스 C의 파일 읽기 완료
- 스케줄러가 프로세스 D를 선택
정답 보기
1. A: Running → Ready (타임아웃/인터럽트) 2. B: Running → Waiting (I/O 요청) 3. C: Waiting → Ready (I/O 완료) 4. D: Ready → Running (디스패치)문제 3: fork() 출력 예측¶
다음 코드의 출력 결과를 예측하세요.
#include <stdio.h>
#include <unistd.h>
int main() {
printf("A\n");
fork();
printf("B\n");
fork();
printf("C\n");
return 0;
}
정답 보기
A (1번 출력 - 최초 프로세스)
B (2번 출력 - 첫 번째 fork 후 2개 프로세스)
B
C (4번 출력 - 두 번째 fork 후 4개 프로세스)
C
C
C
총: A 1번, B 2번, C 4번
프로세스 분기:
main
│
┌─────┴─────┐
│ fork() │
│ │
main child1
│ │
┌──┴──┐ ┌──┴──┐
│fork()│ │fork()│
│ │ │ │
main c2 c1 c3
4개의 프로세스가 각각 "C" 출력
문제 4: PCB 정보¶
다음 상황에서 PCB에 저장되는 정보의 변화를 설명하세요.
- 프로세스가 Running에서 Waiting으로 전이할 때
- 컨텍스트 스위치가 발생할 때
정답 보기
1. Running → Waiting 전이: - PCB의 상태(state) 필드가 Running에서 Waiting으로 변경 - I/O 상태 정보에 대기 중인 I/O 작업 기록 - 프로세스가 대기 큐로 이동 2. 컨텍스트 스위치: - 현재 프로세스의 PC, 레지스터, 스택 포인터 등을 PCB에 저장 - 새 프로세스의 PCB에서 PC, 레지스터, 스택 포인터 등을 복원 - 메모리 관리 정보(페이지 테이블) 갱신 - 새 프로세스의 상태를 Running으로 변경문제 5: 컨텍스트 스위치 비용¶
컨텍스트 스위치의 직접 비용과 간접 비용을 각각 두 가지씩 설명하세요.
정답 보기
**직접 비용:** 1. 레지스터 저장/복원: 현재 프로세스의 레지스터를 PCB에 저장하고, 새 프로세스의 레지스터를 복원하는 시간 2. 커널 모드 전환: 사용자 모드에서 커널 모드로, 다시 사용자 모드로 전환하는 오버헤드 **간접 비용:** 1. 캐시 오염(Cache Pollution): 새 프로세스가 사용하는 데이터가 캐시에 없어서 캐시 미스 증가 2. TLB 플러시: 새 프로세스는 다른 가상 주소 공간을 사용하므로 TLB 항목이 무효화됨다음 단계¶
- 03_Threads_and_Multithreading.md - 스레드 개념과 멀티스레딩 모델