커널 코딩 스타일(Coding Style) 가이드

Linux 커널 코딩 스타일: checkpatch.pl, 들여쓰기, 네이밍, 주석, goto, 매크로(Macro) 작성 규칙 완벽 가이드

전제 조건: 빌드 시스템(Build System)커널 모듈(Kernel Module) 문서를 먼저 읽으세요. 코딩 스타일은 패치(Patch) 제출 전 필수 검증 단계이며, 빌드 과정과 모듈 구조 이해가 선행되어야 합니다.
일상 비유: 이 주제는 건축 설계 규범과 비슷합니다. 건물마다 구조는 다르지만 안전 규격과 표준은 동일하게 적용되듯이, 커널 코드는 일관된 스타일로 유지보수성을 보장합니다.

핵심 요약

  • 자동 검증 — checkpatch.pl로 패치 제출 전 스타일 검사
  • 8칸 탭 — 들여쓰기는 8칸 탭, 공백 사용 금지
  • 80칸 제한 — 가독성을 위한 줄 길이 제한 (예외 허용)
  • 명확한 네이밍 — 함수는 동사, 변수는 명사, 매크로는 대문자
  • goto는 도구 — 에러 처리 경로 정리에 적극 사용

단계별 이해

  1. checkpatch.pl 설치
    커널 소스의 scripts/ 디렉토리에 위치, 패치 제출 전 필수 실행
  2. 기본 규칙 적용
    들여쓰기, 중괄호, 공백 등 기계적으로 확인 가능한 규칙부터 학습
  3. 네이밍 습관화
    기존 커널 코드를 읽으며 네이밍 패턴 익히기
  4. 코드 리뷰 참여
    메일링 리스트에서 다른 개발자의 피드백 관찰

Linux 커널 개발에서 일관된 코딩 스타일은 수천 명의 개발자가 협업하는 환경에서 코드 가독성과 유지보수성을 보장하는 핵심입니다. Documentation/process/coding-style.rst 기반으로 checkpatch.pl 사용법, 들여쓰기, 네이밍, 주석, goto, 매크로 등 모든 규칙을 실전 예제와 함께 설명합니다.

참고 문서: Documentation/process/coding-style.rst, Documentation/process/submitting-patches.rst — 커널 스타일 가이드의 공식 문서입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

checkpatch.pl 사용법

checkpatch.pl은 커널 소스 트리의 scripts/ 디렉토리에 있는 Perl 스크립트로, 패치 제출 전 코딩 스타일을 자동 검증합니다. 메일링 리스트에 제출된 패치의 약 30%가 스타일 문제로 거부되므로, 제출 전 필수 실행이 권장됩니다.

설치 및 기본 실행

커널 소스 트리가 있으면 별도 설치 없이 바로 사용 가능합니다:

# 단일 파일 검사
./scripts/checkpatch.pl -f drivers/net/dummy.c

# 패치 파일 검사 (가장 일반적인 용도)
./scripts/checkpatch.pl my-patch.patch

# git 커밋 검사
git format-patch -1 HEAD --stdout | ./scripts/checkpatch.pl -

# 전체 디렉토리 재귀 검사
./scripts/checkpatch.pl -f --terse drivers/staging/example/*.c

주요 옵션

옵션설명
-f파일 모드 (패치가 아닌 소스 파일 검사)
--strict엄격 모드 (추가 스타일 검사 활성화)
--terse간결한 출력 (파일명:줄번호: 메시지)
--no-tree커널 트리 외부에서 실행 시 사용
--fix자동 수정 가능한 항목 표시
--fix-inplace자동 수정 직접 적용 (주의 필요)
--ignore TYPE특정 경고 유형 무시
--show-types경고 유형 코드 표시

출력 해석

ERROR: trailing whitespace
#42: FILE: drivers/net/dummy.c:123:
+       return 0; $

WARNING: line over 80 characters
#58: FILE: drivers/net/dummy.c:139:
+       pr_info("This is a very long message that exceeds the recommended 80 character limit");

CHECK: Alignment should match open parenthesis
#65: FILE: drivers/net/dummy.c:146:
+       printk(KERN_INFO "message",
+               arg1, arg2);

total: 1 errors, 1 warnings, 1 checks, 120 lines checked
심각도 수준:
  • ERROR — 반드시 수정 (패치 거부 가능성 높음)
  • WARNING — 수정 권장 (정당한 이유 있으면 예외 허용)
  • CHECK — 선택적 개선 사항 (--strict 모드에서 표시)

실전 워크플로우

# 1. 코드 작성
vim drivers/example/mydriver.c

# 2. 로컬 커밋
git add drivers/example/mydriver.c
git commit -s -m "Add new feature to example driver"

# 3. checkpatch.pl 실행
git format-patch -1 HEAD --stdout | ./scripts/checkpatch.pl --strict -

# 4. 오류 수정 후 amend
vim drivers/example/mydriver.c
git add drivers/example/mydriver.c
git commit --amend --no-edit

# 5. 최종 확인
git format-patch -1 HEAD --stdout | ./scripts/checkpatch.pl -

# 6. 패치 생성
git format-patch -1 HEAD

들여쓰기 규칙

8칸 탭 원칙

커널은 공백(space)이 아닌 탭(tab)으로 들여쓰기합니다. 탭 너비는 8칸입니다. 이는 과도한 중첩을 방지하는 심리적 장벽 역할을 합니다.

절대 금지: 탭과 공백을 혼용하지 마세요. 에디터 설정에서 탭을 공백으로 변환하는 옵션을 비활성화해야 합니다.
/* ❌ 잘못된 예: 공백 사용 */
int bad_function(void)
{
    if (condition) {
        return 1;
    }
}

/* ✅ 올바른 예: 8칸 탭 사용 */
int good_function(void)
{
	if (condition) {
		return 1;
	}
}

정렬 규칙

함수 인자나 조건문이 여러 줄에 걸칠 때, 후속 줄은 여는 괄호에 맞춰 정렬합니다. 이 경우에만 탭 + 공백 조합이 허용됩니다.

/* ✅ 올바른 정렬 */
void netdev_info(const struct net_device *dev,
		  const char *fmt, ...)
{
	/* ... */
}

