소개

확장성을 확보하기 위한 방법은 여러가지가 있을 것이다. 여기에서는 그중 PlugIn 방식을 이용한 확장성확보에 대한 내용을 다룰 것이다.

Agent&Manager 방식의 프로그램을 만든다고 가정해보자. SNMP 프로토콜을 응용한 Net SNMP가 가장 대표적인 경우가 될 것이다. 이왕 Net SNMP를 예로 들었으니, Agent&Manager 방식의 SMS을 만드는 것으로 가닥을 잡아보겠다.

이러한 시스템에서 Agent 프로그램을 만들려고 한다면, 설계단계에서 가장 중요하게 생각해야 할게 확장성의 확보가 될 것이다. 왜냐면 시스템 관리의 범위가 매우 넓은 관계로 필요에 따라 관리 요소가 계속 추가될 수 있기 때문이다. 당장 생각나는게, CPU, Memory, Disk 관리쯤이 될것이다. 물론 초기에 완전하게 관리요소를 몽땅 예상하고 설계를 하는 방법도 있겠지만, 그렇게 될 경우 설계에 지나치게 많은 시간을 소비해야 할 것이다. 막상 그렇게 만들었다고 해도, 중간쯤 만들다 보면 관리요소가 새로 추가될 수도 있다. 심지어는 모두 만들고 나서 관리요소가 추가될 수도 있을 것이다.

이러한 경우 PlugIn 방식으로 각각의 성능을 모듈화 시켜서 붙이는 방식으로 개발시간을 아낄 수 있다. 거기에 덤으로 유연하고 확장성 좋은 시스템을 만들 수도 있다. 대략 다음과 같은 시스템 구성을 가지게 된다.

agent.png

필요한 기술

이 문서를 읽기 위해서는 함수포인터, STL, 라이브러리를 제어하기 위한 기술들을 가지고 있어야 한다.

Dynamic Module Loading

동적으로 모듈을 로딩하는 PlugIn방식을 구현하기 위한 기본적인 기술요구 사항은 그리 복잡하지 않다. 동적라이브러리를 이용하면, 쉽게 구현할 수 있다. 이 경우 중요한 것은 모듈과 Agent와의 인터페이스를 통일하는게 될 것이다. 어떠한 기능이 모듈형태로 추가되더라도, Agent와 Manager의 소스코드 수정없이 모듈이 로딩될 수 있어야 하기 때문이다.

인터페이스 이름을 맞추는 것은 문제가 되지 않을 것이다. 문제는 인터페이스를 통해서 이동하는 데이터가 될것이다. 이는 각각의 성능마다 보여줘야 하는 정보가 다를 수 있기 때문인데, CPU의 경우라면 사용율을 Disk라면 장치명,마운트이름,사용율을 보내야 하기 때문이다. 그러므로 예상가능한 모든 종류의 데이터를 처리할 수 있는 방법이 준비되어야 한다. 이 문제는 세가지 정도의 방식으로 해결할 수 있다.
  1. 문자열 전송
    간단하게 문자열을 전송한다. 성능=값1,값2 정도로 보내면 될 것이다. 포맷은 대략 아래와 같을 것이다. 성능이름이 들어가는 이유는, 나중에 Manager로 정보가 전달되었을때, 성능이름을 Key로 해서, 해당되는 모듈을 Plugin 방식으로 로딩하기 위함이다.

    CPU=89
    DSK=/dev/sda1,/root,58
  2. 구조체전송
    구조체로도 전송이 가능하다. Agent는 데이터를 처리하지 않고, Manager로 보내기만 하면되므로, 구조체에 어떤 멤버변수들이 있는지는 알 필요가 없다. 단지 보내야 하는 구조체의 크기성능이름만 알고 있으면 된다. 구조체를 받은 Manager 측은 성능이름에 해당되는 Plugin 모듈을 로딩해서 구조체의 값을 처리하면 된다.

    struct Info
    {
    int size; // 구조체의 크기
    char id[4]; // 성능 이름 : DSK, CPU, MEM...
    ... // 나머지 정보들은 성능에 따라 달라질 수 있다.
    ...
    }
  3. XML 데이터 전송
    잘 정의한다면, 유연하게 사용할 수 있을 것이다. 데이터의 크기가 커진다는 점을 고려하지 않아도 된다면, 가장 좋은 방법이라고 생각된다.

여기에서는 문자열을 보내는 것을 기준으로 설명하도록 하겠다.

Module Config

이제 설정파일을 만들어야 한다. 이 설정파일은 Key라고 할 수 있는 모듈 ID와 호출해야할 라이브러리의 이름들을 가진다. 다음과 같은 구조를 가지도록 하겠다.
[plugin]
CPU=libmycpu.so
MEM=libmymem.so
설정파일을 읽을 수 있는 라이브러리가 필요할 것 같아서, 급조한 코드가 있다. 간단 설정파일 Reader를 참고하기 바란다. 이 코드를 그대로 사용할 것이다.

