第三节 组件 axlog - 支持 log

现在我们来为内核正式实现日志组件 axlog。在 crates.io 中已经有一个非常通用的日志 crate - log,它主要用于普通应用的开发;本节我们将以它为基础进行封装和扩展,满足内核对日志设施的需要。

首先实现日志的初始化过程和级别设置:

// axlog/Cargo.toml
[dependencies]
log = "0.4"
crate_interface = "0.1.1"

// axlog/src/lib.rs
#![no_std]

use core::fmt::{self, Write};
use core::str::FromStr;
use crate_interface::call_interface;
use log::{Level, LevelFilter, Log, Metadata, Record};
pub use log::{debug, error, info, trace, warn};

#[crate_interface::def_interface]
pub trait LogIf {
    fn write_str(s: &str);
    fn get_time() -> core::time::Duration;
}

struct Logger;

pub fn init() {
    log::set_logger(&Logger).unwrap();
    log::set_max_level(LevelFilter::Warn);
}

pub fn set_max_level(level: &str) {
    let lf = LevelFilter::from_str(level).ok().unwrap_or(LevelFilter::Off);
    log::set_max_level(lf);
}

第 4 行:Cargo.toml 引入 log 和 crate_interface 两个 crates。

第 21 行:全局的日志实例 Logger,它代表了日志对象,将来对日志的各种操作主要都是针对它。

第 23~26 行:上一节 init() 的实现仅是为了测试 crate_interface,这里重新实现。先是指定日志对象,然后设置默认日志级别 warn。

第 28~31 行:控制日志级别 set_max_level,支持关闭 off 以及 error, warn, info, debug, trace5 个级别。默认是 warn 级,即默认情况下只输出警告与错误。

然后,最重要的一步,按照 crate log 的实现要求,为 Logger 实现 trait Log 接口。这个外部的 crate log 本身是一个框架,实现了日志的各种通用功能,但是如何对日志进行输出需要基于所在的环境,这个 trait Log 就是通用功能与环境交互的接口。

下面列出实现 Log 接口的具体逻辑:

macro_rules! with_color {
    ($color_code:expr, $($arg:tt)*) => {{
        format_args!("\u{1B}[{}m{}\u{1B}[m", $color_code as u8, format_args!($($arg)*))
    }};
}

#[repr(u8)]
#[allow(dead_code)]
enum ColorCode {
    Red = 31, Green = 32, Yellow = 33, Cyan = 36, White = 37, BrightBlack = 90,
}

impl Log for Logger {
    #[inline]
    fn enabled(&self, _metadata: &Metadata) -> bool {
        true
    }

    fn log(&self, record: &Record) {
        let level = record.level();
        let line = record.line().unwrap_or(0);
        let path = record.target();
        let args_color = match level {
            Level::Error => ColorCode::Red,
            Level::Warn => ColorCode::Yellow,
            Level::Info => ColorCode::Green,
            Level::Debug => ColorCode::Cyan,
            Level::Trace => ColorCode::BrightBlack,
        };
        let now = call_interface!(LogIf::get_time);

        print_fmt(with_color!(
            ColorCode::White,
            "[{:>3}.{:06} {path}:{line}] {args}\n",
            now.as_secs(),
            now.subsec_micros(),
            path = path,
            line = line,
            args = with_color!(args_color, "{}", record.args()),
        ));
    }

    fn flush(&self) {}
}

第 1~11 行:为日志输出功能准备一个宏 with_color 和颜色代码,后面将根据级别为日志文本增加不同的颜色。

第 13~17 行:是否启用日志功能,硬编码启用即可。

第 19~41 行:Log::log 方法是关键,准备好显示颜色、当前时间、当前模块路径、行号以及日志内容等一系列参数,然后调用 print_fmt 执行日志的输出功能。

第 43 行:flush 刷新日志缓存。我们内核日志目前只是打印到屏幕,不涉及刷新,所以忽略。

下一步来看 print_fmt 的具体实现:

impl Write for Logger {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        call_interface!(LogIf::write_str, s);
        Ok(())
    }
}

pub fn print_fmt(args: fmt::Arguments) {
    let _ = Logger.write_fmt(args);
}

第 1~6 行:为 Logger 实现 Write trait,目的是借助 Rust 提供的这个 trait,完成从变参到最终字符串的转换。我们只需要实现 write_str 方法,输入参数已经是处理好的结果字符串,然后通过 call_interface 调用 axhal 中实现的 LogIf::write_str 来完成日志输出。

第 8~10 行:print_fmt 的实现。既然 Logger 已经实现了 Write trait,我们只需要调用 Logger 的 write_fmt 方法,Logger 就会自动处理变参,进而如上面所述,通过 write_str 进行输出。

上面已经完成了 axlog 的实现,下面在 axruntime 中初始化 axlog,并尝试打印两行日志:

#![no_std]

pub use axhal::ax_println as println;

#[macro_use]
extern crate axlog;

#[no_mangle]
pub extern "C" fn rust_main(hartid: usize, dtb: usize) -> ! {
    extern "C" {
        fn _skernel();
        fn main();
    }

    println!("\nArceOS is starting ...");

    // We reserve 2M memory range [0x80000000, 0x80200000) for SBI,
    // but it only occupies ~194K. Split this range in half,
    // requisition the higher part(1M) for early heap.
    axalloc::early_init(_skernel as usize - 0x100000, 0x100000);

    axlog::init();
    axlog::set_max_level(option_env!("LOG").unwrap_or(""));
    info!("Logging is enabled.");
    info!("Primary CPU {} started, dtb = {:#x}.", hartid, dtb);

    unsafe { main(); }
    loop {}
}

第 22 行:初始化 axlog 组件。上节已经实现这行。

第 23 行:通过 make 传入外部环境变量 LOG,指定日志级别。

第 24~25 行:打印两行日志到屏幕。

执行 make run LOG=info 验证 axlog 是否能够完成日志初始化和 info 级别的日志输出:

ArceOS is starting ... [ 0.098586 axruntime:24] Logging is enabled. [ 0.103594 axruntime:25] Primary CPU 0 started, dtb = 0x87000000.

Now: 0.107696 Hello, ArceOS![from String] Elapsed: 0.002323

在屏幕输出中显示了我们记录的 info 日志,axlog 验证成功!

之前我们的内核已经实现了一个最简单的 panic,处理方式就是进入无限循环等待。本节来完善对它的实现。

// axhal/src/riscv64/lang_items.rs
use axlog::error;

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    error!("{}", info);
    axhal::misc::terminate()
}

很简单,先打印错误信息日志,然后内核中止运行。对于中止运行这个功能,我们需要扩展一下 axhal 组件。

// axhal/src/riscv64/misc.rs
pub fn terminate() -> ! {
    sbi_rt::system_reset(sbi_rt::Shutdown, sbi_rt::NoReason);
    loop {}
}

// axhal/src/riscv64.rs
mod misc;
pub use misc::terminate;

既然有了 terminate 功能,我们把 axruntime 的 rust_main 函数最后一行 loop {},直接替换为 terminate(),如下:

// axruntime/src/lib.rs
#[no_mangle]
pub extern "C" fn rust_main(hartid: usize, dtb: usize) -> ! {
	... ...
    unsafe { main(); }
    axhal::terminate();
}

再次执行 make run,查看执行结果:

ArceOS is starting ...

Now: 0.098169 Hello, ArceOS![from String] Elapsed: 0.003439

结果显示没有变化,但是内核执行完毕后自动就退出了,不再需要 ctrl + A +X 退出 qemu,方便了以后对内核的开发测试。