전처리문의 종류(#include, #define, #ifdef, ... )

 

전처리문이란?

실질적인 컴파일 이전에 미리 처리되는 문장을 가리킨다. 선행처리기라고도 한다. 따라서 컴파일러는 사용자가 작성한 코드를 컴파일하기 전에 전처리문에서 정의해 놓은 작업들을 먼저 수행한다.

종류로는 #define, #if, #ifdef, #ifndef, #defined, #undef 등이 있다. 이것은 기존에 있는 방대한 소스 코드를 지우지 않고 활성화 비활성화 하는 데에 가장 많이 이용된다. 즉, 기존에 있는 소스 코드를 건드리지 않는 상태에서 부분적인 컴파일을 하는 것이다.

C의 전처리문이 오는 줄(Line)의 첫 문자는 항상 '#'으로 시작한다. ANSI 표준에 따른 C의 전처리문의 종류가 아래에 나와 있다.

 

- 파일 처리를 위한 전처리문 : #include

- 형태 정의를 위한 전처리문 : #define, #undef

- 조건 처리를 위한 전처리문 : #if, #ifdef, #ifndef, #else, #elif, #endif

- 에러 처리를 위한 전처리문 : #error

- 디버깅을 위한 전처리문 : #line

- 컴파일 옵션 처리를 위한 전처리문 : #pragma 

 

조건 처리를 위한 전처리문은 어떤 조건에 대한 검사를 하고 그 결과를 참(0 이 아닌 값) 또는 거짓(0)으로 돌려준다.

#if : ...이 참이라면

#ifdef : ...이 정의되어 있다면

#else : #if나 #ifdef에 대응된다.

#elif : "else + if"의 의미

#endif : #if, #ifdef, #infdef 이 끝났음을 알린다.

 

이하 각 전처리문의 상세를 설명한다.

 

#include

헤더 파일과 같은 외부 파일을 읽어서 포함시키고자 할 때 사용된다. 이때의 파일은 이진파일(Binary file)이 아닌 C의 소스파일과 같은 형태의 일반 문서파일을 말한다.

 

    #include <stdio.h>        /* 이 위치에 stdio.h라는 파일을 포함시킨다. */

    #include "text.h"           /* 이 위치에 text.h라는 파일을 포함시킨다. */

 

'<...>'를 사용할 때와 '"..."'를 사용할 때의 차이점은 '<...>'는 컴파일러의 표준 포함 파일 디렉토리(또는 사용자가 별도로 지정해 준)에서 파일을 찾는 것을 기본으로 한다.  그리고 "..."'를 사용했을 때는 현재의 디렉토리를 기본으로 파일을 찾게 된다. 아예 디렉토리를 같이 지정할 수도 있다.

 

    #include <C:\MYDIR\MYHEAD.H>

    #include "C:\MYDIR\MYHEAD.H"

 

#define

상수값을 지정하기 위한 예약어로 매크로라고 부른다. 구문의 상수로 치환한다.

또한 #define은 함수 역활과 비슷하게 아래와 같이 쓰일 수 있다.

#define SUM(x) ((x) = (x) + (x))

동작원리는 함수와 같다. 말 그대로 main소스에서 SUM을 호출하면 옆에 있는 더하기 코드가 치환되는 것이다.

 

#define으로 정의할 수 있는 것은 숫자만이 아니다.

#define MYNAME "Young Hee"

printf(MYNAME);

라고 한다면 이는 전처리기에 의해

printf("Young Hee");와 같이 전개되는 것이다.

 

이렇게 #define으로 정의된 것은 일반적인 변수와는 다르다. 그 차이는 명백한데, 예를 들어서

#define MYNAME "Turbo"

char my_name[] = "Turbo"

의 두가지 경우가 있다고 하자. "MYNAME"은 전처리문으로 "my_name"은 문자형 배열 변수로 정의되었다.

printf(MYNAME);

printf(MYNAME);

printf(my_name);

printf(my_name);

이것을 전처리한 상태는 다음과 같이 될 것이다.

printf("Turbo");

printf("Turbo");

printf(my_name);

printf(my_name);

이런 결과에서 우리가 유추해 볼 수 있는 것은 전처리 명령을 사용했을 경우 "Turbo"라는 동일한 동작에 대해서 두개의 똑같은 문자열이 사용됐고, 변수를 사용했을 경우에는 하나의 문자열을 가지고 두번을 사용하고 있다는 것이다. 결과적으로 이런 경우에는 전처리문을 사용했을 경우 메모리 낭비를 가져 온다는 것을 알 수 있다.

 

#undef

#define으로 이미 정의된 매크로를 무효화한다.

#define ADD(a, b) (a + b)

#undef ADD(a, b)

라고 하면 앞으로 사용되는 ADD(...)는 'undefined symbol'이 되어 에러 처리 된다.

 

#if ~ #endif

#if 구문은 if랑 아주 비슷하다. 이것은 어떠한 구문을 컴파일 할지 안할지를 지정할 수 있다.
#define A 1
#if A
    source code.....
#endif
위 source code 부분은 컴파일이 된다. if문에서와 같이 참, 거짓을 구분하여 컴파일이 된다. 위에서 A값은 1 즉 0보다 큰 수이기 때문에 참인 것이다. 

직접 아래와 같이 하면 거짓이기 때문에 source code 부분은 컴파일이 되지 않는다.

#if 0

    source code....

#endif

 

#ifdef ~ #endif
컴파일 할 때

#define MYDEF               /* MYDEF는 값은 가지지 않았지만 어쨋든 정의는 되었다 */

#ifdef YOURDEF              /* 만약 YOURDEF가 정의되어 있다면... */

    #define BASE 10         /* BASE == 10 */

#elif MYDEF                 /* 그외에 MYDEF가 정의되었다면... */

    #define BASE 2       /* BASE == 2 */

#endif
와 같이 쓰면 BASE는 상수 2로 치환되어 
전처리기가 컴파일러에게 넘겨준다.

 

#ifndef 헤더명_H__ ~ #endif

헤더파일이 겹치는 것을 막기 위한 일종의 매크로이다. 예를 들어 헤더파일에 어떤 클래스의 인터페이스 선언을 넣었다고 하자. 이 클래스 인터페이스에서 다른 파일의 프로토타입이 필요해서 다른 A 파일을 include 하고 있는데 이 헤더 파일을 include 하는 파일에서 A라는 헤더 파일을 이미 include 하고 있다면 두번 define한 것이 된다. 그러면 SYNTEX 에러가 난다. 그래서 그런 것을 막는 방법의 하나로 #ifndef을 사용한다. 이전에 include되어 있으면 #endif쪽으로 점프해버려 결국 한번 선언되는 것이다.

#include  <stdio.h>    ------ (a)

#include  <stdio.h>    ------ (b)
이러허게 두번 썼다고 하자. 그런데 앞에 이미 include를 했는데 밑에 또 한다면 문제가 된다. 컴파일러가 검사해야할 코드량도 많아진다. 그래서 stdio.h에는

#ifndef STDIO_H__

#define STDIO_H__

가 선언되어 있다. 만약 STDIO_H가 선언되어 있지 않다면 선언한다는 뜻이다. 그 뒤 (b)에서는 이미 (a)쪽에서 STDIO_H__ 을 선언한 상태이기 때문에 전처리기 쪽에서 무시해버린다. 그러므로 컴파일러는 (a)만 검사한다.

 

#defined

define이 여러 개 되어 있는지를 검사할 때 쓴다. 이것은 여러 개를 동시에 검사 할 수 있다.

#if #defined A || #defined B

 

#ifdef와 #if defined의 차이

#ifdef는 정의가 되어 있는지를 테스트 하기 때문에, 한번에 여러 개를 사용할 수 없다. 형식이

#ifdef name

처럼, 하나의 토큰이 되어야 하기 때문이다.

 

여러 개가 정의되어 있는지를 테스트 하기 위해서는

#if defined(MACRO1) || defined(MACRO2)

처럼 사용한다.

#if는 ||로 중첩해서 사용할 수 있다. 형식이, #if expression이므로, C 표현이 올 수 있다.

#if MACRO1 || MACRO2

처럼 사용해도 된다.

 

#error

소스 라인에 직접 에러 메세지를 출력한다. 전처리기가 #error 문을 만나면 그 즉시 컴파일을 중단하고 다음과 같은 에러 메시지를 출력한다.

ERROR : XXXXX.c ########: Error directive: 내용

    - XXXXX.c --> 현재 컴파일 중인 파일명

    - ####### --> 전처리기가 #error 문을 만난 시점에서의 행 번호(헤더 포함)

 

#ifdef __LARGE__

#error This program must be compiled in LARGE memory model!

#endif

이 내용은 만일 프로그램이 LARGE 모델이 아니라면 "#error" 뒤에 표시된 메세지를 출력하고 컴파일을 중지하게 된다.

 

#line

이 명령은 소스 코드의 행번호를 지정하기 위한 것으로 주로 컴파일러에 의해 미리 정의된 __LINE__과 함께 사용된다. C에서는 베이식과 같은 행번호를 사용하지 않는다. 하지만 디버깅을 하다 보면 "행번호를 알 수 있으면 좋을텐데"하는 경우가 있다. 예를 들자면 어떤 표현들이 있고, 그 표현들 중의 어딘가에 잘못된 부분이 있는 것 같은데 정확히 그 표현이 있는 행을 찾지 못한다면 "#line"을 사용함으로써 그 일을 보다 수월하게 할 수 있게 된다.

__LINE__과 __FILE__을 각각 행번호와 파일명으로 변경한다.

#include <stdio.h>

 

#define DEBUG

 

void main(void)

{

    int count = 100;

 

    #line 100               /* 다음 줄번호를 100으로 설정한다 */

                               /* <-- 이 줄의 번호가 100이다 */

    #ifdef DEBUG        /* <-- 이 줄의 번호가 101이다 */

    printf("line:%d, count = %d\n", __LINE__, count);

    #endif

 

    count = count * count - 56;

    #ifdef DEBUG

    printf("line:%d, count = %d\n", __LINE__, count);

    #endif

 

    count = count / 2 + 48;

    #ifdef DEBUG

    printf("line:%d, count = %d\n", __LINE__, count);

    #endif

}

 

#pragma

컴파일 옵션의 지정. 컴파일러 작성자에 의해서 정의된 다양한 명령을 컴파일러에게 제공하기 위해서 사용되는 지시어이다. 컴파일러의 여러가지 옵션을 명령행 상에서가 아닌 코드상에서 직접 설정한다.  #pragma는 함수의 바로 앞에 오며, 그 함수에만 영향을 준다. Turbo C는 9개의 #pragma문(warn, inline, saveregs, exit, argsused, hdrfile, hdrstop, option, startup)을 지원하고 있다.

 

#pragma inline : 컴파일할 때 어셈블러를 통해서 하도록 지시한다.즉, 인라인 어셈블리 코드가 프로그램에 있음을 알려준다. (명령행 상에서 '-B' 옵션)

#pragma saveregs : 이 홉션은 휴즈 메모리 모델에 대해 컴파일된 함수에게 모든 레지스터를 저장하도록 한다.

#pragma warn : 이 지시어는 Turbo C에게 경고 메시지 옵션을 무시하도록 한다.

 

#pragma warn -par

이는 매개 변수(parAMETER)가 사용되지 않았다고 경고(warnING)를 내지 못하도록 한다. 이와 반대되는 표현은

#pragma warn +par

이다.

경고의 내용 앞에 (+)를 사용하면 경고를 낼 수 있도록 설정하고, (-)를 사용하면 경고를 내지 못하도록 하는 것은 모든 경고에 대해 동일하다. 명령행에서는 "-wxxx"로 경고를 설정하고 "-w-xxx"로 경고를 해제한다. 경고의 종류는 무척 많은데 자주 사용되는 것을 아래에 나타냈다. 모든 것을 알고 싶다면 컴파일러 User's Guide의 명령행 컴파일러 부분을 참고하시기 바란다.

 

par : 전해진 파라미터가 사용되지 않음

rvl : void 형이 아닌 함수에서 리턴값이 없음

aus : 변수에 값을 할당했으나 사용하지 않았음

voi : void 형 함수에서 리턴값이 사용되었음

sig : 부호비트(MSB)가 고려되지 않은 형변환(type-castion)에서 부호 비트를 소실할 수 있음

 

C의 predefined macro

__FILE__a string that holds the path/name of the compiled file
__LINE__an integer that holds the number of the current line number
__DATE__a string that holds the current system date
__TIME__a string that holds the current system time
__STDC__defined as the value '1' if the compiler conforms with the ANSI C standard
__cplusplusdetermines if your compiler is in C or C++ mode. Usually used in headers

 

예)