프로시져

  1. 실행
  2. plugin 을 로딩하기 위해서 설정파일을 읽어들인다.
  3. plugin 목록을 읽어들인다.
  4. plugin 목록의 갯수만큼 루프를 돌면서, 라이브러리를 동적으로 적재한다.
  5. while 루프를 돌면서, 공통 인터페이스를 호출한다.

공통 인터페이스

공통 인터페이스를 정의해보도록 하자. 최대한 간단하게 정의하도록 하겠다.
  1. Init : 플러그인 모듈을 초기화 한다.
  2. Read : 플러그인 모듈로 부터, 데이터를 요청한다. 데이터는 문자열로 Key.IndexNum=Value,Value 형식으로 전달된다. IndexNum은 데이터가 2개이상일때, 사용하는 인덱스 번호다. 예를 들어 CPU가 2개라면
    • CPU.1=87
    • CPU.2=21
  3. RowNum : 몇개의 데이터가 있는지를 알려준다.
    • CPU가 2개라면, 2를 출력한다.
  4. Close : 플러그인 모듈을 닫는다.

테스트용 플러그인 모듈

libmycpu 와 libmysms 를 위한 모듈을 작성할 것이다. 이들은 공유라이브러리 형태로 작성될 것이다. 작성된 이들 플러그인 모듈은 dlopen(2) 함수를 이용해서 동적으로 적재 된다.

여기에서는 단순히 문자열을 리턴하는 dummy 모듈을 작성할 것이다.

플러그인 기능을 지원하는 Agent 프로그램

다음은 Agent 프로그램이다.
#include <iostream>
#include <cstdlib>
#include <qosconfig.h>
#include <dlfcn.h>
#include <vector>

using namespace std;

typedef char *(*Function)();
int main(int argc, char *argv[])
{
Config *agentCfg;
int rtv;
char *key;
char *value;
void *handle;
agentCfg = new Config();
Function myFunc;
// 플러그인을 저장할 Vector로 함수 포인터를 원소로 가진다.
vector<Function> FuncPList;

// 설정파일을 Open 한다.
rtv = agentCfg->openCfg("config.cfg");
if (rtv == -1)
{
perror("Config Read Error");
}

// 플러그인 섹션에서 플러그인 목록을 읽어온다.
if (agentCfg->findSection("PLUGIN"))
{
while(key = agentCfg->NextItem())
{
printf("Loding Module %s:%sn", key, agentCfg->NextValue());
// 플러그인을 로드한다.
handle = dlopen(agentCfg->NextValue(), RTLD_NOW);
if (!handle)
{
fputs(dlerror(), stderr);
}
else
{
// 공통인터페이스인 Read 함수를 함수포인터로 얻어오고
// vector에 push 한다.
myFunc = (char *(*)())dlsym(handle, "Read");
FuncPList.push_back(myFunc);
}
}
}

// 1초 간격으로 로딩된 플러그인 모듈로 부터, 데이터를 읽어온다.
while(1)
{
for (int i = 0; i < FuncPList.size(); i++)
{
printf("%s",FuncPList());
}
printf("==============n");
sleep(1);
}
return EXIT_SUCCESS;
}

모듈 프로그램

이 프로그램은 dummy 프로그램으로, 공통 인터페이스 포멧에 맞는 문자열을 리턴한다.
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

char *rtvstr = NULL;
int count;
int Init()
{
rtvstr = (char *)malloc(80);
count = 0;
return 1;
}

char *Read()
{
sprintf(rtvstr, "%s=%dn", count);
count++;
}

int RowNum()
{
return 1;
}

int Close()
{
if (rtvstr != NULL)
free(rtvstr);
count = 0;
}
이 코드는 공유라이브러리로 컴파일을 한다. 라이브러리의 이름은 libdummy.so 로 하겠다. 공유라이브러리를 만드는 방법은 라이브러리 만들기 문서를 참고하기 바란다.

이제 아래와 같은 설정파일 만들고, 실행하면 된다.
[PLUGIN]
DUMMY=libdummy.so

결론

동적라이브러리를 통한 확장가능한 프로그램을 만드는 방법에 대해서 알아보았다. 이 경우에는 데이터를 단순하게 제한했기 때문에, 비교적 손쉽게 인터페이스를 설계할 수 있었지만 실제 프로젝트에 도입해서 사용할때는 XML을 이용하거나, 데이터 처리 함수를 만들어야 하는 등, 좀더 복잡하게 구현될 수 있을 것이다.
크리에이티브 커먼즈 라이센스
Creative Commons License
이올린에 북마크하기
Posted by 소리나는연탄.
TAGS ,

Leave your greetings here.