0. 서론

프로그래머는 코드를 작성해 컴퓨터에게 명령을 내리는 사람들이다. 여기서 중요한 건, 대부분의 프로그래머는 단지 코드를 작성해 컴퓨터에게 건넬 뿐 작성한 코드가 처리되는 과정에는 크게 관여하지 않는다는 점이다(물론 그 과정 역시 프로그래머들의 작품이다). 그렇다면 전달한 코드가 실행되기 위해 처리되는 대부분의 작업은 컴퓨터가 묵시적으로 수행해 준다는 것을 의미하는데, 과연 이 과정이 어떻게 진행되는지를 살펴보고자 한다.

1. 살펴볼 프로그램

우리가 살펴볼 프로그램은 C언어로 작성된 매우 유명한 예제이며, 다음과 같다.

#include <stdio.h>

int main() {
    printf("Hello World!");

}

2. 작성한 프로그램의 실체

프로그래머라면 당연히 아스키 코드(ASCII Code)에 대한 개념을 알 것이다. 아스키 코드가 우리에게 주는 의미는 ‘컴퓨터 상의 모든 데이터는 비트 수준의 수로 이루어져 있다’는 것이다. 정수, 실수, 주소, 문자, 문자열 등 모든 데이터들은 메모리, 레지스터, 디스크 등의 비트 단위 저장 장치 상에서 0 또는 1의 집합으로 존재하며, 이들은 단지 해당 데이터가 사용되는 시점의 문맥context에 의지해 해석된다.

즉, 예시 프로그램에서 #include~ 로 시작하는 프로그램 코드는 저장 장치 상에 모두 해당 코드의 아스키 코드에 사상되는 비트열로 저장되어 있으며(물론 컴파일 시스템에 의해 기계어로 저장되어 있을 것이다), 출력할 Hello World! 문자열 역시 마찬가지이다.

3. 실행되기 전에 어느 곳에 존재하는가

아무튼 우리가 살펴본 프로그램이 실제로는 비트열 데이터로 저장된다는 것을 안다면, 이들은 어디에 저장되어 있을까? 사실 너무 당연한 답이지만 정답은 ‘프로그램이 실행될 시스템의 보조 저장 장치’이다.

실행되지 않는 프로그램은 모두 보조 저장 장치에 잠들어 있으며, 이들은 실행될 때 주 기억 장치Main Memory로 적재된다. 이렇게 주 기억 장치에 적재돼 실행 준비를 마친, 혹은 실행 중인 프로그램을 ‘프로세스Process’ 라 부르게 되며, 프로그램과 달리 이들은 자신이 현재 얼마나 실행되었는지, 어떤 연산을 수행하고 있었는지를 PCB(Process Context Block)의 형태로 가지게 된다.

4. 메모리에서 프로세서로

메모리에 적재된 프로세스는 프로세서(CPU)에 의해 처리가 시작되는데, 이는 대략적으로 다음과 같은 형태로 수행된다.

  • CPU의 PC(Program Counter)가 프로세스의 명령어 주소를 가리킨다.
  • 해당 주소에서 명령어를 가져와 해독한다(이 과정에서 버스 및 MAR, MBR, IR, CU등 다양한 레지스터가 사용된다)
  • 해독된 명령어에 맞게 명령을 수행한다(이 역시 다양한 레지스터와 버스의 도움을 받는다)
  • PC를 1(Word) 증가시켜 다음 명령어 주소를 가리킨다.

이 과정을 프로세스의 모든 명령어를 수행할 때까지 반복하게 된다.

5. 캐시(Cache)

여기서 중요한 점은 명령어와 데이터가 이동하는 경로이다. 맨 처음 우리의 Hello World 프로그램은 보조 저장 장치 상에 존재했지만, 프로그램 실행 시 메모리에 적재되고, 프로그램이 한 줄씩 실행되면서 필요한 데이터들이 메모리에서 레지스터로 적재된다. 이처럼 컴퓨터 시스템의 저장 장치들은 계층 구조를 지닌 채로 아랫 계층에서 윗 계층으로 순차적으로 적재되는데, 이러한 메모리 계층 구조는 캐시Cache의 탄생으로 이어지게 된다.

만약 메모리에서 꺼내온 데이터를 사용한 직후 또 사용해야 한다면, 다시 메모리에서 데이터를 가져오는 것 보다 레지스터에 잠시 저장해 놓은 채로 이를 다시 사용하는 것이 물리적으로 훨씬 빠를 것이다. 이러한 ‘캐싱’ 아이디어는 실제 컴퓨터 시스템에서 큰 효율 개선을 제공하는데, 이는 지역성의 원리에 기반한다.

