거북이-https://velog.io/@violet_evgadn 이전완료

프로세스와 스레드 본문

CS 지식/OS

프로세스와 스레드

VioletEvgadn 2023. 4. 4. 12:10

리눅스에서의 프로세스 관리법에 대해 알아보기 전 프로세스와 스레드에 대한 개념을 잡고 가는 것이 좋다고 생각했다.

 

프로세스와 스레드, 그리고 이에 이어지는 멀티스레드와 멀티 프로세스와 임계 지역(Critical Section)과 동기화는 면접에서도 물어볼 만큼 중요한 개념이고 학부생 때도 이에 대해서만 1학기 내내 공부했던 만큼 중요하다고 생각하여 제대로 공부하고 넘어가도록 하겠다.


프로세스

◎ 프로세스란?

프로세스에 대해 알기 위해선 프로그램에 대해 먼저 알아볼 필요가 있다.

"프로그램"이란 "어떤 작업을 위해 실행할 수 있는 파일"이라는 의미이다. 이 말을 조금 더 생각해 보면 실행할 수 있는 파일이라면 그 파일이 어떤 상태이든 모두 프로그램이라고 할 수 있다.

 

위키피디아에서 설명한 프로세스(Process)란 컴퓨터 프로그램의 인스턴스로써 하나 이상의 스레드를 통해 실행된다고 나와 있다.

 

프로그램을 실행시키면 OS는 프로그램 실행에 필요한 자원들을 할당할 것이며 해당 자원에 프로그램 실행에 필요한 프로그램 명령어 및 데이터가 적재됨으로써 프로그램 인스턴스가 생성된다.

이렇게 생성된 프로그램 인스턴스를 프로세스라고 하며, 정리하자면 "CPU 자원을 할당받아 실행 중인 프로그램"이라고 할 수 있다.

 

이 때 할당받는 시스템 자원으로는 CPU 시간, 메모리 영역(Code, Data, Stack, Heap 구조), 운영을 위한 주소 공간 등이 있다.

 

◎ 프로세스의 분류

먼저 프로세스는 실행 결과 및 과정을 화면에 띄우느냐에 따라 "포그라운드 프로세스(Foreground)"와 "백그라운드 프로세스(Background)"로 나눈다.

 

포그라운드 프로세스는 명령어를 실행하면 결과가 화면에 나타나며 사용자는 결과를 보고 다음 명령을 입력하는 사용자와 상호작용하며 진행되는 프로세스이다.

백그라운드 프로세스는 실행은 되지만 결과가 화면에는 나타나지 않는 프로세스를 말한다. 따라서 실행 중 사용자와 상호작용하기가 힘들며 백그라운드 프로세스로는 대표적으로 백신이나 데몬이 존재한다.

 

프로세스는 다른 프로세스를 만들었는지 만들어졌는지에 따라 "부모 프로세스"와 "자식 프로세스"로 나눌 수도 있다.

 

한 프로세스는 실행되는 도중 fork()라는 시스템 콜을 통해 새로운 프로세스들을 생성할 수 있다.

이 때 fork() 시스템 콜을 통해 다른 프로세스를 생성하는 프로세스를 "부모 프로세스(Parent Process)"라 하며 부모 프로세스에 의하여 생성된 프로세스를 "자식 프로세스(Child Process)"라 한다.

 

모든 프로세스는 부모 프로세스를 가지고 있으며 부모 프로세스가 중지된다면 자식 프로세스도 자동으로 중지된다는 특징을 가진다.

 

◎ PCB

프로세스 제어 블록(Process Control Block;PCB)은 특정 프로세스에 대한 정보를 저장하고 있는 OS 커널의 자료 구조이다.

 

PCB가 필요한 이유는 나중에 설명할 Context Switching(문맥 교환) 때문인데 CPU가 여러 프로세스를 빠르게 번갈아가며 작업하기 위해서는 프로세스에 대한 정보 및 상태를 저장/복원할 필요가 있기 때문이다.

 

OS마다 PCB가 저장하는 항목이 약간씩 다르지만 일반적으로 아래와 같은 정보를 저장한다.

  • 프로세스 식별자(PID; Process ID)
  • 프로세스 상태(Process State)
  • 프로그램 계수기(Program Counter) : CPU가 다음으로 실행할 명령어를 가리키는 값
  • CPU 스케줄링 정보 등

 

