Rust 커널 프로그래밍 가이드 (4) — 새 모듈을 시작할 때 쓰는 템플릿과 체크리스트

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/modprobeinit()Err를 반환하면 그 즉시 로드를 실패 처리하고 dmesg에 원인이 남으므로, C에서 return -EINVAL; 한 줄로 하던 일과 표현만 다를 뿐 동작은 같다. 언로드 시 정리는 별도 exit 함수 대신 Drop::drop()이 자동으로 호출된다.

Kbuild에 새 샘플 등록하기

커널 트리 안에 새 Rust 샘플을 추가하려면 소스 파일 외에 KconfigMakefile에도 각각 항목을 넣어야 한다. 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.o

tristate로 선언했으므로 이 옵션은 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() 계열은 “이 경우는 논리적으로 절대 실패할 수 없다”고 확신할 때만 제한적으로 써야 한다.
  • 모듈 파라미터 타입은 kernel crate가 지원하는 타입(정수 계열, bool, 문자열 등)으로 제한된다. 커스텀 타입을 파라미터로 받고 싶다면 별도 트레이트 구현이 추가로 필요하다.
  • Kconfig에 옵션만 추가하고 olddefconfig(또는 oldconfig)를 안 돌리면 .config에 반영되지 않은 채로 빌드해 새 샘플이 조용히 빠지는 경우가 있다. scripts/config로 값을 바꾼 뒤에는 항상 grep으로 .config를 재확인하는 습관을 들이는 게 좋다.

마무리

이 템플릿 자체는 짧지만, 1~3부에서 다룬 것들(모듈 등록, Result 기반 에러 처리, 로깅, 정리 로직)이 새 모듈을 시작할 때 최소한 갖춰야 할 형태로 압축되어 있다. 여기에 misc device를 붙이고 싶으면 2부의 MiscDeviceRegistration을, 인터럽트나 공유 상태가 필요하면 3부의 Arc/SpinLock을 얹으면 된다. 새 모듈을 시작할 때는 이 뼈대를 복사한 뒤 module!의 다섯 필드부터 채우고, init()이 실패할 수 있는 지점을 먼저 목록화하는 순서를 권장한다.

참고

답글 남기기