프로세스는 실행 중인 프로그램을 의미하며, 스레드는 프로세스 내에서 실행되는 작업 단위다. 이 글에서는 프로세스와 스레드의 개념, 메모리 구조, 그리고 프로세스 간 통신 방법을 다룬다.
1️⃣ 프로세스의 종류
메모리에는 컴퓨터가 실행되는 순간부터 다양한 프로세스가 적재된다. 프로세스는 실행 방식에 따라 다음과 같이 분류된다.
-
포그라운드 프로세스: 사용자가 보는 공간에서 사용자와 상호작용하며 실행되는 프로세스다.
-
백그라운드 프로세스: 사용자가 보지 못하는 곳에서 실행되는 프로세스다.
-
데몬(Daemon) / 서비스(Service) : 사용자와 별다른 상호작용 없이 주어진 작업만 수행하는 특별한 백그라운드 프로세스다. Windows에서는
서비스라고 부른다.
프로세스의 유형과 관계없이 하나의 프로세스를 구성하는 메모리 내의 정보는 크게 다르지 않다:
- 커널 영역:
PCB(Process Control Block)저장 - 사용자 영역: 코드 영역, 데이터 영역, 힙 영역, 스택 영역으로 구성
2️⃣ 프로세스의 메모리 구조
코드 영역 (Code Segment)
코드 영역은 실행 가능한 명령어가 저장되는 공간으로, 텍스트 영역(Text Segment)이라고도 한다.
- CPU가 읽고 실행할 명령어를 포함
- 읽기 전용(Read-only) 공간으로 쓰기가 금지됨
데이터 영역 (Data Segment)
데이터 영역은 프로그램이 실행되는 동안 유지할 데이터가 저장되는 공간이다.
- 정적 변수(Static Variable)
- 전역 변수(Global Variable)
💡 BSS 영역
데이터 영역은 초기화 여부에 따라 세부적으로 나뉜다:
- 데이터 영역: 초깃값이 있는 정적/전역 변수 저장
- BSS 영역: 초깃값이 없는 정적/전역 변수 저장
코드 영역과 데이터 영역은 프로그램 실행 도중 크기가 변하지 않기 때문에 정적 할당 영역이라고 한다.
힙 영역 (Heap Segment)
힙 영역은 프로그래머가 직접 할당 가능한 저장 공간이다.
🎯 특징
- 프로그램 실행 도중 비교적 자유롭게 할당하여 사용 가능
- 할당한 메모리 공간은 반드시 해제해야 함
- 메모리를 해제하지 않으면
메모리 누수(Memory Leak)발생
🗑️ 가비지 컬렉션(Garbage Collection)
일부 프로그래밍 언어는 사용되지 않는 힙 메모리를 자동으로 해제하는 가비지 컬렉션 기능을 제공한다.
스택 영역 (Stack Segment)
스택 영역은 일시적으로 사용할 값들이 저장되는 공간이다.
- 매개변수(Parameter)
- 지역 변수(Local Variable)
- 함수 복귀 주소(Return Address)
스택 트레이스 (Stack Trace)
스택 트레이스는 특정 시점에 스택 영역에 저장된 함수 호출 정보를 말한다. 문제의 발생 지점을 추적할 수 있어 디버깅에 매우 유용하다.
자바 스택 트레이스 예시
Exception in thread "main" java.lang.NullPointerException
at com.example.myproject.Test.getThis(Test.java:16)
at com.example.myproject.Test2.getThat(Test2.java:25)
at com.example.myproject.Bootstrap.main(Bootstrap.java:14)힙 영역과 스택 영역은 크기가 변할 수 있기 때문에 동적 할당 영역이라고 한다.
3️⃣ PCB와 문맥 교환
PCB (Process Control Block)
운영체제가 메모리에 적재된 다수의 프로세스를 관리하려면 프로세스를 식별할 수 있는 정보가 필요하다. 이 정보가 바로 PCB다.
🔑 PCB의 특징
- 프로세스와 관련한 다양한 정보를 담는 구조체
- 새로운 프로세스가 생성될 때 커널 영역에 만들어짐
- 프로세스 실행이 끝나면 폐기됨
3-1 PCB에 담기는 정보
| 정보 종류 | 설명 |
|---|---|
| PID (Process ID) | 프로세스 식별 번호 |
| 레지스터 값 | 프로세스가 실행 과정에서 사용한 레지스터 값 |
| 프로세스 상태 | 현재 프로세스의 상태 (생성, 준비, 실행, 대기, 종료) |
| CPU 스케줄링 정보 | 프로세스의 우선순위, 스케줄링 큐 포인터 등 |
| 메모리 관련 정보 | 프로세스의 메모리상 적재 위치 |
| 파일 및 I/O 정보 | 프로세스가 사용한 파일 및 입출력장치 관련 정보 |
3-2 리눅스의 PCB 구조
리눅스 운영체제의 PCB는 task_struct라는 구조체로 구현된다:
// filepath: task_struct 구조체 예시
struct task_struct {
pid_t pid; // PID
int prio; // 스케줄링(우선순위) 관련 정보
unsigned int state; // 프로세스 상태 관련 정보
struct mm_struct *mm; // 메모리 관련 정보
void *stack; // 스택 관련 정보
struct files_struct *files; // 파일 관련 정보
}💡 프로세스 테이블 (Process Table)
여러 PCB는 커널 내에서
프로세스 테이블형태로 관리된다:
- 새로운 프로세스 생성 시: PCB를 프로세스 테이블에 추가
- 프로세스 종료 시: 자원 해제 후 PCB를 프로세스 테이블에서 삭제
좀비 프로세스(Zombie Process): 프로세스가 비정상 종료되어 자원이 회수되었음에도 PCB가 프로세스 테이블에 남아 있는 상태
💡 오픈 소스 운영체제, 리눅스
리눅스(Linux)는 소스 코드가 공개된 오픈 소스 운영체제다:
- 안드로이드 등 다양한 운영체제에 영향
- 많은 서버 컴퓨터 환경에서 활용
- 소스 코드 확인: https://kernel.org/
문맥 교환 (Context Switching)
메모리에 적재된 프로세스는 한정된 시간 동안 번갈아 가며 실행된다. 이는 운영체제가 CPU 자원을 번갈아 가며 할당하기 때문이다.
3-3 타이머 인터럽트 (Timer Interrupt)
타이머 인터럽트는 프로세스의 CPU 사용 시간을 제한하는 인터럽트로, 타임아웃 인터럽트(Timeout Interrupt)라고도 한다.
- 프로세스는 정해진 시간만큼 CPU를 사용
- 타이머 인터럽트 발생 시 다음 프로세스에게 CPU를 양보
3-4 문맥 (Context)
문맥은 프로세스의 수행을 재개하기 위해 기억해야 할 정보를 의미한다:
- 프로그램 카운터를 비롯한 각종 레지스터 값
- 메모리 정보
- 실행을 위해 열었던 파일
- 사용한 입출력장치 등
프로세스의 문맥은 해당 프로세스의 PCB에 명시된다.
3-5 문맥 교환의 동작 과정
문맥 교환은 기존 프로세스의 문맥을 PCB에 백업하고, 새로운 프로세스의 문맥을 PCB에서 복구하여 실행하는 과정이다:
- 프로세스 A 실행 중 타이머 인터럽트 발생
- 프로세스 A의 문맥을 PCB에 백업
- 프로세스 B의 문맥을 PCB에서 복구
- 프로세스 B 실행
- 타이머 인터럽트 발생 시 과정 반복
⚠️ 문맥 교환의 오버헤드
프로세스 간 너무 잦은 문맥 교환은 다음과 같은 문제를 발생시킨다:
- 캐시 미스(Cache Miss) 발생 가능성 증가
- 메모리로부터 프로세스 내용을 가져오는 작업 빈번
- 큰 오버헤드로 이어질 수 있음
4️⃣ 프로세스의 상태
하나의 프로세스는 여러 상태를 거치며 실행된다. 운영체제는 PCB를 통해 프로세스의 상태를 인식하고 관리한다.
프로세스 상태 다이어그램
[생성(new)] → [준비(ready)] → [실행(running)] → [종료(terminated)]
↑ ↓
└──[대기(blocked)]4-1 생성 상태 (New)
- 프로세스를 생성 중인 상태
- 메모리에 적재되어 PCB를 할당받은 상태
- 실행 준비가 완료되면 준비 상태로 전환
4-2 준비 상태 (Ready)
- CPU를 할당받아 실행할 수 있지만 차례를 기다리는 상태
- CPU를 할당받으면 실행 상태로 전환
- 준비 상태에서 실행 상태로 전환되는 것을
디스패치(Dispatch)라고 함
4-3 실행 상태 (Running)
- CPU를 할당받아 실행 중인 상태
- 일정 시간 동안만 CPU를 사용할 수 있음
- 타이머 인터럽트 발생 시 준비 상태로 전환
- 입출력 작업 요청 시 대기 상태로 전환
4-4 대기 상태 (Blocked)
- 입출력 작업을 요청하거나 바로 확보할 수 없는 자원을 요청하여 실행이 불가능한 상태
- 입출력 작업이 완료되면 준비 상태로 전환
4-5 종료 상태 (Terminated)
- 프로세스가 종료된 상태
- 운영체제는 PCB와 프로세스가 사용한 메모리를 정리
💡 블로킹 입출력 vs 논블로킹 입출력
블로킹 입출력(Blocking I/O)
- 입출력 작업 완료까지 대기 상태로 접어드는 방식
- 입출력 작업이 완료되면 준비 상태로 전환하여 실행 재개
논블로킹 입출력(Non-blocking I/O)
- 입출력 작업을 입출력장치에 맡긴 뒤 곧바로 다음 명령어 실행
- 실행 결과를 기다리지 않음
예를 들어, 네트워크를 통해 메시지를 보내는 시스템 콜 호출 시:
- 블로킹 I/O: 송신 작업 완료를 확인할 때까지 대기
- 논블로킹 I/O: 송신 작업을 맡기고 곧바로 다음 명령 수행
5️⃣ 멀티프로세스와 멀티스레드
멀티프로세스 (Multiprocess)
멀티프로세스는 동시에 여러 프로세스가 실행되는 것을 말한다. 같은 프로그램을 각기 다른 여러 프로세스로 생성하여 실행한다.
🌐 웹 브라우저의 탭
웹 브라우저는 일반적으로 하나의 탭마다 하나의 프로세스로 동작한다. Windows의 작업 관리자에서 크롬 브라우저를 열면 탭마다 별도의 프로세스가 실행되는 것을 확인할 수 있다.
🔒 멀티프로세스의 특징
- 각 프로세스는 자원을 공유하지 않고 독립적으로 실행
- 각각 다른 PID를 가짐
- 프로세스별로 파일과 입출력장치 등의 자원이 독립적으로 할당
- 한 프로세스에 문제가 발생해도 다른 프로세스에 직접적인 영향이 적음
멀티스레드 (Multithread)
멀티스레드는 하나의 프로세스 내에서 여러 스레드가 동시에 실행되는 것을 말한다.
5-1 스레드의 구성 요소
하나의 스레드는 다음으로 구성된다:
- 스레드 ID (Thread ID)
- 프로그램 카운터 (Program Counter)
- 레지스터 값 (Register)
- 스택 (Stack)
스레드마다 각각의 프로그램 카운터와 스택을 가지므로:
- 스레드마다 다음에 실행할 주소를 가질 수 있음
- 연산 과정의 임시 저장 값을 가질 수 있음
5-2 멀티프로세스 vs 멀티스레드
자원 공유의 차이
| 구분 | 멀티프로세스 | 멀티스레드 |
|---|---|---|
| 자원 공유 | 자원을 공유하지 않음 | 프로세스의 자원을 공유 |
| 독립성 | 독립적으로 실행 | 코드, 데이터, 힙 영역 공유 |
| 통신/협력 | 어려움 | 쉬움 |
| 장애 격리 | 한 프로세스 문제가 다른 프로세스에 영향 적음 | 한 스레드 문제가 프로세스 전체에 영향 |
🤝 멀티스레드의 장점
- 같은 프로세스 내 스레드들은 코드, 데이터, 힙 영역을 공유
- 열린 파일과 같은 프로세스 자원을 공유
- 쉽게 협력하고 통신할 수 있음
⚠️ 멀티스레드의 단점
- 한 스레드에 생긴 문제가 프로세스 전체의 문제가 될 수 있음
멀티스레드 예제
다음은 Python으로 작성한 멀티스레드 예제다:
# filepath: os/multiprocessing.py
import threading
import os
def foo():
pid = os.getpid() # 현재 프로세스의 PID 반환
tid = threading.get_native_id() # 현재 스레드의 ID 반환
print(f"foo: PID={pid}, Thread ID={tid}")
def bar():
pid = os.getpid()
tid = threading.get_native_id()
print(f"bar: PID={pid}, Thread ID={tid}")
def baz():
pid = os.getpid()
tid = threading.get_native_id()
print(f"baz: PID={pid}, Thread ID={tid}")
if __name__ == '__main__':
thread1 = threading.Thread(target=foo) # 첫 번째 스레드 생성
thread2 = threading.Thread(target=bar) # 두 번째 스레드 생성
thread3 = threading.Thread(target=baz) # 세 번째 스레드 생성
thread1.start() # 첫 번째 스레드 실행
thread2.start() # 두 번째 스레드 실행
thread3.start() # 세 번째 스레드 실행실행 결과
foo: PID=5113, TID=2149548
bar: PID=5113, TID=2149549
baz: PID=5113, TID=21495503개의 스레드가 실행하는 각기 다른 함수를 통해 출력되는 PID 값은 같지만, 스레드 ID는 다르다. 스레드들이 같은 프로세스를 공유하기 때문이다.
💡 스레드 조인 (Thread Join)
join()은 스레드를 생성한 주체가 생성/실행된 스레드가 종료될 때까지 대기해야 함을 의미한다.C++ 공식 문서
void join(); // Join thread // The function returns when the thread execution has completed.Python 공식 문서
join(timeout=None) # 스레드가 종료할 때까지 기다립니다.예제 코드에 join 추가
thread1.join() # thread1 종료까지 대기 thread2.join() # thread2 종료까지 대기 thread3.join() # thread3 종료까지 대기
6️⃣ 프로세스 간 통신 (IPC)
프로세스는 기본적으로 자원을 공유하지 않지만, IPC(Inter-Process Communication)를 통해 자원을 공유하고 데이터를 주고받을 수 있다.
IPC 방식은 크게 두 가지로 나뉜다:
- 공유 메모리 (Shared Memory)
- 메시지 전달 (Message Passing)
공유 메모리 (Shared Memory)
공유 메모리는 프로세스 간에 공유하는 메모리 영역을 통해 데이터를 주고받는 통신 방식이다.
6-1 동작 원리
- 특정 메모리 공간을 두 개 이상의 프로세스가 공유
- 프로세스 A는 공유 메모리 공간에 데이터를 쓰기
- 프로세스 B는 해당 메모리 공간을 읽기
- 결과적으로 프로세스 간 데이터 공유
📁 파일 기반 공유 메모리 예시
프로세스 A: hi.txt 파일을 수정하는 프로세스
프로세스 B: hi.txt 파일을 읽는 프로세스
→ 두 프로세스는 hi.txt를 매개로 통신6-2 공유 메모리의 특징
✅ 장점
- 각 프로세스가 자신의 메모리 영역을 읽고 쓰는 것처럼 통신
- 커널의 개입이 거의 없음 (데이터가 커널 영역을 거치지 않음)
- 통신 속도가 빠름
⚠️ 단점
- 여러 프로세스가 동시에 공유 메모리 영역을 읽고 쓸 경우 데이터 일관성 훼손 가능
레이스 컨디션(Race Condition)문제 발생 가능
메시지 전달 (Message Passing)
메시지 전달은 프로세스 간에 주고받을 데이터가 커널을 거쳐 송수신되는 통신 방식이다.
6-3 동작 원리
- 메시지를 보내는 수단과 받는 수단이 명확하게 구분
send()시스템 콜: 메시지 전송recv()시스템 콜: 메시지 수신
6-4 메시지 전달의 특징
✅ 장점
- 커널의 도움을 적극적으로 받을 수 있음
- 레이스 컨디션, 동기화 문제를 고려하는 일이 상대적으로 적음
- 안전한 통신 가능
⚠️ 단점
- 주고받는 데이터가 커널을 통해 송수신
- 공유 메모리 기반 IPC보다 통신 속도가 느림
메시지 전달 방식의 종류
6-5 파이프 (Pipe)
파이프는 단방향 프로세스 간 통신 도구다.
- 프로세스 A가 파이프 한쪽에서 데이터를 쓰기
- 프로세스 B가 파이프 반대쪽에서 데이터를 읽기
- 먼저 삽입된 데이터가 먼저 읽힘 (FIFO)
📊 파이프의 종류
| 종류 | 설명 |
|---|---|
| 익명 파이프 (Unnamed Pipe) | 단방향 통신만 지원, 부모-자식 프로세스 간 통신만 가능 |
| 지명 파이프 (Named Pipe) | 양방향 통신 지원, 임의의 프로세스 간 통신 가능 (FIFO) |
6-6 시그널 (Signal)
시그널은 프로세스에게 특정 이벤트가 발생했음을 알리는 비동기적인 신호다.
⚡ 리눅스의 대표적인 시그널
| 시그널 | 설명 | 기본 동작 |
|---|---|---|
| SIGCHLD | 자식 프로세스 종료 | 무시 |
| SIGILL | 허용하지 않은 명령어 실행 | 코어 덤프 생성 후 종료 |
| SIGINT | 키보드 인터럽트 (Ctrl + C) | 종료 |
| SIGKILL | 프로세스 종료 (핸들러 재정의 불가) | 종료 |
| SIGSEGV | 잘못된 메모리 접근 | 코어 덤프 생성 후 종료 |
| SIGTERM | 프로세스 종료 (핸들러 재정의 가능) | 종료 |
| SIGUSR1, SIGUSR2 | 사용자 정의 시그널 | 종료 |
🔔 시그널 처리 과정
- 프로세스는 시그널 발생 시 하던 일을 잠시 중단
시그널 핸들러(Signal Handler)실행- 실행 재개
프로세스는 직접 특정 시그널을 발생시킬 수 있고, 일부 시그널 핸들러를 재정의할 수 있다.
Python 시그널 처리 예시
# filepath: signal 모듈 사용 예시
import signal
def signal_handler(signum, frame):
print(f"Signal {signum} received")
# SIGINT 시그널에 대한 핸들러 등록
signal.signal(signal.SIGINT, signal_handler)💡 코어 덤프 (Core Dump)
코어 덤프는 주로 비정상적으로 종료하는 경우에 생성되는 파일로, 프로그램이 특정 시점에 작업하던 메모리 상태가 기록되어 있다. 디버깅 용도로 매우 유용하다.코어 덤프 생성 예제
# filepath: os/coredumped.py import ctypes def bug(): ctypes.string_at(0) # 잘못된 메모리 접근 bug()실행 결과
$ python3 coredumped.py Segmentation fault (core dumped)코어 덤프 파일 확인
$ coredumpctl info PID: 7990 (python3) Signal: 11 (SEGV) Timestamp: Mon 2024-02-26 15:40:36 KST Command Line: python3 coredumped.py Executable: /usr/bin/python3.8 Storage: /var/lib/systemd/coredump/core.python3.1000...
6-7 소켓 (Socket)
네트워크 소켓을 통해 IPC를 수행할 수 있다.
6-8 원격 프로시저 호출 (RPC)
RPC(Remote Procedure Call)는 원격 코드를 실행하는 IPC 기술이다.
🌐 RPC의 특징
- 한 프로세스 내의 코드 실행: 로컬 프로시저 호출
- 다른 프로세스의 원격 코드 실행: 원격 프로시저 호출
- 프로그래밍 언어나 플랫폼과 무관하게 메시지 송수신 가능
- 성능 저하 최소화
- 대규모 트래픽 처리 환경, 서버 간 통신 환경에서 주로 사용
⚙️ RPC 프레임워크
대표적인 RPC 프레임워크는 구글의 gRPC다.
7️⃣ 정리
프로세스와 스레드의 핵심 개념을 정리하면 다음과 같다:
프로세스
- 실행 중인 프로그램
- 코드, 데이터, 힙, 스택 영역으로 구성
- PCB를 통해 운영체제가 관리
- 문맥 교환을 통해 여러 프로세스가 번갈아 가며 실행
스레드
- 프로세스 내 실행 단위
- 프로세스의 자원을 공유
- 각자의 스택과 프로그램 카운터를 가짐
IPC
- 공유 메모리: 빠르지만 동기화 문제 발생 가능
- 메시지 전달: 안전하지만 상대적으로 느림
- 파이프, 시그널, 소켓, RPC 등 다양한 방식 존재