前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Rust for Linux 源码导读 | Rust 驱动开发与通用时钟管理框架抽象

Rust for Linux 源码导读 | Rust 驱动开发与通用时钟管理框架抽象

作者头像
张汉东
发布2022-03-29 08:27:52
1.2K0
发布2022-03-29 08:27:52
举报
文章被收录于专栏:Rust 编程

背景概念

Rust for Linux 这个项目的目的就是为了将 Rust 引入 Linux,让 Rust 成为 C 语言之后的第二语言。但它最初的目的是:实验性地支持Rust来写内核驱动。

以往,Linux 内核驱动的编写相对于应用其实是比较复杂的,具体复杂性主要表现在以下两个方面:

  • 编写设备驱动必须了解Linux 内核基础概念、工作机制、硬件原理等知识
  • 设备驱动中涉及内存和多线程并发时容易出现 Bug,linux驱动跟linux内核工作在同一层次,一旦发生问题,很容易造成内核的整体崩溃

引入 Rust 从理想层面来看,一方面在代码抽象和跨平台方面比 C 更有效,另一方面代码质量会更高,有效减少内存和多线程并发类 Bug 。但具体实践如何,是否真如理想中有效,这就需要后续的实验。

Rust for Linux 就是为了帮助实现这一目标,为 Linux 提供了 Rust 相关的基础设施和方便编写 Linux 驱动的安全抽象。

Rust for Linux 第四次补丁提审

补丁的具体细节可以在Linux 邮件列表中找到:

  • RFC: https://lore.kernel.org/lkml/20210414184604.23473-1-ojeda@kernel.org/
  • v1: https://lore.kernel.org/lkml/20210704202756.29107-1-ojeda@kernel.org/
  • v2: https://lore.kernel.org/lkml/20211206140313.5653-1-ojeda@kernel.org/
  • v3: https://lore.kernel.org/lkml/20220117053349.6804-1-ojeda@kernel.org/

第二次补丁改进摘要可参考:Rust for Linux 源码导读 | Ref 引用计数容器[1]

