· KLDP.org · KLDP.net · KLDP Wiki · KLDP BBS ·
Linux Kernel Coding Style

리눅스 커널 코딩 스타일

번역: 김남형(pastime)

이 문서는 리눅스 커널 트리에 포함된 CodingStyle 문서를 번역한 것입니다.



이 문서는 리눅스 커널에서 주로 사용되는 코딩 스타일을 설명한 간략한 문서이다. 코딩 스타일은 매우 개인적인 내용이며 제 의견을 다른 사람들에게 강요하고 싶지는 않지만, 이것은 내가 관리해야 할 부분이며 다른 곳에서도 이를 즐겨 사용한다. (FIXME!) 최소한 여기서 설명한 내용들을 한 번 고려해 보기라도 해 주길 바란다.

먼저, GNU 코딩 표준 문서를 출력하길 바란다. 그리고 그 문서를 읽지 말고 태워버려라! 이건 매우 상징적인 행동이다.

어쨌든 시작해보자:


1. 들여쓰기


탭 크기는 8 글자이고, 따라서 들여쓰기 단위도 8 글자이다. 들여쓰기를 4 글자 (심지어는 2 글자!) 단위로 하려는 반대 운동이 있지만, 이것은 PI (원주율) 값을 3으로 정의하는 것과 같다.

논리: 들여쓰기에 대한 아이디어는 제어 블럭의 시작과 끝을 명확히 정의하기 위한 것이다. 특히 여러분이 20 시간동안 계속 스크린을 보고 있을 때는, 큰 들여쓰기를 이용할 때가 훨씬 편하다는 것을 알게 될 것이다.

이제, 어떤 사람들은 8 글자 들여쓰기를 하면 코드가 너무 오른쪽으로 가버려서 80 글자 크기의 터미널 화면에서 코드를 읽기가 힘들다고 불평할 것이다. 이에 대한 대답은, 프로그램에서 3 단계를 넘어가는 들여쓰기가 필요한 경우라면 이미 프로그램이 잘못된 것이므로 이를 고쳐야 한다는 것이다.

간단히 말해 8 글자 들여쓰기는 코드를 읽기 편하게 해주며, 함수가 너무 깊이 작성된 경우 경고를 해 주는 이점을 가진다. 이 경고에 유의하도록 하자.

switch 문에서 들여쓰기를 하는 방식은, "switch" 와 그에 따른 "case" 레이블들을 이중 들여쓰기 하지 않고 다음과 같이 동일한 열에 배치하는 것이다:

         switch (suffix) {
         case 'G':
         case 'g':
                 mem <<= 30;
                 break;
         case 'M':
         case 'm':
                 mem <<= 20;
                 break;
         case 'K':
         case 'k':
                 mem <<= 10;
                 /* fall through */
         default:
                 break;
         }

무언가를 숨기고 싶은 경우가 아니라면, 한 줄에 여러 문장을 함께 쓰지 말자:

         if (condition) do_this;
           do_something_everytime;

여러 개의 할당문을 한 줄에 같이 쓰지 않도록 하라. 커널 코딩 스타일은 매우 단순하다. 혼동되기 쉬운(tricky) 표현을 사용하지 않는 것이다.

주석이나 문서 (KConfig 는 예외) 이외에서는 공백 문자로 들여쓰기를 해서는 안된다. 위의 예제는 일부러 잘못된 경우를 보여준 것이다.

좋은 편집기를 사용하고, 줄의 끝 부분에 공백 문자를 남기지 않도록 하자.


2. 긴 줄과 문자열 나누기


코딩 스타일은 널리 사용되는 도구를 이용해 가독성과 유지 보수의 용이성을 높이는 것을 목표로 한다.

한 줄의 길이는 80 글자로 제한되며, 이는 필수적인 제한 사항 (hard limit) 이다.

80 글자 이상의 문장은 적당한 단위로 나누어 질 것이다. 나누어진 부분은 항상 원래의 문장보다 짧고, 오른쪽에 위치한다. 이는 긴 매개변수 목록을 가지는 함수에도 똑같이 적용된다. 긴 문자열도 마찬가지로 짧게 나누어진다.

void fun(int a, int b, int c)
{
        if (condition)
                printk(KERN_WARNING "Warning this is a long printk with "
                                                "3 parameters a: %u b: %u "
                                                "c: %u \n", a, b, c);
        else
                next_statement;
}

3. 괄호와 공백 문자 위치


C 코딩 스타일에서 항상 나오는 이슈로는 중괄호({ })의 위치에 대한 문제가 있다. 들여쓰기 크기와는 달리 어떤 방식을 선택하는 것에 대한 몇 가지 기술적인 이유가 있다. 하지만 (커널에서) 주로 사용되는 방식은 선지자 Kernighan 과 Ritchie 가 사용한, 다음과 같이 여는 중괄호를 같은 줄에 쓰고, 닫는 중괄호는 다음 줄의 처음에 쓰는 것이다:

        if (x is true) {
                we do y
        }

