Porting Rust STD

Table of Contents

1. Porting Rust 标准库

1.1. overview

rust 标准库包含 core, alloc, std, 当使用 no_std 时, 只会使用 core, alloc.

core, alloc 是平台无关的, 基本不依赖 libc, 除了 memcpy, memmove, memset, memcmp, bcmp, strlen 这几个符号. 为了解决这个问题, rust 的 compiler_builtins 包含了这几个符号的实现

core 和 alloc 包含了 vec, string, fmt, ffi, box, rc, option, result 以及 collections 中的 binary_heap, btree_map 等, 足够开发 bare_metal 的程序. 其中涉及到动态内存分配的功能例如 vec 依赖 app 自己定义的 allocator, 避免了对 libc malloc 的依赖.

linux 下的 rust std 大量依赖 libc.a, 以编译一个空的 rust executable 为例, 输出的 linker map 中引用 lib.a 接近 3000 次. 但这并不表示 libc 之上的 rust std 是平台无关的, rust std 中也有一些平台相关的代码, 例如它的 Platform Abstraction Layer

1.2. 定制 rust toolchain

porting rust 标准库的最终目的是提供一个 toolchain, 包含定制的 libc 以及定制的 rust 标准库

1.2.1. 定制 libc

使用 aarch64_unknown_linux_gnu target 时, 默认会使用系统自带的 /lib/gcc-cross/aarch64-linux-gnu/ 下的 libc, crt{xx}.o, libgcc. 但使用 aarch64_unknown_linux_musl target 时, 默认会使用 rust toolchain 下 self-contained 里放置的 crt{xx}.o 和 libc.a, 通过使用自定义的基于 aarch64_unknown_linux_musl 的 target, 可以方便的定制自己使用的 libc

1.2.2. 定制 std

rust std 中有一个 PAL (Platform Abstraction Layer) 以及散布在代码各处的 cfg 宏, 会针对 os, feature 等进行条件编译, 只替换 libc 并不能完全定制 std, 例如:

cfg_if::cfg_if! {
    if #[cfg(any(
        all(target_os = "windows", not(target_vendor = "win7")),
        target_os = "linux",
        target_os = "android",
        all(target_arch = "wasm32", target_feature = "atomics"),
        target_os = "freebsd",
        target_os = "openbsd",
        target_os = "fuchsia",
    ))] {
        mod futex;
        pub use futex::Parker;
        // ....
    } else if #[cfg(target_family = "unix")] {
        mod pthread;
        pub use pthread::Parker;
    } else {
        mod unsupported;
        pub use unsupported::Parker;
    }
}

针对 linux 会使用 futex 实现 thread parking, 针对 unix 才会使用 pthread, 因此需要针对 cfg 加一些定制, 例如让 std 里的大部分组件的实现和 linux 一致, 但 thread parking 使用 pthread 实现

通过 cargo 的 -Zbuild-std 可以让它每次都重新编译 rust toolchain 中的 rust std 源码, 而不使用预编译的 std, 例如:

$> cat hello/.cargo/cargo.toml
[build]
target = "<path_to>/taishan_rust_toolchain/aarch64-unknonw-taishan-musl.json"

[unstable]
build-std = ["std"]

因此定制一个 rust toolchain 需要:

  1. 包含修改过的 libc.a
  2. 包含修改过的 rust std 源码
  3. 通过 aarch64-unknonw-taishan-musl.json 定制需要的条件编译的配置

1.3. platform 相关接口

有一个简单的方法可以快速识别哪些接口是 platform 相关的: 查找 unsupported 关键字, 因为 unsupported 是 platform 提供的默认的空实现

