BoostUs로그인

[쉽게 배우는 운영체제] 프로세스와 스레드

J003 강동훈10
0
0
2025-06-19
원문 보기
[쉽게 배우는 운영체제] 프로세스와 스레드 글의 썸네일 이미지

⚙️ 프로세스와 PCB

프로세스는 디스크에 저장된 정적인 파일인 프로그램을 실행시켜 메모리에 로드된 상태를 의미한다.

PCB(Process Control Block)는 프로세스를 처리하기 위해 필요한 다양한 정보를 갖고 있는 자료구조이며 커널 영역에 저장된다.

과거 운영체제에서는 일괄처리 시스템으로 동작하였고 CPU는 한 번에 하나의 작업만 수행이 가능하였다. FIFO 방식인 큐에 작업을 순차적으로 집어넣고 먼저 들어온 작업 순서대로 처리되는 시스템이다.

하지만 기술의 발전과 점차 실행해야 할 작업의 수가 늘어감에 따라 한 번에 여러 작업을 수행하는 기능이 필요해졌고 일괄처리 시스템은 해당 방식을 처리할 수 없기에, 다중처리시스템 중 시분할 방식을 고안하게 된다.

시분할 방식이란 각각의 프로세스가 주어진 타임 슬라이스 동안 작업이 수행되며 해당 타임 슬라이스를 초과하면 다른 프로세스의 작업을 수행하는 방식을 의미한다. 시분할 방식에서 각 프로세스의 전환(Context Switching)은 매우 빠르게 이루어지기 때문에 사용자 입장에서는 여러 프로그램을 동시에 사용하는 것처럼 보이게 된다.

프로세스 상태

프로세스는 생성, 준비, 대기, 실행, 왼료 상태로 구분되며 해당 상태값에 의해 작업이 수행된다.

  1. 생성상태: 프로그램을 실행하며 메모리에 로드되고 PCB를 할당받은 상태이다. 생성된 프로세스는 준비 상태로 옮겨진다.

  2. 준비상태: 바로 실행할 수 있는 프로세스들이 ready queue에 담겨서 기다리고 있는 상태이다. 프로세스들은 CPU 스케줄러에 의해 우선순위가 높은 프로세스를 결정하여 실행상태로 보낸다.

    • ready queue: 준비상태의 프로세스를 저장하는 queue 자료구조이며 각 PCB는 다음 PCB에 대한 위치를 헤더에 포인터로 저장해두고 있다.
  3. 실행상태: 프로세스가 CPU를 할당받아 실행되는 상태이다. 실행상태에 들어갈 수 있는 프로세스의 수는 CPU 개수만큼이다. 각 프로세스는 할당된 타임 슬라이스만큼 작업을 수행하며 이를 초과하면 준비 상태로 옮기고, 작업이 마무리되면 완료 상태로 옮기며 입출력요청이 오면 대기 상태로 옮긴다.

    • Dispatch: 준비상태의 프로세스를 실행상태로 옮기는 작업
    • Timeout: 타임 슬라이스동안 작업을 끝내지 못해, 준비 상태로 돌아가는 상태
  4. 대기상태: 실행 상태에 있는 프로세스가 입출력 요청이 들어오면 CPU는 프로세스의 입출력 요청이 완료될 때까지 아무 작업을 하지 못하고 대기해야 한다. 하지만 이러한 방식은 비효율적이기 때문에 입출력 요청을 받은 프로세스는 대기 상태로 옮기고 준비 상태의 다른 프로세스를 수행한다. 대기 상태는 입출력장치별로 wait queue가 존재하며 대기 상태에 있는 프로세스는 입출력이 완료되면 인터럽트를 발생시켜 준비 상태로 이동해 실행을 기다리게 된다.

  5. 완료상태: 작업이 완료되어 프로세스가 종료되는 상태를 의미한다. PCB가 제거되고 메모리 속 데이터를 제거한다.