이것은 (함수가 아닌) 모든 문장 (if, switch, for, while, do 등) 에 대해 적용된다. 즉:

        switch (action) {
        case KOBJ_ADD:
                return "add";
        case KOBJ_REMOVE:
                return "remove";
        case KOBJ_CHANGE:
                return "change";
        default:
                return NULL;
        }

하지만 (주로) 함수의 경우에 대해서는 예외이다. 이들은 아래와 같이 여는 중괄호를 다음 줄의 처음에 쓴다:

int function(int x)
{
        body of function
}

전세계의 반론자들은 이러한 불일치를 ... 음 ... 모순이라고 불평하지만, 모든 올바른 사고 방식을 가진 사람들은 K&R 이 옳음을 알고 있다. (FIXME!!) 어쨌든 함수는 특별하게 처리된다. (C 에서는 함수를 중첩해서 정의할 수 없다)

닫는 중괄호는 그 자체로 한 줄을 차지하지만, if 문의 "else" 부분이나 do 문의 "while" 부분과 같이 동일한 문장이 연속되는 경우에는 아래와 같이 닫는 중괄호 뒤에 다른 내용이 올 수 있다:

        do {
                body of do-loop
        } while (condition);

또는

        if (x == y) {
                ..
        } else if (x > y) {
                ...


        } else {
                ....
        }

논리: K&R

또한, 이러한 중괄호 위치는 가독성을 떨어뜨리지 않으면서, 빈 줄 (혹은 거의 빈 줄) 의 수를 최소화 시켜 준다. 따라서 줄바꿈 문자(\n)로 인해 화면을 갱신하지 않아도 되며 (여기서는 25줄 짜리 터미널 스크린을 고려하였다) 주석을 달 수 있는 빈 줄을 확보하게 된다.

한 문장으로 가능한 곳에는 불필요하게 중괄호를 사용하지 말자.

if (condition)
        action();

이것은 여러 분기점에서 하나 만이 해당할 때는 적용되지 않는다. 이 경우에는 모두 중괄호를 이용한다.

if (condition) {
        do_this();
        do_that();
} else {
        otherwise();
}

3.1. 공백 문자


리눅스 커널에서 공백 문자를 사용하는 방식은 (주로) 함수와 키워드의 사용에 따라 달라진다. (대부분의) 키워드 뒤에는 공백 문자를 사용한다. 이에 대한 몇 가지 예외는 sizeof, typeof, alignof, attribute 이며 이들은 다소 함수와 같이 보인다. (이들은 또한 리눅스 내에서 괄호와 함께 사용된다. 하지만 C 언어 자체에서는 괄호의 사용이 필수적이지는 않다. 즉, "struct fileinfo info" 가 선언된 후에는 "sizeof info" 와 같은 형태로 사용할 수 있다.)

정리하면 다음 키워드 뒤에는 공백 문자를 사용하자:

if, switch, case, for, do, while

하지만 sizeof, typeof, alignof, attribute 에서는 사용하지 말자. 즉:

s = sizeof(struct file);

괄호로 둘러싸인 식 (내부) 에서는 공백 문자를 사용하지 않는다. 아래의 예제는 올바르지 않은 것이다:

s = sizeof( struct file );

포인터 데이터 혹은 포인터 타입을 반환하는 함수를 선언할 때는, 다음과 같이 '*' 기호를 타입 이름이 아닌 데이터 혹은 함수 이름에 붙여쓰는 방식이 사용된다:

char *linux_banner;
unsigned long long memparse(char *ptr, char **retptr);
char *match_strdup(substring_t *s);

다음과 같은 이항 및 삼항 연산자의 (양쪽) 주위에는 공백 문자를 사용한다:

= + - < > * / % | & ^ <= >= == != ? :

하지만 단항 연산자의 뒤에는 공백 문자를 사용하지 않는다:

& * + = ~ ! sizeof typeof alignof __attribute__ defined

접미 증가/감소 단항 연산자의 앞에는 공백 문자를 사용하지 않는다:

++ --

접두 증가/감소 단항 연산자의 뒤에는 공백 문자를 사용하지 않는다:

++ --

구조체 멤버 연산자인 '.' 과 "->" 주위에는 공백 문자를 사용하지 않는다.

줄의 끝 부분에 공백 문자를 남겨두지 않도록 하라. 똑똑한 들여쓰기 (smart indentation) 기능을 지원하는 몇몇 편집기들은 새 줄의 첫 부분에 적절히 공백 문자를 추가해 주므로, 적절한 지점에서 바로 코딩을 시작할 수 있다. 하지만 이러한 편집기들은 여기에 아무런 코드도 작성하지 않고 빈 줄 처럼 남겨둔 경우에는 공백 문자를 지워주지 않는다. 따라서 줄의 끝 부분에 공백 문자가 남아있게 된다.

git 는 여러분의 패치에 이러한 공백 문자가 포함되어 있는 경우에 경고를 보여주며, 이를 삭제하는 기능도 포함하고 있다. 하지만 여러 개의 패치를 한 번에 보내는 경우에는 이는 변경 사항으로 인해 뒷 부분의 패치들이 실패하게 될 수도 있다.

4. 이름짓기