第三次补丁改进摘要:

  • 对 Rust 的支持有一些改进:
    • 升级到 Rust 1.58.0
    • 增加了自动检测来判断是否有合适的 Rust 可用工具链(CONFIG_RUST_IS_AVAILABLE,用于替换HAS_RUST
    • 移除!COMPILE_TEST
    • 其他构建系统的改进
    • 文档改进
    • 需要的不稳定功能之一,-Zsymbol-mangling-version=v0,在 1.59.0 中变得稳定。另一个,“maybe_uninit_extra”,将在 1.60.0 中。
  • 对抽象和示例驱动程序的一些改进:
    • 加了将在总线中使用的“IdArray”和“IdTable”,以允许驱动程序指定在编译时保证为零终止(zero-terminated)的设备 ID 表。
    • 更新了 amba 以使用新的常量设备 ID 表支持。
    • 初始通用时钟框架抽象。
    • 平台驱动程序现在通过实现特质(trait)来定义。包括用于简化平台驱动程序注册的新宏和新示例/模板。
    • dev_* 打印宏。
    • IoMem<T>{read,write}*_relaxed 方法。
    • 通过删除 FileOpener 来简化文件操作。
    • 在驱动程序注册的参数中添加了“ThisModule”。
    • 添加用 Rust 编写的树外 Linux 内核模块的基本模板:[https ://github.com/Rust-for-Linux/rust-out-of-tree-module](https ://github.com/Rust-for-Linux/rust-out-of-tree-module "https ://github.com/Rust-for-Linux/rust-out-of-tree-module")

第四次补丁改进摘要:

基础设施更新:

  • 整合 CI : 英特尔 0DAY/LKP 内核测试机器人 / kernelCI/ GitHub CI
  • 添加了单目标支持,包括.o.s.ll.i(即宏扩展,类似于 C 预处理源)。
  • 文档Logo现在基于矢量 (SVG)。此外,已经为上游提出了 Tux 的矢量版本,并且用于改进自定义Logo支持的 RFC 已提交至上游 Rust。
  • is_rust_module.sh 返工。
  • 其他清理、修复和改进。

抽象和驱动更新:

  • 增加了对静态(全局共享变量)同步原语的支持。CONFIG_CONSTRUCTORS被用于实现。
  • 可选参数添加到杂项设备(misc device)的注册中。遵循构建者模式,例如: miscdev::Options::new() .mode(0o600) .minor(10) .parent(parent) .register(reg, c_str!("sample"), ())
  • 新的mm模块和VMA抽象(包装C端struct vm_area_struct)用于mmap
  • 支持!CONFIG_PRINTK情况。

Rust 与 Linux 设备驱动开发

基础概念

和应用程序不同,驱动程序是可以直接和硬件设备进行通讯的。驱动程序作为 Linux 内核的一种模块被动态加载到内核中。编译好的模块一般以.ko(kernel object)为文件扩展后缀 (这让我想起自己在 Linux 下安装 Nvidia 驱动报错找不到 nvidia.ko 文件时有多么崩溃)。

“但要注意的是,驱动不一定必须是模块,有些驱动是直接编译进内核的;同时模块也不全是驱动。

创建内核驱动基本模式如下:

  1. 通过实现初始化入口点和退出点来对应内核加载和卸载驱动的行为

基于 Rust for Linux 提供的 kernel 库来实现驱动的话,代码如下:

代码语言:javascript
复制
// From: https://github.com/Rust-for-Linux/linux/blob/rust/samples/rust/rust_miscdev.rs

// RustMiscdev 是某个设备
// 通过实现 KernelModule trait 来进行加载时初始化
impl KernelModule for RustMiscdev {
    fn init(name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {
        pr_info!("Rust miscellaneous device sample (init)\n");

        let state = SharedState::try_new()?;

        Ok(RustMiscdev {
            _dev: miscdev::Registration::new_pinned(fmt!("{name}"), state)?,
        })
    }
}

// 通过实现 Drop trait 来进行卸载时的清理
impl Drop for RustMiscdev {
    fn drop(&mut self) {
        pr_info!("Rust miscellaneous device sample (exit)\n");
    }
}
  1. 编写驱动逻辑

比如实现文件操作之类:

代码语言:javascript
复制
// From: https://github.com/Rust-for-Linux/linux/blob/rust/samples/rust/rust_miscdev.rs

struct Token;

// 通过实现 kernel crate 中抽象的 FileOperations 接口
impl FileOperations for Token {
    type Wrapper = Ref<SharedState>;
    type OpenData = Ref<SharedState>;

    kernel::declare_file_operations!(read, write);

    fn open(shared: &Ref<SharedState>, _file: &File) -> Result<Self::Wrapper> {
        Ok(shared.clone())
    }

    fn read(
        shared: RefBorrow<'_, SharedState>,
        _: &File,
        data: &mut impl IoBufferWriter,
        offset: u64,
    ) -> Result<usize> {
        // do_something
    }

    // do more things
}
  1. 编译、加载和测试等。

编写 Linux 驱动有很多细节内容,这里只是介绍一个简单的模式,来帮助理解 Rust for Linux 中的 kernel 抽象的作用。

Rust 驱动开发示例

去年有人提交过一份 C vs Rust 驱动代码示例:A GPIO driver in Rust[2]。可以看得出来,Rust 对硬件抽象的表达能力显著要高于 C 语言。

下面是这段驱动代码实现的摘要:

代码语言:javascript
复制
use core::ops::DerefMut;
use kernel::{
    amba, bit, declare_id_table, device, gpio,
    io_mem::IoMem,
    irq::{self, IrqData, LockedIrqData},
    power,
    prelude::*,
    sync::{IrqDisableSpinLock, Ref},
};

impl gpio::Chip for PL061Device { 
    // do_something
}

impl irq::Chip for PL061Device {
    // do_something
}

impl amba::Driver for PL061Device {
    // do_something
}

impl power::Operations for PL061Device {
    // do_something
}

module_amba_driver! {
    type: PL061Device,
    name: b"pl061_gpio",
    author: b"Wedson Almeida Filho",
    license: b"GPL v2",
}

这段驱动代码中使用了 Rust for Linux 中提供的 kernel 库,用其中包括的抽象 gpioirqambapower准确表达了这段驱动代码中的结构和实现意图,可以更好地和驱动开发者对驱动开发建立的心智模型对应起来,这就是 Rust 语言的抽象表达能力。

  • gpio::Chip trait 是对 gpio chip 的一种跨平台抽象接口,它也是一种依赖倒置。让底层不同的gpio chip都依赖同一个接口。
  • irq::Chip trait 是对内核中 irq_data结构体的抽象接口。中断描述符中会包括底层irq chip相关的数据结构,内核中把这些数据组织在一起,形成struct irq_data
  • amba::Driver trait 是 amba(高级微控制器总线结构,)设备驱动抽象接口,由于ARM众多的合作伙伴都会或多或少的使用ARM提供的片内外设,所以众多厂商的ARM处理器的一些外设可以使用相同的驱动。这个 GPIO 驱动也是针对 ARM 平台写的,所以用到这个。
  • power::Operations trait 是对 Linux 电源管理中各种 Callback 数据结构dev_pm_ops的抽象接口。power 模块中还定义了方便设置callback的宏等。

看得出来,基于 Rust for Linux 的 kernel 抽象,可以方便地使用 Rust 开发架构良好、可读性强、易维护且更加健壮的内核驱动代码。

Linux 通用时钟框架介绍

背景介绍

当下通用计算机中的CPU中各个模块都需要时钟驱动,内核就需要一套通用的机制来进行时钟管理。这套通用机制还必须跨平台地方便管理CPU上所有的时钟资源。

Linux 平台中提供一套通用时钟框架(common clock framework)来管理系统clock资源的子系统,其职能可以分为下面三个部分:

  • 向其它driver提供操作clocks的通用API。
  • 实现clock控制的通用逻辑,这部分和硬件无关。
  • 将和硬件相关的clock控制逻辑封装成操作函数集,交由底层的platform开发者实现,由通用逻辑调用。

现在主流的 Linux 处理器平台都包含了非常复杂的 clock tree,对应很多clock相关的器件。而这个通用时钟框架管理的对象就是这些clock器件。框架的主要功能包括:

  • 使能(enable/disable)clk
  • 设置clk频率
  • 选择clk的parent

通用时钟框架的通用接口定义在 Linux 内核中(include/linux/clk.h[3])。

每个时钟源对象使用一个struct clk结构来表示。而struct clk结构的具体内容由各平台自己定义。clk.h头文件定义了操作一个clk对象的所有接口。内核的其他地方可以也只能使用clk.h中提供的这些接口函数来操作clk。Rust for Linux 的 kernel crate 就是对 clk.h 的封装。这也是 Rust for Linux 第四次提审的补丁改进的内容之一。

关于通用时钟框架还有很多细节,这里为了帮助理解代码,只做简要介绍,对细节感兴趣的朋友可以自行检索学习资料(例如:http://www.wowotech.net/pm_subsystem/clk_overview.html[4])。

Rust for Linux 中的简单抽象

Rust 对 clk.h 对绑定相对比较简单,只绑定了部分控制clock的接口,比如 prepare_enable/disable_unprepareget_rate,还有很多接口没有绑定。另外只能用于 atomic 上下文。

但我们能从中学习到 Unsafe Rust的一些最佳实践(注意代码中的注释):

代码语言:javascript
复制
// From: https://github.com/Rust-for-Linux/linux/blob/rust/rust/kernel/clk.rs

use crate::{bindings, error::Result, to_result};
use core::mem::ManuallyDrop;

/// 表示 `struct clk *`
///
/// # Invariants (不变性说明)
///
/// 这个指针来自 C语言 端,这里默认 C 端来的是有效指针,信任 C 端
/// 这种信任对性能有益:零成本(没有检查开销)
pub struct Clk(*mut bindings::clk);

impl Clk {
    /// Creates new clock structure from a raw pointer.
    ///
    /// # Safety
    ///
    /// The pointer must be valid.
    /// 这里使用unsafe 函数,是因为无法保证传入的指针是否有效,只能靠其调用者来保证
    /// 所以无法将其抽象为安全防范
    pub unsafe fn new(clk: *mut bindings::clk) -> Self {
        Self(clk)
    }

    /// Returns value of the rate field of `struct clk`.
    pub fn get_rate(&self) -> usize {
        // SAFETY: The pointer is valid by the type invariant.
        // 安全性说明:这个指针已经有上嘛结构体定义时的不变性来保证安全了,所以这是个安全方法
        unsafe { bindings::clk_get_rate(self.0) as usize }
    }

    /// Prepares and enables the underlying hardware clock.
    ///
    /// This function should not be called in atomic context.
    pub fn prepare_enable(self) -> Result<EnabledClk> {
        // SAFETY: The pointer is valid by the type invariant.
        // 同上
        to_result(|| unsafe { bindings::clk_prepare_enable(self.0) })?;
        Ok(EnabledClk(self))
    }

    impl Drop for Clk {
    fn drop(&mut self) {
        // SAFETY: The pointer is valid by the type invariant.
        unsafe { bindings::clk_put(self.0) };
    }
}


// 对clock 使能
/// A clock variant that is prepared and enabled.
pub struct EnabledClk(Clk);

impl EnabledClk {
    // 获取频率
    /// Returns value of the rate field of `struct clk`.
    pub fn get_rate(&self) -> usize {
        self.0.get_rate()
    }

    /// Disables and later unprepares the underlying hardware clock prematurely.
    ///
    /// This function should not be called in atomic context.
    pub fn disable_unprepare(self) -> Clk {
        let mut clk = ManuallyDrop::new(self);
        // SAFETY: The pointer is valid by the type invariant.
        unsafe { bindings::clk_disable_unprepare(clk.0 .0) };
        core::mem::replace(&mut clk.0, Clk(core::ptr::null_mut()))
    }
}

impl Drop for EnabledClk {
    fn drop(&mut self) {
        // SAFETY: The pointer is valid by the type invariant.
        // 安全性同上
        unsafe { bindings::clk_disable_unprepare(self.0 .0) };
    }
}

上面 clk Rust 安全绑定代码虽然简单,但它反映出了 Rust for Linux 中 Unsafe Rust 安全抽象的一种规范:利用文档注释中不变性声明,强调了对依赖的内核 C 代码的信任,从而减少了检查达到零成本抽象。 关于这一点在 https://github.com/Rust-for-Linux/linux/pull/324[5] 中有很多讨论。

除clk之外还有 Device 也有部分相关代码。设备驱动在操作设备的clock之前,需要先获取和该clock关联的struct clk指针,在 Rust 代码中就是 clk_ptr

代码语言:javascript
复制
// From: https://github.com/Rust-for-Linux/linux/blob/rust/rust/kernel/device.rs

#[cfg(CONFIG_COMMON_CLK)]
use crate::{clk::Clk, error::from_kernel_err_ptr};

// 该 trait 是一个 Unsafe trait
// 注释表明,在实现该 trait 的时候需要注意满足其安全条件
/// A raw device.
///
/// # Safety
///
/// Implementers must ensure that the `*mut device` returned by [`RawDevice::raw_device`] is
/// related to `self`, that is, actions on it will affect `self`. For example, if one calls
/// `get_device`, then the refcount on the device represented by `self` will be incremented.
///
/// Additionally, implementers must ensure that the device is never renamed. Commit a5462516aa994
/// has details on why `device_rename` should not be used.
pub unsafe trait RawDevice {
    
    // do more things

    // clk_get 方法是一个默认实现
    /// Lookups a clock producer consumed by this device.
    ///
    /// Returns a managed reference to the clock producer.
    #[cfg(CONFIG_COMMON_CLK)]
    fn clk_get(&self, id: Option<&CStr>) -> Result<Clk> {
        let id_ptr = match id {
            Some(cstr) => cstr.as_char_ptr(),
            None => core::ptr::null(),
        };

        // 安全性保证: `id_ptr`是一个可选值,要么是有效指针 要么是空指针,应该不会产生UB
        // 但是有效性也是依赖对 Linux C 语言端绑定指针的信任
        // SAFETY: `id_ptr` is optional and may be either a valid pointer
        // from the type invariant or NULL otherwise.
        let clk_ptr = unsafe { from_kernel_err_ptr(bindings::clk_get(self.raw_device(), id_ptr)) }?;

        // 因为上面的 `bindings::clk_get` 安全,那其返回值也认为是有效指针
        // SAFETY: Clock is initialized with valid pointer returned from `bindings::clk_get` call.
        unsafe { Ok(Clk::new(clk_ptr)) }
    }

    // do more things 
}

看得出来, SAFETY 注释粒度非常细,因为它依赖一个前提

另外,关于更多 Unsafe Rust 代码规范可以参考:Rust 编码规范 - Unsafe Rust 部分[6]

参考

  • http://www.wowotech.net/pm_subsystem/clk_overview.html[7]

[1]Rust for Linux 源码导读 | Ref 引用计数容器: https://rustmagazine.github.io/rust_magazine_2021/chapter_12/ref.html

[2]A GPIO driver in Rust: https://lwn.net/Articles/863459/

[3]include/linux/clk.h: https://github.com/Rust-for-Linux/linux/blob/rust/include/linux/clk.h

[4]http://www.wowotech.net/pm_subsystem/clk_overview.html: http://www.wowotech.net/pm_subsystem/clk_overview.html

[5]https://github.com/Rust-for-Linux/linux/pull/324: https://github.com/Rust-for-Linux/linux/pull/324

[6]Rust 编码规范 - Unsafe Rust 部分: https://rust-coding-guidelines.github.io/rust-coding-guidelines-zh/safe-guides/coding_practice/unsafe_rust.html

[7]http://www.wowotech.net/pm_subsystem/clk_overview.html: http://www.wowotech.net/pm_subsystem/clk_overview.html

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-02-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 觉学社 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景概念
  • Rust for Linux 第四次补丁提审
  • Rust 与 Linux 设备驱动开发
    • 基础概念
      • Rust 驱动开发示例
      • Linux 通用时钟框架介绍
        • 背景介绍
          • Rust for Linux 中的简单抽象
          • 参考
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档