#include <stdio.h>  

void main(void)  
{  
    printf("The path/name of this file is %s\n", __FILE__);  
    printf("The current line is %d\n", __LINE__);  
    printf("The current system date is %s\n", __DATE__);  
    printf("The current system time is %s\n", __TIME__);  

    #ifdef __STDC__  
        printf("The compiler conforms with the ANSI C standard\n");  
    #else  
        printf("The compiler doesn't conform with the ANSI C standard\n");  
    #endif  
    #ifdef __cplusplus  
        printf("The compiler is working with C++\n");  
    #else  
        printf("The compiler is working with C\n");  
    #endif  

 

프로그래머들 마다 코딩 스타일(암시적 약속)이 있다. 보통 매크로, const 변수는 대문자로 적는 것이 원칙이다. 매크로 함수와 일반함수, 매크로 대상체(object-like macro)와 일반 변수를 구분하기 쉽게 해주는 것이기 때문이다.

#define STDIO_H_

왜 뒤에 _를 붙였을까? 이것도 하나의 암시적 약속이다. 컴파일러 제작회사는 매크로를 정의할 때 사용자들과 이름이 충돌이 나지 않게 하기 위해서 대부분 _를 뒤어 덧붙인다. 또한 _를 하나 혹은 두 개 연속으로 시작하는 것은 컴파일러 내부에서 사용하는 매크로라는 성격이 강하다. 물론 강제적인 뜻은 없으며 단지 관습상 그렇다. 왜 이것이 관습이 되었나 하면 보통 매크로 변수 이름이나 함수 이름을 지을 때 뒤에 _를 붙이지 않기 때문이다. 그래서 함수 제작자들이 _를 단골로 붙였다.


출처 - http://blog.naver.com/dusjjang/60017406476





Posted by linuxism
,


이번에는 FUSE 파일 시스템에 대해서 살펴 보려고 한다.

아직 커널에 대해서 깊이 알고 있지 못한 상태에서 바로 커널 레벨의 파일 시스템을 만드는 것은 여러모로 무리가 있다. 따라서, 이번에는 사용자 어플리케이션 레벨에서 파일 시스템을 만들 수 있는 방법을 먼저 알아보도록 하겠다.

FUSE는 Filesystem in USEr space의 약자로서 http://fuse.sourceforge.net/ 에서 프로젝트가 진행중이다. FUSE는 리눅스 커널 2.6.15부터는 기본적으로 탑재되어 있다. 물론, FUSE가 부팅시부터 사용가능하도록 되어 있지는 않을 수 있으며, 커널 설정을 확인할 필요가 있다. 만약, 그 이전 버전에서 사용하고자 한다면 코드를 받아서 직접 컴파일해서 사용하면 된다.

FUSE의 장점은 어플리케이션 레벨에서 작업이 이루어지기 때문에 보안이나 안정성 등의 면에 있어서 좀더 나을 수 있다는 것이며, 리눅스 이외의 운영체제에서도 FUSE가 사용 가능하기 때문에 한 번 작성된 사용자 파일 시스템을 여러 운영체제에서 큰 문제 없이 돌릴 수 있다는 장점이 있다.

현재, FUSE는 리눅스를 비롯해, Mac OS X, 윈도우즈, 솔라리스 등등에서 사용 가능하다. 자세한 것은 프로젝트 사이트에서 확인 가능하다.

하지만, FUSE는 단점도 있는데, 가장 큰 단점으로는 계층이 추가됨으로 인해서 속도저하가 발생할 수 있다는 점이다.

FUSE를 사용하기 위해서는 세 가지 요소가 필요하다. 
첫째, 커널 모듈(fuse.ko)가 적재되어 있어야 한다. 현재 커널에 FUSE가 적재되어 있는지 확인하기 위해서는 /proc/filesystems를 살펴보면 된다.
둘째, 유저스페이스 라이브러들이 필요하다. 여기에는 libfuse.so, libfuse.a가 해당된다.
셋째, 사용자가 작성한 파일시스템 코드가 필요하다. 

만약, 사용하는 시스템에 FUSE가 설치되어 있지 않다면, http://fuse.sourceforge.net/ 에서 소스를 받아서 다음 과정을 거쳐 컴파일을 먼저 해야 한다.

설치 과정도 해당 사이트에 잘 나와 있지만, 간단히 보자면 다음과 같은 단계를 거치면 /usr/local/ 밑에 설치가 된다.

[work] $ ./configure
[work] $ make
[work] $ make install

만약 설치되는 경로를 바꾸고 싶다면  ./configure --prefix=/usr 등과 같이 원하는 경로를 prefix 다음에 지정하면 된다.

다음에는 FUSE를 기반으로 한 사용자 파일 시스템을 작성한 후 다음과 같은 형태로 fuse 라이브러리를 사용하도록 컴파일해야 한다.

[work] $ gcc -o my_filesystem my_fs.c -lfuse


다음에는 해당 프로그램을 컴파일하고 사용하면된다.

./my_filesystem   /마운트포인트
 
만약 컴파일이나 실행이 안된다면 라이브러리 경로가 제대로 되어 있는지 확인을 해야 한다. 만약, FUSE 라이브러리가 있는 /usr/local/lib 경로가 빠져 있다면 다음과 같이 경로를 먼저 잡아 준 후 수행해야 한다.

LD_LIBRARY_PATH=/usr/local/lib   ./my_filesystem   /마운트포인트

파일 시스템을 마운트 하는 것은 mount 명령을 사용하지 않으며, 작성한 사용자 프로그램을 올리면서 뒤에 인자로 마운트할 경로를 적어 주면 된다.

반면, 마운트를 해제할 경우에는 기존의 umount 명령을 사용해서 처리하면 된다.

umount /마운트포인트

다음 그림은 FUSE 파일 시스템의 전체적인 그림을 나타내는 것으로 fuse 프로젝트 사이트에서 가져온 것이다.
사용자 삽입 이미지
맥 버전의 FUSE 경우에는  http://code.google.com/p/macfuse/ 에서 코드를 구할 수 있다. 맥의 경우에는 다운로드후 더블클릭만으로 쉽게 설치가 되므로 더 편하게 사용할 수 있다.
MacFUSE 로고


다음 코드는 FUSE  프로젝트 사이트에서 제공하는 코드로 간단히 파일시스템을 구현하는 방법을 보여주고 있다.
/*
    FUSE: Filesystem in Userspace
    Copyright (C) 2001-2005  Miklos Szeredi <miklos@szeredi.hu>
    This program can be distributed under the terms of the GNU GPL.
    See the file COPYING.
*/
#include <fuse.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
static const char *hello_str = "Hello World!\n";
static const char *hello_path = "/hello";
static int hello_getattr(const char *path, struct stat *stbuf)
{
    int res = 0;
    memset(stbuf, 0, sizeof(struct stat));
    if(strcmp(path, "/") == 0) {
        stbuf->st_mode = S_IFDIR | 0755;
        stbuf->st_nlink = 2;
    }
    else if(strcmp(path, hello_path) == 0) {
        stbuf->st_mode = S_IFREG | 0444;
        stbuf->st_nlink = 1;
        stbuf->st_size = strlen(hello_str);
    }
    else
        res = -ENOENT;
    return res;
}
static int hello_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
                         off_t offset, struct fuse_file_info *fi)
{
    (void) offset;
    (void) fi;
    if(strcmp(path, "/") != 0)
        return -ENOENT;
    filler(buf, ".", NULL, 0);
    filler(buf, "..", NULL, 0);
    filler(buf, hello_path + 1, NULL, 0);
    return 0;
}
static int hello_open(const char *path, struct fuse_file_info *fi)
{
    if(strcmp(path, hello_path) != 0)
        return -ENOENT;
    if((fi->flags & 3) != O_RDONLY)
        return -EACCES;
    return 0;
}
static int hello_read(const char *path, char *buf, size_t size, off_t offset,
                      struct fuse_file_info *fi)
{
    size_t len;
    (void) fi;
    if(strcmp(path, hello_path) != 0)
        return -ENOENT;
    len = strlen(hello_str);
    if (offset < len) {
        if (offset + size > len)
            size = len - offset;
        memcpy(buf, hello_str + offset, size);
    } else
        size = 0;
    return size;
}
static struct fuse_operations hello_oper = {
    .getattr = hello_getattr,
    .readdir = hello_readdir,
    .open = hello_open,
    .read = hello_read,
};
int main(int argc, char *argv[])
{
    return fuse_main(argc, argv, &hello_oper);
}






출처 - http://blog.naver.com/PostView.nhn?blogId=xogml_blog&logNo=130141202446








'System > Common' 카테고리의 다른 글

grep - 특정 단어 빼고 출력  (0) 2013.03.08
GCC Optimization Options  (0) 2012.10.28
TCP Offload Engine(TOE)에 대한 이해  (0) 2012.10.28
scale-up vs scale-out  (0) 2012.10.28
경영 정보학(Business Intelligence, BI)  (0) 2012.10.27
Posted by linuxism
,


I. TCP/IP와 TOE

TCP/IP는 가장 널리 사용되는 컴퓨터 간의 통신 프로토콜이다. 그러나 불행히TCP/IP는 현재 서버 호스트 CPU의 가장 큰 부하로 작용하고 있다. 일반적으로 1bit/sec의 TCP/IP 데이터를 처리하는데 1Hz의 CPU 사이클이 필요하다. 즉 10/100Mbps 네트워크 속도에서 CPU는 TCP/IP를 처리하는데 큰 어려움이 없었다. 그러나 기가비트 이더넷의 출현으로 서버CPU는 TCP/IP 처리에 대부분의 사이클을 소모하게 되었다. (그림 1 (a))에 의하면 CPU 속도는 18개월마다 2배씩 증가하지만 NIC 대역폭은 그보다 훨씬 더 빠르게12개월에 3배씩 증가하고 있다. 네트워크 대역폭은 무어의 법칙을 훨씬 앞서가고 있는 것이다[4]. 예를 들면10 기가비트 이더넷만을 처리하기 위해서 전이중 통신을 고려할 때 20GHz의 프로세스가 필요하다. 이는 2007년에나 가능한 일이다. 더욱이 최근들어 TCP/IP 프로토콜 위에 SCSI 프로토콜을 올린 iSCSI 란 네트워크 스토리지 프로토콜의 출현과 네트워크로 연결된 한 컴퓨터에서 다른 컴퓨터로 직접 메모리 데이터를 액세스하는 RDMA(Remote Direct Memory Access) 기술 모두 TCP/IP 가속 기술이 먼저 선행되어야 가능한 기술이다. 
TCP/IP Offload Engine(TOE)은 CPU의 TCP/IP 패킷 처리의 부하를 NIC 하드웨어가 처리하는 TCP/IP 가속 장치이다. (그림 1 (b))와 같이 TOE는 소프트웨어가 담당하던 Transport, Network 계층을 하드웨어가 담당하도록 한다. 그러나 소프트웨어가 담당하던 다양하고 복잡한 TCP/IP 처리를 하드웨어로 모두 구현하기는 힘들다. 따라서 TOE를 생산하는 업체들은 각기 Offloading 수준을 달리하고 있다. 




II. TCP/IP 프로토콜 문제점

1. TCP/IP 프로토콜 기능

TCP/IP 프로토콜은 데이터 전송에 사용되는 표준으로 LAN이나 WAN 에 광범위하게 사용되고 있다. IP 레이어는 송수신 IP 주소를 포함하고 있으며 비연결성 특징을 가지고 있다. TCP 레이어에서는 데이터 스트림 서비스를 위해 신뢰성 있고 연결 지향적인 서비스를 제공한다. 일반적으로 인터넷 프로토콜로 사용되는 FTP, HTTP 등은 모두 TCP를 이용하고 있다. TCP 계층에서는 체크섬(checksum)을 통하여서 패킷의 헤더뿐 아니라 패킷 데이터까지 전송 시 발생하는 오류를 검사하고 있다. 초기 연결 시 설정되는 MSS(Maximum segment size)를 기준으로 패킷의 분할, 조립 기능을 수행한다. 또한 시퀀스 번호(Sequence number)를 통하여 순서대로 도착하지 않은 데이터들에 대한 정렬 기능을 제공하고 있다. 슬라이딩 윈도(sliding window) 기법을 통하여 수신부의 버퍼 상태를 파악하여 송신부에서 흐름 제어(Flow control)를 수행하며 반대로 송신부에서 나타내는 슬라이딩 윈도를 통하여 혼잡 제어(Congestion control)을 수행한다. 또한 TCP는 포트 제어를 통하여서 여러 가지 애플리케이션의 데이터를 다중화하여 송수신을 담당한다. 

2. Traditional TCP/IP 구조와 문제점

TCP/IP 프로토콜은 (그림 2)와 같은 레이어 프로토콜들로 이루어져 있다. 이는 소프트웨어로 구현되어 있으며 OS에 포함되어 있다. TCP 프로토콜과 애플리케이션의 인터페이스는 소켓(socket) 인터페이스가 이용된다. 



(그림 2)와 같은 인터페이스를 통해 애플리케이션이 네트워크를 통해 데이터를 보낼 경우 다음과 같은 데이터 이동과 프로토콜 작업을 거치게 된다. 
애플리케이션이 전송할 데이터를4~64KB 크기로 TCP/IP 소켓 인터페이스에 기록하면 OS는 데이터를 MTU(maximum transmission unit) 사이즈에 맞게 분할한 후 TCP/IP 헤더를 추가한다. 이후 OS는 데이터를 NIC 카드의 송신 큐로 복사를 한다. NIC은 TCP 버퍼 영역에서 NIC로 DMA 전송을 수행하며, 전송 종료를 알리는 인터럽터(Interrupts)가 발생하게 된다. 이러한 전통적인 TCP/IP 처리에는 다음과 같은 문제점이 있다.
- 인터럽트: 프로세스는 매 패킷이 더 할 때마다 패킷 헤더 처리를 위한 인터럽트가 발생하게 된다. 이는 대용량 데이터 전송 시 큰 부하로 작용한다.
- Context Switch 문제: 입력되는 패킷은 커널 레벨 프로세스를 위해서 컨텍스트 스위치가 필요하다. 사용자 영역(user space)에서 커널 영역(kernel space)으로 전환 시 데이터 전송의 초기화 혹은 종료가 필요하다. 
- 메모리 복사: (그림 3)은 송수신시 일반적인 NIC 카드의 데이터 복사 과정을 나타낸다. 송신 시 NIC은 먼저 애플리케이션 사용자 영역에서부터 OS 커널 영역으로 데이터 복사가 필요하다. 이후 NIC 드라이버는 커널에서 NIC 패킷 버퍼로 복사를 수행한다. 즉 두 번의 복사가 이루어진다. 반면 패킷을 수신할 경우, 먼저 NIC은 수신한 패킷을 메인 메모리에 위치한 NIC 버퍼에 저장한 후 이를 TCP 버퍼에 복사하고 이후 최종적으로 애플리케이션 영역에 복사한다. 총 세 번의 복사가 이루어지는 것이다. 만약 TOE를 사용한다면 NIC 버퍼가 필요없이 두 번의 복사만으로 처리가 가능해진다. 또한 TOE 를 사용한 RDMA는 제로카피(Zero Copy) 알고리즘에 의해 한 번에 애플리케이션 버퍼에 복사가 가능해진다. 



- 프로토콜 처리: 각각의 입출력 패킷은 TCP/IP 모든 스택을 거치면서 앞서 TCP/IP 프로토콜 기능에서 언급한 TCP/IP 패킷 처리가 이루어진다. 이는 애플리케이션 처리에 사용될 CPU 활용도를 크게 저하시킨다.


III. TOE 구조와 특징

기가비트 이더넷으로 네트워크 속도가 빨라짐에 따라 이러한 전통적인 이더넷 NIC 카드에서 CPU의 TCP/IP 처리의 부담을 덜어 주는 방법이 점차 늘어나고 있다. 다수의 기가비트 이더넷 컨트롤러에는 호스트 오프로드 특징으로TCP/IP/UDP 체크섬이나 TCP 분할(segmentation)을 하드웨어로 구현하고 있다[5].
체크섬은 데이터 무결성을 체크하기 위해 사용되며 송수신측에서 16비트 1의 보수 합으로 계산되어진다. TCP/IP/UDP 체크섬 오프로드 기술은 TCP와 IP 레이어에서 수행되는 체크섬 계산을 호스트 CPU가 담당하지 않고 NIC에서 처리하는 방법이다. IP 체크섬은 헤더만 이루어지며 TCP/UDP 체크섬의 경우에는 데이터까지 모두 계산하게 된다. 
TCP 분할 오프로드는 기존의 OS가 전송 데이터를 MTU 사이즈로 분할하는 작업을 담당한다. TCP 분할이 오프로드됨으로 대용량의 데이터를 송신할 때에 OS가 TCP 분할의 부담을 덜 수 있다. 이는 기가비트 이더넷과 같은 대용량의 메시지를 송신할 경우 매우 유용하다. 그러나 작은 패킷들이 많을 경우, 연결이 자주 끊기는 상황 등에서는 효과가 없다. 뿐만 아니라 수신측에는 전혀 도움이 되지 않는다. 
그러나 이 두 가지 오프로드만으로 높은 성능을 기대하기는 힘들다. 따라서 기존의 OS가 수행하는 TCP/IP 프로토콜 처리의 많은 부분을 오프로드한 TOE 제품들이 개발되고 있다. TOE를 구현하기 위해서는 TCP/IP를 처리하는 NIC 카드와 이 카드에 특화된 TCP/IP 소프트웨어 구현이 필요하다. (그림 4)는 완전한 TOE 기반의 TCP/IP 통신 스택을 나타내고 있다. (그림 2)의 스택과 달리 커널 영역에서 담당하던 TCP/IP 처리가 없으며 사용자 영역에서 커널 영역을 거치지 않고 TOE로 직접 접근이 가능하다.