C 언어는 간소한(Spartan) 언어이므로, 이름짓기 규칙도 이를 따라야 한다. Modula-2 나 파스칼 프로그래머와는 달리, C 프로그래머들은 ThisVariableIsATemporaryCounter 와 같은 귀여운(?) 이름을 사용하지 않는다. C 프로그래머들은 "tmp" 와 같이, 쓰기 쉽고 이해하기도 그리 어렵지 않은 이름의 변수를 사용할 것이다.

하지만, (대소문자를 섞어쓰는 것은 보기 안좋지만) 전역 변수에 대해서는 반드시 충분한 설명이 될 만한 이름을 붙여야 한다. 전역 함수의 이름을 "foo" 라고 짓는 것은 범죄 행위(shooting offense) 이다 (FIXME!)

전역 변수는 (정말로 필요한 경우에만 사용하자) 충분한 설명이 될 만한 이름을 가져야 하며, 이는 전역 함수에 대해서도 마찬가지이다. 만약 활동 중인 사용자의 수를 세는 함수를 작성했다면 이 함수의 이름은 "count_active_users()" 혹은 이와 비슷한 형태가 될 것이며, "cntusr()" 과 같은 형태가 되어서는 안 된다.

함수의 이름에 타입을 포함시키는 방식 ("헝가리안 표기법"이라고 한다) 은 멍청한 짓이다. 컴파일러는 타입을 알고 체크할 수 있으며, 이러한 표기법은 단지 프로그래머를 혼동스럽게 할 뿐이다. MicroSoft 에서 버그가 많은 프로그램들을 만들어 내는 것을 보면 당연하다.

지역 변수는 짧게 핵심만을 나타내는 이름을 사용한다. 만약 어떤 임의의 정수 루프 카운터가 필요하다면 그 이름은 "i" 가 될 것이다. 이를 "loop_counter" 라고 표기하는 것은 오해를 살 만한 여지가 없는 경우에는 생산적이지 않다. 마찬가지로, "tmp" 일시적인 값을 가지는 어떤 타입의 변수에도 사용될 수 있다.

혹시 여러분이 지역 변수 이름이 많아져 뒤섞이지 않을까 걱정하고 있다면, 여러분은 함수-성장-호르몬-불균형 증후군이라는 다른 문제를 가지고 있는 것이다. 6장 (함수) 부분을 보기 바란다.

5. 타입 정의


제발 "vps_t" 와 같은 것을 사용하지 않도록 하자.

구조체와 포인터에 대해 typedef 를 사용하는 것은 실수이다. 만약 소스에서 다음과 같은 내용을 보았다면

vps_t a;

이것이 무슨 의미인지 알 수 있겠는가?

하지만 다음과 같은 경우라면
struct virtual_container *a;

"a" 가 무엇인지 확실히 말해 줄 수 있게 된다.

많은 사람들이 typedef 가 "가독성에 도움을 준다"라고 생각하고 있지만, 그렇지 않다. typedef 는 오직 다음과 같은 경우에만 유용하다:


  1. 완전히 불투명한(opaque) 객체 (typedef 가 실제 객체를 숨기기위해 주로 사용되는 방식) 예: "pte_t" 등. 불투명 객체는 오직 적절한 접근 함수를 통해서만 접근되어야 한다.

    주의! 불투명성과 "접근 함수"는 그 자체로는 좋지 않다. 우리가 pte_t 등에서 이러한 방식을 사용하는 이유는 여기에는 실제로 이식성있게 접근할 수 있는 정보가 없기 때문이다.

  2. 정수형의 명확한 구분. 추상화는 해당 데이터가 "int" 인지 "long" 인지 혼란스럽지 않게 도와준다.

    u8/u16/u32 는 완전히 올바른 typedef 의 용례이다. (사실 이들은 여기보다 4번 항목에 더 잘 부합된다)

    주의! 여기에는 합당한 이유가 있어야 한다. 만약 어떤 데이터가 "unsigned long" 이라면 이를
    typedef unsigned long myflags_t;
    이라고 정의할 필요가 없다. 하지만 이 데이터가 특정 상황에서는 "unsigned int" 가 되고, 다른 상황에서는 "unsigned long" 이 된다면, typedef 를 사용하도록 하자.

  3. sparse 를 이용하여 타입 체크를 하기 위해 새로운 타입을 만드는 경우. (검토 필요!)

  4. 특정한 예외적인 환경에서 C99 표준과 동일한 새로운 타입.

    비록 'uint32_t' 와 같은 표준 타입에 익숙해 지는 데는 그리 오랜 시간이 필요한 것은 아니지만, 몇몇 사람들은 어쨌든 이와 같은 타입을 사용하지 않는다.

    따라서 표준과 동일한 의미를 가지지만 리눅스에서만 사용되는 'u8/u16/u32/u64' 타입 및 이에 대응하는 부호있는 (signed) 타입을 사용하는 것을 허용한다. 하지만 이것은 여러분이 새로운 코드를 작성할 때 반드시 지켜야 하는 사항은 아니다.

    기존의 코드를 작성할 때는 이미 선택하여 사용하고 있는 타입을 따라서 사용해야 한다.

  5. 사용자 영역에서 안전하게 사용할 수 있는 타입.

    사용자 공간에서 접근할 수 있는 특정 구조체들은 C99 타입을 요구하거나 위에서 말한 'u32' 형태의 타입을 사용할 수 없다. 따라서 사용자 공간과 공유할 수 있는 모든 구조체 내의 데이터들은 __u32 와 비슷한 타입을 사용한다.


