[운영체제] 2. 프로세스와 스레드

index

프로세스는 실행 중인 프로그램을 의미하며, 스레드는 프로세스 내에서 실행되는 작업 단위다. 이 글에서는 프로세스와 스레드의 개념, 메모리 구조, 그리고 프로세스 간 통신 방법을 다룬다.

1️⃣ 프로세스의 종류

메모리에는 컴퓨터가 실행되는 순간부터 다양한 프로세스가 적재된다. 프로세스는 실행 방식에 따라 다음과 같이 분류된다.

  1. 포그라운드 프로세스: 사용자가 보는 공간에서 사용자와 상호작용하며 실행되는 프로세스다.

  2. 백그라운드 프로세스: 사용자가 보지 못하는 곳에서 실행되는 프로세스다.

  3. 데몬(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에서 복구하여 실행하는 과정이다:

  1. 프로세스 A 실행 중 타이머 인터럽트 발생
  2. 프로세스 A의 문맥을 PCB에 백업
  3. 프로세스 B의 문맥을 PCB에서 복구
  4. 프로세스 B 실행
  5. 타이머 인터럽트 발생 시 과정 반복

⚠️ 문맥 교환의 오버헤드

프로세스 간 너무 잦은 문맥 교환은 다음과 같은 문제를 발생시킨다:

  • 캐시 미스(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=2149550

3개의 스레드가 실행하는 각기 다른 함수를 통해 출력되는 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 사용자 정의 시그널 종료

🔔 시그널 처리 과정

  1. 프로세스는 시그널 발생 시 하던 일을 잠시 중단
  2. 시그널 핸들러(Signal Handler) 실행
  3. 실행 재개

프로세스는 직접 특정 시그널을 발생시킬 수 있고, 일부 시그널 핸들러를 재정의할 수 있다.

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 등 다양한 방식 존재
© 2026, Design & Developed by 정인영