세상에 나쁜 코드는 없다

프로세스 & 쓰레드 개념 정리 본문

Computer Science/Operating System

프로세스 & 쓰레드 개념 정리

Beomseok Seo 2023. 8. 17. 18:38

Process

개념

Program In Execution
The Unit of Work

프로세스란 프로그램이 수행되는 작업단위를 의미하며, 운영체제에 의하여 관리됩니다. 운영체제의 가장 큰 임무 중 하나는 디스크의 프로그램이 수행될 수 있도록 메모리로 가져오고, CPU가 어떤 프로세스의 명령어들을 수행하게 할지 결정하는 것입니다. 또한 모든 명령어가 수행되면 프로세스를 종료시키고 프로세스가 사용중이던 메모리와 자원들을 반환하는 작업도 수행합니다.

프로세스가 무엇인지 정확하게 이해하기 위해서는 우리가 생성한 프로그램이 어떻게 실제로 동작하는지 그 원리를 알아야할 필요가 있습니다. 아래부터는 프로그램을 수행시키기 위해 운영체제가 수행하는 일들을 살펴보고 그 과정에서 프로세스가 무엇인지 이해도를 높인다음, 프로세스와 관련된 정보들을 알아보는 시간을 가질 예정입니다.

In Memory…

우리가 작성한 프로그램은 소스코드로 디스크에 저장됩니다. 이 소스코드를 컴파일을 통해 실행가능한 파일로 변환하면 XXX.out 혹은 XXX와 같은 파일이 생성됩니다. 이것이 ‘프로그램’으로, 아직 수행중이지는 않지만 운영체제로부터 호출되어 수행될 수 있는 상태인 파일을 의미합니다.

이후 컴퓨터의 사용자에 의해 프로그램이 호출되면, 운영체제는 디스크에 있는 프로그램이 수행될 수 있도록 메모리로 가져오는 과정loading을 거칩니다. 메모리에 프로그램이 로딩된 이 instance를 ‘프로세스’라고 합니다. 이후 프로세스는 OS에 의해 프로세서를 점유하게 되고 프로그램은 수행됩니다.

https://www.includehelp.com/operating-systems/memory-layout-of-a-process.aspx

메모리 상의 프로세스는 위와 같이 여러 섹션을 갖게 됩니다.

TEXT 섹션은 수행되는 명령어들을 포함하고 있는 섹션입니다. CPU의 PC Register가 이 공간의 주소를 가리키고 있으며, 하나의 명령어가 수행될 때마다 그 다음 위치의 메모리 공간을 가리켜 프로그램이 순차적으로 수행됩니다.

DATA 섹션은 전역 변수를 포함하고 있는 섹션입니다. 프로그램 수행 중 전역변수에 접근할 일이 있을 때 이 곳에 접근하게 됩니다.

HEAP 섹션은 동적할당된 변수를 포함하고 있는 메모리 공간입니다. C에서의 malloc, Java의 new 를 통해 생성된 변수들은 이 곳에 저장됩니다. 이 변수들은 선형적인 논리적 메모리 공간의 낮은 주소부터 높은 주소로 채워집니다.

STACK 섹션은 메서드의 파라미터, 반환 값, 지역 변수들을 포함하고 있는 메모리 공간입니다. 이 변수들은 높은 주소부터 낮은 주소로 채워집니다. 흔히 얘기하는 StackOverflow란 함수의 루프 호출로 인하여 이 공간이 한계치를 넘겼을 때 발생하는 오류입니다. 함수의 호출을 통해 파라미터와 반환 값, 지역 변수들이 저장되어야 하는 공간이 무한히 늘어나기 때문입니다.

PCB : Process Control Block

위와 같은 방식으로 프로세스들은 메모리에 저장됩니다. 운영체제는 메모리에 저장되어 있는 프로세스가 몇개가 있는지, 어디에 있는지, 어떤 상태인지 관리해야할 필요가 있습니다. 따라서 운영체제는 별도의 자료구조를 통해 프로세스들을 관리하는데, 이 자료구조를 PCB라고 부릅니다.

하나의 PCB에는 하나의 프로세스에 대한 정보가 들어있습니다. PCB에는 프로세스의 고유한 식별자, 상태를 비롯하여 CPU에게 전달해야할 Context, 할당되어있는 메모리 공간의 주소, 생성한 사용자 등의 정보가 저장되어 있습니다.

