반응형

이번 장에서는 텍스트 형태의 코드를 컴파일 가능한 논리적 구조로 파싱하는 방법을 다룸

CPython은 코드를 파싱하기 위해 CST(concrete syntax tree)와 AST(abstract syntax tree) 두가지 구조를 사용

파싱 과정은 아래와 같음

  1. 파서-토크나이저 또는 렉서(lexer)가 CST를 생성
  2. 파서가 CST로 부터 AST를 생성

6.1 CST 생성

  • 파스 트리라고도 부르는 CST는 문맥 자유 문법에서 코드를 표현하는 루트와 순서가 있는 트리
  • 토크나이저와 파서가 CST를 생성. 파서 생성기는 문맥 자유 문법이 가질 수 있는 상태에 대한 결정적 유한 오토마타 파싱 테이블을 생성
  • CST에서 if_stmt같은 심벌은 분기로, 토큰과 단말 기호는 리프 노드로 표시

ex) 산술 표현식 a + 1 을 CST로 표현하면 아래와 같음

 

  • 산술 표현식은 크게 좌측 항, 연산자, 우측항으로 나뉨
  • 파서는 입력 스트미으로 들어오는 토큰들이 문법적으로 허용되는 토큰과 상태인지 확인하며 CST를 생성
  • CST를 구성하는 모든 심벌은 Grammar/Grammar 파일에서 정의
