-
[컴퓨터 밑바닥의 비밀] 2.1 운영체제, 프로세스, 스레드Study/컴퓨터 밑바닥의 비밀 2025. 3. 1. 18:21
2.1.1 모든 것은 CPU에서 시작된다
- CPU는 스레드, 프로세스, 운영체제 같은 개념을 알지 못함
- CPU는 단지 다음 두 가지 상항만 알고 있음
- 메모리에서 명령어를 하나 가져온다.(dispatch)
- 이 명령어를 실행한 후 다시 1로 돌아간다.
Q. CPU는 어떤 기준으로 메모리에서 명령어를 가져올까?
- PC(program counter) 레지스터에 저장된 명령어 주소를 사용해 가져온다.
- 레지스터는 용량은 매우 작지만 속도는 빠른 일종의 메모리
- PC 레지스터에 저장되는 것은 CPU가 다음에 실행할 명령어의 주소이다.
Q. PC 레지스터의 명령어 주소는 누가 설정할까?
- PC 레지스터가 저장하는 주소는 기본적으로 1씩 자동 증가한다.
- 대부분 CPU가 주소를 하나씩 증가시키면서 차례대로 명령어를 실행하기 때문
- if else 또는 함수 호출 같은 명령어를 실행할 때는 연산 결과 또는 명령어에서 지정한 점프할 대상 주소에 따라 CPU가 PC 레지스터 값을 동적으로 변경한다.
Q. 최초의 PC 레지스터 값은 어떻게 설정될까?
- CPU가 실행하는 기계 명령어는 프로그램이 작성된 코드로부터 생성된다.
- 작성하는 프로그램에는 반드시 시작 지점이 있어야 하고, main 함수가 바로 그것이다.
- 프로그램이 시작되면 먼저 main 함수에 대응하는 첫 번째 기계 명령어를 찾고, 이어서 그 메모리 주소를 PC 레지스터에 기록한다.
- 실제 실행될 때는 main 함수를 실행하기 전 레지스터 초기화 과정이 추가로 진행됨
2.1.2 CPU에서 운영체제까지
- CPU가 프로그램을 실행하게 하려면 다음 모든 작업을 직접 해야 함
- 프로그램을 적재할 수 있는 적절한 크기의 메모리 영역을 찾고, 메모리에 적재한다.
- CPU 레지스터를 초기화하고 함수의 진입 포인트(entry point)를 찾아 PC 레지스터에 설정한다.
- CPU의 멀티태스킹을 가능하게 하려면
- CPU는 한 번에 한 가지 일만 할 수 있음
- CPU의 전환 빈도가 충분히 빠르다면 여러 프로그램이 '동시에 실행' 되는 것처럼 보임 (멀티태스킹)
- 프로그램 입장에서는 일시 중지되었다가 다시 시작되는 것을 반복하는 것
Q. 어떻게 하면 프로그램을 일시 중지했다가 다시 시작할 수 있을까?
- 프로그램의 실행 상태를 저장해야 하고, 이때 저장되는 상태를 상황 정보(context)라고 함
- CPU가 어떤 기계 명령어를 실행했는지, CPU 내부의 기타 레지스터 값 등의 상태 값
- 실행 중인 모든 프로그램은 필요한 정보를 기록할 수 있는 구조체를 가지고 있어야 하고, 이를 '프로세스'라고 명칭함
- 모든 프로그램은 실행된 후 프로세스 형태로 관리됨
- 프로세스를 사용하면 모든 프로세스를 원하는 대로 일시 중지하거나 다시 시작할 수 있음
- 따라서 CPU가 프로세스 사이를 충분히 빠르게 전환하는 한, CPU가 하나뿐인 시스템에서도 수많은 프로세스를 동시에 실행하거나 동시에 실쟁 중인 것처럼 보이게 할 수 있음
운영체제
- 위의 기능들을 제공해주는 도구를 '운영체제'라고 명칭함
- 프로그램을 자동으로 메모리에 적재해 주는 적재 도구
- 멀티태스킹을 실현해 주는 프로세스 관리 도구
- 이러한 기능들을 백그라운드에서 처리함
2.1.3 프로세스는 매우 훌륭하지만, 아직 불편하다
예시)
int main() { int resA = funcA(); int resB = funcB(); print(resA + resB); return 0; }
위 프로그램이 실행 중일 때 메모리 내 해당 프로세스의 상황
- 운영체제의 가상 메모리를 통해 프로세스는 독립적인 주소 공간을 갖음
- 코드 영역 : 코드를 컴파일하여 생성된 기계 명령어 저장
- 데이터 영역 : 전역 변수 등 저장
- 힙 영역 : malloc 함수가 요청을 반환한 메모리가 여기에 할당
- 스택 영역 : 함수의 실행 시간 스택
Q. 위 프로그램의 전체 실행 속도를 높일 수 있을까?
- 상황
- 함수가 순차적으로 수행되어 funcA 함수의 실행이 끝날 때까지 funcB 함수는 실행되지 않는다.
- 방안
- funcA와 funcB 함수가 독립적일 때 funcA 함수의 실행이 끝날 때까지 기다리지 않아도 된다.
- 다중 프로세스 프로그래밍을 사용한다.
- 프로세스 A, B를 각각 생성하여 함수를 실행하고, 프로세스 B의 결과를 프로세스 A에 전달함
- 이때, 프로세스 사이에 통신 문제가 발생할 수 있음
다중 프로세스 프로그래밍의 단점
- 프로세스 생성할 때 비교적 큰 부담(overhead)이 발생한다.
- 프로세스마다 자체적인 주소 공간을 가지고 있기 때문에 프로세스 간 통신은 프로그래밍하기에 더 복잡하다.
2.1.4 프로세스에서 스레드로 진화
- 프로세스 단점은 진입 함수가 main 함수 하나밖에 없어 프로세스의 기계 명령어를 한 번에 하나의 CPU에서만 실행할 수 있다는 것
Q. CPU 여러 개가 동일한 프로세스의 기계 명령어를 실행하게 할 방법은 없을까?
- PC 레지스터가 main 함수를 가리키게 할 수 있듯이 PC 레지스터가 다른 함수를 가리키게 하여 새로운 실행 흐름을 형성할 수 있다.
- 중요한 것은 이런 실행 흐름이 동일한 프로세스 주소 공간을 공유하므로 더 이상 프로세스 간 통신이 필요하지 않게 된다.
- 하나의 프로세스에 진입 함수가 2개 이상 있을 수 있고, 하나의 프로세스에 속한 기계 명령어를 CPU 여러 개에서 동시에 실행할 수 있다.
스레드
- 하나 혹은 여러 개의 CPU가 공유 프로세스 주소 공간에서 동일한 프로세스에 속한 명령어를 동시에 실행할 수 있음
- 즉, 하나의 프로세스 안에 여러 실행 흐름이 존재할 수 있음
- 이러한 실행 흐름을 '스레드'라고 명칭함
- 프로세스를 시작하고 스레드 여러 개를 생성하여 다중 코어를 충분히 이용해 모든 CPU를 최대한 활용할 수 있음
예시)
int resA; int resB; void funcA() { resA = 1; } void funcB() { resB = 2; } int main() { thread ta(funcA); thread tb(funcB); ta.join(); tb.join(); print(resA + resB); return 0; }
- 두 값을 더하는 과정에서 프로세스 간 통신이 일어나지 않았음
- 스레드 사이에는 근본적으로 통신의 개념이 존재하지 않음
- 서로 다른 주소 공간이 아닌 동일한 프로세스 주소 공간에 속하기 때문
- 스레드는 자신이 속해 있는 프로세스의 주소 공간을 공유함
- 스레드가 프로세스보다 훨씬 가볍고 생성 속도가 빠른 이유
- 스레드를 경량 프로세스라고도 함
- 주의) 다중 스레드가 공유 리소스에 접근할 때
- CPU가 명령어를 실행할 때 스레드를 전혀 고려하지 않기 때문에 공유 리소스 접근 시 오류가 발생할 수 있음
- 상호 배제(mutual exclusion)와 동기화(synchronization)를 이용해 명시적으로 직접 해결해야 함
2.1.5 다중 스레드와 메모리 구조
- CPU의 PC 레지스터에 스레드의 진입 함수 주소를 지정하면 스레드를 실행시킬 수 있음
- 스레드를 생성할 때 진입 함수를 반드시 지정해야 하는 이유
Q. 스레드와 메모리는 어떤 관련이 있을까?
- 여러 개의 실행 흐름(스레드) 가지는 프로세스는 각 흐름이 실행될 때의 정보를 저장하기 위해 스택 영역이 여러 개 필요함
- 프로세스의 주소 공간에 각 스레드를 위한 스택 영역이 별도로 있어야 함 (즉, 모든 스레드는 자신만의 스택 영역을 갖음)
- 참고) 스택 영역
- 함수가 실행될 때 필요한 정보에는 함수의 매개변수, 지역 변수, 반환 주소 등이 있음
- 이런 정보는 대응하는 스택 프레임에 저장되고, 모든 함수는 실행 시에 자신만의 실행 시간 스택 프레임(runtime stack frame)을 갖음
- 함수가 호출되고 반환될 때마다 스택 프레임은 후입선출(last in first out) 순서로 증가하거나 감소함
- 이런 스택 프레임의 증감이 프로세스 주소 공간에서 스택 영역을 형성함
- 따라서 스레드를 생성하면 프로세스의 메모리 공간이 소모됨
2.1.6 스레드 활용 예
- 요청당 스레드(thread-per-request)
- 요청이 들어올 때마다 매번 스레드를 생성하는 것
- 구현이 간단한 장점이 있지만, 대량의 짧은 작업을 처리할 때는 다음의 단점이 존재함
- 스레드의 생성과 종료에 많은 시간 허비
- 스레드마다 독립적인 스택 영역이 필요한데, 많은 수의 스레드를 생성하면 메모리와 기타 시스템 리소스 낭비
- 스레드 수가 많으면 스레드 간 전환에 따른 부담 증가
- 이를 보완하기 위한 것이 바로 '스레드 풀(thread pool)'임
2.1.7 스레드 풀의 동작 방식
- 스레드 여러 개를 미리 생성해 두고, 스레드가 처리할 작업이 생기면 해당 스레드에 처리를 요청하는 것
- 중요한 점은 스레드를 재사용하는 것
- 장점
- 스레드 여러 개가 미리 생성되어 있어 스레드의 생성과 종료 작업이 빈번하게 발생하지 않음
- 스레드 풀 내 스레드 수가 일정하게 관리되므로 불필요하게 많은 메모리를 소비하지 않음
Q. 스레드 풀 내에 있는 스레드에 작업을 어떻게 전달할까?
- Queue 사용
- 작업을 전달하는 것은 생산자(producer), 작업을 처리하는 스레드는 소비자(consumer)임 (생산자-소비자 패턴)
struct task { void* data; // 작업이 처리할 데이터 handler handle; // 데이터 처리 함수 }
- 스레드 풀에 전달되는 작업
- 처리할 데이터, 데이터를 처리하는 함수 두 부분으로 구성
while (true) { struct task = GetFromQueue(); // 작업 대기열에서 데이터 꺼내기 task -> handle(task -> data); // 데이터 처리 }
- 스레드 풀의 스레드는 블로킹 상태로 대기함
- 생산자가 작업 대기열에 데이터를 기록하면 스레드 풀의 스레드가 깨어나고, 작업 대기열에서 정의한 구조체를 가져온 후 구조체의 handle이 가리키는 처리 함수(handler function)를 실행함
주의) 작업 대기열(task queue)은 여러 스레드 간 공유되는 리소스이므로 동기화 문제 처리 필요함
2.1.8 스레드 풀의 스레드 수
- 스레드 풀의 스레드 수
- 너무 적으면? CPU를 최대한 활용할 수 없음
- 너무 많으면? 시스템의 성능 저하, 메모리의 과다한 점유, 스레드 전환으로 생기는 오버헤드 등 문제 발생 가능함
Q. 몇 개가 적당할까?
- 스레드 풀에서 처리할 작업 고려
- 작업을 처리할 때 필요한 리소스 관점으로 작업 구분
- CPU 집약적인 작업 (CPU Intensive Task)
- I/O 집약적인 작업 (Input/Output Intensive Task)
- CPU 집약적인 작업
- 작업 처리할 때 외부 입출력에 의존할 필요 없이 처리할 수 있는 작업
- 스레드 수와 CPU 코어 수를 동일하게 하면 CPU의 리소스를 충분히 활용 가능
- I/O 집약적인 작업
- 대부분의 시간을 디스크 I/O나 네트워크 I/O 등에 소비하는 작업
- 이론상
- 성능 테스트 도구 사용해 입출력 대기 시간(WT: Waiting Time)과 CPU 연산에 필요한 시간(CT: Computing Time)을 평가해야 함
- N개의 코어를 가진 시스템에서 적절한 스레드 수 = N x (1 + WT / CT)
- WT와 CT가 동일하다고 가정하면, 대략 2N개의 스레드가 있어야 함
- 스레드 수를 결정하는 절대 공식은 없고, 구체적인 상황과 분석이 필요함
'Study > 컴퓨터 밑바닥의 비밀' 카테고리의 다른 글
[컴퓨터 밑바닥의 비밀] 3.3 스택 영역: 함수 호출은 어떻게 구현될까? (0) 2025.03.09 [컴퓨터 밑바닥의 비밀] 2.7 블로킹과 논블로킹 (0) 2025.03.02 [컴퓨터 밑바닥의 비밀] 2.3 스레드 안전 코드 (0) 2025.03.01 [컴퓨터 밑바닥의 비밀] 2.2 스레드 간 공유되는 프로세스 리소스 (0) 2025.03.01 [컴퓨터 밑바닥의 비밀] 2.6 동기와 비동기 (0) 2025.03.01