/* ✅ 조건문 정렬 */
if (very_long_variable_name == some_value &&
    another_long_name > threshold) {
	do_something();
}

80칸 제한

한 줄은 80칸을 넘지 않는 것이 원칙입니다. 다만 최근 커널에서는 100칸까지 허용하는 경향이 있으며, 다음 경우는 예외를 인정합니다:

/* ✅ 예외: 문자열은 분리하지 않음 */
pr_err("This is a very long error message that should not be split for greppability");

/* ❌ 잘못된 예: 문자열 분리 */
pr_err("This is a very long error message "
       "that was incorrectly split");

switch 문 들여쓰기

switch 문에서 case 레이블은 switch와 같은 깊이에 위치합니다:

switch (action) {
case ACTION_READ:
	read_data();
	break;
case ACTION_WRITE:
	write_data();
	break;
default:
	return -EINVAL;
}

중괄호 배치

함수 중괄호

함수의 여는 중괄호는 다음 줄에 위치합니다 (K&R 스타일과 다름):

/* ✅ 올바른 예 */
int function(int x)
{
	/* body */
}

/* ❌ 잘못된 예 */
int function(int x) {
	/* body */
}

제어문 중괄호

if, for, while, do 등 제어문의 여는 중괄호는 같은 줄에 위치합니다 (K&R 스타일):

/* ✅ 올바른 예 */
if (condition) {
	do_this();
	do_that();
} else {
	otherwise();
}

/* ❌ 잘못된 예 */
if (condition)
{
	do_this();
}

단일 문장 예외

단일 문장만 있는 경우 중괄호를 생략할 수 있습니다. 다만 한 분기에만 중괄호가 필요하면 모든 분기에 사용합니다:

/* ✅ 단일 문장 - 중괄호 생략 가능 */
if (condition)
	return 0;

/* ✅ 여러 문장 - 중괄호 필수 */
if (condition) {
	do_this();
	return 0;
}

/* ✅ 한 분기가 여러 줄이면 모두 중괄호 사용 */
if (condition) {
	do_this();
	do_that();
} else {
	otherwise();
}

/* ❌ 일관성 없는 중괄호 사용 */
if (condition) {
	do_this();
	do_that();
} else
	otherwise();

do-while 특수 규칙

do-while 문의 while은 닫는 중괄호와 같은 줄에 위치합니다:

do {
	process_data();
} while (condition);

네이밍 규칙

함수명

함수명은 소문자와 언더스코어를 사용하며, 동사로 시작하는 설명적 이름을 선호합니다:

/* ✅ 올바른 예 */
int get_user_pages(unsigned long start, int nr_pages);
void free_pages(unsigned long addr, unsigned int order);
static void update_rq_clock(struct rq *rq);

/* ❌ 잘못된 예: camelCase */
int getUserPages(unsigned long start, int nr_pages);

/* ❌ 잘못된 예: 의미 불명확 */
int foo(int x, int y);
네임스페이스(Namespace) 관행: 서브시스템 또는 드라이버명을 접두사로 사용하여 충돌을 방지합니다 (예: usb_alloc_urb, pci_register_driver).

변수명

변수명은 짧고 명확하게 작성합니다. 지역 변수는 축약 가능하지만, 전역 변수는 설명적이어야 합니다:

/* ✅ 지역 변수 - 짧게 가능 */
int count_pages(void)
{
	int i, cnt = 0;
	for (i = 0; i < nr_pages; i++)
		cnt++;
	return cnt;
}

/* ✅ 전역 변수 - 설명적 */
unsigned long total_memory_pages;

/* ❌ 잘못된 예: 전역 변수가 모호함 */
int tmp;

매크로명

매크로는 모두 대문자로 작성하며, 단어는 언더스코어로 구분합니다:

/* ✅ 올바른 예 */
#define MAX_BUFFER_SIZE 1024
#define IS_ALIGNED(x, a) (((x) & ((a) - 1)) == 0)

/* ❌ 잘못된 예: 소문자 사용 */
#define max_buffer_size 1024
예외: 함수처럼 사용되는 매크로 중 일부는 소문자를 사용할 수 있습니다 (예: list_for_each_entry). 하지만 이는 기존 코드와의 일관성이 있을 때만 허용됩니다.

구조체(Struct) 및 타입명

구조체 태그는 소문자와 언더스코어를 사용합니다. typedef는 최소화하고, 사용 시 _t 접미사를 붙입니다:

/* ✅ 올바른 예 */
struct file_operations {
	ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
};

/* ✅ typedef 사용 시 */
typedef unsigned long pgoff_t;

/* ❌ 잘못된 예: 불필요한 typedef */
typedef struct {
	int x;
} myStruct;

주석 작성법

블록 주석

여러 줄 주석은 다음 형식을 따릅니다. 네트워크 서브시스템은 다른 스타일을 사용하므로 기존 코드를 참고하세요:

/*
 * This is the preferred multi-line comment style.
 * Each line starts with a space, an asterisk, and another space.
 * The closing marker is on a separate line.
 */

/* 네트워크 서브시스템 스타일 (net/) */
/* This is a multi-line comment in networking subsystem.
 * Note the different opening line style.
 */

한 줄 주석