# L147
arith_expr: term (('+'|'-') term)*
term: factor (('*'|'@'|'/'|'%'|'//') factor)*
factor: ('+'|'-'|'~') factor | power
power: atom_expr ['**' factor]
atom_expr: [AWAIT] atom trailer*
atom: ('(' [yield_expr|testlist_comp] ')' |
       '[' [testlist_comp] ']' |
       '{' [dictorsetmaker] '}' |
       NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False')

토큰은 Grammar/Tokens 파일에서 정의

ENDMARKER
NAME
NUMBER
STRING
NEWLINE
INDENT
DEDENT

LPAR                    '('
RPAR                    ')'
LSQB                    '['
RSQB                    ']'
COLON                   ':'
COMMA                   ','
SEMI                    ';'
  • NAME 토큰은 변수나 함수, 클래스 모듈의 이름을 표현
  • 파이썬 문법에서 await이나 async 같은 예약어나 숫자 형식 또는 리터럴 타입 등은 NAME 값으로 쓸 수 없음
  • 예를 들어, 함수 이름으로 1을 사용하려고 하면 SyntaxError가 발생
>>> def 1():
  File "<python-input-0>", line 1
    def 1():
        ^
SyntaxError: invalid syntax

NUMBER는 다양한 숫자 형식 값을 표현하는 토큰으로 다음과 같은 특수 문법들을 사용할 수 있음

  • 8진수 값: 0o20
  • 16진수 값: 0x10
  • 이진수 값: 0b1000
  • 복소수 값: 10j
  • 부동 소수점 값: 1.01
  • 밑줄로 구분된 값: 1_000_000

6.2 파서-토크나이저

  • 렉서 구현은 프로그래밍 언어마다 다르고 렉서 생성기로 파서 생성기를 보완하는 언어도 있음
  • CPython의 파서-토크나이저는 C로 작성되었음

6.2.1 연관된 소스 파일 목록

  • Python/pythonrun.c : 파서와 컴파일러 실행
  • Parser/parsetok.c : 파서와 토크나이저 구현
  • Parser/tokenizer.c : 토크나이저 구현
  • Parser/tokenizer.h : 토큰 상태 등의 데이터 모델을 정의하는 토크나이저 구현 헤더 파일
  • Include/token.h : Tools/scripts/generate_token.py에 의해 생성되는 토큰 정의
  • Include/node.h : 토크나이저를 위한 CST 노드 인터페이스와 매크로

6.2.2 파일 데이터를 파서에 입력하기

  • 파서-토크나이저 진입점인 PyParser_ASTFromFileobject()는 파일 핸들과 컴파일러 플래그, PyArena 인스턴스를 받아 파일 객체를 모듈로 변환

파일을 2단계로 변환됨

  1. PyParser_ParseFileObject()를 사용해 CST로 변환
  2. AST 함수 PyAST_FromNodeObject()를 사용해 CST를 AST 또는 모듈로 변환

PyParser_ParseFileObject() 함수는 2가지 중요 작업을 수행

  1. PyTokenizer_FromFile()을 사용해 토크나이저 상태 tok_state를 초기화
  2. parsetok()을 사용해 토큰들을 CST(노드 리스트)로 변환

6.2.3 파서-토크나이저의 흐름

  • 커서가 텍스트 입력의 끝에 도달하거나 문법 오류가 발견될 때까지 파서와 토크나이저를 실행
// Parser/parsetok.c L164
node *
PyParser_ParseFileObject(FILE *fp, PyObject *filename,
                         const char *enc, grammar *g, int start,
                         const char *ps1, const char *ps2,
                         perrdetail *err_ret, int *flags)
{
    struct tok_state *tok; // (1)

    if (initerr(err_ret, filename) < 0)
        return NULL;

    if (PySys_Audit("compile", "OO", Py_None, err_ret->filename) < 0) {
        return NULL;
    }

    if ((tok = PyTokenizer_FromFile(fp, enc, ps1, ps2)) == NULL) {
        err_ret->error = E_NOMEM;
        return NULL;
    }
    if (*flags & PyPARSE_TYPE_COMMENTS) {
        tok->type_comments = 1;
    }
    Py_INCREF(err_ret->filename);
    tok->filename = err_ret->filename;
    return parsetok(tok, g, start, err_ret, flags);
}

(1) 파서-토크나이저는 실행 전에 토크나이저에서 사용하는 모든 상태를 저장하는 임시 데이터 구조인 tok_state를 초기화

  • tok_state 구조체
더보기
/* Tokenizer state */
struct tok_state {
    /* Input state; buf <= cur <= inp <= end */
    /* NB an entire line is held in the buffer */
    char *buf;          /* Input buffer, or NULL; malloc'ed if fp != NULL */
    char *cur;          /* Next character in buffer */
    char *inp;          /* End of data in buffer */
    const char *end;    /* End of input buffer if buf != NULL */
    const char *start;  /* Start of current token if not NULL */
    int done;           /* E_OK normally, E_EOF at EOF, otherwise error code */
    /* NB If done != E_OK, cur must be == inp!!! */
    FILE *fp;           /* Rest of input; NULL if tokenizing a string */
    int tabsize;        /* Tab spacing */
    int indent;         /* Current indentation index */
    int indstack[MAXINDENT];            /* Stack of indents */
    int atbol;          /* Nonzero if at begin of new line */
    int pendin;         /* Pending indents (if > 0) or dedents (if < 0) */
    const char *prompt, *nextprompt;          /* For interactive prompting */
    int lineno;         /* Current line number */
    int first_lineno;   /* First line of a single line or multi line string
                           expression (cf. issue 16806) */
    int level;          /* () [] {} Parentheses nesting level */
            /* Used to allow free continuations inside them */
    char parenstack[MAXLEVEL];
    int parenlinenostack[MAXLEVEL];
    PyObject *filename;
    /* Stuff for checking on different tab sizes */
    int altindstack[MAXINDENT];         /* Stack of alternate indents */
    /* Stuff for PEP 0263 */
    enum decoding_state decoding_state;
    int decoding_erred;         /* whether erred in decoding  */
    int read_coding_spec;       /* whether 'coding:...' has been read  */
    char *encoding;         /* Source encoding. */
    int cont_line;          /* whether we are in a continuation line. */
    const char* line_start;     /* pointer to start of current line */
    const char* multi_line_start; /* pointer to start of first line of
                                     a single line or multi line string
                                     expression (cf. issue 16806) */
    PyObject *decoding_readline; /* open(...).readline */
    PyObject *decoding_buffer;
    const char* enc;        /* Encoding for the current str. */
    char* str;
    char* input;       /* Tokenizer's newline translated copy of the string. */

    int type_comments;      /* Whether to look for type comments */

    /* async/await related fields (still needed depending on feature_version) */
    int async_hacks;     /* =1 if async/await aren't always keywords */
    int async_def;        /* =1 if tokens are inside an 'async def' body. */
    int async_def_indent; /* Indentation level of the outermost 'async def'. */
    int async_def_nl;     /* =1 if the outermost 'async def' had at least one
                             NEWLINE token after it. */
};

(2) 토크나이저 상태는 커서의 현재 위치 같은 정보를 저장 (Parser/tokenizer.h)

  • Parser/tokenizer.h
더보기
/* Tokenizer state */
struct tok_state {
    /* Input state; buf <= cur <= inp <= end */
    /* NB an entire line is held in the buffer */
    char *buf;          /* Input buffer, or NULL; malloc'ed if fp != NULL */
    char *cur;          /* Next character in buffer */
    char *inp;          /* End of data in buffer */
    const char *end;    /* End of input buffer if buf != NULL */
    const char *start;  /* Start of current token if not NULL */
    int done;           /* E_OK normally, E_EOF at EOF, otherwise error code */
    /* NB If done != E_OK, cur must be == inp!!! */
    FILE *fp;           /* Rest of input; NULL if tokenizing a string */
    int tabsize;        /* Tab spacing */
    int indent;         /* Current indentation index */
    int indstack[MAXINDENT];            /* Stack of indents */
    int atbol;          /* Nonzero if at begin of new line */
    int pendin;         /* Pending indents (if > 0) or dedents (if < 0) */
    const char *prompt, *nextprompt;          /* For interactive prompting */
    int lineno;         /* Current line number */
    int first_lineno;   /* First line of a single line or multi line string
                           expression (cf. issue 16806) */
    int level;          /* () [] {} Parentheses nesting level */
            /* Used to allow free continuations inside them */
    char parenstack[MAXLEVEL];
    int parenlinenostack[MAXLEVEL];
    PyObject *filename;
    /* Stuff for checking on different tab sizes */
    int altindstack[MAXINDENT];         /* Stack of alternate indents */
    /* Stuff for PEP 0263 */
    enum decoding_state decoding_state;
    int decoding_erred;         /* whether erred in decoding  */
    int read_coding_spec;       /* whether 'coding:...' has been read  */
    char *encoding;         /* Source encoding. */
    int cont_line;          /* whether we are in a continuation line. */
    const char* line_start;     /* pointer to start of current line */
    const char* multi_line_start; /* pointer to start of first line of
                                     a single line or multi line string
                                     expression (cf. issue 16806) */
    PyObject *decoding_readline; /* open(...).readline */
    PyObject *decoding_buffer;
    const char* enc;        /* Encoding for the current str. */
    char* str;
    char* input;       /* Tokenizer's newline translated copy of the string. */

    int type_comments;      /* Whether to look for type comments */

    /* async/await related fields (still needed depending on feature_version) */
    int async_hacks;     /* =1 if async/await aren't always keywords */
    int async_def;        /* =1 if tokens are inside an 'async def' body. */
    int async_def_indent; /* Indentation level of the outermost 'async def'. */
    int async_def_nl;     /* =1 if the outermost 'async def' had at least one
                             NEWLINE token after it. */
};
  • 파서-토크나이저는 tok_get()으로 다음 토큰을 얻고 그 아이디를 파서로 전달
// Parser/tokenizer.c L1174
/* Get next token, after space stripping etc. */

static int
tok_get(struct tok_state *tok, const char **p_start, const char **p_end)
{
	int c;
	int blankline, nonascii;
	*p_start = *p_end = NULL;
	nextline:
	tok->start = NULL;
	blankline = 0;
	/* Get indentation level */
	if (tok->atbol) {
	...
	return PyToken_OneChar(c);
	}
  • 파서는 파서 생성기 오토마타(DFA)로 CST에 노드를 추가

640줄이 넘는 tok_get()은 CPython 코드 중에서도 손꼽히게 복잡한 부분중 하나. 루프에서 토크나이저와 파서를 호출하는 과정은 아래와 같음

  • CST→AST로 변환하려면 PyParser_ParseFileObject()에서 반환된 CST의 루트인 node가 필요
node *
PyParser_ParseFileObject(FILE *fp, PyObject *filename,
                         const char *enc, grammar *g, int start,
                         const char *ps1, const char *ps2,
                         perrdetail *err_ret, int *flags)
{
    struct tok_state *tok;

    if (initerr(err_ret, filename) < 0)
        return NULL;

    if (PySys_Audit("compile", "OO", Py_None, err_ret->filename) < 0) {
        return NULL;
    }

    if ((tok = PyTokenizer_FromFile(fp, enc, ps1, ps2)) == NULL) {
        err_ret->error = E_NOMEM;
        return NULL;
    }
    if (*flags & PyPARSE_TYPE_COMMENTS) {
        tok->type_comments = 1;
    }
    Py_INCREF(err_ret->filename);
    tok->filename = err_ret->filename;
    return parsetok(tok, g, start, err_ret, flags);
}

 

노드 구조체는 Include/node.h에서 정의

typedef struct _node {
    short               n_type;
    char                *n_str;
    int                 n_lineno;
    int                 n_col_offset;
    int                 n_nchildren;
    struct _node        *n_child;
    int                 n_end_lineno;
    int                 n_end_col_offset;
} node;

 

CST는 구문, 토큰 아이디, 심벌을 모두 포함하기 때문에 컴파일러가 사용하기에는 적합하지 않음

AST를 살펴보기에 앞서 파서 단계의 결과를 확인하는 방법이 있는데, parser 모듈은 C함수의 파이썬 API를 제공

>>> import parser
st<stdin>:1: DeprecationWarning: The parser module is deprecated and will be removed in future versions of Python
>>> st = parser.expr('a+1')
>>> 
>>> 
>>> pprint(parser.st2list(st))
[258,
 [332,
  [306,
   [310,
    [311,
     [312,
      [313,
       [316,
        [317,
         [318,
          [319,
           [320,
            [321, [322, [323, [324, [325, [1, 'a']]]]]],
            [14, '+'],
            [321, [322, [323, [324, [325, [2, '1']]]]]]]]]]]]]]]]],
 [4, ''],
 [0, '']]
  • Parser 모듈의 출력은 숫자형식으로 make regen-grammar 단계에서 Include/token.h파일에 저장된 코튼과 심벌의 번호와 같음

좀더 보기 쉽게 symbol과 token 모듈의 모든 번호로 딕셔너리를 만든 후 parser.st2list()의 출력을 토큰과 심벌의 이름으로 재귀적으로 바꾸면 아래와 같음

import symbol
import token
import parser
from pprint import pprint

def lex(expression):
    symbols = {v: k for k, v in symbol.__dict__.items() if isinstance(v, int)}
    tokens = {v: k for k, v in token.__dict__.items() if isinstance(v, int)}
    lexicon = {**symbols, **tokens}
    st = parser.expr(expression)
    st_list = parser.st2list(st)

    def replace(l: list):
        r = []
        for i in l:
            if isinstance(i, list):
                r.append(replace(i))
            else:
                if i in lexicon:
                    r.append(lexicon[i])
                else:
                    r.append(i)
        return r

    return replace(st_list)

pprint(lex("a + 1"))
['eval_input',
 ['testlist',
  ['test',
   ['or_test',
    ['and_test',
     ['not_test',
      ['comparison',
       ['expr',
        ['xor_expr',
         ['and_expr',
          ['shift_expr',
           ['arith_expr',
            ['term',
             ['factor', ['power', ['atom_expr', ['atom', ['NAME', 'a']]]]]],
            ['PLUS', '+'],
            ['term',
             ['factor',
              ['power', ['atom_expr', ['atom', ['NUMBER', '1']]]]]]]]]]]]]]]]],
 ['NEWLINE', ''],
 ['ENDMARKER', '']]
  • symbol은 arith_expr 처럼 소문자로, 토큰은 NUMBER처럼 대문자로 출력

 

6.3 추상 구문 트리

  • 파서가 생성한 CST를 실행가능하면서 좀 더 논리적으로 변환하는 단계
  • CST는 코드 파일의 텍스트를 있는 그대로 표현하는 구조로, 텍스트로부터 토큰을 추출하여 토큰 종류만 구분해 둔 상태에 불과
  • CST로 기본적인 문법 구조는 알 수 있지만 함수, 스코프, 루프 같은 파이썬 언어 사양에 대한 의미를 결정할 수 없음
  • 코드를 컴파일 하기 전 CST를 실제 파이썬 언어구조와 의미 요소를 표현하는 고수준 구조인 AST로 변환해야 함
  • 예를 들어 AST에서 이항 연산은 표현식의 한 종류인 BinOp로 표현. 해당 표현식은 세가지 요소로 이루어짐
  1. left: 왼쪽 항
  2. op: 연산자(+, -, * 등)
  3. right: 오른쪽 항

다음은 a + 1 에 대한 AST

 

  • AST는 CPython 파싱 과정 중에 생성하지만 표준 라이브러리 ast모듈을 사용해서 파이썬 코드에서 AST를 생성할 수도 있음

6.3.1 AST 연관된 소스 파일 목록

  • Include/python-ast.h: Parser/asdl_c.py 로 생성한 AST 노드 타입 선언
  • parser/Python.asdl: 도메인 특화 언어인 ASDL(abstract syntax description language) 5로 작성된 ast 노드 타입들과 프로퍼티 목록
  • Python/ast.c: AST 구현

6.3.2 인스타비즈로 AST 시각화하기

$ pip install instaviz
  • AST와 컴파일된 코드를 웹 인터페이스로 시각화 하는 파이썬 패키지
❯ python                                                                     
Python 3.9.20 (main, Sep  6 2024, 19:03:56) 
[Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import instaviz
>>> def example():
...     a = 1
...     b = a + 1
...     return b
... 
>>> 
>>> instaviz.show(example)

 

  • 트리의 각 노드의 타입은 AST 노드 클래스
  • ast 모듈에서 찾을 수 있는 노드 클래스들은 모두 _ast.AST를 상속

  • CST와 달리, AST의 노드들은 특정 프로퍼티들을 통해 자식 노드와 연결됨
  • b = a + 1 이 선언된 줄과 연결된 assign 노드를 클릭하면 아래와 같음

  • Assign 노드는 2개의 프로퍼티를 가짐
  1. targets는 값이 할당될 이름의 목록. 언패킹을 통해 한 번에 여러 이름에 값을 할당할 수 있기 때문에 목록이 필요
  2. value는 이름에 할당할 값. 이 경우에 BinOp 표현식 a + 1 이 할당됨
  • BinOp노드는 세 개의 프로퍼티를 가짐
  1. left: 왼쪽 항
  2. op: 연산자, 이 경우에는 더 하기를 뜻하는 Add 노드(+)
  3. right: 오른쪽 항

 

6.3.3 AST 컴파일

  • C에서 AST를 컴파일하는 것을 매우 복잡한 작업으로 Python/ast.c 모듈은 5000줄이 넘는 코드로 이루어져 있음
  • AST의 공개 API는 CST와 파일 이름, 컴파일러 플래그 ,메모리 저장 영역을 인자로 받음
  • 반환 타입은 파이썬 모듈을 표현하는 mod_ty 타입으로 Include/Python-ast.h 에서 정의

mod_ty는 아래 4가지 모듈 타입 중 하나를 담는 컨테이너 구조체

  1. Module
  2. Interactive
  3. Expression
  4. FunctionType
  • 모듈 타입은 Parser/Python.asdl에서 정의하는데 문장, 표현식, 연산자, 컴프리헨션 타입들도 찾을 수 있음
  • AST가 생성하는 클래스들과 표준 라이브러리 ast 모듈의 클래스들은 Parser/Python.asdl에서 정의하는 타입들

Parser/Python.asdl 파일 내용의 일부

-- ASDL's 4 builtin types are:
-- identifier, int, string, constant

module Python
{
    mod = Module(stmt* body, type_ignore* type_ignores)
        | Interactive(stmt* body)
        | Expression(expr body)
        | FunctionType(expr* argtypes, expr returns)
  • ast 모듈은 문법을 다시 생성할 때 Include/Python-ast.h를 임포트하는데 이 파일은 Parser/Python.asdl에서 자동으로 생성됨
  • Include/Python-ast.h 파라미터와 이름은 Parser/Python.asdl의 정의를 따름
  • Include/Python-ast.h 의 mod_ty 타입은 Parser/Python.asdl 의 Module 정의로부터 생성됨
// Include/Python-ast.h
struct _mod {
    enum _mod_kind kind;
    union {
        struct {
            asdl_seq *body;
            asdl_seq *type_ignores;
        } Module;

        struct {
            asdl_seq *body;
        } Interactive;

        struct {
            expr_ty body;
        } Expression;

        struct {
            asdl_seq *argtypes;
            expr_ty returns;
        } FunctionType;

    } v;
};
  • Python/ast.c 는 이 C 헤더 파일에서 제공하는 구조체들을 사용해 필요한 데이터를 가리키는 포인터를 담은 구조체들을 신속하게 생성
  • AST의 진입점인 PyAST_FromNodeObject()는 TYPE(n)에 대한 switch 문을 실행
  • TYPE()은 CST 노드의 타입을 결정하는 매크로로 결과로 심벌 또는 토큰 타입을 반환
  • 루트 노드의 타입은 항상 Module, Interactive, Expression, FunctionType 중 하나
    • file_input일 경우 Module 타입
    • REPL 등으로 들어오는 eval_input 인 경우는 Expression 타입
  • Python/ast.c에는 각 타입에 대응되는 ast_for_xxx 이름의 C 함수들이 구현되어있음
  • 이 함수들은 CST의 노드 중에 해당 문에 대한 프로퍼티를 찾음

ex) 2 의 4제곱을 뜻하는 2 ** 4같은 제곱에 대한 표현식

  • ast_for_power()는 연산자가 Pow(제곱), 좌측은 e(2), 우측은 f(4)인 BinOp를 반환
// Python/ast.c L2716
static expr_ty
ast_for_power(struct compiling *c, const node *n)
{
    /* power: atom trailer* ('**' factor)*
     */
    expr_ty e;
    REQ(n, power);
    e = ast_for_atom_expr(c, CHILD(n, 0));
    if (!e)
        return NULL;
    if (NCH(n) == 1)
        return e;
    if (TYPE(CHILD(n, NCH(n) - 1)) == factor) {
        expr_ty f = ast_for_expr(c, CHILD(n, NCH(n) - 1));
        if (!f)
            return NULL;
        e = BinOp(e, Pow, f, LINENO(n), n->n_col_offset,
                  n->n_end_lineno, n->n_end_col_offset, c->c_arena);
    }
    return e;
}
import instaviz

def foo():
    2**4

instaviz.show(foo)

요약

  • 모든 타입의 문과 표현식에는 ast_for_xx() 생성자 함수가 있음
  • 함수의 인자들은 Parser/Python.asdl에서 정의하며 표준 라이브러리의 ast 모듈을 통해 외부에 제공
  • 표현식 또는 문이 자식 노드를 가지고 있으면 깊이 우선 탐색을 통해 자식 노드에 대한 ast_for_xx () 함수를 먼저 호출

 

6.4 중요한 용어들

  • AST(Abstract Syntax Tree): 파이썬 문법과 문장들에 대한 문맥 있는 트리 표현
  • CST(Concrete Syntax Tree): 토큰과 심벌에 대한 문맥 없는 트리 표현
  • 파스 트리 (Parse Tree): CST의 다른 이름
  • 토큰: 심벌의 종류 중 하나(ex) +)
  • 토큰화: 텍스트를 토큰으로 변환하는 과정
  • 파싱: 텍스트를 CST나 AST로 변환하는 과정

 

6.5 예제: ‘거의 같음’ 비교 연산자 추가하기

  • 새로운 문법인 거의 같음 연산자 (’~=’)을 추가하고 CPython을 컴파일
  • 아래와 같이 동작
    • 정수와 부동 소수점을 비교할 때 부동 소수점은 정수로 변환해 비교
    • 정수와 정수를 비교할 때는 일반 동등 연산자를 사용

REPL에서 새 연산자를 사용하면 아래와 같은 결과를 볼 수 있어야 함

>>> 1 ~= 1
True

>>> 1 ~= 1.0
True

>>> 1 ~= 1.01
True

>>> 1 ~= 1.9
False

  1. 먼저 CPython 문법을 변경해야 함. 비교 연산자들은 Grammar/python.gram 파일에 comp_op 심벌로 정의 (L398: L413)
comparison[expr_ty]:
    | a=bitwise_or b=compare_op_bitwise_or_pair+ {
        _Py_Compare(a, CHECK(_PyPegen_get_cmpops(p, b)), CHECK(_PyPegen_get_exprs(p, b)), EXTRA) }
    | bitwise_or
compare_op_bitwise_or_pair[CmpopExprPair*]:
    | eq_bitwise_or
    | noteq_bitwise_or
    | lte_bitwise_or
    | lt_bitwise_or
    | gte_bitwise_or
    | gt_bitwise_or
    | notin_bitwise_or
    | in_bitwise_or
    | isnot_bitwise_or
    | is_bitwise_or
    | ale_bitwise_or (추가된 내용)
  • compare_op_bitwise_or_pair 식에 ale_bitwise_or를 허용

L414: L424 에서 ale_bitwise_or 추가해서 ‘~=’ 단말 기호를 포함하는 식을 정의

eq_bitwise_or[CmpopExprPair*]: '==' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, Eq, a) }
noteq_bitwise_or[CmpopExprPair*]:
    | (tok='!=' { _PyPegen_check_barry_as_flufl(p, tok) ? NULL : tok}) a=bitwise_or {_PyPegen_cmpop_expr_pair(p, NotEq, a) }
