icon

안동민 개발노트

3장 : 프로세스

PCB와 컨텍스트 스위칭


OS는 수십, 수백 개의 프로세스를 동시에 관리합니다. 각 프로세스의 상태, CPU 레지스터 값, 메모리 정보, 열린 파일 목록을 어딘가에 기록해야 합니다. 프로세스 A에서 B로 전환한 후, 다시 A로 돌아왔을 때 A가 이전에 실행하던 그 지점에서 정확히 이어서 실행되어야 하니까요. 이 정보를 담는 자료구조가 PCB(Process Control Block)이고, 프로세스를 전환하는 과정이 컨텍스트 스위칭(Context Switching)입니다.


PCB의 구조

PCB는 OS가 프로세스를 관리하기 위해 유지하는 커널 수준의 데이터 구조입니다. 프로세스가 생성될 때 PCB가 할당되고, 종료될 때 PCB가 해제됩니다. PCB는 프로세스의 신분증이자 상태 기록부입니다.

Linux 커널에서는 task_struct라는 거대한 구조체가 이 역할을 합니다. Linux 커널 소스의 include/linux/sched.h에 정의되어 있으며, 수백 개의 필드를 가지고 있습니다. 크기가 수 KB에 달하는 복잡한 구조체입니다.

PCB에 저장되는 주요 정보를 범주별로 정리하면 다음과 같습니다.

식별 정보

  • PID(Process ID): 프로세스를 고유하게 식별하는 양의 정수. Linux에서 기본 최대값은 32768이며, /proc/sys/kernel/pid_max로 확인하고 변경할 수 있습니다.
  • PPID(Parent PID): 이 프로세스를 생성한 부모 프로세스의 PID.
  • UID/GID: 프로세스를 실행한 사용자(User)와 그룹(Group)의 ID. 파일 접근 권한, 시그널 전송 권한 등을 결정합니다.

CPU 상태 정보

  • 프로그램 카운터(PC/IP): 이 프로세스가 다음에 실행할 명령어의 메모리 주소.
  • CPU 레지스터 값: 범용 레지스터(RAX, RBX, RCX, ...), 스택 포인터(RSP), 베이스 포인터(RBP), 플래그 레지스터(RFLAGS) 등의 현재 값. 컨텍스트 스위칭 시 이 값들이 저장되고 복원됩니다.
  • FPU/SIMD 상태: 부동소수점 레지스터, SSE/AVX 레지스터의 값. 멀티미디어나 과학 계산 프로그램에서 중요합니다.

스케줄링 정보

  • 프로세스 상태: Ready, Running, Waiting, Stopped, Zombie 등.
  • 우선순위: 스케줄러가 CPU 할당 순서를 결정하는 데 사용하는 값. Linux에서는 nice 값(-2019)과 실시간 우선순위(099)가 있습니다.
  • CPU 사용 시간: 사용자 모드와 커널 모드에서 소비한 시간을 각각 기록합니다.

메모리 관리 정보

  • 페이지 테이블 포인터: 가상 주소를 물리 주소로 변환하는 페이지 테이블의 위치. x86에서는 CR3 레지스터에 저장됩니다.
  • 메모리 영역 정보: 텍스트, 데이터, 힙, 스택 각 영역의 시작/끝 주소와 접근 권한.

I/O 및 파일 정보

  • 파일 디스크립터 테이블: 프로세스가 열어 놓은 모든 파일에 대한 참조. 0(stdin), 1(stdout), 2(stderr)은 기본으로 열려 있습니다.
  • 현재 작업 디렉토리: pwd 명령어로 보는 경로.
  • 시그널 관련 정보: 등록된 시그널 핸들러, 대기 중인 시그널, 차단된 시그널 마스크.

Linux에서 프로세스의 PCB 정보를 확인하는 가장 쉬운 방법은 /proc/<PID>/ 디렉토리입니다. 이 가상 파일 시스템은 커널이 실시간으로 생성하는 정보를 파일 형태로 제공합니다.