Context란 프로세서가 프로세스를 수행하는데 필요한 정보들로, PC(Program Counter), General Purpose Register들을 포함합니다. 아래에서 설명하겠지만 서로 다른 프로그램들은 동시에 돌아가는 것처럼 보여야 하기 때문에, 프로세서는 여러 프로세스를 빠르게 번갈아가면서 수행합니다. 이 과정에서 프로세스가 프로세서를 떠나고 다시 점유할 때, 프로세서에게 “기존에 이 부분에서 끊겼으니 이제는 이 부분부터 수행하면 돼” 라는 이야기를 해줄 주체가 필요합니다. OS는 PCB의 context에 프로세서를 떠나는 시점의 PC와 General Purpose Register를 저장해놓고, 이후 프로세스가 다시 CPU를 점유할 때 해당 값들을 CPU에 제공하여 끊겼던 상태에서부터 프로세스가 다시 수행될 수 있도록 합니다. 이 부분은 Context Switch에서 더 자세하게 알아볼 것입니다.

결국 운영체제가 프로세스를 생성한다는 것은 디스크에 있는 프로그램을 수행가능한 상태로 만들기 위해 메모리로 로딩하며, 그 과정에서 생성된 프로세스에 대한 메타데이터들을 저장한 PCB를 만들고 운영체제가 관리하고 있는 PCB Table에 저장한다는 것입니다. 이후 PCB는 여러 운영체제의 동작 중에 사용되며, 이후 운영체제는 프로세스의 종료 요청을 받으면 PCB의 정보를 통해 프로세스가 갖고있는 자원들을 모두 반환해준뒤 PCB는 삭제될 것입니다.

Context Switch

일반적으로 컴퓨터는 여러가지 프로세스들이 수행되고 있습니다. 제가 줌을 켜놓고, 노션을 동시에 작성하는 것도 여러 프로세스가 수행되고 있기 때문입니다. 반면, 하나의 프로세서는 한 순간에 하나의 동작밖에 하지 못합니다. 프로세서는 오직 PC에 있는 메모리 주소에 있는 명령어를 가져오고fetch 수행하는execution 행동밖에 하지 못합니다. 즉, 서로 다른 프로세스는 동시에 수행될 수 없다는 것인데, 이 경우 줌과 노션이 동시에 실행되는 것은 불가능할 지도 모릅니다. 운영체제는 이러한 문제를 Multiprogramming으로 해결합니다.

Multiprogramming이란 메모리에 여러 프로세스를 상주시키고 프로세서가 수시로 프로세스들을 전환하며 수행하여 마치 사용자가 여러 프로그램이 동시에 수행되는 것과 같은 환상을 주는 방법입니다. 프로세스들은 우리가 미처 알아차리지도 못할 정도로 빠르게 전환되며 프로세서로부터 수행되기 때문에 우리는 노션과 줌이 동시에 수행되는 것과 같은 느낌을 받을 수 있습니다. 이 때 프로세서가 수행하는 프로세스를 바꾸는 과정을 Context Switch라고 부릅니다.

https://www.crocus.co.kr/1364

Context Switch를 더 상세히 이해하기 위해 현재 메모리에 2개의 프로세스가 있는 상황을 가정합니다. 현재 CPU는 Process 0의 코드를 수행하고 있습니다. PC에 저장되어있는 명령어의 주소를 통해 명령어를 fetch 하고, 명령어를 수행할 것입니다. 이제 Process 0은 어느정도 수행했으니 동시성의 환상을 주기 위해 Process 1을 수행해야할 것입니다. CPU가 Process 1을 수행하게 만들어야 합니다.

이러한 전환은 여러 이유로 발생할 수 있습니다. 단순히 동시성을 위해 P0이 일정 시간 이상 수행되어서 전환을 하는 경우도 있고, P0이 I/O 요청을 하여서 그 응답을 기다리는 동안 CPU가 할 일이 없어져서 P1를 불러와야 하는 경우도 있을 수 있습니다.

여러 이유를 통해 P0이 전환되어야 하는 상황에서, 운영체제는 CPU의 제어를 뺏어온 뒤interrupt or system call, Context Switch를 수행합니다. Context Switch를 성공적으로 하기 위해서는 먼저 수행중이던 P0의 현재 상황을 P0의 PCB에 저장해야 합니다save state into PCB0. 어떤 명령어를 수행하다가 멈췄는지, 현재 어떤 함수를 수행하다가 멈췄는지에 대한 정보가 저장되어있어야 이후 다시 context switch를 통해 P0의 차례가 됐을 때 프로그램이 마저 동작할 수 있기 때문입니다. P0의 정보를 저장한 다음, CPU의 레지스터에는 PCB1 의 context 정보를 불러옵니다reload state from PCB1. 이를 통하여 CPU는 자연스럽게 P1을 수행할 수 있습니다.

