앞선 글에서 운영체제의 핵심 역할로 자원 관리, 자원 보호, 하드웨어/사용자 인터페이스 제공 등을 살펴봤다.
이 중에서도 CPU와 메모리 같은 컴퓨터의 핵심 자원을 여러 프로그램이 안전하고 효율적으로 사용하도록 관리하는 것이 운영체제의 가장 중요한 일 중 하나다.
여기서 등장하는 것이 바로 프로세스와 스레드다.
운영체제는 우리가 실행하는 각각의 프로그램(예: 웹브라우저, 음악플레이어, 에디터 등)을 ‘프로세스’라는 단위로 관리한다.
또한, 현대 소프트웨어에서는 한 프로그램 안에서도 여러 작업을 동시에 처리할 필요가 있어,
운영체제는 ‘스레드’라는 더 작은 실행 단위를 제공한다.
이 글에서는
프로세스와 스레드가 무엇인지
운영체제가 왜 이 두 가지 단위로 자원을 관리하는지
CPU 스케줄링, 메모리 관리, 자원 보호와 프로세스/스레드가 어떻게 연결되는지
차근차근 풀어볼 예정이다.
프로세스란 간단히 말하면 실행중인 프로그램이다.
하지만 그냥 프로그램이 아니라 운영체제로부터 PCB(프로세스 제어 블록)을 얻은 프로그램이다.
그렇다면 PCB란 뭘까?
프로세스 제어 블록에는 대표적으로 3가지 정보가 있다.
1. PID(프로세스 구분자) : 각 프로세스를 구분하는 구분자
2. 메모리 관련 정보: 프로세스의 메모리 위치 정보, 메모리 보호를 위한 경계 레지스터와 한계 레지스터
3. 각종 중간값(프로그램 카운터, 레지스터 값, 메모리 정보 등): 프로세스의 진행 도중의 상태 정보(다시 시작될때 이어가기 위함)
그 외에도 프로세스 제어 블록에는 더 많은 정보가 있지만, 모두 다루진 않겠다.
경계 레지스터와 한계 레지스터?
경계 레지스터와 한계 레지스터는 운영체제가 프로세스의 메모리 보호를 위해 메모리의 시작주소와 메모리의 공간의 크기를 저장하는 레지스터이다.
운영체제는 두 레지스터를 함께 써서, 프로세스가 자기 메모리 범위만 안전하게 접근하도록 운영체제가 감시한다.
그렇게 PCB블록을 얻고 프로세스가 되면 프로세스는 어떤 과정을 거치면서 완료하는 걸까?
프로세스의 상태를 크게 다섯가지 상태로 나눌수 있다.
프로세스의 다섯 가지 상태를 보기전에 이해를 돕기 위해 단계를 나눠보겠다.
각 단계는 CPU 스케줄러에 의해 통제 받는다.
1. CPU 스케줄러가 준비 상태에 있는 여러 프로세스 중 다음에 실행할 프로세스를 선정한다.
2. 준비 상태의 프로세스 중 하나를 골라 실행 상태로 바꾼다.
3. 프로세스가 자신에게 주어진 하나의 타임 슬라이스 동안 작업을 끝내지 못하면 다시 준비 상태로 돌아간다.
프로세스의 네 가지 상태에 추가로 대기상태가 추가 됐다.
대기 상태란 실행 상태의 프로세스가 입출력을 요청하면 입출력이 완료될 때까지 기다리는 상태다. 이는 작업의 효율을 높이기 위해 만들어진 것이다.
표로 보는 프로세스 작업 상태
기본적으로는 준비(Ready), 실행(Running), 대기(Waiting), 종료(Terminated)의 네 가지 상태, 그리고 대기(입출력 등)로 인한 “다섯 가지 상태”가 핵심이다.
하지만 실제 운영체제에서는,
특정 프로세스가 외부 요인(예: 메모리 부족, 관리자가 일시 중지 등)으로 인해 “일시적으로 완전히 중단”되는 상황도 존재한다.
이런 경우, 프로세스는 “보류(Suspended, 휴식) 상태”로 추가 관리된다.
참고로 활성 상태와 보류 상태의 차이는 메모리 위에 올라왔는가 아닌가로 구분하면 된다.
그렇다면 여러 프로세스가 번갈아가며 실행되는 환경에서,
운영체제는 어떻게 각 프로세스의 작업을 중단했다가 다시 이어서 실행할 수 있을까?
여기서 사용되는 개념이 바로 문맥 교환이다.
문맥교환이란 운영체제가 현재 실행 중인 프로세스의 상태(문맥)를 PCB에 저장하고,
다른 프로세스의 PCB에 기록된 정보를 다시 CPU에 복원해서
새로운 프로세스가 끊김 없이 실행을 이어가도록 하는 과정을 말한다.
위에서 PCB의 주요 역할중에 각종 중간값(프로그램 카운터, 레지스터 값, 메모리 정보 등)을 저장하는 역할에 대해 언급 했는데, 그 각종 중간값의 개념이 이때 사용된다.
각종 프로세스의 마지막 값을 기억하고, 다시 실행될 때 그 정보를 불러와서 그 지점부터 작업을 계속 이어간다.
타임 슬라이스의 크기의 기준으로 문맥 교환을 진행하기 때문에 타임 슬라이스의 크기를 적절하게 설정하는것이 중요하다.
그렇다면 프로세스의 구조는 어떻게 나뉘어져 있을까?
malloc
, new
등)하거나 해제해야 함이렇게 프로세스는 각각의 영역을 나눠서 프로그램 실행에 필요한 다양한 정보를 저장한다.
운영체제에서 새로운 프로세스를 생성하는 대표적인 방식이 바로 fork()와 exec() 시스템 콜이다.
fork()
는 현재 실행 중인 프로세스를 “복제”해서exec()
는 실행 중인 프로세스의 메모리(코드, 데이터, 스택 등)를fork()
로 자식 프로세스를 만든 뒤,exec()
를 호출해서이 두 가지 시스템 콜을 조합하면
운영체제는 동시에 여러 프로그램을 독립적으로 실행할 수 있게
프로세스를 효과적으로 생성·관리할 수 있다.
스레드는 프로세스 내에서 실제로 작업을 수행하는 실행의 흐름(작업의 최소 단위)이다.
즉 정리하면
• 운영체제 입장에서의 작업 단위는 프로세스
• CPU 입장에서의 작업 단위는 스레드
스레드는 프로세스 내에서 실제로 작업을 수행하는 실행의 흐름(작업의 최소 단위)입니다.
CPU가 처리할 실제 "일"을 의미하며, 한 프로세스 안에 여러 개의 스레드가 존재할 수 있습니다.
기본적으로 스레드와 프로세스는 모두 운영체제에서 작업을 수행하는 실행 단위 이지만, 구조와 자원 관리 방식, 실제 동작 방식에서 차이가 있다.
구분 | 프로세스(Process) | 스레드(Thread) |
---|---|---|
정의 | 실행 중인 프로그램, 자원을 독립적으로 가지는 단위 | 프로세스 내에서 실행되는 더 작은 실행 단위 |
자원 | 각각 별도의 메모리(코드, 데이터, 힙, 스택)와 PCB 보유 | 코드, 데이터, 힙 등은 프로세스와 공유, 스택만 따로 가짐 |
독립성 | 완전히 독립적 (다른 프로세스에 영향 X) | 같은 프로세스 내 스레드끼리는 서로 영향을 줄 수 있음 |
통신 | 프로세스 간 통신(IPC)은 복잡하고 비용이 큼 | 스레드 간 통신은 매우 빠르고 쉽다 (공유 자원 이용) |
오버헤드 | 생성/종료/문맥교환에 많은 오버헤드(부하)가 발생 | 스레드끼리의 전환은 오버헤드가 작음 |
에러 발생 시 | 한 프로세스가 죽어도 다른 프로세스에 영향 없음 | 한 스레드가 에러로 죽으면 같은 프로세스의 다른 스레드도 영향 받을 수 있음 |
멀티태스킹: 운영체제가 CPU에 작업을 줄 대 시간을 잘게 나누어 배분하는 기법
멀티스레드: 프로세스 내 작업을 여러 개의 스레드를 분할해 작업 부담을 줄이는 프로세스 운영 기법
멀티 프로세싱: 여러 개 CPU로 여러 개 스레드를 동시에 처리하는 작업 환경
CPU 멀티스레드: 한 번에 하나씩 처리해야 하는 스레드를 잘게 쪼개어 동시에 처리하는 병렬 처리 기법
단일 프로세스만으로 여러 작업을 처리하면 하나의 작업이 오래 걸릴때, 전체 프로그램의 반응성이 떨어진다. 또한 I/O 작업이나 네트워크 요청으로 인해 프로세스가 블록되면 전체 기능이 멈출 수도 있다.
멀티스레드란 하나의 프로세스 안에서 여러 개의 스레드가 동시에 실행되는 구조를 의미한다. 즉, 하나의 프로그램(프로세스)이 여러 작업(스레드)을 동시에 처리할 수 있도록 만드는 기술이다.
위 사진을 보면 쓰레드와 프로세스가 공유하는 자원에 대해 다른걸 확인 할 수 있다. 자원 공유의 차이에 대해 한번 다뤄 보겠다.
그러나 프로세스 자원 공유는 RAM과 CPU 사이의 캐시 메모리까지 초기화 되기때문에 자원 부담이 크다는 단점이 있다. 그렇기에 다중 작업이 필요한 경우 스레드를 이용하는것이 일반적이다.
병렬 수행의 경우 직관적으로 좋은 방식이라는게 느껴진다. 하지만 병행 수행의 경우 의문이 생길 수 있다. 병행 수행은 하나의 작업을 잘게 나누어서 조금씩 작업을 진행하고, 다음 작업으로 넘어가는 방식을 빠르게 반복하는건데, 그렇다면 결국 최종 작업이 걸리는 시간은 차이가 없다. 그렇다면 왜 번거롭게 작업을 나눠서 스위칭하며 처리하는 것일까?
가장 근본적인 이유는 CPU(코어) 개수의 한계 때문이다.
실제 대부분의 시스템은 프로세서(코어) 수보다 훨씬 많은 프로그램과 작업을 동시에 처리해야 한다.
모든 작업을 병렬로 처리하고 싶어도,
물리적으로 사용 가능한 코어가 적다면
하나의 코어에 여러 작업을 순차적으로 몰아넣을 수밖에 없다.
실제로 사용자는 여러 앱(코어보다 많은 갯수)을 실행하게 될 때, 운영체제는 병행 수행의 타임슬라이스를 적절하게 조정하여 마치 모든 프로그램이 동시에 동작하는 것처럼 느끼게 해준다.
병행 수행의 또 다른 목적은 시스템의 반응성과 자원 활용 효율을 높이기 위함이다.
만약 현재 4코어 8스레드로 환경에 16개의 작업이 있는데, 이때 8개의 작업은 오래 걸리는 작업, 나머지 8개의 작업은 빠르게 끝나는 작업이라고 가정할때, 만일 8개의 오래걸리는 작업이 먼저 수행되면, 나머지 8개는 금방 끝나는 작업임에도 불구하고 나머지 8개의 오래 걸리는 작업이 끝날때까지 기다려야 한다. 하지만 병행 수행을 통해 작업을 하게되면 이러한 일이 사전에 방지 된다.
스레드가 운영체제에서 어떻게 관리되는지에 따라
실제 시스템에서는 여러 방식이 사용된다.
아래는 각 모델의 실제 동작 예시와 함께 설명한다.
5개의 크롤링 스레드 중 한 스레드가 네트워크 I/O로 블록되면,
나머지 4개 스레드도 모두 같이 대기 → 전체 작업 지연
멀티코어 CPU여도 한 코어만 사용
예) 웹 서버에서 100개 요청을 각각 스레드로 처리할 때
일부 스레드가 DB I/O로 대기해도,
나머지 스레드는 그대로 요청을 계속 처리 (진짜 병렬성)
예) Go 서버에서 10만 개 Goroutine을 수천 개 커널 스레드에 자동 분산
일부 Goroutine이 I/O로 멈춰도,
나머지는 계속 다른 커널 스레드에서 실행(진짜 대규모 동시성)
커널 스레드(1:1)
멀티레벨(M:N)
모델 | 대표 기술/언어 | I/O 블록 영향 | 멀티코어 활용 | 초대규모 동시성 |
---|---|---|---|---|
사용자 스레드(1:N) | Green Thread(구자바) | 전체 멈춤 | × | × |
커널 스레드(1:1) | Linux, Java, Python | 영향 없음(독립적) | ○ | △(자원부담) |
멀티레벨(M:N) | Go, Solaris, Erlang | 영향 적음(자동분산) | ◎ | ◎ |
글 잘보고 갑니다.
스레드를 왜 사용할까?에서 하고자하는 말이
멀티스레드를 사용하면 병목 현상이 줄어든다 인가요?