Linux Tutorial #4 리눅스 커널 코딩 스타일 3장

문연수·2021년 5월 16일
1

Linux Tutorial

목록 보기
5/25
post-thumbnail

11. Kconfig 설정 파일 (Kconfig configuration files)

소스 트리 전체에 걸친 모든 Kconfig* 설정 파일에서, 들여쓰기는 다소 다르다. config 이 정의된 행 아래는 하나의 탭으로 들여써지는 반면, help 에선 추가적인 두 공백으로 들여써진다. 예시:

config AUDIT
	bool "Auditing support"
	doepnds 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.

정말로 위험한 특징 (예를 들어 특정 파일 시스템에 대한 지원을 작성할 때) 은 그들의 조치 문자열(prompt string)에서 이러한 즁요성을 알려야 한다:

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

설정 파일들에 대한 전체 문서를 위해서는, Documentation/kbuild/kconfig-language.txt 파일을 보라.

12. 자료구조 (Data structures)

단일-쓰레드 환경 밖에서 가시성을 가진 자료구조의 생성과 제거에 대해서 언제나 참조 카운터(reference count)를 가져야 한다. 커널에서, 쓰레기 수집가(garbage collection)은 존재하지 않으며, 이는 네가 반드시 전적으로 너의 모든 사용에 대한 참조사항을 가져야 한다는 것을 의미한다.

참조 카운터는 네가 잠금을 피할 수 있다는 것과 다중 사용자가 병렬적으로 데이터 구조에 대한 접근을 가지는걸 허용하는 것을 의미한다. 그리고 또한 이들이 잠들거나 잠시 다른 일을 함으로 인해 구조체가 갑자기 이들 아래에서 사라지는 것을 걱정할 필요가 없다.

잠금은 참조 카운트의 대체제가 아님에 주목하라. 잠그는 것은 자료구조가 일관성있게 유지되는 것으로 이용되는 반면, 참조 카운트는 메모리 관리 기술이다. 일반적으로 둘 다 필요하고, 그들은 서로에 대해 혼동되어질 수 없다.

다른 계층(class)의 사용자들이 존재할 때 많은 자료 구조는 실제로 2단계 참조 카운트를 가질 수 있다. 하위 계층 (subclass) 카운트는 하위 계층 유저 수를 센다. 그리고 하위 계층 카운터가 0 으로 변하면, 전체 카운트를 단 한번 줄인다.

이런 종류의 예로 multi-level-reference-counting 은 메모리 관리(sturct mm_struct: mm_users 그리고 mm_count) 와 파일 시스템 코드(struct super_block: s_count 그리고 s_active)에서 발견되어 질 수 있다.

기억하라: 만일 또 다른 쓰레드가 너의 자료 구조를 찾을 수 있고, 이에 대한 참조 카운트를 가질 필요가 없다면, 너는 확실하게 버그를 가지고 있는 것이다.

13. 매크로, 열거형 그리고 RTL (Macros, Enums and RTL)

상수를 정의하는 매크로의 이름 그리고 열거형의 레이블들은 대문자화(capitalized) 되어야 한다.

 #define CONSTANT 0x12345

열거형은 관련된 여러 상수를 정의할 때 선호 되어진다.

대문자화 매크로 이름은 적합하지만 함수를 닮은 매크로는 아마 소문자로 이름이 지어질 수 있다.

일반적으로, 인라인 함수들은 함수를 닮은 매크로보다 더 좋다.

다중 구문의 매크로는 do - while 블럭으로 둘러쌓여야 한다:

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

매크로를 사용할 때 피해야 하는 것들:
1. 제어 흐름에 영향을 주는 매크로:

#define FOO(X)					\
	do {					\
		if (blah(x) < 0)		\
			return -EBUGGERED;	\
	} while (0)

매우 나쁜 생각이다. 이는 함수와 같이 생겼지만 호출자 함수를 탈출한다; 앞으로 코드를 읽을 사람들의 내부 파서를 망치지 마라.

  1. 마법 이름(magic name) 의 지역 변수를 가지는 것에 의존적인 매크로:
#define FOO(val) bar(index, val)

