본문 바로가기

프로그램이 하드웨어를 움직이는 과정

CPU, 주기억장치, 저장장치, 입출력 장치가 프로그램을 실행할 때 각각 맡는 일을 구분하고, 명령어도 데이터처럼 메모리에 두는 저장 프로그램 방식을 세 개의 명령과 작은 모의 실행기로 따라갑니다.

사진 편집기에서 밝기를 올리면 화면의 사진이 밝아집니다. 이 짧은 순간에도 여러 부품이 차례로 일합니다. 사진 파일은 저장장치에서 메모리로 올라와 있어야 하고, CPU는 픽셀 값을 가져와 더한 뒤 다시 메모리에 써야 하며, 바뀐 값이 화면으로 보내져야 합니다. 컴퓨터구조(computer architecture)는 이처럼 어떤 데이터가 언제, 어디에서 어디로 움직이는지를 설명하는 과목입니다. 이 글에서는 컴퓨터를 이루는 부품들의 역할을 나누고, 프로그램이 실행되는 가장 기본적인 흐름을 따라갑니다.

하드웨어와 소프트웨어

하드웨어(hardware)는 CPU, 메모리, 저장장치, 키보드와 화면처럼 물리적인 부품입니다. 소프트웨어(software)는 그 위에서 실행되는 프로그램으로, 명령어와 그 명령어가 다루는 데이터로 이루어집니다. 소프트웨어 가운데 운영체제(operating system)는 여러 프로그램이 CPU, 메모리, 파일, 장치를 나누어 쓸 수 있게 관리하는 시스템 소프트웨어이고, 사진 편집기처럼 사용자의 일을 직접 처리하는 프로그램은 응용 소프트웨어입니다. 운영체제가 이 자원들을 어떻게 나누어 관리하는지는 운영체제 교재의 운영체제란 무엇인가에서 다룹니다.

부품사진 편집기에서 하는 일헷갈리기 쉬운 점
CPU밝기 값을 더하고 비교한다사진을 오래 보관하는 곳이 아니다
주기억장치실행 중인 명령어와 사진 데이터를 담는다전원이 꺼지면 내용이 사라진다
저장장치프로그램과 사진 파일을 보관한다CPU가 그 자리에서 바로 계산하지 않는다
입출력 장치마우스 입력을 받고 화면에 출력한다장치마다 CPU와 주고받는 규칙이 필요하다

주기억장치(main memory)는 흔히 메모리 또는 RAM이라고 부르며, 전원이 꺼지면 내용이 사라지는 휘발성(volatile) 기억장치입니다. SSD나 하드 디스크 같은 저장장치(storage)는 전원이 꺼져도 내용이 남지만, CPU가 계산에 바로 쓰기에는 느려서 필요한 부분을 메모리로 옮겨 온 뒤 씁니다. 편집한 사진을 저장하지 않고 프로그램을 끄면 변경이 사라지는 이유가 여기에 있습니다. 바뀐 값은 메모리에만 있었고, 저장장치의 파일에는 아직 쓰이지 않았기 때문입니다.

CPU 안의 세 가지 역할

CPU(central processing unit)는 기능에 따라 세 부분으로 나누어 생각할 수 있습니다. 산술 논리 장치(arithmetic logic unit, ALU)는 덧셈이나 비교 같은 계산을 합니다. 레지스터(register)는 계산에 쓸 값을 CPU 안에 잠시 담아 두는 아주 작고 빠른 저장 자리입니다. 제어 장치(control unit)는 다음에 무엇을 어디로 옮기고 무엇을 계산할지 순서를 정합니다. 이 구분은 역할을 설명하기 위한 것이며, 실제 칩 안의 회로가 이렇게 세 덩어리로 나뉘어 있다는 뜻은 아닙니다.

저장 프로그램 방식

초기의 컴퓨터 가운데에는 하는 일을 바꿀 때마다 배선을 다시 연결해야 하는 것도 있었습니다. 저장 프로그램(stored program) 방식은 명령어도 데이터처럼 메모리에 넣어 두고, CPU가 메모리에서 명령어를 하나씩 가져와 실행하게 합니다. 그래서 하는 일을 바꾸려면 메모리의 명령어만 바꾸면 됩니다. 오늘날의 컴퓨터는 모두 이 방식을 따릅니다.

명령어와 데이터가 같은 메모리와 같은 통로를 쓰는 구조를 폰 노이만 구조(von Neumann architecture), 명령어와 데이터의 메모리와 통로를 따로 두는 구조를 하버드 구조(Harvard architecture)라고 합니다. 실제 CPU는 둘을 섞어 쓰는 경우가 많습니다. 주기억장치는 하나로 두면서, CPU 가까이에 두는 빠른 기억장치인 캐시는 명령어용과 데이터용으로 나누는 식입니다. 캐시는 6장에서 다룹니다.