실행 상태에서 준비 상태로 돌아가는 경우는 여러 가지 경우가 존재한다.

  1. 입출력 요청(I/O Request)에 의해 대기 상태로 이동하였다가 입출력이 완료되어 준비 상태로 돌아오는 경우
  2. 실행 상태에서 프로세스에 할당된 타임 슬라이스가 초과되어 준비 상태로 돌아오는 경우
  3. 자식 프로세스를 생성하고 자식 프로세스가 종료되기 전까지 대기 상태에 있다가 종료 후 준비 상태로 돌아오는 경우
  4. 하드웨어 혹은 소프트웨어 인터럽트가 발생하여 대기 상태에 있다가 인터럽트 처리 완료 후 준비 상태로 돌아오는 경우

PCB

프로세스는 자신의 상태값에 따라 작업을 처리하게 된다. 이렇게 CPU를 차지하였던 프로세스가 나가고 새로운 프로세스를 받아들이는 작업을 문맥 교환(Context Switching)이라고 한다.

CPU는 문맥 교환을 통해 기존 작업하던 프로세스에서 다른 환경을 가진 프로세스로 전환하여 작업을 수행하는데, CPU는 어떻게 각각 다른 프로세스들의 작업을 순조롭게 교체해가며 수행할 수 있을까?

바로 각각의 프로세스는 PCB를 갖고 있기 때문이다. PCB는 프로세스와 관련된 정보를 저장하고 있고 CPU는 이 PCB를 넘겨받아, 작업을 수행할 프로세스의 정보를 읽고 그에 맞는 환경을 구성하기에 순조롭게 문맥 교환을 통해 작업을 수행할 수 있다.

PCB가 저장하고 있는 정보들은 다음과 같다.

  1. 포인터: 준비 큐나 대기 큐에 프로세스 간 순서를 저장하기 위해 사용된다.
  2. 프로세스 상태: 생성, 준비, 실행, 대기, 완료 등의 상태값이 저장된다.
  3. 프로세스 구분자: 프로세스를 구분하기 위한 식별자
  4. 프로그램 카운터: 다음에 실행한 명령어의 위치를 가리키는 프로그램 카운터 값을 저장한다.
  5. 프로세스 우선순위: 프로세스의 중요도에 따라 우선순위가 부여되며 준비, 대기 큐에서 우선순위에 따라 먼저 실행된다.
  6. 레지스터 정보: 프로세스가 실행될 때 사용하던 각종 레지스터의 정보를 담는다.
  7. 메모리 관리 정보: 프로세스가 저장된 메모리 위치나 경계, 한계 레지스터의 값 등이 저장된다.
  8. PPID, CPID: PPID는 부모 프로세스의 구분자이고, CPID는 자식 프로세스의 구분자며 이를 통해 계층 구조를 형성한다.

프로세스 P0과 프로세스 P1의 Context Switching 과정을 표현한 다이어그램이다.

P0은 CPU를 할당받아 작업을 수행하다가 인터럽트가 발생하면 현재까지 작업하던 내용들을 PCB0에 저장하고 P1을 실행하기 위해 PCB1에서 데이터를 불러온다.

P1을 실행하기 위한 환경이 구성되면 P1은 CPU를 할당받아 작업을 수행한다. 중간에 인터럽트가 발생하면 동일하게 작업 내용을 PCB1에 저장하고 PCB0을 불러와 환경을 구성한 뒤 P0을 수행하게 된다.

프로세스 구조

프로세스는 text, data, heap, stack 영역으로 이루어져 있다.

  1. text: 코드 영역이라고도 불리며 프로그램의 실행 코드가 저장된다.
  2. data: 전역 변수와 데이터가 저장된다.
  3. stack: 지역변수, 반환 주소, 매개변수 등 실행되는 함수와 관련된 데이터를 임시적으로 저장한다.
  4. heap: 런타임 중에 동적으로 할당되는 변수 영역이다.

text와 data는 컴파일 단계에서 결정되기 때문에 정적 할당 영역이며 stack과 heap은 런타임 중에 동적으로 변경되기 때문에 추가적인 여유 공간을 갖는 동적 할당 영역이다.

프로세스 생성과 복사

프로그램을 실행하면 운영체제는 프로그램을 메모리에 로드하여 코드 영역에 저장하고 PCB를 생성한다. 그 후 메모리에 데이터 영역, 스택, 힙 영역을 확보하여 프로세스를 실행한다.