한 줄 주석은 C99 스타일(//)보다 전통적인 /* */ 스타일이 선호됩니다:

/* ✅ 선호되는 스타일 */
int ret;  /* return value */

/* ⚠️ 허용되나 비권장 */
int ret;  // return value

함수 설명 주석

공개 함수는 kernel-doc 형식으로 문서화합니다:

/**
 * fget_light - 빠른 파일 디스크립터 변환
 * @fd: 파일 디스크립터 번호
 * @fput_needed: 출력 플래그 (fput 필요 여부)
 *
 * 현재 프로세스의 파일 디스크립터 테이블에서 struct file을 가져옵니다.
 * 싱글 스레드 프로세스의 경우 참조 카운트 증가를 생략하여 성능을 향상시킵니다.
 *
 * Return: struct file 포인터, 실패 시 NULL
 */
struct file *fget_light(unsigned int fd, int *fput_needed)
{
	/* ... */
}

데이터 구조 주석

구조체 멤버에는 인라인 주석을 추가합니다:

struct task_struct {
	volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	void *stack;		/* kernel stack pointer */
	unsigned int flags;	/* per process flags, defined below */
};

goto 문 사용

에러 처리 패턴

커널에서 goto는 에러 처리 경로를 정리하는 권장 패턴입니다. 중첩된 if 문과 중복된 cleanup 코드를 방지합니다:

/* ✅ 올바른 goto 사용 */
int setup_device(struct device *dev)
{
	int ret;
	void *buffer;

	buffer = kmalloc(SIZE, GFP_KERNEL);
	if (!buffer) {
		ret = -ENOMEM;
		goto err_alloc;
	}

	ret = register_device(dev);
	if (ret)
		goto err_register;

	ret = enable_interrupts(dev);
	if (ret)
		goto err_irq;

	return 0;

err_irq:
	unregister_device(dev);
err_register:
	kfree(buffer);
err_alloc:
	return ret;
}
/* ❌ 잘못된 예: goto 없이 중복 cleanup */
int setup_device_bad(struct device *dev)
{
	void *buffer = kmalloc(SIZE, GFP_KERNEL);
	if (!buffer)
		return -ENOMEM;

	if (register_device(dev)) {
		kfree(buffer);
		return -EIO;
	}

	if (enable_interrupts(dev)) {
		unregister_device(dev);
		kfree(buffer);  /* 중복! */
		return -EIO;
	}

	return 0;
}

레이블 네이밍

레이블명은 해제할 자원 또는 에러 상황을 명확히 표현합니다:

해제 순서: 레이블은 할당의 역순으로 배치합니다. 가장 마지막에 할당한 자원을 가장 먼저 해제합니다.

매크로 작성 규칙

대문자 사용

매크로는 모두 대문자로 작성하며, 함수처럼 보이는 매크로도 예외가 아닙니다 (일부 기존 코드 제외):

#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
#define min(x, y) ((x) < (y) ? (x) : (y))

괄호 규칙

매크로 인자는 항상 괄호로 감싸서 연산자 우선순위(Priority) 문제를 방지합니다:

/* ❌ 잘못된 예 */
#define SQUARE(x) x * x
/* SQUARE(a + 1) = a + 1 * a + 1 = 2a + 1 (잘못됨!) */

/* ✅ 올바른 예 */
#define SQUARE(x) ((x) * (x))
/* SQUARE(a + 1) = ((a + 1) * (a + 1)) (올바름) */

do-while(0) 패턴

여러 문장을 포함하는 매크로는 do { ... } while (0)으로 감쌉니다. 이는 세미콜론 필수화와 if 문 호환성을 보장합니다:

/* ❌ 잘못된 예 */
#define FREE_BOTH(x, y) kfree(x); kfree(y)

/* if (condition) FREE_BOTH(a, b); else foo();
 * → if (condition) kfree(a); kfree(y); else foo();
 * kfree(y)가 if 밖으로 나가고 else가 컴파일 에러 */

/* ✅ 올바른 예 */
#define FREE_BOTH(x, y) do { \
	kfree(x); \
	kfree(y); \
} while (0)

/* if (condition) FREE_BOTH(a, b); else foo();
 * → 정상 작동 */

typeof 활용

매크로에서 타입 안전성을 위해 typeof를 사용합니다 (GCC 확장):

#define min_t(type, x, y) ({ \
	type __x = (x); \
	type __y = (y); \
	__x < __y ? __x : __y; \
})

#define container_of(ptr, type, member) ({ \
	const typeof(((type *)0)->member) *__mptr = (ptr); \
	(type *)((char *)__mptr - offsetof(type, member)); \
})

typedef 사용 제한

커널은 typedef를 최소화합니다. 구조체를 숨기는 것은 캡슐화(Encapsulation)가 아니라 혼란을 초래한다는 철학입니다:

허용되는 경우

금지되는 경우

/* ❌ 잘못된 예: 구조체 숨기기 */
typedef struct {
	int x, y;
} point_t;

/* ✅ 올바른 예: 구조체 노출 */
struct point {
	int x, y;
};
이유: struct point p;는 타입이 구조체임을 명시하지만, point_t p;는 타입 정보를 숨깁니다. 커널 개발자는 코드를 읽을 때 타입의 본질을 즉시 파악해야 합니다.

함수 길이 및 분할

함수 길이 제한

함수는 한 화면(24-25줄) 내에 들어가는 것이 이상적입니다. 더 길어지면 가독성이 떨어지고 복잡성이 증가합니다:

경험 법칙:
  • 함수가 3개 화면을 넘으면 반드시 분할 고려
  • 지역 변수가 10개를 넘으면 논리적 단위로 분리
  • 중첩이 3단계를 넘으면 helper 함수로 추출

함수 분할 예제

/* ❌ 너무 긴 함수 */
int process_request(struct request *req)
{
	/* 50줄의 검증 로직 */
	/* 30줄의 데이터 처리 */
	/* 20줄의 결과 전송 */
}

/* ✅ 분할된 함수들 */
static int validate_request(struct request *req)
{
	/* 검증 로직만 */
}

static int handle_request_data(struct request *req)
{
	/* 데이터 처리만 */
}

static int send_response(struct request *req)
{
	/* 결과 전송만 */
}

int process_request(struct request *req)
{
	int ret;

	ret = validate_request(req);
	if (ret)
		return ret;

	ret = handle_request_data(req);
	if (ret)
		return ret;

	return send_response(req);
}

static 함수 활용

파일 내부에서만 사용되는 함수는 static으로 선언하여 심볼 네임스페이스를 오염시키지 않습니다:

/* ✅ 내부 helper 함수 */
static void cleanup_resources(struct device *dev)
{
	/* ... */
}

/* ✅ 공개 API 함수 */
int device_init(struct device *dev)
{
	/* ... */
	cleanup_resources(dev);
}
EXPORT_SYMBOL_GPL(device_init);

지역 변수 선언

선언 위치