는 보기엔 좋은 것 같지만, 코드를 읽는 사람에겐 지옥과 같이 혼란스러우며 겉보기엔 무해한 변경으로 인해 손상되기 쉽다.

  1. 좌변값(l-value)으로써 사용되어 질 수 있는, 인자를 가진 매크로: FOO(X) = y; 는 다른 사람에 의해 너를 물어 뜯을 것이다. e.g. FOO 를 인라인 함수로 변경한다.
  2. 우선순위(precedence) 에 대해 잊는 것: 표현식을 이용하여 상수를 정의하는 매크로는 반드시 표현식이 괄호로 닫혀야 한다. 매개변수를 사용하는 매크로에서 발생하는 유사한 문제에 주의하라.
    #define CONSTANT 0x4000
    #define CONSTEXP (CONSTANT | 3)
  3. 함수를 닮은 매크로 내 지역 변수를 정의할 때의 이름 공간 충돌:
    #define FOO(x)			\
    ({				\
    	typeof(x) ret;		\
    	ret = calc_ret(x);	\
        (ret);			\
    })

ret 은 지역 변수를 위한 흔한 이름이다 - __foo_ret 은 존재하는 변수에 대한 충돌이 덜하다.

cpp 메뉴얼은 매크로를 철저하게 다룬다. gcc 내부 매뉴얼은 또한 커널 내에서 어셈블리 언어와 함께 주로 이용되는 RTL 을 포함한다.

14. 커널 메세지 출력하기 (Printing kernel messages)

커널 개발자는 글을 읽고 쓰길 좋아하는 것처럼 보인다. 커널 메세지를 작성할 땐 좋은 인상을 남기기 위해 노력하라. dont 와 같은 잘못된 단축을 사용하지 마라: 대신 do not 혹은 dont't 를 써라. 메세지를 간결하게(concise), 명확하게(clear), 애매모호하지 않게(unambiguous) 만들어라.

커널 메세지는 구두점으로 종단 되어질 필요가 없다.

괄호로 둘러쌓인 숫자 (%d) 를 출력하는 것은 어떠한 가치도 없기에 피해야 한다.

<linux/device.h> 에는 메세지가 올바른 장치와 드라이버에 일치하는지 그리고 메세지가 올바른 수준으로 태그 되었는지를 확실하게 하기 위해 사용할 수 있는 수 많은 장치 모델 진단 매크로들이 있다: dev_err(), dev_warn(), dev_info(), 그리고 기타 등등. 특정 장치와 연관되지 않은 메세지들을 위해, <linux/printk.h>pr_notice(), pr_info(), pr_warn(), pr_err(), etc. 를 정의한다.

좋은 디버깅 메세지가 나오게 하는 것은 꽤나 어려운 도전이다. 일단 네가 그렇게 만들게 되면, 그들은 원격 문제 해결에 대한 큰 도움을 받을 수 있다. 그러나 디버깅 메세지 출력은 디버깅 메세지가 아닌 출력과는 다르게 처리되어질 수 있다. 반면 또 다른 pr_XXX() 함수는 무조건적으로 출력되지만, pr_debug() 는 그렇지 않다; 적어도 DEBUG 가 정의되거나 또는 CONFIG_DYNAMIC_DEBUG 가 설정되어 있지 않는 한 이는 기본적으로 컴파일되어 진다. 이는 dev_dbg() 에서 또한 그러하며 관련된 개념은 이미 허용된 DEBUG로부터 dev_vdbg() 메세지를 더하기 위해 VERBOSE_DEBUG 를 사용한다.

많은 하위 시스템은 대응하는 Makefile-DDEBUG 를 켜기 위한 Kconfig 디버그 설정을 가진다. 또 다른 경우에는 #define DEBUG 파일을 식별한다. 그리고 예를 들어 이것이 이미 디버그와 관련된 #ifdef 섹션 안에 있을 때, 디버그 메세지가 무조건적으로 출력되어져야 한다면, printk(KERN_DEBUG ---) 또한 사용되어질 수 있다.

15. 메모리 할당 (Allocating memory)

커널은 다음의 일반 목적 메모리 할당기를 제공한다: kmalloc(), kzalloc(), kmalloc_array(), vmalloc(), 그리고 vzalloc(). 더 추가적인 정보에 대해서는 이들의 API 문서를 참고하길 바란다.