매 프로세스를 생성할 때마다 이러한 과정을 거치게 되면 프로세스의 실행 속도가 느려져 비효율적일 것이다. 이를 해결해주기 위해 프로세스를 복사하는 함수인 fork() 시스템 콜이 제공된다. fork()를 통해 프로세스를 매번 새로 생성할 필요없이 복제를 통해 빠르게 프로세스를 실행시킬 수 있다.

fork()를 통해 프로세스를 복제하면 기존 프로세스는 부모 프로세스, 새로 생성된 프로세스는 자식 프로세스가 된다.

예를 들어, PID가 100인 P0을 fork()를 통해 프로세스를 복제해 PID가 101인 P1을 생성하였다면 P0의 CPID(Childe PID)는 101, P1 의 PPID(Parent PID)는 100이 된다.

#include <stdio.h>
#include <unistd.h>

void main() { int pid;

pid = fork();

// 에러
if (pid &lt; 0){
    printf(&quot;Error&quot;);
    exit(-1);
}

// 자식 프로세스
else if (pid == 0){
    printf(&quot;Child process: PID = %d\n&quot;, getpid());
    exit(0);
}

// 부모 프로세스
else {
    wait(NULL);
    printf(&quot;Parent process: PID = %d, Child PID = %d\n&quot;, getpid(), pid);
    exit(0);
}

}

fork()를 통해 자식 프로세스를 생성하게 되면 pid를 반환하게 되는데, pid가 0이라면 자식 프로세스, 0 초과라면 부모 프로세스를 의미하게 된다.

즉, P0에서는 Parent process: PID = 100, Child PID = 101이 출력되고 P1에서는 Child process: PID = 101이 출력된다.

fork()를 통해서 프로세스를 복제하여도 복제된 프로세스에 작업할 내용을 전부 변경해주는 것 또한 비효율적인 자원의 소모로 이어지게 된다. 이를 위해서 간단하게 기존 프로세스를 새로운 프로세스로 변환해주는 exec() 시스템 콜이 제공된다.

exec()함수는 fork() 이후에 생성된 프로세스에서 PID, CPID, PPID를 제외하고 코드 영역, 데이터 영역, 스택 영역 등 프로세스의 나머지 내용들을 초기화시켜준다.

즉, 부모 / 자식 프로세스에 대한 계층 구조는 유지한 채로 새로운 프로세스를 실행시킬 수 있다는 장점이 있다.

상위 트리 이미지는 유닉스 프로세스의 계층 구조이다.

유닉스에서는 시스템이 부팅되면 root에 init 프로세스가 pid 1번으로 할당된다. 이 후에 생성되는 모든 프로세스는 init 프로세스를 기반으로 fork()를 통해 자식 프로세스로 복제하고 exec()을 통해 프로세스를 변경하여 실행된다.

스레드

스레드란 프로세스의 코드에 정의된 절차에 따라 CPU에 작업 요청을 하는 실행 단위이다. 즉, 운영체제 입장에서 작업의 단위는 프로세스지만 준비 상태에서 실행 상태로 옮겨져 CPU가 실제 작업을 하는 단위는 스레드이다.

워드프로세서는 프로그램이 실행된 하나의 프로세스지만 프로세스 내의 문서 편집, 입출력, 맞춤범 검사 등 여러 스레드들이 동시에 작업하고 있다.

프로세스는 운영체제가 설계될 당시에 만들어졌지만 스레드라는 개념은 그 이후에 나왔다. 과거에는 프로세서가 순차적으로 실행되었고 프로세서의 내부 작업을 여러 개로 구분짓지 못하였다. 하지만 기술의 발전에 따라 프로세스 안에 여러 스레드로 나누고 여러 코어를 가진 CPU의 자원을 할당하면서 시스템의 효율을 높일 수 있게 되었다.

멀티 스레드

프로세스는 fork()exec()을 통해 부모 프로세스를 복제하고 이를 활용하여 새로운 자식 프로세스로 활용하여 계층 구조를 만들어왔다. 하지만 이러한 작업은 아직도 낭비 요소가 많다.

