 |  |
|
난이도 : 고급 Sachin Agrawal, 선임 소프트웨어 엔지니어, IBM Software Labs India
옮긴이: 박재호 이해영 dwkorea@kr.ibm.com
2008 년 6 월 10 일 대다수 공유 메모리 활용법은 그다지 수월하지 않습니다. IBM에 근무하는 Sachin Agrawal은 C++ 전문 경험을 공유해 유용한 IPC(InterProcess Communication) 채널을 활용하는 핵심 기법을 객체 지향적으로 풀어내는 해법을 보여줍니다.
시간과 공간 관점에서, 공유 메모리는 현대적인 운영체제가 제공하는 IPC 통신 채널 중에서 아마도 가장 효율이 높을 것이다. 공유 메모리는 프로세스 둘 이상에 주소 영역을 동시에 사상한다. 프로세스는 단순히 공유 메모리에 접속해서 일반적인 메모리를 사용하듯 읽고 쓰는 방법으로 다른 프로세스와 통신을 시작한다.
하지만, 객체 지향 프로그래밍 세계에서, 프로세스는 가공되지 않은 정보보다는 객체 공유를 더 선호한다. 객체를 사용한다면 객체 내부에 포함된 정보를 직렬화해 전송하고 다시 직렬화를 푸는 작업이 필요하지 않다. 공유 객체는 또한 공유 메모리에 존재하므로 비록 객체를 생성한 특정 프로세스에 속해 있긴 하지만, 시스템에서 동작하는 모든 프로세스가 공유 객체에 접근할 수 있다. 따라서 공유 객체 내부에 있는 모든 정보는 엄격히 프로세스 중립이다.
이는 대다수 인기 있는 컴파일러가 현재 지원하는 C++ 객체 모델에 정확히 반대되는 행동양식이다. C++ 객체는 프로세스에 밀접한 다양한 가상 테이블과 하위 객체를 가리키는 포인터를 항상 포함하기 때문이다. 이런 객체를 공유하려면, 이 포인터가 가리키는 대상이 모든 프로세스에서 동일한 주소가 되도록 만들 필요가 있다.
자그마한 예제 프로그램의 도움을 받아 이 기사에서는 C++ 모델이 성공하는 경우와 공유 메모리 모델과 함께 동작하는 과정에서 실패하는 경우, 가능한 임시변통이 존재하는 경우를 설명한다. 토론과 간단한 프로그램은 비정적 자료 멤버와 가상 함수에 국한한다. 그 이외의 다른 경우는 C++ 객체 모델에서 그다지 빛을 발하지 못한다. 정적이거나 정적이 아닌 비가상 멤버 함수는 공유 환경에서 쟁점이 되지 못한다. 프로세스 단위 정적 멤버는 공유 메모리에 존재하지 않는 반면에(이렇기 때문에 역시 쟁점이 되지 못한다), 공유 정적 멤버에는 여기서 토론하는 쟁점과 비슷한 문제점이 있다.
실행 환경 가정
이 기사는 특정 실행 환경을 가정하는데, 32비트 x86 인텔 아키텍처를 위한 레드햇 리눅스 7.1 배포판, GNU C++ 컴파일러 버전 2.95와 gcc 연계 유틸리티를 사용해 예제 프로그램을 빌드하고 테스트했다. 하지만 전반적인 개념은 기계, 운영체제, 컴파일러 조합에 무관하게 현실에 적용할 수 있다.
예제 프로그램
예제 프로그램은 shm_client1과 shm_client2라는 클라이언트 둘을 포함하며, 공유 라이브러리인 shm_server가 제공하는 공유 객체 서비스를 활용한다. 객체 정의는 common.h를 살펴보자.
Listing 1. common.h에 실려있는 정의
#ifndef __COMMON_H__
#define __COMMON_H__
class A {
public:
int m_nA;
virtual void WhoAmI();
static void * m_sArena;
void * operator new (unsigned int);
};
class B : public A {
public:
int m_nB;
virtual void WhoAmI();
};
class C : virtual public A {
public:
int m_nC;
virtual void WhoAmI();
};
void GetObjects(A ** pA, B ** pB, C ** pC);
#endif //__COMMON_H__
|
Listing 2는 클래스 셋(A, B, C)을 WhoAmI()라는 공통 가상 함수와 함께 정의한다. 기초 클래스인 A는 m_nA라는 멤버를 포함한다. 정적 멤버인 m_sArena와 중복 정의된 new() 연산자는 공유 메모리에서 객체 생성이 가능하도록 만든다. 클래스 B는 단순히 A에서 파생되었으며, 클래스 C는 A에서 가상적으로 파생되었다. B::m_nB와 C::m_nC는 A, B, C 크기를 다르게 만들도록 추가했다. 이런 방식은 A::operator new() 구현을 단순하게 만든다. 인터페이스 GetObjects()는 공유 객체 포인터를 반환한다.
공유 라이브러리 구현 내역은 shm_server.cpp를 살펴보기 바란다.
Listing 2. 라이브러리 - shm_server.cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <stdio.h>
#include <iostream>
#include "common.h"
void * A::m_sArena = NULL;
void * A::operator new (unsigned int size)
{
switch (size)
{
case sizeof(A):
return m_sArena;
case sizeof(B):
return (void *)((int)m_sArena + 1024);
case sizeof(C):
return (void *)((int)m_sArena + 2048);
default:
cerr << __FILE__ << ":" << __LINE__ << " Critical error" <<
endl;
}
}
void A::WhoAmI() {
cout << "Object type: A" << endl;
}
void B::WhoAmI() {
cout << "Object type: B" << endl;
}
void C::WhoAmI() {
cout << "Object type: C" << endl;
}
void GetObjects(A ** pA, B ** pB, C ** pC) {
*pA = (A *)A::m_sArena;
*pB = (B *)((int)A::m_sArena + 1024);
*pC = (C *)((int)A::m_sArena + 2048);
}
class Initializer {
public:
int m_shmid;
Initializer();
static Initializer m_sInitializer;
};
Initializer Initializer::m_sInitializer;
Initializer::Initializer()
{
key_t key = 1234;
bool bCreated = false;
m_shmid = shmget(key, 3*1024, 0666);
if (-1 == m_shmid) {
if (ENOENT != errno) {
cerr << __FILE__ << ":" << __LINE__ << " Critical error" <<
endl;
return;
}
m_shmid = shmget(key, 3*1024, IPC_CREAT | 0666);
if (-1 == m_shmid) {
cerr << __FILE__ << ":" << __LINE__ << " Critical error" <<
endl;
return;
}
cout << "Created the shared memory" << endl;
bCreated = true;
}
A::m_sArena = shmat(m_shmid, NULL, 0);
if (-1 == (int)A::m_sArena) {
cerr << __FILE__ << ":" << __LINE__ << " Critical error" <<
endl;
return;
}
if (bCreated) {
// 공유 메모리에 객체를 생성한다
A * pA;
pA = new A;
pA->m_nA = 1;
pA = new B;
pA->m_nA = 2;
pA = new C;
pA->m_nA = 3;
}
return;
}
|
Listing 2를 좀 더 자세히 살펴보자.
행 9-25: operator new ()
동일한 중복 정의 연산자는 클래스 A, B, C 객체를 공유 메모리에 만들도록 지원한다. 객체 A는 공유 메모리 바로 앞 부분에 시작한다. 객체 B는 변위 1024에서 시작하고, C는 변위 2048에서 시작한다.
행 26-34: 가상 함수
가상 함수는 단순히 표준 출력으로 문자열을 출력한다.
행 35-39: GetObjects
GetObjects()는 공유 객체 포인터를 반환한다.
행 40-46: 초기화 함수
이 클래스는 공유 메모리 식별자를 저장한다. 생성자는 공유 메모리를 만들고 여기에 객체를 만든다. 공유 메모리가 이미 존재한다면, 단순히 공유 메모리에 연결한다. 정적 멤버인 m_sInitializer는 생성자가 공유 라이브러리를 사용하는 클라이언트 모듈의 main() 함수 호출에 앞서 생성자를 호출하도록 만든다.
행 48-82: Initializer::Initializer()
공유 메모리가 존재하지 않을 경우 새로 생성하고, 여기서 공유 객체를 생성한다. 공유 메모리가 이미 존재한다면 객체 생성 과정을 건너뛴다. Initializer::m_shmid는 식별자를 기록하고 A::m_sArena는 공유 메모리 주소를 기록한다.
공유 메모리는 프로세스가 분리되고 나서도 제거되지 않는다. ipcrm 명령을 사용해 명시적으로 제거하고 ipcs 명령으로 상태를 살펴봐야 한다.
클라이언트 프로세스 구현은 shm_client.cpp를 살펴보기 바란다.
Listing 3. 클라이언트 - shm_client.cpp
#include "common.h"
#include <iostream>
#include <stdlib.h>
int main (int argc, char * argv[])
{
int jumpTo = 0;
if (1 < argc) {
jumpTo = strtol(argv[1], NULL, 10);
}
if ((1 > jumpTo) || (6 < jumpTo)) {
jumpTo = 1;
}
A * pA;
B * pB;
C * pC;
GetObjects(&pA, &pB, &pC);
cout << (int)pA << "\t";
cout << (int)pB << "\t";
cout << (int)pC << "\n";
switch (jumpTo) {
case 1:
cout << pA->m_nA << endl;
case 2:
pA->WhoAmI();
case 3:
cout << pB->m_nA << endl;
case 4:
pB->WhoAmI();
case 5:
cout << pC->m_nA << endl;
case 6:
pC->WhoAmI();
}
return 0;
}
#include <pthread.h>
void DoNothingCode() {
pthread_create(NULL, NULL, NULL, NULL);
}
|
행 6-35
클라이언트 프로세스는 공유 객체 셋을 가리키는 포인터를 얻으며, 자료 멤버를 가리키도록 참조 값을 만들고, 명령행 입력에 따라 가상 함수 셋을 호출한다.
행 36-39
여기서 생뚱맞은 pthread_create() 함수를 사용한 이유는 또 다른 공유 라이브러리와 강제로 링크하기 위해서다. 어떤 공유 라이브러리에 있는 어떤 함수라도 이런 목적으로 활용이 가능하다.
공유 라이브러리와 클라이언트 실행 파일 두 개는 다음과 같은 방법으로 빌드한다.
gcc shared g shm_server.cpp o libshm_server.so lstdc++
gcc -g shm_client.cpp -o shm_client1 -lpthread -lshm_server -L .
gcc -g shm_client.cpp -o shm_client2 -lshm_server -L . -lpthread
shm_client1과 shm_client2를 링크할 때 shm_server와 pthread 순서를 뒤바꿔 실행 파일 양쪽에서 shm_server 공유 라이브러리 기본 주소가 달라지게 만들었다는 사실에 주목하자. 이는 나중에 ldd 명령으로 비교가 가능하다. ldd 실행 예는 일반적으로 다음과 같다.
Listing 4. shm_client1을 위한 라이브러리 사상
ldd shm_client1
libpthread.so.0 => (0x4002d000)
libshm_server.so => (0x40042000)
libc.so.6 => (0x4005b000)
ld-linux.so.2 => (0x40000000)
|
Listing 5. shm_client2를 위한 라이브러리 사상
ldd shm_client2
libshm_server.so => (0x40018000)
libpthread.so.0 => (0x40046000)
libc.so.6 => (0x4005b000)
ld-linux.so.2 => (0x40000000)
|
여기서 주요 목표는 두 클라이언트 바이너리를 빌드할 때 서버 라이브러리 기본 주소를 다르게 만드는 데 있다. 이런 예제 프로그램 맥락에서 바라보면, 실행도 하지 않은 pthread_create() 함수 사용과 공개 라이브러리를 위한 각기 다른 링크 순서를 활용해 이런 목표를 달성했다. 하지만 모든 링커에 먹혀 들어가는 유일하게 활용 가능한 절차나 완벽한 규칙은 존재하지 않는다. 경우에 따라 여러 가지 방법을 사용해야 한다.
사례 1: shm_client1 vs. shm_client1
다음 출력 결과에서, shm_client1은 셸에서 먼저 호출된다. 공유 객체가 존재하지 않기 때문에 공유 객체를 만들고, 자료 멤버를 참조하고, 가상 함수를 호출하고, 종료한다. 메모리에 객체를 그대로 놓아둔다. 두 번째로, 프로세스는 단순히 자료 멤버와 가상 함수를 참조한다.
Listing 6. shm_client1 vs. shm_client1 실행 출력 결과
$ ./shm_client1
Created the shared memory
1073844224 1073845248 1073846272
1
Object type: A
2
Object type: B
3
Object type: C
$ ipcs
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x000004d2 2260997 sachin 666 3072 0
$ ./shm_client1
1073840128 1073841152 1073842176
1
Object type: A
2
Object type: B
-> 0
-> Segmentation fault (core dumped)
|
두 번째 프로세스가 자료 멤버인 A::m_nA를 타입 C *(C는 가상적으로 A에서 상속받았다는 사실을 기억하자) 포인터로 참조할 때, 공유된 객체 내부에서 기초 하위 객체 포인터를 읽는다. 공유 객체는 이제 존재하지 않는 프로세스 문맥에 만들어진다. 따라서 A::m_nA와 C::WhoAmI()는 엉뚱한 값을 반환한다.
이렇게 되는 이유는 가상 테이블과 가상 함수가 shm_server 공유 라이브러리 내부에 존재하며, 동일한 가상 주소에 올라오기 때문이다. 여기서 타입 A *와 B * 포인터 역참조에는 아무런 문제가 없음이 밝혀졌다.
여기에서는, GNU가 채택한 C++ 객체 모델이 가상 상속에 실패하는 상황을 제시했다.
사례 2: shm_client1 vs. shm_client2
다음 예제는 명령행에서 먼저 shm_client1을 수행한 다음에 shm_client2를 수행한 결과다.
Listing 7. shm_client1 vs. shm_client2 실행 출력 결과
$ ./shm_client1
Created the shared memory
1073844224 1073845248 1073846272
1
Object type: A
2
Object type: B
3
Object type: C
$ ipcs
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x000004d2 2359301 sachin 666 3072 0
$ ./shm_client2
1073942528 1073943552 1073944576
1
-> Segmentation fault (core dumped)
$ ./shm_client2 3
1073942528 1073943552 1073944576
2
-> Segmentation fault (core dumped)
$ ./shm_client2 5
1073942528 1073943552 1073944576
-> 1048594
-> Segmentation fault (core dumped)
|
하지만 가상 테이블은 shm_server 공유 라이브러리 내부에 있다. 공유 라이브러리는 shm_client1과 shm_client2에서 다른 가상 주소에 놓인다. 따라서 A::WhoAmI()와 B::WhoAmI()는 엉뚱한 값을 반환한다.
공유 메모리를 동작하도록 만들기
공유 메모리 내부에서 C++ 객체를 생성하는 과정에서 두 가지 주요 문제점을 고려해야 한다. 먼저, 가상 테이블 포인터는 가상 함수 접근에 사용하며, 자료 멤버는 컴파일 시점에 만들어진 변위를 사용해 직접 접근한다. 따라서, 모든 공유 객체에 대해 가상 테이블과 가상 함수는 모든 프로세스에서 동일 가상 주소에 위치해야 한다. 이렇게 하기 위한 완벽한 규칙은 없지만, 공유 라이브러리 의존성을 고려한 적절한 링크 순서를 지키면 대부분 통한다.
또한 가상적으로 상속받은 객체에는 기초 객체를 가리키는 기초 포인터가 있다. 기초 포인터는 프로세스의 자료 영역을 참조하며, 항상 프로세스에 밀접하게 붙어 있다. 모든 클라이언트 프로세스에 이런 동일한 기초 포인터 값을 유지하기란 쉽지 않다. 따라서 가상으로 상속받은 객체를 공유 메모리에서 만드는 작업은 C++ 객체 모델에서 피해야 한다. 하지만 여러 가지 컴파일러가 여러 가지 모델을 채택하고 있다는 사실을 기억하는 편이 좋겠다. 예를 들어, 마이크로소프트 컴파일러는 가상적으로 상속받은 클래스를 위한 기초 객체 프로세스를 가리키기 위해 중립 변위를 사용한다. 따라서 이번 기사에서 제시한 문제점이 생기지 않는다. 모든 클라이언트 프로세스에서 공유 라이브러리에 동일한 주소를 사용하도록 만들어야 한다는 사실이 중요하다.
참고자료
- C++ 내부를 이해하기 위한 훌륭한 참고 문헌은 Stanley B. Lippman이 집필한
Inside the C++ Object Model
(Addison-Wesley, 1996)이다.
-
RFC 1014 - XDR:
External Data Representation Standard는 다양한 컴퓨터 아키텍처 사이에 자료를 기술하고 인코딩하는 표준이다. XDR은 문자열, 가변 길이 배열, 비슷한 구조를 기술하는 언어를 정의한다.
-
ld, ldd, ipcs, ipcrm 같은 개별 함수를 위한 시스템 매뉴얼 페이지를 잊지 말고 점검하자.
- 이클립스는 주로 자바 개발 환경이지만, 이클립스 아키텍처는 다른 프로그래밍 언어도 지원한다. Eclipse Platform에서의 C/C++ 개발(한국 developerWorks, 2003년 4월) 기사를 읽어 개념을 익히자.
- IBM VisualAge C++는 첨단 C/C++ 컴파일러로 AIX와 선택된 리눅스 배포판용 버전이 제공된다.
-
Java programming for C/C++ developers(developerWorks, 2002년 5월) 튜토리얼은 C++에 이미 익숙한 개발자 관점에서 자바 언어를 소개한다.
-
developerWorks 리눅스 영역에서 리눅스 개발자를 위한 참고 자료를 찾아보자.
-
온라인 도서관: 리눅스와 기타 기술 주제에 대한 책을 살펴보자.
-
developerWorks에 가입한 다음에 최선 IBM 도구와 미들웨어를 활용해 리눅스 응용 프로그램을 개발하고 테스트하자. WebSphere, DB2, Lotus, Rational, Tivoli에서 IBM 소프트웨어를 얻은 다음에 비용 절감을 위해 12개월 동안 사용 가능한 라이선스를 얻자.
- WebSphere Studio Site Developer, WebSphere SDK for Web services, WebSphere Application Server, DB2 Universal Database Personal Developers Edition, Tivoli Access Manager, and Lotus Domino Server를 포함한 developerWorks에 가입자에게 선택적으로 제공되는 다양한 리눅스용 평가판 소프트웨어를 사용해보자. developerWorks에서 리눅스 응용 프로그램 섹션에서 각종 제품을 찾아보기 바란다. 좀 더 빠르게 시작하려면, 제품별 모음집에서 하우투 기사와 기술 지원을 찾아보자.
필자소개  | |  | Sachin은 3년에 걸친 여러 컴파일러에 대한 C++ 객체 모델 연구를 비롯하여 5년 동안 C++로 광범위한 작업을 진행했다. Sachin은 현재 IBM 글로벌 서비스 인디아에서 일하고 있다. 전자편지 주소는 sachin_agrawal@in.ibm.com이다. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|  |