월별 글 목록: 2009년 4월월

32bit, 4GB, 램디스크.

컴퓨터 구조라는 수업을 들어보면, 현재 x86 아키텍쳐에서 주변장치들은 메모리처럼 특정 주소에 매핑된다는 것을 알 수 있다.
물론 이 부분은 사용자는 건들 수 없고, 커널 수준에서 관리를 하게 된다. 드라이버는 커널에서 주소를 받아다가 하드웨어하고 통신하는거고.

근데 안타까운 것은, 32bit OS에서 메모리 4GB를 설치하는 경우, 이 공간이 불가피하게 메모리와 겹치면서, 일부 메모리를 사용할 수 없다는 것이다.
이게 용량이 작으면 그냥 넘어가겠는데, 그래픽카드 및 기타 주변장치에 따라서 적게는 200MB에서 크게는 1GB가 넘는 공간을 뺏긴다.
나도 4GB를 물렸지만, 실사용 공간은 3.25GB밖에 안 된다.

물론 64bit OS를 설치하면 이 문제는 말끔하게 해결된다. 헌데 PAE를 활성화하고 상용 툴을 이용하면, 32bit OS에서도 이렇게 묻힌 부분을 램디스크로 재활용할 수 있다는 글이 꽤 전부터 심심치 않게 올라오고 있다. 그리고 실제로 해보면 되는 것 같다. 사용가능한 공간은 2GB로 유지되면서, 2GB짜리 램디스크가 잡힌다.
근데, 이게 과연 안정적일까…?

우선 2GB인 경우부터 살펴보자.

2GB 메모리인 경우.

대충 개념도이다. 가로 전체는 8GB 공간을 나타내는데, XP/Vista/Win7 32bit는 메모리 주소공간 4GB 이상을 관리할 수 없다.
어쨌든, 물리적 메모리와 커널 예약공간은 서로 멀찍이 떨어져 있다. 연속적으로 존재할 수도 있겠으나, 개념적으로는 차이가 없다.
어쨌든 메모리가 2GB인 경우, 커널 예약공간이 2GB를 넘나드는 미친 경우[footnote]그래픽카드 메모리가 1.5GB 정도면 가능할 수도 있다… 오 쉣.[/footnote]가 아닌 이상 물리적 메모리와 커널 예약공간이 겹쳐서 손해볼 일은 발생하지 않는다.

자 이제, 4GB인 경우를 살펴보자. 단, PAE를 켜지는 않은 경우이다.

4GB, noPAE인 경우.

안타깝지만, 이 경우 커널 예약공간때문에 일부 물리적 메모리가 가려지는 것을 볼 수 있다. 안타깝지만, 저 공간은 못 쓴다.

그런데 PAE를 켜면 문제가 좀 복잡해진다.

4GB, PAE인 경우

일단 PAE를 켜면 BIOS나 OS에서 4GB 경계는 없어진다. 색이 옅게 표시되었음을 잘 보도록.

그런데, 이렇게 4GB 경계가 없어졌을 때 커널 예약공간은 어떻게 되는가? 라는 의문이 생긴다.
1과 같은 경우라면 물리적 메모리와 겹치는 부분이 없으니 4GB를 온전히 쓸 수 있을 것이고, 그 중 일부를 램디스크로 잡아도 문제가 없을 것이다.
2와 같다면? 단지 주소공간의 4GB 경계만 없어졌을 뿐, 실질적으로 PAE를 껐을 때와 차이는 없다. 다만 저 상태로 그냥 램디스크를 쓴다면 시스템 불안정을 초래할 수 있다. 윈도우로 말하자면 BSOD, 리눅스라면 커널 패닉.

불행하게도 XP 32bit는 2처럼 작동하는 것 같다. 하여 램디스크로 커널 예약공간을 당겨다 쓰는 것을 추천할 수 없다. 뭐 쓰겠다면 말리지는 않겠지만, 뒷감당은 스스로 하면 된다.
신뢰할 수 있는 정보는 아니지만, 서버 2008 32bit는 PAE를 켰을 때 64GB까지 지원하며, 4GB 장착상태에서 1처럼 작동한다고 한다. 64bit로 넘어가기는 꺼려지는데 메모리가 아까운 사람이라면 서버 2008을 써 보는것도 나쁘지는 않을 듯 하다. 물론 라이센스 구하기가 힘들다.

여기서 한 발짝 더 나가서, 8GB라면 어떻게 될까? 물론 PAE를 켠 상황이 되겠다. PAE를 끄면 4GB 이상은 접근조차 못한다.

8GB 메모리인 경우.