lte_bitwise_or[CmpopExprPair*]: '<=' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, LtE, a) }
lt_bitwise_or[CmpopExprPair*]: '<' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, Lt, a) }
gte_bitwise_or[CmpopExprPair*]: '>=' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, GtE, a) }
gt_bitwise_or[CmpopExprPair*]: '>' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, Gt, a) }
notin_bitwise_or[CmpopExprPair*]: 'not' 'in' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, NotIn, a) }
in_bitwise_or[CmpopExprPair*]: 'in' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, In, a) }
isnot_bitwise_or[CmpopExprPair*]: 'is' 'not' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, IsNot, a) }
is_bitwise_or[CmpopExprPair*]: 'is' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, Is, a) }
ale_bitwise_or[CmpopExprPair*]: '~=' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, ALE, a) }  (추가된 내용)
  • _PyPegen_cmpop_expr_pair(p, ALE, a) 함수 호출은 AST에서 ‘거의 같음’ 연산자를 뜻하는 AlE(Almost Equal) 타입 cmpop 노드를 가져옴
  1. Grammar/Tokens에 토큰 추가 (L54)
ELLIPSIS                '...'
COLONEQUAL              ':='
ALMOSTEQUAL             '~=' (추가된 내용)
  1. 변경된 문법과 토큰을 C코드에 반영하기 위해 헤더를 다시 생성
❯ make regen-token regen-pegen
# Regenerate Doc/library/token-list.inc from Grammar/Tokens
# using Tools/scripts/generate_token.py
python3.9 ./Tools/scripts/generate_token.py rst \\
                ./Grammar/Tokens \\
                ./Doc/library/token-list.inc
# Regenerate Include/token.h from Grammar/Tokens
# using Tools/scripts/generate_token.py
python3.9 ./Tools/scripts/generate_token.py h \\
                ./Grammar/Tokens \\
                ./Include/token.h
# Regenerate Parser/token.c from Grammar/Tokens
# using Tools/scripts/generate_token.py
python3.9 ./Tools/scripts/generate_token.py c \\
                ./Grammar/Tokens \\
                ./Parser/token.c
# Regenerate Lib/token.py from Grammar/Tokens
# using Tools/scripts/generate_token.py
python3.9 ./Tools/scripts/generate_token.py py \\
                ./Grammar/Tokens \\
                ./Lib/token.py
PYTHONPATH=./Tools/peg_generator python3.9 -m pegen -q c \\
                ./Grammar/python.gram \\
                ./Grammar/Tokens \\
                -o ./Parser/pegen/parse.new.c
python3.9 ./Tools/scripts/update_file.py ./Parser/pegen/parse.c ./Parser/pegen/parse.new.c
  • 헤더를 다시 생성하면 토크나이저도 자동으로 변경됨. Parser/token.c 파일 안에 _PyParser_TokenNames 배열에 "ALMOSTEQUAL"추가 및 PyToken_TwoChars()함수의 case에 ‘~’와 ‘=’가 추가된 것을 확인 할 수 있음
const char * const _PyParser_TokenNames[] = {
	...
	"ALMOSTEQUAL",
	...
};

