1~3부에서 최소 모듈, misc device, 인터럽트 동기화를 각각 다뤘는데, 매번 module! 매크로부터 새로 채워나갔다. 실제로 새 모듈을 시작할 때는 그 반대가 편하다 — 어떤 필드가 필수고 어떤 게 선택인지, 초기화 실패는 어떻게 표현하는지, 정리 로직은 어디에 두는지가 정리된 뼈대 하나를 복사해서 채워나가는 쪽이다. 이 글은 그 뼈대를 정리하고, 실제로 커널 소스 트리에 새 샘플로 등록해 컴파일까지 검증한 템플릿을 공유한다.
module! 매크로 — 필수 필드와 선택 필드
type, name, authors, description, license 다섯 개는 항상 채워야 한다. params는 모듈 파라미터가 필요할 때만 추가하는 선택 항목이다. license는 C의 MODULE_LICENSE()와 동일하게 정확한 문자열이어야 하며, "GPL" 계열이 아니면 GPL 전용으로 export된 커널 심볼에 접근할 수 없다 — 오타 하나로 링크 단계에서 막히는 경우가 흔하다.
초기화 실패를 다루는 패턴 — Result<Self>
kernel::Module::init()은 Result<Self>를 반환한다. 파라미터 검증처럼 실패할 수 있는 초기화 로직이 있다면 C에서 -EINVAL을 리턴하던 자리에 커널이 정의한 errno 상수(EINVAL, ENOMEM 등)를 그대로 Err()에 담아 반환하면 된다. 아래는 파라미터 하나를 받아 값을 검증하고, 로그를 남기고, 언로드 시 정리까지 하는 일반적인 형태의 템플릿이다.
// SPDX-License-Identifier: GPL-2.0
//! Rust kernel module template sample.
use kernel::prelude::*;
module! {
type: RustTemplate,
name: "rust_template",
authors: ["Junorion"],
description: "Rust kernel module template",
license: "GPL",
params: {
retries: u32 {
default: 3,
description: "Number of retries (must be > 0)",
},
},
}
struct RustTemplate {
retries: u32,
}
impl kernel::Module for RustTemplate {
fn init(_module: &'static ThisModule) -> Result<Self> {
let retries = *module_parameters::retries.value();
if retries == 0 {
pr_err!("retries must be greater than 0\n");
return Err(EINVAL);
}
pr_info!("Rust template loaded (retries={})\n", retries);
Ok(RustTemplate { retries })
}
}
impl Drop for RustTemplate {
fn drop(&mut self) {
pr_info!("Rust template unloaded (retries was {})\n", self.retries);
}
}retries == 0일 때 pr_err!로 원인을 남기고 Err(EINVAL)을 반환하는 부분이 핵심이다. insmod/modprobe는 init()이 Err를 반환하면 그 즉시 로드를 실패 처리하고 dmesg에 원인이 남으므로, C에서 return -EINVAL; 한 줄로 하던 일과 표현만 다를 뿐 동작은 같다. 언로드 시 정리는 별도 exit 함수 대신 Drop::drop()이 자동으로 호출된다.
Kbuild에 새 샘플 등록하기
커널 트리 안에 새 Rust 샘플을 추가하려면 소스 파일 외에 Kconfig와 Makefile에도 각각 항목을 넣어야 한다. samples/rust/Kconfig에 옵션을 추가한다.
config SAMPLE_RUST_TEMPLATE
tristate "Template sample"
help
This option builds the Rust template sample.
To compile this as a module, choose M here:
the module will be called rust_template.
If unsure, say N.samples/rust/Makefile에는 obj-$(CONFIG_...) 한 줄을 추가해 소스 파일과 Kconfig 옵션을 연결한다.
obj-$(CONFIG_SAMPLE_RUST_TEMPLATE) += rust_template.otristate로 선언했으므로 이 옵션은 y(빌트인)와 m(모듈) 둘 다 가능하다.
실전 빌드로 컴파일 검증
환경 구성 글에서 준비해둔 UBUNTU_TEST_HOST의 커널 트리에 위 세 파일(rust_template.rs, Kconfig, Makefile)을 그대로 적용하고, 옵션을 켠 뒤 실제로 빌드했다.
$ scripts/config --enable SAMPLE_RUST_TEMPLATE
$ make LLVM=1 olddefconfig
$ grep "^CONFIG_SAMPLE_RUST_TEMPLATE" .config
CONFIG_SAMPLE_RUST_TEMPLATE=y
$ time make LLVM=1 -j$(nproc) samples/rust/
RUSTC samples/rust/rust_template.o
AR samples/rust/built-in.a
real 0m8.570s
RUSTC samples/rust/rust_template.o가 에러 없이 찍혔다는 건 파라미터 검증, Err(EINVAL) 반환, pr_err!/pr_info!, Drop 구현까지 이 템플릿 전체가 실제 kernel crate 기준으로 타입 체크를 통과했다는 뜻이다. 기존에 빌드해둔 rust/kernel.o 등을 그대로 재사용하기 때문에 8.5초 만에 끝났다.
주의사항
- 여기서 확인한 건 “커널 소스 트리 안에서 컴파일이 성공한다”는 것까지다. 실제로
insmod으로 로드해보려면 이 소스로 빌드한 커널 이미지로 부팅했거나, 최소한 지금 실행 중인 커널과 버전·설정이 일치하는Module.symvers가 있어야 한다. 이번 테스트 호스트는 우분투 배포 커널(6.17.0-35-generic)로 부팅된 상태라, 방금 clone한 최신 소스 트리로 빌드한 모듈을 그대로insmod하면 버전 불일치로 거부된다 — 컴파일 검증과 로드 검증은 별개의 문제라는 점을 유의해야 한다. - 커널 코드에는 표준 Rust의 panic 핸들러가 없다.
unwrap()/expect()가 패닉을 일으키면 그대로 커널 패닉으로 이어지므로, 실패할 수 있는 경로는 전부Result로 표현하고unwrap()계열은 “이 경우는 논리적으로 절대 실패할 수 없다”고 확신할 때만 제한적으로 써야 한다. - 모듈 파라미터 타입은
kernelcrate가 지원하는 타입(정수 계열,bool, 문자열 등)으로 제한된다. 커스텀 타입을 파라미터로 받고 싶다면 별도 트레이트 구현이 추가로 필요하다. - Kconfig에 옵션만 추가하고
olddefconfig(또는oldconfig)를 안 돌리면.config에 반영되지 않은 채로 빌드해 새 샘플이 조용히 빠지는 경우가 있다.scripts/config로 값을 바꾼 뒤에는 항상grep으로.config를 재확인하는 습관을 들이는 게 좋다.
마무리
이 템플릿 자체는 짧지만, 1~3부에서 다룬 것들(모듈 등록, Result 기반 에러 처리, 로깅, 정리 로직)이 새 모듈을 시작할 때 최소한 갖춰야 할 형태로 압축되어 있다. 여기에 misc device를 붙이고 싶으면 2부의 MiscDeviceRegistration을, 인터럽트나 공유 상태가 필요하면 3부의 Arc/SpinLock을 얹으면 된다. 새 모듈을 시작할 때는 이 뼈대를 복사한 뒤 module!의 다섯 필드부터 채우고, init()이 실패할 수 있는 지점을 먼저 목록화하는 순서를 권장한다.