Linux Kernel 개발 시 고려사항
커널 개발은 기능 구현만으로 끝나지 않습니다. ABI 안정성, 동시성, 메모리 수명, 인터럽트 지연, 관측 가능성, 보안, 업스트림 전략을 동시에 설계해야 실서비스에서 버티는 코드가 됩니다.
커널 개발의 기본 원칙
커널은 사용자 공간 애플리케이션과 다르게 장애가 시스템 전체 중단으로 이어집니다. 따라서 "동작한다"보다 "회귀 없이 오래 유지된다"가 우선입니다.
- 보수적 인터페이스: 사용자 공간에 노출한 인터페이스는 장기 계약이라고 가정
- 실패 경로 우선: 성공 경로보다 실패 경로에서 자원 해제가 정확한지 먼저 검증
- 관측 가능성 내장: tracepoint, 통계, 에러 카운터를 설계 단계부터 포함
- 작은 패치: 기능 추가, 리팩터링, 스타일 변경을 분리해 리뷰 가능성 확보
- 재현성 확보: 재현 스크립트, 커널 설정, 하드웨어 정보를 함께 기록
실행 컨텍스트 이해 (process / softirq / hardirq)
커널 코드는 실행 컨텍스트에 따라 가능한 연산이 다릅니다. 같은 함수라도 어디서 호출되는지에 따라 sleep 가능 여부, 락 종류, 메모리 할당 플래그가 달라집니다.
그림 1: 실행 컨텍스트별 제약과 권장 작업
UAPI/ABI 안정성과 내부 API 진화
커널 내부 API는 버전 업에서 바뀔 수 있지만, 사용자 공간과 맞닿은 UAPI는 최대한 깨지지 않아야 합니다. 특히 ioctl 구조체는 필드 추가 방식, 정렬, 크기 확장 전략을 처음부터 보수적으로 잡아야 합니다.
그림 2: UAPI 안정성과 내부 구현 변경의 경계
/* UAPI 구조체 확장 예시: 기존 ABI를 깨지 않도록 뒤에 필드 추가 */
struct mydev_req_v1 {
__u32 cmd;
__u32 flags;
__u64 user_ptr;
__u32 len;
__u32 reserved;
};
struct mydev_req_v2 {
struct mydev_req_v1 v1;
__u64 timeout_ns;
__u64 feature_mask;
};
자료구조와 캐시 친화적 레이아웃
성능 병목의 상당수는 알고리즘보다 메모리 배치에서 발생합니다. hot path에서 자주 접근하는 필드끼리 묶고, false sharing이 발생하지 않도록 캐시라인 배치를 의식해야 합니다.
- hot/cold 분리: 자주 접근하는 필드와 통계/디버그 필드를 분리
- 캐시라인 정렬: 경합 필드는 cacheline padding으로 분리
- per-cpu 구조: 글로벌 락 경합 대신 per-cpu 카운터 사용
- RCU 읽기 경로: 읽기 다수/쓰기 소수 워크로드에서 효과적
동시성 모델과 락킹 전략
커널 동시성 버그는 재현이 어렵고 피해가 큽니다. 락 타입 선택뿐 아니라 락 순서, 임계구역 길이, 메모리 순서 보장이 함께 맞아야 안전합니다.
그림 3: 락 계층과 데드락 예방 원칙
/* 컨텍스트 제약을 고려한 스핀락 예시 */
unsigned long flags;
spin_lock_irqsave(&q->lock, flags);
/* 공유 큐 업데이트 */
list_add_tail(&req->node, &q->pending);
spin_unlock_irqrestore(&q->lock, flags);
메모리 관리와 객체 수명 주기
커널 메모리 버그는 즉시 크래시로 이어지거나 지연된 use-after-free로 나타납니다. 할당 플래그, 소유권, 참조 카운트, 해제 시점을 구조적으로 설계해야 합니다.
그림 4: 커널 객체 수명 주기와 대표 결함 유형
/* 실패 경로 누수를 줄이는 역순 정리 패턴 */
ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
if (!ctx)
return -ENOMEM;
ret = init_queue(ctx);
if (ret)
goto err_free_ctx;
ret = register_device(ctx);
if (ret)
goto err_cleanup_queue;
return 0;
err_cleanup_queue:
cleanup_queue(ctx);
err_free_ctx:
kfree(ctx);
return ret;
인터럽트, 지연 작업, I/O 경로 분리
IRQ 핸들러에서 무거운 작업을 하면 전체 시스템 지연이 급증합니다. 상단부는 "빠르게 ACK + 최소 상태 기록"만 수행하고, 실질 작업은 threaded IRQ나 workqueue로 내리는 것이 안전합니다.
그림 5: 인터럽트 처리 경량화와 지연 작업 위임
디바이스 드라이버 모델 설계
드라이버는 probe/remove, suspend/resume, reset, hotplug를 모두 고려해야 합니다. 특히 remove 경로는 시스템 종료/장치 분리/오류 복구에서 반복적으로 호출될 수 있으므로 idempotent 설계가 중요합니다.
드라이버 수명 주기
- probe: 자원 할당, IRQ 등록, 큐 초기화, 장치 활성화
- runtime PM: 사용량 기반 전력 상태 전환
- suspend/resume: 상태 저장/복원, 링크 재동기화
- remove: 사용자 진입 차단 후 역순 해제
서브시스템별 설계 포인트 (VFS / Net / Block)
커널은 서브시스템별로 관례가 명확합니다. 같은 기능이라도 서브시스템 스타일에 맞게 구현해야 리뷰/업스트림 채택 가능성이 올라갑니다.
| 서브시스템 | 핵심 고려사항 | 자주 발생하는 실수 |
|---|---|---|
| VFS/파일시스템 | 락 순서, dentry/inode 수명, writeback 일관성 | 경계 조건에서 stale dentry 처리 누락 |
| 네트워크 | NAPI budget, skb 수명, GRO/GSO 영향 | softirq 경로에서 과도한 작업 수행 |
| 블록 I/O | 큐 깊이, merge 정책, 완료 경로 지연 | tail latency 측정 없이 평균값만 최적화 |
| 메모리 관리 | 페이지 수명, reclaim 압력, NUMA locality | 고압 메모리 상황에서 오류 처리 미흡 |
디버깅과 관측 가능성(Observability)
운영 환경에서 문제를 빠르게 좁히려면 "로그 + 트레이스 + 카운터"가 함께 있어야 합니다. 로그만으로는 타이밍/경합 버그를 잡기 어렵습니다.
그림 6: 로그-트레이스-성능 분석을 연결한 관측 파이프라인
- lockdep: 락 순서 위반 탐지
- KASAN/KFENCE: 메모리 오류 탐지
- KCSAN: 데이터 레이스 탐지
- UBSAN: 정의되지 않은 동작 탐지
성능 최적화와 회귀 방지
최적화는 "측정-가설-검증" 반복으로 진행해야 합니다. 평균 성능만 보고 튜닝하면 p99 지연이 악화되는 역효과가 자주 발생합니다.
핵심 지표
- 지연: p50/p95/p99, 최대 지연, 장시간 정지 구간
- 경합: lock contention, runqueue 길이, irq disabled 시간
- 메모리: alloc 실패율, reclaim 빈도, cache miss
- I/O: 큐 깊이, 완료율, tail latency
# 예시 측정 루틴(운영 전후 비교)
perf stat -a -e cycles,instructions,cache-misses,context-switches sleep 30
perf record -a -g -- sleep 20
perf report
trace-cmd record -e sched_switch -e irq_handler_entry -e irq_handler_exit
보안, 권한 경계, 하드닝
커널 결함은 권한 상승으로 이어질 가능성이 높습니다. 입력 검증, 권한 모델, 공격 표면 축소를 설계 단계에서 반영해야 합니다.
- 입력 검증: 길이/오프셋/정렬/정수 오버플로우 검증
- 권한 제어: capability + namespace + LSM 영향 점검
- 노출 최소화: debugfs는 운영 의존 경로로 사용하지 않기
- 정보 유출 방지: 초기화되지 않은 메모리 노출 차단
- 하드닝 옵션: stack protector, FORTIFY, CFI 적용 검토
신뢰성, 장애 복구, 운영 고려사항
실무에서는 버그를 "완전히 예방"하기보다 "빠르게 감지하고 제한된 범위에서 복구"하는 설계가 중요합니다.
- 에러 분류: 일시 오류와 영구 오류를 구분해 재시도 정책 차등 적용
- 복구 경로: reset/reinit 경로를 정상 경로만큼 자주 테스트
- 격리: 기능 단위로 fail-stop이 가능하도록 상태머신 분리
- 운영 가이드: 장애 시 수집해야 할 로그/트레이스/덤프 체크리스트 문서화
테스트 전략과 자동화
커널 테스트는 단일 도구로 충분하지 않습니다. 정적 분석, 단위 테스트, 통합 테스트, 실기기 검증을 계층적으로 결합해야 회귀를 줄일 수 있습니다.
| 단계 | 도구 | 목적 |
|---|---|---|
| 정적 분석 | sparse, smatch, Coccinelle | API 오용, 패턴 결함 조기 탐지 |
| 단위 테스트 | KUnit | 상태머신/유틸리티 로직 검증 |
| 기능 테스트 | kselftest, LTP | 시스템 인터페이스 회귀 점검 |
| 가상화 테스트 | QEMU/KVM | 재현 가능한 자동 회귀 테스트 |
| 실기기 검증 | HW farm, 부팅/전력/IO 시나리오 | 실환경 타이밍/전원 이벤트 확인 |
# 예시: 기본 검증 루틴
make olddefconfig
make -j$(nproc)
make -j$(nproc) kselftest
./tools/testing/kunit/kunit.py run
./run-ltp-smoke.sh
업스트림 제출과 장기 유지보수
사내 포크만 유지하면 시간이 갈수록 리베이스 비용이 폭증합니다. 가능한 한 초기에 업스트림 규칙을 따라 개발하고, 리뷰 피드백을 빠르게 반영하는 루프를 구축해야 합니다.
- 패치 분할: 리팩터링/버그 수정/신기능을 분리
- 커밋 메시지: 문제 배경, 재현 방법, 설계 대안 비교, 테스트 근거 명시
- 리뷰 대응: v2/v3에서 변경점 요약(changelog) 제공
- 백포트: LTS 대상으로 위험도/의존성 기준을 문서화
- 문서 동기화: 코드와 문서(UAPI 설명, 운영 가이드) 동시 업데이트
아키텍처별 체크포인트 (x86_64 / arm64)
아키텍처마다 메모리 모델, 인터럽트, 캐시 일관성, IOMMU 설정이 다르므로 동일 코드라도 동작 차이가 나타날 수 있습니다.
- x86_64: 강한 메모리 모델 가정에 의존하지 않도록 주의
- arm64: 약한 메모리 모델에서 배리어 누락 시 레이스 노출 가능
- IOMMU: DMA 매핑/언매핑 순서와 스트리밍 버퍼 일관성 점검
- 엔디안/정렬: UAPI 구조체 정렬과 패딩 의도 확인
실무 종합 체크리스트
| 영역 | 핵심 질문 |
|---|---|
| 실행 컨텍스트 | 현재 경로에서 sleep 가능 여부를 정확히 알고 있는가? |
| UAPI/ABI | 기존 사용자 공간 동작을 깨지 않고 확장 가능한가? |
| 동시성 | 락 순서가 문서화되어 있고 lockdep으로 검증되는가? |
| 메모리 수명 | 실패 경로 누수/이중 해제/use-after-free 위험이 없는가? |
| 인터럽트 | top-half는 최소화되고 무거운 처리는 지연 경로로 이동했는가? |
| 관측 | 운영 중 문제를 tracepoint/perf/eBPF로 좁힐 수 있는가? |
| 성능 | 평균뿐 아니라 p99 지연과 경합 지표를 비교했는가? |
| 보안 | 입력 검증, 권한 경계, 정보 유출 방어가 반영되었는가? |
| 업스트림 | 패치 분할/커밋 메시지/테스트 근거가 리뷰 가능한 수준인가? |
실전 적용 로드맵 (팀 기준)
- 1주차: 실행 컨텍스트/락 순서 문서화 + 최소 tracepoint 설계
- 2주차: 실패 경로 정리, 자원 해제 역순 패턴 통일, KASAN 테스트
- 3주차: kselftest/KUnit 기본 파이프라인 구축, 성능 기준선 수집
- 4주차: 운영 장애 대응 플레이북(수집/분석/복구) 정리
- 이후: 패치마다 "기능 + 계측 + 테스트 + 문서" 4종 세트 유지