아마도 다른 경우들도 있을 것이다. 하지만 기본적인 규칙은 위의 경우 중의 하나와 정확히 일치하지 않는다면 절대로 typedef 를 사용하지 말라는 것이다.

일반적으로 포인터나 적절히 접근 가능한 요소를 포함하는 구조체는 typedef 를 사용하지 않는다.


6. 함수


함수는 짧고, 멋지고, 오직 한 가지 일만 수행하도록 작성되어야 한다. 함수는 (80x24 크기를 가지는 ISO/ANSI 스크린에서) 하나 혹은 두 페이지 내에 표시될 수 있어야 하며, 한 가지 일 만을 잘 해내야 한다.

함수의 최대 길이는 함수의 복잡도와 들여쓰기 수준에 반비례(해야) 한다. 따라서 만약 개념적으로 간단한 함수의 경우에는 (이 함수는 하나의 긴 (하지만 단순한) case 문을 가지며, 각 case 에 따라 짧은 작업을 수행한다고 가정한다) 길게 작성하는 것이 허용된다.

하지만 복잡한 함수를 작성하는 경우에 있어, 보통의 (less-than-gifted) 고등학교 1학년생이 이 함수가 무엇을 하는 것인지 이해하고 힘들 것이라고 생각된다면 보다 엄격히 기본 규칙을 따라야 한다. 충분한 설명이 될 만한 이름을 가지는 보조 함수를 사용하도록 하자. (만약 이 함수의 성능이 중요한 영향을 미친다면 컴파일러에게 이를 in-line 시키도록 요청하자. 아마도 여러분이 하던 것보다 더 나은 결과를 가져올 것이다.)

함수의 (복잡도를 측정하는) 또 다른 척도는 지역 변수의 개수이다. 지역 변수의 수는 5~10 개를 넘어서는 안 되며, 그렇지 않다면 이미 잘못된 것이다. 함수를 다시 생각해보고 더 작은 부분으로 나누도록 하자. 사람의 뇌는 일반적으로 7 가지 다른 생각은 쉽게 기억할 수 있지만, 그 이상이 되면 혼란스러워 진다. 아마도 여러분은 자신이 똑똑하지 않다는 것을 알고 있겠지만, 2주 전에 했던 일을 이해하고 싶어할 것이다.

소스 파일에서, 각 함수들을 하나의 빈 줄을 이용해 구분해 두자. 만약 함수가 export 된다면, 함수의 닫는 중괄호 바로 아래에 EXPORT* 매크로를 넣어두도록 한다. 예를 들면:

int system_is_up(void)
{
        return system_state == SYSTEM_RUNNING;
}
EXPORT_SYMBOL(system_is_up);

함수 원형에서 매개 변수의 데이터 타입과 함께 변수 이름을 포함시킨다. 비록 C 언어에서는 이를 꼭 사용하지 않아도 되지만, 리눅스에서는 코드를 읽는 사람에게 보다 중요한 정보를 제공하기 위해서 이를 사용한다.


7. 단일화된 함수 종료


비록 어떤 사람들은 사용하지 않지만, goto 문과 동일한 문장은 컴파일러에서 무조건 점프 명령의 형태로 자주 사용된다.

goto 문은 함수가 여러 위치에서 종료되고, 몇 가지 공통적인 정리(cleanup) 작업을 수행해야 할 때 유용하게 사용된다.

논리적으로는:

  • 무조건 점프 명령은 이해하고 따라가기 쉽다
  • 중첩된 작업이 감소된다
  • 함수 변경 시 개별적인 종료 지점에 대한 갱신을 하지 않아 생길 수 있는 에러를 방지한다
  • 중복된 코드를 최적화 해야 하는 컴파일러의 작업을 줄여준다

int fun(int a)
{
        int result = 0;
        char *buffer = kmalloc(SIZE);


        if (buffer == NULL)
                return -ENOMEM;


        if (condition1) {
                while (loop1) {
                        ...
                }
                result = 1;
                goto out;
        }
        ...
out:
        kfree(buffer);
        return buffer;
}


8. 주석달기


주석은 좋지만, 과도한 주석에 의한 문제도 있을 수 있다. 절대로 주석 내에서 코드가 어떻게 동작하는지를 설명하지 않도록 하자. 코드가 하는 일을 명확히 나타낼 수 있도록 작성하는 것이 훨씬 좋으며, 잘 못 작성된 코드를 설명하는 것은 시간 낭비이다.