시간적, 공간적, 순차적 지역성에 의해 대부분의 코드와 데이터는 한 번 사용될 경우 빠른 시간 내에 재사용 될 가능성이 높고, 이러한 데이터를 매 번 메모리에서 프로세서로 가져오는 대신 그 사이에 작지만 고속의 저장 장치인 캐시를 두어 데이터의 중간 경유지로 삼는다. 이를 통해 재사용될 데이터를 메모리까지 가지 않아도 캐시에서 찾아 가져올 수 있어 프로그램의 수행 속도가 크게 상승한다.

6. 멀티프로세스와 문맥 교환

지금까진 우리가 실행하고자 하는 프로그램의 실행 과정만을 알아보았지만, 실제 프로그램이 실행되기 전에 다른 프로그램이 실행되고 있었다면 어떨까? 새로운 프로그램이 수행되기 위해 기존에 실행하던 프로그램이 종료되어야 할까? 우리는 이것이 아니라는 걸 잘 알고 있다. 현대의 CPU는 멀티프로세서 구조를 지원하며, 여기에는 시분할time-sharing 시스템을 통해 여러 프로그램을 수행하더라도 이들이 동시에 수행되는 것 처럼 착각하게 하는 기법이 함께 쓰이게 된다.

본질적으로 한 번에 하나의 프로세스만을 수행할 수 있는 프로세서에게 시분할 시스템은 매우 짧은 시간마다 수행하는 프로세스를 바꿔 주는 시스템 방식인데, 이처럼 프로세스가 교체되는 것을 문맥 교환Context Switch이라 한다. 중요한 점은 문맥 교환이 일어날 때마다 수행이 중지된 프로세스가 추후에 재개되기 위한 정보들을 저장해 놓을 필요가 있다는 것이다. 이는 앞서 언급한 PCB를 이용하는데, OS는 PCB의 리스트를 관리하며 각 프로세스가 교체될 때 마다 해당 프로세스의 PCB에 현재 PC가 가리키는 위치, 레지스터의 값들 등을 저장해 놓음으로써 추후 프로세스가 재개되더라도 직전의 상태에서 부드럽게 작업이 이어 수행될 수 있도록 한다.

7. 멀티스레드

직전의 내용을 통해 현대 컴퓨터 시스템은 여러 개의 프로세서를 동시에(실제로는 매우 빠른 시간마다 바꿔 가면서) 수행할 수 있음을 알게 되었다. 그렇다면 개개의 프로세스 입장에선 어떨까? 모든 프로세스는 적어도 한 개의 흐름을 가지게 되는데, 과거와 달리 현대 프로세서 아키텍처는 한 프로세스 내에서도 여러 흐름을 가질 수 있도록 해 준다. 이 때 이 흐름을 스레드Thread라 하며, 여러 스레드를 이용해 프로세스를 수행하는 것을 멀티스레드Multi-Thread 방식이라 한다.

멀티스레드 방식은 멀티프로세스 방식과 달리 스레드 간의 데이터 공유가 훨씬 간단하다는 장점이 있으며, 일반적으로 그에 대비되는 싱글스레드Single-Thread방식에 비해서도 좋은 효율을 보이지만 각 스레드들이 무분별하게 경쟁하는 과정에서 논리적 결함이 생길 가능성이 높다는 점이 단점으로 존재하며, 그 외에도 실제 프로그램이 병렬 효율성을 보장하기 위한 구조로 짜여져 있지 않다면 그 효율이 의미를 가질 만큼 좋지 않을 수도 있다는 단점 역시 존재한다.

우리가 살펴본 Hello World! 프로그램은 너무나도 간단해 병렬 수행이 가능할 여지가 없다시피 한데, 이러한 경우도 역시 병렬 효율성을 보장하지 않는 구조의 일종이라 볼 수 있을 것이다.

8. 결론

이렇게 한 프로그램이 사용자에 의해 수행되는 과정을 개략적으로 살펴 보며 그 기저에 놓인 컴퓨터 시스템의 개념들을 살펴보았다. 실제로는 여기서 언급하지 않은 다양한 개념들이 존재하며, OS의 역할이 글의 흐름보다 훨씬 더 강조되게 된다. 프로그래머라면 이 글에 모두 담기지 못한 개념들에 대해서도 한 번쯤 살펴보는 것을 추천한다.