int
PyToken_TwoChars(int c1, int c2)
{
	switch (c1){
    case '~':
        switch (c2) {
        case '=': return ALMOSTEQUAL;
        }
        break;
        ...
  1. CPython을 다시 컴파일하고 REPL을 실행해보면 토크나이저는 새 토큰을 처리할 수 있지만 AST는 처리하지 못함

ast.c의 ast_for_comp_op()는 ALMOSTEQUAL을 올바른 비료 연산자로 인식할 수 없기 때문에 예외를 발생시킴

Parser/Python.asdl에 정의하는 Compare 표현식은 좌측 표현식 left, 연산자목록인 ops, 비교할 표현식 목록인 comparators로 이루어져 있음

Compare 정의는 cmpop 열거 형을 참조하면되고, 이 열거형은 비교 연산자로 사용할 수 있는 AST 리프 노드의 목록. 여기에 ‘거의 같음’ 연산자인 AlE를 추가

    cmpop = Eq | NotEq | Lt | LtE | Gt | GtE | Is | IsNot | In | NotIn | AlE

$ make regen-ast

Incude/Python-ast.h에서 비교 연산자를 정의하는 열거형인 _cmpop에 AlE가 추가된 것을 확인할 수 있음

//Include/Python-ast.h L30
typedef enum _cmpop { Eq=1, NotEq=2, Lt=3, LtE=4, Gt=5, GtE=6, Is=7, IsNot=8,
                      In=9, NotIn=10, AlE=11 } cmpop_ty;

AST는 ALMOSTEQUAL 토큰이 비교 연산자인 AlE라는 것을 아직 알 수 없어서, 토큰을 연산자로 인식할 수 있게 AST관련 C 코드를 수정

Python/ast.c의 ast_for_comp_op()로 이동해서 switch문을 찾아보면, _cmpop 열거형 값 중 하나를 반환 함

static cmpop_ty
ast_for_comp_op(struct compiling *c, const node *n)
{
    /* comp_op: '<'|'>'|'=='|'>='|'<='|'!='|'in'|'not' 'in'|'is'
               |'is' 'not'
    */
    REQ(n, comp_op);
    if (NCH(n) == 1)
    {
        n = CHILD(n, 0);
        switch (TYPE(n))
        {
        case LESS:
            return Lt;
        case GREATER:
            return Gt;
        case ALMOSTEQUAL: // 추가된 내용
            return AlE;   // 추가된 내용
        case EQEQUAL: /* == */
            return Eq;
        case LESSEQUAL:
            return LtE;
        case GREATEREQUAL:
            return GtE;
        case NOTEQUAL:
            return NotEq;
        case NAME:
            if (strcmp(STR(n), "in") == 0)
                return In;
            if (strcmp(STR(n), "is") == 0)
                return Is;
            /* fall through */

이제 토크나이저와 AST모두 코드를 파싱할 수 있지만 컴파일러는 아직 이 연산자를 실행하는 방법을 모름

AST로 거의 같음 연산자를 아래와 같이 확인해볼 수 있음

import ast
m = ast.parse('1 ~= 2')
m.body[0].value.ops[0]
<_ast.AlE object at 0x111111>
반응형
반응형

03-2 IP 주소

네트워크 주소와 호스트 주소

  • 아래는 네트워크 주소가 16비트 , 호스트 주소가 16비트인 IP 주소의 예시

  • 네트워크 주소: 네트워크 ID, 네트워크 식별자로 불리기도 함
  • 호스트 주소: 호스트 ID, 호스트 식별자로 불리기도 함

  • 위와 같이 네트워크 주소가 하나의 옥텟으로 이루어져 있다면, 한 네트워크당 호스트 주소 할당에 3바이트 (24바이트)를 사용할 수 있어서 상대적으로 많은 호스트 IP 주소 할당 가능

  • 위와 같이 네트워크 주소가 3개의 옥텟으로 이루어져 있다면, 네트워크 당 호스트 주소 할 당에 1바이트(8비트)를 사용할 수 있으며 상대적으로 적은 IP 주소만 할당 가능
  • 위 예들처럼 IP 주소에서 네트워크 주소와 호스트 주소를 구분하는 범위는 유동적인데 각각 어느정도를 할당하는게 적당할까?

 

클래스풀 주소 체계

  • 클래스는 네트워크 크기에 따라 IP 주소를 분류하는 기준
  • 클래스를 이용하면 필요한 호스트 IP 개수에 따라 네트워크 크기를 가변적으로 조정해 네트워크 주소와 호스트 주소를 구획할 수 있음
  • 클래스를 기반으로 IP 주소를 관리하는 주소 체계를 클래스풀 주소 체계(classful addressing)라고 함

A, B, C 클래스가 있다고 가정

A 클래스

  • B, C클래스에 비해 할당 가능한 호스트 주소의 수가 많음
  • 네트워크 주소는 ‘’비트 ‘0’으로 시작하고 1옥텟으로 구성되며, 호스트 주소는 3옥텟. 이론상으로 $2^7(128)$ 개의 A클래스 네트워크가 존재할 수 있고 $2^{24}(16,777,216)$ 개의 호스트 주소를 가질 수 있음
  • A 클래스로 나타낼 수 있는 IP 주소의 최솟값을 10진수로 표현하면 0.0.0.0, 최대값은 127.255.255.255
  • 요컨대 가장 처음 옥텟의 주소가 0~127일 경우 A 클래스 주소임을 짐작할 수 있음

B 클래스

  • 네트워크 주소는 비트 ‘10’으로 시작하고 2옥텟으로 구성되며 호스트 주소도 2옥텟으로 구성
  • 이론상으로 $2^{14}(16,384)$개의 B클래스 네트워크와 $2^{16}(65,534)$개의 호스트 주소를 가질 수 있음
  • B클래스 IP 주소값의 최소값을 10진수로 표현하면, 128.0.0.0, 최대값은 191.255.255.255임
  • 가장 처음 옥텟의 주소가 128~192 일 경우 B클래스 주소임을 짐작할 수 있음

C 클래스

  • 네트워크 주소는 비트’110’으로 시작하고 3옥텟으로 구성되며 호스트 주소는 1옥텟으로 구성
  • 이론상으로 $2^{21}(2,097,152)$개의 C클래스 네트워크가 존재할 수 있고, 각 네트워크는 $2^8(256)$개의 호스트 주소를 가질 수 있음
  • C클래스 IP 주소값의 최소값을 10진수로 표현하면, 192.0.0.0, 최대값은 223.255.255.255임
  • 가장 처음 옥텟의 주소가 192~224일 경우 C클래스 주소임을 짐작할 수 있음

다만 호스트의 주소 공간을 모두 사용할 수 있는 것은 아님. 호스트 주소가 전부 0인 IP 주소는 해당 네트워크 ㅈ체를 의미하는 네트워크 주소로 사용되고, 호스트 주소가 모두 1인 IP 주소는 브로드캐스트를 위한 주소로 사용됨

 

References

 

반응형
반응형

ARP(Address Resolution Protocol)

  • 통신을 주고 받고자 하는 호스트의 IP 주소는 알지만 MAC 주소는 모르는 경우 사용되는 프로토콜
  • 동일 네트워크 내에 있는 송수신 대상의 IP 주소를 통해 MAC 주소를 알아낼 수 있음

예시 상황

  • 동일 네트워크에 속한 호스트 A, B가 있음
  • 호스트 A는 호스트 B의 IP 주소는 알지만 MAC 주소는 모름
  • 이 상황에서 호스트 B의 MAC 주소를 알아내기 위해 ARP를 이용함

1. ARP 요청

  • 호스트 A는 브로드캐스트 메시지를 전송. 브로드캐스트 메시지란 네트워크에 속한 모든 호스트에게 보내는 메시지
  • 브로드캐스트 메시지는 ARP요청이라는 ARP 패킷

 

2. ARP 응답 (ARP Reply)

  • 호스트 B이외에 나머지 호스트들은 받은 메시지의 수신 IP가 자신의 IP 주소가 아니므로 무시
  • 호스트 B는 자신의 MAC 주소를 담은 유니캐스트 메시지를 A에게 전송 (1:1 통신을 위한 유니캐스트 메시지)
  • 유니캐스트 메시지 = ARP 응답이라는 ARP 패킷으로 이 메시지를 수신한 A는 B의 MAC 주소를 알게 됨

 

3. ARP 테이블 갱신

  • ARP 테이블: ARP 요청-응답을 통해 알게된 IP주소와 MAC 주소의 연관 관계를 기록한 테이블
  • 테이블 항목은 일정시간이 지나면 삭제되거나 임의 삭제도 가능
  • 테이블에 등록된 호스트에 대해선 ARP 요청을 보낼 필요 없음

 

ARP 패킷

  • ARP 요청과 응답 과정에서 송수신되는 패킷

  • 오퍼레이션 코드(Opcode; Operation Code) : ARP 요청의 경우 1, ARP 응답의 경우 2
  • 송신지 하드웨어 주소와 수신지 하드웨어 주소에는 MAC 주소
  • 송신지/수신지 프로토콜 주소에는 IP 주소

ARP 테이블 확인 (Mac OS 터미널)

$ arp -a

 

유의할 점

  • ARP 프로토콜은 같은 네트워크 내에 속해있는 호스트의 IP 주소를 통해 MAC 주소를 알아내는 프로토콜임
  • 그렇다면 다른 네트워크에 속해있는 IP 주소는 알지만 MAC 주소는 모르는 경우는?
  • 통신하고자 하는 호스트 A와 B가 서로 다른 네트워크에 속해있는 상황

 

1. ARP 요청/응답 과정을 통해 라우터 A의 MAC 주소를 알아낸 뒤, 이를 향해 패킷 전송

 

2. 라우터 A가 라우터 B의 MAC 주소를 모르는 경우에는 ARP 요청/응답 과정을 통해 라우터 B의 MAC 주소를 알아낸 뒤, 이를 향해 패킷 전송

 

3. 라우터 B가 호스트 B의 MAC 주소를 모르는 경우 ARP 요청/응답 과정을 통해 호스트 B의 MAC 주소를 알아낸 뒤, 이를 향해 패킷 전송

 

 

 

 

References

 

반응형
반응형

LAN을 넘어서 다른 네트워크와 통신하기 위해서는 네트워크 계층의 역할이 필수적이다

데이터 링크 계층의 한계

아래의 이유들로 물리 계층과 데이터 링크 계층만으로 다른 도시나 국가에 있는 사람과 통신하기 어려움

1. 물리 계층과 데이터 링크 계층만으로는 다른 네트워크까지의 도달 경로를 파악하기 어려움

  • 물리 계층과 데이터 링크 계층은 기본적으로 LAN을 다루는 계층으로 지구 반대편에 있는 사람의 컴퓨터와 정보를 주고 받으려면, 서로에게 도달하기까지 수많은 네트워크 장비를 거치며 다양한 경로를 통해 정보가 이동
  • 통신을 빠르게 주고받으려면 최적의 경로로 패킷이 이동해야하는데 이를 결정하는 것을 라우팅(routing)이라고 함
  • 물리계층과 데이터 링크 계층의 장비로는 라우팅을 수행할 수 없지만, 네트워크 계층의 장비로는 가능한데 대표적인 장비로 라우터(router)가 있음

2. MAC 주소만으로는 모든 네트워크에 속한 호스트의 위치를 특정하지 어려움

  • 현실적으로 모든 호스트가 모든 네트워크에 속한 모든 호스트의 MAC 주소를 서로 알고 있기 어려움
  • 네트워크를 통해 정보를 주고 받는 과정을 택배를 보내고 받는 것에 비유하면, MAC 주소는 네트워크 인터페이스(NIC)마다 할당된 일종의 개인 정보와 같음
  • 택배를 보낼 때는 인물을 특정하는 정보 이외에 수신지도 써야 함. 수신지 역할을 하는 것이 네트워크 계층의 IP 주소
  • IP주소는 논리주소라고도 부르며, NIC마다 할당되는 고정된 주소인 MAC 주소와 달리, IP 주소는 호스트에 직접 할당 가능

인터넷 프로토콜(Internet Protocol, IP)

  • 2가지 버전이 있음 IP버전 4(IPv4)와 IP버전 6(IPv6). 일반적으로 IP 혹은 IP 주소를 이야기할 때 주로 IPv4를 의미

IP 주소 형태

  • 4바이트로 주소를 표현할 수 있고 숫자당 8비트로 표현되기에 0~255 범위안에 있는 4개의 10진수로 표기
  • 각 10진수는 점으로 구분되며, 점으로 구분된 8비트를 옥텟(octet)이라고 함

ex) 192.168.1.1

IP의 2가지 기능

  1. IP 주소 지정 (IP addressing)
  • IP 주소를 바탕으로 송수신 대상을 지정하는 것을 의미
  1. IP 단편화 (IP Fragmentation)
  • 전송하고자 하는 패킷의 크기가 MTU라는 최대 전송 단위보다 클 경우, 이를 MTU 크기 이하의 복수의 패킷으로 나누는 것을 의미
  • MTU란 Maximum Transmission Unit이란 뜻으로 한 번에 전송 가능한 IP 패킷의 최대 크기를 의미하며 일반적으로 1500 바이트

IPv4

  • IPv4 패킷은 프레임의 페이로드로 데이터 필드를 명시

핵심 필드

  1. 식별자(Identifier)
    1. 패킷에 할당된 번호로 MTU를 초과하여 쪼개져서 수신지로 도착한 IPv4 패킷들이 어떤 메시지에서 쪼개졌는지 알기 위해 사용
  2. 플래그(Flag)
    1. 총 3개 비트로 구성
    2. 첫번째 비트는 항상 0으로 예약된 비트로 사용되지 않음
    3. 비트중 DF(Don’t Fragment)는 IP 단편화 수행 여부를 나타내는 표시로, 1이면 단편화 수행 X, 0이면 단편화 가능
    4. MF(More Fragment)는 단편화된 패킷이 더 있는지를 나타내는데 1이면 패킷이 더 있음을 나타내고 0이면 이 패킷이 마지막을 나타냄
  3. 단편화 오프셋(Fragment offset)
    1. 초기 데이터에서 몇 번째로 떨어진 패킷인지를 나타냄
    2. 쪼개진 패킷들은 같은 순서대로 수신지에 도착하지 않을 수 있음
    3. 수신지가 패킷들을 순서대로 재조합하려면 단편화된 패킷이 초기 데이터에서 몇번째 해당하는 패킷인지 알아야 함
  4. TTL(Time To Live)
    1. 패킷의 수명
    2. 무의미한 패킷이 네트워크 상에 지속적으로 남아있는 것을 방지하기 위해 존재
    3. 패킷이 하나의 라우터를 거칠때마다 TTL이 1씩 감소, TTL이 0으로 떨어진 패킷은 폐기
    4. 홉(hop): 패킷이 호스트 또는 라우터에 한번 전달되는 것으로 TTL 필드 값은 홉마다 1씩 감소
  5. 프로토콜
    1. 상위 계층에 프로토콜을 나타냄
  6. 송신지 IP 주소
  7. 수신지 IP 주소

IPv6

  • 이론적으로 할당 가능한 IPv4 주소 개수는 4바이트(32비트)로 표현가능한 숫자이므로 약 43억개
  • 시간이 지나면서 부족한 숫자가 되고 IPv4의 주소의 총량은 쉽게 고갈되어 이 문제를 해결하고자 IPv6가 등장
  • 16바이트로 주소를 표현할 수 있고 콜론(:) 으로 구분된 8개 그룹의 16진수로 표기
1050:0000:0000:0000:0005:0600:300c:326b

핵심필드

  1. 다음 헤더(next header)
    1. 상위 계층의 프로토콜 또는 확장 헤더를 가리키거나 확장 헤더를 가리킴
    2. 확장 헤더란 기본 헤더 이외에 추가정보가 필요한 경우 사용하는 헤더
    3. 수신지에서만 패킷을 검사하도록 하는 수신지 옵션, 송신지에서 수신지에 이르는 모든 경로의 네트워크 장비가 패킷을 검사하도록 하는 홉 간 옵션 등이 있음
  2. 홉 제한
    1. IPv4 패킷의 TTL 필드와 비슷하게 패킷의 수명을 나타내는 필드

 

References

반응형
반응형

02-4 스위치

스위치(Switch)

  • 데이터 링크 계층의 네트워크 장비로 2계층에서 사용한다 하여 L2스위치라고도 부름
  • 허브와는 달리 MAC 주소를 학습해 특정 MAC 주소를 가진 호스트에만 프레임을 전달할 수 있고 전이중 모드의 통신을 지원

 

특징

  • 특정 포트와 해당 포트에 연결된 호스트의 MAC 주소와의 관계를 기억하는데 이러한 기능을 MAC 주소 학습이라 부름
  • 관계를 기억하기 위해 메모리에 표 형태로 기억하는데 이 정보를 MAC 주소 테이블(MAC address table)이라고 부름

 

MAC 주소 학습

  • 특정 포트와 해당 포트에 연결된 호스트의 MAC 주소의 관계를 기억하는 기능으로, 포트에 연결된 호스트의 MAC 주소를 알 수 있음

MAC 주소 테이블

  • 스위치 포트와 연결된 호스트의 MAC 주소 간의 연관 관계를 나타내는 정보

 

스위치의 세 가지 기능을 통해 MAC 주소 테이블을 채우고 원하는 수신지가 연결된 포트에만 프레임을 보낼 수 있음

  1. 플러딩
  2. 포워딩과 필터링
  3. 에이징

아래 그림처럼 구성된 네트워크에서 호스트 A가 호스트로 C로 프레임을 전송하는 상황을 가정. 호스트 A, B, C, D는 각각 포트 1, 2, 3, 4번에 연결되어 있음

 

  • 처음에 스위치는 호스트의 MAC 주소와 연결된 포트의 연관관계를 모르는 상태로 MAC 주소 테이블이 비어 있음
  • 호스트 A가 1번 포트를 통해서 메세지를 전달했더라고, 2번 포트로 내보내야 하는지, 3번 포트로 내보내야 하는지, 4번 포트로 내보내야 하는지 알 수 없음

 

  • 스위치의 MAC 주소 학습은 프레임 내 ‘송신지 MAC 주소’ 필드를 바탕으로 이루어짐
  • 스위치가 호스트 A에서 프레임을 수신하면, 프레임 내 ‘송신지 MAC 주소’ 정보를 바탕으로 호스트 A의 MAC 주소와 연결된 포트를 MAC 주소 테이블에 저장
  • 하지만 여전히 수신지 호스트 C가 어떤 포트와 연결되어있는지에 대한 정보는 없음

 

 

  • 플러딩(Flooding): 허브처럼 모든 포트로 프레임 전송

  • 호스트 B, C, D는 프레임을 수신 → 메세지의 수신지 MAC 주소 정보를 보고 호스트 B와 D는 프레임 폐기

 

  • 호스트 C 응답 프레임의 송신지 MAC 주소 필드로 호스트 C의 MAC 주소를 학습, MAC 주소 테이블에 기록

 

  • 호스트 A와 C가 프레임을 주고 받을 때는 다른 포트로 프레임을 내보낼 필요가 없음
  • 스위치는 호스트 B, D가 연결된 포트로는 내보내지 않도록 필터링(Filtering)
  • 호스트 C가 연결된 포트로 프레임을 포워딩(Forwarding)

에이징(aging)

  • MAC 주소 테이블에 등록된 포트에서 일정 시간 동안 프레임을 받지 못하면 해당 항목은 삭제하는데 이를 에이징이라고 함

참고) 브리지 (Bridge)

  • 스위치와 유사한 장비로 네트워크 영역을 구획하여 콜리전 도메인을 나누거나 네트워크를 확장하기 위해 사용
  • 최근에는 스위치에 비해 사용빈도가 줄어드는 추세고 스위치가 브리지의 기능들을 포괄하며 프레임의 처리 성능 면에서도 우수

 

스위치의 VLAN 기능

  • Virtual LAN의 줄임말로, 한대의 스위치로 가상의 LAN을 만드는 방법
  • 불필요한 트래픽(허브, 스위치의 플러딩)으로 인한 성능 저하 방지

  • 스위치에 연결된 호스트들 중에서도 서로 메시지를 주고받을 일이 적거나 브로드캐스트 케시지가 필요없는 경우, 굳이 같은 LAN에 속할 필요가 없음
  • 이를 분리하고자 매번 새로운 스위치 장비를 구비하는 것은 낭비인데 이를 방지 하기위해 VLAN 기능을 활용할 수 있음
  • 한대의 물리적 스위치를 여러 대의 스위치가 있는 것처럼 논리적인 단위로 LAN을 구획

  • VLAN은 사실상 다른 LAN으로 브로드캐스트 도메인이 달라짐
  • 위 예에서 개발부와 총무부가 통신하기 위해서는 네트워크 같의 통신을 위한 장치가 필요

 

VLAN의 종류

Port 기반 VLAN

  • 스위치의 포트가 VLAN을 결정하는 방식
  • 특정 포트에 VLAN을 할당한 뒤, 해당 포트에 호스트를 연결하여 VLAN에 참여
  • 위 그림에서 호스트 A, B는 VLAN2를 할당한 포트에 연결되어 있어 같은 LAN에 속한 상태
  • 호스트 C는 VLAN3에 속해있으므로 호스트 A, B와 다른 LAN에 속한 상태

 

MAC 기반 VLAN

  • 사전에 설정된 MAC 주소에 따라 VLAN이 결정
  • 송수신하는 프레임 속 MAC 주소가 호스트가 속할 VLAN을 결정하는 방식
  • 호스트 A는 어떤 포트와 연결되더라도 VLAN3에 할당됨

 

References

반응형
반응형

계약에 의한 디자인

관계자가 기대하는 바를 암묵적으로 코드에 삽입 X

양측이 동의하는 계약을 먼저 한 다음, 계약을 어겼을 경우는 명시적으로 왜 계속할 수 없는지 예외를 발생시키라는 것

책에서 말하는 계약은 소프트웨어 컴포넌트 간의 통신 중에 반드시 지켜져야 할 몇 가지 규칙을 강제하는 것

  • 사전조건: 코드가 실행되기 전 체크해야하는 것들(ex) 파라미터에 제공된 데이터의 유효성 검사)
  • 사후조건: 함수 반환값의 유효성 검사로 호출자가 이 컴포넌트에서 기대한 것을 제대로 받았는지 확인하기 위해 수행
  • 불변식: 함수가 실행되는 동안 일정하게 유지되는 것으로 로직에 문제가 없는지 확인하기 위한 것(docstring 문서화하는 것이 좋다)
  • 부작용: 선택적으로 코드의 부작용을 docstring에 언급하기도 한다

사전조건(precondition)

  • 함수나 메소드가 제대로 동작하기 위해 보장해야 하는 모든 것들
  • 함수는 처리할 정보에 대한 적절한 유효성 검사를 해야 하는데 어디서 할지에 대해 2가지로 나뉨
    • 관대한(tolerant) 접근법: 클라이언트가 함수를 호출하기 전에 모든 유효성 검사를 진행
    • 까다로운(demanding) 접근법: 함수가 자체적으로 로직을 실행하기 전에 검사를 진행

⇒ 어디에서 유효성 검사를 진행하든 어느 한쪽에서만 진행해야 함

사후조건(postcondition)

  • 함수나 메소드가 반환된 후의 상태를 강제하는 것

파이썬스러운 계약

  • 메소드, 함수, 클래스에 제어 메커니즘을 추구하고 검사에 실패할 경우 RuntimeError나 ValueError를 발생시키는 것
  • 사전조건, 사후조건 검사, 핵심 기능 구현은 가능한 한 격리된 상태로 유지하는 것이 좋음

계약에 의한 디자인(DbC) - 결론

  • 문제가 있는 부분을 효과적으로 식별하는데 가치가 있음
  • 명시적으로 함수나 메소드가 정상적으로 동작하기 위해 필요한 것이 무엇인지, 무엇을 반환하는지를 정의해 프로그램의 구조를 명확히 할 수 있음
  • 원칙에 따라 추가적인 작업이 발생하지만 이방법으로 얻은 품질은 장기적으로 보상됨

방어적(defensive) 프로그래밍

  • 계약에 의한 디자인과는 다른 접근 방식
  • 계약에서 예외를 발생시키고 실패하게 되는 모든 조건을 기술하는 대신 코드의 모든 부분을 유효하지 않은 것으로부터 스스로 보호할 수 있게 하는 것
    • 예상할 수 있는 시나리오의 오류를 처리 - 에러 핸들링 프로시져
    • 발생하지 않아야 하는 오류를 처리하는 방법 - assertion error

에러 핸들링

  • 일반적으로 데이터 입력확인 시 자주 사용
  • 목적은 예상되는 에러에 대해서 실행을 계속할지/ 프로그램을 중단할지 결정하는 것

에러처리방법

  • 값 대체(value substitution)
  • 에러 로깅
  • 예외 처리

값 대체

  • 일부 시나리오에서 오류가 있어 소프트웨어가 잘못된 값을 생성하거나 전체가 종료될 위험이 있을 경우 결과 값을 안전한 다른 값으로 대체하는 것
  • 항상 가능하지는 않고 신중하게 선택해야 함 (견고성과 정확성 간의 trade-off)
  • 정보가 제공되지 않을 경우 기본 값을 제공할 수도 있음
import os

configuration = {"dbport": 5432}
print(configuration.get("dbhost", "localhost"))  # localhost
print(configuration.get("dbport"))  # 5432

print(os.getenv("DBHOST"))  # None

print(os.getenv("DPORT", 5432))  # 5432
  • 두번째 파라미터 값을 제공하지 않으면 None을 반환

사용자 정의함수에서도 파라미터의 기본 값을 직접 정의할 수 있음

def connect_database(host="localhost", port=5432):
    pass
  • 일반적으로 누락된 파라미터를 기본 값으로 바꾸어도 큰 문제가 없지만 오류가 있는 데이터를 유사한 값으로 대체하는 것을 더 위험하여 일부 오류를 숨겨버릴 수 있음

예외처리

어떤 경우에는 잘못된 데이터를 사용하여 계속 실행하는 것보다는 차라리 실행을 멈추는 것이 더 좋을 수 있음

  • 입력이 잘못되었을 때만 함수에 문제가 생기는 것이 아님 (외부 컴포넌트에 연결되어 있는 경우)
  • 이런 경우에는 함수 자체의 문제가 아니기 때문에 적절하게 인터페이스를 설계하면 쉽게 디버깅 할 수 있음

⇒ 예외적인 상황을 명확하게 알려주고 원래의 비즈니스 로직에 따라 흐름을 유지하는 것이 중요

정상적인 시나리오나 비즈니스 로직을 예외처리하려고 하면 프로그램의 흐름을 읽기가 어려워짐

→ 예외를 go-to문처럼 사용하는 것과 같다. 올바른 위치에서 추상화를 하지 못하게 되고 로직을 캡슐화하지도 못하게 됨.

마지막으로 예외를 대게 호출자에게 잘못을 알려주는 것으로 캡슐화를 약화시키기 때문에 신중하게 사용해야 함→이는 함수가 너무 많은 책임을 가지고 있다는 것을 의미할 수도 있음. 함수에서 너무 많은 예외를 발생시켜야 한다면 여러개의 작은 기능으로 나눌 수 있는지 검토해야 함

올바른 수준의 추상화 단계에서 예외 처리

  • 예외는 오직 한가지 일을 하는 함수의 한 부분이어야 함
  • 서로 다른 수준의 추상화를 혼합하는 예제. deliver_event 메소드를 중점적으로 살펴보면
import logging
import time

logger = logging.getLogger(__name__)

class DataTransport:
    """다른 레벨에서 예외를 처리하는 객체의 예"""

    _RETRY_BACKOFF: int = 5
    _RETRY_TIMES: int = 3

    def __init__(self, connector):
        self._connector = connector
        self.connection = None

    def deliver_event(self, event):
        try:
            self.connect()
            data = event.decode()
            self.send(data)
        except ConnectionError as e:
            logger.info("커넥션 오류 발견: %s", e)
            raise
        except ValueError as e:
            logger.error("%r 이벤트에 잘못된 데이터 포함: %s", event, e)
            raise

    def connect(self):
        for _ in range(self._RETRY_TIMES):
            try:
                self.connection = self._connector.connect()
            except ConnectionError as e:
                logger.info("%s: 새로운 커넥션 시도 %is", e, self._RETRY_BACKOFF)
                time.sleep(self._RETRY_BACKOFF)
            else:
                return self.connection
        raise ConnectionError(f"연결실패 재시도 횟수 {self._RETRY_TIMES} times")

    def send(self, data):
        return self.connection.send(data)
    def deliver_event(self, event):
        try:
            self.connect()
            data = event.decode()
            self.send(data)
        except ConnectionError as e:
            logger.info("커넥션 오류 발견: %s", e)
            raise
        except ValueError as e:
            logger.error("%r 이벤트에 잘못된 데이터 포함: %s", event, e)
            raise
  • ConnectionError와 ValueError는 별로 관계가 없음
  • 매우 다른 유형의 오류를 살펴봄으로써 책임을 어떻게 분산해야 하는지에 대한 아이디어를 얻을 수 있음
    • ConnectionError는 connect 메소드 내에서 처리되어야 함. 이렇게 하면 행동을 명확하게 분리할 수 있다. 메소드가 재시도를 지원하는 경우 메소드 내에서 예외처리를 할 수 있음
    • ValueError는 event의 decode 메소드에 속한 에러로 event를 send 메소드에 파라미터로 전달 후 send 메소드 내에서 예외처리를 할 수 있음
  • 위 내용처럼 구현을 수정하면 deliver_event 메소드에서 예외를 catch할 필요가 없음
def connect_with_retry(connector, retry_n_times: int, retry_backoff: int = 5):
    """<connector>를 사용해 연결을 시도함.
    연결에 실패할 경우 <retry_n_times>회 만큼 재시도
    재시도 사이에는 <retry_backoff>초 만큼 대기

    연결에 성공하면 connection 객체를 반환
    재시도 횟수를 초과하여 연결에 실패하면 ConnectionError 오류 발생

    :param connector: connect() 메소드를 가진 객체
    :param retry_n_times: 연결 재시도 횟수
    :param retry_backoff: 재시도 사이의 대기 시간(초)

    """
    for _ in range(retry_n_times):
        try:
            return connector.connect()
        except ConnectionError as e:
            logger.info("%s: 새로운 커넥션 시도 %is", e, retry_backoff)
            time.sleep(retry_backoff)

    exc = ConnectionError(f"연결 실패 ({retry_n_times}회 재시도)")
    logger.exception(exc)
    raise exc
class DataTransport:
    """추상화 수준에 따른 예외 분리를 한 객체"""

    _RETRY_BACKOFF: int = 5
    _RETRY_TIMES: int = 3

    def __init__(self, connector: Connector) -> None:
        self._connector = connector
        self.connection = None

    def deliver_event(self, event: Event):
        self.connection = connect_with_retry(
            self._connector, self._RETRY_TIMES, self._RETRY_BACKOFF
        )
        self.send(event)

    def send(self, event: Event):
        try:
            return self.connection.send(event.decode())
        except ValueError as e:
            logger.error("%r contains incorrect data: %s", event, e)
            raise
  • deliver_event 메소드 내에서 예외 catch 하는 부분 없어짐

엔드 유저에게 Traceback 노출 금지

  • 보안을 위한 고려사항으로 예외가 전파되도록하는 경우는 중요한 정보를 공개하지 않고 “알 수 없는 문제가 발생했습니다” 또는 “페이지를 찾을 수 없습니다”와 같은 일반적인 메세지를 사용해야 함

비어있는 except 블록 지양

  • 파이썬의 안티패턴 중 가장 악마같은 패턴(REAL 01)으로 어떠한 예외도 발견할 수 업슨 문제점이 있음
try:
    process_data()
except: 
    pass
  • 아무것도 하지 않는 예외 블록을 자동으로 탐지할 수 있도록 CI 환경을 구축하면 좋음
더보기

flake8
pylint

https://pylint.pycqa.org/en/latest/user_guide/messages/warning/bare-except.html

name: Lint Code

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v2

    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.x'

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt

    - name: Run flake8
      run: |
        flake8 . --select=E722
      
    - name: Run pylint
      run: |
        find . -name "*.py" | xargs pylint --disable=all --enable=W0702

대안으로 아래 두 항목 동시에 적용하는 것이 좋다

  1. 보다 구체적인 예외처리 (AttributeError 또는 KeyError)
  2. except 블록에서 실제 오류 처리
  • pass를 사용하는 것은 그것이 의미하는 바를 알 수 없기 때문에 나쁜 코드이다
  • 명시적으로 해당 오류를 무시하려면 contextlib.suppress 함수를 사용하는 것이 올바른 방법
import contextlib

with contextlib.suppress(KeyError):
    process_data()

원본 예외 포함

  • raise <e> from <original_exception> 구문을 사용하면 여러 예외를 연결할 수 있음
  • 원본 오류의 traceback 정보가 새로운 exception에 포함되고 원본 오류는 새로운 오류의 원인으로 분류되어 cause 속성에 할당 됨
class InternalDataError(Exception):
    """업무 도메인 데이터의 예외"""

def process(data_dictionary, record_id):
    try:
        return data_dictionary[record_id]
    except KeyError as e:
        raise InternalDataError("데이터가 존재하지 않음") from e

test_dict = {"a": 1}

process(test_dict, "b")

Traceback (most recent call last):
File "/Users/woo-seongchoi/Desktop/CleanCode/ch3/main.py", line 7, in process
return data_dictionary[record_id]
~~~~~~~~~~~~~~~^^^^^^^^^^^
KeyError: 'b'*

The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/Users/woo-seongchoi/Desktop/CleanCode/ch3/main.py", line 14, in <module>
process(test_dict, "b")
File "/Users/woo-seongchoi/Desktop/CleanCode/ch3/main.py", line 9, in process
raise InternalDataError("데이터가 존재하지 않음") from e
InternalDataError: 데이터가 존재하지 않음*

 

파이썬에서 assertion 사용하기

  • 절대로 일어나지 않아야 하는 상황에 사용되므로 assert 문에 사용된 표현식을 불가능한 조건을 의미로 프로그램을 중단시키는 것이 좋다
try: 
    assert condition.holds(), "조건에 맞지 않음"
except AssertionError:
    alternative_procedure() # catch 후에도 계속 프로그램을 실행하면 안됨

위 코드가 나쁜 또 다른 이유는 AssertionError를 처리하는 것 이외에 assertion 문장이 함수라는 것

assert condition.holds(), "조건에 맞지 않음"
  • 함수 호출은 부작용을 가질 수 있으며 항상 반복가능하지 않음. 또한 디버거를 사용해 해당 라인에서 중지하여 오류 결과를 편리하게 볼 수 없으며 다시 함수를 호출한다 하더라도 잘못된 값이었는지 알 수 없음
result = condition.holds()
assert result > 0, f"Error with {result}"

예외처리와 assertion의 차이

  • 예외처리는 예상하지 못한 상황을 처리하기 위한 것 ⇒ 더 일반적
  • assertion은 정확성을 보장하기 위해 스스로 체크하는 것
반응형
반응형

CPython에서는 다양한 방식으로 파이썬 코드를 실행할 수 있음

  1. python -c로 파이썬 문자열을 통해 코드 실행
$ ./python.exe -c "print(3 + 5)"
> 8
  1. python -m으로 모듈 실행하기
  2. python <file>로 파이썬 코드가 들어 있는 파일(경로 명시) 실행하기
  3. cat <file> | python처럼 파이썬 코드를 stdin으로 python에 파이프하기
  4. REPL에서 한번에 하나씩 명령 실행
  5. C API를 이용해 파이썬을 임베디드 환경으로 사용하기

 

인터프리터가 파이썬 코드를 실행하려면 세가지 요소가 필요

  1. 실행할 모듈(modules)
  2. 변수 등을 저장할 상태(state)
  3. 활성화된 옵션 등의 구성(configuration)

출처: CPython 파헤치기 5장

 

5.1 구성 상태

  • 파이썬 코드를 실행하기 전 CPython 런타임은 먼저 사용자 옵션과 구성을 설정
  • CPython 구성은 세 부분으로 나뉨 (PEP587)
  1. PyPreConfig 딕셔너리 초기화 구성
  2. PyConfig 런타임 구성
  3. CPython 인터프리터에 같이 컴파일된 구성

PyPreConfig와 PyConfig 구조체는 Include>cpython>initconfig.h에서 정의

더보기
/* --- PyPreConfig ----------------------------------------------- */

typedef struct {
    int _config_init;     /* _PyConfigInitEnum value */

    /* Parse Py_PreInitializeFromBytesArgs() arguments?
       See PyConfig.parse_argv */
    int parse_argv;

    /* If greater than 0, enable isolated mode: sys.path contains
       neither the script's directory nor the user's site-packages directory.

       Set to 1 by the -I command line option. If set to -1 (default), inherit
       Py_IsolatedFlag value. */
    int isolated;

    /* If greater than 0: use environment variables.
       Set to 0 by -E command line option. If set to -1 (default), it is
       set to !Py_IgnoreEnvironmentFlag. */
    int use_environment;

    /* Set the LC_CTYPE locale to the user preferred locale? If equals to 0,
       set coerce_c_locale and coerce_c_locale_warn to 0. */
    int configure_locale;

    /* Coerce the LC_CTYPE locale if it's equal to "C"? (PEP 538)

       Set to 0 by PYTHONCOERCECLOCALE=0. Set to 1 by PYTHONCOERCECLOCALE=1.
       Set to 2 if the user preferred LC_CTYPE locale is "C".

       If it is equal to 1, LC_CTYPE locale is read to decide if it should be
       coerced or not (ex: PYTHONCOERCECLOCALE=1). Internally, it is set to 2
       if the LC_CTYPE locale must be coerced.

       Disable by default (set to 0). Set it to -1 to let Python decide if it
       should be enabled or not. */
    int coerce_c_locale;

    /* Emit a warning if the LC_CTYPE locale is coerced?

       Set to 1 by PYTHONCOERCECLOCALE=warn.

       Disable by default (set to 0). Set it to -1 to let Python decide if it
       should be enabled or not. */
    int coerce_c_locale_warn;

#ifdef MS_WINDOWS
    /* If greater than 1, use the "mbcs" encoding instead of the UTF-8
       encoding for the filesystem encoding.

       Set to 1 if the PYTHONLEGACYWINDOWSFSENCODING environment variable is
       set to a non-empty string. If set to -1 (default), inherit
       Py_LegacyWindowsFSEncodingFlag value.

       See PEP 529 for more details. */
    int legacy_windows_fs_encoding;
#endif

    /* Enable UTF-8 mode? (PEP 540)

       Disabled by default (equals to 0).

       Set to 1 by "-X utf8" and "-X utf8=1" command line options.
       Set to 1 by PYTHONUTF8=1 environment variable.

       Set to 0 by "-X utf8=0" and PYTHONUTF8=0.

       If equals to -1, it is set to 1 if the LC_CTYPE locale is "C" or
       "POSIX", otherwise it is set to 0. Inherit Py_UTF8Mode value value. */
    int utf8_mode;

    /* If non-zero, enable the Python Development Mode.

       Set to 1 by the -X dev command line option. Set by the PYTHONDEVMODE
       environment variable. */
    int dev_mode;

    /* Memory allocator: PYTHONMALLOC env var.
       See PyMemAllocatorName for valid values. */
    int allocator;
} PyPreConfig;

 

5.1.1 딕셔너리 초기화 구성

  • 사용자 환경 또는 운영 체제와 관련된 구성이기 때문에 런타임 구성과 구분됨

PyPreConfig 의 세가지 주요 기능

  1. 파이썬 메모리 할당자 설정
  2. LC_CTYPE 로캘(locale)을 시스템 또는 사용자 선호 로캘로 구성하기
  3. UTF-8 모드 설정하기(PEP540)

아래와 같은 int 타입 필드들을 포함

  1. allocator: PYMEM_ALLOCATOR_MALLOC 같은 값을 사용해 메모리 할당자를 선택
  2. configure_locale: LC_CTYPE 로캘을 사용자 선호 로캘로 설정. 0으로 설정하면 coerce_c_locale과 coerce_c_locale_warn을 0으로 설정
  3. coerce_c_locale: 2로 설정하면 C 로캘을 강제로 적용. 1로 설정하면 LC_CTYPE을 읽은 후 강제로 적용할지 결정
  4. coerce_c_locale_warn: 0이 아니면 C로캘이 강제로 적용될 때 경고가 발생
  5. dev_mode: 개발 모드를 활성화
  6. isolated: 격리 모드를 활성화. sys.path에 스크립트 디렉토리와 사용자의 사이트 패키지 디렉토리가 포함되지 않음
  7. legacy_windows_fs_encoding: 0이 아니면 UTF-8 모드를 비활성화하고 파이썬 파일 시스템 인코딩을 mbcs로 설정(윈도우 전용)
  8. parse_argv: 0이 아니면 명령줄 인자를 사용
  9. use_environment: 0보다 큰 값이면 환경 변수를 사용
  10. utf8_mode_: 0이 아니면 UTF-8모드를 활성화

 

5.1.2 연관된 소스 파일 목록

PyPreConfig와 연관된 소스 파일 목록

  1. Python/initconfig.c : 시스템 환경에서 불러온 구성을 명령줄 플래그와 결합
  2. Include/cpython/initconfig.h : 초기화 구성 구조체를 정의

5.1.3 런타임 구성 구조체

PyConfig 런타임 구성 구조체는 아래 값들을 포함

  • ‘디버그’나 ‘최적화’같은 실행 모드 플래그
  • 스크립트 파일이나 stdin, 모듈 등 실행 모드
  • -X <option>으로 설정 가능한 확장 옵션
  • 런타임 설정을 위한 환경 변수

런타임 구성 데이터는 CPython 런타임 기능의 활성화 여부를 결정

 

5.1.4 명령줄로 런타임 구성 설정하기

  • 파이썬은 다양한 명령줄 인터페이스 옵션을 제공하는데 예로 상세 모드(verbose) 가 있고 주로 개발자 대상으로 활용됨
  • -v 플래그로 상세모드를 활성화하면 파이썬은 모듈을 로딩할 때마다 화면에 메시지를 출력
$ ./python.exe -v -c "print('hello world')"
...
# installing zipimport hook
import 'time' # <class '_frozen_importlib.BuiltinImporter'>
import 'zipimport' # <class '_frozen_importlib.FrozenImporter'>
# installed zipimport hook
  • 사용자 site-packages 디렉토리에 설치된 모듈들과 시스템 호나경의 모든 항목을 임포트하면 수백줄이 출력됨
  • 아래는 상세 모드 설정에 대한 우선순위
  1. config→verbose의 기본값은 -1로 소스 코드에 하드코딩되어 있음
  2. PYTHONVERBOSE 환경 변수를 config→verbose를 설정하는데 사용
  3. 환경 변수가 없으면 기본값인 -1을 사용
  4. Python/initconfig.c의 config_parse_cmdline()은 명시된 명령중 플래그를 사용해 모드를 설정
  5. _Py_GetGlobalVariablesAsDict() 가 값을 전역 변수 Py_VerboseFlag로 복사

 

모든 Pyconfig값에는 같은 순서와 우선순위가 적용됨

출처: CPython 파헤치기 5장

 

5.1.5 런타임 플래그 확인하기

  • Cpython 인터프리터의 런타임 플래그는 CPython의 동작들을 끄고 켜는데 사용하는 고급 기능
  • X 옵션을 이용해 다양한 기능을 활성화할 수 있음
  • 파이썬 세션 중에 sys.flags 네임드 튜플로 상세 모드나 메시지 없는(quite)모드 같은 런타임 플래그에 접근할 수 있음
  • sys._xoptions 딕셔너리에 모든 -X 플래그를 확인할 수 있음

 

-X dev -X utf8 플래그 사용: 기본 인코딩으로 utf-8로 설정

./python.exe -X dev -X utf8

Python 3.9.19+ (heads/3.9:a04a0f6585, Mar 25 2024, 08:21:31)
[Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys; sys.flags
sys.flags(debug=0, inspect=0, interactive=0, optimize=0, dont_write_bytecode=0, no_user_site=0, no_site=0, ignore_environment=0, verbose=0, bytes_warning=0, quiet=0, hash_randomization=1, isolated=0, dev_mode=True, utf8_mode=1, int_max_str_digits=-1)

 

5.2 빌드 구성

  • 런타임 구성을 Include/cpython/initconfig.h에서 정의하듯이 빌드 구성은 최상의 폴더의 pyconfig.h에서 정의
  • 이 파일은 macOS나 리눅스용 빌드 과정중 ./configure 단계에서 자동으로 생성됨
  • 다음 명령으로 빌드 구성을 확인할 수 있음
$ ./python.exe -m sysconfig
Platform: "macosx-14.5-arm64"
Python version: "3.9"
Current installation scheme: "posix_prefix"

Paths: 
  data = "/usr/local"
  include = "/Users/woo-seongchoi/Desktop/SE/cpython-internals/cpython/Include"
  platinclude = "/Users/woo-seongchoi/Desktop/SE/cpython-internals/cpython"
...
  • 빌드 구성항목들은 컴파일 시에 결졍되는 값으로 바이너리에 링크할 추가 모듈 선택애 사용됨
  • 예를 들어 디버거나 계측(instrumentation) 라이브러리, 메모리 할당자는 모두 컴파일 시 결정됨
  • 세 단계의 구성(Build configuration, PyPreconfig, PyConfig)을 모두 완료하면 CPython 인터프리터는 입력된 텍스트를 코드로 실행할 수 있음

5.3 입력에서 모듈 만들기

코드를 실행하려면 먼저 입력을 모듈로 컴파일해야 함. 입력 방식은 아래와 같이 여러가지가 있음

  • 로컬 파일과 패키지
  • 메모리 파이프나 stdin 같은 I/O 스트림
  • 문자열

읽어 들인 입력은 파서를 거쳐 컴파일러에 전달됨

유연한 입력 방식을 제공하기 위해 CPython은 소스 코드의 상당 부분을 파서의 입력 처리에 사용됨

 

5.3.1 연관된 소스 파일 목록

  • Lib > runpy.py : 파이썬 모듈을 임포트 하고 실행하는 표준 라이브러리 모듈
  • Modules > main.c : 파일이나 모듈, 입력 스트림 같은 외부 코드 실행을 감싸는 함수
  • Programs > python.c : 윈도우나, 리눅스, macOS에서 Python의 진입점. 위의 main.c 를 감싸는 역할만 맡음
더보기
/* Minimal main program -- everything is loaded from the library */

#include "Python.h"

#ifdef MS_WINDOWS
int
wmain(int argc, wchar_t **argv)
{
    return Py_Main(argc, argv);
}
#else
int
main(int argc, char **argv)
{
    return Py_BytesMain(argc, argv);
}
#endif
  • Python > pythonrun.c : 명령줄 입력을 처리하는 내부 C API를 감싸는 함수

5.3.2 입력과 파일 읽기

  • CPython은 런타임 구성과 명령줄 인자가 준비디ㅗ면 실행할 코드를 불러옴
  • 이 작업은 modules/main.c의 pymain_main()이 실행

main.c에서 pymain_main()

static int
pymain_main(_PyArgv *args)
{
    PyStatus status = pymain_init(args);
    if (_PyStatus_IS_EXIT(status)) {
        pymain_free();
        return status.exitcode;
    }
    if (_PyStatus_EXCEPTION(status)) {
        pymain_exit_error(status);
    }

    return Py_RunMain();
}
  • 다음으로 CPython은 불러온 코드를 새로 생성된 PyConfig 인스턴스에 설정된 옵션들과 함께 실행함

 

5.3.3 명령줄 문자열 입력

  • -c 옵션을 사용해 명령줄에서 작은 파이썬 애플리케이션을 실행할 수 있음 (ex) print(2 ** 2))
❯ ./python.exe -c "print(2 ** 2)"        
4

 

  • Modules/main.c에서 pymain_run_command()가 실행되며 -c로 전달된 명령은 C의 wchar_t* 타입 인자로 함수에 전달됨
    • wchar_t* 타입은 UTF-8 문자를 저장할 수 있기 때문에 CPython에서 저수준 유니코드 데이터를 저장하는 타입으로 사용됨
    • Objects/unicodeobject.c의 헬퍼 함수 PyUnicode_FromWideChar()를 이용해 wchar_t*를 파이썬 유니코드 문자열로 변환할 수 있음. 유니코드 문자열→ UTF-8로 인코딩하려면 PyUnicode_AsUTF8String() 을 사용

pymain_run_command() 함수 코드

static int
pymain_run_command(wchar_t *command, PyCompilerFlags *cf)
{
    PyObject *unicode, *bytes;
    int ret;

    unicode = PyUnicode_FromWideChar(command, -1);
    if (unicode == NULL) {
        goto error;
    }

    if (PySys_Audit("cpython.run_command", "O", unicode) < 0) {
        return pymain_exit_err_print();
    }

    bytes = PyUnicode_AsUTF8String(unicode);
    Py_DECREF(unicode);
    if (bytes == NULL) {
        goto error;
    }

    ret = PyRun_SimpleStringFlags(PyBytes_AsString(bytes), cf);
    Py_DECREF(bytes);
    return (ret != 0);

error:
    PySys_WriteStderr("Unable to decode the command from the command line:\n");
    return pymain_exit_err_print();
}

 

pymain_run_command()는 파이썬 바이트열 객체를 PyRun_SimpleStringFlags()로 넘겨서 실행

static int
pymain_run_command(wchar_t *command, PyCompilerFlags *cf)
{
	PyObject *unicode, *bytes;
  int ret;

	unicode = PyUnicode_FromWideChar(command, -1);
	bytes = PyUnicode_AsUTF8String(unicode);
	ret = PyRun_SimpleStringFlags(PyBytes_AsString(bytes), cf);
}

Python/pythonrun.c의 PyRun_SimpleStringFlags()는 문자열을 파이썬 모듈로 변환하고 실행

int
PyRun_SimpleStringFlags(const char *command, PyCompilerFlags *flags)
{
    PyObject *m, *d, *v;
    m = PyImport_AddModule("__main__");   // (1)
    if (m == NULL)
        return -1;
    d = PyModule_GetDict(m);
    v = PyRun_StringFlags(command, Py_file_input, d, d, flags);  // (2)
    if (v == NULL) {
        PyErr_Print();
        return -1;
    }
    Py_DECREF(v);
    return 0;
}

(1) 파이썬 모듈을 독립된 모듈로 실행하려면 __main__ 진입점이 필요하기 때문에 PyRun_simpleStringFlags()가 진입점을 자동으로 추가

(2) PyRun_simpleStringFlags() 는 딕셔너리와 모듈을 만든 후 PyRun_StringFlags() 를 호출해, 가짜 파일 이름을 만들고 파이썬 파서를 실행해 문자열에서 추상 구문 트리(abstract syntax tree, AST)를 생성해 모듈로 반환

5.3.4 로컬 모듈 입력

  • 파이썬의 -m 옵션과 모듈 이름으로 파이썬 명령을 실행할 수도 있음

예시

$ ./python.exe -m unittest
  • 위 명령으로 표준 라이브러리의 unittest 모듈을 실행할 수 있음
  • -m 옵션은 모듈 패키지의 진입점(__main__)을 실행함. 이 때 해당 모듈은 sys.path에서 검색
  • 임포트 라이브러리(importlib)의 검색 메커니즘 덕분에 unittest 모듈의 파일 시스템 위치를 기억할 필요 없음
  • CPython은 표준 라이브러리 모듈 runpy를 임포트하고 PyObject_Call()로 해당 모듈을 실행. runpy 모듈은 Lib/runpy.py에 위치한 순수한 파이썬 모듈임.
  • 임포트는 Python/import.c의 C API 함수 PyImport_ImportModule()이 담당
  • python -m <module> 을 실행하는 것은 python -m runpy <module> 을 실행하는 것과 같음. runpy 모듈은 운영체제에서 모듈을 찾아 실행하는 프로세스를 추상화

runpy는 세단계의 모듈을 실행 …?

1. 제공된 모듈 이름을 __import__()로 임포트
2. __name__(모듈 이름)을 __main__ 이름 공간에서 설정
3. __main__ 이름 공간에서 모듈을 실행

5.3.5 표준 입력 또는 스크립트 파일 입력

  • python test.py처럼 python을 실행할 때 첫 번째 인자가 파일명이라면 Cpython은 파일 핸들을 열어 Python/pythonrun.c의 PyRun_SimpleFileExFlags()로 핸들을 넘김

이 함수는 세 종류의 파일 경로를 처리할 수 있음

  1. .pyc 파일 경로면 run_pyc_file()을 호출
  2. 스크립트 파일(.py) 경로면 PyRun_FileExFlags()를 호출
  3. <command> | python 처럼 파일 경로가 stdin이면 stdin을 파일 핸들로 취급하고 PyRun_FileExFlags()를 호출
  • stdin이나 스크립트 파일의 경우 CPython은 파일 핸들을 Python/pythonrun.c의 PyRunFileExFlags()로 넘기는데 이는 PyRun_SimpleStringFlags()와 비슷. CPython은 파일 핸들을 PyParser_ASTFromFileObject()로 전달
  • PyRun_SimpleStringFlags() 처럼 PyRunFileExFlags()는 파일에서 파이썬 모듈을 생성하고 run_mode()로 보내 실행

5.3.6 컴파일된 바이트 코드 입력

  • python을 .pyc 파일 경로와 함께 실행하면 CPython은 파일을 텍스트 파일로 불러와 파싱하는 대신 .pyc 파일에서 디스크에 기록된 코드 객체를 찾음
  • PyRun_SimpleFileExFlags()에는 .pyc파일 경로를 처리하는 부분이 있음
  • Python/pythonrun.c의 run_pyc_file()은 파일 핸들을 사용해 .pyc 파일에서 코드 객체를 마셜링함
    • 마셜링은 파일 내용을 메모리를 복사하여 특정 데이터 구조로 변환하는 것을 의미
  • CPython 컴파일러는 스크립트가 호출될 때 마다 파싱하는 대신 디스크의 코드 객체 구조체에 컴파일한 코드를 캐싱
  • 메모리에 마셜링된 코드 객체는 Python/ceval.c 를 호출하는 run_eval_code_obj()로 전달되어 실행됨
반응형
반응형

컴파일러

  • 목적: 통역사처럼 한 언어를 다른 언어로 변환하는 것
  • 통역을 하려면 출발어(source language)와 도착어(target language)의 문법 구조를 알아야 함

컴파일러의 선택기준: 이식성

  • 저수준 기계어: C/C++, Go, 파스칼은 바이너리 실행파일로 컴파일하는데 이는 컴파일한 플랫폼과 동일한 플랫폼에서만 사용할 수 있음
  • 중간 언어: java, 닷넷 CLR은 여러 시스템 아키텍처에서 사용할 수 있는 중간언어로 컴파일 해서 가상머신에서 실행될 수 있음
  • 파이썬 애플리케이션은 보통 소스 코드 형태로 배포됨
  • 파이썬 인터프리터는 소스 코드를 변환한 후 한 줄씩 실행
  • CPython 런타임이 첫 번째 실행될 때 코드를 컴파일하지만 이 단계는 일반 사용자에게 노출되지 않음
  • 파이썬 코드는 기계어 대신 바이트코드라는 저수준 중간 언어로 컴파일되고 바이트코드는 .pyc 파일에 저장됨(캐싱)
  • 코드를 변경하지 않고 같은 파이썬 애플리케이션을 다시 실행하면 매번 다시 컴파일하지 않고 컴파일된 바이트 코드를 불러오기 때문에 더 빠르게 실행

4.1 CPython이 파이썬이 아니라 C로 작성된 이유

  • 컴파일러가 작동하는 방식 때문

컴파일러가 작동하는 방식 유형

  1. 셀프 호스팅 컴파일러: 자기 자신으로 작성한 컴파일러로 부트스트래핑이라는 단계를 통해서 만들어짐
    1. Go: C로 작성된 첫 번째 Go 컴파일러가 Go를 컴파일할 수 있게 되자 컴파일러를 Go 언어로 재 작성
    2. PyPy: 파이썬으로 작성된 파이썬 컴파일러
  2. source to source 컴파일러: 컴파일러를 이미 가지고 있는 다른 언어로 작성한 컴파일러
    1. Cpython : C를 사용하여 Python 컴파일
    • ssl 이나 sockets 같은 표준 라이브러리 모듈이 저수준 운영체제 API에 접근하기 위해 C로 작성됨

 

4.2 파이썬 언어 사양

  • 컴파일러가 언어를 실행하려면 문법 구조에 대한 엄격한 규칙이 필요
  • CPython 소스 코드에 포함된 언어 사양은 모든 파이썬 인터프리터 구현이 사용하는 레퍼런스 사양
  • 사람이 읽을 수 있는 형식 + 기계가 읽을 수 있는 형식으로 제공
  • 문법 형식과 각 문법 요소가 실행되는 방식을 자세히 설명

4.2.1 파이썬 언어 레퍼런스

  • 파이썬 언어의 기능을 설명하는 reStructuredText(.rst) 파일을 담고 있음 (사람이 읽기 위한 언어 사양)
cpython/Doc/reference
├── compound_stmts.rst      # 복합문 (if, while, for, 함수 정의 등)
├── introduction.rst        # 레퍼런스 문서 개요
├── index.rst               # 언어 레퍼런스 목차
├── datamodel.rst           # 객체, 값, 타입
├── executionmodel.rst      # 프로그램 구조
├── expressions.rst         # 표현식 구성 요소
├── grammar.rst             # 문법 규격(Grammar/Grammar 참조)
├── import.rst              # import 시스템
├── lexical_analysis.rst    # 어휘 구조 (줄, 들여쓰기, 토큰, 키워드 등)
├── simple_stmts.rst        # 단순문 (assert, import, return, yield 등)
└── toplevel_components.rst # 스크립트 및 모듈 실행 방법 설명

예시

  • Doc→reference→compound_stmts.rst 에서 간단한 예시로 with 문의 정의를 찾을 수 있음

기계가 읽을 수 있는 사양은 Grammar→python.gram이라는 단일 파일 안에 들어 있음

4.2.2 문법 파일

파서 표현식 문법(Parsing Expression Grammar, PEG) 사양을 사용하고 아래 표기법을 사용

  • *: 로 반복을 표현
    • : 최소 한번의 반복 표현
  • []: 선택적인 부분을 표현
  • | : 대안을 표현
  • (): 그룹을 표현

ex 1) 커피 한잔을 정의

  • 컵이 있어야 함
  • 최소 에스프레소 한 샷을 포함하고 여러 샷을 포함할 수 도 있음
  • 우유를 사용할 수도 있지만 선택적
  • 물을 사용할 수도 있지만 선택적
  • 우유를 사용했다면 두유나 저지방 우유 등 여러 종류의 우유를 선택할 수 있음
coffee: 'cup' ('expresso')+ ['water'] [milk]
milk: 'full-fat' | 'skimmed' | 'soy'

철도 다이어그램 (railroad diagram)

 

ex 2) while 문