위에서 확인했듯이 프로세스는 코드 영역, 데이터 영역, 스택 영역, 힙 영역으로 구분된다. 만약 비슷한 작업을 하는 프로세스를 fork()를 통해 새로 생성하게 된다면 정적 영역이었던 코드 영역과 데이터 영역은 동일한 데이터를 갖게 되면서 메모리에 중복적인 데이터 낭비가 발생하게 된다.

멀티 스레드는 하나의 프로세스 안에 여러 스레드를 생성하고 프로세스 내의 자원을 공유하면서 스레드 내의 독립적인 메모리를 관리하며 여러 작업을 수행할 수 있다는 장점이 있다. 정적인 코드, 데이터 영역과 영역은 모든 스레드가 함께 공유하고 각 스레드는 스택 영역을 독립적으로 갖는다.

예를 들어, 워드프로세스의 창을 여러 개를 띄우고 작업을 하게 된다면 단일 스레드의 프로세스를 여러 개 복제하여 사용(멀티태스킹)하는 것보다 하나의 프로세스에 여러 스레드를 사용(멀티스레드)하는 것이 자원을 효율적으로 사용할 수 있으며 속도 또한 빠르게 실행시킬 수 있다는 장점이 있다.

반면에 멀티 스레드는 프로세스의 모든 스레드가 공유된 자원을 사용하기 때문에 하나의 스레드에 문제가 발생하면 다른 스레드에 영향을 준다는 단점이 존재한다.

CPU 스케줄링

CPU 스케줄링이란 여러 프로세스의 상황을 고려하여 CPU와 시스템 자원을 어떻게 배정할지 결정하는 일을 의미한다.

  1. 고수준 스케줄링 시스템 내의 전체 작업 수를 조절한다. 작업(Job)은 운영체제에서 다루는 일의 가장 큰 단위로, 1개 혹은 여러 개의 프로세스로 이루어져 있다. 고수준 스케줄링에 따라 시스템의 전체 프로세스 수가 결정되는데 이를 '멀티프로그래밍 정도'라고 한다
  2. 중간수준 스케줄링 프로세스가 활성화된 상태에서 시스템 부하 수준에 따라 일부 프로세스를 보류 상태로 옮기며 나머지 프로세스를 원만하게 작동하도록 지원한다.
  3. 저수준 스케줄링 각 프로세스의 실행을 미세하게 조절하는 스케줄링이다. 준비 상태에서 실행 상태로, 실행 상태에서 대기 상태로, 대기 상태에서 준비 상태로 보내는 작업은 저수준 스케줄링에 의해 이루어진다.

프로세스 우선순위

프로세스는 CPU 버스트와 I/O 버스트 두 상태를 번갈아가며 작업한다.

I/O 버스트가 많은 I/O 집중 프로세스(I/O Bounded Process)는 CPU 버스트가 많은 CPU 집중 프로세스(CPU Bounded Process)에 비해 CPU 버스트가 적으며 입출력을 위한 대기 시간을 많이 갖게 된다.

이런 경우, I/O 집중 프로세스는 CPU 집중 프로세스보다 높은 우선순위를 할당하여 CPU가 I/O 버스트를 먼저 처리하고 해당 I/O 요청 대기 시간 동안 CPU 버스트를 처리하면 더 효율적인(멀티프로그래밍의 정도가 높은) 작업이 가능하다.

프로세스는 또한 커널 프로세스와 사용자 프로세스로 구분된다.

커널 프로세스는 안정성과 효율성을 위해 동작되기 때문에 사용자 프로세스보다 우선순위가 높으며 먼저 CPU를 점유하여 작업을 수행한다.

다중 큐

준비 상태와 대기 상태에서는 프로세스를 다중 큐를 통해 우선순위를 관리한다.

준비 상태

준비 상태는 우선순위 별로 여러 큐가 존재한다.

새로운 프로세스가 생성되면 해당 프로세스의 우선순위에 맞는 큐의 마지막 프로세스 포인터에 이어 저장된다.

준비 상태에는 몇 개의 준비 큐가 존재하는지, 각 큐 안에 프로세스는 어떤 순서로 실행되는지는 스케줄링 알고리즘에 의해 달라진다.

프로세스의 우선순위는 고정 우선순위 방식과 변동 우선순위 방식으로 구분될 수 있다.

대기 상태

