커널 코딩 스타일(Coding Style) 가이드
Linux 커널 코딩 스타일: checkpatch.pl, 들여쓰기, 네이밍, 주석, goto, 매크로(Macro) 작성 규칙 완벽 가이드
핵심 요약
- 자동 검증 — checkpatch.pl로 패치 제출 전 스타일 검사
- 8칸 탭 — 들여쓰기는 8칸 탭, 공백 사용 금지
- 80칸 제한 — 가독성을 위한 줄 길이 제한 (예외 허용)
- 명확한 네이밍 — 함수는 동사, 변수는 명사, 매크로는 대문자
- goto는 도구 — 에러 처리 경로 정리에 적극 사용
단계별 이해
- checkpatch.pl 설치
커널 소스의 scripts/ 디렉토리에 위치, 패치 제출 전 필수 실행 - 기본 규칙 적용
들여쓰기, 중괄호, 공백 등 기계적으로 확인 가능한 규칙부터 학습 - 네이밍 습관화
기존 커널 코드를 읽으며 네이밍 패턴 익히기 - 코드 리뷰 참여
메일링 리스트에서 다른 개발자의 피드백 관찰
Linux 커널 개발에서 일관된 코딩 스타일은 수천 명의 개발자가 협업하는 환경에서 코드 가독성과 유지보수성을 보장하는 핵심입니다. Documentation/process/coding-style.rst 기반으로 checkpatch.pl 사용법, 들여쓰기, 네이밍, 주석, goto, 매크로 등 모든 규칙을 실전 예제와 함께 설명합니다.
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칸까지 허용하는 경향이 있으며, 다음 경우는 예외를 인정합니다:
- 문자열 리터럴 (검색 가능성을 위해 분리하지 않음)
- printk, pr_* 함수의 포맷 문자열
- 함수 선언이 80칸을 살짝 초과하는 경우
/* ✅ 예외: 문자열은 분리하지 않음 */
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);
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;
}
레이블 네이밍
레이블명은 해제할 자원 또는 에러 상황을 명확히 표현합니다:
err_alloc,err_register— 에러 지점 명시out_free,out_unlock— 수행할 작업 명시
매크로 작성 규칙
대문자 사용
매크로는 모두 대문자로 작성하며, 함수처럼 보이는 매크로도 예외가 아닙니다 (일부 기존 코드 제외):
#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 __foobar *foobar_t; - 정수 타입의 명확한 별칭:
typedef u32 dma_addr_t; - 복잡한 함수 포인터:
typedef int (*callback_t)(void *); - 스파스(Sparse) 타입 검사:
typedef __bitwise__ u32 __be32;
금지되는 경우
/* ❌ 잘못된 예: 구조체 숨기기 */
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" |
| 2 | linux/ 핵심 헤더 | #include <linux/module.h> |
| 3 | linux/ 서브시스템 헤더 | #include <linux/netdevice.h> |
| 4 | asm/ 아키텍처 헤더 | #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 */
#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 | -5 | I/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()을 반환하면 호출자가 양쪽 모두 검사해야 합니다.
일관되게 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
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는 메뉴에서 보이는 항목!
코딩 스타일 검증 워크플로우
패치 제출 전까지 거치는 코딩 스타일 검증 과정을 전체적으로 정리합니다. 각 단계에서 확인해야 할 항목과 도구를 시각적으로 보여줍니다.
코딩 스타일 규칙 전체 구조
커널 코딩 스타일의 각 규칙이 어떤 목적과 관계를 가지는지 전체 구조를 한눈에 파악합니다.
메모리 할당 스타일
커널 메모리 할당에는 고유한 스타일 규칙이 있습니다. 잘못된 패턴은 메모리 누수, 버퍼 오버플로(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);
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_* 버전을 사용하세요.
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);
추가 자료
공식 문서
- Documentation/process/coding-style.rst — 코딩 스타일 공식 문서
- Documentation/process/submitting-patches.rst — 패치 제출 가이드
- Documentation/doc-guide/kernel-doc.rst — kernel-doc 주석 형식
검증 도구
scripts/checkpatch.pl— 스타일 검사 스크립트scripts/get_maintainer.pl— 메인테이너 확인scripts/kernel-doc— kernel-doc 주석 검증make coccicheck— Coccinelle 시맨틱 패치 도구
참고 코드
다음 파일들은 모범적인 코딩 스타일을 보여줍니다:
kernel/sched/core.c— 복잡한 로직의 명확한 구조화mm/slab.c— 상세한 주석과 문서화drivers/base/core.c— 디바이스 드라이버 모범 사례lib/list_sort.c— 알고리즘 구현 예제
실무 코드 리뷰 플레이북
스타일 문서를 읽는 것만으로는 품질이 올라가지 않습니다. 리뷰 단계에서 반복 가능한 기준으로 검사해야 실제로 버그와 유지보수 비용을 줄일 수 있습니다.
| 검사 축 | 핵심 질문 | 빠른 점검 방법 |
|---|---|---|
| 가독성 | 함수가 한 번에 이해되는가? | 함수 길이, 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 영향 같은 설계 품질은 별도 리뷰 없이는 놓치기 쉽습니다.
참고자료
공식 문서
- Linux Kernel Coding Style — 공식 코딩 스타일 문서 (들여쓰기, 네이밍, 중괄호, 주석 등)
- Submitting Patches — 패치 형식, 커밋 메시지 규칙, Signed-off-by 태그
- Patch Submission Checklist — 패치 제출 전 확인해야 할 체크리스트
- checkpatch.pl Documentation — 코딩 스타일 자동 검사 도구 공식 가이드
- Deprecated Interfaces — 사용이 금지된 API와 권장 대체 방법 목록
- Maintainer Tip Tree Handbook — tip 트리 메인테이너의 코드 스타일 기준 (엄격한 기준 사례)
- Writing kernel-doc Comments — 함수/구조체 문서화 주석(kernel-doc) 작성 규칙
- Programming Language — 커널이 사용하는 C 표준(C11)과 컴파일러 확장 정책
스타일 검사 도구
- checkpatch.pl (소스) — 코딩 스타일 위반 자동 검출 스크립트
- Sparse — 타입 검사, __user/__iomem 어노테이션, endian 검사
- Coccinelle — 시맨틱 패치 도구 (API 변경 시 코드 일괄 변환)
- clang-tidy — C/C++ 정적 분석 및 스타일 검사 (커널 빌드에도 활용 가능)
- kmemleak — 메모리 누수 검출 (코딩 실수 사전 방지)
- KMSAN — 초기화되지 않은 메모리 사용 검출
커뮤니티 리소스
- Rethinking the kernel coding style (LWN) — 커널 코딩 스타일 변천사와 최신 논의
- Proper use of typedefs (LWN) — typedef 사용 규칙과 커널에서의 관례
- Kernel Janitors (KernelNewbies) — 코딩 스타일 정리 작업에 참여하는 방법
- First Kernel Patch (KernelNewbies) — checkpatch를 활용한 첫 패치 연습
- lore.kernel.org — 메일링 리스트 아카이브에서 스타일 관련 리뷰 코멘트 검색
- Greg KH YouTube — 코딩 스타일과 패치 리뷰 프로세스 강의
관련 문서
- 첫 커널 모듈 튜토리얼 — 모듈 작성 실습
- 커널 소스 읽기 — 소스 탐색 방법
- 커널 개발 환경 설정 — 개발 도구 구성
- 커널 모듈 — 모듈 아키텍처 이해
- 동기화 — 동기화 프리미티브
- 메모리 관리 — 메모리 할당 API
- 개발 도구 — GCC, Clang, 정적 분석, QEMU, 크로스 컴파일(Cross Compilation)
- C 언어 & 커널 C 관용어 — 커널에서 사용하는 C 언어 패턴과 관용구