여러 형태로 사용할 수 있는데 가장 간단한 형태는 표현식과 : 단말 기호(terminal), 코드 블록으로 이루어짐

while finished == True:
    do_things()

named_expression 대입 표현식을 사용할 수도 있음

while letters := read(document, 10):
	print(letters)

while 문 다음에 else 블록을 쓸 수도 있음

while item := next(iterable):
	print(item)
else:
	print("Iterable is empty")

while_stmt 는 문법 파일에 다음과 같이 정의되어 있음

# Grammar/python.gram L165
while_stmt[stmt_ty]:
    | 'while' a=named_expression ':' b=block c=[else_block] { _Py_While(a, b, c, EXTRA) }
  • 따옴표로 둘러싸인 부분은 단말기호라는 문자열 리터럴
  • 키워드는 단말 기호로 인식
  • block: 한개 이상의 문장이 있는 코드 블록
  • named_expression: 간단한 표현식 또는 대입 표현식을 나타냄

 

ex 3) try 문 (좀 더 복잡한 예시)

# Grammar/python.gram L189

try_stmt[stmt_ty]:
    | 'try' ':' b=block f=finally_block { _Py_Try(b, NULL, NULL, f, EXTRA) }
    | 'try' ':' b=block ex=except_block+ el=[else_block] f=[finally_block] { _Py_Try(b, ex, el, f, EXTRA) }