일반적으로 주석에서는 코드가 어떻게 동작하는지가 아닌 무엇을 하는 지를 설명하고 싶을 것이다. 또한 함수 내부에 주석을 달지 않도록 하자. 만약 함수가 너무 복잡해서 별도의 주석을 부분적으로 달고 싶다면, 잠시 6장으로 돌아가서 다시 한 번 읽어보기 바란다. 함수의 일부분에서 똑똑한 (혹은 멍청한) 일을 수행할 때 이에 대한 짧은 메모나 경고 문구를 추가하는 것은 가능하지만, 너무 많아 지지 않도록 주의한다. 대신 주석을 함수의 시작 부분에 달고, 이 코드가 무엇을 하는지 그리고 가능하다면 왜 하는지에 대한 내용을 설명하도록 한다.

커널 API 함수에 대한 주석을 달 때는 kernel-doc 형식을 따르기 바란다. 자세한 내용은 Documentation/kernel-doc-nano.txt 와 scripts/kernel-doc 파일을 참고하자.

리눅스에서 사용되는 주석은 C89 형식인 "/* ... */" 스타일이다. C99 형식의 "// ..." 주석을 사용하지 말자.

긴 (여러 줄에 걸친) 주석은 주로 다음과 같은 형태가 된다:

/*
 * This is the prefered style for multi-line
 * comments in the Linux kernel source code.
 * Please use it consistently.
 *
 * Description:  A column of asterisks on the left side,
 * with beginning and ending almost-blank lines.
 */

데이터에 대한 주석을 (기본 타입인지 파생 타입인지) 다는 것도 중요하다. 이를 위해서는 한 줄에 하나의 데이터 만을 선언하도록 한다 (컴마를 이용하여 여러 데이터를 한 줄에 선언하지 않도록 하자.) 그러면 각 항목을 위한 짧은 주석을 (데이터가 어떻게 사용되는지) 달 수 있는 공간을 확보할 수 있다.


9. 스타일을 망쳐버린 경우


괜찮다. 다들 그런다. 아마도 여러분은 오래전부터 유닉스를 사용해 왔던 사람들이 "GNU 이맥스(emacs)" 는 자동으로 C 소스 파일의 형식을 맞춰준다고 얘기했던 것을 들어봤을 것이다. 그렇다, 이맥스는 이러한 작업을 해 준다. 하지만 이맥스의 기본 설정은 생각했던 것과는 다르게 동작할 것이다 (사실, 이것은 무작위 타이핑보다 나쁘다 - 무수히 많은 원숭이들이 이맥스를 이용해 타이핑한다고 해도 절대로 좋은 프로그램을 작성할 수는 없다.)

따라서 여러분은 이맥스를 사용하지 않거나, 올바른 설정을 사용하도록 변경할 수 있다. 후자를 선택한다면 다음 내용을 여러분의 .emacs 파일에 입력하도록 하자:

(defun linux-c-mode ()
  "C mode with adjusted defaults for use with the Linux kernel."
  (interactive)
  (c-mode)
  (c-set-style "K&R")
  (setq tab-width 8)
  (setq indent-tabs-mode t)
  (setq c-basic-offset 8))

이것은 M-x linux-c-mode 명령을 정의할 것이다. 어떤 모듈을 해킹할 때, 파일의 첫 두 줄 내의 어딘가에 -*- linux-c -*- 라는 문자열을 넣어두면 이 모드가 자동으로 호출된다. 혹은 다음 내용을 .emacs 파일에 넣어두면 /usr/src/linux 아래의 소스 파일들을 편집할 때 자동으로 linux-c-mode 가 호출될 것이다.

(setq auto-mode-alist (cons '("/usr/src/linux.*/.*\\.[ch]$" . linux-c-mode)
                        auto-mode-alist))

하지만 이맥스를 통해 올바른 설정을 하도록 하는 일에 실패했더라도, 모든 것이 끝난 것은 아니다: 이 때는 "indent" 를 이용해 보자.

자 다시, GNU 인덴트(indent)는 GNU 이맥스가 가진 것과 동일한 멍청한 설정들을 가지고 있으며, 따라서 여러분은 이를 명령행 옵션으로 넘겨줘야 한다. 하지만 그렇게 나쁜 것 만은 아니다. 인덴트를 작성한 사람들도 K&R의 권위를 인지하고 있기 때문에 (GNU 측 사람들이 나쁘다는 것이 아니다, 그들은 단지 이 문제에 대해 완전히 잘 못 판단하고 있는 것 뿐이다) 단지 "-kr -i8" 이라는 옵션 (이것은 "K&R 스타일에 8 글자 들여쓰기"를 의미한다) 만을 이용하면 되기 때문이다. 아니면 최신 스타일로 들여쓰기를 사용하는 'scripts/Lindent" 를 사용하자.

인덴트는 많은 옵션을 가지고 있으며, 특히나 주석의 형식을 변경하고 싶은 경우라면 man 페이지를 읽어보기 바란다. 하지만 기억해야 할 것은 인덴트는 잘 못 작성된 프로그램을 수정해 주지는 않는다는 것이다.


10. Kconfig 설정 파일


소스 트리 내의 모든 Kconfig* 설정 파일들에서 사용되는 들여쓰기 규칙은 약간 다르다. "config" 정의 아래에 있는 줄들은 하나의 탭으로 들여쓰고, 이에 대한 도움말은 거기에 두 개의 공백 문자가 더 붙는다. 예를 들면 아래와 같다:

config AUDIT
        bool "Auditing support"
        depends on NET
        help
          Enable auditing infrastructure that can be used with another
          kernel subsystem, such as SELinux (which requires this for
          logging of avc messages output). Does not do system-call
          auditing without CONFIG_AUDITSYSCALL.

아직 불안정하다고 생각되는 기능들은 "EXPERIMENTAL" 에 의존하도록 정의해야 한다:

config SLUB
        depends on EXPERIMENTAL && !ARCH_USES_SLAB_PAGE_STRUCT
        bool "SLUB (Unqueued Allocator)"
        ...

하지만 (어떤 파일 시스템에 대한 쓰기 지원과 같은) 매우 위험한 기능들은 프롬프트 문자열 내에 확실히 경고해야 한다:

config ADFS_FS_RW
        bool "ADFS write support (DANGEROUS)"
        depends on ADFS_FS
        ...

설정 파일에 대한 완전한 문서는 Documentation/kbuild/kconfig-lanaguae.txt 파일을 보기 바란다.

11. 자료 구조


자신이 생성되고 소멸되는 단일 쓰레드 환경의 외부에서도 접근할 수 있는 자료 구조는 언제나 참조 횟수를 관리해야 한다. 커널 내에는 가비지 컬렉션이 존재하지 않으며 (커널 외부의 가비지 컬렉션은 느리고 효율적이지 않다) 따라서 여러분은 반드시 참조 횟수를 스스로 관리해야 한다.

참조 횟수를 관리하면 락킹(locking)을 사용하지 않고 여러 사용자가 해당 자료 구조를 동시에 이용할 수 있게 되며, 누군가가 잠시 sleep 되었거나 다른 작업을 수행하고 온 경우에도 해당 자료 구조가 사라져 버릴 걱정은 하지 않아도 된다.

락킹은 참조 횟수 관리를 대체하는 것이 아니다라는 사실에 주의하자. 락킹은 자료 구조를 일관성있게 관리하기 위한 기법이고, 참조 횟수 관리는 메모리 관리에 대한 기법이다. 보통은 이 두 가지가 모두 필요하며, 서로 혼동되지 않는다.

사실, 많은 자료 구조는 서로 다른 "클래스"의 사용자들이 있는 경우 두 단계의 참조 횟수를 관리할 수 있다. 하위 클래스 참조 횟수는 하위 클래스 사용자들의 수를 유지하며, 하위 클래스 사용자 수가 0 이 되면 전역 참조 횟수를 하나 감소시킨다.

이러한 "다 단계 참조 횟수 관리"는 메모리 관리 ("struct mm_struct": mm_users 와 mm_count) 및 파일 시스템 코드 ("struct super_block": s_count 와 s_active) 에서 찾아볼 수 있다.

기억하자: 만약 다른 쓰레드가 여러분의 자료 구조를 찾을 수 있고, 여러분이 해당 자료 구조에 대한 참조 횟수를 가지고 있지 않으면, 거의 대부분은 버그를 가지고 있는 것이다.

12. 매크로, 열거형, RTL


상수를 정의하는 매크로와 열거형 내의 레이블들은 대문자로 표기한다.

#define CONSTANT 0x12345

여러 개의 관련된 상수를 정의할 때는 열거형을 쓰는 것이 좋다.

매크로의 이름은 대문자로 표기하는 것이 원칙이지만, 함수를 흉내낸 매크로의 이름에는 소문자를 사용할 수 있다.

일반적으로, 매크로로 함수를 흉내내는 것 보다는 인라인(inline) 함수를 이용하는 것이 좋다.

여러 문장으로 이루어진 매크로는 do-while 블럭으로 감싸야 한다:

#define macrofun(a, b, c)                       \
        do {                                    \
                if (a == 5)                     \
                        do_this(b, c);          \
        } while(0)

매크로를 사용하는 경우 다음과 같은 경우를 조심해야 한다:

12.1. 제어 흐름에 영향을 주는 매크로


#define macrofun(a, b, c)                       \
        do {                                    \
                if (a == 5)                     \
                        return -EBUGGERED;      \
        } while(0)

위와 같은 경우는 매우 좋지 않다. 이것은 함수처럼 보이지만 호출하는 함수를 종료시켜 버린다. 코드를 읽는 사람의 구문 분석의 흐름을 깨지 마라.

12.2. 특별한 이름을 가지는 지역 변수에 의존하는 매크로


#define FOO(val)  bar(index, val)

위의 매크로는 괜찮아 보이지만, 코드를 읽는 사람에게 큰 혼란을 줄 수 있으며 겉으로 보기에는 무관한 내용을 변경했을 때에도 프로그램이 동작하지 않을 수 있다.


12.3. 매개 변수를 가지는 매크로를 좌변값(l-value)으로 사용


FOO(x) = y

위의 코드는 만약 누군가가 FOO 를 인라인 함수로 바꾼다면 문제가 될 것이다.


12.4. 우선 순위를 고려하지 않은 매크로