P1이 모종에 이유로 전환되어야 한다면 같은 과정이 반복됩니다. 운영체제가 CPU를 점유한 뒤 CPU의 context 정보를 PCB1에 저장하고, PCB0의 정보를 다시 CPU에 옮김으로써 P0은 기존에 종료된 상태로 돌아가 수행을 이어갈 수 있습니다.

이러한 방식을 통해 여러개의 프로세스는 번갈아가며 CPU를 점유하고, 이것으로 우리는 여러 프로그램을 동시에 활용할 수 있게 됩니다. 운영체제는 Context Switch를 하는 과정에서 여러 의사결정을 하기도 합니다. 예를 들어, Context Switch시 다음에 수행될 Process를 누가 되게 할 것인가, Context Switch가 되는 시점을 언제로 할 것인가 등이 있습니다.

💡
여기서 ConcurrencyParallelism이라는 개념이 나옵니다. 두 단어 모두 번역하면 동시성, 병렬성으로 비슷해보이지만 두 용어는 완전히 다른 용어입니다. Concurrency는 하나의 코어에서 여러개의 프로세스를 순차적으로 수행시키는 것을 의미합니다. 반면 Parallelism은 여러개의 코어에서 여러개의 프로세스가 병렬적으로 수행되는 것을 의미합니다.

프로세스 생명주기와 State

프로세스의 생명주기에 대해서는 흔히 2-model, 5-model, 7-model 다이어그램으로 나타내곤 합니다. 여기서는 5-model을 통해 설명하도록 하겠습니다.

https://www.baeldung.com/cs/process-lifecycle

프로세스를 효과적으로 관리하기 위하여 운영체제는 프로세스에 상태를 부여하고, 이를 PCB에 저장하여 활용합니다.

Start 혹은 New 상태는 프로세스의 생성을 요청받아 필요한 초기화를 하는 상태를 의미합니다. 이후 수행되기 위한 모든 준비가 완료되면 프로세스는 Ready 상태로 바뀝니다.

Ready 상태의 프로세스들은 CPU를 획득하기 위해 기다리고 있는 프로세스 상태를 의미합니다. 레디 상태에 있는 프로세스들의 PCB는 큐로 관리되어, context switch에 의해서 프로세스를 점유하게 될 시점을 기다립니다. 이후 context switch에 의하여 프로세스를 점유하게 되면, 프로세스는 Running 상태가 됩니다. 이때 context switch를 하는 과정을 dispatch라고 부르기도 합니다.

Running 상태의 프로세스는 현재 CPU에 의해 수행되고 있는 프로세스를 의미합니다. 만약 프로세서가 하나밖에 없다면 모든 프로세스 중 Running 상태의 프로세스 역시 하나밖에 없을 것입니다. Running 상태의 프로세스는 3가지의 상태로 바뀔 수 있습니다.

첫번째는 Ready 상태로 돌아가는 경우입니다. 이 경우는 프로세스가 CPU에 의해 수행되던 도중 동시성을 위해 context switch가 일어나는 경우입니다. 프로세스는 Ready 상태로 돌아가 자기 차례를 기다리게 됩니다.

두번째는 Waiting 상태로 바뀌는 경우입니다. 이 경우는 I/O Operation이 발생하여 Disk에서 필요한 값을 읽어올 때까지 기다리는 경우를 포함합니다. 이 때 프로세스는 I/O Operation이 끝나기 전까지는 프로세스를 점유할 필요가 없으므로 Ready가 아닌 Waiting 상태로 바뀝니다. 이후 I/O Operation이 끝나고 프로세스가 다시 수행할 수 있는 환경이 된다면 Ready 상태로 변경되고, CPU를 점유할 수 있을 때까지 기다리게 됩니다.

세번째는 Termination 상태로 바뀌는 경우입니다. 이 경우는 프로그램의 모든 명령어를 수행하고 종료되는 상황입니다. Termination 상태가 된 프로세스는 운영체제에 의해 사용했던 모든 자원을 반납하고 종료됩니다. 또한 운영체제는 예기치 못한 상황이 발생하여 프로세스를 종료해야할 때 Termination 상태로 바꾸기도 합니다.

Thread

a light-weight process

쓰레드란 프로세스에서 내에서 실행되는 독립적인 수행 흐름이자 프로세스에서 PC를 비롯한 context와 stack 영역만 분리되어 수행될 수 있는 작업 단위입니다. Context와 stack만 분리되어 수행되기 때문에 Data Section과 Text Section 등의 정보는 프로세스와 공유하게 됩니다.

위 그림은 단일 쓰레드 프로세스 모델과 멀티 쓰레드 프로세스 모델의 차이를 보여줍니다.

싱글 쓰레드 프로세스 모델에서는 하나의 프로세스에서는 하나의 수행 흐름만 갖습니다. 이 예시는 위에서 쭉 설명한 프로세스와 동일합니다. 반면 멀티쓰레드 프로세스 모델에서는 하나의 프로세스가 여러개의 쓰레드를 갖습니다. 각각의 쓰레드는 Thread Control Block을 갖고 있어서, 프로세스와 별개로 보유하고 있는 Context와 Stack의 정보를 저장합니다. 이를 통해 하나의 프로세스가 동작할 때 그 내부적으로 여러개의 쓰레드들이 동시적으로 CPU를 점유하면서, 여러 동작이 동시에 수행될 수 있습니다. 이는 줌이라는 하나의 프로그램에서 실시간 화상 통신과 채팅을 동시에 할 수 있는 기반이 됩니다.

쓰레드가 CPU를 점유하는 동작은 프로세스가 CPU를 점유하는 동작과 거의 유사하지만 (TCB에도 PCB와 동일하게 State가 존재하며 이러한 쓰레드 메타데이터를 통해 프로세서를 순차적으로 점유하게 됩니다), 프로세스 전환에서는 PCB의 정보를 활용하고 쓰레드의 전환에서는 TCB의 정보를 활용하는데에서 차이가 있습니다. 또한 User Level Thread의 경우 별도의 OS의 도움 없이 쓰레드를 전환하기도 하는데, 이는 나중에 살펴보도록 하겠습니다.

쓰레드 사용의 장점

쓰레드의 사용은 다음과 같은 장점을 갖습니다.

Multithread server architecture

쓰레드의 사용은 특히 Server - Client 구조의 프로그램에서 빛을 발합니다. 일반적으로 서버 프로그램은 클라이언트의 요청이 오기를 기다리고, 요청이 들어온 뒤 그 요청을 처리하는 방식으로 작동합니다. 단일 쓰레드 환경에서 만약 한 클라이언트의 요청을 처리하는 도중에 다른 클라이언트로부터 요청이 오는 경우엔 기존의 요청을 다 처리한 후에야 새로 들어온 요청을 처리할 수 있을 것입니다. 반면 멀티 쓰레드 환경에서는 클라이언트의 요청을 받으면 새로운 쓰레드를 만들어 이를 처리해줄 것을 위임하고, 해당 쓰레드는 다시 다른 사용자의 요청을 받기를 기다립니다. 이 각각의 쓰레드가 병렬적으로 수행되는 경우 전체적인 시스템의 응답시간이 크게 개선될 것입니다.

이러한 작업은 사실 별개의 프로세스를 만들어도 해결되는 부분입니다. 클라이언트의 요청만을 받는 프로세스를 만들고, 요청이 들어오는 경우 새로운 프로세스를 만들어 요청을 처리할 것을 위임하면 됩니다. 하지만 이러한 상황에서 쓰레드는 프로세스보다 더 많은 장점을 제공합니다.

쓰레드는 프로세스와 일부 데이터를 공유하기 때문에 쓰레드 하나는 프로세스 하나보다 훨씬 가볍습니다. 따라서 생성과 삭제 등의 작업이 프로세스보다 훨씬 저비용으로 가능하기 때문에 생성과 삭제가 빈번한 경우에 훨씬 유리합니다.

또한 데이터를 공유하기 때문에 불필요한 데이터 공유 로직을 거치지 않아도 됩니다. 프로세스의 경우 서로의 메모리 영역이 분되어 있어서 데이터를 공유하기 위해서는 복잡한 과정Inter Process Communication을 거쳐야 합니다.

ULT와 KLT

쓰레드는 User Level Thread와 Kernel Level Thread로 나눌 수 있습니다.

KLT가 지금까지 설명했던 구조와 유사하므로 먼저 설명하도록 하겠습니다. Kernel Level Thread는 운영체제의 지원을 받아 관리되는 쓰레드를 의미합니다. 이 쓰레드들의 TCB는 운영체제에 의하여 관리되며 운영체제는 이 정보들을 통해 쓰레드의 스케줄링 작업을 수행합니다. 각각의 쓰레드가 독립된 단위로 관리되기 때문에 하나의 쓰레드에서 I/O Operation 등의 이유로 작업이 중단된다 하더라도 다른 쓰레드는 프로세서를 점유할 수 있고 수행될 수 있다는 장점이 있습니다. 멀티코어 시스템에서는 각각의 쓰레드가 서로 다른 프로세서에 점유되어 병렬적으로 수행될 수도 있습니다. 반면 쓰레드간 context switch나 쓰레드 생성 및 상태 변환에 대해서 운영체제가 관여해야하기 때문에, 시스템이 커널모드로 자주 변환되어 오버헤드가 발생한다는 단점이 있습니다.