대기 상태에서는 입출력장치 별로 큐가 존재한다.

실행 단계에서 입출력 요청이 들어오면 대기 상태의 입출력 요청에 맞는 큐에 저장되며 입출력을 대기하게 된다.

  1. 실행 상태에서 입출력 요청으로 인해 대기 상태로 변경
  2. CPU는 준비 상태의 다른 프로세스 실행
  3. 대기 상태의 프로세스는 입출력 장치 별 큐에 삽입되고 입출력 작업 수행
  4. 입출력 작업이 완료되면 인터럽트 발생
    1. IRQ 번호에 맞게 인터럽트 벡터의 값을 1로 변경
    2. 인터럽트 핸들러에 의해 인터럽트 작업 수행
  5. 입출력이 완료된 프로세스들이 한 번에 준비 상태로 이동

인터럽트 인터럽트란 CPU가 현재 실행 중인 프로세스를 중단하고 특정 이벤트를 먼저 처리하는 것을 의미한다.

기존에는 인터럽트가 아닌 폴링 방식을 사용하였었다. 폴링 방식이란 운영체제가 입출력의 완료를 직접 주기적으로 확인하는 방식을 의미한다. 현대로 올수록 다양한 입출력장치의 개발에 따라 운영체제가 모든 입출력을 관찰하는 것은 많은 자원의 소모를 요구하기 때문에, 입출력이 완료되면 운영체제에 직업 이벤트를 발생시켜 소식을 전달하는데, 이를 인터럽트라고 한다.

각각의 인터럽트에는 IRQ라는 인터럽트 번호가 존재하며 동시에 발생하는 인터럽트들을 하나로 묶어 인터럽트 벡터에 저장한다. 인터럽트가 발생하면 인터럽트 벡터에 있는 IRQ의 값을 0에서 1로 변환시키고 1의 값을 갖는 인터럽트들은 각각 사전에 정의된 인터럽트 핸들러에 의해 처리된다.

  1. 인터럽트 발생, CPU에서 실행 중이던 프로세스 내용 임시 저장
  2. 인터럽트 컨트롤러에 의해 인터럽트의 우선순위에 따라 처리 순서 결정
  3. 인터럽트 벡터를 참고하여 해당 IRQ에 의한 인터럽트 핸들러를 통해 작업 수행
  4. 인터럽트 작업 완료 후, 기존 프로세스가 이어서 실행

CPU 스케줄링

선점형 스케줄링: 어떤 프로세스가 CPU를 할당받아 실행 중이어도 운영체제가 CPU를 강제로 뺏을 수 있다. 비선점형 스케줄링: 어떤 프로세스가 CPU를 할당받아 실행 중이라면 해당 프로세스가 종료될 때까지 다른 프로세스가 CPU를 점유할 수 없다.

구분종류
비선점형 알고리즘FCFS, SJF, HRN
선점형 알고리즘RR, SRT, MLQ, MLFQ
모두 가능우선순위 스케줄링

다단계 큐

다단계 큐(Multi-Level Queue, MLQ) 스케줄링은 우선순위에 따라 준비 큐를 여러 개 사용하는 방식의 스케줄링이다.

다단계 큐 스케줄링은 이전 준비 상태에서 보았던 이미지와 동일하게 우선순위 별로 큐를 준비하여 상위 우선순위의 프로세스부터 처리하는 방식이다. 이러한 방식 뿐만 아니라 다단계 큐는 프로세스의 유형에 따라서도 여러 큐로 분류될 수 있다.

예를 들어, 사용자와 직접 상호작용을 많이하는 foreground 프로세스(interactive)와 사용자의 개입이 없이 실행되는 background 프로세스(batch) 는 서로 CPU를 점유하는 시간이 다를 것이며 이에 따라 서로 다른 CPU 스케줄링이 필요할 것이다.

그렇기 때문에 우선순위가 높은 큐에서는 라운드-로빈(Round-Robin, RR) 방식을 사용하여, 각 프로세스마다 일정한 타임슬라이스 동안만 CPU를 점유하여 작업을 진행하고 이 후 높은 우선순위의 모든 작업들은 마무리되었기 때문에 우선순위가 낮은 큐에서는 FCFS(First Come First Served)방식을 사용하여 CPU점유가 긴 작업들을 먼저 들어온 순서대로 작업을 진행하며 멀티프로그래밍의 효율을 높일 수 있다.

