엔지니어 블로그

[컴퓨터 밑바닥의 비밀] 운영체제,프로세스,스레드의 근본 이해하기 - 1 본문

글공부

[컴퓨터 밑바닥의 비밀] 운영체제,프로세스,스레드의 근본 이해하기 - 1

안기용 2025. 3. 11. 10:28

1.모든것은 CPU에서 시작된다.

CPU는 운영체제,프로세스,스레드와 같은 개념을 알지 못한다. 단지 메모리에서 명령어를 가져다가 수행하고 다시 명령어를 가져오고 수행하는 것을 반복 할 뿐이다. 그렇다면 CPU는 어떤 기준으로 명령어를 메모리로부터 가져오는 것일까? 바로 PC(Program Counter) 라는 레지스터에서 찾을 수 있다. PC레지스터에는 CPU가 다음에 가져 올 명령어 주소를 저장하고 있다. 

PC 레지스터의 명령어 주소 값은 자동으로 1씩 증가한다. CPU가 주소를 하나씩 증가시키면서 차례대로 명령어를 실행하기에 자연스러운 모습이다. 하지만 if~else,함수 호출 같은 명령어를 만나게 되면 이 순서는 파괴된다. 이때는 CPU가 대상 명령어 주소로 PC 레지스터의 값을 동적으로 변경한다. 

그렇다면 최초의 PC레지스터 값은 어떻게 설정되는 것 인가? 이 질문을 위해서 명령어가 CPU에 도달하는 과정을 이해 할 필요가 있다. 

소스코드 -> 컴파일러 -> 실행파일 -> 디스크 -> 메모리 -> CPU

결국 명령어는 소스코드에서 나오는 것이고 시작점은 소스코드의 시작점 즉, main 함수에 대응하는 명령어 주소가 PC레지스터의 첫 명령어 주소로 주어진다.

2.CPU에서 운영체제까지

과거에 프로그램을 작성하던 방법을 보면 직접 메모리 영역을 찾고 함수의 진입점을 찾아 PC 레지스터를 설정하는 등 불편한 점이 많았다. 이를 보완하고자 Loader라는 것이 개발되었다. Loader를 실행하며 자동으로 프로그램을 메모리에 적재해준다. 프로그램이 실행 된 후에는 관리가 필요 할 것이다. 단일 코어 CPU 환경에서 다양한 프로그램을 동시에 실행하고 싶다면 어떻게 해야할까?

CPU는 한번에 하나의 작업만 할 수 있다. 그렇기 때문에 멀티태스킹을 구현하기 위해서는 여러 프로그램을 전환하면서 실행하는 방법이 필요하다. 전환 속도를 매우 빠르게하여 여러 프로그램이 동시에 실행중인 것 처럼 보이게 하는 것이다. 이 전환을 위해서 필요한 것이 바로 프로세스다.

프로세스는 프로그램의 상태를 저장한 상황정보(Context)를 구조체를 의미한다. 이 상황정보를 이용해서 프로그램 전환시 이전에 중단된 상태를 기억하고 다시 재개할 수 있는 것이다. 

3.프로세스는 훌륭하지만, 아직 불편하다

함수 A,B가 있고 이 함수들의 결과값을 더하는 코드가 있다고 가정해보자.

함수A -> 함수B -> 두 값을 더하기

위와 같은 실행흐름을 가지게 될 것이다. 여기서 생각해 봐야 할 것이 있다. 함수끼리는 값을 공유하지 않는데 동시에 실행할 순 없을까? 이때 멀티 프로세스를 활용할 수 있을 것 이라고 떠올릴 수 있다. 함수A와 함수B를 각각 다른 프로세스에서 실행시킨 후 B의 결과를 A프로세스로 전달하여 값을 더하는 방식으로 말이다. 하지만 이 방법은 큰 어려움이 존재한다. 따라서 새로운 개념이 필요하게 되었다.

1.프로세스 생성에 발생하는 오버헤드
2.프로세스마다 자체적인 메모리 공간이 존재하기 때문에 통신에 어려움

4.프로세스에서 스레드로 진화

프로세스의 단점은 진입 함수가 main 하나로 제한되어 CPU가 한번에 하나의 실행흐름만 가질 수 있는 것이다. 사실 PC레지스터는 어떠한 함수라도 가리킬 수 있으며 이를 통해 새로운 실행흐름을 만들 수 있다. 이렇게 실행흐름을 다수 만들게 된다면 동일한 프로세스 메모리 주소를 공유하기 때문에 통신 문제에서 자유로워진다. 이러한 방식은 하나의 프로세스에 속한 기계 명령어를 CPU 여러개에서 동시에 실행할 수 있도록 만들어 준 것이다. 다시 말해 단일 프로세스 내에 다수의 실행흐름이 존재하게 된 것이다. 이것을 스레드(Thread)라고 부른다.