반면 User Level Thread는 운영체제의 레벨이 아닌 애플리케이션 레벨에서 직접 만들고 사용하는 쓰레드를 일컫습니다. 실제로 운영체제에서 관리되는 쓰레드는 아니지만, 애플리케이션 레벨의 코드로 쓰레드를 구현하여 마치 쓰레드를 쓰는 것과 같은 효과를 낼 수 있습니다. 이 경우 TCB는 애플리케이션 레벨에서 관리되므로 운영체제는 해당 프로세스가 여러개의 쓰레드를 갖고 있다는 정보를 전혀 모르게 됩니다.

ULT는 Thread간 전환이 애플리케이션 레벨에서 일어나기 때문에 커널의 도움을 받을 필요가 없습니다. 따라서 커널 모드로 진입할 필요가 없고 이는 오버헤드가 감소되는 효과를 낳습니다. 또한 쓰레드 간 전환을 할 때의 정책에 대해서 KLT는 OS에 종속적이지만, ULT는 OS와 상관없이 애플리케이션 레벨에서 자유롭게 정할 수 있다는 장점이 있습니다.

ULT는 위와 같은 장점이 있지만, 운영체제가 쓰레드의 존재를 모르기 때문에 발생하는 부작용도 존재합니다. 예를 들어 ULT 중 하나가 I/O Operation을 수행해야하여 system call을 호출할 때, 프로세스는 중단되고 커널이 프로세서를 점유하게 되는데 이 과정에서 모든 ULT가 중단됩니다. KLT 시스템에서는 하나의 쓰레드가 중단되더라도 다른 쓰레드들은 Ready 상태로 스케쥴링되어 프로세서를 점유할 수 있지만, ULT는 운영체제 레벨에서 하나의 프로세스로 관리되므로 전체가 Blocked 되는 결과를 낳습니다. 비슷한 이유로 ULT는 멀티코어 환경에서 병렬성의 장점을 전혀 가질 수 없게 됩니다.

현대의 시스템들은 ULT 와 KLT의 특성을 모두 활용하는 혼합된 방식을 채택하기도 합니다. 위 그림의 (c)가 그 예시입니다. 이러한 방법을 통해 각기 다른 쓰레드 모델의 장점을 극대화 할 수 있습니다.

💡
User Mode와 Kernel Mode User Mode와 Kernel Mode는 컴퓨터 시스템이 실행되는 두 상태를 나타냅니다. User Mode는 사용자의 응용 프로그램이 수행되는 상태를 의미합니다. 사용자의 응용프로그램이 컴퓨터 시스템의 민감하고 중요한 영역에 접근하는 것은 악성 코드나 해킹의 문제를 발생시킬 수 있으므로, 이 상태에서는 하드웨어나 중요한 메모리에 직접적으로 접근하는 것을 제한합니다. 만약 유저 프로그램이 디스크와 같은 다른 하드웨어에 접근해야하는 경우, 직접 수행하지 않고 운영체제의 기능(system call)을 빌려 수행합니다. Kernel Mode는 프로세스의 생성이나 system call 등 운영체제의 기능이 수행될 때의 상태를 의미합니다. Kernel Mode에서는 유저 프로그램이 아닌 커널이 프로세서를 점유합니다. User Mode에서와 다르게 이 때에는 시스템의 민감한 자원에도 접근할 수 있게 됩니다. 운영체제는 Kernel Mode에서 요청받은 작업을 수행한 뒤 프로세서의 제어를 유저 애플리케이션으로 넘기고 User Mode로 상태를 전환합니다.
💡
System call System call이란 User Mode에서 동작하는 프로세스 혹은 쓰레드가 운영체제의 핵심 기능을 사용하기 위해 요청하는 함수입니다. open(), write(), fort(), exit() 등의 함수가 그 예이며, 프로세스가 system call을 호출하는 경우 Kernel Mode로 변환되며 운영체제가 요청받은 내용을 수행합니다.

Uploaded by N2T

'Computer Science > Operating System' 카테고리의 다른 글

7장 - Memory Management  (0) 2023.06.15