C99 표준을 따라 변수는 사용 지점에 가깝게 선언할 수 있습니다. 다만 함수 시작 부분에 선언하는 전통적 방식도 여전히 사용됩니다:

/* ✅ 전통적 스타일 */
int function(void)
{
	int ret, i;
	struct device *dev;

	/* 로직 */
}

/* ✅ C99 스타일 (허용) */
int function(void)
{
	struct device *dev = get_device();

	for (int i = 0; i < 10; i++) {
		/* ... */
	}
}

초기화

선언과 동시에 초기화하는 것이 권장되지만, 불필요한 초기화는 피합니다:

/* ✅ 의미 있는 초기화 */
int ret = 0;
struct list_head *pos, *n;

/* ❌ 불필요한 초기화 */
int x = 0;
x = get_value();  /* 바로 덮어씌워짐 */

공백 사용 규칙

연산자 주변 공백

대부분의 이항 및 삼항 연산자는 양쪽에 공백을 넣습니다:

/* ✅ 올바른 예 */
x = y + z;
if (a == b && c != d)
result = (x > 0) ? x : -x;

/* ❌ 잘못된 예 */
x=y+z;
if(a==b&&c!=d)
예외: 단항 연산자(&, *, ++, --, !, ~)와 피연산자 사이에는 공백을 넣지 않습니다.
예: *ptr, !flag, i++

키워드와 괄호

대부분의 키워드는 여는 괄호 앞에 공백을 넣습니다. sizeof, typeof, alignof 등은 예외입니다:

/* ✅ 올바른 예 */
if (condition)
while (count > 0)
switch (value)

/* ✅ sizeof는 함수처럼 취급 (공백 없음) */
size = sizeof(struct device);

/* ❌ 잘못된 예 */
if(condition)
sizeof (struct device)

함수 호출

함수명과 여는 괄호 사이에는 공백을 넣지 않습니다:

/* ✅ 올바른 예 */
ret = function(arg1, arg2);

/* ❌ 잘못된 예 */
ret = function (arg1, arg2);

포인터 선언

포인터 타입 선언 시 *는 변수명 쪽에 붙입니다:

/* ✅ 올바른 예 */
char *name;
struct device *dev;

/* ❌ 잘못된 예 */
char* name;
char * name;

실전 예제

디바이스 드라이버 초기화

다음은 커널 스타일을 모두 적용한 완전한 예제입니다:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/io.h>

#define DRIVER_NAME "example-device"

struct example_dev {
	void __iomem *base;	/* MMIO base address */
	int irq;			/* interrupt number */
	struct device *dev;
};

/**
 * example_hw_init - 하드웨어 초기화
 * @edev: example_dev 구조체 포인터
 *
 * Return: 성공 시 0, 실패 시 음수 에러 코드
 */
static int example_hw_init(struct example_dev *edev)
{
	u32 val;

	/* 하드웨어 리셋 */
	writel(0x1, edev->base + 0x00);
	usleep_range(100, 200);

	/* 초기화 확인 */
	val = readl(edev->base + 0x04);
	if (!(val & 0x80000000)) {
		dev_err(edev->dev, "Hardware init failed\\n");
		return -EIO;
	}

	return 0;
}

static int example_probe(struct platform_device *pdev)
{
	struct example_dev *edev;
	struct resource *res;
	int ret;

	edev = devm_kzalloc(&pdev->dev, sizeof(*edev), GFP_KERNEL);
	if (!edev)
		return -ENOMEM;

	edev->dev = &pdev->dev;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	edev->base = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(edev->base)) {
		ret = PTR_ERR(edev->base);
		goto err_alloc;
	}

	edev->irq = platform_get_irq(pdev, 0);
	if (edev->irq < 0) {
		ret = edev->irq;
		goto err_alloc;
	}

	ret = example_hw_init(edev);
	if (ret)
		goto err_alloc;

	platform_set_drvdata(pdev, edev);
	dev_info(&pdev->dev, "Device initialized successfully\\n");

	return 0;

err_alloc:
	return ret;
}

static int example_remove(struct platform_device *pdev)
{
	dev_info(&pdev->dev, "Device removed\\n");
	return 0;
}

static const struct of_device_id example_of_match[] = {
	{ .compatible = "vendor,example-device" },
	{ }
};
MODULE_DEVICE_TABLE(of, example_of_match);

static struct platform_driver example_driver = {
	.probe = example_probe,
	.remove = example_remove,
	.driver = {
		.name = DRIVER_NAME,
		.of_match_table = example_of_match,
	},
};
module_platform_driver(example_driver);

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Your Name <your.email@example.com>");
MODULE_DESCRIPTION("Example device driver");
코드 설명
  • 1-3행 플랫폼 디바이스 드라이버에 필요한 헤더 포함. io.h는 MMIO 접근 함수 제공.
  • 5행 드라이버명 매크로 정의. 대문자로 작성.
  • 7-11행 드라이버 프라이빗 데이터 구조체. __iomem은 스파스 타입 체크용 어노테이션.
  • 13-20행 kernel-doc 형식 함수 주석. 공개 함수는 이 형식으로 문서화.
  • 23-24행 MMIO 레지스터(Register) 읽기/쓰기. writel/readl 사용.
  • 28-31행 에러 처리 패턴. 실패 시 로그 출력 후 음수 에러 코드 반환.
  • 40-42행 devm_* API 사용으로 자동 메모리 관리(Memory Management). sizeof(*edev) 패턴 사용.
  • 51-53행 에러 처리 goto 패턴. IS_ERR로 포인터 에러 확인.
  • 66행 에러 레이블. 할당 역순으로 정리.
  • 77-80행 Device Tree 매칭 테이블. MODULE_DEVICE_TABLE로 모듈 로딩 자동화.
  • 82-89행 플랫폼 드라이버 구조체. 구조체 멤버 초기화 스타일.
  • 90행 드라이버 등록(Driver Registration) 매크로. module_init/exit를 내부에서 처리.

일반적인 위반 사례

공백 관련 오류

위반 사례수정 방법
if(condition) if (condition) (키워드 뒤 공백)
function (arg) function(arg) (함수명 뒤 공백 제거)
x=y+z; x = y + z; (연산자 양쪽 공백)
줄 끝 공백 (trailing whitespace) 에디터 설정으로 자동 제거
탭/공백 혼용 들여쓰기는 탭만 사용