◎ 프로세스 메모리 구조

출처 : https://code-lab1.tistory.com/38

  • Code 영역 : 프로그램을 실행시키는 실행 파일 내의 명령어들이 저장됨
  • Data 영역 : 전역변수나 static 변수들이 저장됨
  • Heap 영역 : 동적 할당 된 변수들이 저장됨
    • malloc(C언어), new(Java) 등으로 선언된 변수
  • Stack 영역 : 지역변수, 파라미터(함수에 전달되는 인자)가 저장됨

일단 재미있게 봐야 하는 점이 Stack 영역은 아래에서부터 데이터가 쌓이고 Heap 영역은 위에서부터 데이터가 쌓인다는 점이다. 또한 Code 영역에서 스택 영역으로 갈수록 메모리 주소값이 증가한다.

 

하나의 프로세스는 위에서 설명한 구조로 독립된 메모리 영역을 할당받는다.

만약 OS에 프로세스가 3개 존재한다면 아래 이미지와 같이 프로세스가 존재하고 있을 것이다.

출처 : https://gmlwjd9405.github.io/2018/09/14/process-vs-thread.html

이 메모리 구조를 외우는 데에 예전에는 애를 먹었는데 원리를 이해하면 왜 이렇게 나누어 놓았는지 조금 이해가 될 것이다.

 

먼저 코드 영역이다.

이 부분은 실행할 프로그램의 코드 및 매크로 상수가 기계어 형태로 저장되어 있다.

이 부분은 당연히 나뉘어져 있어야 하는데 CPU는 코드 영역에 저장된 다수의 명령어를 차례로 실행함으로써 프로그램을 실행시킬 수 있기 때문이다.

 

이후 전역 변수 및 static 변수를 저장하는 데이터 영역이다.

데이터 영역을 나눠 놓은 이유를 알기 위해선 전역 변수와 static 변수의 특징을 알아야 한다.

전역 변수는 (자바 기준) 클래스 내에서 유일한 값, static 변수는 전체 프로젝트 내에서 유일한 값으로 프로그램이 실행되기도 전에 지정될 수 있는 값이다.

이를 CS적으로 말하자면 전역 변수와 static 변수는 컴파일될 때부터 어떤 값인지 지정되어 있거나 유추할 수 있는 값이라는 의미이다.

즉, 데이터 영역의 데이터는 프로그램 시작과 동시에 할당될 수 있는 값들이 저장되며 프로그램이 종료할 때 소멸되는 프로그램 수명과 동일한 수명을 가진 변수인 것이다.

 

하지만 프로그램에는 프로그램 시작과 동시에 할당될 수 있는 값들만 저장되어 있는 것은 아니다.

예를 들어 웹 프로젝트의 경우 URL Parameter에 의하여 고객이 선택하는 값에 따라 다르게 할당되는 값도 존재한다.

이렇게 프로그램 실행 과정에서 값이 정해지는 변수들을 저장하기 위하여 스택 영역과 힙 영역을 준비한 것이다.

그렇다면 이 둘을 왜 나눠 놓은 건지 알아보자.

 

둘 중 스택(Stack) 영역에 대해 먼저 알아보자.

스택 영역은 함수 안에서 선언되는 지역변수, 매개변수, 리턴값 등이 저장되는 공간이다.

저장되는 값들을 보면 유추할 수 있겠지만 함수가 호출될 때 사용되며 동시에 함수가 종료될 때 사라져야 하는 값들이다.

즉, 데이터 영역이 프로그램 수명과 동일한 수명을 가진 변수를 저장했다면 스택 영역은 호출한 함수의 수명과 동일한 수명을 가진 변수를 저장하는 공간인 것이다.

스택이라는 자료구조 명칭에서도 알 수 있듯 후위선출(LIFO) 메커니즘을 따른다.

재귀함수가 너무 많이 실행될 경우 "Stack Overflow" 에러가 발생하는데 호출된 재귀함수가 종료되기 전까지는 재귀함수를 호출한 부모 함수 또한 Stack 영역에 메모리를 차지하고 있고 이런 재귀함수가 너무 많아지면 모든 재귀함수가 스택 영역에 각자의 재귀 함수 변수들을 저장하고 있기 때문에 결국 스택 영역에 할당된 메모리가 부족해져 생기는 에러이다.

 

마지막으로 힙(Heap) 영역이다.

힙 영역은 관리가 가능한 데이터 이외의 다른 형태의 데이터를 관리하기 위한 공간으로 동적 메모리 할당 공간이다.

지금까지 배웠던 데이터들은 어찌되었든 사전에 크기가 미리 정해져 있는 데이터였다. 예를 들어 "int a"일 경우 a값이 무엇이 될지는 모르겠으나 a가 4 바이트를 차지한다는 것은 알 수 있다.

하지만 동적 메모리 할당을 할 경우 해당 객체가 몇 바이트를 차지할지 알 수 없다. 예를 들어 String 데이터를 지정할 경우 사용자가 1글자만 입력할지 1024글자를 입력할지 사용자가 직접 입력하기 전까지는 모르는 것이다.

 

즉, 프로그램이 실행되는 순간 프로그램이 사용할 메모리 크기를 고려하여 메모리 할당이 이뤄질 수 있는 스택이나 데이터 영역과는 다르게 힙 영역의 데이터는 프로그램이 실행되고 객체에 직접 데이터가 주입되기 전까지는 어느 정도의 메모리가 할당되어야 하는지 모르는 것이다.

따라서 힙 영역의 데이터는 런타임에서만 확인할 수 있다.

 

◎ IPC

위에서 설명했던 것처럼 프로세스는 독립된 메모리를 할당받기 때문에 모든 프로세스는 별도의 주소 공간에서 실행되며 이 때문에 일반적인 방법으로는 한 프로세스가 다른 프로세스의 변수나 자료 구조에 접근할 수 없을 것이다.

 

하지만 프로세스 간 통신(IPC; Inter-process communication)을 사용한다면 다른 프로세스의 자원에 접근할 수도 있다.

 

IPC란 OS에서 실행중인 프로세스 간 정보를 주고받는 것을 말하는데 방법을 간단히 말하자면 2개 프로세스 사이에 길을 만들어 만들어진 길을 통해 데이터를 주고받음으로써 프로세스 간 통신이 가능해지는 것이다.

 

쌍둥이 빌딩은 원래 별개의 건물 2개이기 때문에 한쪽 건물에서 다른 쪽 건물로 이동할 수 없어야 하지만 그 사이를 잇는 다리를 만들어 그 다리를 통해 사람이나 물건이 다른 쪽 건물로 이동할 수 있는 것을 생각하면 된다.

 

IPC 종류로는 파이프, 소켓, 시그널, 공유 메모리 등을 이용한 통신 방법이 있다.

 

◎ 프로세스 상태 전이

출처 : https://afteracademy.com/blog/what-is-long-term-short-term-and-medium-term-scheduler/

프로세스 상태
  • New(생성)
    • 프로세스가 막 생성된 상태
  • Ready(준비)
    • 프로세스가 CPU에 할당되어 처리되기를 기다리는 상태
  • Running(실행)
    • 프로세스가 CPU에 할당되어 실제로 명령어들이 실행되는 상태
  • Block(Wait; 대기/보류)
    • 이벤트의 발생으로 프로세스가 특정 동작을 기다리는 상태
    • 처리 속도가 느린 I/O 작업 중이거나 외부 사건이 생길 때까지 실행될 수 없는 상태
  • Blocked Suspended
    • 프로세스가 대기 상태에서 기억 장치를 잃은 상태
  • Suspended Ready
    • 프로세스가 기억장치를 제외한 다른 모든 필요한 자원을 보유한 상태
  • Exit(Terminated; 종료)
    • 프로세스의 실행이 완료되어 할당된 CPU를 반납한 상
프로세스 상태 변화
  • Dispatch(Ready → Running)
    • 준비 상태의 프로세스가 CPU를 할당받아 실행 상태로 변함
  • Timer Run Out(Running → Ready)
    • 할당된 시간만큼 CPU를 사용하였기 때문에 준비 상태로 변함
  • I/O 발생(Running → Block)
    • 프로세스가 동작 중 I/O 행위가 필요하여 대기 상태로 이동
  • Wake up(Block→  Ready)
    • I/O 작업이 완료되었거나 자원이 할당되어 다시 준비 상태로 변함
  • Suspended(Swap-Out; Ready → Suspended Ready)
    • 준비 상태에서 기억 장치를 반납
  • Resume(Swap-In; Suspended Ready → Ready)
    • 지연 준비 상태에서 기억 장치를 할당 받아 준비 상태로 변함
프로세스 스케줄링
  • Long-Term(장기 스케줄러; Job Scheduler)
    • 보조기억장치에서 어떤 프로그램을 가져와 커널에 등록하여 준비 상태로 만들지 결정
  • Middle-Term(중기 스케줄러)
    • 메모리에 적재된 프로세스의 수를 동적으로 조절하기 위해 추가된 스케줄러로 Swap-Out 과정을 말함
    • 메모리에 너무 많은 프로세스가 적재되어 있을 경우 Context Switching이 너무 많이 발생하여 오버헤드 때문에 시스템 성능이 저하될 수 있다. 따라서 대기 상태의 프로세스 중 일부로부터 메모리를 통째로 빼앗아 메모리의 여유 공간을 마련하는 과정을 말한다. 만약 메모리에 데이터가 저장되어 있다면 디스크 스왑 영역에 저장한다.
  • Short-Term(단기 스케줄러; CPU Scheduler)
    • 준비 상태의 프로세스 중 어떤 프로세스를 실행 상태로 만들지 결정
    • 시분할 시스템에서 타이머 인터럽트가 발생하면 단기 스케줄러가 호출됨

스레드

◎ 스레드란?

위키피디아에서 설명하는 스레드는 "프로세스 내에서 실행되는 흐름의 단위"라고 설명하고 있다.

 

스레드는 프로세스가 할당받은 자원을 이용하여 작업을 처리하는 실행의 단위라고도 말할 수 있다.

사실 스레드에 대해 설명하기보다는 스레드와 프로세스의 차이에 대해 설명하는 것이 이해하기 더 편하다.

 

위에서 말했듯 프로세스는 OS로부터 자원을 할당받아 4개의 메모리 영역(Code, Data, Stack, Heap 구조)을 구성한 뒤 프로그램을 실행시켰다.

하지만 스레드는 OS로부터 자원을 받는 것이 아닌 프로세스로부터 자원을 받아 실행되는 Task이다.

 

여기에서 중요한 점이 있는데, 스레드는 프로세스 내에서 Stack 영역만 따로 할당받고 Code, Data, Heap 영역은 프로세스 것과 공유한다는 것이다.

출처 : https://gmlwjd9405.github.io/2018/09/14/process-vs-thread.html

이런 상황이다 보니 프로세스와 큰 차이점이 또 하나 존재한다.

프로세스는 각각이 독립된 메모리에 데이터를 저장하기 때문에 IPC를 사용하지 않을 경우 다른 프로세스의 데이터에 접근할 수 없다.

 

하지만 스레드는 Stack 영역만 나뉘어져 있을 뿐 Code, Data, Heap 영역은 모두 공유하고 있는 상태이다.

예를 들어 위 이미지에서 Thread1이 Heap 영역에 A라는 동적 객체를 생성했다면 Thread2에서도 A 객체에 접근할 수 있다.

 

즉, 프로세스 내에 존재하는 자식 스레드들은 서로 주소 공간이나 자원들을 공유하면서 실행될 수 있다는 의미이다.


멀티 프로세스와 멀티 스레드

◎ 문맥 교환(Context Switching)

문맥 교환을 알아보기 전 프로세스 컨텍스트(Context)에 대해서 알아볼 필요가 있다.

프로세스 Context는 프로세스가 현재 어떤 상태에서 수행되고 있는지 파악하기 위해 필요한 정보를 말한다.

 

프로세스는 CPU를 점유하여 명령을 수행하는데 현재는 멀티태스킹 작업을 수행하기 위해 프로세스가 한 번 CPU를 잡으면 작업이 종료될 때까지 점유하고 있는 것이 아니라 일정 시간 동안 점유한 뒤 다음 프로세스에게 CPU를 넘겨주는 방식으로 프로세스가 실행된다. 이는 아래에서 조금 더 설명하겠다.

 

이렇다보니 프로세스가 진행되다가 다음 프로세스에게 CPU를 넘겨줄 경우 CPU를 넘겨주기 전까지 진행되었던 프로세스 상태를 저장한 뒤 해당 프로세스가 다시 CPU를 할당받으면 저장한 시점부터 프로세스를 진행해야 한다.

