cpu 스케줄링을 통한 성능향상을 실현하려면 많은 프로세스를 메모리에 유지해야 한다.
메모리 관리 방법은 시스템의 하드웨어 설계에 따라 달라진다.
따라서 하드웨어와 메모리 관리를 통합해야 한다.
메모리는 각각의 주소가 할당된 바이트들로 구성된다.
cpu는 program counter가 지시하는 대로
메모리로부터 다음 수행할 명령어를 가져오는데,
필요한 경우 데이터를 더 가져올 수도, 데이터를 메모리로 내보낼 수도 있다.
전형적인 명령어 실행은
메모리로부터 명령어를 가져오고, 해독하고,
메모리에서 operand(피연산자)를 가져와
이에 명령어를 실행한 후, 결과를 메모리에 저장한다.
메모리는 주소에 데이터를 매핑할 뿐 이게 어떤 데이터인지는 알 필요가 없다.
cpu가 직접 접근할 수 있는 범용 저장장치는 메인 메모리와 각 코어에 내장된 레지스터들 밖에 없다.
따라서 모든 명령어와 데이터들은 이 둘 중에 있어야 한다.
일반적으로 레지스터는 cpu 클록의 1사이클내에 접근이 가능하다.
그러나 메모리 버스를 통해 전송되는 메인 메모리는
많은 cpu 클록 사이클이 필요하며,
이 때문에 명령어를 수행하지 못하는 지연(stall)이 발생한다.
이 문제를 해결하기 위해
cpu와 메모리 사이에 캐시를 추가한다. (통상적으로 cpu 안에)
캐시를 이용해 메모리 접근 속도를 향상 시킨다.
이와 같은 접근 속도 차이 외에도 올바른 동작도 고려해야 한다.
시스템이 올바르게 동작하기 위해서는
유저 프로그램과 운영체제 사이, 유저 프로그램끼리도 서로 보호해야 한다.
이러한 보호 기법은 반드시 하드웨어가 지원해야 한다. (운영체제가 하면 성능이 떨어진다)
여러 보호 기법 중 하나에 대한 개요만을 설명한다.
먼저 각 프로세스가 독립된 메모리 공간을 가지도록 보장해야 한다.
이를 위해 특정 프로세스만 접근할 수 있는 legal(합법) 메모리 주소 영역을 설정하고 이곳만을 접근하도록 한다.
이는 base와 limit 두 레지스터를 사용하여 구현한다.
base는 시작 주소를, limit은 크기를 저장한다.
메모리 공간의 보호는
사용자 모드에서 만들어진 모든 주소와 레지스터를 비교함으로써 이루어진다.
만약 다른 공간에 접근했다면 운영체제는 이를 치명적인 오류로 간주하고 trap을 발생시킨다.
base와 limit 레지스터는 특권 명령에 의해서만 적재된다.
즉, 응용 프로그램이 변경할 수 없고, 커널모드에서 운영체제의 커널에 의해서만 적재될 수 있다.
프로그램은 원래 이진 실행 파일 형태로 디스크에 저장되어 있다.
실행하려면 메모리에 적재하고 cpu가 메모리에서 명령어 및 데이터에 접근한다.
운영체제가 프로세스를 실제 물리 메모리에 적재하는 방법을 알아보자.
대부분의 프로그램은 컴파일, 링킹, 로더에 의해 여러 단계를 거쳐 실행되는데,
이 동안 주소들은 여러 다른 표현방식을 거친다.
원시 프로그램에서 주소는 숫자가 아닌 심볼형태로 표현된다.
컴파일러는 심볼 주소를 재배치 가능 주소로 바인딩시키고,
링커나 로더가 이를 절대 주소로 바인딩 시킨다.
바인딩은 이루어지는 시점에 따라 다음과 같이 구분된다.
compile time binding: 프로세스가 절대 주소를 컴파일 시간에 미리 알 수 있으면
컴파일러는 절대 코드를 생성할 수 있다. 하지만 이 위치가 변경 되어야 하면 다시 컴파일 되어야 한다.
load time binding: 컴파일 시점에 물리 주소를 알지 못하면
컴파일러는 이진 코드를 relocatable code(재배치 가능 코드)로 만들어야 한다.
이 경우 프로그램이 메모리에 적재될 때(시작 주소가 변경될 때), 심볼주소가 실제 물리주소로 바인딩된다.
execution time binding: 만약 프로세스 실행중에 메모리 내의 다른 세그먼트로 옮겨질 수 있다면
바인딩이 실행시간에 일어난다. 이를 위해 MMU라는 하드웨어가 필요하다.
이 장의 주요 요점은 컴퓨터 시스템에서 여러 가지 바인딩을 어떻게 효과적으로 구현할 수 있는가이며,
그를 위한 하드웨어 지원에 대해 언급하는 것이다.
일반적으로 cpu가 생성하는 주소를 logical address라고 하며,
메모리가 취급하게 되는 주소(즉 메모리 주소 레지스터(MAR)에 주어지는 주소)는 physical address라 한다.
컴파일 또는 적재 시에 바인딩을 하면 논리 주소와 물리 주소가 같다.
그러나 실행 시간 바인딩에서는 논리, 물리 주소가 다르다.
이 때의 논리 주소를 virtual address라 한다.
프로그램에 의해 생성된 모든 논리 주소 집합을 logical address space라 하며,
이 논리 주소와 일치하는 모든 물리 주소 집합을 physical address space라 한다.
프로그램 실행 중에 MMU(Memory Management Unit)이라는 하드웨어 장치에 의해
가상 주소가 물리 주소로 변환된다.
이러한 변환 방법은 여러가지가 있는데,
여기서는 앞서 얘기한 base register 기법을 일반화시킨 단순한 MMU 기법에 따른 변환을 설명할 것이다.
이제 base register를 relocation register라고 부른다.
재배치 레지스터 속에 들어있는 값은 주소가 메모리로 보내질 때마다 그 모든 주소에 더해진다.
사용자 프로그램은 결코 실제 물리 주소에 접근하지 않는다.
사용자 프로그램은 논리주소를 통해 작업하고
MMU가 물리 주소로 바인딩하여 메모리에 접근한다.
메모리 공간의 더 효율적인 이용을 위해서는 dynamic loading(동적 적재)를 해야 한다.
동적 적재에서 각 루틴은 실제 호출되기 전까지 메모리에 올라오지 않고 재배치 가능한 상태로 디스크에서 대기하고 있다.
먼저 main이 메모리에 올라와 실행된다.
이 루틴이 다른 루틴을 호출하면 메모리에 있는지 확인하고,
없으면 relocatable linking loader가 루틴을 메모리로 가져오고,
이러한 변화를 테이블에 기록한다.
그 후 cpu 제어는 중단되었던 루틴으로 보내진다.
동적 적재의 장점은 루틴이 필요한 경우에만 적재된다는 것이다.
이는 간혹 발생하면서 양은 많은 코드에 특히 유용하다.
이러한 상황에서는 전체 프로그램의 크기가 커도 특정 시간에 사용되는(적재된) 부분은 훨씬 작을 수 있다.
동적 적재는 운영체제로부터 특별한 지원이 필요없다.
특정 코드 또는 라이브러리를 언제 메모리로 로드할지는 프로그램의 로직과 프로그래머의 결정에 따라 달라진다.
운영체제가 이를 해주지 않고 단지 동적 적재를 쉽게 구현할 수 있도록 라이브러리 루틴 정도만 제공해줄뿐이다.
DLL(동적 연결 라이브러리)는
프로그램이 실행될 때 연결되는 시스템 라이브러리이다.
어떤 운영체제는 정적 링킹만을 지원한다.
정적 링킹에선 라이브러리가 프로그램의 이진 프로그램 이미지에 끼어 들어가게 된다.
반대로 동적 링킹은 링킹이 실행 시기까지 미루어진다.
이는 주로 표준 C언어 라이브러리와 같은 시스템 라이브러리에 사용된다.
DLL의 두번째 장점은 라이브러리를 프로세스 간에 공유할 수 있어
메인 메모리에 DLL 인스턴스가 하나만 있을 수 있다는 점이다.
이러한 이유로 DLL은 shared library라고도 하며 Windows 및 Linux에서 광범위하게 사용된다.
프로그램이 동적 라이브러리에 있는 루틴을 참조하면 로더는 DLL을 찾아 메모리에 적재한다.
그리고 라이브러리 함수를 참조하는 주소를 DLL이 저장된 메모리 주소로 조정한다.
또한 DLL이 업데이트 되어도 프로그램은 새롭게 뭔가를 할 필요가 없다.
하지만 프로그램이 의도하지 않은 업데이트 된 혹은 옛날 버전의 라이브러리 코드를 수행할 수 있어지므로
라이브러리 버전에 대한 정보를 프로그램에 포함해야 한다.
이러한 동적 링킹은 일반적으로 운영체제의 도움이 필요하다.
왜냐하면 프로그램은 자기 메모리 영역만 접근할 수 있으므로
운영체제가 라이브러리를 찾아서 연결하도록 도와줘야 한다.
메모리는 운영체제뿐만 아니라 사용자 프로세스도 수용해야 한다.
그리고 이 각 영역은 각각 목적에 맞도록 효율적으로 관리되어야 한다.
이 절에서는 초기 메모리 할당 방법의 하나인 연속 메모리 할당에 관해 설명한다.
메모리는 일반적으로 운영체제를 위한 부분과 사용자 프로세스를 위한 부분으로 나누어진다.
Linux, Windows를 포함한 많은 운영체제가 운영체제를 높은 메모리에 배치한다.
연속적인 메모리 할당에서 각 프로세스는 다음 프로세스 인접하게 적재된다.
앞의 방식들을 결합하면 프로세스가 자신의 메모리 영역에만 접근하게 강제할 수 있다.
dispatcher는 CPU를 사용하게 될 다음 프로세스를 결정하는 scheduler에 의해 호출되며,
context switching이 일어날 때, 재배치 레지스터와 리밋 레지스터에 새로운 값을 적재하는 역할을 한다.
재배치 레지스터는 프로그램이나 운영 체제의 일부분이 메모리의 어느 부분에 위치해 있는지 정보를 저장하는데,
이는 동적으로 로드되거나 제거된 부분의 주소를 다시 계산할 수 있게 합니다.
운영 체제의 일부인 장치 드라이버와 같은 구성 요소는 항상 메모리에 유지될 필요가 없습니다.
사용되지 않는 동안에는 메모리에서 제거하고, 필요할 때만 메모리에 로드하도록 할 수 있습니다.
이렇게 하면 메모리 사용을 효율적으로 관리하면서 운영 체제의 크기를 필요에 따라 동적으로 변경할 수 있습니다.
메모리를 할당하는 가장 간단한 방법은
프로세스를 메모리의 가변 크기 파티션에 할당하는 것이다.
각 파티션에는 하나의 프로세스만 적재될 수 있다.
이러한 가변 파티션 기법에서 운영체제는
사용 가능한 메모리 부분과 사용 중인 부분을 나타내는 테이블을 유지한다.
처음에는 하나의 큰 hole(메모리 블록)로 간주한다.
결국에는 메모리에는 다양한 크기의 hole이 생기게 된다.
프로세스가 시스템에 들어오면,
운영체제는 프로세스가 메모리를 얼마나 요구하는지,
또 사용 가능한 메모리 공간이 어디에 얼마나 있는지를 고려하여 공간을 할당한다.
프로세스가 공간을 할당받게 되면, 이후로는 cpu를 할당받기 위해 경쟁한다.
이 때 메모리가 충분하지 않으면 어떻게 되는가?
한 가지 옵션은 그냥 요청을 거부하고 오류를 띄우는 것이다.
또는 프로세스를 대기 큐에 넣을 수 있다.
만약 hole이 요청한 것보다 크면 요청한만큼 할당해주고 나머지는 hole 집합으로 만든다.
또한 서로 다른 hole이 인접해있다면 이를 합쳐 한개의 큰 hole로 만든다.
이러한 기법은 동적 메모리 할당 문제의 특별한 한 예이다.
이러한 문제에 대한 일반적인 해결책은 다음과 같다.
모의실험을 통해 시간과 메모리 이용 효율 측면에서 나머지 두개가 최악 적합보다 좋다는 것이 입증되었다.
최초나 최적적합끼린 공간 효율성을 비슷하고 속도는 일반적으로 최초 적합이 더 빠르다.
최초 적합과 최적 적합 모두 External fragmentation으로 인해 어려움을 겪는다.
프로세스가 적재되고 제거되는 일이 반복되다 보면,
가용 공간들이 작은 조각이 되어 버린다.
외부 단편화는 이러한 작은 조각들이 여러 곳에 떨어져서 분산되어 있을 때 발생한다.
최초 적합의 경우 통계적으로 N개의 블록이 할당되었을 때 1/2개의 블록이 단편화 때문에 손실될 수 있다.
이 현상은 50% 규칙으로 알려져 있다.
단편화는 내부적으로도 발생할 수 있다.
작은 크기의 메모리 조각을 관리하려면 그 자체보다 더 많은 시스템 자원을 사용해야 하므로 효율적이지 않습니다.
따라서 메모리를 고정된 크기의 작은 블록으로 나누고 이러한 블록 단위로 메모리를 할당하는 방법을 사용하는 것이 일반적이다.
하지만 이 경우 할당된 공간은 필요한 공간보다 약간 더 클 수 있다.
이러한 공간이 internal fragmentation이다.
외부 단편화의 해결 방법으로 compaction(압축)이 있다.
압축은 메모리 모든 내용을 한군데로 몰고 모든 가용 공간을 모아서 클 블록을 만드는 것이다.
그러나 압축이 항상 가능한 것은 아니다.
재배치가 어셈블 또는 적재 시에 정적으로 행해지면 압축은 불가능하다.
주소가 동적으로 재배치할 수 있다면 base register 값만 변경하면 완료다.
하지만 압축이 가능하더라도 그 비용을 따져봐야 한다.
단순히 한쪽 끝으로 모는 방법은 비용이 매우 많이 든다.
외부 단편화 문제의 또 다른 해결법으로는
한 프로세스의 논리 주소 공간을 여러개의 비연속적인 공간으로 나누어 할당하는 것이다.
이러한 기법을 paging이라 한다.
페이징은 외부 단편화와 압축을 해결하여 대부분의 운영체제에서 다양한 형태로 사용된다.
페이징은 운영체제와 하드웨어 간의 협력을 통해 구현된다.
물리 메모리는 frame이라 불리는 같은 크기의 블록으로 나누어진다.
논리 메모리는 page라 불리는 같은 크기의 블록으로 나누어진다.
프로세스가 실행될 때 프로세스의 페이지는 메모리의 프레임으로 적재된다.
프로세스가 있던 예비 저장장치는 프레임 혹은 클러스터(프레임 묶음)와 동일한 크기의 블록들로 나누어진다.
cpu에서 나오는 모든 주소는 페이지 번호와 페이지 오프셋 두 부분으로 나누어 진다.
페이지 번호는 프로세스 page table을 액세스할 때 사용된다.
페이지 테이블은 물리 메모리의 각 프레임의 시작 주소를 저장하고 있으며
오프셋과 결합하여 물리 메모리 주소가 된다.
MMU가 페이지 번호를 페이지 테이블을 이용해 프레임 번호로 바꾼다.
페이지 크기는 하드웨어에 의해 정해진다.
모든 논리주소는 페이징 하드웨어에 의해 실제 주소로 바인딩된다.
페이징을 사용하면 외부 단편화는 발생하지 않지만 내부 단편화가 발생한다.
이런 측면에서는 작은 페이지 크기가 좋지만
작아질수록 페이지 테이블의 크기가 커지게 되어 공간이 낭비된다.
일부 시스템은 여러개의 페이지 크기를 허용하기도 한다.
프로세스가 실행되려면 요구하는 페이지만큼 프레임이 있어야 한다.
페이지가 할당된 프레임 중 하나에 적재되고, 그 프레임 번호가 페이지 테이블에 기록된다.
프로그램은 실제로 여러 곳에 프레임 단위로 분산되어 있고, 많은 다른 프로그램이 올라와있다.
프로세스는 페이지 테이블을 통해서만 메모리에 접근할 수 있고,
페이지 테이블은 그 프로세스가 소유하고 있는 페이지들만을 가리키고 있어
자기 것이 아닌 다른 영역으로는 접근할 수 없다.
운영체제는 물리 메모리를 관리하기 때문에
어느 프레임이 할당되어 있고,
어느 프레임이 사용 가능한지,
총 프레임은 몇 개나 되는지 등을 알아야 한다.
이러한 정보는 보통 frame table에 있고, 이는 시스템에 하나밖에 없는 자료구조이다.
프레임 테이블은 각 프레임당 하나의 항목을 가지며,
프레임이 비어있는지, 할당되었는지, 어느 프로세스의 어느 페이지에 할당되었는지를 나타낸다.
운영체제는 명령카운터와 레지스터의 사본을 유지하는 것처럼
각 프로세스의 페이지 테이블 사본을 유지한다.
이는 운영체제가 직접 논리주소를 물리주소로 변환할 때 사용된다.
또한 프로세스가 cpu에 할당될 때
cpu 디스패처가 하드웨어 디스패처 테이블을 설정하는 데 사용된다.
따라서 페이징은 문맥 교환 시간을 늘린다.
페이지 테이블은 프로세스별 자료구조이므로
테이블에 대한 포인터는 각 프로세스의 PCB에 다른 레지스터 값(명령 포인터 같은)과 함께 저장된다.
cpu 스케줄러가 실행할 프로세스를 선택하면
사용자 레지스터를 다시 적재하고
저장된 사용자 페이지 테이블로부터 하드웨어 페이지 테이블 값을 다시 적재해야 한다.
페이지 테이블의 하드웨어 구현은 여러 가지 방법으로 수행할 수 있다.
가장 간단한 경우, 전용 레지스터 세트로 구현된다.
하지만 이 경우 각각의 레지스터가 문맥 교환 중에 교체되어야 하므로 문맥 교환 시간을 증가시킨다.
페이지 테이블에 레지스터를 사용하는 것은 페이지 테이블이 작은 경우에 적합하다.
그러나 현대에는 훨씬 큰 페이지 테이블을 지원한다.
따라서 대부분 페이지 테이블을 메인 메모리에 저장하고
PTBR(page-table base register)로 하여금 페이지 테이블을 가리키도록 한다.
다른 페이지 테이블을 사용하려면 레지스터만 변화하면 돼서 문맥교환 시간을 줄일 수 있다.
메인 메모리에 페이지 테이블을 저장하면 문맥 교환 속도가 빨라지지만 메모리 액세스 시간이 느려질 수도 있다.
페지이테이블때문에 한번, 실제 접근에 한번 총 두번의 메모리 액세스가 필요하다.
이는 허욜할 수 없는 지연시간이다.
이를 해결하기 위해 TLB라고 불리는 특수한 소형 하드웨어 캐시가 사용된다.
TLB는 매우 빠른 연관 메모리로 구성된다.
TLB 내의 각 항목은 key와 value로 이루어져 있다.
TLB 검색은 명령어 파이프라인의 일부로 동작하며 성능에 추가적인 손해를 끼치지 않는다.
TLB(Translation Lookaside Buffer)는 메모리 접근 속도를 높이기 위한 하드웨어 캐시입니다.
이는 가상 주소를 물리 주소로 변환하는 과정에서
일반적으로 시간이 많이 소요되는 페이지 테이블을 건너뛰고
직접 주소 변환을 가능하게 하는 역할을 합니다.
이때, "TLB 검색은 명령어 파이프라인의 일부로 동작하며 성능에 추가적인 손해를 끼치지 않는다"라는 말은,
TLB 검색 작업이 CPU의 명령어 실행 과정인 파이프라인에 잘 통합되어 있어서,
이 검색 작업이 별도의 시간을 소비하지 않고
파이프라인의 일부 과정으로 수행되어 성능 저하를 일으키지 않는다는 것을 의미합니다.
즉, TLB 검색은
CPU가 명령어를 수행하는 동안 병렬적으로 수행되어 별도의 시간 비용 없이 주소 변환을 가능하게 합니다.
이런 방식으로 TLB는 CPU의 메모리 접근 속도를 크게 향상시키게 됩니다.
그러나 파이프라인 단계에 포함되기 위해선 TLB 크기는 작게 유지할 수 밖에 없다.
TLB는 페이지 테이블의 일부를 저장한다.
cpu가 논리 주소를 생성하면 MMU는 먼저 TLB를 뒤진다.
즉 캐시처럼 작동하는 것이다.
만약 TLB가 가득차면 기존 항목 중 교체될 항목을 선택해야 한다.
하지만 보통 중요 커널 코드는 TLB에 고정시킨다.
TLB (Translation Lookaside Buffer)는 가상 메모리 시스템에서 가상 주소를 물리 주소로 변환하는 데 사용되는 캐시입니다.
여기에서 언급하는 ASID (Address-Space Identifier)는 각 프로세스를 고유하게 식별하는 식별자입니다.
이 ASID는 각 TLB 항목에 저장되며, 프로세스간의 주소 공간을 보호하는 데 사용됩니다.
즉, 현재 실행 중인 프로세스의 ASID와 가상 페이지에 연결된 ASID가 일치해야 TLB가 가상 페이지 번호를 해결할 수 있습니다.
일치하지 않으면 이를 TLB 미스로 처리하게 됩니다.
또한 ASID를 사용하면 TLB가 동시에 여러 프로세스의 항목을 포함할 수 있게 됩니다.
만약 TLB가 별도의 ASID를 지원하지 않으면, 새로운 페이지 테이블이 선택될 때마다 (예: 문맥 교환 시) 다음 실행 프로세스가 잘못된 변환 정보를 사용하지 않도록 TLB를 flush해야 합니다.
그렇지 않으면 TLB에는 이전 프로세스에서 남은 유효한 가상 주소를 포함하지만 잘못되거나 유효하지 않은 물리 주소가 포함된 이전 항목이 포함될 수 있습니다.
즉, ASID를 사용함으로써 TLB는 프로세스 간에 주소 공간을 고유하게 보호하고,
여러 프로세스를 동시에 관리할 수 있게 됩니다.
이로 인해 효율적인 문맥 교환과 주소 변환을 가능하게 합니다.
현재의 cpu는 여러 단계의 TLB를 가지고 있다.
이러한 시스템에서 페이징 오버헤드를 계산하려면
각 TLB 단계에서의 미스율 정보가 필요하다.
이에 대한 내용은 10장에서 자세히 다룰 것이다.
운영체제 설계는 페이징 구현을 포함한다.
또한 이는 TLB의 설계에 영향을 받는다.
따라서 운영체제 설계자는 TLB에 대해서도 잘 알고 있어야 한다.
페이지, 프레임, 페이지 테이블, 보호 비트, 유효-무효 비트 등을 통해 메모리 보호가 어떻게 이루어지는지에 대해 알아보자.
페이지 기반의 메모리 환경에서의 보호는 각 프레임에 연결된 보호 비트를 통해 이루어집니다. 일반적으로 이 비트들은 페이지 테이블에 저장됩니다. 한 비트는 페이지를 읽기-쓰기 또는 읽기 전용으로 정의하는 데 사용될 수 있습니다. 메모리에 대한 모든 참조는 올바른 프레임 번호를 찾기 위해 페이지 테이블을 통해 이루어집니다. 물리 주소가 계산되는 동시에, 보호 비트를 확인하여 읽기 전용 페이지에 쓰기가 없는지 확인할 수 있습니다.
또한, 유효-무효 비트라는 추가 비트는 일반적으로 페이지 테이블의 각 항목에 연결됩니다. 이 비트가 유효로 설정되면, 연결된 페이지는 프로세스의 논리 주소 공간에 있으므로 유효한 페이지입니다. 비트가 무효로 설정되면, 페이지는 프로세스의 논리 주소 공간에 없습니다.
페이지 테이블의 크기를 나타내는 페이지 테이블 길이 레지스터(PTLR)라는 하드웨어를 제공하는 시스템도 있습니다. 이 값은 모든 논리 주소에 대해 검사되어 주소가 프로세스의 유효 범위에 있는지 확인합니다. 이 테스트에 실패하면 운영 체제로의 에러 트랩이 발생합니다.
페이징 시스템의 큰 이점 중 하나는 공통 코드의 공유 가능성입니다. 이는 여러 프로세스가 작동하는 환경에서 매우 중요합니다.
표준 C 라이브러리 같은 것은 UNIX와 Linux의 많은 버전에서 시스템 호출 인터페이스의 일부를 제공합니다.
일반적인 Linux 시스템에서 대부분의 사용자 프로세스는 표준 C 라이브러리가 필요합니다.
각 프로세스가 자신만의 libc 복사본을 주소 공간에 로드하는 것이 한 가지 방법이지만,
이렇게 하면 2MB의 libc 라이브러리를 사용하는 40개의 사용자 프로세스가 있다면 총 80MB의 메모리가 필요하게 됩니다.
그러나 코드가 재진입 가능한 코드(reentrant code)인 경우 공유할 수 있습니다.
재진입 가능한 코드는 자기 수정 코드가 아니며, 실행 동안에는 변하지 않습니다.
따라서 두 개 이상의 프로세스가 동시에 같은 코드를 실행할 수 있습니다.
각 프로세스는 프로세스 실행에 필요한 데이터를 보유한 자신만의 레지스터와 데이터 저장소의 복사본을 가집니다.
물론 두 개의 다른 프로세스의 데이터는 다르게 될 것입니다.
표준 C 라이브러리의 한 복사본만 물리 메모리에 유지되면 되며,
각 사용자 프로세스의 페이지 테이블은 libc의 같은 물리 복사본에 매핑됩니다.
따라서 40개의 프로세스를 지원하기 위해선 라이브러리의 한 복사본만 필요하며,
이제 필요한 총 공간은 80MB가 아닌 2MB입니다. 이는 상당한 절약을 의미합니다.
libc와 같은 런타임 라이브러리 외에도 컴파일러, 윈도우 시스템, 데이터베이스 시스템 등 많이 사용되는 다른 프로그램들도 공유될 수 있습니다.
공유 가능한 코드는 재진입 가능해야 하며,
공유 코드의 읽기 전용 특성은 코드의 정확성에 의존해서는 안 되며,
운영 체제가 이 속성을 강제해야 합니다.
이러한 메모리 공유는 시스템의 프로세스간에, 또는 스레드가 작업의 주소 공간을 공유하는 것과 비슷합니다.
또한, 일부 운영체제는 공유 페이지를 사용하여 공유 메모리를 구현합니다.
페이지에 따라 메모리를 조직화하는 것은 여러 프로세스가 동일한 물리 페이지를 공유할 수 있게 하는 것 외에도 많은 이점을 제공합니다.
다른 여러 가지 이점들은 다음 장에서 다룹니다.
재진입 가능(reentrant)이라는 용어는 주로 함수나 서브루틴에 사용되며,
여러 스레드 또는 프로세스에 의해 동시에 안전하게 호출하거나 사용할 수 있는 것을 의미합니다.
현대 컴퓨터는 주소 공간이 커서 페이지 테이블도 상당히 커진다.
따라서 페이지 테이블을 여러 작은 조각으로 나누는 방법이다.
예로 2단계 페이징 기법이 있는데, 페이지 테이블을 페이징하는것이다.
이 방식은 주소 변환이 바깥 페이지 테이블에서 안쪽으로 들어오므로 forward-mapped 페이지 테이블이라고도 부른다.
하지만 64비트 체계에서는 이마저도 부족해 한번 더 페이징 해 3단계 페이지 방법으로 만들 수 있다.
하지만 이렇게 단계가 많아질수록 메모리 접근을 많이 해야 해서 실용적이지 않다.