더 깊이: 방금 쓴 명령어를 바로 실행할 수 있을까

명령어가 데이터처럼 메모리에 있으니, 프로그램이 메모리에 새 명령어를 쓰고 그 자리로 건너뛰어 실행할 수도 있습니다. Java나 JavaScript의 JIT 컴파일러가 실행 중에 기계어를 만들어 실행하는 것이 그런 예입니다. 그런데 명령어 캐시와 데이터 캐시를 따로 두는 CPU에서는, 데이터로 쓴 새 명령어가 명령어를 가져오는 쪽에 바로 보인다는 보장이 없습니다. 명령어 캐시에 옛 내용이 남아 있을 수 있기 때문입니다.

그래서 소프트웨어가 볼 수 있는 CPU의 명령어와 동작을 정한 명령어 집합 구조(instruction set architecture, ISA)는 이 둘을 맞추는 방법을 따로 정합니다. RISC-V 규격의 Zifencei 확장은 fence.i 명령을, 같은 하드웨어 스레드 안에서 명령어 메모리에 쓴 내용을 이후의 명령어 가져오기에 보이게 하는 현재 유일한 표준 방법으로 정합니다. 단순한 구현은 이 명령을 만나면 명령어 캐시와 파이프라인을 비웁니다. 다만 fence.i는 그 명령을 실행한 하드웨어 스레드에만 효과가 있어서, 운영체제가 프로그램을 다른 코어로 옮기면 소용이 없어집니다. 그래서 RISC-V의 표준 Linux ABI는 사용자 프로그램이 fence.i를 직접 쓰는 대신 시스템 콜로 커널에 요청하도록 정했습니다.

명령 세 개로 실행을 따라가기

메모리 주소 20에 7이 들어 있고 레지스터 R은 0이라고 합시다. 아래 명령은 이 교재에서 설명을 위해 쓰는 표기이며 특정 CPU의 실제 기계어는 아닙니다. M[20]은 주소 20에 든 값을, 화살표 ←는 오른쪽 값을 왼쪽 자리에 쓴다는 것을 뜻합니다.

순서명령실행 뒤 상태
1R ← M[20]R = 7, M[20] = 7
2R ← R + 3R = 10, M[20] = 7
3M[20] ← RR = 10, M[20] = 10

두 번째 명령에서 R은 10이 되지만 메모리의 값은 아직 7입니다. 메모리를 바꾸는 것은 세 번째 명령의 쓰기뿐입니다. 이처럼 읽기, 계산, 쓰기는 서로 다른 동작이고, CPU가 계산했다고 해서 메모리가 저절로 바뀌지는 않습니다.

CPU가 다음에 실행할 명령어의 주소는 프로그램 카운터(program counter, PC)라는 레지스터가 가리킵니다. CPU는 PC가 가리키는 명령어를 가져오고(fetch), 무슨 명령인지 해석하고(decode), 실행하는(execute) 일을 되풀이하며, 명령어를 가져올 때마다 PC를 다음 명령어로 옮깁니다. 명령어도 메모리에 있다는 저장 프로그램 방식을 그대로 흉내 낸 작은 모의 실행기로 위 표를 다시 만들어 보겠습니다.

memory = {0: ("load", 20), 1: ("add", 3), 2: ("store", 20), 3: ("halt", None), 20: 7}
r, pc = 0, 0
while True:
    op, arg = memory[pc]
    pc += 1
    if op == "halt":
        break
    if op == "load":
        r = memory[arg]
    elif op == "add":
        r += arg
    elif op == "store":
        memory[arg] = r
    print(f"PC={pc} {op} {arg}: R={r}, M[20]={memory[20]}")
PC=1 load 20: R=7, M[20]=7
PC=2 add 3: R=10, M[20]=7
PC=3 store 20: R=10, M[20]=10

memory는 주소를 키로 하는 딕셔너리이고, 주소 0부터 3까지에는 명령어가, 주소 20에는 데이터가 들어 있습니다. 반복할 때마다 PC가 가리키는 주소에서 명령어를 가져오고 PC를 1 늘린 뒤 실행하므로, 출력의 PC는 방금 실행한 명령의 다음 주소를 가리킵니다. 주소 3의 halt를 만나면 멈춥니다. 같은 memory에 명령어와 데이터가 함께 들어 있다는 점이 폰 노이만 구조의 모습이고, 실제 CPU는 명령어를 비트로 부호화해 저장한다는 점이 다릅니다. 이 흐름은 CPU 데이터 경로에서 레지스터와 ALU 사이의 연결과 함께 다시 봅니다.

컴퓨터의 종류

범용 컴퓨터는 여러 프로그램을 바꾸어 가며 실행하도록 만들고, 세탁기나 자동차의 제어기처럼 정해진 일에 맞추어 기계 안에 넣은 컴퓨터는 임베디드 시스템(embedded system)이라고 부릅니다. 디지털 컴퓨터는 값을 0과 1의 비트로 나누어 표현하고, 아날로그 장치는 전압 같은 연속적인 물리량을 그대로 씁니다. 센서의 연속적인 신호를 디지털 값으로 바꾸는 변환기가 두 방식을 잇습니다.

PC, 서버, 메인프레임, 슈퍼컴퓨터는 크기보다 쓰임새로 나누는 것이 정확합니다. 서버는 여러 사용자의 요청을 오래 안정적으로 처리하도록, 슈퍼컴퓨터는 큰 과학 계산을 수많은 처리 장치로 나누어 빠르게 하도록 설계합니다. 진공관에서 트랜지스터, 집적 회로로 이어진 역사는 같은 회로를 더 작고 빠르고 안정적으로 만드는 방향으로 발전해 온 과정입니다.

정리

  • CPU는 계산을, 주기억장치는 실행 중인 명령어와 데이터의 보관을, 저장장치는 전원이 꺼져도 남는 보관을, 입출력 장치는 바깥과의 교환을 맡습니다.
  • CPU는 기능에 따라 계산하는 ALU, 값을 잠시 담는 레지스터, 순서를 정하는 제어 장치로 나누어 볼 수 있습니다.
  • 저장 프로그램 방식은 명령어도 메모리에 두고 가져와 실행하며, 폰 노이만 구조는 명령어와 데이터가 메모리를 함께 쓰고 하버드 구조는 따로 씁니다.
  • CPU는 PC가 가리키는 명령어를 가져오고, 해석하고, 실행하는 일을 되풀이하며, 읽기, 계산, 쓰기는 서로 다른 동작입니다.

연습 문제

1. 명령 세 개의 결과

처음에 M[12] = 4, M[13] = 9, R = 0입니다. R ← M[12], R ← R + M[13], M[14] ← R을 차례로 실행한 뒤의 R, M[12], M[13], M[14]를 구하세요.

해설 보기

첫 명령으로 R = 4가 되고, 둘째 명령으로 R = 4 + 9 = 13이 되며, 셋째 명령이 그 값을 주소 14에 씁니다. 그래서 R = 13, M[12] = 4, M[13] = 9, M[14] = 13입니다. 메모리에서 값을 읽어도 원래 자리의 값은 지워지지 않으므로 M[12]와 M[13]은 처음 값 그대로입니다. 메모리를 바꾸는 명령은 왼쪽에 주소가 오는 마지막 명령 하나뿐입니다.

2. 저장하지 않고 끄면

사진을 편집한 뒤 저장하지 않고 편집기를 끄면 왜 변경이 사라지나요? 메모리와 파일의 차이로 설명하세요.

해설 보기

편집한 내용은 실행 중인 편집기가 쓰는 주기억장치에만 있습니다. 저장 명령은 메모리의 내용을 저장장치의 파일에 쓰는 별도의 작업인데, 이것을 하지 않았으므로 파일은 편집 전 그대로입니다. 편집기가 끝나면 그 프로그램이 쓰던 메모리는 운영체제가 거두어 다른 용도로 쓰고, 전원을 끄면 휘발성인 메모리의 내용은 모두 사라집니다. 그래서 다시 파일을 열면 편집 전의 사진이 나옵니다.

3. 명령어를 덮어쓰면

모의 실행기에서 주소 1의 명령을 ("store", 2)로 바꾸면 어떻게 될까요?

해설 보기

주소 0의 load로 R = 7이 된 뒤, 주소 1의 store가 R의 값 7을 주소 2에 씁니다. 주소 2에는 원래 add 명령어가 있었으므로, 데이터가 명령어를 덮어쓴 것입니다. 다음 반복에서 PC가 2를 가리켜 정수 7을 명령어로 가져오면, 튜플로 풀 수 없어 TypeError: cannot unpack non-iterable int object가 납니다. 실제 CPU라면 7의 비트를 어떤 명령어로 해석해 엉뚱한 일을 하게 됩니다. 명령어와 데이터가 같은 메모리에 있기 때문에 생기는 일이며, 운영체제가 프로그램의 코드 영역을 쓰기 금지로 보호하는 이유이기도 합니다.

부품의 역할이 헷갈리면 값이 출발한 자리와 도착한 자리를 화살표로 적어 보세요. 다음 글 비트·바이트·주소에서는 메모리의 자리를 세는 단위와 주소를 붙이는 방법을 다룹니다.