램디스크 부분을 제외하면, XP 32bit에서 메모리는 저런 식으로 할당되게 된다. 실제 사용가능한 부분은 짙은 파란색으로 표시된 부분이며, 그 중 일부가 커널 예약공간으로 할당된다. 옅은 파란색으로 표시된 부분은 비관리 공간(Unmanaged area)이라고 하며, BIOS에서는 인식되지만 OS 수준에서 관리하지 않으므로 사용할 수 없다[footnote]물론 특수한 API를 사용하면 사용가능할 수 있다고는 하지만, 일반적으로 사용하는 WIN32 API에서는 안 된다.[/footnote].
그렇다면, 그림에 긁어둔 것처럼 저 부분을 램디스크로 사용할 수 있지 않을까? 글쎄 아직 테스트를 안 해봐서 모르겠다. 내 컴퓨터는 4GB 밖에 안 물려둬서… 혹시 누가 테스트 가능하다면 결과를 보고해주기 바란다. 또, 위에서 언급했다시피 서버 2008은 8GB 전부를 관리한다. 64GB까지 지원하므로 메모리가 아까우면 그 쪽으로 넘어가면 되겠다. 아니면 64bit OS를 사용하던지.

배열, 포인터, 배열 포인터, 그 미묘한 상관관계

대체로 C/C++에서 배열과 포인터 부분이 어려운 부분이라는 점은 대부분 공감하는 사실이다.
나 또한 이 부분이 어렵고, 이해 안 되고, 쓰기 싫어서 대체로 외면하곤 한다. 속도 최적화와는 거리가 좀 있는 분야를 다루고 있기도 하고.

헌데 이런저런 사정 – 물론 쓰잘데기 없는 것이긴 하지만 – 이 좀 있어서 배열과 포인터에 관련된 내용을 찾아보았다.
일단 VC++ 2005에서 돌려본 것이니, 적어도 틀린 것은 아닐 것이다.

1. 문제의 시작

펼쳐!

꽤 근래에 디스어셈블로 비교해 본 것이기도 하지만, 근본적으로 배열이나 포인터나 다를 것은 없다.

int a[3];
(…)
a[i] = 3;
*(a+i) = 3;

위 두 코드는, 디스어셈블 뜯어보면 똑같은 코드가 나온다는 이야기.

이제 형식상의 문제인데, C 수업을 착실히 들은 사람은 알겠지만 a[i]라는 배열에서 a는 첫 번째 원소의 주소를 나타내는 포인터 상수[footnote]int * const. 틀리면 댓글주셈.[/footnote]라고 하였다.
자 그렇다면, &a 는 무엇을 나타낼까?

컴파일을 해 봐도, &a는 틀린거야! 라면서 오류를 뿜어내지는 않는다. 그 값을 출력해도 a와 다를 것은 없다.
그런데, 이상하게도

int a[3];
int *p1 = a;
int *p2 = &a;

라는 코드를 실행하면, 세 번째 줄에서 오류를 내뿜는다.
그렇다면, & 연산자는 주소 연산자니까 * 를 하나 더 붙이면 될까…??

int a[3];
int *p1 = a;
int **p2 = &a;

한 번 돌려보자. 역시나 오류를 내뿜는다. 아ㅅㅂ 뭐가 문제야.

접어!

2. 작은 결론

펼쳐!

오랜시간 뒤적이지는 않았지만, 여튼 내가 내린 결론은
“& 연산자가 반환하는 주소는 형태가 좀 지랄같다.”
이다. 정확하게는
“& 연산자는, 피연산자를 하나의 원소로 갖는 가상의 배열에서, 피연산자가 위치하는 부분의 시작주소”
를 반환한다. 아ㅅㅂ 거 조낸 복잡하네.

예를 들어서…

int data;
&data; // 형식이 뭔데?

를 생각해 보자.
data는 그냥 int형이 되겠다. 따라서 &data는

{ int, int, int, …, int, int data, int, …, int, int, int }

라는 가상의 배열에서 data의 위치이다.
자, 이런 배열을 만들려면 뭘 써야 하는가? 당연히 int * 지.

이제 좀 더 가보자.

int a[3]; // 이번엔 배열이다.
&a[0]; // 이건 뭐야…
a; // 이건 왠지 알거같아…
&a; // 이건 또 뭐고.

자 우선. &a[0]. 연산자 우선순위상 [] 가 먼저 연산되니까, &의 피연산자는 int형이다.
또 쓰기 귀찮다. 결국 위에서 본 data와 경우가 같다. 형태는 int *.

다음은 a. 이건 수업시간에 들은 것처럼 첫 번째 원소의 주소, 그러니까 &a[0] 인 것은 확실하다…
그런데 뭔가 미묘하게 다르다. 이놈의 자료형은 int [3] 이다. 응?
이 말은 무슨 말이냐면, “int *와 비슷하긴 한데[footnote]실제로는 int * const 에 해당한다. 배열 주소가 바뀔 수는 없으니까…[/footnote], 길이가 3으로 명백해!” 라는 의미이다. 아ㅅㅂ 머리아프다.