구조체의 크기를 전달하는 선호되는 양식은 다음과 같다:

 p = kmalloc(sizeof(*p), ...);

다른 형태인, 구조체의 이름을 적는 것은 가독성을 손상시키고 포인터 변수의 자료형이 변경되었지만 대응하는 메모리 할당기에 전달되어지는 sizeof 가 변하지 않았을 때, 버그의 기회를 제공한다.

void 포인터형의 반한 값을 캐스팅 하는 것은 불필요하다. void 포인터을 또 다른 포인터형으로의 변환하는 것은 C 프로그래밍 언어로부터 보장되어진다.

배열을 할당하는데 선호되어지는 형태는 다음과 같다:

p = kmalloc_array(n, sizeof(...), ...);

0 으로 채워진 배열을 할당하는 선호되는 형태는 다음과 같다:

p = kcalloc(n, sizeof(...), ...);

두 형태는 n * sizeof(...) 크기 할당에 대한 오버 플로우를 확인하고, 이것이 발생하면 NULL 을 반환한다.

16. 인라인 질병 (The inline diseas)

여기에는 gccinline 이라 불리는 (나를 더 빠르게 해) 마법의 속도 상승 설정을 가진다는 흔한 오해가 나타단다. 적절한 inline 사용은 알맞을 수 있지만 (예를 들어, 매크로를 대체하는 수단으로, 12 장을 보라), 너무 낮으면 그렇지 않다.

풍부한 인라인 키워드 사용은 더 큰 커널로 이끌 것이며, CPU 에 대한 더 큰 i-캐쉬 발자국 그리고 단순하게 페이지 캐쉬로 이용 가능한 메모리가 적어지면, 이는 전체 시스템의 속도를 낮춘다. 한번 생각해봐라: 페이지 캐쉬 미스는 디스크 탐색을 야기하며 이는 단순히 5 밀리초가 걸린다. 여기에는 이 5 밀리초 동안 돌 수 있는 수 많은 CPU 사이클이 있다.

합리적인 주먹구구식 규칙은 3 줄 이상의 코드를 가지는 함수에 인라인을 넣지 않는 것이다. 이 규칙에 대한 예외는 매개변수가 컴파일 타임에 알 수 있는 상수인 경우이며, 그리고 이 상수화의 결과로서 너는 컴파일러가 대부분의 함수에 대해 컴파일 시간에 최적화할 수 있다는걸 알 것이다. 후자 경우의 좋은 예로, kmalloc() 인라인 함수를 보라:

종종 사람들은 static 이며 오직 한번만 사용되는 함수에 인라인을 추가하는 것은 공간 트레이드오프가 없으므로 언제나 승리한다고 주장한다. 이는 기술적으로 올바르지만, gcc 는 이러한 것들을 도움없이 인라인할 수 있는 능력이 있다. 그리고 두 번째 사용자가 나타났을 때 인라인을 제거하는 것에 대한 관리 문제는 어짜피 이뤄질 것에 대해 gcc 에게 무언가 하라는 힌트를 주는 것에 대한 잠재적 가치를 뛰어 넘는다.

17. 함수의 반환값과 이름들 (Function return values and names)

함수는 다양한 종류의 값을 반환할 수 있다. 그리고 그 중 가장 흔한 것은 함수의 성공과 실패의 유무이다. 이러한 값은 에러 코드 정수로 표현되어질 수 있다. (-Exxx = 실패, 0 = 성공) 혹은 성공 부울 (0 = 실패, 0 이 아닌 = 성공)

이러한 두 종류의 표현을 섞는 것은 에러를 찾기 어려운 버그가 비옥한 코드이다. 만일 C 언어가 정수와 부울을 엄밀하게 구분한다면 컴파일러는 이러한 실수를 찾을 수 있지만... 그러하지 않다.

이러한 에러를 예방하기 위한 도움을 받기 위해서는, 항상 다음의 관례를 따라라:

만일 함수의 이름이 동작이나 반드시 수행해야 하는 명령이라면,
함수는 정수 에로 코드를 반환해야 한다. 만일 이름이
술부(predicate)라면, 함수는 "성공"에 대한 부울을 반환해야 한다.