프로세스 PCB 정보 확인
ls /proc/1/
# cmdline  environ  fd  maps  status  stat  ...

cat /proc/1/status
# Name:   systemd
# State:  S (sleeping)
# Pid:    1
# PPid:   0
# Uid:    0  0  0  0
# VmSize: 173048 kB
# VmRSS:  12940 kB
# Threads: 1

ls -l /proc/1/fd/
# 0 -> /dev/null (stdin)
# 1 -> /dev/null (stdout)
# 2 -> /dev/null (stderr)
# 3 -> socket:[12345]
# 4 -> /var/log/journal/...

VmSize는 프로세스의 가상 메모리 총 크기, VmRSS는 실제 물리 메모리에 상주하는 크기(Resident Set Size)입니다. top이나 ps에서 보는 메모리 사용량이 바로 이 값입니다.


컨텍스트 스위칭

컨텍스트 스위칭(Context Switching)은 CPU에서 실행 중인 프로세스를 다른 프로세스로 교체하는 과정입니다. "컨텍스트"란 프로세스가 실행을 재개하는 데 필요한 모든 CPU 상태(레지스터 값, PC, 스택 포인터 등)를 의미합니다.

프로세스 A가 실행 중인데, 타이머 인터럽트가 발생하여 프로세스 B로 전환해야 하는 상황을 단계별로 따라가 보겠습니다.

  1. 인터럽트 발생: 타이머 인터럽트가 CPU에 전달됩니다. CPU가 커널 모드로 전환됩니다.

  2. 현재 컨텍스트 저장: CPU의 현재 레지스터 값(PC, 범용 레지스터, 스택 포인터, 플래그 등)을 프로세스 A의 PCB(task_struct)에 저장합니다. 이 시점에서 A의 스냅샷이 PCB에 캡처됩니다.

  3. 스케줄링 결정: 커널의 스케줄러가 Ready Queue를 확인하여 다음에 실행할 프로세스(B)를 결정합니다. 5장에서 이 결정 알고리즘을 다룹니다.

  4. 메모리 공간 전환: B의 페이지 테이블로 전환합니다. x86에서는 CR3 레지스터에 B의 페이지 테이블 주소를 로드합니다. 이 순간 가상 주소 공간이 A의 것에서 B의 것으로 바뀝니다.

  5. 새 컨텍스트 복원: B의 PCB에서 저장된 레지스터 값들을 CPU에 복원합니다. PC가 B가 마지막으로 실행하던 명령어 주소로 설정됩니다.

  6. 사용자 모드 전환: 커널 모드에서 사용자 모드로 전환하여 프로세스 B의 실행을 재개합니다.

A의 입장에서는 마치 잠깐 멈추었다가 이어서 실행되는 것처럼 보입니다. 컨텍스트 스위칭이 발생했다는 사실 자체를 알지 못합니다. 이것이 가능한 이유는 PCB에 모든 상태가 완벽하게 저장되고 복원되기 때문입니다.


컨텍스트 스위칭의 오버헤드

컨텍스트 스위칭은 순수한 오버헤드입니다. 전환하는 동안 CPU는 사용자의 어떤 작업도 수행하지 않습니다. 오버헤드는 크게 두 가지입니다.

직접적 오버헤드

레지스터를 저장하고 복원하는 시간, 스케줄링 결정에 걸리는 시간입니다. 일반적으로 한 번의 컨텍스트 스위칭에 수 마이크로초(μs)가 걸립니다. 현대 하드웨어에서는 1~10μs 정도입니다.

간접적 오버헤드 (캐시 오염)

이것이 더 큰 문제입니다. 프로세스 A가 실행되면서 L1/L2/L3 캐시에 A의 데이터가 채워져 있습니다. B로 전환하면 B의 데이터가 캐시에 올라오면서 A의 데이터가 밀려납니다. 나중에 A로 돌아오면, 캐시에 A의 데이터가 없으므로 캐시 미스(Cache Miss)가 연쇄적으로 발생합니다. 이 워밍업 비용이 직접적 오버헤드보다 수십 배 클 수 있습니다.

