ABOUT ME

Today
Yesterday
Total
  • [컴퓨터 밑바닥의 비밀] 2.1 운영체제, 프로세스, 스레드
    Study/컴퓨터 밑바닥의 비밀 2025. 3. 1. 18:21

    2.1.1 모든 것은 CPU에서 시작된다

    • CPU는 스레드, 프로세스, 운영체제 같은 개념을 알지 못함
    • CPU는 단지 다음 두 가지 상항만 알고 있음
      1. 메모리에서 명령어를 하나 가져온다.(dispatch)
      2. 이 명령어를 실행한 후 다시 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에 전달함
    • 이때, 프로세스 사이에 통신 문제가 발생할 수 있음

     

    다중 프로세스 프로그래밍의 단점

    1. 프로세스 생성할 때 비교적 큰 부담(overhead)이 발생한다.
    2. 프로세스마다 자체적인 주소 공간을 가지고 있기 때문에 프로세스 간 통신은 프로그래밍하기에 더 복잡하다.

     

    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)
      • 요청이 들어올 때마다 매번 스레드를 생성하는 것
      • 구현이 간단한 장점이 있지만, 대량의 짧은 작업을 처리할 때는 다음의 단점이 존재함
        1. 스레드의 생성과 종료에 많은 시간 허비
        2. 스레드마다 독립적인 스택 영역이 필요한데, 많은 수의 스레드를 생성하면 메모리와 기타 시스템 리소스 낭비
        3. 스레드 수가 많으면 스레드 간 전환에 따른 부담 증가
    • 이를 보완하기 위한 것이 바로 '스레드 풀(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. 몇 개가 적당할까?

    • 스레드 풀에서 처리할 작업 고려
    • 작업을 처리할 때 필요한 리소스 관점으로 작업 구분
      1. CPU 집약적인 작업 (CPU Intensive Task)
      2. 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개의 스레드가 있어야 함
    • 스레드 수를 결정하는 절대 공식은 없고, 구체적인 상황과 분석이 필요함

    댓글

Designed by Tistory.