여하튼 여기에서 체크해 두어야 할 것은, 배열도 결국에는 유도 자료형이라는 말이다.
int a[3] 은 “int형 3개를 원소로 갖는 배열 a를 만들어!” 가 아니고, “int형 3개를 이어붙인 배열구조를 갖는 변수 a를 만들어!” 라는 의미다.
아아… C 배운지 3년찍고 4년만에 겨우 이해했다. 변수가 배열이 아니라, 배열 형태를 갖는 변수다. ㅅㅂ 무슨소리래.

어쨌든 마지막, &a. &의 피연산자 형태는? int [3]인 배열이다. 그렇다면 다음과 같이 되겠다.

{ {int, int, int}, {int, int, int}, …, {int, int, int}, {a[0], a[1], a[2]}, {int, int, int}, …, {int, int, int}, {int, int, int} }

라는 배열에서 a 배열의 위치란 말이지. 반환형은? “int 3개를 이어붙인 구조의 배열” 형태의 변수를 가리키는 포인터, 즉 int (*)[3] 이다.
아ㅅㅂ 복잡하다… 이건 또 뭐여… 하겠지만, 그냥 납득해라. 글쓰는 나도 뭔소린지 제대로 모르겠다.
일단은, int 3개짜리 배열을 통짜로 가리키는 포인터라고만 알아두면 되겠다. 이 말을 이해했다면 이 글을 읽을 필요도 없는거고…

참고로, 포인터를 그 맴버로 갖는 배열과, 배열 자체를 가리키는 포인터 하나를 구분하기 위하여 다음과 같은 방법을 사용한다.

포인터를 맴버로 갖는 배열 : int *p[n] : int형 포인터 n개 모인 새로운 자료형을 갖는 변수 p.
배열 자체를 가리키는 포인터 : int (*p)[n] : int형 n개가 모인 자료형을 가리키는 포인터 p.

구분 참 지랄같지만, 여튼 둘이 다르다는 것만 이해해 두자. 참고로 이런 구분방식은 함수 포인터에서도 나온다. 젭라…

그렇담 &&a 도 있을 수 있지 않을까? 미안하지만 그딴건 없다 -.- 대신 int (**)[3] 은 있다.

접어!

3. 머리아픈 문제

펼쳐!

문제는 이중 배열이다. 이제부터 고민할 내용은 이거다.

int a[2][3];

이제부터 &a, a, &a[0], a[0], &a[0][0], a[0][0] 여섯 개를 다 살펴보아야 한다.
불친절하게도 그림이 없으니, 알아서 메모지에 그림 그리면서 판단하면 되겠다.
일단 배열구조는 다음과 같겠지.

{ {n00, n01, n02} {n10 n11 n12} }

1. a[0][0]
이건 간단하다. n00 자체를 가리킨다.

2. &a[0][0]
&의 피연산자는? int 형이다. 따라서 다음과 같겠다.

{ int, int, …, int, n00, n01, n02, n10, n11, n12, int, …, int, int }

하여 형태는 int * 되시겠다.

3. a[0]
a[0]은 무엇을 가리키는가…?? a 배열의 첫 번째 원소일 것이다.
그렇다면 a 배열의 첫 번째 원소는 무엇인가? 이중 배열의 첫 번째 원소이니, 그냥 1차원 배열이다.
가리키는 대상은 {n00, n01, n02} 이고, 그 형태는 int [3]이다.

4. &a[0]
&의 피연산자는? 위에서 본 것처럼 int [3]형이다. 따라서…

{ {int, int, int}, {int, int, int}, …, {n00, n01, n02}, {n10, n11, n12}, {int, int, int}, …, {int, int, int} }

가 된다. 반환형은? 당연히 int (*)[3].

5. a
a는 위에서 살펴본 바, 이중배열 전체를 다 가리키는 것이 분명하다.
이제는 좀 보이겠지. 형태도 int [2][3] 이다.

6. &a
그리기 귀찮다. 이쯤 되면 보일게다.
형태는 int (*)[2][3].

접어!

이 정도면 뭐 대충 감잡았을지도 모르겠다.

물론 나도 제대로 이해한 것은 아니지만, 어쨌든 무언가 다르게 보이기 시작했다. 그리고 머리가 좀 더 아파지기 시작했고.
어쨌든 본문 내용을 한 줄로 줄이면, “포인터 ㅅㅂ 조낸 어려워.”

… 쓰고 나서 보니까 내용이 영 지저분하다. 나중에 시간나면 정리해야지.