식(expression)을 사용해서 상수를 정의하는 매크로는 반드시 해당 식을 괄호로 감싸야 한다. 매개 변수를 이용하는 매크로에서도 이와 같은 문제에 신경써야 한다.

#define CONSTANT  0x4000
#define CONSTEXP  (CONSTANT | 3)

cpp 설명서는 매크로에 대해서 자세히 다루고 있다. gcc 내부 설명서에서는 커널에서 어셈블리 언어와 자주 사용되는 RTL에 대해서도 다루고 있다.

13. 커널 메시지 출력하기


커널 개발자들은 텍스트로 보길 좋아한다. 좋은 인상을 심어주려면 커널 메시지의 철자에 주의를 기울여야 한다. "dont" 와 같은 약어(crippled word)를 사용하지 말고, "do not" 혹은 "don't" 와 같은 표현을 사용하자. 메시지는 간결하고 명료하고 모호하지 않도록 작성한다.

커널 메시지는 마침표로 끝나지 않아야 한다.

괄호 안에 숫자를 출력하는 것은 (%d) 아무런 가치가 없으며 이러한 표현은 삼가하도록 하자. (FIXME!)

<linux/device.h> 파일에는 드라이버 모델 진단을 위한 여러 매크로들이 있으며, 메시지가 올바른 장치 및 드라이버와 일치하는지와 적절한 수준으로 표시되는지 확인해야 한다. (예를 들어, dev_err(), dev_warn(), dev_info() 등) 특정한 장치와 관련이 없는 메시지들은 <linux/kernel.h> 파일에 정의된 pr_debug() 와 pr_info() 를 이용하도록 한다.

디버깅 메시지를 잘 삽입하는 것은 꽤나 힘든 작업이 될 수 있다. 하지만 일단 이를 완료하면, 이후에 문제가 발생했을 때 커다란 도움이 될 것이다. 이러한 메시지들은 DEBUG 심볼이 정의되지 않은 경우에는 (기본적으로는 포함되지 않는다) 컴파일되지 않아야 (compiled out) 한다. 이는 dev_dbg() 혹은 pr_debug() 등을 이용하는 경우에는 자동으로 처리된다. 많은 서브 시스템 들은 -DDEBUG 를 켤 수 있는 Kconfig 옵션을 가지고 있다. 이와 관련된 기능으로 VERBOSE_DEBUG 를 사용하면 DEBUG 가 정의된 경우 디버깅 메시지에 dev_vdbg() 메시지를 추가할 수 있다.

14. 메모리 할당


커널은 다음과 같은 범용 메모리 할당 함수들을 제공한다:

kmalloc(), kzalloc(), kcalloc(), vmalloc()
이에 대한 자세한 정보는 API 문서를 참조하기 바란다.

구조체의 크기를 전달하는 기본적인 방식은 다음과 같다:

p = kmalloc(sizeof(*p), ...):
다른 방식은 구조체의 이름을 모두 적는 것인데, 이는 가독성을 해칠 뿐만 아니라 포인터 변수 타입이 변경되었지만 메모리 할당 함수로 넘겨지는 크기는 변경되지 않은 경우 잠재적인 버그를 포함할 수 있다.

void 포인터인 반환값을 캐스팅하는 것은 불필요한 (redundant) 작업이다. void 포인터를 다른 포인터 타입으로 변환하는 것은 C 프로그래밍 언어에서 보장한다.

15. inline 중독


사람들 사이에서는 gcc의 "inline" 기능이 프로그램을 "더 빠르게 실행하라"라는 옵션인 것으로 생각하는 잘못된 인식이 퍼져 있는 듯 하다. inline 기능은 (예를 들어 매크로를 대체하는 수단 으로 사용되는 경우, 12장 참조) 적절하게 사용될 수 있지만, 많은 경우에 있어 그렇지 않다.

inline 기능의 무분별한 사용은 커널의 크기를 증가시키고, 그에 따라 (최악의 경우) 시스템을 거의 멈추게 만들 정도로 느려지게 할 수도 있다. 이는 CPU의 icache 사용량이 증가하여, 단순히 시스템에 페이지 캐시를 위한 메모리가 부족해 지기 때문이다. 생각해 보자. 한 번의 페이지 캐시 미스가 발생하면 디스크 탐색이 이루어 지고 이는 약 5ms 정도의 시간을 소모한다. 이 5ms 내에는 매우 많은 cpu 사이클이 진행될 수 있다.

이성적인 규칙은 4줄 이상의 코드를 포함하는 함수에는 inline 키워드를 쓰지 않는 것이다. 이 규칙의 예외가 되는 경우들은 함수의 인자가 컴파일 시에 상수로 처리할 수 있는 경우나, 이러한 상수화(constantness)의 결과로 함수의 대부분이 컴파일 시에 최적화 될 것이라고 알고 있는 경우이다. 후자의 좋은 예로는 kmalloc() 인라인 함수를 보기 바란다.