예를 들어, add work 는 명령이다. 그러므로 add_work() 함수는 성공에 대해 0 을 반환하고 실패에 대해 -EBUSY 를 반환하한다. 같은 방식으로 PCI device present 는 술부이다. 그러므로 pci_dev_present() 함수는 일치하는 장치를 발견했을 때 1 을 반환하고 그렇지 않을 때 0 을 반환한다.

모든 수출되어진 함수들은 반드시 이 관습을 준수해야 하며, 따라서 모든 공용(public) 함수들에 적용된다. 개인 (static) (private) 함수는 필요하진 않으나, 그렇게 하는 것이 권장 되어진다.

계산 성공 여부를 나타내는 것이 아닌 반환 값이 실제 계산을 반환하는 함수라면 이 규칙은 적용되지 않는다. 일반적으로 그들은 범위 밖 값을 반환함으로 실패를 나타낸다.

전형적인 예제는 포인터를 반환하는 함수이다; 이들은 에러 보고를 위해 NULL 혹은 ERR_PTR 기계 작용(mechanism) 을 사용한다.

18. 커널 매크로를 재발명하지 마라 (Don't re-invent the kernel macros)

너를 위한 일부 변체(variant)를 명시적으로 코딩하기보단, 헤더 파일 include/linux/kernel.h 에는 수 많은 매크로를 포함하고 있으므로, 너는 이를 사용해라. 예를 들어, 만일 네가 배열의 길이를 계산하길 원한다면, 다음 매크로의 이점을 가져라

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

유사하게, 만일 네가 구조체 멤버의 크기를 계산하길 원한다면, 써라

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

여기에는 또한, 네가 이들을 필요로 한다면 사용 가능한, 엄격한 자료형 검사를 사용하는 min() 그리고 max() 매크로 또한 있다. 너의 코드를 재생산하지 않게 그 밖에 무언가 이미 정의된 것들을 보면서 편하게 헤더 파일을 숙독(peruse)하라.

19. 편집기 모드라인과 그 밖의 것(Editor modelines and other craft)

일부 편집기는 특별한 표시로 나타나는, 소스 파일에 내장된 설정 정보를 해석할 수 있다. 예를 들어, emacs 는 다음과 같이 표시된 줄을 해석할 수 있다:

  -*- mode: c -*-

혹은 다음과 같은:

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

vim 은 다음과 같은 형태의 표시를 해석한다:

  /* vim:set sw=8 noet */

이런 것들을 어떤 소스파일에도 포함하지 마라. 사람들은 그들의 개인적인 편집기 설정을 가지고, 너의 소스코드가 그것들을 덮어써선 안된다. 이는 들여쓰기에 대한 표시와 모드 설정도 포함한다. 사람들은 그들의 독자적인 모드를 사용할 것이다. 혹은 들여쓰기를 정상적으로 동작시키기 위한 또 다른 마법의 방식을 사용할 수 있다.

20. 인라인 어셈블리 (Inline assembly)

아키텍처-종속 코드에서, 너는 CPU 혹은 플랫폼 기능과 통신하기 위해 인라인 어셈블리를 필요로 할 수도 있다. 필요하다면 사용하는 것을 망설이지 마라. 그러나, C 언어로 가능한 것들에 근거없이 인라인 어셈블리를 사용하지 마라. 너는 하드웨어를 C 를 통해 쑤실 수 있고, 또 그래야만 한다.

약간의 변형으로 반복적으로 작성하기 보단 몇 개의 단순한 인라인 어셈블리를 간단한 도움말 함수로 감싸는 것을 고려하라. 인라인 어셈블리가 C 매개변수로 이용될 수 있다는 것을 기억하라.

더 큰, 사소하지 않은 어셈블리 함수는 대응하는 C 프로토타입 정의를 가지는 C 헤더와 함께 .S 파일로 가야 한다. 어셈블리 함수를 위한 C 프로토타입은 asmlinkage 를 사용해야 한다.

만일 GCC 가 어떤 부작용도 발견치 못해 GCC 로부터 이것을 제거하는 것을 막기 위해 너는 asm 구문에 volatile 로 표시해야 할 수도 있다. 너는 언제나 항상 이를 필요치 않는다. 그러므로, 불필요하게 그런 짓을 하면 최적화를 제한할 수 있다.

다중 명령을 포함하는 단일 인라인 어셈블리 구문을 작성할 땐, 각 명령어를 나뉘어진 행에, 나뉘어진 따옴표 문자열에 넣어라, 그리고 마지막을 제외한 각 문자열을 \n\t 로 종료하여 다음 명령어의 어셈블리 출력을 적절하게 들여쓰기 하라.

  asm("magic %reg1, #42\n\t"
      "more magic %reg2, %reg3"
      : /* output */ : /* input */ : /* clobbers */);

21. 조건부 컴파일

가능하다면, 전처리기 조건(#if, #ifdef)를 .c 파일 내에서 사용하지 마라. 그렇게 하는 것은 코드를 읽기 어렵게 만들고 논리를 더 따라가기 어렵게 만든다. 대신 해당 .c 파일에서 사용할 함수를 정의하는 헤더 파일에서 이러한 조건을 사용하여 #else 경우에 no-op 토막(stub) 버전을 제공한 다음 .c 파일에서 해당 함수를 무조건 호출하라. 컴파일러는 토막 호출에 대한 코드를 생성하지 않고 동일한 결과를 생성하지만 논리는 더 쉽게 따라갈 수 있다.

함수의 일부나 표현식의 일부보다는 전체 함수를 컴파일 하는 것을 선호한다. 표현식에 #ifdef 를 넣는 대신 표현식의 일부 또는 전체를 빼내어 별도의 도우미 함수로 분리하고 해당 함수에 조건을 적용한다.

만일 네가 특정 설정에 대한 잠재적으로 사용되지 않는 쪽으로 가는 함수를 가진다면, 그리고 컴파일러는 그것의 정의가 사용되지 않는 쪽으로 가는 것에 대해 경고할 것이다. 그것을 조건 전처리기로 감싸기보단, __maybe_unused 와 같은 형태로 표시하라 (그러나, 만일 함수나 변수가 언제나 사용되지 않는 쪽으로 간다면, 삭제해라)

코드 내에서 가능할 때, IS_ENABLE 매크로를 Kconfig 기호에서 C 부울 표현식으로 변환하기 위해 사용해라. 그리고 이를 일반적인 C 조건문에서 사용하라:

if (IS_ENABLED(CONFIG_SOMETHING)) {
  	...
}

컴파일러는 조건에 맞춰 정리할 것이고, #ifdef 와 같이 코드의 블럭을 포함하거나 제외할 것이다. 따라서, 이는 어떠한 런타임 오버헤드를 추가하지 않는다. 그러나, 이러한 접근은 컴파일러가 블럭 내 코드를 보는 것을 여전히 허용한다. 그리고, 교정(문법, 자료형, 기호 참조, 기타 등등)을 위해 이를 확인한다.
그러므로, 만일 블럭 내 코드가 조건이 충족되지 않아 존재하지 않을 기호를 참조한다면, 너는 여전히 #ifdef 를 사용해야 한다.

사소하지 않은 #if 혹은 #ifdef 블럭 (몇 줄 이상의)의 끝에서, 조건 표현식이 사용되었다는 것을 주의하기 위해 #endif 이후 같은 라인에 주석을 위치 시켜라

#ifdef CONFIG_SOMETHING
...
#endif /* CONFIG_SOMETHING */

부록 I) 출처들

  • The C Programming Language, Second Edition by Brian W. Kernighan and Dennis M. Ritchie. Prentice Hall, Inc., 1988. ISBN 0-13-110362-8 (paperback), 0-13-110370-9 (hardback).
  • The Practice of Programming by Brian W. Kernighan and Rob Pike. Addison-Wesley, Inc., 1999. ISBN 0-201-61586-X.
  • GNU manuals - where in compliance with K&R and this text - for cpp, gcc, gcc internals and indent, all available from http://www.gnu.org/manual/
  • WG14 is the international standardization working group for the programming language C, URL: http://www.open-std.org/JTC1/SC22/WG14/
  • Kernel process/coding-style.rst, by greg@kroah.com at OLS 2002: http://www.kroah.com/linux/talks/ols_2002_kernel_codingstyle_talk/html/
profile
2000.11.30

2개의 댓글

comment-user-thumbnail
2021년 5월 18일

리눅스 커널이라니 멋저요!

1개의 답글