구조 관련 오류

위반 사례수정 방법
함수가 100줄 초과 논리적 단위로 함수 분할
중첩 if 문 5단계 이상 early return 또는 helper 함수로 평탄화
goto를 사용하지 않은 중복 cleanup goto 에러 처리 패턴 적용
지역 변수 15개 이상 구조체로 그룹화 또는 함수 분할

네이밍 관련 오류

위반 사례수정 방법
getUserData() (camelCase) get_user_data() (snake_case)
int tmp, data, x; (의미 불명) int page_count, user_data, offset; (설명적)
#define max(a,b) (소문자 매크로) #define MAX(a, b) (대문자)
typedef struct { ... } foo; struct foo { ... }; (typedef 제거)

checkpatch.pl 빈발 경고

ERROR: trailing whitespace
WARNING: line over 80 characters
WARNING: please, no spaces at the start of a line
ERROR: space prohibited before that ',' (ctx:WxW)
WARNING: Prefer 'unsigned int' to bare use of 'unsigned'
CHECK: Alignment should match open parenthesis
WARNING: Missing a blank line after declarations
ERROR: do not use C99 // comments

에디터 설정

Vim 설정

~/.vimrc에 다음 설정 추가:

" 커널 스타일 설정
set tabstop=8
set shiftwidth=8
set noexpandtab
set textwidth=80

" 공백 문제 강조
highlight ExtraWhitespace ctermbg=red guibg=red
match ExtraWhitespace /\s\+$/

" 커널 소스 디렉토리에서 자동 적용
autocmd BufRead,BufNewFile */linux-*/*.c,*/linux-*/*.h set cindent cinoptions=:0,l1,t0,g0,(0

Emacs 설정

~/.emacs에 다음 설정 추가:

(defun c-lineup-arglist-tabs-only (ignored)
  "Line up argument lists by tabs, not spaces"
  (let* ((anchor (c-langelem-pos c-syntactic-element))
         (column (c-langelem-2nd-pos c-syntactic-element))
         (offset (- (1+ column) anchor))
         (steps (floor offset c-basic-offset)))
    (* (max steps 1) c-basic-offset)))

(add-hook 'c-mode-hook
          (lambda ()
            (let ((filename (buffer-file-name)))
              (when (and filename
                         (string-match "linux" filename))
                (setq indent-tabs-mode t)
                (setq show-trailing-whitespace t)
                (c-set-style "linux-tabs-only")))))

VS Code 설정

.vscode/settings.json에 추가:

{
  "[c]": {
    "editor.insertSpaces": false,
    "editor.tabSize": 8,
    "editor.rulers": [80, 100],
    "editor.detectIndentation": false,
    "editor.renderWhitespace": "boundary",
    "files.trimTrailingWhitespace": true
  }
}

#include 정렬 규칙

헤더 파일 포함 순서는 커널 전체에서 일관성을 유지하기 위해 중요합니다. 올바른 순서는 컴파일 의존성 문제를 방지하고 코드 리뷰를 용이하게 합니다.

권장 순서

커널 소스 파일에서 #include는 다음 순서를 따릅니다:

순서카테고리예시
1해당 모듈/드라이버의 자체 헤더#include "mydriver.h"
2linux/ 핵심 헤더#include <linux/module.h>
3linux/ 서브시스템 헤더#include <linux/netdevice.h>
4asm/ 아키텍처 헤더#include <asm/io.h>
5로컬 헤더#include "internal.h"
/* ✅ 올바른 #include 순서 예제 (drivers/net/ethernet/intel/e1000e/netdev.c 참고) */
#include <linux/module.h>
#include <linux/types.h>
#include <linux/init.h>
#include <linux/pci.h>
#include <linux/vmalloc.h>
#include <linux/pagemap.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/tcp.h>

#include <asm/io.h>

#include "e1000.h"
카테고리 간 빈 줄: 각 카테고리 사이에 빈 줄을 넣어 시각적으로 구분합니다. 같은 카테고리 내에서는 알파벳 순서가 권장됩니다.

자기 완결 헤더

모든 헤더 파일은 자기 완결적(self-contained)이어야 합니다. 즉, 헤더 파일 하나만 포함해도 컴파일이 가능해야 합니다:

/* ❌ 잘못된 예: mydriver.h가 linux/types.h에 의존하지만 포함하지 않음 */
/* mydriver.h */
struct my_device {
	u32 status;  /* u32 정의가 없음! */
};

/* ✅ 올바른 예: 필요한 헤더를 직접 포함 */
/* mydriver.h */
#ifndef _MYDRIVER_H
#define _MYDRIVER_H

#include <linux/types.h>

struct my_device {
	u32 status;
};