TOE 구현은 오프로드 정도에 따라 크게 두 가지 형태로 나뉜다. TCP/IP의 일부 기능을 하드웨어로 구현하는 부분적 오프로딩(Partial offloading)과 모든 기능을 구현하는 전체 오프로딩(Full offloading) 방법이 있다. 일반적으로 부분적 오프로딩을 데이터 패스 오프로딩(Data path offloading)이라 부르며 이는 TCP/IP 데이터 송수신에 관련된 기능만을 하드웨어로 구현한다. 반면 전체 오프로딩은 데이터 송수신 기능뿐 아니라 TCP/IP 패킷을 처리하기 위한 TCP 연결 설정, 타임아웃, 오류 처리, 혼잡 제어, 슬라이딩 윈도 제어, ACK 처리 등과 같은 다양한 제어 기능들까지 모든 TCP/IP 기능을 하드웨어로 구현한다. 
TOE NIC구현 방법에는 크게 프로세스 기반 구현 방법과 칩(ASIC) 기반 구현 방법이 있다. 프로세스 기반 구현 방법은 시중에 출시된 네트워크 프로세스를 사용하여 빠르고 유연성(flexibility) 있는 설계가 가능하나 칩 기반 구현에 비해 높은 가격과 낮은 성능의 단점을 가지고 있다. 따라서 대부분의 TOE 업체에서는 TCP/IP 오프로딩을 위한 전용 칩을 개발하여 칩 기반의 TOE를 구현하고 있다. 


출처 - http://www.solway.co.kr/bbs/board.php?bo_table=resource&wr_id=2



'System > Common' 카테고리의 다른 글

GCC Optimization Options  (0) 2012.10.28
FUSE(Filesystem in USEerspace)  (0) 2012.10.28
scale-up vs scale-out  (0) 2012.10.28
경영 정보학(Business Intelligence, BI)  (0) 2012.10.27
Lustre 파일시스템이란?  (0) 2012.10.27
Posted by linuxism
,