스레드는 근본적으로 통신이라는 개념이 없다. 동일한 프로세스 메모리 공간을 공휴하기 때문이다. 이는 스레드가 프로세스보다 가볍고 생성속도가 빠른 이유이기도 하다. 

다중 스레드는 편의성을 많이 제공한다. 하지만 통신이 없다는 점에서 문제점이 발생한다. CPU가 명령어를 실행할 때 스레드는 고려하지 않기 때문이다. 따라서 상호배제,동기화를 이용하여 다중 스레드가 공유 리소스 문제를 명시적으로 직접 해결해야한다.

5.다중 스레드와 메모리 구조

메모리 공간 내의 스택 영역에는 명령어 실행을 위한 다양한 정보들이 저장된다. 프로세스 내에 단일 실행흐름이 존재했을때는 이 스택 영역이 하나만 존재했다. 하지만 스레드가 생겨난 후 여러 스택  영역이 필요하게 되었다. 따라서 모든 스레드는 각자 자신만의 스택 영역을 가지게 되었고 스레드가 이를 인지하고 있는 것은 매우 중요하다. 

6.스레드 활용 예

수명주기 관점에서 볼 때 스레드가 처리해야 할 작업은 크게 2가지고 구분해 볼 수 있다. 긴 작업과 짧은 작업이다. 긴 작업은 말 그대로 작업의 시작부터 끝까지의 시간이 긴 작업이다. 예를 들어 word 작업을 하게 되면 문서를 편집하여 디스크에 저장하게 된다. 이때 디스크에 저장하는 전용 스레드를 생성하여 사용하게 된다. word의 수명주기와 기록 수명주기는 동일하기 때문에 이 상황은 긴 작업에 해당한다. 이러한 상황에서 특정 작업 처리를 위해서는 전용 스레드를 생성하는 것이 적합하다. 

짧은 작업은 네트워크 요청,데이터베이스 쿼리 등 처리 시간이 매우 짧고 작업 수가 많은 작업이다. 이러한 작업에서 긴 작업과 동일하게 스레드를 생성하는 것은 큰 단점을 보인다. 

1.스레드 생성과 종료에 시간 소요
2.스레드 수에 맞는 스택 생성으로 인한 리소스 낭비
3.많은 스레드 간 전환에 부담 증가

이러한 이유로 스레드 풀 이라는 개념이 탄생했다.

7.스레드 풀의 동작 방식

스레드풀은 간단하다. 스레드 여러개를 생성해두고 스레드가 처리할 작업이 생기면 처리 후 다시 풀로 돌아온다. 즉 스레드의 재사용이 일어나게 된다. 그렇다면 작업을 어떻게 스레드 풀 내의 스레드로 전달할까? 바로 자료구조 queue를 사용한다. 작업 요청이 발생하면 queue에 쌓이게 된다. 이때 스레드 풀 내의 스레드들은 작업 queue내의 요청을 기다리고있다가 수행한 후 다시 풀로 돌아오게 된다.

8.스레드 풀의 스레드 수

그렇다면 스레드 풀 내의 스레드 수는 어느정도로 유지하는 것이 좋을까? 너무 많다면 과다한 메모리 점유, 많은 전환으로 발생하는 부담 등 문제가 발생한다. 너무 적다면 CPU를 최대한 활용할 수 없을 것이다. 작업을 리소스 관점에서 구분하고, 이에 따라 적정 스레드 수를 잡아볼 수 있다. 

먼저 CPU 집약적 작업이다. 작업 처리시 외부 입출력 의존 없이 처리할 수 있는 작업을 의미한다. 이 경우 스레드 수를 코어 수와 비슷하게 내지는 조금 더 많이 설정한다면 CPU를 최대한 활용할 수 있다.

다음으로 I/O 집약적 작업이다. 연산이 차지하는 시간은 적은 대신 디스크 입출력,네트워크 입출력 등에 소비하는 작업이다. 이때는 계산이 필요하다. 성능테스트 도구를 이용해 W/T(대기시간) 과 C/T(컴퓨팅 시간) 을 계산한다. N개 코어를 가진 시스템 기준 아래 처럼 계산 한다.

N * (1 + WT / CT)

W/T,C/T 가 동일하다고 가정하면 2N개의 스레드가 필요하다. 하지만 이 경우 식에 의존하지 않고 경험적으로 스레드 수를 찾아 가는 것이 더 중요하다.