또한 다단계 큐 스케줄링은 선점형 알고리즘에 속하기 때문에, 만약 interactive process가 입출력 요청을 완료하고 준비 상태로 옮겨진다면 현재 실행 중인 batch process는 즉시 중단되고 interactive process가 CPU를 선점하여 실행될 것이다.

1. 새 프로세스 도착 → 유형에 따라 적절한 큐에 배치
2. 항상 최고 우선순위 큐부터 확인
3. Interactive 큐에 프로세스 있음 → RR으로 스케줄링
4. Interactive 큐 비어있음 → Batch 큐에서 FCFS로 스케줄링
5. 실행 중 상위 큐에 프로세스 도착 → 즉시 선점

다단계 큐 스케줄링에서 우선순위가 낮은 큐의 프로세스는 상위 큐의 모든 작업이 마무리되기 전까지는 작업을 할 수 없다는 문제점이 있다. 이를 기아(Starvation)현상이라고 하는데, 이러한 문제점을 해결하기 위해 제안된 것이 다단계 피드백 큐 스케줄링이다.

다단계 피드백 큐

다단계 피드백 큐(Multi-Level Feedback Queue, MLFQ)는 피드백 방식을 통해 이전 MLQ의 기아 현상 문제를 보완한 방식이다.

기존 MLQ와 달리 MLFQ에서는 큐 간 이동이 가능해진다. 그렇다면 어떤 경우에 프로세스는 큐 간 이동이 가능해질까?

MLFQ에서는 CPU Burst가 많으면 낮은 우선순위, I/O Burst가 많으면 높은 우선순위를 갖는다는 특징을 기반으로 동작한다.

각 큐 별로 프로세스에 할당된 시간(quantum)이 존재하며, 만약 프로세스가 그 시간 안에 작업이 마무리되면 우선순위가 유지되고, 마무리되지 못한다면 우선순위가 떨어지게 된다. 하지만 기아 현상을 방지하기 위해서 낮은 우선순위의 작업이 오랫동안 실행되지 않는다면 해당 프로세스의 우선순위를 높여주는 에이징(aging)도 같이 동작된다.

예를 들어 3개의 다단계 큐가 존재한다고 가정해보자.

하위 우선순위의 큐들은 상위 우선순위 큐가 비어있기 전까지 작업을 실행할 수 없다.
  1. 모든 프로세스들은 queue 0에서 시작
  2. 만약 8초 이내에 작업을 마무리하지 못하면 queue 1로 이동
  3. queue 0이 비어있다면 queue 1 작업 시작
  4. 만약 16초 이내에 작업을 마무리하지 못하면 queue 2로 이동
  5. 가장 낮은 우선선위의 큐는 상위 우선순위가 없기에 FCFS 방식으로 작업
  6. 구현된 에이징 알고리즘에 의해 오랫동안 작업을 수행하지 못한 프로세스는 상위 큐로 승격
>> processA: CPU 3ms → I/O 10ms → CPU 2ms → I/O 5ms → CPU 4ms → 종료

시간 0: Process A 도착 → Queue 0 시간 0-3: Process A 실행 (3ms 사용) 시간 3: I/O 요청으로 자발적 CPU 반납 → I/O 대기 시간 13: I/O 완료 → Queue 0 복귀 시간 13-15: Process A 실행 (2ms 사용)
시간 15: I/O 요청으로 자발적 CPU 반납 → I/O 대기 시간 20: I/O 완료 → Queue 0 복귀 시간 20-24: Process A 실행 (4ms 사용) → 종료

>> Process B: CPU 25ms

시간 0: Process B 도착 → Queue 0 시간 0-8: Process B 실행 (8ms 사용) 시간 8: Time quantum 초과로 선점 → Queue 1으로 이동

시간 8-24: Process B 실행 (16ms 사용, Queue 1에서) 시간 24: Time quantum 초과로 선점 → Queue 2로 이동

시간 24-25: Process B 실행 (1ms 사용, Queue 2에서) → 종료

Reference