except_block[excepthandler_ty]:
    | 'except' e=expression t=['as' z=NAME { z }] ':' b=block {
        _Py_ExceptHandler(e, (t) ? ((expr_ty) t)->v.Name.id : NULL, b, EXTRA) }
    | 'except' ':' b=block { _Py_ExceptHandler(NULL, NULL, b, EXTRA) }
finally_block[asdl_seq*]: 'finally' ':' a=block { a }

try 문을 사용하는 방법은 2가지

  1. finally 문만 붙어있는 try
  2. 한개 이상의 except 뒤에 else나 finally가 붙는 try

 

4.3 파서 생성기

  • 파이썬 컴파일러는 문법 파일을 직접 사용하지 않고 파서 생성기가 문법 파일에서 생성한 파서를 사용
  • 문법 파일을 수정하면 파서를 재생성한 후 CPython을 다시 컴파일해야 함
    • 파서란?
  • 파이썬 3.9부터 CPython은 파서 테이블 생성기(pgen 모듈) 대신 문맥 의존 문법 파서를 사용
  • 기존 파서는 파이썬 3.9까지는 -X oldparser 플래그를 활성화해 사용할 수 있으며 파이썬 3.10에서 완전히 제거됨

4.4 문법 다시 생성하기

  • 새로운 PEG 생성기인 pegen을 테스트해보기 위해 문법 일부를 변경해봄
