Rust 커널 프로그래밍 가이드
Linux 커널 Rust 프로그래밍 심층 가이드: Rust for Linux 아키텍처, bindgen/Kbuild 통합, 안전성 모델(safe/unsafe 경계), 커널 추상화 계층, misc/platform/PCI/네트워크 드라이버 작성, Workqueue/Timer/UserSlice API, KUnit 테스팅, GDB 디버깅, C→Rust 마이그레이션 전략까지 실무 관점으로 다룹니다.
Rust in Kernel 개요
Linux 6.1(2022년 12월)부터 Rust가 커널 개발의 제2 언어로 공식 지원됩니다. Linus Torvalds는 "Rust가 커널에 가져올 메모리 안전성 이점은 충분히 가치 있다"고 평가했습니다. Rust의 소유권 시스템과 타입 안전성은 safe Rust 경계에서 use-after-free, 버퍼 오버플로, 데이터 레이스 같은 메모리 안전 버그를 크게 줄이며, unsafe/FFI 경계는 별도 검증이 필요합니다.
커널 Rust 프로젝트의 핵심 원칙은 "기존 C 코드를 깨뜨리지 않으면서, 새로운 코드에서 점진적으로 메모리 안전한 Rust를 도입한다"는 것입니다. 이는 30년 이상의 C 코드베이스를 가진 Linux 커널에서 현실적이면서도 효과적인 전략입니다.
| 측면 | 설명 |
|---|---|
| 목표 | 새로운 드라이버·모듈을 Rust로 작성하여 메모리 안전성 향상 |
| 전략 | 기존 C 코드를 대체하지 않고, 새로운 코드에서 점진적으로 Rust 도입 |
| 구조 | C API → unsafe Rust 래퍼 → safe Rust 드라이버 3계층 |
| 도구 | rustc (LLVM 백엔드), bindgen (C→Rust 바인딩), Kbuild 통합 |
| 제약 | no_std, 커널 전용 alloc, panic 금지, nightly 컴파일러 필요 |
| 커뮤니티 | Rust for Linux 팀 주도, Miguel Ojeda 메인테이너, LKML을 통한 리뷰 |
| 소스 위치 | rust/ (인프라), rust/kernel/ (추상화), samples/rust/ (예제) |
커널 소스 트리에서 Rust 코드의 위치
| 경로 | 내용 | 역할 |
|---|---|---|
rust/kernel/ | Rust 커널 크레이트 | safe 추상화 계층 (Mutex, Arc, Vec, Error 등) |
rust/bindings/ | bindgen 출력 | C 헤더 → Rust FFI 바인딩 자동 생성 |
rust/helpers/ | C 헬퍼 함수 | 인라인 C 함수를 Rust에서 호출 가능하게 래핑 |
rust/macros/ | 절차적 매크로 | module!, #[vtable], #[pin_data] 등 |
samples/rust/ | Rust 예제 모듈 | 학습 및 빌드 검증용 |
drivers/*/xxx_rust.rs | 실제 Rust 드라이버 | 프로덕션 드라이버 코드 |
참고 자료: 공식 문서는 docs.kernel.org/rust, 프로젝트 홈페이지는 rust-for-linux.com, 소스 저장소는 GitHub Rust-for-Linux/linux를 참조하세요.
왜 커널에 Rust인가
공개된 대규모 소프트웨어 보안 보고서에서 메모리 안전 버그(use-after-free, 버퍼 오버플로, 이중 해제 등)의 비중이 약 70%로 보고됩니다. C 언어는 이러한 버그를 컴파일 타임에 일반적으로 감지하기 어렵지만, Rust는 소유권(ownership) 시스템을 통해 많은 클래스를 컴파일 시점에 차단합니다.
| 조직/프로젝트 | 메모리 안전 버그 비율 | 출처/연도 |
|---|---|---|
| Microsoft | CVE의 ~70%가 메모리 안전 이슈 | MSRC 블루핫 발표, 2019 |
| Google Chrome | 고위험 버그의 ~70%가 메모리 안전 | Chromium 보안 팀, 2020 |
| Android | 취약점의 ~65%가 메모리 안전 | Android 보안 보고서, 2022 |
| Linux 커널 | CVE의 상당 부분이 UAF/오버플로 | 커널 보안 메일링 리스트 |
| Apple iOS/macOS | 0-day의 다수가 메모리 관련 | Project Zero 분석 |
Rust 안전성 보장 원리
| 원리 | 커널에서의 의미 | 방지하는 C 버그 |
|---|---|---|
| 소유권(Ownership) | 모든 커널 객체는 정확히 하나의 소유자가 있으며, 소유자가 스코프를 벗어나면 자동 해제 | 메모리 누수, 이중 해제 |
| 빌림(Borrowing) | 공유 참조(&T)는 여러 개 가능하지만 수정 불가, 가변 참조(&mut T)는 독점적 | Use-After-Free, 데이터 레이스 |
| 수명(Lifetime) | 참조가 가리키는 데이터보다 오래 살 수 없음을 컴파일러가 보증 | 댕글링 포인터 |
| 타입 시스템 | Send/Sync 트레이트로 스레드 안전성 강제, Result로 에러 전파 강제 | 잘못된 에러 코드 경로, 동기화 누락 |
// 개념 예시: C에서 흔한 Use-After-Free — Rust에서는 컴파일 에러
fn dangling_example() {
let reference;
{
let data = Vec::new();
reference = &data; // ← 컴파일 에러! data보다 reference가 오래 삶
}
// data는 여기서 해제됨 — reference를 사용하면 UAF
// Rust 컴파일러가 차단
}
C-Rust FFI 경계 원리
커널 Rust 코드의 핵심 설계 원칙은 "unsafe 경계를 최소화하고, 안전한 추상화 뒤에 숨긴다"는 것입니다:
커널 Rust의 제약: 표준 라이브러리(std)를 사용할 수 없고, no_std + 커널 전용 alloc 크레이트를 사용합니다. 힙 할당은 실패 가능하므로 try_ 접두사 API를 사용하며(Vec::try_push()), panic은 커널 oops로 이어지므로 반드시 Result로 에러를 전파해야 합니다.
커널 Rust 아키텍처 전체 구조
커널 Rust 코드의 전체 아키텍처는 여러 계층으로 구성됩니다. 각 계층은 명확한 역할을 가지며, unsafe 경계를 최소화합니다.
| 계층 | 안전성 수준 | 역할 | 파일 위치 |
|---|---|---|---|
| Rust 드라이버 | Safe Rust (99%) | 실제 디바이스 드라이버 구현 | drivers/*/*.rs |
| 커널 크레이트 | Safe API + 내부 unsafe | C API의 safe Rust 래퍼 제공 | rust/kernel/*.rs |
| macros 크레이트 | proc_macro (빌드 타임) | module!, #[vtable], pin_init! 등 | rust/macros/ |
| alloc 크레이트 | 커널 전용 (try_ API) | Box, Vec, String (할당 실패 처리) | rust/alloc/ |
| bindings | 전부 unsafe | bindgen이 생성한 C FFI 바인딩 | rust/bindings/ |
| helpers | C 헬퍼 (unsafe) | 인라인 C 함수 래핑 (bindgen 한계 극복) | rust/helpers/ |
이 아키텍처의 핵심 원칙은 "unsafe 경계를 최소화"하는 것입니다. bindings 계층에서 발생하는 unsafe는 커널 크레이트가 safe API로 감싸고, 드라이버 개발자는 대부분 safe Rust만 사용합니다. unsafe 코드의 정확성은 커널 크레이트 개발자가 // SAFETY: 주석으로 문서화하고, 커뮤니티가 집중 리뷰합니다.
Rust 커널 개발 환경 설정
커널 Rust 빌드를 위해서는 정확한 버전의 도구 체인이 필요합니다. 커널은 특정 rustc/bindgen 버전을 요구하며, 버전 불일치는 빌드 실패의 가장 흔한 원인입니다.
필수 도구 체인
| 도구 | 용도 | 버전 확인 | 설치 방법 |
|---|---|---|---|
rustc | Rust 컴파일러 | scripts/min-tool-version.sh rustc | rustup으로 설치 |
rust-src | core/alloc 소스 | — | rustup component add rust-src |
bindgen | C→Rust 바인딩 생성 | scripts/min-tool-version.sh bindgen | cargo install |
clang | LLVM 컴파일러 | clang --version | 패키지 관리자 |
lld | LLVM 링커 | — | 패키지 관리자 |
clippy (권장) | Rust 린트 도구 | — | rustup component add clippy |
# === 1단계: Rust 툴체인 설치 ===
# rustup이 없다면 먼저 설치
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 커널이 요구하는 정확한 rustc 버전으로 설정
# (커널 소스 디렉터리에서 실행)
rustup override set $(scripts/min-tool-version.sh rustc)
# 필수 컴포넌트 설치
rustup component add rust-src # core/alloc 소스 (커널 빌드 필수!)
rustup component add clippy rustfmt # 린트/포맷 도구 (권장)
# === 2단계: bindgen 설치 ===
cargo install --locked --version $(scripts/min-tool-version.sh bindgen) bindgen-cli
# === 3단계: LLVM/Clang 설치 ===
# Debian/Ubuntu
apt install llvm clang lld
# Fedora/RHEL
dnf install llvm clang lld
# Arch Linux
pacman -S llvm clang lld
# === 4단계: 환경 검증 ===
make LLVM=1 rustavailable
# 성공: "Rust is available!"
# 실패 시 누락된 도구와 필요 버전이 표시됨
# === 5단계: Kconfig에서 Rust 활성화 ===
make LLVM=1 menuconfig
# General setup → Rust support → [Y]
# (또는) 직접 .config에 추가:
# CONFIG_RUST=y
# === 6단계: 빌드 ===
make LLVM=1 -j$(nproc)
# === 선택: Rust 문서 생성 ===
make LLVM=1 rustdoc
# 결과: Documentation/output/rust/kernel/index.html
트러블슈팅
| 증상 | 원인 | 해결 |
|---|---|---|
rustc not found | PATH에 rustup 미등록 | source $HOME/.cargo/env |
Rust is NOT available | rustc/bindgen 버전 불일치 | scripts/min-tool-version.sh로 필요 버전 확인 후 재설치 |
error: component 'rust-src' is not available | nightly에 rust-src 미포함 | 다른 nightly 날짜로 전환 |
| 빌드 중 bindgen 에러 | bindgen/clang 버전 불일치 | bindgen --version 확인, 정확한 버전 재설치 |
| 링크 에러 | lld 미설치 | apt install lld |
버전 일치 필수: 커널은 특정 rustc/bindgen 버전을 요구합니다. scripts/min-tool-version.sh rustc와 scripts/min-tool-version.sh bindgen으로 확인하세요. 최신 stable은 사용 불가이며, 커널이 지정한 nightly 버전을 정확히 사용해야 합니다.
IDE 지원: make LLVM=1 rust-analyzer 명령으로 rust-project.json을 생성하면, VS Code나 CLion에서 rust-analyzer가 커널 Rust 코드의 자동완성, 타입 추론, 에러 표시를 지원합니다.
Rust 커널 모듈 예제
// 실습 예제: 최소 Rust 커널 모듈
// SPDX-License-Identifier: GPL-2.0
use kernel::prelude::*;
module! {
type: MyModule,
name: "my_rust_module",
author: "Developer",
description: "A simple Rust kernel module",
license: "GPL",
}
struct MyModule {
number: i32,
}
impl kernel::Module for MyModule {
fn init(_module: &'static ThisModule) -> Result<Self> {
pr_info!("Rust module loaded!\\n");
Ok(MyModule { number: 42 })
}
}
impl Drop for MyModule {
fn drop(&mut self) {
pr_info!("Rust module unloaded (number={})\\n", self.number);
}
}
module! 매크로는 절차적 매크로로, C의 module_init()/module_exit()에 해당하는 코드를 자동 생성합니다. Drop 트레이트 구현이 module_exit 정리 함수 역할을 하며, 모듈 언로드 시 RAII 패턴으로 자원이 자동 해제됩니다.
C 모듈 vs Rust 모듈 비교
| 요소 | C 모듈 | Rust 모듈 |
|---|---|---|
| 진입점 | module_init(fn) | module! { ... } + impl Module |
| 종료점 | module_exit(fn) | impl Drop |
| 에러 반환 | return -ENOMEM; (잊으면 조용히 진행) | Result 필수 (컴파일러가 강제) |
| 자원 정리 | 수동 goto err_cleanup | RAII 자동 정리 (Drop) |
| 라이선스 | MODULE_LICENSE("GPL") | license: "GPL" |
모듈용 Kconfig / Makefile
# drivers/my_driver/Kconfig
config MY_RUST_DRIVER
tristate "My Rust Driver"
depends on RUST
help
A sample Rust kernel driver.
# drivers/my_driver/Makefile
obj-$(CONFIG_MY_RUST_DRIVER) += my_rust_driver.o
# 모듈 빌드 및 테스트
make LLVM=1 M=drivers/my_driver -j$(nproc)
# 모듈 적재
insmod drivers/my_driver/my_rust_driver.ko
dmesg | tail -3 # "Rust module loaded!" 확인
# 모듈 제거
rmmod my_rust_driver
dmesg | tail -3 # "Rust module unloaded" 확인
빌드 검증 실습 세트
아래 순서는 실제로 빌드 성공/실패를 빠르게 확인할 수 있는 최소 실습 경로입니다.
# 실습 1: 툴체인/환경 검증
make LLVM=1 rustavailable
# 실습 2: 현재 커널 트리에서 Rust 샘플 심볼 탐색
grep -R "config SAMPLE_RUST" -n samples/rust Kconfig* */Kconfig
# 실습 3: menuconfig에서 Rust + Rust samples 활성화
make LLVM=1 menuconfig
# 실습 4: Rust 샘플만 선택 빌드
make LLVM=1 M=samples/rust -j$(nproc)
# 실습 5: 결과 확인
find samples/rust -name "*.o" -o -name "*.ko"
| 검증 단계 | 성공 기준 | 실패 시 점검 포인트 |
|---|---|---|
rustavailable | Rust/LLVM 도구체인 요구사항 충족 | scripts/min-tool-version.sh 출력 버전과 실제 설치 버전 비교 |
| Rust 샘플 빌드 | samples/rust 오브젝트 생성 | 비활성화된 Kconfig 심볼, 누락된 rust-src, bindgen 버전 불일치 |
| 모듈 적재 테스트 | insmod/rmmod 후 커널 로그 정상 | 커널/모듈 버전 불일치, 서명 정책(Secure Boot) 확인 |
QEMU로 빠르게 테스트하기
# QEMU에서 Rust 모듈 테스트 (전체 워크플로)
# 1. 최소 Kconfig 생성 (x86_64)
make LLVM=1 x86_64_defconfig
./scripts/config --enable RUST
./scripts/config --enable SAMPLE_RUST_MINIMAL
./scripts/config --enable MODULES
./scripts/config --enable BLK_DEV_INITRD
make LLVM=1 olddefconfig
# 2. 커널 + 모듈 빌드
make LLVM=1 -j$(nproc)
# 3. QEMU로 부팅 (initramfs가 준비되어 있다면)
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd rootfs.cpio.gz \
-append "console=ttyS0" \
-nographic \
-m 512M
# 4. QEMU 내에서 Rust 샘플 모듈 테스트
# insmod rust_minimal.ko
# dmesg | grep -i rust
# rmmod rust_minimal
빠른 피드백 루프: 전체 커널을 재빌드하지 않고 Rust 모듈만 빠르게 수정/빌드하려면 make LLVM=1 M=samples/rust -j$(nproc)으로 모듈만 빌드한 후, QEMU에서 insmod/rmmod로 테스트하세요. 모듈 수정 → 빌드 → 테스트 사이클이 수 초로 단축됩니다.
Kbuild 통합과 빌드 과정
Rust 코드는 Kbuild 시스템에 완전히 통합됩니다. Makefile에서 Rust 소스를 지정하면 rustc가 자동으로 호출됩니다.
# samples/rust/Makefile
obj-$(CONFIG_SAMPLE_RUST_MINIMAL) += rust_minimal.o
obj-$(CONFIG_SAMPLE_RUST_PRINT) += rust_print.o
# 커널 전체 빌드 시 Rust 코드 포함
make LLVM=1 -j$(nproc)
Kbuild Rust 빌드 타겟
| 명령어 | 설명 | 출력 |
|---|---|---|
make LLVM=1 rustavailable | Rust 툴체인 사용 가능 여부 확인 | Rust is available! 또는 에러 |
make LLVM=1 rustfmt | 커널 Rust 코드 포맷팅 | rustfmt 적용 |
make LLVM=1 rustdoc | 커널 Rust API 문서 생성 | Documentation/output/rust/ |
make LLVM=1 rust-analyzer | rust-analyzer 설정 생성 | rust-project.json |
make LLVM=1 clippy | 커널 Rust 코드 린트 | 경고/제안 출력 |
make LLVM=1 M=samples/rust | Rust 예제 모듈만 빌드 | *.ko 파일 |
# Rust 지원 활성화 전체 워크플로
rustup override set $(scripts/min-tool-version.sh rustc)
rustup component add rust-src
make LLVM=1 rustavailable # 툴체인 확인
make LLVM=1 menuconfig # General setup → Rust support [Y]
make LLVM=1 -j$(nproc) # 전체 빌드
make LLVM=1 rust-analyzer # IDE 설정 생성
LLVM 필수: 커널 Rust 빌드는 반드시 LLVM=1 플래그가 필요합니다. GCC 백엔드로는 Rust 코드를 컴파일할 수 없습니다. LLVM 버전은 scripts/min-tool-version.sh llvm에서 최소 요구 버전을 확인하세요. rustc와 Clang이 동일한 LLVM 버전을 사용해야 링크 호환성이 보장됩니다.
bindgen과 C 바인딩 생성
bindgen은 C 헤더 파일에서 Rust FFI 바인딩을 자동 생성합니다. 커널 빌드 시 rust/bindings/bindings_generated.rs가 생성되며, 모든 함수가 unsafe입니다.
// rust/bindings/lib.rs — 자동 생성된 바인딩 모듈
#![allow(non_camel_case_types)]
#![allow(non_upper_case_globals)]
mod bindings_raw {
// bindgen이 C 헤더에서 자동 생성
include!(concat!(env!("OBJTREE"), "/rust/bindings/bindings_generated.rs"));
}
pub use bindings_raw::*;
// 바인딩 사용 예시
unsafe {
// SAFETY: GFP_KERNEL은 프로세스 컨텍스트에서 안전하게 호출 가능
let ptr = bindings::kmalloc(64, bindings::GFP_KERNEL);
if !ptr.is_null() {
bindings::kfree(ptr);
}
}
커널 Rust 추상화 계층
커널 Rust 코드는 C API를 직접 호출하지 않고, 안전한 Rust 래퍼(abstraction)를 통해 접근합니다. 이 추상화 계층이 커널 Rust의 핵심 가치입니다.
추상화 작성 패턴 (C API → Safe Rust)
// 추상화 작성의 3단계 패턴
// 1단계: bindgen이 생성한 unsafe 바인딩
// extern "C" { fn mutex_lock(lock: *mut mutex); }
// extern "C" { fn mutex_unlock(lock: *mut mutex); }
// 2단계: safe 래퍼 작성 (rust/kernel/sync/mutex.rs)
pub struct Mutex<T> {
inner: Opaque<bindings::mutex>, // C struct을 불투명하게 래핑
data: UnsafeCell<T>, // 내부 가변성
}
// SAFETY: Mutex로 보호되므로 Send + Sync
unsafe impl<T: Send> Send for Mutex<T> {}
unsafe impl<T: Send> Sync for Mutex<T> {}
impl<T> Mutex<T> {
pub fn lock(&self) -> Guard<Self> {
// SAFETY: mutex가 init()으로 초기화된 상태이고,
// Guard가 Drop될 때 unlock이 보장됩니다.
unsafe { bindings::mutex_lock(self.inner.get()) }
Guard::new(self) // Guard가 unlock 책임을 가짐
}
}
// 3단계: 드라이버에서 safe하게 사용
let mut guard = my_mutex.lock(); // 잠금 획득
*guard += 1; // 보호된 데이터 수정
// guard가 Drop → mutex_unlock() 자동 호출
안전성 모델
커널 Rust의 안전성 모델은 "unsafe를 최소한의 경계 계층에 집중하고, 나머지는 safe Rust로 보장한다"는 원칙에 기반합니다. 이 모델은 전체 커널 코드를 한 번에 검증하는 대신, unsafe 블록만 집중적으로 리뷰하여 검증 가능한 범위를 크게 줄입니다.
안전성 계층 구조
| 계층 | 안전성 수준 | 코드 위치 | 검증 방법 |
|---|---|---|---|
| Safe Rust 드라이버 | 컴파일러가 보증 | drivers/xxx/src/ | 컴파일 통과 = 메모리 안전 |
| Rust 추상화 계층 | SAFETY 주석 + 리뷰 | rust/kernel/ | 수동 리뷰 + SAFETY 근거 |
| bindgen 바인딩 | 자동 생성 (unsafe) | rust/bindings/ | C API 계약 준수 검증 |
| C 커널 API | C 규칙 (수동 관리) | include/, kernel/ | 전통적 코드 리뷰 |
// === Safe Rust 패턴: 컴파일러가 안전성 보증 ===
fn safe_example(data: &[u8]) -> Result {
let mutex = Mutex::new(Vec::new());
let mut guard = mutex.lock(); // RAII 잠금
guard.try_push(data[0])?;
// guard가 스코프를 벗어나면 자동으로 unlock
// → C에서 흔한 "lock 후 unlock 누락" 버그 불가능
Ok(())
}
// === Unsafe Rust: C 함수 호출 시 필요 ===
// 모든 unsafe 블록에는 반드시 SAFETY 주석을 작성해야 합니다.
// 이 주석은 "왜 이 코드가 안전한지"를 명확히 설명합니다.
// SAFETY: ptr은 probe()에서 kmalloc으로 할당된 유효한 메모리를 가리키며,
// 이 함수의 호출 시점에서 ptr에 대한 다른 참조가 없으므로
// 데이터 레이스가 발생하지 않습니다.
unsafe { bindings::some_c_function(ptr) };
// === SAFETY 주석 작성 가이드 ===
// 나쁜 예: "// SAFETY: 안전합니다" ← 근거 없음!
// 좋은 예:
// SAFETY: 다음 조건들이 충족됩니다:
// 1. `ptr`은 `Box::into_raw()`에서 얻은 유효한 포인터입니다.
// 2. `ptr`은 정확히 한 번만 `Box::from_raw()`로 변환됩니다.
// 3. `ptr`이 가리키는 메모리는 이 시점에서 다른 참조가 없습니다.
let obj = unsafe { Box::from_raw(ptr) };
안전 불변조건(Safety Invariant) 패턴
// 커널 Rust에서 자주 사용되는 패턴:
// 구조체 수준의 안전 불변조건을 문서화하고 강제합니다.
/// MMIO 디바이스 레지스터 접근 래퍼.
///
/// # Invariants
///
/// - `base`는 유효하게 매핑된 MMIO 영역의 시작 주소입니다.
/// - `size`는 매핑된 영역의 크기이며, 모든 접근은 이 범위 내에서 이루어집니다.
/// - 이 구조체가 존재하는 동안 MMIO 매핑은 해제되지 않습니다.
struct MmioRegion {
base: *mut u8,
size: usize,
}
impl MmioRegion {
// safe 메서드: 범위 검사를 통해 안전성 보장
fn read32(&self, offset: usize) -> Result<u32> {
if offset + 4 > self.size {
return Err(code::EINVAL); // 범위 초과 방지
}
// SAFETY: offset이 범위 내임을 위에서 검증했고,
// base는 불변조건에 의해 유효한 MMIO 주소입니다.
Ok(unsafe { core::ptr::read_volatile(self.base.add(offset) as *const u32) })
}
}
SAFETY 주석 규칙: 커널 Rust 코딩 가이드라인에 따라, 모든 unsafe 블록에는 // SAFETY: 주석으로 안전성 근거를 명시해야 합니다. 이는 커널 코딩 규칙이며, 누락 시 패치 리뷰에서 거부됩니다. 구조체에는 /// # Invariants 섹션으로 안전 불변조건을 문서화합니다.
unsafe ≠ 위험: unsafe는 "이 코드의 안전성을 프로그래머가 보증한다"는 의미이지, "이 코드가 위험하다"는 뜻이 아닙니다. 올바르게 작성된 unsafe 코드는 safe 코드와 동일하게 안전합니다. 핵심은 SAFETY 주석을 통해 그 근거를 투명하게 드러내는 것입니다.
Pin과 커널 초기화 패턴
커널 Rust에서는 Pin<T>을 활용한 in-place 초기화가 핵심 패턴입니다. C 커널의 많은 구조체(mutex, list_head, timer_list 등)는 초기화 후 메모리 주소가 고정되어야 하는 자기 참조 구조체입니다. Rust에서 이를 안전하게 다루기 위해 pin_init! 매크로와 PinInit 트레이트가 도입되었습니다.
왜 Pin 초기화가 필요한가?
| 문제 | C에서의 처리 | Rust Pin 초기화 |
|---|---|---|
| mutex는 초기화 후 이동 불가 | mutex_init() 후 이동하지 않는 것은 프로그래머 책임 | Pin<T>이 이동을 타입 수준에서 방지 |
| list_head는 자기 참조 | INIT_LIST_HEAD() 후 이동하면 댕글링 | #[pin] 필드는 이동 불가 보장 |
| 이동 시 메모리 주소 변경 | 프로그래머가 주의해야 함 | 컴파일러가 이동 시도를 차단 |
// === Pin 기반 초기화 패턴 ===
use kernel::sync::{new_mutex, Mutex};
use kernel::init::PinInit;
// #[pin_data]: 이 구조체에 Pin 필드가 있음을 선언
#[pin_data]
struct SharedState {
#[pin]
data: Mutex<Vec<u8>>, // Pin 필드: 이동 불가
name: CString, // 일반 필드: 이동 가능
count: u32, // 일반 필드
}
impl SharedState {
// PinInit을 반환하는 생성자 — 아직 초기화되지 않은 "계획"을 반환
fn new(name: &CStr) -> impl PinInit<Self> {
pin_init!(Self {
// <- 연산자: Pin 필드의 in-place 초기화
data <- new_mutex!(Vec::new(), "SharedState::data"),
// : 연산자: 일반 필드 초기화 (이동 가능)
name: CString::try_from_fmt(fmt!("{}", name))?,
count: 0,
})
}
}
// === 다양한 할당 방식 ===
// 1. Box::pin_init() — 힙 할당 + Pin 초기화
let state = Box::pin_init(SharedState::new(c_str!("example")))?;
// 2. Arc::pin_init() — 공유 힙 할당 + Pin 초기화
let shared = Arc::pin_init(SharedState::new(c_str!("shared")))?;
// 3. UniqueArc::pin_init() — 단독 소유 후 Arc로 변환
let unique = UniqueArc::pin_init(SharedState::new(c_str!("unique")))?;
let shared: Arc<SharedState> = unique.into();
pin_init! 매크로 문법
// pin_init! 매크로의 두 가지 초기화 연산자:
//
// field: value — 일반 초기화 (값을 이동하여 대입)
// field <- init_expr — in-place 초기화 (Pin 필드용)
// init_expr은 PinInit 트레이트를 구현해야 함
//
// ? 사용 가능: 초기화 실패 시 에러 전파
#[pin_data]
struct DriverState {
#[pin]
lock: Mutex<DriverInner>,
#[pin]
timer: Timer<Self>,
#[pin]
work: Work<Self>,
irq: u32,
name: CString,
}
impl DriverState {
fn new(irq: u32, name: &CStr) -> impl PinInit<Self> {
pin_init!(Self {
lock <- new_mutex!(DriverInner::default(), "DriverState::lock"),
timer <- Timer::new(),
work <- Work::new(),
irq, // 동명 축약
name: CString::try_from_fmt(fmt!("{}", name))?, // ? 에러 전파
})
}
}
init vs pin_init: init! 매크로는 Pin이 필요 없는 일반 in-place 초기화에 사용합니다. pin_init!은 Pin 필드가 있는 구조체에 사용합니다. 두 매크로 모두 fallible(실패 가능)하며, ?로 에러를 전파할 수 있습니다. 커널에서는 메모리 할당이 실패할 수 있으므로, 이 fallible 특성이 필수적입니다.
에러 처리 (kernel::error)
커널 Rust는 C의 -ENOMEM, -EINVAL 등의 errno 패턴을 Result<T, Error>로 래핑합니다. C 반환값과 자동 변환되며, ? 연산자를 통해 에러가 호출 체인을 따라 자동 전파됩니다.
C vs Rust 에러 처리 비교
// C: 에러 처리를 잊으면 조용히 진행됨 (컴파일러가 강제하지 않음)
int probe(struct platform_device *pdev) {
struct resource *res;
void __iomem *base;
int irq;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
// res가 NULL인지 확인하지 않으면? → NULL 역참조 → 커널 oops
base = devm_ioremap_resource(&pdev->dev, res);
// IS_ERR(base) 확인을 빼먹으면? → 잘못된 포인터 사용
irq = platform_get_irq(pdev, 0);
// irq < 0 확인을 빼먹으면? → 잘못된 IRQ 번호 사용
return 0;
}
// Rust: Result를 처리하지 않으면 컴파일 에러
fn probe(dev: &mut platform::Device) -> Result<Self::Data> {
let res = dev.get_resource(0).ok_or(code::ENXIO)?; // None → ENXIO
let base = res.ioremap()?; // 실패 시 자동 전파
let irq = dev.get_irq(0).ok_or(code::ENXIO)?; // None → ENXIO
// ? 연산자: 에러가 있으면 즉시 Err를 반환
// 에러 처리를 빼먹을 수 없음!
Ok(Box::try_new(DeviceData { base, irq })?)
}
kernel::error API 상세
// 개념 예시: kernel::error 기반 errno 매핑
use kernel::error::{Error, Result, code};
fn allocate_resource(size: usize) -> Result<Vec<u8>> {
if size == 0 {
return Err(code::EINVAL); // -EINVAL에 대응
}
if size > 1024 * 1024 {
return Err(code::ENOMEM); // 1MB 초과 시 거부
}
let mut v = Vec::new();
v.try_reserve(size).map_err(|_| code::ENOMEM)?; // 할당 실패 변환
Ok(v)
}
// 에러 체이닝: 여러 fallible 연산 순차 실행
fn complex_init() -> Result {
let buf = allocate_resource(4096)?; // 실패 시 즉시 반환
let dev = find_device()?; // 실패 시 즉시 반환
let irq = request_irq(&dev)?; // 실패 시 즉시 반환
// 모든 것이 성공해야 여기 도달
Ok(())
}
// C에서 Rust 함수 호출 시 자동 변환:
// Ok(()) → 0, Ok(42) → 42, Err(EINVAL) → -EINVAL
| C errno | Rust code | 의미 | 대표적 사용 상황 |
|---|---|---|---|
-ENOMEM | code::ENOMEM | 메모리 부족 | kmalloc/Vec 할당 실패 |
-EINVAL | code::EINVAL | 잘못된 인자 | 파라미터 검증 실패 |
-ENOENT | code::ENOENT | 엔트리 없음 | 파일/디바이스 미발견 |
-EBUSY | code::EBUSY | 리소스 사용중 | 디바이스 이미 열림 |
-EAGAIN | code::EAGAIN | 다시 시도 | 논블로킹 I/O 시 데이터 없음 |
-EPERM | code::EPERM | 권한 없음 | CAP 검사 실패 |
-ENODEV | code::ENODEV | 디바이스 없음 | DT 노드 미발견 |
-ENXIO | code::ENXIO | 디바이스/주소 없음 | 리소스/IRQ 미발견 |
-EFAULT | code::EFAULT | 잘못된 주소 | copy_from_user 실패 |
-ETIMEDOUT | code::ETIMEDOUT | 타임아웃 | 하드웨어 응답 대기 초과 |
-ENOTTY | code::ENOTTY | 부적절한 ioctl | 지원하지 않는 ioctl 명령 |
Result vs panic: 커널에서는 unwrap()이나 expect()로 인한 panic이 커널 oops를 유발합니다. 따라서 항상 ? 연산자와 Result를 사용하여 에러를 전파해야 합니다. 유일한 예외는 "이 상황이 발생하면 프로그램 로직 자체가 잘못된 것"이라고 확신할 때의 assert! 사용입니다.
Arc와 참조 카운팅
커널에서 여러 컨텍스트(스레드, 인터럽트 핸들러, 워크큐 등)에서 공유해야 하는 객체는 Arc<T>(Atomic Reference Counted)로 래핑합니다. C의 refcount_t + kref를 Rust 타입 시스템으로 안전하게 추상화한 것입니다.
| C 패턴 | Rust Arc 대응 | 안전성 개선 |
|---|---|---|
refcount_inc() | arc.clone() | overflow 검사 포함, 0→1 전환 방지 |
refcount_dec_and_test() | 자동 (Drop 시 감소) | 감소/해제 원자적 보장 |
kref_put(release_fn) | 자동 Drop 호출 | release 함수 호출 누락 불가 |
kref_get() / put() 쌍 | clone() / drop 쌍 | RAII로 쌍 불일치 방지 |
// 개념 예시: Arc와 ArcBorrow 사용 패턴
use kernel::sync::{Arc, ArcBorrow};
#[pin_data]
struct SharedResource {
#[pin]
data: Mutex<Vec<u8>>,
id: u32,
}
// === Arc 생성 (pin_init과 함께) ===
let resource = Arc::pin_init(pin_init!(SharedResource {
data <- new_mutex!(Vec::new(), "SharedResource::data"),
id: 1,
}))?;
// 이 시점에서 refcount = 1
// === clone(): 참조 카운트만 증가 (데이터 복사 없음) ===
let resource_clone = resource.clone();
// refcount = 2
// === ArcBorrow: 참조 카운트 증가 없이 임시 참조 ===
// C의 "빌려온 참조" 개념 — 참조 카운트 조작 오버헤드 없음
fn use_resource(res: ArcBorrow<'_, SharedResource>) {
pr_info!("Using resource ID {}\\n", res.id);
// ArcBorrow의 수명이 원본 Arc보다 짧음을 컴파일러가 보증
}
// === 콜백에서 Arc 사용 패턴 ===
// 타이머나 워크큐 콜백에 Arc를 전달하면
// 콜백 실행 중에도 객체가 살아있음을 보장
fn schedule_work(resource: &Arc<SharedResource>) {
let cloned = resource.clone(); // refcount 증가
// cloned를 워크큐/타이머에 전달
// 콜백 완료 후 cloned가 drop되면 refcount 감소
}
// === Drop: 자동 자원 해제 ===
drop(resource_clone); // refcount = 1
drop(resource); // refcount = 0 → Drop::drop() 호출 → 메모리 해제
// C에서 kref_put() 호출을 잊어서 발생하는 메모리 누수 원천 차단
커널 Arc vs 표준 라이브러리 Arc: 커널의 kernel::sync::Arc는 std::sync::Arc와 유사하지만 차이가 있습니다. (1) new() 대신 try_new() / pin_init()로 생성 (할당 실패 처리), (2) ArcBorrow 타입으로 참조 카운트 없는 빌림 지원, (3) Weak 참조는 현재 미지원 (향후 추가 예정). 표준 라이브러리의 Arc는 커널에서 사용할 수 없습니다.
Rust misc 디바이스 드라이버
Misc(miscellaneous) 디바이스 드라이버는 커널에서 가장 간단한 문자 디바이스 모델로, /dev/에 자동으로 디바이스 노드가 생성됩니다. 주 번호(major number) 10을 공유하며, 부 번호(minor number)만 다릅니다. Rust misc 드라이버는 file::Operations 트레이트를 구현하고, #[vtable] 매크로가 C의 file_operations vtable을 자동 생성합니다.
| C 구조체/함수 | Rust 대응 | 역할 |
|---|---|---|
struct miscdevice | miscdev::Registration<T> | 디바이스 등록/해제 (RAII) |
struct file_operations | #[vtable] impl file::Operations | open/read/write/ioctl 핸들러 |
misc_register() | Registration::new_pinned() | 디바이스 등록 |
misc_deregister() | Drop 자동 호출 | 모듈 해제 시 자동 등록 해제 |
container_of() | Arc<DeviceState> 공유 상태 | 파일 컨텍스트에서 디바이스 상태 접근 |
// 개념 예시: Rust misc 디바이스 드라이버 패턴
use kernel::prelude::*;
use kernel::{miscdev, file};
use kernel::sync::{Arc, Mutex, new_mutex};
module! {
type: RustMiscDev,
name: "rust_miscdev",
author: "Developer",
description: "Rust misc device driver example",
license: "GPL",
}
#[pin_data]
struct DeviceState {
#[pin]
counter: Mutex<u64>,
}
#[vtable]
impl file::Operations for DeviceState {
type Data = Arc<DeviceState>;
fn open(shared: &Arc<DeviceState>, _file: &file::File) -> Result {
let mut cnt = shared.counter.lock();
*cnt += 1;
pr_info!("Device opened {} times\\n", *cnt);
Ok(())
}
fn read(shared: &Arc<DeviceState>, _file: &file::File,
data: &mut impl file::IoBufferWriter, _offset: u64) -> Result<usize> {
let cnt = shared.counter.lock();
let msg = format!("count: {}\\n", *cnt);
data.write_slice(msg.as_bytes())?;
Ok(msg.len())
}
}
struct RustMiscDev {
_dev: Pin<Box<miscdev::Registration<DeviceState>>>,
}
impl kernel::Module for RustMiscDev {
fn init(_module: &'static ThisModule) -> Result<Self> {
let state = Arc::pin_init(pin_init!(DeviceState {
counter <- new_mutex!(0u64, "DeviceState::counter"),
}))?;
let reg = miscdev::Registration::new_pinned(
fmt!("rust_misc"), state)?;
Ok(RustMiscDev { _dev: reg })
}
}
misc 드라이버 동작 흐름
# 모듈 빌드 (커널 소스 트리에서)
make LLVM=1 M=samples/rust modules
# 모듈 로드
sudo insmod samples/rust/rust_miscdev.ko
dmesg | tail -3 # "rust_miscdev: loaded" 확인
# 디바이스 노드 확인
ls -la /dev/rust_misc # crw------- 1 root root 10, 120 ...
# 디바이스 읽기
sudo cat /dev/rust_misc # "count: 1" 출력
# 모듈 제거 (Drop → misc_deregister 자동 호출)
sudo rmmod rust_miscdev
RAII 패턴: C에서는 module_exit()에서 명시적으로 misc_deregister()를 호출해야 합니다. Rust에서는 Registration의 Drop 구현이 자동으로 호출합니다. 에러 경로에서도 자원이 반드시 해제되므로, 자원 누수 버그가 원천 차단됩니다.
Platform 드라이버 예제
Platform 드라이버는 SoC 내장 디바이스(UART, SPI, I2C, GPIO 등)를 위한 드라이버 모델입니다. Device Tree와 연동되어 하드웨어 리소스(메모리 영역, IRQ, 클록)를 자동으로 바인딩합니다. C의 platform_driver_register() 패턴을 Rust의 트레이트와 매크로로 안전하게 표현합니다.
| C 구조체/함수 | Rust 대응 | 역할 |
|---|---|---|
struct platform_driver | trait platform::Driver | 드라이버 인터페이스 |
.probe() | fn probe() | 디바이스 발견 시 초기화 |
.remove() | fn remove() / Drop | 디바이스 제거 시 정리 |
platform_get_resource() | dev.get_resource() | 메모리 리소스 획득 |
platform_get_irq() | dev.get_irq() | IRQ 번호 획득 |
of_match_table[] | of_match_table: [...] | Device Tree 호환 문자열 |
// 개념 예시: platform 드라이버 전체 구조
use kernel::{prelude::*, platform, of, io_mem::Resource};
use core::sync::atomic::{AtomicBool, Ordering};
// 디바이스 데이터 — probe()에서 생성, 드라이버 수명 동안 유지
struct DeviceData {
mem: IoMem, // 매핑된 MMIO 영역
irq: u32, // IRQ 번호
enabled: AtomicBool, // 디바이스 활성 상태
}
struct MyPlatformDriver;
impl platform::Driver for MyPlatformDriver {
type Data = Box<DeviceData>;
fn probe(dev: &mut platform::Device, _id: Option<&Self::IdInfo>)
-> Result<Self::Data>
{
pr_info!("Probing device: {}\\n", dev.name()?);
// 1. Device Tree 노드 확인
let _node = dev.of_node().ok_or(code::ENODEV)?;
// 2. 메모리 리소스 획득 및 매핑
let res = dev.get_resource(0).ok_or(code::ENXIO)?;
let mem = res.ioremap()?; // MMIO 영역 매핑
// 3. IRQ 획득
let irq = dev.get_irq(0).ok_or(code::ENXIO)?;
// 4. 하드웨어 초기화
// SAFETY: mem은 방금 매핑한 유효한 MMIO 영역
unsafe { mem.writel(0x01, 0x00) }; // 디바이스 활성화
pr_info!("Device probed: IRQ={}, mem={:#x}\\n", irq, res.start());
// 5. 디바이스 데이터 생성 (힙 할당)
Ok(Box::try_new(DeviceData {
mem,
irq,
enabled: AtomicBool::new(true),
})?)
}
fn remove(data: &Self::Data) {
// 디바이스 비활성화
data.enabled.store(false, Ordering::Relaxed);
// SAFETY: probe()에서 매핑한 유효한 MMIO
unsafe { data.mem.writel(0x00, 0x00) }; // 디바이스 비활성화
pr_info!("Device removed\\n");
// Box<DeviceData>가 drop되면서 메모리 자동 해제
}
}
// 모듈 등록 매크로
kernel::module_platform_driver! {
type: MyPlatformDriver,
name: "my_platform_drv",
author: "Developer",
license: "GPL",
of_match_table: [
// Device Tree의 compatible 문자열과 매칭
of::DeviceId::new(c_str!("vendor,my-device")),
of::DeviceId::new(c_str!("vendor,my-device-v2")),
],
}
대응하는 Device Tree 바인딩
// Device Tree 예시 (.dts)
my_device@10000000 {
compatible = "vendor,my-device";
reg = <0x10000000 0x1000>; // MMIO 영역 (base, size)
interrupts = <0 42 4>; // SPI, IRQ 42, level high
clocks = <&clk_apb>; // 클록 소스
status = "okay"; // 디바이스 활성화
};
RAII 리소스 관리: Rust platform 드라이버에서는 probe()에서 획득한 리소스(메모리 매핑, IRQ 등)가 Data 타입에 의해 소유됩니다. 드라이버가 제거될 때 Drop이 자동 호출되어 리소스가 해제되므로, C에서 흔한 remove() 함수의 리소스 해제 누락 버그를 방지합니다.
PCI 드라이버 (Rust)
PCI 디바이스를 Rust로 제어하는 패턴입니다. BAR(Base Address Register) 매핑, 인터럽트 등록, DMA 설정 등 PCI 드라이버의 핵심 요소를 다룹니다.
// 개념 예시: Rust PCI 드라이버 구조
use kernel::{prelude::*, pci};
struct MyPciDriver;
kernel::pci_device_table!(
MY_PCI_TABLE,
MODULE_PCI_TABLE,
<MyPciDriver as pci::Driver>::IdInfo,
[
(pci::DeviceId::new(0x1234, 0x5678), ()), // vendor:device ID
]
);
impl pci::Driver for MyPciDriver {
type IdInfo = ();
fn probe(
dev: &mut pci::Device,
_id: &pci::DeviceId,
_info: &Self::IdInfo,
) -> Result<Box<Self>> {
dev.enable_device()?;
dev.set_master();
// BAR 0 매핑
let bar = dev.iomap_region(0, c_str!("my_pci_drv"))?;
pr_info!("PCI device probed: {:04x}:{:04x}\\n",
dev.vendor_id(), dev.device_id());
Ok(Box::try_new(MyPciDriver)?)
}
}
| C PCI API | Rust 대응 | Rust 이점 |
|---|---|---|
pci_enable_device() | dev.enable_device()? | 에러 반환을 ? 로 자동 전파 |
pci_set_master() | dev.set_master() | DMA 버스 마스터 활성화 |
pci_iomap() | dev.iomap_region()? | RAII — Drop 시 자동 unmap |
pci_disable_device() | Drop 자동 호출 | remove() 구현 불필요 |
MODULE_DEVICE_TABLE(pci, ...) | pci_device_table! | 타입 안전한 ID 테이블 |
RAII와 PCI remove(): C PCI 드라이버에서 remove() 콜백은 pci_iounmap(), pci_release_regions(), pci_disable_device() 등을 역순으로 호출해야 합니다. Rust에서는 Box<MyPciDriver>가 Drop될 때 모든 RAII 래퍼가 자동으로 역순 해제되므로, 별도의 cleanup 코드가 필요 없습니다.
네트워크 드라이버 (Rust)
Rust로 네트워크 디바이스 드라이버를 작성하는 패턴입니다. PHY(Physical Layer) 드라이버가 Rust 네트워크 드라이버의 초기 진입점이며, 실제로 ax88796b PHY 드라이버가 Rust로 커널에 머지된 첫 사례입니다. PHY 드라이버는 비교적 단순한 구조이면서도 네트워크 스택의 핵심 계층에 해당하여, Rust 드라이버의 실용성을 증명하는 중요한 이정표입니다.
| 네트워크 드라이버 계층 | Rust 지원 상태 | 주요 API |
|---|---|---|
| PHY 드라이버 | 안정적 (6.6+) | kernel::net::phy |
| NIC 드라이버 | 개발 중 | kernel::net::dev (진행 중) |
| 소켓/프로토콜 | 미지원 | 향후 계획 |
// 실제 사례 기반: Rust PHY 드라이버 (ax88796b)
// 이 드라이버는 Linux 6.6에서 머지된 최초의 Rust 네트워크 드라이버입니다.
use kernel::{prelude::*, net::phy};
kernel::module_phy_driver! {
drivers: [PhyAX88796B],
device_table: [
phy::DeviceId::new_with_driver::<PhyAX88796B>(),
],
name: "ax88796b_rust",
author: "FUJITA Tomonori",
description: "Rust Asix PHY driver",
license: "GPL",
}
struct PhyAX88796B;
#[vtable]
impl phy::Driver for PhyAX88796B {
// PHY 디바이스 이름과 ID
const NAME: &CStr = c_str!("Asix Electronics AX88796B");
const PHY_DEVICE_ID: phy::DeviceId =
phy::DeviceId::new_with_exact_mask(0x003b1841);
// 소프트 리셋 — genphy 기본 구현 위임
fn soft_reset(dev: &mut phy::Device) -> Result {
dev.genphy_soft_reset()
}
// 링크 상태 읽기
fn read_status(dev: &mut phy::Device) -> Result {
dev.genphy_read_status()
}
}
PHY 드라이버 API 상세
// PHY 드라이버가 구현할 수 있는 주요 콜백들
#[vtable]
impl phy::Driver for MyPhyDriver {
const NAME: &CStr = c_str!("My Custom PHY");
const PHY_DEVICE_ID: phy::DeviceId =
phy::DeviceId::new_with_exact_mask(0xDEADBEEF);
// 초기화 — 디바이스 발견 시 호출
fn config_init(dev: &mut phy::Device) -> Result {
pr_info!("PHY config init\\n");
// 레지스터 초기화
dev.write(0x1F, 0x0000)?; // 페이지 0 선택
dev.write(0x00, 0x1140)?; // 기본 설정
Ok(())
}
// 소프트 리셋
fn soft_reset(dev: &mut phy::Device) -> Result {
// BMCR 레지스터의 리셋 비트 설정
dev.write(0x00, 0x8000)?;
// 리셋 완료 대기 (비트가 자동으로 클리어됨)
let mut retries = 100;
while retries > 0 {
let val = dev.read(0x00)?;
if val & 0x8000 == 0 {
return Ok(());
}
retries -= 1;
}
Err(code::ETIMEDOUT)
}
// 링크 상태 읽기
fn read_status(dev: &mut phy::Device) -> Result {
dev.genphy_read_status()?;
// 추가 상태 정보 읽기 (벤더 특화)
let ext_status = dev.read(0x1B)?;
if ext_status & 0x0100 != 0 {
pr_info!("Link partner supports EEE\\n");
}
Ok(())
}
// 서스펜드/리줌 — 전력 관리
fn suspend(dev: &mut phy::Device) -> Result {
dev.genphy_suspend()
}
fn resume(dev: &mut phy::Device) -> Result {
dev.genphy_resume()
}
}
PHY 레지스터 접근: dev.read(reg)/dev.write(reg, val)은 내부적으로 MDIO 버스를 통해 PHY 칩의 레지스터에 접근합니다. 이 함수들은 safe Rust로 래핑되어 있으며, 버스 잠금과 타이밍을 자동으로 처리합니다. 레지스터 주소 0x00~0x1F는 IEEE 802.3 표준으로 정의된 공통 레지스터이고, 0x10 이상은 벤더별 확장입니다.
Workqueue 사용
Workqueue는 프로세스 컨텍스트에서 비동기 작업을 실행하는 메커니즘입니다. 인터럽트 핸들러나 타이머 콜백에서 슬립이 필요한 작업(메모리 할당, mutex 잠금, I/O 등)을 수행할 때 사용합니다. C의 INIT_WORK()/schedule_work()에 대응합니다.
| Workqueue 종류 | C API | Rust 추상화 | 특징 |
|---|---|---|---|
| 시스템 WQ | system_wq | workqueue::system() | 범용, 공유 워커 풀 |
| 고우선순위 WQ | system_highpri_wq | workqueue::system_highpri() | 높은 우선순위 작업 |
| 정렬 WQ | alloc_ordered_workqueue() | 커스텀 생성 | 순서 보장 (직렬 실행) |
// 개념 예시: Workqueue 패턴
use kernel::workqueue::{self, Work, WorkItem};
use kernel::sync::Arc;
use core::sync::atomic::{AtomicU64, Ordering};
#[pin_data]
struct MyWorkItem {
#[pin]
work: Work<MyWorkItem>, // Work 구조체 (자기참조 → Pin 필수)
counter: AtomicU64, // 실행 카운터
message: CString, // 작업 관련 데이터
}
impl WorkItem for MyWorkItem {
type Pointer = Arc<Self>; // 소유권 관리 포인터
fn run(this: Arc<Self>) {
// 프로세스 컨텍스트에서 실행됨
// → 슬립, mutex, 메모리 할당 모두 가능
let val = this.counter.fetch_add(1, Ordering::Relaxed);
pr_info!("Work executed (counter={}): {}\\n", val, &*this.message);
// 슬립 가능한 작업 수행 예시
// let mut guard = some_mutex.lock(); // OK!
// let buf = Vec::try_with_capacity(1024)?; // OK!
}
}
// === 워크 생성 및 스케줄 ===
let item = Arc::pin_init(pin_init!(MyWorkItem {
work <- Work::new(),
counter: AtomicU64::new(0),
message: CString::try_from_fmt(fmt!("batch job"))?,
}))?;
// 시스템 workqueue에 작업 등록
workqueue::system().enqueue(item.clone());
// === 인터럽트 핸들러에서 하단부(bottom half) 위임 패턴 ===
// 인터럽트 핸들러 (하드 IRQ):
// 1. 하드웨어 상태 읽기 (빠른 작업)
// 2. workqueue에 처리 작업 등록
// 3. IRQ 해제
// Workqueue 콜백 (프로세스 컨텍스트):
// 1. 데이터 처리, 메모리 할당
// 2. 사용자 공간 통지
타이머 vs Workqueue: 타이머 콜백은 softirq 컨텍스트에서 실행되어 빠르지만 슬립 불가. Workqueue는 프로세스 컨텍스트에서 실행되어 느리지만 슬립 가능. 인터럽트 핸들러에서 시간이 오래 걸리는 작업은 Workqueue로 위임(bottom-half 패턴)하세요.
타이머 사용
커널 Rust에서 타이머는 지연된 작업 실행에 사용됩니다. C의 timer_list와 hrtimer에 대응하는 Rust 추상화가 제공되며, TimerCallback 트레이트를 구현하여 콜백 동작을 정의합니다. 타이머 구조체는 자기 참조이므로 Pin으로 고정해야 합니다.
| 타이머 종류 | C API | Rust 추상화 | 해상도 | 용도 |
|---|---|---|---|---|
| 일반 타이머 | timer_list | kernel::time::Timer | jiffies 단위 (보통 1~10ms) | 주기적 점검, 타임아웃 |
| 고해상도 타이머 | hrtimer | kernel::time::hrtimer (6.15+) | 나노초 | 정밀 타이밍, 주기적 인터럽트 |
| 지연 작업 | delayed_work | Workqueue + 타이머 조합 | jiffies | 지연된 프로세스 컨텍스트 작업 |
// 개념 예시: Timer 패턴 — 주기적 타이머
use kernel::time::{Timer, TimerCallback, Jiffies};
use kernel::sync::Arc;
use core::sync::atomic::{AtomicU32, Ordering};
#[pin_data]
struct TimerData {
#[pin]
timer: Timer<Self>, // 타이머 구조체 (자기참조 → Pin 필수)
count: AtomicU32, // 발동 횟수 카운터
max_count: u32, // 최대 실행 횟수 (0이면 무한)
}
impl TimerCallback for TimerData {
type Pointer = Arc<Self>; // 콜백에서 사용할 포인터 타입
fn run(this: Arc<Self>) {
let cnt = this.count.fetch_add(1, Ordering::Relaxed);
pr_info!("Timer fired {} times\\n", cnt + 1);
// max_count가 0이면 무한 반복, 아니면 횟수 제한
if this.max_count == 0 || cnt + 1 < this.max_count {
// 1초 후 다시 스케줄 (주기적 타이머)
this.timer.schedule(Jiffies::from_secs(1));
} else {
pr_info!("Timer stopped after {} executions\\n", cnt + 1);
}
}
}
// 타이머 생성 및 시작
let timer_data = Arc::pin_init(pin_init!(TimerData {
timer <- Timer::new(),
count: AtomicU32::new(0),
max_count: 10, // 10번 발동 후 자동 중지
}))?;
// 최초 스케줄: 500ms 후 첫 실행
timer_data.timer.schedule(Jiffies::from_millis(500));
// 타이머 취소 (필요 시)
// timer_data.timer.cancel();
// Arc가 drop되면 타이머도 자동 정리 (RAII)
타이머 콜백은 인터럽트 컨텍스트(softirq)에서 실행됩니다. 따라서 콜백 내에서 슬립이 가능한 연산(mutex 잠금, 메모리 할당 등)을 수행할 수 없습니다. 슬립이 필요한 작업은 Workqueue와 조합하여 프로세스 컨텍스트에서 실행해야 합니다.
RAII와 타이머: Rust의 Drop 트레이트 덕분에, 타이머를 소유한 구조체가 해제될 때 타이머도 자동으로 취소됩니다. C에서는 del_timer_sync()를 명시적으로 호출해야 했지만, Rust에서는 이를 잊을 수 없습니다.
커널 실행 컨텍스트 비교
| 컨텍스트 | 메커니즘 | 슬립 가능 | 선점 가능 | 사용 API |
|---|---|---|---|---|
| 하드 IRQ | 인터럽트 핸들러 | ❌ | ❌ | SpinLock, atomic ops |
| Softirq/Tasklet | 타이머 콜백, 네트워크 RX | ❌ | ❌ | SpinLock, atomic ops |
| Workqueue | WorkItem::run() | ✅ | ✅ | Mutex, Vec::try_push, I/O |
| 프로세스 | 시스템 콜, ioctl | ✅ | ✅ | 모든 safe API |
| kthread | 커널 쓰레드 | ✅ | ✅ | 모든 safe API |
UserSlice: 사용자 공간 통신
커널과 사용자 공간 사이의 데이터 전송은 UserSlice를 통해 안전하게 수행됩니다. C의 copy_from_user()/copy_to_user()에 대응하며, Rust 타입 시스템으로 방향성(읽기/쓰기)과 경계를 강제합니다.
| C API | Rust 추상화 | 방향 | 설명 |
|---|---|---|---|
copy_to_user() | IoBufferWriter::write_slice() | 커널 → 사용자 | 커널 데이터를 사용자 버퍼에 복사 |
copy_from_user() | IoBufferReader::read_slice() | 사용자 → 커널 | 사용자 데이터를 커널 버퍼로 복사 |
get_user() | UserSliceReader::read() | 사용자 → 커널 | 단일 값 읽기 |
put_user() | UserSliceWriter::write() | 커널 → 사용자 | 단일 값 쓰기 |
// 개념 예시: UserSlice 기반 사용자 공간 I/O 패턴
use kernel::user_ptr::{UserSlice, UserSliceReader, UserSliceWriter};
// === 파일 read 구현 (커널 → 사용자) ===
fn read(_data: &Self::Data, _file: &File,
data: &mut impl IoBufferWriter, offset: u64
) -> Result<usize> {
// offset != 0이면 이미 전체를 읽은 것이므로 EOF 반환
if offset != 0 { return Ok(0); }
let msg = b"Hello from Rust kernel!\\n";
// 사용자 버퍼가 충분한지 확인
if data.len() < msg.len() {
return Err(code::EINVAL);
}
// 커널 데이터를 사용자 공간으로 안전하게 복사
// 내부적으로 copy_to_user()를 호출하며,
// 잘못된 사용자 주소 시 -EFAULT를 반환
data.write_slice(msg)?;
Ok(msg.len())
}
// === 파일 write 구현 (사용자 → 커널) ===
fn write(shared: &Self::Data, _file: &File,
data: &mut impl IoBufferReader, _offset: u64
) -> Result<usize> {
let len = data.len();
// 최대 크기 제한 (커널 메모리 과도 사용 방지)
if len > 4096 {
return Err(code::EINVAL);
}
// 커널 버퍼 할당 (실패 가능 → try_resize)
let mut buffer = Vec::new();
buffer.try_resize(len, 0u8)?;
// 사용자 데이터를 커널 버퍼로 안전하게 복사
data.read_slice(&mut buffer)?;
pr_info!("Received {} bytes from user: {:?}\\n", len,
&buffer[..core::cmp::min(len, 32)]); // 최대 32바이트만 출력
Ok(len)
}
// === ioctl에서 UserSlice 직접 사용 ===
fn ioctl(shared: &Self::Data, _file: &File,
cmd: u32, arg: usize
) -> Result<i32> {
match cmd {
0x1001 => {
// 사용자 포인터에서 구조체 읽기
let slice = UserSlice::new(arg, core::mem::size_of::<MyIoctlData>());
let mut reader = slice.reader();
let mut data = MyIoctlData::default();
reader.read_raw(&mut data)?;
// ... data 처리 ...
Ok(0)
},
_ => Err(code::ENOTTY),
}
}
사용자 포인터 안전성: 사용자 공간 포인터는 항상 신뢰할 수 없습니다. Rust의 UserSlice 추상화는 (1) 잘못된 주소 시 -EFAULT 반환, (2) 경계를 초과한 접근 방지, (3) 방향성(읽기/쓰기) 타입 레벨 강제를 보장합니다. 절대로 사용자 포인터를 raw pointer로 직접 역참조하지 마세요.
Custom Trait 구현
커널 Rust 드라이버에서 Custom Trait은 하드웨어 추상화 인터페이스를 정의하는 핵심 패턴입니다. C 커널의 struct file_operations, struct platform_driver 같은 vtable 패턴을 Rust의 트레이트로 표현하면 타입 안전한 다형성을 얻을 수 있습니다.
| C 커널 패턴 | Rust 트레이트 패턴 | 장점 |
|---|---|---|
struct file_operations | trait FileOperations | 미구현 함수 포인터 NULL 접근 방지 |
struct platform_driver | trait platform::Driver | 필수 메서드 구현 강제 |
| 함수 포인터 콜백 | 트레이트 메서드 + #[vtable] | 컴파일 타임 타입 검사 |
// 개념 예시: 디바이스 공통 인터페이스 trait
trait HardwareDevice {
// 필수 메서드 — 구현체가 반드시 제공해야 함
fn init(&mut self) -> Result;
fn read_register(&self, offset: u32) -> Result<u32>;
fn write_register(&mut self, offset: u32, value: u32) -> Result;
// 연관 상수 — 디바이스별 메타데이터
const VENDOR_ID: u32;
const DEVICE_NAME: &'static str;
// 기본 구현 — 오버라이드 가능
fn reset(&mut self) -> Result {
self.write_register(0x0, 0x1)?;
pr_info!("Device {} reset\\n", Self::DEVICE_NAME);
Ok(())
}
// 기본 구현: 상태 덤프
fn dump_status(&self) -> Result {
let status = self.read_register(0x0)?;
pr_info!("[{}] status: {:#x}\\n", Self::DEVICE_NAME, status);
Ok(())
}
}
// UART 디바이스 구현
struct UartDevice { base_addr: usize }
impl HardwareDevice for UartDevice {
const VENDOR_ID: u32 = 0x1234;
const DEVICE_NAME: &'static str = "UART-16550";
fn init(&mut self) -> Result {
// UART 초기화: FIFO 활성화 + 8N1 설정
self.write_register(0x10, 0x8)?; // FCR: FIFO Enable
self.write_register(0x14, 0x3)?; // LCR: 8 data bits
Ok(())
}
fn read_register(&self, offset: u32) -> Result<u32> {
// SAFETY: base_addr은 probe()에서 검증된 유효한 MMIO 주소이며,
// 이 참조가 유효한 동안 base_addr 영역은 매핑 상태를 유지합니다.
unsafe {
Ok(kernel::io::readl((self.base_addr + offset as usize) as *const u32))
}
}
fn write_register(&mut self, offset: u32, value: u32) -> Result {
// SAFETY: 위와 동일한 근거
unsafe {
kernel::io::writel(value, (self.base_addr + offset as usize) as *mut u32);
}
Ok(())
}
}
// SPI 디바이스 — 같은 trait, 다른 구현
struct SpiDevice { base_addr: usize, cs_pin: u8 }
impl HardwareDevice for SpiDevice {
const VENDOR_ID: u32 = 0x5678;
const DEVICE_NAME: &'static str = "SPI-Controller";
fn init(&mut self) -> Result {
self.write_register(0x00, 0x01)?; // SPI Enable
self.write_register(0x04, 0x07)?; // Clock divider
Ok(())
}
// read_register, write_register 구현 생략 (유사 패턴)
fn read_register(&self, offset: u32) -> Result<u32> { /* ... */ Ok(0) }
fn write_register(&mut self, _offset: u32, _value: u32) -> Result { Ok(()) }
// reset을 오버라이드 — SPI는 CS 핀도 리셋해야 함
fn reset(&mut self) -> Result {
self.write_register(0x0, 0x1)?;
pr_info!("SPI CS pin {} deasserted\\n", self.cs_pin);
Ok(())
}
}
// 트레이트 객체 사용 — 런타임 다형성
fn initialize_devices(devices: &mut [&mut dyn HardwareDevice]) -> Result {
for dev in devices.iter_mut() {
dev.init()?;
dev.dump_status()?;
}
Ok(())
}
#[vtable] 매크로 상세
커널의 #[vtable] 절차적 매크로는 Rust 트레이트를 C의 vtable(함수 포인터 구조체)로 자동 변환합니다. 이 매크로가 생성하는 코드를 이해하면 커널 Rust의 C FFI 브릿지를 더 잘 파악할 수 있습니다.
// #[vtable] 매크로가 하는 일:
// 1. 트레이트의 각 메서드에 대응하는 C 함수 포인터를 생성
// 2. 선택적 메서드(기본 구현)는 HAS_xxx 상수로 존재 여부 추적
// 3. NULL 대신 기본 구현을 C 측에 전달
// Rust 측: 트레이트 정의
#[vtable]
trait FileOperations {
// 필수 메서드: HAS_OPEN = true (항상)
fn open(&self) -> Result;
// 선택적 메서드: HAS_READ = 구현 여부에 따라 true/false
fn read(&self, _buf: &mut [u8]) -> Result<usize> {
Err(code::EINVAL) // 기본 구현
}
}
// 매크로가 자동 생성하는 코드 (개념):
// impl<T: FileOperations> T {
// const HAS_OPEN: bool = true;
// const HAS_READ: bool = /* 구현체가 read를 오버라이드했는지 */;
// }
//
// C 측 vtable 구성:
// struct file_operations {
// .open = Some(rust_open_wrapper),
// .read = if T::HAS_READ { Some(rust_read_wrapper) } else { None },
// }
| #[vtable] 기능 | 설명 | C 대응 |
|---|---|---|
| 필수 메서드 | 미구현 시 컴파일 에러 | NULL이면 런타임 에러 |
| 선택적 메서드 | 기본 구현으로 안전한 폴백 | NULL 검사 필요 |
| HAS_xxx 상수 | C 측에서 함수 존재 여부 확인 | 수동 NULL 비교 |
| 타입 검사 | 시그니처 불일치 → 컴파일 에러 | 잘못된 포인터 할당 가능 |
#[vtable]의 핵심 가치: C에서 file_operations.read = NULL이면 커널이 -EINVAL을 반환하는 것이 기본 동작이지만, 이 동작은 호출 시점에서만 확인됩니다. Rust의 #[vtable]은 미구현 필수 메서드를 컴파일 타임에 잡아내고, 선택적 메서드는 안전한 기본 구현을 자동 제공합니다.
커널 Rust 테스팅 (KUnit, rustdoc)
커널 Rust 코드는 세 가지 레벨의 테스트를 지원합니다: KUnit 단위 테스트(커널 내부 실행), rustdoc 문서 테스트(문서 내 코드 검증), 통합 테스트(모듈 로드/언로드). 각 테스트 방법의 특성과 적합한 사용 시나리오를 이해하면 효과적인 테스트 전략을 수립할 수 있습니다.
| 테스트 방법 | 실행 환경 | 대상 | 속도 | 커버리지 |
|---|---|---|---|---|
| KUnit | 커널 내부 (부팅 시 또는 UML) | 내부 로직, 유닛 테스트 | 빠름 | 개별 함수/모듈 |
| rustdoc | 호스트 (문서 빌드 시) | API 사용 예제 검증 | 매우 빠름 | 공개 API |
| 통합 테스트 | 실제 커널/QEMU | 모듈 로드, 디바이스 I/O | 느림 | 전체 동작 |
KUnit 테스트 작성
// KUnit 테스트 — 커널 내부에서 실행
// #[cfg(CONFIG_KUNIT)]으로 테스트 빌드에서만 포함
#[cfg(CONFIG_KUNIT)]
mod tests {
use kernel::prelude::*;
use kernel::kunit;
// 기본 어서션 테스트
#[test]
fn test_vec_push() {
let mut v = Vec::new();
v.try_push(42).expect("push failed");
assert_eq!(v.len(), 1);
assert_eq!(v[0], 42);
}
// 에러 코드 검증 테스트
#[test]
fn test_error_codes() {
use kernel::error::code;
let err: Result<()> = Err(code::EINVAL);
assert!(err.is_err());
// Result 변환 검증
let ok: Result<i32> = Ok(42);
assert_eq!(ok.unwrap(), 42);
}
// 커널 메모리 할당 테스트
#[test]
fn test_box_allocation() {
let boxed = Box::try_new(100u64);
assert!(boxed.is_ok());
assert_eq!(*boxed.unwrap(), 100);
}
// 문자열 처리 테스트
#[test]
fn test_cstring() {
let s = CString::try_from_fmt(fmt!("hello {}", 42));
assert!(s.is_ok());
}
}
rustdoc 테스트
Rust 문서 주석(///) 내의 코드 블록은 자동으로 테스트됩니다. 커널에서는 make rustdoctest로 실행합니다. 이 방식은 문서와 코드의 동기화를 보장합니다.
// rustdoc 테스트 — 문서 내 코드가 자동으로 테스트됨
/// 두 숫자를 더합니다.
///
/// # Examples
///
/// ```
/// use kernel::prelude::*;
/// assert_eq!(add(2, 3), 5);
/// assert_eq!(add(-1, 1), 0);
/// ```
///
/// # Errors
///
/// 이 함수는 에러를 반환하지 않습니다.
pub fn add(a: i32, b: i32) -> i32 { a + b }
/// 실패 케이스를 테스트하는 문서 테스트
///
/// ```should_panic
/// // should_panic 어트리뷰트: panic이 발생해야 통과
/// let v: Vec<i32> = Vec::new();
/// let _ = v[0]; // index out of bounds → panic
/// ```
///
/// ```compile_fail
/// // compile_fail: 컴파일이 실패해야 통과
/// let x: u32 = -1_i32; // 타입 불일치
/// ```
테스트 실행 명령
# === KUnit 테스트 실행 ===
# 방법 1: UML(User-Mode Linux)에서 빠르게 실행
./tools/testing/kunit/kunit.py run --arch=um
# Rust 관련 테스트만 필터링
./tools/testing/kunit/kunit.py run --arch=um --filter="rust_*"
# 방법 2: 전체 커널 빌드와 함께 실행
# Kconfig: CONFIG_KUNIT=y, CONFIG_RUST=y
make LLVM=1 -j$(nproc)
# 부팅 시 자동으로 KUnit 테스트 실행, dmesg로 결과 확인
dmesg | grep -A5 "kunit"
# === Rust 문서 테스트 ===
make LLVM=1 rustdoctest
# === Rust 커널 문서 생성 ===
make LLVM=1 rustdoc
# 결과: Documentation/output/rust/
# === 통합 테스트: QEMU에서 모듈 로드 ===
# 1. 모듈 빌드
make LLVM=1 M=samples/rust -j$(nproc)
# 2. QEMU로 커널 부팅 후
insmod rust_minimal.ko
dmesg | tail -5 # "Rust minimal sample (init)" 확인
rmmod rust_minimal
dmesg | tail -5 # "Rust minimal sample (exit)" 확인
테스트 어서션 매크로 비교
| 매크로 | 사용처 | 동작 | 예시 |
|---|---|---|---|
assert!(expr) | 일반 | false이면 panic | assert!(v.len() > 0) |
assert_eq!(a, b) | 일반 | a ≠ b이면 panic + 양쪽 값 출력 | assert_eq!(2+2, 4) |
assert_ne!(a, b) | 일반 | a = b이면 panic | assert_ne!(ptr, null_mut()) |
debug_assert!() | 일반 | debug 빌드에서만 검사 | debug_assert!(idx < len) |
kunit::assert!() | KUnit | 실패해도 panic 않고 테스트 실패 보고 | KUnit 테스트 내부 전용 |
테스트 전략 권장: (1) 내부 로직은 KUnit으로 단위 테스트, (2) 공개 API는 rustdoc 테스트로 예제 겸 검증, (3) 하드웨어 상호작용은 QEMU 통합 테스트로 검증하세요. KUnit은 UML에서 실행하면 실제 하드웨어 없이도 빠르게 피드백을 받을 수 있습니다. 커널 코드에서는 assert!가 panic을 유발하므로, KUnit 전용 어서션을 사용하는 것이 좋습니다.
Clippy 린트와 커널 코딩 스타일
커널 Rust 코드는 make LLVM=1 CLIPPY=1으로 Clippy 린트를 실행합니다. 커널 빌드 시스템은 기본적으로 여러 Clippy 경고를 에러로 취급하며, 커널 특화 린트 규칙이 적용됩니다.
| Clippy 린트 | 규칙 | 커널 적용 |
|---|---|---|
clippy::dbg_macro | dbg!() 사용 금지 | pr_debug! 사용 |
clippy::unwrap_used | unwrap() 사용 금지 | ? 또는 ok_or() 사용 |
clippy::expect_used | expect() 사용 금지 | ? 또는 ok_or() 사용 |
clippy::needless_return | 불필요한 return 제거 | 표현식 기반 반환 사용 |
clippy::redundant_closure | 불필요한 클로저 래핑 제거 | 함수 참조 직접 전달 |
# Clippy 실행 (커널 전체)
make LLVM=1 CLIPPY=1
# 특정 모듈만 Clippy 검사
make LLVM=1 CLIPPY=1 M=samples/rust
# rustfmt으로 코드 포맷팅
make LLVM=1 rustfmt
# rust-analyzer 설정 생성 (IDE 지원)
make LLVM=1 rust-analyzer
커널 Rust 문서 주석 규칙
/// 디바이스 레지스터를 읽습니다.
///
/// `offset`에 해당하는 32비트 레지스터 값을 반환합니다.
/// offset은 MMIO 영역 내에 있어야 합니다.
///
/// # Errors
///
/// offset이 MMIO 영역을 벗어나면 [`EINVAL`]을 반환합니다.
///
/// # Examples
///
/// ```
/// let val = device.read32(0x00)?;
/// pr_info!("Status: {:#x}\n", val);
/// ```
///
/// # Safety (unsafe fn인 경우)
///
/// 호출자는 `self.base`가 유효한 MMIO 매핑임을 보장해야 합니다.
pub fn read32(&self, offset: usize) -> Result<u32> {
// 구현...
Ok(0)
}
문서 주석 필수 섹션: 커널 Rust 코딩 가이드에 따라 공개 API에는 (1) 함수 설명, (2) # Errors (에러 반환 가능 시), (3) # Examples (사용 예시), (4) # Safety (unsafe fn인 경우)를 작성해야 합니다. # Panics 섹션은 panic 가능 조건을 명시하지만, 커널에서는 panic을 최소화해야 합니다.
커널 Rust 디버깅 (panic, oops, GDB)
커널 Rust 코드의 디버깅은 C 코드와 유사하지만, Rust의 심볼 이름이 맹글링(name mangling)되어 있어 추가 도구가 필요합니다. Rust v0 맹글링 형식을 이해하고, 적절한 도구를 사용하면 효과적으로 디버깅할 수 있습니다.
| 디버깅 도구 | 용도 | Rust 지원 수준 | 주요 명령/사용법 |
|---|---|---|---|
| rustfilt | Rust 심볼 디맹글링 | 완벽 | dmesg | rustfilt |
| addr2line | 주소→소스 변환 | 완벽 | addr2line -e vmlinux -f 0x... |
| GDB | 대화형 디버깅 | 좋음 (타입 정보 지원) | break rust_module::init |
| ftrace | 함수 호출 추적 | 좋음 (맹글링 이름) | echo '_RNv*' > set_ftrace_filter |
| KASAN | 메모리 안전성 검사 | unsafe 블록에만 해당 | CONFIG_KASAN=y |
| pr_info!/pr_debug! | 로그 출력 (printk) | 네이티브 지원 | pr_info!("val={}\n", x) |
| objdump | 디스어셈블리 분석 | 좋음 | objdump -d vmlinux | rustfilt |
| perf | 성능 프로파일링 | 좋음 (맹글링 해제 필요) | perf record / perf report |
pr_info! / pr_debug! 디버깅
// 커널 Rust에서 가장 기본적인 디버깅 방법
use kernel::prelude::*;
// 로그 레벨별 매크로
pr_emerg!("Emergency: system is unusable\\n"); // KERN_EMERG
pr_alert!("Alert: action must be taken\\n"); // KERN_ALERT
pr_crit!("Critical condition\\n"); // KERN_CRIT
pr_err!("Error: something went wrong\\n"); // KERN_ERR
pr_warn!("Warning: unusual condition\\n"); // KERN_WARNING
pr_notice!("Notice: normal but significant\\n"); // KERN_NOTICE
pr_info!("Info: driver loaded, irq={}\\n", irq); // KERN_INFO
pr_debug!("Debug: register val={:#x}\\n", val); // KERN_DEBUG (CONFIG_DYNAMIC_DEBUG)
// Rust의 Debug/Display 트레이트 활용
pr_info!("State: {:?}\\n", my_state); // Debug 포맷
pr_info!("Buf (hex): {:02x?}\\n", &buf[..16]); // 16진수 배열
Rust 심볼 디맹글링
커널 oops 메시지에서 Rust 함수 이름은 _R로 시작하는 맹글링된 형태로 나타납니다. rustfilt로 사람이 읽을 수 있는 형태로 변환합니다.
# rustfilt 설치
cargo install rustfilt
# 맹글링된 심볼 디맹글링
echo "_RNvNtCs1234_my_module3foo4init" | rustfilt
# → my_module::foo::init
# 커널 oops 출력 전체를 파이프로 디맹글링
dmesg | rustfilt
# 또는 특정 모듈의 심볼 목록 확인
nm vmlinux | rustfilt | grep "my_rust_module"
# objdump으로 디스어셈블 + 디맹글링
objdump -d vmlinux | rustfilt | less
addr2line으로 소스 위치 추적
# 커널 oops에서 나온 주소를 소스 파일:라인으로 변환
addr2line -e vmlinux -f 0xffffffff81234567
# → my_module::MyDriver::probe
# → drivers/my_driver/src/lib.rs:42
# 여러 주소를 한 번에 변환
addr2line -e vmlinux -f -i 0xffffffff81234567 0xffffffff81234890
# -i: 인라인된 함수 정보도 포함
# 콜스택 전체를 소스 위치로 변환
# oops 메시지에서 RIP/PC 주소 추출 후:
for addr in 0xffffffff81234567 0xffffffff81234890 0xffffffff81234abc; do
echo -n "$addr: "; addr2line -e vmlinux -f $addr | tr '\n' ' '; echo
done
GDB + QEMU 원격 디버깅
# 1. QEMU에서 커널을 디버그 모드로 실행
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd rootfs.cpio.gz \
-append "console=ttyS0 nokaslr" \
-nographic \
-s -S # -s: gdb server on :1234, -S: 시작 시 대기
# 2. GDB 연결
gdb vmlinux
(gdb) target remote :1234
# 3. Rust 함수에 브레이크포인트 설정
# 맹글링된 이름 또는 디맹글링된 이름 모두 사용 가능
(gdb) break rust_module::init
(gdb) break _RNvNtCs1234_my_module4init # 맹글링 이름
# 4. Rust 변수 검사
(gdb) print *self
(gdb) print self.inner.data # Rust 구조체 필드 접근
# 5. Rust 타입 정보 확인
(gdb) ptype my_module::MyDriver
(gdb) info locals # 현재 스코프의 로컬 변수
# 6. 디버그 심볼이 충분한지 확인
# Kconfig에서 CONFIG_DEBUG_INFO=y 필수
(gdb) continue
ftrace로 Rust 함수 추적
# Rust 함수를 ftrace로 추적
echo function > /sys/kernel/debug/tracing/current_tracer
# 특정 Rust 모듈의 함수만 필터링
# (맹글링된 이름 사용)
echo '_RNv*my_module*' > /sys/kernel/debug/tracing/set_ftrace_filter
# 또는 available_filter_functions에서 검색
cat /sys/kernel/debug/tracing/available_filter_functions | rustfilt | grep my_module
# 추적 시작/종료
echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... 테스트 수행 ...
echo 0 > /sys/kernel/debug/tracing/tracing_on
# 결과 확인 (디맹글링 적용)
cat /sys/kernel/debug/tracing/trace | rustfilt
KASAN과 Rust unsafe 디버깅
KASAN(Kernel Address Sanitizer)은 unsafe 블록 내의 메모리 오류를 런타임에 탐지합니다. Safe Rust에서는 메모리 오류가 발생하지 않으므로, KASAN은 주로 unsafe 코드와 FFI 경계를 검증하는 데 활용됩니다.
# KASAN 활성화 (Kconfig)
# CONFIG_KASAN=y
# CONFIG_KASAN_GENERIC=y (또는 CONFIG_KASAN_SW_TAGS)
make LLVM=1 menuconfig
# Kernel hacking → Memory Debugging → KASAN
# KASAN 탐지 시 커널 로그 예시:
# BUG: KASAN: slab-use-after-free in _RNvNtCs..._my_driver4read
# Read of size 4 at addr ffff888012345678 by task test/1234
# Call Trace:
# _RNvNtCs..._my_driver4read+0x42/0x80
# 이 출력을 디맹글링하면:
# BUG: KASAN: slab-use-after-free in my_driver::read
| 디버깅 도구 | 용도 | Rust 관련 참고 | 활성화 방법 |
|---|---|---|---|
dmesg | 커널 로그 확인 | pr_info!/pr_err! 출력 확인 | 기본 제공 |
rustfilt | Rust 심볼 디맹글링 | cargo install rustfilt | 사용자 도구 |
addr2line | 주소→소스 매핑 | CONFIG_DEBUG_INFO=y 필요 | binutils 포함 |
GDB | 인터랙티브 디버깅 | QEMU + -s -S로 원격 디버깅 | nokaslr 추가 권장 |
ftrace | 함수 호출 추적 | Rust 함수도 추적 가능 (맹글링 이름) | CONFIG_FTRACE=y |
KASAN | 메모리 오류 탐지 | unsafe 블록의 OOB/UAF 탐지 | CONFIG_KASAN=y |
KMSAN | 초기화되지 않은 메모리 | unsafe 내 미초기화 접근 탐지 | CONFIG_KMSAN=y |
lockdep | 잠금 순서 검증 | Rust Mutex/SpinLock도 lockdep 통합 | CONFIG_LOCKDEP=y |
Rust panic 발생 시 커널 동작
// 커널에서 panic이 발생하는 상황들:
// 1. unwrap() 실패 → 커널 oops
let val = some_option.unwrap(); // None이면 panic!
// 2. 배열 경계 초과 → panic
let arr = [1, 2, 3];
let x = arr[5]; // index out of bounds → panic!
// 올바른 패턴: 항상 Result/?로 에러 전파
fn safe_operation() -> Result {
let val = some_option.ok_or(code::EINVAL)?; // panic 대신 에러 반환
let x = arr.get(5).ok_or(code::EINVAL)?; // 안전한 경계 검사
Ok(())
}
Rust panic과 커널: safe Rust에서 panic이 발생하면 커널 oops로 이어집니다. 따라서 커널 Rust에서는 unwrap()/expect() 사용을 최소화하고, 항상 Result와 ? 연산자로 에러를 전파해야 합니다. panic = "abort"가 커널 빌드 기본값입니다.
디버그 빌드 주의: CONFIG_DEBUG_INFO=y와 CONFIG_KASAN=y를 동시에 활성화하면 커널 이미지 크기가 수 배로 증가하고 빌드 시간이 크게 늘어납니다. 디버깅이 끝나면 비활성화하세요. 또한 nokaslr 커널 파라미터를 사용하면 GDB 브레이크포인트가 안정적으로 동작합니다.
C vs Rust 커널 코드 비교
동일한 기능을 C와 Rust로 구현했을 때의 차이를 비교합니다.
/* C 커널 코드: 뮤텍스 보호 리소스 접근 */
static DEFINE_MUTEX(my_lock);
static int shared_data = 0;
int my_read(void) {
int val;
mutex_lock(&my_lock);
val = shared_data;
/* 여기서 에러 반환 시 unlock 빠뜨리기 쉬움! */
mutex_unlock(&my_lock);
return val;
}
// Rust 커널 코드: 동일 기능
let shared_data: Mutex<i32> = Mutex::new(0);
fn my_read(data: &Mutex<i32>) -> Result<i32> {
let guard = data.lock(); // 잠금 획득
let val = *guard;
Ok(val)
// guard가 여기서 Drop → 자동 unlock
// 에러 경로든 정상 경로든 반드시 unlock됨!
}
에러 경로 비교: 다중 자원 할당
/* C: 에러 경로에서 역순 해제 — "goto cleanup" 패턴 */
int my_probe(struct platform_device *pdev) {
struct resource *res;
void __iomem *base;
int irq, ret;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) return -ENODEV;
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base)) return PTR_ERR(base);
irq = platform_get_irq(pdev, 0);
if (irq < 0) return irq;
ret = devm_request_irq(&pdev->dev, irq, my_handler,
0, "mydev", pdev);
if (ret) return ret; /* 각 단계마다 에러 체크 필요 */
return 0;
}
// Rust: ? 연산자로 에러 전파 — 자원은 Drop이 자동 해제
fn probe(pdev: &mut PlatformDevice) -> Result {
let res = pdev.get_resource(0)?; // 실패 시 자동 반환
let base = pdev.ioremap_resource(&res)?; // 실패 시 자동 반환
let irq = pdev.get_irq(0)?; // 실패 시 자동 반환
pdev.request_irq(irq, my_handler)?; // 실패 시 자동 반환
Ok(())
// 에러 시: 이미 생성된 res, base가 Drop으로 자동 해제
// goto cleanup 패턴이 완전히 불필요!
}
버퍼 오버플로 비교
/* C: 버퍼 오버플로 — 컴파일러가 경고하지 않음 */
char buf[64];
int len = copy_from_user(buf, user_buf, user_len);
/* user_len > 64이면 스택 버퍼 오버플로! */
buf[len] = '\0'; /* len이 음수일 수도 있음 */
// Rust: 슬라이스 범위 검사 + UserSlice 안전 추상화
let mut buf = [0u8; 64];
let len = reader.read_slice(&mut buf)?; // 최대 64바이트만 읽음
// buf[65] 접근 → 컴파일 타임 에러 (고정 크기 배열)
// reader가 64를 초과하면 Err 반환
| 버그 유형 | C에서의 발생 원인 | Rust의 방지 메커니즘 |
|---|---|---|
| Use-After-Free | kfree(ptr); use(ptr); | 소유권 이전 후 사용 → 컴파일 에러 |
| Double Free | kfree(ptr); kfree(ptr); | 소유권 이전은 1회만 → 컴파일 에러 |
| 버퍼 오버플로 | 배열 범위 체크 누락 | 슬라이스 범위 검사 (런타임 panic 또는 Result) |
| 자원 누수 | 에러 경로 cleanup 누락 | Drop 트레이트 → RAII 자동 해제 |
| 데이터 레이스 | 잠금 없이 공유 데이터 접근 | Send/Sync + Mutex 타입 시스템 강제 |
| 널 포인터 | NULL 체크 누락 | Option<T> → None 처리 강제 |
| 정수 오버플로 | 부호 있는 정수 UB | 디버그: panic, 릴리스: wrapping (명시적) |
| 포맷 문자열 | printk(user_data) | 타입 안전 매크로 (pr_info!) |
C→Rust 점진적 마이그레이션 전략
커널에서 C 코드를 Rust로 마이그레이션하는 것은 전면 교체가 아닌 점진적 공존 전략입니다. 기존 C 서브시스템은 그대로 유지하면서, 새로운 드라이버와 모듈을 Rust로 작성하고, 추상화 계층을 점진적으로 확장합니다.
마이그레이션 의사 결정 기준
| 기준 | Rust 도입 적합 | C 유지 적합 |
|---|---|---|
| 코드 상태 | 신규 작성 또는 전면 리팩토링 예정 | 안정적으로 작동 중인 기존 코드 |
| 메모리 안전 | 버퍼 오버플로/UAF 이력이 있는 서브시스템 | 안전성 이슈가 거의 없는 코드 |
| 복잡도 | 동시성/에러 경로가 복잡한 드라이버 | 단순한 레지스터 접근 코드 |
| 추상화 가용성 | 필요한 Rust 추상화가 이미 존재 | 추상화 계층을 새로 만들어야 함 |
| 메인테이너 | Rust에 호의적이고 리뷰 가능 | C만 리뷰하는 메인테이너 |
| 성능 민감도 | 안전성이 성능보다 중요한 경로 | 핫 패스 (ns 단위 최적화 필요) |
실제 마이그레이션 사례
| 프로젝트 | 서브시스템 | 상태 | 설명 |
|---|---|---|---|
| ax88796b | PHY 드라이버 | 머지 완료 | 최초의 프로덕션 Rust 커널 드라이버 (Linux 6.8) |
| Nova | DRM/GPU | 개발 중 | NVIDIA GPU를 위한 신규 Rust DRM 드라이버 |
| Binder | Android IPC | RFC | Android Binder를 Rust로 재작성 |
| NVMe | 블록 장치 | RFC | NVMe 드라이버 Rust 구현 (안전성 개선 목적) |
| Null Block | 블록 장치 | 개발 중 | null_blk 테스트 드라이버 Rust 포팅 |
| r8169 PHY | 네트워크 | 논의 중 | Realtek PHY 드라이버 Rust 전환 가능성 |
새 Rust 드라이버 작성 체크리스트
| 단계 | 작업 | 확인 사항 |
|---|---|---|
| 1 | 필요한 추상화 확인 | rust/kernel/에 필요한 C API 래퍼가 있는지 확인 |
| 2 | 추상화 추가 (필요 시) | rust/kernel/에 새 safe 래퍼 작성 → LKML 패치 제출 |
| 3 | Kconfig 항목 생성 | config MYDRIVER_RUST + depends on RUST |
| 4 | Makefile 규칙 추가 | obj-$(CONFIG_MYDRIVER_RUST) += my_driver.o |
| 5 | 드라이버 구현 | module! 매크로 + 트레이트 구현 |
| 6 | SAFETY 주석 | 모든 unsafe 블록에 SAFETY 근거 작성 |
| 7 | 문서 작성 | /// 문서 주석 + # Examples, # Errors |
| 8 | 테스트 | KUnit + rustdoc 테스트 + QEMU 통합 테스트 |
| 9 | Clippy / rustfmt | make LLVM=1 CLIPPY=1 경고 0개 확인 |
| 10 | LKML 제출 | 서브시스템 메인테이너에게 패치 시리즈 전송 |
마이그레이션 황금 규칙: (1) 기존 C 코드를 강제로 교체하지 않는다 — 새 드라이버에서 Rust를 선택적으로 사용. (2) C와 Rust가 같은 바이너리에 공존 — 링커가 C .o와 Rust .o를 함께 링크. (3) 추상화 계층을 점진적으로 확장 — 필요한 C API에 대한 safe Rust 래퍼를 점진적으로 추가. (4) 각 서브시스템의 메인테이너가 Rust 도입 여부를 결정.
커널 버전별 Rust 변천사
아래 표는 학습용 요약입니다. 세부 지원 범위와 API 변경은 커널 버전별 공식 문서(docs.kernel.org/rust)와 해당 브랜치의 rust/ 디렉터리를 우선 기준으로 확인하세요.
| 커널 버전 | Rust 관련 변경사항 | 주요 추가 API / 기능 |
|---|---|---|
| 6.1 | Rust 기본 인프라 도입, alloc 크레이트, 기본 모듈 지원 | module! 매크로, pr_info!, kernel::prelude, bindgen 통합, Kbuild 연동 |
| 6.2 | Rust 문서 개선, 빌드 시스템 안정화 | rustdoc 커널 통합, make rustdoc 지원, 빌드 의존성 정리 |
| 6.3 | Rust 코드 정리 및 인프라 보강 | kernel::sync 모듈 확장, 타입 안전 래퍼 개선 |
| 6.4 | pin_init! 매크로, 개선된 에러 처리 | pin_init!, #[pin_data], PinInit 트레이트, Init 트레이트, Opaque<T> |
| 6.5 | 추상화 계층 확장 | kernel::workqueue, kernel::task, ARef 개선 |
| 6.6 | 실사용 드라이버/서브시스템 적용 범위 확대 | kernel::net::phy (PHY 드라이버 프레임워크), Mutex/SpinLock lock guard 개선 |
| 6.7 | 커널 Rust 유틸리티 확장 | kernel::time, Vec fallible allocation API 개선 |
| 6.8 | 드라이버 지원 영역과 커널 Rust 유틸리티 지속 확장 | kernel::block 초기 인프라, RCU 추상화 개선 |
| 6.9 | Rust 문서/테스트 강화 | KUnit Rust 통합 개선, rustdoc 테스트 프레임워크 |
| 6.10 | 버전별 API 정리 및 드라이버 바인딩 계층 확장 | #[vtable] 매크로 개선, firmware 로딩 추상화 |
| 6.11 | Rust 코드 품질 및 안정성 개선 | Clippy 린트 적용, kernel::error 리팩터링 |
| 6.12 | 추가 서브시스템으로 확장 진행, PREEMPT_RT 호환성 | DMA 추상화 초기 작업, kernel::device 리팩터링, PREEMPT_RT Rust 호환 |
| 6.13 | Rust trace events — 커널 트레이스 이벤트 직접 발행 | trace_event! 매크로, ftrace/perf 통합, kernel::tracing 모듈 |
| 6.14 | Nova GPU 드라이버 — DRM 서브시스템의 첫 Rust 드라이버 | Nova DRM 드라이버 머지, kernel::drm 추상화, IOMMU 바인딩 |
| 6.15 | Rust hrtimer, ARMv7 아키텍처 지원 | kernel::time::hrtimer, ARMv7 Rust 지원, NVMe PCI 드라이버 진행 |
Rust 커널 코드 성장 추이
| 커널 버전 | rust/ 디렉터리 코드 규모 (약) | Rust 커널 모듈/드라이버 수 |
|---|---|---|
| 6.1 | ~12,500줄 | 샘플 모듈만 (samples/rust/) |
| 6.4 | ~18,000줄 | Pin 초기화 인프라 + 샘플 |
| 6.6 | ~22,000줄 | PHY 드라이버 프레임워크 + ax88796b |
| 6.12 | ~30,000줄 | 다수 추상화 + PHY 드라이버 |
| 6.14 | ~38,000줄 | Nova GPU 드라이버 + PHY + 인프라 |
| 6.15 | ~42,000줄+ | hrtimer, ARMv7, NVMe 진행 중 |
핵심 이정표: Linux 6.1에서 Rust 인프라가 머지된 후, 6.6에서 첫 실사용 드라이버(PHY)가 등장했고, 6.14에서 복잡한 서브시스템인 DRM에 Rust 드라이버(Nova)가 머지되면서 "Rust는 커널에서 진짜로 쓸 수 있다"는 것이 증명되었습니다.
Rust 커널 지원의 미래와 전망
커널 Rust의 미래는 다음과 같은 방향으로 발전하고 있습니다. Linux 재단과 Rust for Linux 팀은 점진적 확장 전략을 채택하여, 검증된 서브시스템부터 단계적으로 Rust 지원을 넓혀가고 있습니다.
| 영역 | 현황 | 전망 |
|---|---|---|
| 드라이버 | PHY, GPU(Nova), NVMe 등 머지 진행 | USB, 블록, 네트워크 드라이버 확대 |
| 파일시스템 | Rust 파일시스템 인터페이스 논의 중 | 실험적 FS 구현 가능성 |
| 아키텍처 | x86_64, ARM64, ARMv7 지원 | RISC-V, 추가 아키텍처 확장 |
| 안정화 | nightly 컴파일러 필요 | 필요 기능의 stable 승격 진행 |
| 도구 | bindgen, Kbuild 통합 완료 | rust-analyzer 커널 지원, CI 강화 |
| 커뮤니티 | Rust for Linux 워킹 그룹 활동 중 | 더 많은 서브시스템 메인테이너 참여 |
주요 기술적 과제
| 과제 | 현재 상태 | 해결 방향 |
|---|---|---|
| nightly 의존성 | 커널 빌드에 nightly rustc 필수 | 필요한 불안정 기능들을 개별적으로 stable로 승격 추진 중. allocator_api, coerce_unsized 등이 대상 |
| ABI 안정성 | Rust는 안정적 ABI를 보장하지 않음 | 커널 내부에서만 사용하므로 ABI는 소스 레벨 호환성으로 대체. repr(C)로 C 측과의 인터페이스 보장 |
| 코드 크기 | Rust 바이너리가 C보다 약간 큼 | LTO(Link-Time Optimization)와 -Copt-level=z 적용으로 최적화. 제네릭 단형화(monomorphization) 비용 관리 |
| 디버깅 경험 | Rust 심볼 맹글링으로 가독성 저하 | rustfilt 통합, v0 맹글링 형식 개선, GDB pretty-printer 지원 확대 |
| 빌드 시간 | Rust 컴파일이 C보다 느림 | 증분 컴파일 활용, 크레이트 단위 캐싱, sccache 도입 가능성 |
Rust가 커널에서 해결할 수 있는 버그 클래스
Microsoft, Google, Android 팀의 보고서에 따르면, 보안 취약점의 약 70%가 메모리 안전 버그입니다. Rust가 컴파일 타임에 방지할 수 있는 대표적 버그 클래스:
| 버그 클래스 | C에서의 발생 패턴 | Rust의 방지 메커니즘 | CVE 영향도 |
|---|---|---|---|
| Use-After-Free | kfree(obj); obj->field | 소유권 시스템: 해제된 메모리 접근 불가 | 높음 (RCE 가능) |
| Double Free | kfree(p); ... kfree(p); | 소유권 이동: 한 번만 해제 가능 | 높음 |
| Buffer Overflow | memcpy(dst, src, wrong_len) | 경계 검사, 슬라이스 타입 | 높음 (RCE 가능) |
| Data Race | 락 없이 공유 데이터 접근 | Send/Sync + 빌림 규칙 | 중간~높음 |
| Null Deref | ptr = get_thing(); ptr->field | Option<T> 강제 체크 | 중간 (DoS) |
| Uninitialized Memory | struct foo s; use(s.field) | 초기화 강제, MaybeUninit 명시적 사용 | 중간 |
Rust ≠ 만병통치약: Rust는 safe Rust 경계 안에서 메모리 안전을 보장합니다. 커널에서는 unsafe 블록과 FFI 경계가 불가피하므로, 이 부분은 여전히 수동 검증이 필요합니다. 또한 논리적 버그, 교착 상태(Mutex 순서 역전), 자원 누수(non-memory resources) 등은 Rust로도 방지할 수 없습니다.
Rust 커널 API는 아직 불안정(unstable)합니다. 버전마다 인터페이스가 크게 변경될 수 있으므로, 항상 해당 커널 버전의 rust/ 디렉터리 소스코드를 참조하세요.
관련 문서
Rust in Kernel과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.
사이트 내부 문서
외부 참고 자료
| 자료 | URL | 설명 |
|---|---|---|
| Rust for Linux 공식 | rust-for-linux.com | 프로젝트 홈페이지, 최신 소식 |
| 커널 Rust 문서 | docs.kernel.org/rust | 공식 커널 Rust 문서 |
| Rust Book | doc.rust-lang.org/book | Rust 공식 학습서 (The Book) |
| Rust by Example | rust-by-example | 예제 중심 학습 |
| Rustonomicon | nomicon | unsafe Rust 심층 가이드 |
| GitHub 저장소 | Rust-for-Linux/linux | 소스 코드 및 이슈 트래커 |
| LKML Rust | lore.kernel.org | 메일링 리스트 아카이브 |