또한 페이지 테이블 전환 시 TLB(Translation Lookaside Buffer)가 무효화됩니다. TLB는 가상→물리 주소 변환의 캐시인데, 프로세스마다 페이지 테이블이 다르므로 전환 시 TLB를 비워야 합니다. 이후 메모리 접근마다 TLB 미스가 발생하여 페이지 테이블을 다시 탐색해야 합니다. 이 오버헤드를 줄이기 위해 현대 CPU는 ASID(Address Space Identifier)를 사용하여 TLB 항목에 프로세스 태그를 달아, 전환 시 TLB를 완전히 비우지 않아도 되게 합니다.

이 오버헤드들이 누적되면 체감 성능에 영향을 줍니다. 초당 1000번의 컨텍스트 스위칭이 발생하고, 한 번에 10μs가 걸린다면, CPU 시간의 1%가 컨텍스트 스위칭에만 소모됩니다. 캐시/TLB 오염까지 합치면 5~10%에 달할 수 있습니다.

이 때문에 스레드가 등장합니다. 같은 프로세스 내의 스레드 간 전환은 메모리 공간(페이지 테이블)을 바꿀 필요가 없으므로, TLB가 유효하게 유지되고 캐시 오염도 줄어들어 프로세스 간 전환보다 훨씬 가볍습니다. 4장에서 자세히 다룹니다.

컨텍스트 스위칭 횟수 확인
# 시스템 전체 컨텍스트 스위칭 횟수
vmstat 1
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
#  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
#  1  0      0 3241284 145832 1284776  0    0     0     0  234  512 2  1  97  0  0
#                                                          ^^^  ^^^
#                                                          인터럽트 컨텍스트스위칭

# 특정 프로세스의 컨텍스트 스위칭 확인
cat /proc/<PID>/status | grep ctxt
# voluntary_ctxt_switches:    1523   (자발적: I/O 대기 등)
# nonvoluntary_ctxt_switches: 847    (비자발적: 타이머에 의한 선점)

voluntary_ctxt_switches는 프로세스가 I/O 등으로 자발적으로 CPU를 양보한 횟수이고, nonvoluntary_ctxt_switches는 타이머 인터럽트에 의해 강제로 빼앗긴 횟수입니다. 비자발적 전환이 매우 많다면 CPU 경쟁이 심하다는 의미입니다.


프로세스 큐

OS는 프로세스들을 여러 큐(Queue)로 조직합니다. 큐는 프로세스의 현재 상태에 따라 분류됩니다.

Ready Queue(준비 큐): CPU를 사용할 준비가 된 프로세스들을 담은 큐입니다. 스케줄러가 이 큐에서 다음에 실행할 프로세스를 선택합니다. 실제로는 단순한 FIFO 큐가 아니라, 우선순위 큐, 멀티레벨 큐 등 다양한 구조가 사용됩니다. Linux의 CFS(Completely Fair Scheduler)는 레드-블랙 트리를 사용하여 O(logN)O(\log N)에 다음 프로세스를 선택합니다.

Wait Queue(대기 큐): 특정 이벤트(디스크 I/O 완료, 네트워크 패킷 도착, 시그널 수신 등)를 기다리는 프로세스들의 목록입니다. 이벤트의 종류별로 별도의 큐가 존재합니다. 디스크 I/O를 기다리는 프로세스들의 큐, 네트워크 I/O를 기다리는 큐가 각각 있습니다. 이벤트가 발생하면 해당 큐의 프로세스(들)를 Ready Queue로 옮깁니다.

큐의 구현과 스케줄러의 알고리즘에 따라, 어떤 프로세스가 먼저 CPU를 받는지, 얼마나 오래 실행하는지, 얼마나 빨리 응답하는지가 결정됩니다. 이것이 5장에서 다룰 CPU 스케줄링의 핵심입니다.

다음 절에서는 프로세스가 어떻게 생성되고 종료되는지, fork()exec()의 실제 동작을 살펴보겠습니다.

목차