#endif /* _MYDRIVER_H */
중복 포함 방지: 모든 헤더 파일에 include guard(#ifndef/#define/#endif)를 반드시 사용합니다. 커널에서는 #pragma once를 사용하지 않습니다.

조건부 컴파일 스타일

#ifdef를 남용하면 코드 가독성이 급격히 떨어집니다. 커널은 조건부 컴파일을 구조적으로 관리하는 패턴을 권장합니다.

#ifdef 최소화

함수 본문 안에 #ifdef를 넣는 대신, 헤더 파일에서 조건에 따라 빈 인라인 함수(Inline Function)를 제공합니다:

/* ❌ 잘못된 예: 함수 안에 #ifdef 산재 */
void process_data(struct data *d)
{
#ifdef CONFIG_DEBUG_INFO
	pr_debug("processing %p\n", d);
#endif
	do_work(d);
#ifdef CONFIG_STATISTICS
	stats.processed++;
#endif
}

/* ✅ 올바른 예: 헤더에서 조건부 인라인 함수 제공 */
/* mydriver.h */
#ifdef CONFIG_DEBUG_INFO
static inline void debug_log(struct data *d)
{
	pr_debug("processing %p\n", d);
}
#else
static inline void debug_log(struct data *d) { }
#endif

/* mydriver.c — 깔끔한 코드 */
void process_data(struct data *d)
{
	debug_log(d);
	do_work(d);
}

IS_ENABLED() 매크로

C 코드 안에서 Kconfig 옵션을 검사할 때는 #ifdef 대신 IS_ENABLED()를 사용하면 컴파일러가 데드 코드를 자동 제거합니다:

/* ❌ 전통적 방식 */
#ifdef CONFIG_NET
	setup_networking();
#endif

/* ✅ IS_ENABLED 사용 (타입 체크 유지) */
if (IS_ENABLED(CONFIG_NET))
	setup_networking();

/* IS_ENABLED는 tristate(y/m/n)도 처리 */
if (IS_ENABLED(CONFIG_USB))    /* CONFIG_USB=y 또는 =m 이면 true */
	init_usb();

if (IS_BUILTIN(CONFIG_USB))   /* CONFIG_USB=y 일 때만 true */
	init_usb_builtin();

if (IS_MODULE(CONFIG_USB))    /* CONFIG_USB=m 일 때만 true */
	init_usb_module();
장점: IS_ENABLED()를 사용하면 해당 코드가 비활성화된 설정에서도 컴파일은 되므로 빌드 오류를 조기에 발견할 수 있습니다. #ifdef로 감싸면 해당 설정이 꺼졌을 때 코드가 아예 컴파일되지 않아 구문 오류가 숨겨집니다. 자세한 Kconfig 구성 방법은 빌드 시스템 문서를 참고하세요.

반환값 규칙

커널 함수의 반환값은 일관된 규칙을 따릅니다. 이 규칙을 어기면 호출자 쪽에서 에러 처리가 누락되거나 혼란이 생깁니다.

에러 코드 반환

커널 함수는 성공 시 0을 반환하고, 실패 시 음수 errno 값을 반환하는 것이 표준입니다:

/* ✅ 표준 반환값 패턴 */
int my_init(struct device *dev)
{
	struct resource *res;

	res = request_resource(dev);
	if (!res)
		return -ENOMEM;	/* 메모리 부족 */

	if (!valid_config(dev))
		return -EINVAL;	/* 잘못된 인자 */

	if (!hw_ready(dev))
		return -EBUSY;	/* 장치 사용 중 */

	return 0;		/* 성공 */
}

자주 사용되는 에러 코드

에러 코드의미사용 예
-ENOMEM-12메모리 할당 실패kmalloc, kzalloc 실패
-EINVAL-22잘못된 인자유효하지 않은 파라미터
-ENODEV-19장치 없음하드웨어 미감지
-EBUSY-16자원 사용 중이미 초기화된 장치
-EIO-5I/O 오류하드웨어 통신 실패
-EPERM-1권한 없음권한 검사 실패
-EFAULT-14잘못된 주소copy_from_user 실패
-ENOSPC-28공간 부족버퍼(Buffer)/디스크 공간 부족
-ETIMEDOUT-110타임아웃대기 시간(Latency) 초과
-ENOENT-2항목 없음파일/엔트리 미발견

포인터 반환과 ERR_PTR

포인터를 반환하는 함수에서 에러를 전달할 때는 ERR_PTR/IS_ERR/PTR_ERR 매크로를 사용합니다:

/* ✅ ERR_PTR 패턴 */
struct buffer *alloc_buffer(size_t size)
{
	struct buffer *buf;

	if (size == 0)
		return ERR_PTR(-EINVAL);

	buf = kzalloc(sizeof(*buf) + size, GFP_KERNEL);
	if (!buf)
		return ERR_PTR(-ENOMEM);

	buf->size = size;
	return buf;
}

/* 호출자 측 */
struct buffer *buf = alloc_buffer(4096);
if (IS_ERR(buf)) {
	pr_err("allocation failed: %ld\n", PTR_ERR(buf));
	return PTR_ERR(buf);
}
NULL과 ERR_PTR을 혼용하지 마세요: 하나의 함수에서 실패 시 어떤 경우에는 NULL을, 어떤 경우에는 ERR_PTR()을 반환하면 호출자가 양쪽 모두 검사해야 합니다. 일관되게 ERR_PTR()만 사용하거나, NULL만 사용하세요. 두 방식을 섞어야 한다면 IS_ERR_OR_NULL()로 검사할 수 있지만, 이는 설계 결함의 징후입니다.

kernel-doc

kernel-doc은 커널 소스 코드에서 API 문서를 자동 생성하는 시스템입니다. 공개 함수와 구조체에는 반드시 kernel-doc 형식의 주석을 작성해야 합니다.

함수 문서화

/**
 * devm_kzalloc - Resource-managed kzalloc
 * @dev: Device to allocate memory for
 * @size: Allocation size
 * @gfp: Allocation gfp flags
 *
 * Managed kzalloc. Memory allocated with this function is
 * automatically freed on driver detach. Like all other
 * devres functions, resulting memory is zeroed on allocation.
 *
 * Context: Can sleep. GFP_KERNEL is typical.
 * Return: Pointer to allocated memory on success, NULL on failure.
 */
void *devm_kzalloc(struct device *dev, size_t size, gfp_t gfp)
{
	/* ... */
}

구조체 문서화

/**
 * struct net_device - The DEVICE structure.
 * @name: This is the first field of the "visible" part of this structure
 *        (i.e. as seen by users in the "Space.c" file).
 * @mem_start: Shared memory start address
 * @mem_end: Shared memory end address
 * @base_addr: Device I/O address
 * @irq: Device IRQ number
 * @state: Generic network queuing layer state, see netdev_state_t
 * @features: Currently active device features
 *
 * This structure is the main network device structure. It holds
 * hardware and software configuration for each network interface.
 *
 * Members of this structure should be accessed using accessor
 * functions where provided.
 */
struct net_device {
	char name[IFNAMSIZ];
	unsigned long mem_start;
	unsigned long mem_end;
	unsigned long base_addr;
	int irq;
	unsigned long state;
	netdev_features_t features;
	/* ... */
};

kernel-doc 태그 정리

태그위치설명
@param파라미터 설명함수/구조체 멤버마다 한 줄씩 기술
Context:호출 컨텍스트슬립(Sleep) 가능 여부, 락 조건 명시
Return:반환값 설명성공/실패 각각의 반환값 기술
Note:주의 사항호출자가 알아야 할 특이사항
Warning:경고잘못 사용하면 발생하는 문제

문서 검증

# kernel-doc 형식 검증
./scripts/kernel-doc -v -none drivers/example/mydriver.c

# 누락된 문서 확인
./scripts/kernel-doc -Werror -none drivers/example/mydriver.c

# HTML 문서 생성
make htmldocs

# 특정 서브시스템만 빌드
make SPHINXDIRS=driver-api htmldocs
kernel-doc 규칙: EXPORT_SYMBOL 또는 EXPORT_SYMBOL_GPL로 내보내는 모든 함수에는 kernel-doc 주석이 필수입니다. static 함수는 선택 사항이지만, 복잡한 로직이 있으면 추가를 권장합니다.

Kconfig 스타일

Kconfig 파일에도 고유한 스타일 규칙이 있습니다. scripts/checkpatch.pl은 Kconfig 파일도 검사합니다.

기본 형식

# ✅ 올바른 Kconfig 스타일
config MY_DRIVER
	tristate "My Example Driver"
	depends on PCI && NET
	select FW_LOADER
	default n
	help
	  This driver supports the Example PCI network device.

	  To compile this driver as a module, choose M here.
	  The module will be called my_driver.

	  If unsure, say N.

# ❌ 잘못된 예: 들여쓰기 오류
config MY_DRIVER
tristate "My Example Driver"
depends on PCI
help
This driver supports the Example device.
들여쓰기 규칙: config 아래 속성은 탭 1개로 들여씁니다. help 텍스트는 탭 1개 + 공백 2개로 들여씁니다. 이 규칙은 C 코드와 다르므로 주의하세요. 빌드 시스템 전체 구성은 빌드 시스템 문서에서 다룹니다.

의존성 규칙

# ✅ 올바른 의존성 패턴
config USB_STORAGE
	tristate "USB Mass Storage support"
	depends on USB
	select SCSI
	help
	  Say Y here if you want to connect USB mass storage devices to
	  your computer's USB port.

# select vs depends on
# - depends on: 사용자가 의존 항목을 먼저 켜야 함 (약한 결합)
# - select: 자동으로 의존 항목을 켬 (강한 결합, 주의 필요)

# ❌ 잘못된 예: user-visible 옵션을 select하면 안 됨
config BAD_EXAMPLE
	bool "Bad Config"
	select NETDEVICES  # NETDEVICES는 메뉴에서 보이는 항목!

코딩 스타일 검증 워크플로우

패치 제출 전까지 거치는 코딩 스타일 검증 과정을 전체적으로 정리합니다. 각 단계에서 확인해야 할 항목과 도구를 시각적으로 보여줍니다.

패치 제출 전 코딩 스타일 검증 워크플로우 1. 코드 작성 에디터 설정 적용 2. checkpatch.pl 자동 스타일 검사 PASS FAIL 스타일 수정 git commit --amend 3. 빌드 검증 make W=1 / make C=1 4. 자체 리뷰 리뷰 플레이북 적용 5. 패치 생성 git format-patch 6. 최종 checkpatch 패치 파일에 대해 실행 7. 메인테이너 확인 get_maintainer.pl 8. 패치 제출 git send-email checkpatch.pl 검사 항목 - 들여쓰기 (탭 vs 공백) - 줄 길이 (80/100칸) - 중괄호 배치 - 공백/네이밍 규칙 - typedef 사용 빌드 검증 명령 make M=drivers/foo make W=1 (추가 경고) make C=1 (Sparse 분석) make coccicheck (Coccinelle) make htmldocs (문서 빌드) 자체 리뷰 체크리스트 - 함수 길이 25줄 이하 - goto 에러 처리 패턴 - kernel-doc 주석 완성 - 커밋 메시지 형식 - Signed-off-by 태그
실무 팁: 위 워크플로우에서 가장 많은 시간이 소요되는 단계는 2단계(checkpatch.pl)와 4단계(자체 리뷰)입니다. 에디터 설정을 올바르게 해두면 2단계에서 발견되는 오류를 크게 줄일 수 있습니다. 패치 제출 전체 과정은 커널 패치 제출 문서에서 자세히 다룹니다.

코딩 스타일 규칙 전체 구조

커널 코딩 스타일의 각 규칙이 어떤 목적과 관계를 가지는지 전체 구조를 한눈에 파악합니다.

커널 코딩 스타일 규칙 구조도 유지보수 가능한 코드 수천 명 협업 환경의 핵심 목표 형식 규칙 8칸 탭 들여쓰기 80칸 줄 길이 제한 K&R 중괄호 배치 연산자 공백 규칙 포인터 선언 스타일 #include 정렬 네이밍 규칙 snake_case 함수명 서브시스템 접두사 대문자 매크로 설명적 전역 변수 typedef 제한 구조 규칙 함수 25줄 이하 goto 에러 처리 static 함수 활용 매크로 do-while(0) 반환값 규칙 문서화 규칙 kernel-doc 주석 블록 주석 형식 구조체 멤버 주석 Kconfig help 텍스트 커밋 메시지 형식 자동화 도구 checkpatch.pl 형식 + 네이밍 검사 Sparse 타입 안전성 분석 Coccinelle 시맨틱 패치 분석 kernel-doc / htmldocs 문서 생성 + 검증

메모리 할당 스타일

커널 메모리 할당에는 고유한 스타일 규칙이 있습니다. 잘못된 패턴은 메모리 누수, 버퍼 오버플로(Buffer Overflow)우, 또는 커널 패닉(Kernel Panic)을 유발할 수 있습니다.

sizeof 사용 패턴

/* ✅ 올바른 예: 변수 기반 sizeof (타입 변경에 안전) */
struct foo *p;
p = kmalloc(sizeof(*p), GFP_KERNEL);

/* ❌ 잘못된 예: 타입 기반 sizeof (타입 변경 시 불일치 위험) */
struct foo *p;
p = kmalloc(sizeof(struct foo), GFP_KERNEL);

/* ✅ 배열 할당: kcalloc 또는 kmalloc_array 사용 */
struct entry *entries;
entries = kcalloc(count, sizeof(*entries), GFP_KERNEL);

/* ❌ 잘못된 예: 오버플로우 위험 */
entries = kmalloc(count * sizeof(*entries), GFP_KERNEL);
정수 오버플로(Integer Overflow)우 주의: count * sizeof(...)count가 크면 정수 오버플로우가 발생할 수 있습니다. kmalloc_array() 또는 kcalloc()은 내부적으로 오버플로우를 검사합니다. 자세한 메모리 관리 API는 메모리 관리 개요 문서를 참고하세요.

devm_* 관리 할당

디바이스 드라이버에서는 devm_* API를 사용하여 디바이스 수명 주기에 맞게 자원을 자동 해제합니다:

/* ✅ devm_* 사용 — 드라이버 해제 시 자동 정리 */
static int my_probe(struct platform_device *pdev)
{
	struct my_dev *mdev;
	void __iomem *base;

	mdev = devm_kzalloc(&pdev->dev, sizeof(*mdev), GFP_KERNEL);
	if (!mdev)
		return -ENOMEM;

	base = devm_platform_ioremap_resource(pdev, 0);
	if (IS_ERR(base))
		return PTR_ERR(base);

	mdev->irq = platform_get_irq(pdev, 0);
	if (mdev->irq < 0)
		return mdev->irq;

	/* devm 할당이므로 remove에서 kfree/iounmap 불필요 */
	return 0;
}
devm_* 사용 원칙: probe 함수에서 할당하는 자원은 가능하면 모두 devm_* 버전을 사용하세요. devm_kzalloc, devm_ioremap_resource, devm_request_irq, devm_clk_get 등 대부분의 자원 API에 devm_ 접두사 버전이 존재합니다. 커널 모듈 문서에서 모듈 수명 주기를 함께 학습하세요.

printk 및 로깅 스타일

커널 로그 메시지는 디버깅(Debugging)의 핵심 도구입니다. 일관된 로깅 스타일은 문제 추적 시간을 크게 단축합니다.

로그 레벨

매크로레벨용도
pr_emerg()0시스템 사용 불가
pr_alert()1즉시 조치 필요
pr_crit()2치명적 상태
pr_err()3에러 상태
pr_warn()4경고 상태
pr_notice()5정상이지만 주목할 만한 상태
pr_info()6일반 정보
pr_debug()7디버그 메시지 (DEBUG 또는 CONFIG_DYNAMIC_DEBUG 필요)

디바이스 드라이버 로깅

디바이스 드라이버에서는 pr_*() 대신 dev_*()를 사용하여 디바이스 정보를 자동으로 포함합니다:

/* ❌ pr_*: 어떤 디바이스의 메시지인지 알 수 없음 */
pr_err("failed to initialize hardware\n");

/* ✅ dev_*: 디바이스 이름이 자동 포함 */
dev_err(&pdev->dev, "failed to initialize hardware\n");
/* 출력: "example-device 0000:01:00.0: failed to initialize hardware" */

/* 네트워크 드라이버: netdev_* 사용 */
netdev_err(netdev, "link down\n");

/* ✅ 포맷 지정자 — 커널 전용 확장 */
pr_info("MAC address: %pM\n", dev->addr);     /* MAC 주소 */
pr_info("IP address: %pI4\n", &ip_addr);      /* IPv4 주소 */
pr_info("device: %pOF\n", np);                /* Device Tree 노드 */
pr_info("resource: %pR\n", res);              /* struct resource */
로그 메시지 규칙:
  • 메시지 끝에 반드시 \n(개행)을 포함하세요
  • 메시지에 함수명을 수동으로 넣지 마세요 — __func__ 매크로를 사용하세요
  • 문자열을 여러 줄로 분리하지 마세요 (grep 검색 가능성 유지)
  • 초기화 성공 메시지에 pr_info(), 실패에 pr_err()를 사용하세요
커널 디버깅 기법에 대한 자세한 내용은 디버깅 문서를 참고하세요.

rate limiting

반복적으로 발생하는 에러 메시지는 로그를 범람시킬 수 있습니다. rate limiting 매크로를 사용합니다:

/* ✅ 반복 메시지 제한 */
if (error_condition)
	pr_err_ratelimited("hardware error detected\n");

/* ✅ 한 번만 출력 */
pr_warn_once("deprecated API used, please update\n");

/* ✅ 디바이스 버전 */
dev_err_ratelimited(dev, "DMA error on channel %d\n", ch);

추가 자료

공식 문서

검증 도구

참고 코드

다음 파일들은 모범적인 코딩 스타일을 보여줍니다:

실무 코드 리뷰 플레이북

스타일 문서를 읽는 것만으로는 품질이 올라가지 않습니다. 리뷰 단계에서 반복 가능한 기준으로 검사해야 실제로 버그와 유지보수 비용을 줄일 수 있습니다.

검사 축 핵심 질문 빠른 점검 방법
가독성 함수가 한 번에 이해되는가? 함수 길이, early return, 의미 있는 변수명 확인
에러 경로 실패 시 자원 정리가 완전한가? goto 레이블 순서, 누수 여부 확인
동시성 락/원자성/컨텍스트 규칙이 맞는가? sleep 가능 컨텍스트와 irq 컨텍스트 분리 확인
패치 품질 커밋 단위가 논리적으로 분리되었는가? 기능 변경과 리팩터링을 같은 커밋에 섞지 않기

리뷰 전 자동 점검 명령

# 스타일 검사
./scripts/checkpatch.pl --file drivers/foo/bar.c

# 최소 빌드 검증
make M=drivers/foo -j$(nproc)

# 경고 확대
make W=1 M=drivers/foo

# 정적 분석
make C=1 M=drivers/foo
주의: checkpatch.pl 통과는 시작점일 뿐입니다. 동시성, 에러 처리, ABI 영향 같은 설계 품질은 별도 리뷰 없이는 놓치기 쉽습니다.

참고자료

공식 문서

스타일 검사 도구

커뮤니티 리소스

이전 단계:
다음 단계:
참고: