
프로그램이 실행되어 프로세스가 된다는 것은 저장장치에 있던 프로그램이 메모리로 올라온다는 것입니다.
메모리의 구조는 1바이트(B) 크기로 나뉩니다. 1B로 나뉜 각 영역은 주소(address)로 구분되는데 보통 0번지부터 시작합니다. CPU는 메모리에 있는 데이터를 가져오거나 자업 결과를 메모리에 저장하기 위해 주소를 사용합니다.
운영체제도 프로그램이므로 메모리에 올라와야 실행할 수 있습니다. 컴퓨터의 전원 버튼을 누르면 사용자가 컴퓨터를 사용할 수 있는 환경을 마련하는 과정, 즉 부팅이 이루어지는데, 이때 하드디스크에 저장된 운영체제가 메모리에 올라갑니다. 부팅이 끝나면 여러 응용 프로그램이 메모리에서 작업을 시작할 수 있습니다.
메모리는 매우 빠른 장치 같지만, CPU 입장에서 보면 느린 장치입니다. CPU 안에 있는 레지스터에 접근하는 속도보다 메모리에 접근하는 속도가 몇 배 이상 느립니다.
이러한 속도 차이를 극복하기 위해 CPU 안에 캐시(cache)를 만들어두고 메모리의 일정 부분을 미리 가져와(prefetch) 작업합니다.
메모리를 계층적 구조로 만들어 작업 속도를 올리고 가격을 낮추는 방법을 계층적 메모리 구조라고 합니다. 메모리 관리의 복잡성은 충분히 크지 않은 메모리에서 여러 작업을 동시에 실행하는 문제에서 비롯되었습니다.
메모리 관리는 운영체제를 비롯해 여러 작업을 동시에 처리할 때 메모리를 어떻게 관리하는가에 관한 문제입니다. 이처럼 복잡한 메모리 관리는 메모리 관리 시스템(MMS; Memory Management System)이 담당합니다.
메모리 공간 관리는 매우 복잡합니다. 프로세스가 작업하는 도중에 할당된 공간이 부족하면 메모리 관리자는 새로운 공간을 확보하기 위해 옆의 프로세스를 밀어내거나 더 큰 공간으로 해당 프로세스를 옮겨줍니다. 빈 공간이 생기면 다음 작업을 위해 합쳐서 하나의 큰 공간을 만듭니다. 이때 현재 작업 중인 공간을 옆으로 밀고 작은 공간을 합칩니다. 매번 작업 공간을 키웠다 줄였다 하는 것은 매우 복잡한 일입니다.
프로세스 입장에서는 메모리를 독차지하려고 하고, 메모리 관리자 입장에서는 관리를 효율적으로 하고 싶어하는데 이를 메모리 관리의 이중성이라고 합니다. 메모리 관리의 이중성을 달리 표현하면 프로세스 입장에서 작업의 편리함과 관리자 입장에서 관리의 편리함이 충돌하는 것입니다. 현대의 메모리 관리 시스템은 프로세스와 메모리 관리자의 상충되는 요구 사항을 완벽하게 처리합니다.
컴퓨터에서 작동하는 응용 프로그램은 프로그래밍 언어로 만들며, 작성한 프로그램은 보통 컴파일러를 사용하여 실행 가능한 코드로 변경합니다.
기계어와 어셈블리어는 컴퓨터의 동작을 가장 직접적으로 표현한 언어로 저급언어(low level language)라고 합니다. 반대되는 개념인 고급언어(high level language)는 사용자가 이해하기 쉽게 프로그래밍할 수 있는 언어로, C언어와 자바가 대표적인 예입니다.
언어 번역 프로그램은 고급언어로 작성한 소스코드를 컴퓨터가 실행할 수 있는 기계어로 번역합니다. 대표적인 것이 컴파일러(compiler)와 인터프레터(interpretor)입니다. 컴파일러를 사용하는 방식에서는 소스코드를 컴퓨터가 실행할 수 있는 기계어로 번역한 후 한꺼번에 실행합니다. C언어, 자바 등이 이 방식으로 프로그램을 실행합니다. 인터프리터를 사용하는 방식에서는 소스코드를 한 행씩 번역하여 실행합니다. 자바스크립트, 파이썬 등이 이 방식으로 프로그램을 실행합니다.
컴파일러 방식을 사용하는 목적은 다음과 같습니다.
오류 발견
컴파일러 방식의 첫 번째 목적은 소스코드에서 오류를 발견하여 실행 시 문제가 없도록 하는 것입니다. 컴파일러는 오류를 찾기 위해 심벌 테이블(symbol table)을 사용합니다. 심벌 테이블은 변수 선언부에 명시한 각 변수의 이름과 종류를 모아놓은 테이블로, 선언하지 않은 변수를 사용하지는 않았는지, 변수에 다른 종류의 데이터를 저장하지는 않았는지 알 수 있습니다.
소스코드 최적화
컴파일 방식을 사용하는 두 번째 목적은 최적화입니다. 소스코드에서도 군더더기와 사용하지 않는 변수를 삭제하면 더욱 간결해져 실행 속도가 빨라집니다.
컴파일러를 사용하는 프로그래밍 언어는 사용할 변수를 먼저 선언한 후 코드를 작성합니다. 이 과정이 다소 번거롭게 느껴질 수 있으나 변수 선언은 오류를 찾고 코드를 최적화하기 위해 반드시 필요한 작업입니다.
컴파일러는 실행 전에 소스코드를 점검하여 오류를 수정하고 필요 없는 부분을 정리하여 최적화된 실행 파일을 만듭니다. 그러나 인터프리터는 한 행씩 위에서부터 아래로 실행되기 때문에 같은 일을 반복하는 경우나 필요 없는 변수를 확인할 수 없습니다.
컴파일러는 오류가 있는지 점검하고 최적화를 통해 필요 없는 변수와 코드를 삭제합니다. 이렇게 해서 만들어진 기계어 코드가 목적 코드(object code)로, 확장자는 obj입니다. 기계어 코드라고 하지 않고 목적 코드라고 부르는 이유는 실행 전 단계의 코드이며 아직 처리할 단계가 남아 있기 때문입니다. 목적 코드는 기계어 코드로 가기 직전의 초벌 번역 상태입니다.
목적 코드가 만들어지면 라이브러리에 있는 코드를 목적 코드에 삽입하여 최종 실행 파일을 만듭니다.
printf() 문에 더 좋은 기능이 추가되었다고 가정해봅시다. 과거에는 새로운 printf() 문의 실행 코드가 담긴 <stdio.h> 라이브러리를 가져와 다시 컴파일해야 했습니다. 하지만 매번 새로운 라이브러리를 가져와 다시 컴파일하는 것은 번거로운 일입니다. 오늘날의 프로그래밍에서는 printf() 문의 자리를 비워놓고 컴파일한 후 printf()문의 실행 코드를 라이브러리에서 가져와 실행하는 방법을 사용합니다. 이렇게 실행할 때 삽입되는 함수를 가진 라이브러리를 동적 라이브러리(dynamic library)라고 합니다. 동적 라이브러리 방식에서는 함수가 변경되어도 새로 컴파일할 필요 없이 새로운 라이브러리만 사용하여 실행하면 됩니다.
윈도우에서는 동적 라이브러리 파일을 DLL(Dynamic Link Loader) 이라고 합니다. 윈도우에서 함수의 변경이 일어나면 해당 DLL 파일을 특정 폴더에 삽입하여 새로운 기능을 사용할 수 있습니다.
메모리 관리 작업은 크게 가져오기(fetch), 배치(placement), 재배치(replacement)로 구분됩니다.
메모리 가져오기
실행할 프로세스와 데이터를 메모리로 가져오는 작업입니다.
메모리 배치
가져온 프로세스와 데이터를 메모리의 어떤 부분에 올려놓을지 결정하는 작업입니다. 이렇게 나뉜 메모리의 구역에 따라 프로세스와 데이터를 어떤 위치에 놓을지 결정하는 것이 바로 배치 작업입니다.
메모리를 같은 크기로 자르는 것을 페이징(paging)이라 하고, 프로세스의 크기에 맞게 자르는 것을 세그먼테이션(segmentation)이라고 합니다. 페이징은 책의 모든 페이지가 같은 크기인데에서 유래된 용어입니다.
메모리 재배치
꽉 찬 메모리에 새로운 프로세스를 가져오기 위해 오래된 프로세스를 내보내는 작업을 재배치 작업이라고 합니다. 앞으로 사용하지 않을 프로세스를 찾아서 내보내는 알고리즘을 교체 알고리즘(replacement algorithm)이라고 합니다.
메모리에 접근할 때는 주소를 이용합니다. 메모리 주소는 물리 주소와 논리 주소로 나뉩니다.
CPU를 나타낼 때의 비트(bit)는 CPU가 한 번에 다룰 수 있는 데이터의 최대 크기를 의미합니다. CPU 내부 부품은 모두 이 비트를 기준으로 제작됩니다. 32bit CPU 내의 레지스터 크기는 전부 32bit이고, 산술논리 연산장치도 32bit를 처리할 수 있도록 설계됩니다. 또한 데이터를 전송하느 ㄴ각종 버스의 크기, 즉 대역폭도 32bit입니다. 32bit 대역폭의 버스를 통해 한 번에 옮겨지는 데이터의 크기도 당연히 32bit입니다.
CPU의 비트는 메모리 주소 공간(address space)의 크기와도 연관이 있습니다. 32bit CPU의 경우 메모리 주소를 지정하는 레지스터인 메모리 주소 레지스터(MAR)의 크기가 32bit이므로 표현할 수 있는 메모리 주소의 범위는 0~-1, 총개수는 개입니다. 이를 16진수로 나타내면 00000000~FFFFFFFF이며 총크기는 B, 약 4GB입니다. 따라서 32bit CPU 컴퓨터는 메모리를 최대 4GB까지 사용할 수 있습니다.
64bit CPU는 레지스터의 크기, 버스의 대역폭, 한 번에 처리되는 데이터의 최대 크기 등이 32bit CPU의 2배입니다. 32bit CPU보다 처리 속도가 빠르고 사용할 수 있는 메모리도 큽니다. 32bit CPU는 0~-1번지의 주소 공간을 제공하지만 64bit CPU는 0~-1 번지의 주소 공간을 제공합니다. 32bit CPU의 메모리는 4GB로 제한되지만 64bit CPU의 메모리는 B, 약 16,777,216TB이기 때문에 거의 무한대에 가까운 메모리를 사용할 수 있습니다.
| 구분 | 32bit CPU | 64bit CPU |
|---|---|---|
| 주소 범위 | 0~-1 번지 | 0~-1 번지 |
| 총크기 | B(약 4GB) | B(약 16,777,216TB) |
메모리의 주소 공간을 물리 주소 공간(pysical address space)이라고 하는데 이는 하드웨어 입장에서 바라본 주소 공간으로 컴퓨터마다 그 크기가 다릅니다. 반대로 사용자 입장에서 바라본 주소 공간을 논리 주소 공간(logical address space)이라고 합니다.
메모리 관리자는 메모리를 운영체제 영역과 사용자 영역으로 나누어 관리합니다. 운영체제는 시슽템을 관리하는 중요한 역할을 하기 때문에 사용자가 운영체제를 침범하지 못하도록 분리해서 관리합니다.
사용자 프로세스가 운영체제의 크기에 따라 매번 적재되는 주소가 달라지면 번거롭습니다. 이를 개선한 방법이 사용자 프로세스를 메모리의 최상위부터 사용하는 것입니다. 즉, 메모리를 최상위에서 운영체제 방향으로 내려오면서 사용합니다. 이 방법을 쓰면 운영체제의 크기에 상관없이 사용자 영역의 시작점을 결정할 수 있으나 메모리를 거꾸로 사용하기 위해 주소를 변경하는 일이 복잡해집니다.
사용자 영역이 운영체제 영역으로 침범하는 것을 막으려면 하드웨어의 도움이 필요한데, 이는 CPU 내에 있는 경계 레지스터가 담당합니다. 경계 레지스터는 운영체제 영역과 사용자 영역 경계 지점의 주소를 가집니다. 메모리 관리자는 사용자가 작업을 요청할 때마다 경계 레지스터의 값을 벗어나는지 검사하고, 만약 경계 레지스터를 벗어나는 작업을 요청하는 프로세스가 있으면 그 프로세스를 종료합니다.
문자형(char) 변수 alp를 선언하고 해당 변수에 대문자 A를 저장하는 경우를 살펴봅시다. 다음 코드가 실행되면 메모리의 어딘가에 문자 A가 저장됩니다. 그 주소를 40번지라 합시다.
char alp = 'A';
중요한 것은 실행 파일에 40번지의 A가 들어간다고 해서 실제로 물리 메모리의 40번지에 A가 들어가는 것은 아니라는 점입니다.
실행을 위해 프로세스가 메모리에 올라오면 이 메모리는 빈 공간에 배치됩니다. 빈 공간의 시작 주소는 물리 메모리 주소 100번지일 수도 있고 300번지일 수도 있습니다. 물리 메모리의 360번지부터 프로세스가 사용한다고 가정해봅시다. 이 경우 40번지는 물리 메모리의 주소 400번지가 됩니다. 여기서 400번지는 물리 주소(physical address)입니다. 물리 주소는 메모리의 입장에서 바라본 주소입니다. 즉 메모리 주소 레지스터가 사용하는 주소로, 컴퓨터에 꽂힌 램 메모리의 실제 주소를 말합니다.
40번지는 논리 주소(logical address)이며, 이는 시작점을 0으로 놓았을 때 40번지의 위치에 있는 주소를 말합니다. 논리 주소는 사용자 프로세스 입장에서 바라본 주소입니다. 프로세스 입장에서는 0번지부터 시작하는 논리 주소가 편합니다.
기억할 것은 프로그램이 실행될 때마다 시작하는 물리 주소는 매번 바뀐다는 점입니다.
프로세스가 사용하는 논리 주소는 논리 주소 공간에 있습니다. 논리 주소 공간은 항상 0번지부터 시작하며 컴퓨터가 허용하는 최대의 주소 크기를 가집니다. 컴파일러는 논리 주소 공간의 0번지부터 변수들을 배치합니다. 물리 주소 공간도 0번지부터 시작하지만 최대 주소 크기는 컴퓨터에 설치된 메모리 크기에 따라 달라집니다. 다시 말해, 컴퓨터에 실제로 설치된 메모리의 크기가 마지막 주소가 됩니다.
논리 주소로 이루어진 프로세스를 실행하면 이를 물리 주소로 변환하는 작업이 필요합니다. 이 작업은 메모리 관리 유닛(MMU; Memory Management Unit)이 담당합니다. 메모리 관리 유닛은 CPU안에 존재하며 프로세스가 논리 주소에 접근할 때마다 이를 물리 주소로 변환하는 작업을 합니다.