rust/library/std/src/sys/sync/thread_parking/unsupported.rs
rust/library/std/src/sys/anonymous_pipe/unsupported.rs
rust/library/std/src/sys/pal/unix/process/process_unsupported.rs
rust/library/std/src/sys/pal/unix/process/process_unsupported/wait_status.rs
rust/library/std/src/sys/pal/unsupported/args.rs
rust/library/std/src/sys/pal/unsupported/common.rs
rust/library/std/src/sys/pal/unsupported/env.rs
rust/library/std/src/sys/pal/unsupported/fs.rs
rust/library/std/src/sys/pal/unsupported/io.rs
rust/library/std/src/sys/pal/unsupported/mod.rs
rust/library/std/src/sys/pal/unsupported/net.rs
rust/library/std/src/sys/pal/unsupported/os.rs
rust/library/std/src/sys/pal/unsupported/pipe.rs
rust/library/std/src/sys/pal/unsupported/process.rs
rust/library/std/src/sys/pal/unsupported/stdio.rs
rust/library/std/src/sys/pal/unsupported/thread.rs
rust/library/std/src/sys/pal/unsupported/time.rs
rust/library/std/src/sys/random/unsupported.rs

1.3.1. thread_parking

1.3.1.1. channel
use std::sync::mpsc::channel;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = channel();
    thread::spawn(move || {
        for i in 1..20 {
            tx.send(i).unwrap();
        }
    });
    thread::sleep(Duration::from_millis(2000));
    for i in 1..20 {
        let x = rx.recv().unwrap();
        println!("recv: {:?}", x);
    }
}

默认的 channel (非 sync_channel) 主要通过线程间共享的一个 block 链表并配合 thread_parking 来实现的

mpsc.png

1.3.1.1.1. block 链表
  • block 链表由多个 block 组成, 每个 block 包含 32 个 slot, 每个 slot 保存一个消息
  • 维护着 head_block, tail_block, 分别指向链表的 head 和 tail
  • head_index 保存着 head_block 当前的 index, recv 操作会读取 head_blockhead_index 对应的 slot. tail_index 保存着 tail_block 当前的 index, send 操作会写 tail_blocktail_index 对应的 slot
  • 默认的 channel 是容量不限的, 所以随着不断的 send, block 链表会不断增长
  • head_block 被 recv 读完以后, 当前 head_block 会被释放, 而不是采用 ring buffer 的形式重复使用
1.3.1.1.2. 依赖的同步操作
  1. atomic

    channel 是通过 mpmc (multi-producer multi-consumer) 实现的, head_{block, index} 会被多个 consumer 访问, 通过 atomic 来保证原子操作, 这个通过 compiler intrinsics 就可以支持

  2. thread::yield_now

    当 recv 线程发现当前 head_index 已经达到 BLOCK_CAP (31), 或者 send 线程发现 tail_index 达到 BLOCK_CAP 时, 会通过 thread::yield_now 等待其它 recv (或 send) 线程切换 head_blocktail_block 到新的 block.

    所有 {send, recv} 线程在 {head, tail}_index 到达 BLOCK_CAP-1 时会在 {head, tail}_index 这个 atomic 上竞争, 获胜者会负责切换新的 {head, tail}_block 并重设 index, 这时之前那些 yield 的线程就可以继续工作了

    yield_nowtaget_family 为 unix 时其实现依赖于 libc 的 sched_yield

  3. thread::park{_timeout}

    当 recv 时发现 head_index = tail_indexhead_block = tail_block 时, recv 线程需要通过 thread::park 停下来 (send 线程永远不会 park, 因为默认的 channel 容量是无限的)

    thread::park 在 linux, android, bsd, fuchsia 上使用 futex_wait 实现, 在其它 unix 上使用 libc 的 pthread_cond_{timed}wait 实现

  4. thread::unpark

    和 park 类似, 依赖于 futex_wakepthread_cond_signal

1.3.2. stdio

1.3.3. pipe

1.3.4. process

1.3.5. thread

1.3.6. time

Author: [email protected]
Date: 2024-11-13 Wed 18:38
Last updated: 2024-11-20 Wed 19:32

知识共享许可协议