# Grammar/python.gram L66 을 아래처럼 변경 (|'proceed' 추가)

| ('pass'|'proceed') { _Py_Pass(EXTRA) }

변경 후 아래 명령어로 문법 파일을 다시 빌드

$ make regen-pegen
PYTHONPATH=./Tools/peg_generator python3.9 -m pegen -q c \\
		./Grammar/python.gram \\
		./Grammar/Tokens \\
		-o ./Parser/pegen/parse.new.c
python3.9 ./Tools/scripts/update_file.py ./Parser/pegen/parse.c ./Parser/pegen/parse.new.c

makefile이 있는 폴더에서 아래 명령어를 실행하면 proceed 키워드를 사용할 수 있음을 확인

$ ./python.exe
Python 3.9.20+ (heads/3.9-dirty:011fb84db5f, Nov  8 2024, 21:32:35) 
[Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> def example():
...     proceed
... 
>>> 
>>> example()
  • 위 과정을 통해 CPython 문법을 수정하고 컴파일해서 새로운 CPython을 만든 시도를 한 것

4.4.1 토큰

  • Grammar 폴더 내에 Tokens 파일에서 파스 트리의 leaf node에서 사용되는 고유한 토큰들을 정의함
  • 각 토큰은 이름과 자동으로 생성된 고유 아이디(ID)를 가지고, 이름을 사용하면 토크타이저에서 토큰을 더 쉽게 참조할 수 있음
LPAR           '('
RPAR           ')'
SEMI           ':'
  • Tokens 파일을 수정하면 pegen을 다시 실행해야 하고 tokenize 모듈을 이용하면 토큰이 사용되는 걸 호가인할 수 있음
# test_tokens.py
def my_function():
    proceed
./python.exe -m tokenize -e test_tokens.py 
0,0-0,0:            ENCODING       'utf-8'        
1,0-1,3:            NAME           'def'          
1,4-1,15:           NAME           'my_function'  
1,15-1,16:          LPAR           '('            
1,16-1,17:          RPAR           ')'            
1,17-1,18:          COLON          ':'            
1,18-1,19:          NEWLINE        '\\n'           
2,0-2,4:            INDENT         '    '         
2,4-2,11:           NAME           'proceed'      
2,11-2,12:          NEWLINE        '\\n'           
3,0-3,1:            NL             '\\n'           
4,0-4,0:            DEDENT         ''             
4,0-4,0:            ENDMARKER      ''
  • 출력에서 첫번째 열은 파일에서 토큰의 위치를 의미
    • def 의 경우 1,0-1,3 이라고 적혀 있는데 첫번째 줄 0번째 위치 부터 첫번재 줄 3번째 위치까지 있다는 의미
  • 두번째 열: 토큰의 이름
  • 세번째 열: 토큰의 값
  • 출력에서 tokenize 모듈은 일부 토큰을 자동으로 추가
    • utf-8 인코딩을 뜻하는 ENCODING 토큰
    • 함수 정의를 마치는 DEDENT 토큰
    • 파일 끝을 뜻하는 ENDMARKER 토큰
    • 끝 공백
  • Lib 폴더의 tokenize.py의 tokenize 모듈은 완전히 파이썬으로만 작성됨
  • 디버그 빌드를 -d 플래그로 실행해 C 파서가 실행되는 과정을 자세히 보면 아래와 같음
$ ./python.exe -d test_tokens.py
 > file[0-0]: statements? $
  > statements[0-0]: statement+
   > _loop1_11[0-0]: statement
    > statement[0-0]: compound_stmt
 ....
 > small_stmt[33-33]: ('pass' | 'proceed')
         > _tmp_15[33-33]: 'pass'
         - _tmp_15[33-33]: 'pass' failed!
         > _tmp_15[33-33]: 'proceed'
         - _tmp_15[33-33]: 'proceed' failed!
 ....
 + statements[0-10]: statement+ succeeded!
 + file[0-11]: statements? $ succeeded!
  • 내용이 길기 때문에 아래 명령어로 디버그 출력 내용을 파일로 저장해서 보면 위와같이 proceed는 키워드로 강조 표시 되어 있음
./python.exe -d test_tokens.py 2> output.txt

 

Grammar 폴더의 python.gram 파일을 원래대로 수정 후 아래 명령어로 문법을 다시 생성한 다음 빌드를 정리하고 다시 컴파일

$ make regen-pegen
$ make -j2 -s

 

 

반응형
반응형
class Solution:
    def firstUniqChar(self, s: str) -> int:
        dict_ = dict()
        
        for char in s:
            dict_[char] = dict_.get(char, 0) + 1
            
        for i in list(dict_.keys()):
            if dict_[i] == 1:
                return s.index(i)
            
        return -1

한번에 풀긴했음.근데 이건 python 3.7+ 에만 적용 가능( dictionay input 키 순서가 유지된다는 전제 필요)

 

 

하지만 index, 알파벳, 각 알파벳이 나오는 횟수 3가지 정보를 저장하는 효율적인 방법은??

def firstUniqChar(s: str) -> int:
    letters = 'abcdefghijklmnopqrstuvwxyz'
    index = [s.index(l) for l in letters if s.count(l) == 1]
		# 하나만 있는 char의 index들을 저장 
    return min(index) if len(index) > 0 else -1

빠른 이유는?

  • string의 index 함수가 c함수이기 떄문
d = {}
    
    for l in s:
        if l not in d: d[l] = 1
        else: d[l] +=1
        
    index = -1
    for i in range(len(s)):
        if d[s[i]]==1:
            index=i
            break

이게 제일 범용적으로 사용할 수 있는 코드 같음

class Solution:
    def firstUniqChar(self, s: str) -> int:
        d = {}

        for l in s:
            if l not in d: d[l] = 1
            else: d[l] +=1

        index = -1
        for i in range(len(s)):
            if d[s[i]]==1:
                index=i
                break
                
        return index

반응형

'코딩테스트' 카테고리의 다른 글

Leetcode 125. Valid Palindrome  (0) 2022.10.02
반응형

02-3 허브

주소 개념이 없는 물리 계층

  • 물리 계층에는 주소 개념이 없어 송수신 되는 정보에 대한 어떠한 조작이나 판단을 하지 않음
  • 데이터 링크 계층에는 주소 개념이 있어 송수신지를 특정할 수 있고, 주소를 바탕으로 정보에 대한 조작과 판단을 할 수 있음

허브(Hub)

  • 여러 대의 호스트를 연결하는 장치로 리피터 허브(repeater hub)라 부르기도 함. 이더넷 네트워크의 허브는 이더넷 허브(Ethernet hub)라고도 부름

허브 (이미지출처:  https://dev-splin.github.io/cs (computer science)/network/Network-OSI-Layer1-Physical/

 

  • 커넥터를 연결할 수 있는 5개의 연결지점이 보이는데 이를 포트(Port)라고 함
  • 포트에 호스트와 연결된 통신 매체를 연결할 수 있음

허브의 특징

허브는 오늘날의 인터넷 환경에서 잘 사용되지 않지만 두가지 특징 때문에 설명

1. 전달받은 신호를 다른 모든 포트로 그대로 다시 보냄

  • 물리 계층에 속하는 장비로, 주소 개념이 없기에 허브는 수신지를 특정할 수 없음
  • 따라서 송신지를 제외한 모든 포트에 그저 내보내기만 함
  • 허브를 통해 신호를 받은 모든 호스트는 데이터 링크 계층에서 패킷의 MAC 주소를 확인하고 자신과 관련 없는 주소는 폐기

2. 반이중 모드로 통신

  • 반이중(half duplex) 모드는 마치 1차선 도로처럼 송수신을 번갈아가면서 하는 통신 방식
  • 전이중(full duplex)모드는 송수신을 동시에 양방향으로 할 수 있는 통신 바익으로 마치 2차선 도로와 같음

충돌 도메인

  • 허브는 반이중 통신을 지원하는데, 한 호스트가 허브에 신호를 보내는 동안 다른 호스트도 허브에 신호를 보낸다면? 충돌(collision)이 발생
  • 허브에 호스트가 많이 연결되어 있을수록 충돌 발생 가능성이 높음. 이런 춘동일 발생할 수 있는 영역을 콜리전 도메인(collision domain)이라고 함. 허브에 연결된 모든 호스트는 같은 콜리전 도메인에 속함
  • 이 문제를 해결하기 위해 CSMA/CD 라는 프로콜을 사용하거나 스위치 장비를 사용

CSMA/CD

  • Carrier Sense Multiple Access with Collision Detection

Carrier Sense

  • 캐리어 감지를 뜻함. 캐리어 감지는 이 프로토콜을 사용하는 반이중 이더넷 네트워크에서는 메시지를 보내기 전에 현재 네트워크 상에서 전송 중인 것이 있는지를 먼저 확인하는 검사하는 과정

Multiple Access

  • 캐리어 감지를 하더라도 두 대 이상의 호스트가 부득이하게 동시에 네트워크를 사용하려 할 때가 있음
  • 이런 상황을 다중 접근(multiple access)라고 하고 이때 충돌이 발생

Collision Detection

  • 충돌 검출이라고 하고, 충돌을 감지하면 전송이 중단되고 충돌을 검출한 호스트는 다른 이들에게 충돌이 발생했음을 알리고자 잼 신호(jam signal)라는 특별한 신호를 보냄. 그리고 임의의 시간이 지난 후 다시 전송

정리하면, 먼저 전송 가능한 상태인지 확인하고, 다른 호스트가 전송 중이지 않을 때 메시지 전송. 만일 충돌이 발생하면 임의의 시간만큼 대기한 후에 다시 전송

 

References

반응형

+ Recent posts