이를 위해 프로세스가 어디까지 명령을 수행하였는지 정확한 시점과 상태를 재현할 수 있는 정보가 필요했고 이 정보가 "프로세스 문맥(Process Context)"인 것이다.

 

프로세스 문맥은 크게 "하드웨어 문맥", "프로세스 주소공간", "커널상 문맥"으로 나눌 수 있다.

하드웨어 문맥은 CPU 상태를 나타내는 register 값으로 PC(Program Counter)와 각종 레지스터에 저장된 값들을 말한다.

프로세스에 CPU가 할당되었다면 PC가 가리키는 부분부터 명령을 실행하면 되는 것이다.

 

프로세스 주소 공간은 Code, Data, Stack 영역으로 구성된 프로세스만의 독자적인 주소 공간으로 현재 프로세스가 사용하는 변수나 데이터 등에 대한 정보를 담고 있다.

 

커널상 문맥은 OS가 해당 프로세스를 어떻게 관리하고 다루는지 파악 가능한 정보를 말한다.

다른 말로 하자면 프로세스 관리를 위한 자료구조인 PCB와 Kernel Stack(커널 내의 주소)를 말한다.

 

문맥 교환은 이러한 프로세스 문맥(Context)을 교환하는 것을 말한다.

즉, 위에서 말했듯 프로세스는 CPU를 계속 점유하는 것이 아닌 일정 시간 동안 사용하다 다른 프로세스에게 CPU를 건네주는데 이때 프로세스 문맥을 바꾸는 과정이라는 것을 유추할 수 있다.

 

문맥 교환이란 하나의 프로세스가 CPU를 사용 중인 상태에서 다른 프로세스가 CPU를 사용하도록 하기 위해 이전 프로세스의 상태를 보관한 뒤 새로운 프로세스의 상태를 적재하는 작업을 의미한다.

문맥 교환을 통해 CPU를 사용하고 있던 프로세스의 상태를 위에서 설명한 PCB에 저장한 뒤 다음 프로세스로 CPU 사용권을 넘겨주는 것이다.

 

문맥 교환은 OS의 CPU 자원을 할당해 주는 스케줄러(Scheduler)에 의해 발생하며 스케줄러가 CPU를 적절하고 효율적으로 사용할 수 있게 해주는 작업을 스케쥴링이라고 한다.

이 CPU 스케쥴링에는 LFU, FIFO 등 여러 방법이 존재하지만 여기까지 공부하기엔 OS에 대하여 깊이 공부를 해야하기 때문에 설명은 생략하겠다.

 

◎ 멀티 프로세스

멀티 프로세스란 하나의 응용 프로그램을 여러 개의 프로세스로 구성하여 각 프로세스가 1개의 작업(Task)을 처리하도록 하는 것이다.

 

사실 1개의 CPU는 1개의 명령어밖에 처리할 수 없다. 즉, 1개 CPU 가지고는 음악을 들으며 구글 검색을 수행할 수 없다는 것이다. 하지만 우리는 1개 CPU로도 다수의 작업을 동시에 할 수 있다. 어떻게 할 수 있는 걸까?

 

CPU는 동시에 여러 개의 작업을 수행하는 것이 아닌 한 번에 한 개의 프로세스만을 처리하되 재빠르게 여러 프로세스들을 번갈아가며 실행함으로써 여러 개의 작업이 수행되는 것처럼 보이게 하는 것이다.

CPU는 상상이상으로 빠르게 동작하기 때문에 프로세스들을 번갈아가며 실행하는 것이 마치 동시에 동작되는 것처럼 보일 뿐인 것이다.

 

이 과정에서 "문맥 교환(Context Switching)" 작업이 필요하다.

멀티 프로세스는 프로그램을 여러 개의 프로세스로 구성하여 각 프로세스가 1개의 작업을 처리하는 것이므로 위에서 설명한 문맥 교환 작업을 통해 여러 개의 작업을 수행할 수 있을 것이다.

멀티 프로세스 장점

모든 프로세스는 고유한 메모리 공간을 할당받고 IPC를 통해서가 아니라면 다른 프로세스의 데이터에 접근할 수 없다.

