Header

  1. View current page

    멤버십 기술 노트

Profile_img_60x60_06
62 16

Shell의 설계와 구현

 

  • 고친 과정 (Revision History)
버전 날짜 고친 내용 작성자 <E-mail>
0.1 2007-06-26 멤버십 기술 문서로 작성 광주 17기 조성현 <linu at nate.com>
0.2 2008-10-03  2007년 본인이 작성한 멤버십 기술 문서를 바탕으로 스프링 노트 형식으로 옮김 광주 17기 조성현 <linu at nate.com>

 

소개#

임베디드 시스템을 포함한 컴퓨터를 사용하는 대부분의 시스템에서 사용자 요구를 처리하기 위해서는 사용자에 대한 인터페이스 환경을 제공하게 된다. 기본적으로 사용자가 원하는 작업들을 대화형 형태로 분석을 하게 되는데, 그 중에서 가장 많이 사용되는 것으로 Shell을 볼 수가 있다. 이러한 인터페이스 역할을 하는 Shell을 구현하려면 어떻게 해야하며, 구성요소가 어떤 것이 있는지 살펴보도록 하자.

서론#

컴퓨터 프로그램의 형태는 크게 운영방법과 실행방법에 따라 대화형(Interactive)과 배치형(Batch)으로 구분된다. 대화형은 데이터를 사용자로부터 직접 받거나 사용자를 대신하는 다른 프로그램으로부터 받는다. 이에 비해 배치형은 주어진 일을 일괄적으로 처리한 후 동작을 멈추는 것을 이른다.

대화형의 예로는 명령어 해석기나 웹브라우저를 들 수 있고, 배치형의 예로는 회사의 급여 내용을 계산하여 출력하는 프로그램 등을 들 수 있다.

이 문서로 EZ-X5 임베디드 보드에서 대표적인 대화형 프로그램인 명령어 해석기(Shell)를 어떻게 설계해야 하는지, 또 실제로 동작하기 위해서는 어떻게 구현을 해야 하는지 알아보도록 하자.

Shell의 개요#

Shell을 간단히 정의를 하자면, 사용자의 명령을 해석하여 커널에 전달하여 주고, 명령을 실행 시켜 주는 명령어 해석기(Command Interpreter)이다. 사용자에 의해서 요구되는 명령어는 시스템 내부적으로 이미 정의해놓은 함수와 연결되어서, 시스템 내부로 접근이 가능하게 된다. 즉, 시스템을 조작하게 되는 것이다.

이제는 Shell이 어떻게 이루어져 있는지 살펴보자.

Shell의 동작을 위한 설계#

구성 요소#

Shell은 어떠한 동작을 원하는 지에 따라 구성해야 되는 항목이 달라질 수 있다.

사용자에게 제공하는 명령 중에 파일에 직접적으로 접근하는 명령이 있다면, 당연히 파일 시스템과 Shell을 연동해야 할 것이다. 하지만, 파일에 접근하지 않는다면 굳이 파일 시스템을 구현해서 연동할 필요까진 없게 된다.

 shell_consist.png
[표 1] Shell의 구성 항목

[표 1]과 같이 이 곳에서 설명하게 되는 Shell은 파일 시스템과의 연동을 고려하지 않게 설계를 했다. 또한, 그에 따라 구성 설계에 대한 복잡성이 많이 줄어들게 되었다.

위와 같이 Shell에 포함되어야 하는 기본적인 항목은 '명령어'와 '환경변수'이다. 그리고, 포함되는 동작으로 그 항목에 대한 처리 함수가 정의 되어야 한다.

명령어의 구조체#

사용자가 Shell에서 어떠한 명령어를 입력하게 되면, Shell은 그 명령어를 읽어서 연결되어 있는 명령어 처리 함수를 실행하게 한다. 이 중간에 연결 처리를 위해서는 명령어 구조체가 필요하게 되는데, 이것은 명령어와 명령어 처리 함수, 명령어에 대한 코멘트, 명령어 사용법 등으로 이루어져 있다.

/**

 * Shedll에서 함수와 연결을 위한 구조체

 */

typedef struct SCommand

{

    char *CmdStr;

    int (*Func)(int argc, char **argv);

    char *Comment;

    char *Usage;

} TCommand;

[표 2] 명령어의 구조체

[표 2]에서 정의한 명령어 구조체에서 CmdStr은 명령어 문자열이 저장되어 있는 곳의 포인터가 되는 것이고, Func(argc, argv)는 명령어를 처리하는 함수 포인터로 미리 정의된 함수를 호출하게 된다.

환경 변수의 구조체#

사용자가 요청한 간단한 명령 처리 외에도 Shell에서 필요한 사항은 바로 환경 변수이다. 이 것은 사용자가 지정해놓은 작업 환경으로 작업을 수행하는데, 반복되는 환경 변수는 시스템 내부에 저장을 해놓고, 사용자에게 편의를 제공하는 역할을 한다.

[표 3]과 같이 환경 변수의 구조체는 환경 변수의 이름인 EnvStr과 환경 변수의 값인 EnvValue로 구성되어 있다.

여기서 주의할 점은 환경 변수의 정보를 시스템 내부에 저장하기 위해서는 환경 변수의 이름과 값을 기록해야 하는데, 이곳에서 문자열 포인터를 사용한다면, 값 자체를 저장하지 못하기 때문에, 일정한 배열의 공간으로 선언을 해야 한다.

/**

 * Shell에서의 환경 변수 설정을 위한 구조체

 */

typedef struct SEnvironment

{

    char EnvStr[20];

    char EnvValue[20];

} TEnvironment;

[표 3] 환경 변수의 구조체

하지만, 위에서 선언하는 환경 변수의 구조체로는 문자열 처리로만 이루어져야 되기 때문에, 환경 변수의 확인과 수정은 용이할 지 모르나, 환경 변수의 실제적인 사용에서는 접근이 용이하지가 않다.

위 문제점을 해결하기 위해서는 두 가지 방법이 있게 되는데, 환경 변수의 사용을 위한 구조체를 따로 사용하는 방법과 그 구조체를 사용하지 않는 방법으로 환경 변수를 읽어 들이게 되는 모든 함수에서 환경 변수의 형태를 변경하는 방법이 있게 된다.

전자는 환경 변수가 변경될 경우, 매번 환경 변수의 사용을 위한 구조체에 갱신을 해줘야 하는 단점이 있으며, 후자는 환경 변수를 사용할 때마다, 처리하는 함수가 필요하게 된다.

상대적으로 전자가 후자보다 효율적이게 되므로, 전자를 택하는 게 낫다.

환경 변수의 읽기 구조체#

[표 4]와 같은 환경 변수의 읽기 구조체는 용어 그대로 읽기를 위한 것이다. 환경 변수를 직접적으로 참조하는 함수에서는 처리하기 위해 가공된 이 읽기 구조체를 통해서 값을 전달받아 사용하게 된다.

/**

 * Shell에서의 환경 변수 읽기를 위한 구조체

 */

typedef struct SConfig

{

  /* 보드 MAC Address[xx:xx:xx:xx:xx:xx] */

  BYTE  Local_MacAddress[8];

  /* 호스트 MAC Address[xx:xx:xx:xx:xx:xx] */

  BYTE  Host_MacAddress[8];

  /* 보드 IP = 0 */

  UINT32  Local_IP;

  /* 호스트 IP = 0 */

  UINT32  Host_IP;

  ....................................중략....................................

} TConfig;

[표 4] 환경 변수의 읽기 구조체

물론, 읽기 구조체 내에 있는 변수들은 환경 변수의 구조체에 문자열 데이터로 저장이 되어 있는 상황이어야 한다.

shell> setenv LOCAL_IP=192.168.10.1  ......①

shell> update                                   ......②

[표 5] 환경 변수의 설정 단계

예를 들어, [표 5]와 같이 환경 변수를 설정하기 위해서는 읽기 구조체에서 보드의 IP 주소를 위한 변수가 있는 상황이어야 한다.

[표 5]의 ①과 같은 방법으로 환경 변수를 문자열 단위에서 설정을 하게 되면, 문자열 환경 변수만 갱신된 상태가 된다.

그리고, ②에서와 같은 명령으로 변경된 문자열 환경 변수는 [표 4]의 읽기 구조체에서 보드의 IP 주소를 저장하는 변수의 데이터 형태로 변환된 후에 저장이 되어야 하는 것이다.

이에 따라, 보드의 IP 주소를 사용하는 어떤 프로그램에서는 문자열로 정의된 환경 변수 값을 사용하는 것이 아니라, 읽기 구조체에 정의된 보드의 IP 주소를 저장하는 UINT32 데이터형의 값을 사용하는 것이다.

환경 변수의 처리 함수#

처리 함수는 위에서 언급된 문자열 환경 변수의 값과 환경 변수의 읽기 구조체 사이의 처리 작업과 사용자가 변경한 환경 변수를 시스템 내부 저장소에 기록하는 작업 등을 담당해야 한다.

간단하게는 SaveConfig, LoadConfig, UpdateConfig, SetConfig로 네 가지 처리 함수가 존재하게 된다.

Shell의 구현#

shell의 실제 구현 코드#

사용자가 입력하는 명령어를 입력받기 위해서는 그 명령어와 명령어 구조체 내에 정의되어 있는 명령과 비교하는 처리 작업, 그리고 그 명령어를 수행하기 위한 처리 함수, 또 그 함수에 전달될 인자를 처리하는 작업 등이 필요하게 된다.

TCommand command_list[] =

{

 { "test",

   TestFunc,

   "test 프로그램",

   "test <argument...>" }, // here

 { "help", Help,

   "Print Usage of Command",

   "help <command>" }, // here

 { "dump",

   DumpMemory,

   "Dump Memory value",

   "dump <address> <size>" }, // here

 { "setenv",

   SetEnv,

   "Set Envirionment Variable",

   "setenv [string=value]" }, // sos_config.c

 { "update",

   UpdateEnv,

   "Update Environment Variable",

   "update" }, // sos_config.c

 { NULL, NULL, "", "" } // EOF

};

[표 6] 명령어의 정의

[표 6]과 같이 명령어를 정의하게 되는데, 함수 포인터에 대한 함수([표 7])가 정의 되어 있어야 한다.

/**

 * @brief  help 명령 처리 함수

 * @param  argc : 전달되는 인자 수

 * @param  argv : 전달되는 인자

 * @return  0 : 정상, -1 : 실패

 */

int Help(int argc, char **argv)

{

  ...............중략...................

}

[표 7] 명령어 처리 함수

기본적으로 Shell에서는 "help" 명령어가 입력되었는지 확인하고, Help 함수를 호출 해주어야 한다. 그리고, Help 함수의 파라미터로 "help" 명령어 다음에 온 인자 값들을 전달해 주어야 한다.

구체적으로 명령어 자체를 처리해주는 main 함수에서 while(1)으로 무한루프를 돌면서, 사용자한테서 어떠한 입력이 있었는지 감시하면서 입력이 들어올 때 처리해주는 그런 함수도 필요하게 된다.

/** while문으로 쉘 처리를 한다. */

while (1)

{

  /** 프롬프트 출력과 문자를 입력 받음 */

  Print_Prompt();

  gets(input);

  printf("\n");

  /** 입력받은 문자를 구분자에 의해서 쪼갬 문자열 input은 구분자(" ")에 의해서,

   *  배열 argv[]에 각 주소가 저장된다.

   */

  if ((tok = strtok(input, " ")) != NULL)

  {

    argv[0] = tok;

    i = 1;

    while ((tok = strtok(NULL, " ")) != NULL)

    {

      argv[i++] = tok;

    }

    argc = i; // i는 인자 개수

    for (i = 0; i < 255; i++)

    {

      if (command_list[i].CmdStr == NULL)

      {

        // command_list에서 해당 명령어를 찾지 못했을 경우

        printf("Invalid Command\n");

        break;

      } else \

      if (!strcmp(command_list[i].CmdStr, input))

      {

        /** 해당 명령어를 호출 */

        if (command_list[i].Func(argc, argv) == -1)

        {

          printf(" Usage : %s\n", command_list[i].Usage);

        }

        break;

      }

    } // for

  } // if (tok..)

} // while (1)

[표 8] main의 while문

[표 8]은 main 함수에서 Shell을 가동하는 모습이다. 거창하게 가동할 것까진 없지만, 살펴보면 while 문으로 사용자 입력을 계속 받아오고, 입력 받은 명령어와 인자들을 분리하고, 명령어 문자열과 비교를 하는 등의 작업을 하게 된다.

Shell의 환경 변수 정의 코드#

환경 변수는 앞서 말한대로 사용자의 편의를 위한 것이므로 넣어도 되고 안 넣어도 되는 사항이다. 넣고 싶다면, 앞에서 살펴본 환경 설정 구조체들을 선언한 후에, 실제 환경 변수를 정의하는 [표 9]와 같은 코드가 필요하게 된다.

/**

 * 환경 변수 저장소

 */

TEnvironment Env[] =

{

  /// 보드가 가지고 있는 cs8900a의 MAC 주소

  { "LOCAL_MAC", "00:A2:55:F2:26:25" },

  /// 통신을 하고자 하는 상대쪽의 MAC 주소

  { "HOST_MAC", "00:00:00:00:00:00" },

  /// 보드가 가지고 있는 IP 주소

  { "LOCAL_IP", "192.168.10.20" },

  /// 통신을 하고자 하는 상대쪽의 IP 주소

  { "HOST_IP", "192.168.10.10 }

};

[표 9] 환경 변수의 정의

결론#

시스템 입장에서 사용자의 요구를 알기 위해서는 여러 가지 방법이 있지만, 그 중에서도 가장 오랫동안 사용해온 방식은 아무래도 Shell 환경일 것이다. 최근에 GUI 환경의 제한된 환경의 사용자 인터페이스보다는 복잡하지만, 사용자에게 보다 많은 설정 권한을 부여하는 Shell이 아직까지 필요한 이유는 분명히 있다. 그것은 다름 아닌 고급사용자를 위한 환경 때문일 것이다.

하지만, GUI 환경의 사용자 인터페이스도 이 문서에서 알아본 Shell의 구현 방식과 크게 다르지 않다. 다만, 보여지는 것이 달라질 뿐이다.

여기까지 복잡하고도 난해한 Shell이 어떻게 구성되어 있는지 살펴보았으며, 실제 Shell 코드는 파일 시스템과 연계 등을 고려하면 훨씬 복잡할 것이다 예상한다. 사용자와 시스템 사이에 수많은 기술들이 산재해 있는 상황에서 어떠한 기술들이 있는지 조금씩 알아가는 계기가 되었기를 바라며 마친다.

참고문헌#

  • 뇌를 자극하는 윈도우즈 시스템 프로그래밍 - 윤성우 저, 한빛미디어 

 

Tags

History

Last edited on 10/04/2008 14:16 by 박성철

Comments (0)

You must log in to leave a comment. Please sign in.