때로 사람들은 static 으로 정의되고 오직 한 번만 호출되는 (따라서 공간적인 낭비가 없는) 함수에 inline 키워드를 써야 한다고 주장하기도 한다. 비록 이러한 주장이 기술적으로 옳긴 하지만, gcc는 이러한 경우에는 특별히 지정하지 않아도 자동으로 이를 inline 화 시킬 수 있으며, inline을 사용하지 않으면 이 함수를 호출하는 다른 함수가 생기는 경우에 gcc에게 어떻게 처리해야 할 지 잠재적인 힌트를 줄 수 있는 관리적인 문제도 있다. (FIXME!)

16. 함수의 반환값과 이름


함수는 매우 다양한 종류의 값들을 반환할 수 있으며, 가장 흔한 경우 중의 하나는 함수가 성공했는지 실패했는지를 알리는 값을 반환하는 것이다. 이러한 값들은 에러 코드를 나타내는 정수형 (-Exxx = 실패, 0 = 성공) 이나 성공 여부를 나타내는 참/거짓 (0 = 실패, 그 외 = 성공) 이 될 수 있다.

이러한 두 종류의 표현을 섞어서 사용하는 것은 찾기 어려운 버그를 만들어 낼 수 있는 기반이 된다. 만약 C 언어에 정수형과 참/거짓을 엄격하게 구분해 주는 기능이 들어 있다면, 컴파일러가 이러한 실수를 찾아줄 수 있겠지만... 실상은 그렇지 못하다. 이러한 버그를 방지하기 위해서, 항상 다음과 같은 관례를 따르도록 하자:

    만약 함수의 이름이 동작이나 명령을 나타내면 에러 코드 정수형을 반환하고,
    어떤 사실을 서술하는 경우(predicate)이면 성공 여부를 나타내는 참/거짓을 반환한다.

예를 들어, "add work"는 명령형이므로, add_work() 함수는 성공 시에 0을, 실패 시에는 _EBUSY 등을 반환한다. 마찬가지로, "PCI device present" 는 서술형이므로 pci_dev_present() 함수는 성공 시에 (해당 장치를 찾은 경우) 1을, 실패 시에는 0을 반환한다.

외부로 공개된 (EXPORTed) 모든 함수들은 반드시 이 관례를 따라야 하며, 다른 모든 공개된 (public) 전역 함수들도 이를 따라야 한다. 내부적인 (static) 함수들은 이 관례를 따를 필요는 없지만 이를 따르는 것이 좋다.

계산이 성공 했는지 여부를 반환하는 것이 아닌, 실제 계산 결과를 반환하는 함수들은 이 규칙의 적용 대상이 아니다. 일반적으로 이러한 함수들은 정상적인 결과의 범위를 넘어선 값을 반환하여 실패를 나타낸다. 이러한 함수들의 전형적인 예로는 포인터를 반환하는 함수들이 해당한다. 이러한 함수들은 NULL 혹은 ERR_PTR 메커니즘을 통해 실패를 알린다.

17. 커널 매크로를 다시 발명하지 말자


/linux/kernel.h 헤더 파일은 여러분이 사용해야 할 여러 매크로들을 포함하고 있다.

이러한 매크로와 비슷한 것들을 직접 정의하지 말고, 이들을 사용해야 한다. 예를 들어 배열의 길이를 계산해야 한다면 다음 매크로를 이용할 수 있다.

#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))

마찬가지로 어떤 구조체 내의 멤버의 크기를 계산해야 한다면 다음을 이용하자.

#define FIELD_SIZEOF(t, f) (sizeof(((t*)0)->f))

또한 필요한 경우 강력한 타입 검사를 수행하는 min(), max() 매크로도 존재한다. 헤더 파일을 잘 살펴보고 다른 어떤 매크로들이 정의되어 있는지 알아보고, 이러한 기능과 비슷한 것들을 여러분의 코드 내에 재정의하지 않도록 주의하자.

18. 편집기 모드라인 및 설정 정보


어떤 편집기들은 소스 파일 내에 특별한 표식으로 지정된 설정 정보를 해석할 수 있다. 예를 들어 이맥스(emacs)는 다음과 같이 표시된 줄을 해석한다:

-*- mode: c -*-

혹은 다음과 같은 것도 가능하다:

/*
Local Variables:
compile-command: "gcc -DMAGIC_DEBUG_FLAG foo.c"
End:
*/

vim은 다음과 같이 표시된 줄을 해석한다:

/* vim:set sw=8 noet */

소스 파일 내에 이러한 것들을 포함시키지 않도록 하자. 사람들은 각각 자신 만의 편집기 설정을 가지고 있으므로, 여러분이 작성한 소스 파일이 이를 무시해서는 안 된다. 여기에는 들여쓰기와 모드 설정에 대한 표시도 해당된다. 어떤 사람들은 자신 만의 고유한 모드를 사용하거나 들여쓰기를 처리하기 위한 특별한 방식(magic method)을 사용할 수도 있다.

19. 부록 I: 참고 문헌




최종 수정 2007-07-13.

이 번역본의 최신 버전은 http://namhyung.springnote.com/pages/858738 에서 보실 수 있습니다.




sponsored by andamiro
sponsored by cdnetworks
sponsored by HP

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2015-05-22 11:03:52
Processing time 0.0299 sec