따라서 멀티 프로세스 같은 경우 안정성이 좋다.

여러 개의 자식 프로세스 중 하나에 문제가 발생하더라도 이 문제가 다른 자식 프로세스에 영향을 주지 않기 때문이다.

 

또한 구현이 비교적 간단하다는 장점도 존재한다.

 

멀티 프로세스 단점

멀티 프로세스에선 스케쥴링에 의한 Context Switching 과정이 많아지는데, 문맥 교환에서도 오버헤드가 발생한다.

문맥 교환 과정에선 캐시 메모리 초기화 등의 무거운 작업이 진행되며 문맥 교환 시간도 오버헤드를 발생시킨다.

 

또한 프로세스 간 통신을 위해선 IPC라는 어렵고 복잡한 통신 기법을 사용해야 한다.

따라서 다른 프로세스가 실행한 결과물을 사용해야 할 경우 구현 난이도가 갑자기 증가하게 될 것이다.

 

마지막으로 모든 프로세스는 다른 메모리 영역을 할당받아야 하기 때문에 메모리 사용량이 많을 것이다.

 

◎ 멀티 스레드

멀티 스레딩은 하나의 응용 프로그램을 여러 개의 스레드로 구성한 뒤 1개의 스레드가 1개의 작업(Task)을 처리하도록 하는 것이다.

윈도우, 리눅스 등 많은 OS에서는 멀티 프로세스도 지원은 하고 있으나 기본적으로 멀티 스레드를 활용하고 있다.

 

사용자와 상호작용하는 애플리케이션에서 단일 스레드로 네트워크 통신이나 DB 접속 같은 긴 작업(Long-running task)을 처리하면 해당 작업을 수행하는 동안 사용자와 상호작용이 불가능해질 수 있으므로 멀티 스레드를 활용한.

 

멀티 스레드 장점

스레드는 Stack 영역을 제외한 나머지 데이터들을 다른 스레드와 공유하기 때문에 데이터를 주고받는 것이 매우 편하다.

따라서 스레드 간 데이터를 주고받을 때 시스템 자원 소모가 줄어들 것이다.

 

또한 프로세스를 생성하여 자원을 할당하는 것보다 이미 자원이 할당된 프로세스가 스레드에 자원을 할당하는 것이 비용이 적게 들기 때문에 자원의 효율성을 극대화시킬 수도 있다.

 

프로세스를 생성할 때는 자원을 할당하기 위한 시스템 콜이 실행되어야 하는데 멀티 스레드를 사용할 경우 프로세스에 이미 자원이 할당된 상태일 것이기 때문에 사용하는 시스템 콜이 줄어들어 자원을 효율적으로 관리할 수 있다.

또한 프로세스 간 Context Switching 시에는 CPU 레지스터 교환뿐만 아니라 RAM과 CPU 캐시 메모리에 대한 데이터까지 초기화해야 하기 때문에 오버헤드가 더욱 커진다. 멀티스레드 같은 경우 Context Switching 시 Stack 영역만 처리하면 되기 때문에 훨씬 전환 속도가 빠르고 오버헤드가 적은 것이다.

 

멀티 스레드 단점

일단 멀티스레드는 구현이 어렵고 기능 테스트 및 디버깅 또한 까다롭다.

또한 너무 많은 스레드 사용은 매우 많은 오버헤드를 발생시킬 수 있다.

 

위에서 말한 스레드가 자원을 공유한다는 점은 단점으로도 다가올 수 있는데, 일단 자식 스레드 중 하나에 문제가 생길 경우 해당 스레드를 포함하고 있는 전체 프로세스에 영향을 줄 수 있다는 단점을 가진다.

 

또한 자원 공유로 인해 발생하는 "교착상태"나 "동기화" 문제가 발생하지 않도록 주의해야 하며 이 때문에 구현 난이도가 더욱 증가하게 된다.

교착상태나 동기화에 대해선 다음 섹션에서 알아보도록 하자.

 

이러한 단점에도 멀티 스레드를 주로 활용하는 이유는 "자원의 효율성이 좋다"라는 장점이 단점을 모두 덮을 만큼 좋은 장점이기 때문이다.

'CS 지식 > OS' 카테고리의 다른 글

임계구역과 교착 상태  (0) 2023.04.05
Comments