diff --git a/.claude/skills/cross-kernel-driver/SKILL.md b/.claude/skills/cross-kernel-driver/SKILL.md index d7432e0c28..5590afbe24 100644 --- a/.claude/skills/cross-kernel-driver/SKILL.md +++ b/.claude/skills/cross-kernel-driver/SKILL.md @@ -49,7 +49,7 @@ For nontrivial driver design or refactoring, read `references/architecture.md` b Use `&mut self` APIs where exclusive access is the natural contract. Do not require callers to provide an OS lock as part of the portable abstraction. -For block-device integration in `axplat-dyn`, wrap the portable driver with `rd_block::Interface` and `rd_block::IQueue`, then register it through the existing `PlatformDeviceBlock::register_block` path. Keep `rd-block`/`rdrive` adapter code in platform glue or behind an explicit adapter feature unless the crate's purpose is to expose that interface. +For block-device integration in ArceOS, expose portable block drivers through `rdif_block::Interface` and `rdif_block::IQueue`. Keep queue creation, DMA/wait policy, and IRQ registration in OS glue/runtime layers; the portable boundary should be submit/poll requests plus `handle_irq() -> Event`. Prefer small interfaces: diff --git a/.claude/skills/cross-kernel-driver/references/architecture.md b/.claude/skills/cross-kernel-driver/references/architecture.md index 547f51a907..cfaccebf56 100644 --- a/.claude/skills/cross-kernel-driver/references/architecture.md +++ b/.claude/skills/cross-kernel-driver/references/architecture.md @@ -46,14 +46,15 @@ Runtime/platform integration belongs elsewhere: - `platform/axplat-dyn/src/drivers/soc//...` for SoC platform glue. - `components/axdriver_crates/axdriver_` for common ArceOS-facing driver traits/adapters. -For block devices in `axplat-dyn`, the existing integration path is: +For block devices in `axplat-dyn`, the integration path is: - probe/FDT/MMIO setup in `platform/axplat-dyn/src/drivers/blk/.rs` -- wrap the portable driver in an `rd_block::Interface` -- expose a queue as `rd_block::IQueue` -- register through `PlatformDeviceBlock::register_block`, which creates `rd_block::Block::new(dev, &DmaImpl)` and registers it with `rdrive` +- expose the portable driver as `rdif_block::Interface` +- expose queues as `rdif_block::IQueue` with `submit_request()` / `poll_request()` +- register boxed `rdif_block::Interface` devices through the ArceOS driver glue and `rdrive` +- keep ArceOS sync block reads/writes, DMA bounce buffers, and IRQ registration policy above the portable interface -Keep `rd-block`/`rdrive` coupling in this adapter layer or behind an explicit adapter feature. Portable driver core should not need to know how `rdrive` probes or registers devices. +Keep `rdrive` coupling in this adapter layer or behind an explicit adapter feature. Portable driver core should not need to know how `rdrive` probes or registers devices. ## Dependency Boundaries @@ -172,11 +173,11 @@ Runtime wrappers can then choose: Avoid a single global `Driver::poll` if the hardware naturally exposes multiple queues or engines. Avoid a "big object + big lock + callbacks" shape unless the device is truly that simple. -For a block queue adapter, align portable queue state with `rd_block::IQueue`: +For a block queue adapter, align portable queue state with `rdif_block::IQueue`: -- `buff_config()` should expose block-size, alignment, and DMA mask constraints. -- `submit_request()` should allocate/map DMA buffers, program descriptors, and return a request id. -- `poll_request()` should check completion and translate device status into `rd_block::BlkError` in the adapter. +- `buffer_config()` should expose block-size, alignment, and DMA mask constraints. +- `submit_request()` should program descriptors and return a request id without installing OS wakeups. +- `poll_request()` should check completion and return `RequestStatus::Pending` or `RequestStatus::Complete`. - Keep descriptor ownership and DMA map/unmap pairing explicit for each request id. ## Concurrency Rules diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4465357e09..cf8dcba1cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,6 +66,7 @@ jobs: uses: dorny/paths-filter@v4 with: base: ${{ github.event_name == 'push' && github.ref || '' }} + token: "" filters: | ci_checks: - ".cargo/**" diff --git a/Cargo.lock b/Cargo.lock index 3b4b99b53d..e649a5c6d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -887,11 +887,12 @@ dependencies = [ "ixgbe-driver", "log", "mmio-api", + "nvme-driver", "pcie", "phytium-mci-host", "ramdisk", - "rd-block", "rd-net", + "rdif-block", "rdif-clk", "rdif-display", "rdif-input", @@ -993,7 +994,6 @@ dependencies = [ "log", "lru 0.16.4", "lwext4_rust", - "rd-block-volume", "rsext4", "scope-local", "slab", @@ -1462,7 +1462,7 @@ dependencies = [ "ax-riscv-plic", "axklib", "log", - "rd-block", + "rdif-block", "riscv 0.16.0", "sbi-rt 0.0.3", "sg200x-bsp", @@ -1599,7 +1599,6 @@ dependencies = [ "cfg-if", "chrono", "indoc", - "rd-block", "rd-net", "rdrive", "spin 0.12.0", @@ -4392,9 +4391,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "eb92f162bf56536459fc83c79b974bb12837acfed43d6bc370a7916d0ae15ecc" dependencies = [ "atomic-waker", "bytes", @@ -5714,7 +5713,7 @@ dependencies = [ "log", "mmio-api", "pcie", - "rd-block", + "rdif-block", "spin 0.12.0", "tock-registers 0.10.1", ] @@ -6037,12 +6036,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" version = "0.3.33" @@ -6380,7 +6373,6 @@ name = "ramdisk" version = "0.1.1" dependencies = [ "dma-api", - "rd-block", "rdif-block", "spin 0.12.0", ] @@ -6592,20 +6584,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rd-block" -version = "0.1.2" -dependencies = [ - "dma-api", - "futures", - "rdif-block", - "spin_on", -] - -[[package]] -name = "rd-block-volume" -version = "0.1.0" - [[package]] name = "rd-net" version = "0.1.1" @@ -7986,15 +7964,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "spin_on" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076e103ed41b9864aa838287efe5f4e3a7a0362dd00671ae62a212e5e4612da2" -dependencies = [ - "pin-utils", -] - [[package]] name = "spinning_top" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 0b26287924..bc93d482c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -271,8 +271,6 @@ riscv_vplic = { version = "0.4.12", path = "virtualization/riscv_vplic" } nvme-driver = { version = "0.4.2", path = "drivers/blk/nvme-driver" } pcie = { version = "0.6.1", path = "drivers/pci/pcie" } ramdisk = { version = "0.1.1", path = "drivers/blk/ramdisk" } -rd-block = { version = "0.1.2", path = "drivers/blk/rd-block" } -rd-block-volume = { version = "0.1.0", path = "drivers/blk/rd-block-volume" } rd-net = { version = "0.1.1", path = "drivers/net/rd-net" } rdif-base = { version = "0.8.1", path = "drivers/interface/rdif-base" } rdif-block = { version = "0.7.1", path = "drivers/interface/rdif-block" } diff --git a/docs/docs/architecture/rdrive-rdif.md b/docs/docs/architecture/rdrive-rdif.md index 9a6c2727c8..74f39a7d96 100644 --- a/docs/docs/architecture/rdrive-rdif.md +++ b/docs/docs/architecture/rdrive-rdif.md @@ -5,7 +5,7 @@ sidebar_label: "rdrive + rdif 驱动框架" # rdrive + rdif 驱动框架 -本文记录 #606 的宿主物理设备重构目标。新的设备路径硬切到 `rdrive + rdif`:`rdrive` 负责发现、probe、注册和查询,`rdif-*` / `rd-*` 负责能力接口和运行时封装,上层系统按领域能力消费设备。旧驱动接口包组已移除,宿主物理设备初始化与交付主线不再经过 legacy driver crates。 +本文记录 #606 的宿主物理设备重构目标。新的设备路径硬切到 `rdrive + rdif`:`rdrive` 负责发现、probe、注册和查询,`rdif-*` 负责能力接口,必要时由对应领域 crate 提供运行时封装,上层系统按领域能力消费设备。旧驱动接口包组已移除,宿主物理设备初始化与交付主线不再经过 legacy driver crates;块设备路径中原有 runtime 已删除,统一以 `rdif-block` 作为 block capability boundary。 `axdevice` 与 `axdevice_base` 不纳入本轮迁移。它们继续作为 Axvisor / axvm 的 guest emulated device model,不参与宿主物理设备 probe,不作为 FS、NET、display、input、vsock 的设备来源。 @@ -51,7 +51,7 @@ flowchart TB end subgraph Capability["Capability Boundary"] - RdifBlock["rdif-block + rd-block"] + RdifBlock["rdif-block"] RdifEth["rdif-eth + rd-net"] RdifDisplay["rdif-display"] RdifInput["rdif-input"] @@ -161,17 +161,23 @@ sequenceDiagram ## Capability Boundary -`rdif-*` 是能力边界,只定义某类设备向上暴露什么能力,不负责设备发现、iomap、IRQ 注册、任务调度或系统启动顺序。`rd-*` 是 runtime wrapper,负责 waker、poll、blocking API、buffer pool 等运行时行为。 +`rdif-*` 是能力边界,只定义某类设备向上暴露什么能力,不负责设备发现、iomap、IRQ 注册、任务调度或系统启动顺序。块设备已移除原 runtime crate,`rdif-block` 直接承载设备 LBA 语义的 submit/poll block capability boundary;其它领域如网络仍可按需保留 runtime wrapper,负责 waker、poll、blocking API、buffer pool 等运行时行为。 | 能力 | interface crate | runtime crate | 上层消费 | | --- | --- | --- | --- | -| 块设备 | `rdif-block` | `rd-block` | block volume service、FS | +| 块设备 | `rdif-block` | 已删除,直接消费 `rdif-block` submit/poll 边界 | block volume service、FS | | 网络设备 | `rdif-eth` | `rd-net` | net interface service、NET/NET-NG | | 显示 | `rdif-display` | `rd-display` | display service、Starry fb | | 输入 | `rdif-input` | `rd-input` | input service、Starry input | | vsock | `rdif-vsock` | `rd-vsock` | vsock service | | 平台设备 | `rdif-intc`、`rdif-pcie`、`rdif-clk`、`rdif-timer`、`rdif-systick`、`rdif-serial` | 按需 | HAL、Axvisor backend、平台 glue | +`rdif-block` 的块请求不暴露 Linux block layer 的 512B sector 公共单位,而使用真实设备的 `lba` / `block_count` / `logical_block_size`。OS glue 负责把上层 byte offset、FS block、Linux-like sector 或分区 region 转换成设备 LBA。接口保留 blk-mq 风格的结构能力:设备可报告 `QueueTopology`,OS 可创建一个或多个 queue,每个 queue 使用 queue-local `RequestId`/tag,经 `submit_request()` 提交、经 `poll_request()` 回收完成。 + +块 IRQ 事件按 source 和 queue 分离。`Interface::irq_sources()` 返回可用 IRQ source 列表,每个 `IrqSourceInfo { id, queues }` 描述该 source 可能影响的 queue mask;`take_irq_handler(source_id)` 只能取走对应 handler。单 INTx/legacy 设备使用 source `0`,未来 NVMe MSI-X 可以把不同 vector 暴露为不同 source。当前 ArceOS `ax-driver` / `ax-runtime` glue 仍只消费 legacy source `0`,保持一个 block 设备一个 IRQ handler 的注册方式。 + +`IrqHandler::handle_irq()` 只确认中断源并返回可 poll 的 queue mask,不做 OS wake、不阻塞、不持有 OS 锁,也不在中断上下文推进慢路径完成。收到事件后,runtime 或 task-side wrapper 再对相应 queue 调用 `poll_request()`。 + 新增接口按多文件拆分: - `rdif-display/src/lib.rs` 只 re-export;`types.rs` 定义 `DisplayInfo`、`PixelFormat`、`FrameBuffer<'_>`;`error.rs` 定义 `DisplayError`;`interface.rs` 定义 `Interface` 和 `Event`。 @@ -229,9 +235,9 @@ IRQ 路径只返回稳定事件和唤醒等待方;不能在 IRQ handler 中执 | Driver Core | `drivers//` | `no_std`、寄存器/队列/描述符、`mmio-api`、`dma-api` 小边界 | `ax-driver`、`ax-hal`、`axplat-dyn`、`rdrive::PlatformDevice` | | Capability Boundary | `drivers/interface/rdif-*` | `rdif-base`、小型错误和事件类型 | 平台、runtime、任务调度 | | OS Glue | `platforms/axplat-dyn/src/drivers/*` 或平台 crate | `rdrive::module_driver!`、FDT/PCI/Static probe、iomap、IRQ 注册、DMA op | 上层 FS/NET 策略 | -| Runtime | `drivers/*/rd-*` | `rdif-*`、waker、poll/blocking wrapper、buffer pool | probe、设备树、ACPI、平台选择 | +| Runtime | `drivers/*/rd-*`,块设备除外 | `rdif-*`、waker、poll/blocking wrapper、buffer pool | probe、设备树、ACPI、平台选择 | -Driver Core 只推进硬件状态机。OS Glue 将硬件实例包装成 `rdif-*::Interface` 后通过 `PlatformDevice::register(...)` 注册。Runtime wrapper 从 `rdif-*::Interface` 构建领域运行时对象,供服务层和上层模块使用。 +Driver Core 只推进硬件状态机。OS Glue 将硬件实例包装成 `rdif-*::Interface` 后通过 `PlatformDevice::register(...)` 注册。除块设备外,Runtime wrapper 从 `rdif-*::Interface` 构建领域运行时对象,供服务层和上层模块使用;块设备服务直接基于 `rdif-block` 的 submit/poll 能力边界组织 volume 和文件系统入口。 ## 领域 Service 与上层消费 @@ -266,7 +272,7 @@ pub struct BlockVolume { block volume 层负责: -- 从 `rd-block` / `rdif-block` 枚举 physical disk。 +- 从 `rdif-block` 枚举 physical disk。 - 支持 GPT、MBR、raw disk。 - 产出稳定 volume metadata。 - 提供裁剪到 `BlockRegion` 的 block reader。 @@ -316,7 +322,7 @@ src/ | 文件 | 当前问题 | 拆分方向 | | --- | --- | --- | | `platforms/axplat-dyn/src/drivers/pci/rk3588.rs` | 单文件超过 600 行 | RC init、ATU/window、MSI/IRQ、config space、FDT glue | -| `platforms/axplat-dyn/src/drivers/blk/rockchip_sd.rs` | 单文件超过 600 行 | probe/FDT、clock/tuning、card init、rd-block adapter | +| `platforms/axplat-dyn/src/drivers/blk/rockchip_sd.rs` | 单文件超过 600 行 | probe/FDT、clock/tuning、card init、rdif-block adapter | | `platforms/axplat-dyn/src/drivers/blk/mod.rs` | 容器、adapter、IRQ、FDT decode 混杂 | registry、adapter、irq、probe | | `platforms/axplat-dyn/src/drivers/mod.rs` | 设备收集、iomap、DMA 混杂 | device collection、iomap、dma | diff --git a/docs/docs/components/crates/ax-api.md b/docs/docs/components/crates/ax-api.md index a489d93256..db419d9f6f 100644 --- a/docs/docs/components/crates/ax-api.md +++ b/docs/docs/components/crates/ax-api.md @@ -67,7 +67,7 @@ graph LR - `ax-config-macros` - `ax-cpu` - `rdrive` -- `rd-block` +- `rdif-block` - `rdif-display` - `rdif-input` - 另外还有 `48` 个同类项未在此展开 diff --git a/docs/docs/components/crates/ax-driver.md b/docs/docs/components/crates/ax-driver.md index ad31157a6a..23f2ae7424 100644 --- a/docs/docs/components/crates/ax-driver.md +++ b/docs/docs/components/crates/ax-driver.md @@ -11,7 +11,7 @@ `ax-driver` 负责把平台发现结果转成上层可消费的设备能力: - 向下连接 FDT、PCI、VirtIO、SoC/板级驱动和 MMIO/DMA 能力。 -- 向上通过 `rdrive` 注册表和 `rd-block`、`rd-net`、`rdif-display`、`rdif-input`、`rdif-vsock` 暴露设备。 +- 向上通过 `rdrive` 注册表和 `rdif-block`、`rd-net`、`rdif-display`、`rdif-input`、`rdif-vsock` 暴露设备。 - 在 ArceOS、StarryOS、Axvisor 之间复用驱动 core,同时把 OS glue 限定在 probe、iomap、IRQ 注册和运行时适配层。 ## 主要模块 @@ -29,13 +29,13 @@ graph LR dma["dma-api / mmio-api / axklib"] --> ax_driver concrete["virtio-drivers / pcie / board driver crates"] --> ax_driver ax_driver --> rdrive["rdrive"] - rdrive --> rdif["rd-block / rd-net / rdif-*"] + rdrive --> rdif["rdif-block / rd-net / rdif-*"] rdif --> upper["ax-fs / ax-net / ax-display / ax-input / starry-kernel"] ``` ### 直接依赖 -- `rdrive`、`rd-block`、`rd-net`、`rdif-*`:设备注册、查询和领域能力接口。 +- `rdrive`、`rdif-block`、`rd-net`、`rdif-*`:设备注册、查询和领域能力接口。 - `dma-api`、`mmio-api`、`axklib`:DMA、MMIO 和内核映射能力边界。 - `virtio-drivers`、`pcie`、SoC/板级驱动 crate:具体硬件协议或平台 glue。 - `ax-alloc`、`ax-errno`、`ax-kspin` 等:分配、错误映射和同步路径。 diff --git a/docs/docs/components/crates/ax-feat.md b/docs/docs/components/crates/ax-feat.md index 8f8b78ce6d..13dce949b9 100644 --- a/docs/docs/components/crates/ax-feat.md +++ b/docs/docs/components/crates/ax-feat.md @@ -72,7 +72,7 @@ graph LR - `ax-cpu` - `ax-dma` - `rdrive` -- `rd-block` +- `rdif-block` - `rdif-display` - `rdif-input` - 另外还有 `48` 个同类项未在此展开 diff --git a/docs/docs/components/crates/ax-fs-ng.md b/docs/docs/components/crates/ax-fs-ng.md index eb89e7bb05..0aa3365ff6 100644 --- a/docs/docs/components/crates/ax-fs-ng.md +++ b/docs/docs/components/crates/ax-fs-ng.md @@ -69,7 +69,7 @@ graph LR - `ax-cpu` - `ax-dma` - `rdrive` -- `rd-block` +- `rdif-block` - 另外还有 `37` 个同类项未在此展开 ### 3.3 被依赖情况 diff --git a/docs/docs/components/crates/ax-hal.md b/docs/docs/components/crates/ax-hal.md index 708885cf8d..6d51b26891 100644 --- a/docs/docs/components/crates/ax-hal.md +++ b/docs/docs/components/crates/ax-hal.md @@ -137,7 +137,7 @@ graph LR - `ax-alloc`:在页表/虚拟化路径下承担帧或内存块来源。 ### 间接依赖 -- 各类驱动能力接口,如 `rdrive`、`rd-block` 和 `rdif-*`,会通过平台与上层模块间接参与 bring-up。 +- 各类驱动能力接口,如 `rdrive`、`rdif-block` 和其它 `rdif-*`,会通过平台与上层模块间接参与 bring-up。 - `ax-percpu`、`kernel_guard`、`memory_addr` 等基础组件通过 `ax-cpu`、`axplat` 或 `paging` 路径提供底层支持。 ### 3.3 关键直接消费者 diff --git a/docs/docs/components/crates/ax-runtime.md b/docs/docs/components/crates/ax-runtime.md index 00e065b7bf..1324275856 100644 --- a/docs/docs/components/crates/ax-runtime.md +++ b/docs/docs/components/crates/ax-runtime.md @@ -71,7 +71,7 @@ graph LR - `ax-cpu` - `ax-dma` - `rdrive` -- `rd-block` +- `rdif-block` - `rd-net` - `rdif-display` - 另外还有 `43` 个同类项未在此展开 diff --git a/docs/docs/components/crates/axplat-dyn.md b/docs/docs/components/crates/axplat-dyn.md index 8d67c3cd75..8cb072d14f 100644 --- a/docs/docs/components/crates/axplat-dyn.md +++ b/docs/docs/components/crates/axplat-dyn.md @@ -16,7 +16,7 @@ - 向下:依赖 `somehal` 提供的入口宏、内存映射、控制台、时钟、中断、电源和 CPU 元数据。 - 向上:实现 `InitIf`、`ConsoleIf`、`MemIf`、`TimeIf`、`PowerIf` 以及可选 `IrqIf`,并通过 `ax_plat::call_main()` / `call_secondary_main()` 把控制权交给内核入口。 -- 向旁:公开 `drivers` 模块,为 `ax-driver` 的 `dyn` 设备模型提供基于 `rdrive` 的设备探测与块设备封装。 +- 向旁:公开 `drivers` 模块,为 `ax-driver` 的 `dyn` 设备模型提供基于 `rdrive` 和 `rdif-block` 的设备探测与块设备封装。 - 向链接层:通过 `build.rs` 生成 `axplat.x`,把 `linkme` 段、异常表和若干 ArceOS 约定符号补进最终镜像。 这决定了它与普通板级包的一个根本差异: @@ -46,7 +46,7 @@ | --- | --- | --- | --- | | 启动 glue | `somehal` | `boot.rs` | 把主核/次核入口收敛到 `ax_plat::call_main()` / `call_secondary_main()` | | 平台契约 glue | `axplat` | `init.rs`、`console.rs`、`mem.rs`、`generic_timer.rs`、`irq.rs`、`power.rs` | 用 `#[impl_plat_interface]` 把 `somehal` 能力接到 `axplat` trait | -| 设备 glue | `rdrive`、`rd-block`、`ax-driver` | `drivers/*` | 把 FDT/PCIe/VirtIO 探测结果转成 `rdrive` 可查询的动态块设备 | +| 设备 glue | `rdrive`、`rdif-block`、`ax-driver` | `drivers/*` | 把 FDT/PCIe/VirtIO 探测结果转成 `rdrive` 可查询的动态块设备 | | 链接 glue | `build.rs`、`link.ld` | crate 根目录 | 生成 `axplat.x`,插入所需段和符号,适配 ArceOS 链接约定 | 这套装配方式说明它承担的是“桥接与接线”职责,而不是“重新定义平台抽象”。 @@ -132,13 +132,13 @@ flowchart TD 2. 调用 `rdrive::probe_all(true)` 触发探测。 3. `drivers/pci.rs` 通过 `module_driver!` 注册通用 PCIe ECAM 控制器探测器。 4. `drivers/blk/virtio.rs` 通过 `module_driver!` 注册 `virtio,mmio` block 设备探测器。 -5. 探测出的 `rd_block::Block` 被包成实现 `ax_driver_block::BlockDriverOps` 的 `Block`,最终进入 `BLOCK_DEVICES`。 +5. 探测出的 `rdif_block::Interface` 设备被包成实现 `ax_driver_block::BlockDriverOps` 的 `Block`,最终进入 `BLOCK_DEVICES`。 6. `drivers/ax-driver/src/dyn_drivers/mod.rs` 再通过 `take_block_devices()` 把它们取走,转成 `ax-driver` 的动态设备集合。 ```mermaid flowchart TD A[rdrive::probe_all] --> B[PCIe / VirtIO probe modules] - B --> C[rd_block::Block] + B --> C[rdif_block::Interface] C --> D[drivers::blk::Block] D --> E[BLOCK_DEVICES] E --> F[ax-driver::dyn_drivers::probe_all_devices] @@ -197,7 +197,7 @@ flowchart TD | `ax-cpu` | 在 `hv` 等场景提供 CPU 模式支持 | | `axklib` | 提供 `iomap()` 等内核内存映射辅助 | | `ax-driver` | 提供共享驱动 probe 与 adapter 入口 | -| `rdrive`、`rd-block` | 提供运行时设备探测与块设备抽象 | +| `rdrive`、`rdif-block` | 提供运行时设备探测与块设备能力边界 | | `dma-api` | 为设备 DMA 提供抽象接口 | | `heapless`、`spin` | 用于固定容量缓存与锁/一次初始化结构 | @@ -213,7 +213,7 @@ flowchart TD graph TD A[somehal / ax-cpu / axklib] --> B[axplat-dyn] C[axplat] --> B - D[rdrive / rd-block / dma-api / ax-driver] --> B + D[rdrive / rdif-block / dma-api / ax-driver] --> B B --> E[ax-hal: plat-dyn] B --> F[ax-driver: dyn] diff --git a/docs/docs/components/layers.md b/docs/docs/components/layers.md index c71560dab7..7039d0d051 100644 --- a/docs/docs/components/layers.md +++ b/docs/docs/components/layers.md @@ -284,7 +284,7 @@ flowchart TB | `ax-ctor-bare-macros` | 0 | Macros for registering constructor functions for … | — | `ax-ctor-bare` | | `ax-display` | 10 | ArceOS graphics module | `ax-driver` `ax-lazyinit` `ax-sync` | `ax-api` `ax-feat` `ax-runtime` `starry-kernel` | | `ax-dma` | 8 | ArceOS global DMA allocator | `ax-alloc` `ax-allocator` `ax-config` `ax-hal` `ax-kspin` `ax-memory-addr` `ax-mm` | `ax-api` `ax-driver` | -| `ax-driver` | 9 | ArceOS device drivers | `anyhow` `ax-alloc` `ax-crate-interface` `ax-dma` `ax-errno` `ax-kernel-guard` `ax-kspin` `axklib` `dma-api` `mmio-api` `rdrive` `rd-block` `rd-net` `rdif-*` `virtio-drivers` | `ax-api` `ax-display` `ax-feat` `ax-fs` `ax-fs-ng` `ax-input` `ax-net` `ax-net-ng` `ax-runtime` `starry-kernel` | +| `ax-driver` | 9 | ArceOS device drivers | `anyhow` `ax-alloc` `ax-crate-interface` `ax-dma` `ax-errno` `ax-kernel-guard` `ax-kspin` `axklib` `dma-api` `mmio-api` `rdrive` `rd-net` `rdif-*` `virtio-drivers` | `ax-api` `ax-display` `ax-feat` `ax-fs` `ax-fs-ng` `ax-input` `ax-net` `ax-net-ng` `ax-runtime` `starry-kernel` | | `ax-errno` | 0 | Generic error code representation. | — | `arm_vcpu` `arm_vgic` `ax-alloc` `ax-allocator` `ax-api` `ax-driver` `ax-fs` `ax-fs-ng` `ax-fs-vfs` `ax-io` `ax-libc` `ax-memory-set` `ax-mm` `ax-net` `ax-net-ng` `ax-page-table-multiarch` `ax-posix-api` `ax-std` `ax-task` `axaddrspace` `axdevice` `axdevice_base` `axfs-ng-vfs` `axhvc` `axklib` `axplat-dyn` `axvcpu` `axvisor` `axvm` `axvmconfig` `riscv_vcpu` `riscv_vplic` `starry-kernel` `starry-vm` `x86_vcpu` `x86_vlapic` | | `ax-feat` | 13 | Top-level feature selection for ArceOS | `ax-alloc` `ax-config` `ax-display` `ax-driver` `ax-fs` `ax-fs-ng` `ax-hal` `ax-input` `ax-ipi` `ax-kspin` `ax-log` `ax-net` `ax-runtime` `ax-sync` `ax-task` `axbacktrace` | `ax-api` `ax-libc` `ax-posix-api` `ax-std` `starry-kernel` `starryos` `starryos-test` | | `ax-fs` | 10 | ArceOS filesystem module | `ax-cap-access` `ax-driver` `ax-errno` `ax-fs-devfs` `ax-fs-ramfs` `ax-fs-vfs` `ax-hal` `ax-io` `ax-lazyinit` `rsext4` | `ax-api` `ax-feat` `ax-posix-api` `ax-runtime` | diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js index 4495589b35..260328a254 100644 --- a/docs/src/pages/index.js +++ b/docs/src/pages/index.js @@ -745,7 +745,7 @@ function DriverSection() { ]; const driverSubsystems = [ - { name: 'rd-block', label: '块设备' }, + { name: 'rdif-block', label: '块设备' }, { name: 'rd-net', label: '网络' }, { name: 'rdif-display', label: '显示' }, { name: 'rdif-input', label: '输入' }, diff --git a/drivers/ax-driver/Cargo.toml b/drivers/ax-driver/Cargo.toml index a23d8dbf09..8dcac250e3 100644 --- a/drivers/ax-driver/Cargo.toml +++ b/drivers/ax-driver/Cargo.toml @@ -17,7 +17,7 @@ plat-static = [] plat-dyn = ["dep:fdt-edit"] irq = [] -block = ["dep:ax-errno", "dep:ax-kspin", "dep:rd-block"] +block = ["dep:ax-errno", "dep:ax-kspin", "dep:rdif-block"] net = ["dep:rd-net"] display = ["dep:ax-errno", "dep:rdif-display"] input = ["dep:ax-errno", "dep:rdif-input"] @@ -33,8 +33,9 @@ virtio-socket = ["vsock", "virtio"] ramdisk = ["block", "dep:ramdisk"] bcm2835-sdhci = ["block", "dep:bcm2835-sdhci"] -cvsd = ["block", "plat-dyn", "dep:sg200x-bsp"] ahci = ["block", "dep:simple-ahci"] +nvme = ["block", "plat-static", "dep:nvme-driver"] +cvsd = ["block", "plat-dyn", "dep:sg200x-bsp"] ixgbe = ["net", "dep:ax-alloc", "dep:ixgbe-driver"] fxmac = ["net", "dep:ax-alloc", "dep:ax-crate-interface", "dep:fxmac_rs"] intel-net = ["net", "dep:eth-intel"] @@ -86,6 +87,7 @@ rk3588-pcie = [ rockchip-soc = ["plat-dyn", "dep:rdif-clk", "dep:rockchip-soc"] rockchip-pm = ["rockchip-soc", "dep:rockchip-pm"] list-pci-devices = [] +pci-list-devices = ["list-pci-devices"] [dependencies] anyhow = { workspace = true } @@ -108,10 +110,11 @@ heapless.workspace = true ixgbe-driver = { workspace = true, optional = true } log.workspace = true mmio-api.workspace = true +nvme-driver = { workspace = true, optional = true } pcie.workspace = true ramdisk = { workspace = true, optional = true } -rd-block = { workspace = true, optional = true } rd-net = { workspace = true, optional = true } +rdif-block = { workspace = true, optional = true } rdif-clk = { workspace = true, optional = true } rdif-display = { workspace = true, optional = true } rdif-input = { workspace = true, optional = true } diff --git a/drivers/ax-driver/src/block/ahci.rs b/drivers/ax-driver/src/block/ahci.rs index c7b54ce28c..00e621b688 100644 --- a/drivers/ax-driver/src/block/ahci.rs +++ b/drivers/ax-driver/src/block/ahci.rs @@ -79,29 +79,29 @@ impl SyncBlockOps for AhciBlock { self.0.block_size() } - fn read_blocks(&mut self, block_id: u64, buf: &mut [u8]) -> Result<(), rd_block::BlkError> { + fn read_blocks(&mut self, block_id: u64, buf: &mut [u8]) -> Result<(), rdif_block::BlkError> { if !buf.len().is_multiple_of(self.block_size()) || !(buf.as_ptr() as usize).is_multiple_of(4) { - return Err(rd_block::BlkError::NotSupported); + return Err(rdif_block::BlkError::NotSupported); } if self.0.read(block_id, buf) { Ok(()) } else { - Err(rd_block::BlkError::Other("AHCI read failed".into())) + Err(rdif_block::BlkError::Other("AHCI read failed")) } } - fn write_blocks(&mut self, block_id: u64, buf: &[u8]) -> Result<(), rd_block::BlkError> { + fn write_blocks(&mut self, block_id: u64, buf: &[u8]) -> Result<(), rdif_block::BlkError> { if !buf.len().is_multiple_of(self.block_size()) || !(buf.as_ptr() as usize).is_multiple_of(4) { - return Err(rd_block::BlkError::NotSupported); + return Err(rdif_block::BlkError::NotSupported); } if self.0.write(block_id, buf) { Ok(()) } else { - Err(rd_block::BlkError::Other("AHCI write failed".into())) + Err(rdif_block::BlkError::Other("AHCI write failed")) } } } diff --git a/drivers/ax-driver/src/block/bcm2835.rs b/drivers/ax-driver/src/block/bcm2835.rs index ee656103eb..dff5e81be7 100644 --- a/drivers/ax-driver/src/block/bcm2835.rs +++ b/drivers/ax-driver/src/block/bcm2835.rs @@ -18,14 +18,12 @@ pub fn register(plat_dev: PlatformDevice) -> Result<(), OnProbeError> { struct Bcm2835Sdhci(EmmcCtl); impl Bcm2835Sdhci { - fn try_new() -> Result { + fn try_new() -> Result { let mut ctrl = EmmcCtl::new(); if ctrl.init() == 0 { Ok(Self(ctrl)) } else { - Err(rd_block::BlkError::Other( - "BCM2835 SDHCI init failed".into(), - )) + Err(rdif_block::BlkError::Other("BCM2835 SDHCI init failed")) } } } @@ -43,28 +41,28 @@ impl SyncBlockOps for Bcm2835Sdhci { self.0.get_block_size() } - fn read_blocks(&mut self, block_id: u64, buf: &mut [u8]) -> Result<(), rd_block::BlkError> { + fn read_blocks(&mut self, block_id: u64, buf: &mut [u8]) -> Result<(), rdif_block::BlkError> { let block_count = buf.len() / BLOCK_SIZE; if block_count == 0 || !buf.len().is_multiple_of(BLOCK_SIZE) { - return Err(rd_block::BlkError::NotSupported); + return Err(rdif_block::BlkError::NotSupported); } let (prefix, aligned, suffix) = unsafe { buf.align_to_mut::() }; if !prefix.is_empty() || !suffix.is_empty() { - return Err(rd_block::BlkError::NotSupported); + return Err(rdif_block::BlkError::NotSupported); } self.0 .read_block(block_id as u32, block_count, aligned) .map_err(map_sdhci_err) } - fn write_blocks(&mut self, block_id: u64, buf: &[u8]) -> Result<(), rd_block::BlkError> { + fn write_blocks(&mut self, block_id: u64, buf: &[u8]) -> Result<(), rdif_block::BlkError> { let block_count = buf.len() / BLOCK_SIZE; if block_count == 0 || !buf.len().is_multiple_of(BLOCK_SIZE) { - return Err(rd_block::BlkError::NotSupported); + return Err(rdif_block::BlkError::NotSupported); } let (prefix, aligned, suffix) = unsafe { buf.align_to::() }; if !prefix.is_empty() || !suffix.is_empty() { - return Err(rd_block::BlkError::NotSupported); + return Err(rdif_block::BlkError::NotSupported); } self.0 .write_block(block_id as u32, block_count, aligned) @@ -72,11 +70,11 @@ impl SyncBlockOps for Bcm2835Sdhci { } } -fn map_sdhci_err(err: SDHCIError) -> rd_block::BlkError { +fn map_sdhci_err(err: SDHCIError) -> rdif_block::BlkError { match err { - SDHCIError::Again => rd_block::BlkError::Retry, - SDHCIError::NoMemory => rd_block::BlkError::NoMemory, - SDHCIError::Unsupported => rd_block::BlkError::NotSupported, - _ => rd_block::BlkError::Other("BCM2835 SDHCI I/O error".into()), + SDHCIError::Again => rdif_block::BlkError::Retry, + SDHCIError::NoMemory => rdif_block::BlkError::NoMemory, + SDHCIError::Unsupported => rdif_block::BlkError::NotSupported, + _ => rdif_block::BlkError::Other("BCM2835 SDHCI I/O error"), } } diff --git a/drivers/ax-driver/src/block/binding.rs b/drivers/ax-driver/src/block/binding.rs index b5a13ecefb..30d781a90e 100644 --- a/drivers/ax-driver/src/block/binding.rs +++ b/drivers/ax-driver/src/block/binding.rs @@ -1,44 +1,55 @@ use alloc::{ + boxed::Box, string::{String, ToString}, vec::Vec, }; -#[cfg(feature = "irq")] -use alloc::{ - sync::{Arc, Weak}, - task::Wake, -}; -#[cfg(feature = "irq")] -use core::{ - future::Future, - pin::pin, - sync::atomic::{AtomicBool, Ordering}, - task::{Context, Poll, Waker}, -}; +use core::alloc::Layout; use ax_errno::{AxError, AxResult}; use ax_kspin::SpinNoIrq; +use dma_api::{ContiguousArray, DeviceDma, DmaDirection}; use log::{error, warn}; -use rd_block::BlkError; +use rdif_block::{ + BlkError, Buffer, IQueue, Interface, QueueInfo, Request, RequestFlags, RequestId, RequestOp, + RequestStatus, TransferChunk, TransferPlan, TransferPlanner, TransferRuntimeCaps, +}; use rdrive::Device; + pub struct Block { name: String, irq_num: Option, + irq_enabled: bool, #[cfg(feature = "irq")] - irq_state: Option>, - queue: SpinNoIrq, + irq_handler: Option, + interface: Box, + queues: SpinNoIrq, +} + +struct BlockQueues { + queue: Box, + pool: BlockBufferPool, +} + +struct BlockBufferPool { + dma: DeviceDma, + planner: TransferPlanner, + size: usize, + align: usize, } pub struct PlatformBlockDevice { name: String, - block: Option, + interface: Option>, irq_num: Option, } +const MAX_BLOCK_BUFFER_SIZE: usize = 16 * 1024; + impl PlatformBlockDevice { - fn new(name: String, block: rd_block::Block, irq_num: Option) -> Self { + fn new(name: String, interface: Box, irq_num: Option) -> Self { Self { name, - block: Some(block), + interface: Some(interface), irq_num, } } @@ -51,40 +62,23 @@ impl rdrive::DriverGeneric for PlatformBlockDevice { } #[cfg(feature = "irq")] -struct BlockIrqState { - handler: rd_block::IrqHandler, -} - -#[cfg(feature = "irq")] -impl BlockIrqState { - fn handle_irq(&self) { - self.handler.handle(); - } -} - -#[cfg(feature = "irq")] -struct BlockIrqWaiter { - woken: AtomicBool, +pub struct BlockIrqHandler { + handler: Box, } #[cfg(feature = "irq")] -impl Wake for BlockIrqWaiter { - fn wake(self: Arc) { - self.wake_by_ref(); +impl BlockIrqHandler { + fn new(handler: Box) -> Self { + Self { handler } } - fn wake_by_ref(self: &Arc) { - self.woken.store(true, Ordering::Release); + pub fn handle(&self) -> rdif_block::Event { + self.handler.handle_irq() } } -#[cfg(feature = "irq")] -const BLOCK_IRQ_SLOTS: usize = 16; -#[cfg(feature = "irq")] -static IRQ_SOURCES: [SpinNoIrq>>; BLOCK_IRQ_SLOTS] = - [const { SpinNoIrq::new(None) }; BLOCK_IRQ_SLOTS]; -#[cfg(feature = "irq")] -const BLOCK_IRQ_REPOLL_SPINS: usize = 256; +#[cfg(not(feature = "irq"))] +pub struct BlockIrqHandler; impl Block { pub fn name(&self) -> &str { @@ -95,105 +89,157 @@ impl Block { self.irq_num } - fn use_irq_completion(&self) -> bool { - #[cfg(feature = "irq")] - { - self.irq_state.is_some() - } - - #[cfg(not(feature = "irq"))] - { - false - } + pub fn enable_irq(&mut self) { + self.interface.enable_irq(); + self.irq_enabled = self.interface.is_irq_enabled(); } - fn read_blocks_wait( - queue: &mut rd_block::CmdQueue, - block_id: usize, - block_count: usize, - use_irq: bool, - ) -> Vec> { - #[cfg(feature = "irq")] - { - if use_irq { - return wait_on_block_irq(queue.read_blocks(block_id, block_count)); - } - } - #[cfg(not(feature = "irq"))] - let _ = use_irq; + pub fn disable_irq(&mut self) { + self.interface.disable_irq(); + self.irq_enabled = self.interface.is_irq_enabled(); + } - queue.read_blocks_blocking(block_id, block_count) + pub const fn is_irq_enabled(&self) -> bool { + self.irq_enabled } - fn write_blocks_wait( - queue: &mut rd_block::CmdQueue, - block_id: usize, - data: &[u8], - use_irq: bool, - ) -> Vec> { - #[cfg(feature = "irq")] - { - if use_irq { - return wait_on_block_irq(queue.write_blocks(block_id, data)); - } - } - #[cfg(not(feature = "irq"))] - let _ = use_irq; + #[cfg(feature = "irq")] + pub fn take_irq_handler(&mut self) -> Option<(usize, BlockIrqHandler)> { + let irq_num = self.irq_num.take()?; + let handler = self.irq_handler.take()?; + Some((irq_num, handler)) + } - queue.write_blocks_blocking(block_id, data) + #[cfg(not(feature = "irq"))] + pub fn take_irq_handler(&mut self) -> Option<(usize, BlockIrqHandler)> { + let _ = self; + None } pub fn num_blocks(&self) -> u64 { - self.queue.lock().num_blocks() as _ + self.queues.lock().queue.info().device.num_blocks } pub fn block_size(&self) -> usize { - self.queue.lock().block_size() + self.queues.lock().queue.info().device.logical_block_size } pub fn flush(&mut self) -> AxResult { - Ok(()) + let mut queues = self.queues.lock(); + let mut segments = []; + let request = Request { + op: RequestOp::Flush, + lba: 0, + block_count: 0, + segments: &mut segments, + flags: RequestFlags::NONE, + }; + let request_id = match queues.queue.submit_request(request) { + Ok(request_id) => request_id, + Err(BlkError::NotSupported) => return Ok(()), + Err(err) => return Err(map_blk_err_to_ax_err(err)), + }; + queues.poll_until_complete(request_id) } pub fn read_block(&mut self, block_id: u64, buf: &mut [u8]) -> AxResult { - let block_size = self.block_size(); - if block_size == 0 || !buf.len().is_multiple_of(block_size) { - return Err(AxError::InvalidInput); - } - - let use_irq = self.use_irq_completion(); - let mut queue = self.queue.lock(); - let block_count = buf.len() / block_size; - let blocks = Self::read_blocks_wait(&mut queue, block_id as usize, block_count, use_irq); - let mut copied = 0; - for block in blocks { - let block = block.map_err(map_blk_err_to_ax_err)?; - let end = copied + block.len(); - if end > buf.len() { - return Err(AxError::Io); - } - buf[copied..end].copy_from_slice(&block); - copied = end; - } - if copied != buf.len() { - return Err(AxError::Io); + let mut queues = self.queues.lock(); + let info = queues.queue.info(); + validate_io(info, block_id, buf.len())?; + + let plan = transfer_plan(queues.pool.planner, block_id, buf.len())?; + for chunk in plan { + let block_buf = + &mut buf[chunk.byte_offset..chunk.byte_offset.saturating_add(chunk.byte_len)]; + let mut dma_buffer = queues.pool.alloc(DmaDirection::FromDevice)?; + dma_buffer.sync_for_device(0, block_buf.len()); + let mut segments = segments_from_dma(&mut dma_buffer, chunk)?; + let request_id = queues + .queue + .submit_request(Request { + op: RequestOp::Read, + lba: chunk.lba, + block_count: chunk.block_count, + segments: &mut segments, + flags: RequestFlags::NONE, + }) + .map_err(map_blk_err_to_ax_err)?; + queues.poll_until_complete(request_id)?; + dma_buffer.sync_for_cpu(0, block_buf.len()); + dma_buffer.read_with(block_buf.len(), |data| block_buf.copy_from_slice(data)); } Ok(()) } pub fn write_block(&mut self, block_id: u64, buf: &[u8]) -> AxResult { - let block_size = self.block_size(); - if block_size == 0 || !buf.len().is_multiple_of(block_size) { - return Err(AxError::InvalidInput); + let mut queues = self.queues.lock(); + let info = queues.queue.info(); + validate_io(info, block_id, buf.len())?; + + let plan = transfer_plan(queues.pool.planner, block_id, buf.len())?; + for chunk in plan { + let block_buf = + &buf[chunk.byte_offset..chunk.byte_offset.saturating_add(chunk.byte_len)]; + let mut dma_buffer = queues.pool.alloc(DmaDirection::ToDevice)?; + dma_buffer.write_with(block_buf.len(), |data| data.copy_from_slice(block_buf)); + dma_buffer.sync_for_device(0, block_buf.len()); + let mut segments = segments_from_dma(&mut dma_buffer, chunk)?; + let request_id = queues + .queue + .submit_request(Request { + op: RequestOp::Write, + lba: chunk.lba, + block_count: chunk.block_count, + segments: &mut segments, + flags: RequestFlags::NONE, + }) + .map_err(map_blk_err_to_ax_err)?; + queues.poll_until_complete(request_id)?; } + Ok(()) + } +} - let use_irq = self.use_irq_completion(); - let mut queue = self.queue.lock(); - let blocks = Self::write_blocks_wait(&mut queue, block_id as usize, buf, use_irq); - for block in blocks { - block.map_err(map_blk_err_to_ax_err)?; +impl BlockQueues { + fn new(queue: Box) -> AxResult { + let info = queue.info(); + let block_size = info.device.logical_block_size; + if block_size == 0 { + return Err(AxError::BadState); + } + let planner = block_transfer_planner(info)?; + let layout = block_buffer_layout(info, planner.chunk_size())?; + Ok(Self { + queue, + pool: BlockBufferPool { + dma: DeviceDma::new(info.limits.dma_mask, axklib::dma::op()), + planner, + size: layout.size(), + align: layout.align(), + }, + }) + } + + fn poll_until_complete(&mut self, request: RequestId) -> AxResult { + loop { + match self + .queue + .poll_request(request) + .map_err(map_blk_err_to_ax_err)? + { + RequestStatus::Complete => return Ok(()), + RequestStatus::Pending => core::hint::spin_loop(), + } } - Ok(()) + } +} + +impl BlockBufferPool { + fn alloc(&self, direction: DmaDirection) -> AxResult> { + self.dma + .contiguous_array_zero_with_align(self.size, self.align, direction) + .map_err(BlkError::from) + .map_err(map_blk_err_to_ax_err) } } @@ -204,32 +250,20 @@ impl TryFrom> for Block { let mut dev = base.lock().map_err(|_| AxError::BadState)?; let name = dev.name.clone(); let irq_num = dev.irq_num; - let mut block = dev.block.take().ok_or(AxError::BadState)?; + let mut interface = dev.interface.take().ok_or(AxError::BadState)?; + let queue = interface.create_queue().ok_or(AxError::BadState)?; + let queues = BlockQueues::new(queue)?; + #[cfg(feature = "irq")] - let irq_handler = irq_num.map(|_| block.irq_handler()); - let queue = block.create_queue().ok_or(AxError::BadState)?; + let irq_handler = irq_num + .and_then(|_| take_legacy_irq_handler(interface.as_mut())) + .map(BlockIrqHandler::new); drop(dev); #[cfg(feature = "irq")] - let irq_state = if let (Some(irq_num), Some(handler)) = (irq_num, irq_handler) { - let state = Arc::new(BlockIrqState { handler }); - if let Some(slot) = reserve_block_irq_slot(&state) { - if axklib::irq::register(irq_num, BLOCK_IRQ_HANDLERS[slot]) { - Some(state) - } else { - release_block_irq_slot(slot, &state); - warn!("failed to register block irq handler for irq {irq_num}"); - None - } - } else { - warn!("no free block irq source slot for irq {irq_num}"); - None - } - } else { - None - }; + let irq_num = if irq_handler.is_some() { irq_num } else { None }; #[cfg(feature = "irq")] - let irq_num = irq_state.as_ref().and(irq_num); + let irq_handler = irq_handler; #[cfg(not(feature = "irq"))] let irq_num = { let _ = irq_num; @@ -239,42 +273,28 @@ impl TryFrom> for Block { Ok(Self { name, irq_num, + irq_enabled: interface.is_irq_enabled(), #[cfg(feature = "irq")] - irq_state, - queue: SpinNoIrq::new(queue), + irq_handler, + interface, + queues: SpinNoIrq::new(queues), }) } } pub trait PlatformDeviceBlock { - fn register_block(self, dev: T); - fn register_block_with_irq(self, dev: T, irq_num: Option); + fn register_block(self, dev: T); + fn register_block_with_irq(self, dev: T, irq_num: Option); } impl PlatformDeviceBlock for rdrive::PlatformDevice { - fn register_block(self, dev: T) { + fn register_block(self, dev: T) { self.register_block_with_irq(dev, None); } - fn register_block_with_irq(self, dev: T, irq_num: Option) { - #[cfg(feature = "irq")] - let mut dev = dev; - #[cfg(feature = "irq")] - if irq_num.is_some() { - dev.enable_irq(); - } - #[cfg(not(feature = "irq"))] - let dev = { - let _ = irq_num; - dev - }; + fn register_block_with_irq(self, dev: T, irq_num: Option) { let name = dev.name().to_string(); - let block = rd_block::Block::new(dev, axklib::dma::op()); - #[cfg(feature = "irq")] - let registered_irq_num = irq_num; - #[cfg(not(feature = "irq"))] - let registered_irq_num = None; - self.register(PlatformBlockDevice::new(name, block, registered_irq_num)); + self.register(PlatformBlockDevice::new(name, Box::new(dev), irq_num)); } } @@ -309,87 +329,70 @@ pub fn take_block_devices() -> Vec { } #[cfg(feature = "irq")] -fn handle_block_irq(slot: usize) { - let Some(state) = IRQ_SOURCES[slot].lock().as_ref().and_then(Weak::upgrade) else { - return; - }; - state.handle_irq(); +fn take_legacy_irq_handler( + interface: &mut dyn Interface, +) -> Option> { + let has_legacy_source = interface.irq_sources().iter().any(|source| source.id == 0); + has_legacy_source + .then(|| interface.take_irq_handler(0)) + .flatten() } -#[cfg(feature = "irq")] -fn handle_block_irq_slot(_: usize) { - handle_block_irq(SLOT); +fn validate_io(info: QueueInfo, block_id: u64, len: usize) -> AxResult { + let block_size = info.device.logical_block_size; + if block_size == 0 || !len.is_multiple_of(block_size) { + return Err(AxError::InvalidInput); + } + let block_count = len / block_size; + let end = block_id + .checked_add(block_count as u64) + .ok_or(AxError::InvalidInput)?; + if end > info.device.num_blocks { + return Err(AxError::InvalidInput); + } + Ok(()) } -#[cfg(feature = "irq")] -const BLOCK_IRQ_HANDLERS: [fn(usize); BLOCK_IRQ_SLOTS] = [ - handle_block_irq_slot::<0>, - handle_block_irq_slot::<1>, - handle_block_irq_slot::<2>, - handle_block_irq_slot::<3>, - handle_block_irq_slot::<4>, - handle_block_irq_slot::<5>, - handle_block_irq_slot::<6>, - handle_block_irq_slot::<7>, - handle_block_irq_slot::<8>, - handle_block_irq_slot::<9>, - handle_block_irq_slot::<10>, - handle_block_irq_slot::<11>, - handle_block_irq_slot::<12>, - handle_block_irq_slot::<13>, - handle_block_irq_slot::<14>, - handle_block_irq_slot::<15>, -]; - -#[cfg(feature = "irq")] -fn reserve_block_irq_slot(state: &Arc) -> Option { - for (slot, source) in IRQ_SOURCES.iter().enumerate() { - let mut source = source.lock(); - if source.as_ref().and_then(Weak::upgrade).is_none() { - *source = Some(Arc::downgrade(state)); - return Some(slot); - } +fn block_buffer_layout(info: QueueInfo, size: usize) -> AxResult { + let block_size = info.device.logical_block_size; + if size < block_size { + return Err(AxError::BadState); } - None + Layout::from_size_align(size, info.limits.dma_alignment.max(1)).map_err(|_| AxError::BadState) } -#[cfg(feature = "irq")] -fn release_block_irq_slot(slot: usize, state: &Arc) { - let mut source = IRQ_SOURCES[slot].lock(); - if source - .as_ref() - .and_then(Weak::upgrade) - .is_some_and(|registered| Arc::ptr_eq(®istered, state)) - { - *source = None; - } +fn block_transfer_planner(info: QueueInfo) -> AxResult { + TransferPlanner::new( + info.device, + info.limits, + TransferRuntimeCaps { + max_transfer_bytes: MAX_BLOCK_BUFFER_SIZE, + max_segments: usize::MAX, + }, + ) + .map_err(map_blk_err_to_ax_err) } -#[cfg(feature = "irq")] -fn wait_on_block_irq(future: F) -> F::Output { - let mut future = pin!(future); - let waiter = Arc::new(BlockIrqWaiter { - woken: AtomicBool::new(false), - }); - let waker = Waker::from(waiter.clone()); - let mut cx = Context::from_waker(&waker); - - loop { - waiter.woken.store(false, Ordering::Release); - match future.as_mut().poll(&mut cx) { - Poll::Ready(output) => return output, - Poll::Pending => { - // rd-block registers the waker after a Retry result, so keep a - // bounded repoll fallback for completions racing that window. - for _ in 0..BLOCK_IRQ_REPOLL_SPINS { - if waiter.woken.swap(false, Ordering::AcqRel) { - break; - } - core::hint::spin_loop(); - } - } - } +fn transfer_plan(planner: TransferPlanner, block_id: u64, len: usize) -> AxResult { + planner.plan(block_id, len).map_err(map_blk_err_to_ax_err) +} + +fn segments_from_dma( + buffer: &mut ContiguousArray, + chunk: TransferChunk, +) -> AxResult>> { + let base_virt = buffer.as_ptr().as_ptr(); + let base_bus = buffer.dma_addr().as_u64(); + let planned_segments = chunk.segments(); + let mut segments = Vec::with_capacity(planned_segments.len()); + for segment in planned_segments { + let virt = unsafe { base_virt.add(segment.byte_offset) }; + let bus = base_bus + .checked_add(segment.byte_offset as u64) + .ok_or(AxError::InvalidInput)?; + segments.push(unsafe { Buffer::from_raw_parts(virt, bus, segment.byte_len) }); } + Ok(segments) } fn map_blk_err_to_ax_err(err: BlkError) -> AxError { @@ -397,10 +400,517 @@ fn map_blk_err_to_ax_err(err: BlkError) -> AxError { BlkError::NotSupported => AxError::Unsupported, BlkError::Retry => AxError::WouldBlock, BlkError::NoMemory => AxError::NoMemory, - BlkError::InvalidBlockIndex(_) => AxError::InvalidInput, + BlkError::InvalidBlockIndex(_) | BlkError::InvalidRequest => AxError::InvalidInput, + BlkError::Io => AxError::Io, BlkError::Other(error) => { error!("Block device error: {error}"); AxError::Io } } } + +#[cfg(test)] +mod tests { + extern crate std; + + use alloc::{boxed::Box, string::String, vec::Vec}; + use core::{alloc::Layout, num::NonZeroUsize, ptr::NonNull}; + use std::{ + alloc::{alloc_zeroed, dealloc}, + sync::Mutex, + }; + + use dma_api::{ + DeviceDma, DmaAllocHandle, DmaConstraints, DmaDirection, DmaError, DmaMapHandle, DmaOp, + }; + use rdif_block::{DeviceInfo, DriverGeneric, QueueLimits, validate_request}; + + use super::*; + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum SyncOp { + ForDevice { + size: usize, + direction: DmaDirection, + }, + ForCpu { + size: usize, + direction: DmaDirection, + }, + } + + #[derive(Default)] + struct TrackingDma { + ops: Mutex>, + } + + impl TrackingDma { + fn has_read_device_sync(&self) -> bool { + self.ops.lock().unwrap().iter().any(|op| { + matches!( + op, + SyncOp::ForDevice { + size: 512, + direction: DmaDirection::FromDevice + } + ) + }) + } + + fn sync_count(&self, expected: SyncOp) -> usize { + self.ops + .lock() + .unwrap() + .iter() + .filter(|op| **op == expected) + .count() + } + } + + impl DmaOp for TrackingDma { + fn page_size(&self) -> usize { + 4096 + } + + unsafe fn alloc_contiguous( + &self, + _constraints: DmaConstraints, + layout: Layout, + ) -> Option { + let ptr = unsafe { alloc_zeroed(layout) }; + let ptr = NonNull::new(ptr)?; + Some(unsafe { DmaAllocHandle::new(ptr, (ptr.as_ptr() as u64).into(), layout) }) + } + + unsafe fn dealloc_contiguous(&self, handle: DmaAllocHandle) { + unsafe { dealloc(handle.as_ptr().as_ptr(), handle.layout()) }; + } + + unsafe fn alloc_coherent( + &self, + constraints: DmaConstraints, + layout: Layout, + ) -> Option { + unsafe { self.alloc_contiguous(constraints, layout) } + } + + unsafe fn dealloc_coherent(&self, handle: DmaAllocHandle) { + unsafe { self.dealloc_contiguous(handle) }; + } + + unsafe fn map_streaming( + &self, + _constraints: DmaConstraints, + addr: NonNull, + size: NonZeroUsize, + _direction: DmaDirection, + ) -> Result { + let layout = Layout::from_size_align(size.get(), 1)?; + Ok(unsafe { DmaMapHandle::new(addr, (addr.as_ptr() as u64).into(), layout, None) }) + } + + unsafe fn unmap_streaming(&self, _handle: DmaMapHandle) {} + + fn sync_alloc_for_device( + &self, + _handle: &DmaAllocHandle, + _offset: usize, + size: usize, + direction: DmaDirection, + ) { + self.ops + .lock() + .unwrap() + .push(SyncOp::ForDevice { size, direction }); + } + + fn sync_alloc_for_cpu( + &self, + _handle: &DmaAllocHandle, + _offset: usize, + size: usize, + direction: DmaDirection, + ) { + self.ops + .lock() + .unwrap() + .push(SyncOp::ForCpu { size, direction }); + } + } + + #[derive(Clone, Debug, PartialEq, Eq)] + struct SubmittedRequest { + op: RequestOp, + lba: u64, + block_count: u32, + data_len: usize, + segment_lengths: Vec, + } + + #[derive(Default)] + struct RequestLog { + requests: Mutex>, + } + + impl RequestLog { + fn push(&self, request: SubmittedRequest) { + self.requests.lock().unwrap().push(request); + } + + fn snapshot(&self) -> Vec { + self.requests.lock().unwrap().clone() + } + } + + struct TestInterface; + + impl DriverGeneric for TestInterface { + fn name(&self) -> &str { + "test-block" + } + } + + impl Interface for TestInterface { + fn device_info(&self) -> DeviceInfo { + DeviceInfo { + name: Some("test-block"), + ..DeviceInfo::new(8, 512) + } + } + + fn queue_limits(&self) -> QueueLimits { + QueueLimits::simple(512, u64::MAX) + } + + fn create_queue(&mut self) -> Option> { + None + } + } + + struct TestQueue { + dma: &'static TrackingDma, + } + + // SAFETY: The queue copies data synchronously during `submit_request` and + // never stores segment pointers after the call returns. + unsafe impl IQueue for TestQueue { + fn id(&self) -> usize { + 0 + } + + fn info(&self) -> QueueInfo { + QueueInfo { + id: 0, + device: DeviceInfo { + name: Some("test-block"), + ..DeviceInfo::new(8, 512) + }, + limits: QueueLimits::simple(512, u64::MAX), + } + } + + fn submit_request(&mut self, request: Request<'_>) -> Result { + assert!( + self.dma.has_read_device_sync(), + "read DMA buffers must be synchronized for device ownership before submit" + ); + validate_request(self.info(), &request)?; + request.segments[0].fill(0x5a); + Ok(RequestId::new(0)) + } + + fn poll_request(&mut self, _request: RequestId) -> Result { + Ok(RequestStatus::Complete) + } + } + + struct RecordingQueue { + info: QueueInfo, + log: &'static RequestLog, + } + + impl RecordingQueue { + fn new(log: &'static RequestLog, limits: QueueLimits) -> Self { + Self { + info: QueueInfo { + id: 0, + device: DeviceInfo { + name: Some("recording-block"), + ..DeviceInfo::new(64, 512) + }, + limits, + }, + log, + } + } + } + + // SAFETY: The queue records request metadata synchronously and never + // accesses request segments after `submit_request` returns. + unsafe impl IQueue for RecordingQueue { + fn id(&self) -> usize { + self.info.id + } + + fn info(&self) -> QueueInfo { + self.info + } + + fn submit_request(&mut self, request: Request<'_>) -> Result { + validate_request(self.info, &request)?; + if request.op == RequestOp::Read { + request.segments[0].fill(request.block_count as u8); + } + self.log.push(SubmittedRequest { + op: request.op, + lba: request.lba, + block_count: request.block_count, + data_len: request.data_len(), + segment_lengths: request.segments.iter().map(|segment| segment.len).collect(), + }); + Ok(RequestId::new(self.log.snapshot().len())) + } + + fn poll_request(&mut self, _request: RequestId) -> Result { + Ok(RequestStatus::Complete) + } + } + + fn block_with_queue(queue: Box, dma: &'static TrackingDma) -> Block { + let info = queue.info(); + let planner = block_transfer_planner(info).unwrap(); + let layout = block_buffer_layout(info, planner.chunk_size()).unwrap(); + Block { + name: String::from("test-block"), + irq_num: None, + irq_enabled: false, + #[cfg(feature = "irq")] + irq_handler: None, + interface: Box::new(TestInterface), + queues: SpinNoIrq::new(BlockQueues { + queue, + pool: BlockBufferPool { + dma: DeviceDma::new(u64::MAX, dma), + planner, + size: layout.size(), + align: layout.align(), + }, + }), + } + } + + #[test] + fn read_block_syncs_dma_buffer_for_device_before_submit() { + let dma = Box::leak(Box::::default()); + let mut block = block_with_queue(Box::new(TestQueue { dma }), dma); + let mut buf = [0_u8; 512]; + + block.read_block(0, &mut buf).unwrap(); + + assert_eq!(buf, [0x5a; 512]); + } + + #[test] + fn write_block_batches_requests_to_queue_limits() { + let dma = Box::leak(Box::::default()); + let log = Box::leak(Box::::default()); + let limits = QueueLimits { + dma_mask: u64::MAX, + dma_alignment: 4096, + max_blocks_per_request: 8, + max_segments: 1, + max_segment_size: 4096, + supported_flags: RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + }; + let mut block = block_with_queue(Box::new(RecordingQueue::new(log, limits)), dma); + let buf = [0x42_u8; 8192]; + + block.write_block(4, &buf).unwrap(); + + assert_eq!( + log.snapshot(), + [ + SubmittedRequest { + op: RequestOp::Write, + lba: 4, + block_count: 8, + data_len: 4096, + segment_lengths: alloc::vec![4096], + }, + SubmittedRequest { + op: RequestOp::Write, + lba: 12, + block_count: 8, + data_len: 4096, + segment_lengths: alloc::vec![4096], + }, + ] + ); + assert_eq!( + dma.sync_count(SyncOp::ForDevice { + size: 4096, + direction: DmaDirection::ToDevice, + }), + 2 + ); + } + + #[test] + fn read_block_batches_requests_to_queue_limits() { + let dma = Box::leak(Box::::default()); + let log = Box::leak(Box::::default()); + let limits = QueueLimits { + dma_mask: u64::MAX, + dma_alignment: 4096, + max_blocks_per_request: 8, + max_segments: 1, + max_segment_size: 4096, + supported_flags: RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + }; + let mut block = block_with_queue(Box::new(RecordingQueue::new(log, limits)), dma); + let mut buf = [0_u8; 8192]; + + block.read_block(4, &mut buf).unwrap(); + + assert_eq!( + log.snapshot(), + [ + SubmittedRequest { + op: RequestOp::Read, + lba: 4, + block_count: 8, + data_len: 4096, + segment_lengths: alloc::vec![4096], + }, + SubmittedRequest { + op: RequestOp::Read, + lba: 12, + block_count: 8, + data_len: 4096, + segment_lengths: alloc::vec![4096], + }, + ] + ); + assert_eq!(buf, [8; 8192]); + assert_eq!( + dma.sync_count(SyncOp::ForDevice { + size: 4096, + direction: DmaDirection::FromDevice, + }), + 2 + ); + assert_eq!( + dma.sync_count(SyncOp::ForCpu { + size: 4096, + direction: DmaDirection::FromDevice, + }), + 2 + ); + } + + #[test] + fn block_transfer_planner_caps_large_finite_segments() { + let info = QueueInfo { + id: 0, + device: DeviceInfo { + name: Some("large-segment-block"), + ..DeviceInfo::new(64, 512) + }, + limits: QueueLimits { + dma_mask: u64::MAX, + dma_alignment: 4096, + max_blocks_per_request: 4096, + max_segments: 1, + max_segment_size: 2 * 1024 * 1024, + supported_flags: RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + }, + }; + + assert_eq!( + block_transfer_planner(info).unwrap().chunk_size(), + 16 * 1024 + ); + } + + #[test] + fn block_transfer_planner_uses_simple_limit_preference_for_unbounded_segments() { + let info = QueueInfo { + id: 0, + device: DeviceInfo { + name: Some("simple-block"), + ..DeviceInfo::new(64, 512) + }, + limits: QueueLimits::simple(512, u64::MAX), + }; + + assert_eq!(block_transfer_planner(info).unwrap().chunk_size(), 512); + } + + #[test] + fn block_transfer_planner_applies_runtime_cap_without_unbounded_segment_special_case() { + let info = QueueInfo { + id: 0, + device: DeviceInfo { + name: Some("unbounded-large-preference-block"), + ..DeviceInfo::new(128, 512) + }, + limits: QueueLimits { + dma_mask: u64::MAX, + dma_alignment: 4096, + max_blocks_per_request: u32::MAX, + max_segments: 1, + max_segment_size: usize::MAX, + supported_flags: RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + }, + }; + + assert_eq!( + block_transfer_planner(info).unwrap().chunk_size(), + MAX_BLOCK_BUFFER_SIZE + ); + } + + #[test] + fn write_block_uses_planned_multi_segment_chunks() { + let dma = Box::leak(Box::::default()); + let log = Box::leak(Box::::default()); + let limits = QueueLimits { + dma_mask: u64::MAX, + dma_alignment: 4096, + max_blocks_per_request: 8, + max_segments: 4, + max_segment_size: 1024, + supported_flags: RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + }; + let mut block = block_with_queue(Box::new(RecordingQueue::new(log, limits)), dma); + let buf = [0x42_u8; 4096]; + + block.write_block(4, &buf).unwrap(); + + assert_eq!( + log.snapshot(), + [SubmittedRequest { + op: RequestOp::Write, + lba: 4, + block_count: 8, + data_len: 4096, + segment_lengths: alloc::vec![1024, 1024, 1024, 1024], + }] + ); + } +} diff --git a/drivers/ax-driver/src/block/cvsd.rs b/drivers/ax-driver/src/block/cvsd.rs index a9c87f1149..082821bb29 100644 --- a/drivers/ax-driver/src/block/cvsd.rs +++ b/drivers/ax-driver/src/block/cvsd.rs @@ -110,11 +110,11 @@ impl CvsdDriver { Ok(Self(sdmmc)) } - fn checked_lba(block_id: u64, offset: usize) -> Result { + fn checked_lba(block_id: u64, offset: usize) -> Result { let lba = block_id .checked_add(offset as u64) - .ok_or(rd_block::BlkError::InvalidBlockIndex(block_id as usize))?; - u32::try_from(lba).map_err(|_| rd_block::BlkError::InvalidBlockIndex(block_id as usize)) + .ok_or(rdif_block::BlkError::InvalidBlockIndex(block_id))?; + u32::try_from(lba).map_err(|_| rdif_block::BlkError::InvalidBlockIndex(block_id)) } } @@ -132,28 +132,28 @@ impl SyncBlockOps for CvsdDriver { BLOCK_SIZE } - fn read_blocks(&mut self, block_id: u64, buf: &mut [u8]) -> Result<(), rd_block::BlkError> { + fn read_blocks(&mut self, block_id: u64, buf: &mut [u8]) -> Result<(), rdif_block::BlkError> { if !buf.len().is_multiple_of(BLOCK_SIZE) { - return Err(rd_block::BlkError::NotSupported); + return Err(rdif_block::BlkError::NotSupported); } for (i, block) in buf.chunks_exact_mut(BLOCK_SIZE).enumerate() { self.0 .read_block(Self::checked_lba(block_id, i)?, block) - .map_err(|_| rd_block::BlkError::Other("CVSD read failed".into()))?; + .map_err(|_| rdif_block::BlkError::Other("CVSD read failed"))?; } Ok(()) } - fn write_blocks(&mut self, block_id: u64, buf: &[u8]) -> Result<(), rd_block::BlkError> { + fn write_blocks(&mut self, block_id: u64, buf: &[u8]) -> Result<(), rdif_block::BlkError> { if !buf.len().is_multiple_of(BLOCK_SIZE) { - return Err(rd_block::BlkError::NotSupported); + return Err(rdif_block::BlkError::NotSupported); } for (i, block) in buf.chunks_exact(BLOCK_SIZE).enumerate() { self.0 .write_block(Self::checked_lba(block_id, i)?, block) - .map_err(|_| rd_block::BlkError::Other("CVSD write failed".into()))?; + .map_err(|_| rdif_block::BlkError::Other("CVSD write failed"))?; } Ok(()) } diff --git a/drivers/ax-driver/src/block/mod.rs b/drivers/ax-driver/src/block/mod.rs index 15a6ef7685..630b1efdc2 100644 --- a/drivers/ax-driver/src/block/mod.rs +++ b/drivers/ax-driver/src/block/mod.rs @@ -1,4 +1,11 @@ mod binding; +#[cfg(any( + feature = "virtio-blk", + feature = "phytium-mci", + feature = "rockchip-dwmmc", + feature = "rockchip-sdhci" +))] +mod shared; #[cfg(feature = "ahci")] pub mod ahci; @@ -6,6 +13,8 @@ pub mod ahci; pub mod bcm2835; #[cfg(feature = "cvsd")] pub mod cvsd; +#[cfg(feature = "nvme")] +pub mod nvme; #[cfg(feature = "phytium-mci")] pub mod phytium_mci; #[cfg(feature = "ramdisk")] @@ -22,7 +31,17 @@ use alloc::{boxed::Box, sync::Arc}; pub use binding::*; #[cfg(sync_block_dev)] -use rd_block::{BlkError, BuffConfig, DriverGeneric, Event, IQueue, Interface, Request, RequestId}; +use rdif_block::{ + BlkError, DeviceInfo, DriverGeneric, IQueue, Interface, QueueInfo, QueueLimits, Request, + RequestId, RequestOp, RequestStatus, validate_request, +}; +#[cfg(any( + feature = "virtio-blk", + feature = "phytium-mci", + feature = "rockchip-dwmmc", + feature = "rockchip-sdhci" +))] +pub(crate) use shared::SharedDriver; #[cfg(sync_block_dev)] use spin::Mutex; @@ -44,7 +63,6 @@ pub(crate) fn register_sync_block(plat_dev: rdrive::PlatformDev struct SyncBlockDevice { inner: Arc>, queue_created: bool, - irq_enabled: bool, } #[cfg(sync_block_dev)] @@ -53,7 +71,6 @@ impl SyncBlockDevice { Self { inner: Arc::new(Mutex::new(driver)), queue_created: false, - irq_enabled: false, } } } @@ -67,6 +84,18 @@ impl DriverGeneric for SyncBlockDevice { #[cfg(sync_block_dev)] impl Interface for SyncBlockDevice { + fn device_info(&self) -> DeviceInfo { + let guard = self.inner.lock(); + DeviceInfo { + name: Some(guard.name()), + ..DeviceInfo::new(guard.num_blocks(), guard.block_size()) + } + } + + fn queue_limits(&self) -> QueueLimits { + QueueLimits::simple(self.inner.lock().block_size(), u64::MAX) + } + fn create_queue(&mut self) -> Option> { if self.queue_created { return None; @@ -77,22 +106,6 @@ impl Interface for SyncBlockDevice { inner: Arc::clone(&self.inner), })) } - - fn enable_irq(&mut self) { - self.irq_enabled = true; - } - - fn disable_irq(&mut self) { - self.irq_enabled = false; - } - - fn is_irq_enabled(&self) -> bool { - self.irq_enabled - } - - fn handle_irq(&mut self) -> Event { - Event::none() - } } #[cfg(sync_block_dev)] @@ -102,42 +115,57 @@ struct SyncBlockQueue { } #[cfg(sync_block_dev)] -impl IQueue for SyncBlockQueue { - fn id(&self) -> usize { - self.id +impl SyncBlockQueue { + fn device_info(&self) -> DeviceInfo { + let guard = self.inner.lock(); + DeviceInfo { + name: Some(guard.name()), + ..DeviceInfo::new(guard.num_blocks(), guard.block_size()) + } } - fn num_blocks(&self) -> usize { - self.inner.lock().num_blocks() as usize + fn limits(&self) -> QueueLimits { + QueueLimits::simple(self.inner.lock().block_size(), u64::MAX) } +} - fn block_size(&self) -> usize { - self.inner.lock().block_size() +#[cfg(sync_block_dev)] +// SAFETY: SyncBlockQueue forwards data to a synchronous block driver and does +// not retain request segment pointers after `submit_request` returns. +unsafe impl IQueue for SyncBlockQueue { + fn id(&self) -> usize { + self.id } - fn buff_config(&self) -> BuffConfig { - BuffConfig { - dma_mask: u64::MAX, - align: 0x1000, - size: self.block_size(), + fn info(&self) -> QueueInfo { + QueueInfo { + id: self.id, + device: self.device_info(), + limits: self.limits(), } } fn submit_request(&mut self, request: Request<'_>) -> Result { - match request.kind { - rd_block::RequestKind::Read(mut buffer) => self - .inner - .lock() - .read_blocks(request.block_id as u64, &mut buffer)?, - rd_block::RequestKind::Write(items) => self - .inner - .lock() - .write_blocks(request.block_id as u64, items)?, + validate_request(self.info(), &request)?; + match request.op { + RequestOp::Read => { + let segment = request + .segments + .first_mut() + .ok_or(BlkError::InvalidRequest)?; + self.inner.lock().read_blocks(request.lba, segment)?; + } + RequestOp::Write => { + let segment = request.segments.first().ok_or(BlkError::InvalidRequest)?; + self.inner.lock().write_blocks(request.lba, segment)?; + } + RequestOp::Flush => {} + RequestOp::Discard | RequestOp::WriteZeroes => return Err(BlkError::NotSupported), } Ok(RequestId::new(0)) } - fn poll_request(&mut self, _request: RequestId) -> Result<(), BlkError> { - Ok(()) + fn poll_request(&mut self, _request: RequestId) -> Result { + Ok(RequestStatus::Complete) } } diff --git a/drivers/ax-driver/src/block/nvme.rs b/drivers/ax-driver/src/block/nvme.rs new file mode 100644 index 0000000000..a3961a7e9d --- /dev/null +++ b/drivers/ax-driver/src/block/nvme.rs @@ -0,0 +1,74 @@ +extern crate alloc; + +use alloc::format; + +use log::info; +use nvme_driver::{Config, Nvme, NvmeBlockDriver}; +use pcie::{CommandRegister, DeviceType}; +use rdrive::{ + PlatformDevice, + probe::{ + OnProbeError, + pci::{EndpointRc, FnOnProbe}, + }, +}; + +use super::PlatformDeviceBlock; + +pub const DEVICE_NAME: &str = "nvme"; +const DEFAULT_PAGE_SIZE: usize = 0x1000; +const DEFAULT_IO_QUEUE_PAIRS: usize = 1; + +crate::model_register!( + name: "NVMe", + level: ProbeLevel::PostKernel, + priority: ProbePriority::DEFAULT, + probe_kinds: &[ProbeKind::Pci { + on_probe: probe_pci as FnOnProbe, + }], +); + +fn probe_pci(endpoint: &mut EndpointRc, plat_dev: PlatformDevice) -> Result<(), OnProbeError> { + if endpoint.device_type() != DeviceType::NvmeController { + return Err(OnProbeError::NotMatch); + } + + let Some(bar) = endpoint.bar_mmio(0) else { + return Err(OnProbeError::other("NVMe BAR0 MMIO missing")); + }; + + endpoint.update_command(|mut cmd| { + cmd.insert(CommandRegister::MEMORY_ENABLE | CommandRegister::BUS_MASTER_ENABLE); + cmd.remove(CommandRegister::INTERRUPT_DISABLE); + cmd + }); + + let address = endpoint.address(); + let irq = crate::pci::endpoint_legacy_irq(endpoint); + info!( + "NVMe PCI endpoint {address}: BAR0={:#x}..{:#x}, irq={:?}, int_pin={}, int_line={}", + bar.start, + bar.end, + irq, + endpoint.interrupt_pin(), + endpoint.interrupt_line() + ); + + let nvme = Nvme::new( + bar.start, + bar.count().max(1), + u64::MAX, + axklib::dma::op(), + axklib::mmio::op(), + Config { + page_size: DEFAULT_PAGE_SIZE, + io_queue_pair_count: DEFAULT_IO_QUEUE_PAIRS, + }, + ) + .map_err(|err| OnProbeError::other(format!("failed to initialize NVMe: {err:?}")))?; + let driver = NvmeBlockDriver::from_nvme(nvme).map_err(|err| { + OnProbeError::other(format!("failed to create NVMe block driver: {err:?}")) + })?; + plat_dev.register_block_with_irq(driver, irq); + Ok(()) +} diff --git a/drivers/ax-driver/src/block/phytium_mci.rs b/drivers/ax-driver/src/block/phytium_mci.rs index 64d1c3cf51..921ec7d768 100644 --- a/drivers/ax-driver/src/block/phytium_mci.rs +++ b/drivers/ax-driver/src/block/phytium_mci.rs @@ -1,7 +1,11 @@ -use alloc::{format, sync::Arc, vec::Vec}; -use core::{num::NonZeroUsize, ptr::NonNull, time::Duration}; +use alloc::{format, vec::Vec}; +use core::{ + num::NonZeroUsize, + ptr::NonNull, + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; -use ax_kspin::SpinNoIrq; use dma_api::DeviceDma; use log::{info, warn}; use phytium_mci_host::{BlockRequest, BlockRequestSlot, PhytiumMci, RequestId}; @@ -13,7 +17,7 @@ use sdmmc_protocol::{ }; use crate::{ - block::{PlatformDeviceBlock, decode_fdt_irq}, + block::{PlatformDeviceBlock, SharedDriver, decode_fdt_irq}, mmio::iomap, }; @@ -79,12 +83,13 @@ fn probe(info: FdtInfo<'_>, plat_dev: PlatformDevice) -> Result<(), OnProbeError ); let irq_num = decode_fdt_irq(&info.interrupts()); - let raw = Arc::new(SpinNoIrq::new(card)); + let raw = SharedDriver::new(card); let dev = MciBlockDevice { raw: Some(raw), capacity_blocks: card_info.capacity_blocks.unwrap_or(0), - irq_enabled: false, + irq_enabled: AtomicBool::new(false), queue_created: false, + irq_handler_taken: false, }; plat_dev.register_block_with_irq(dev, irq_num); info!("phytium-mci block device registered irq={:?}", irq_num); @@ -155,20 +160,21 @@ fn is_absent_card_init_error(err: Error) -> bool { } struct MciBlockDevice { - raw: Option>>, + raw: Option>, capacity_blocks: u64, - irq_enabled: bool, + irq_enabled: AtomicBool, queue_created: bool, + irq_handler_taken: bool, } struct MciBlockQueue { - raw: Arc>, + raw: SharedDriver, capacity_blocks: u64, id: usize, dma: DeviceDma, slot: BlockRequestSlot, pending: Option, - completed: Vec, + completed: Vec, } impl DriverGeneric for MciBlockDevice { @@ -177,116 +183,201 @@ impl DriverGeneric for MciBlockDevice { } } -impl rd_block::Interface for MciBlockDevice { - fn create_queue(&mut self) -> Option> { +impl rdif_block::Interface for MciBlockDevice { + fn device_info(&self) -> rdif_block::DeviceInfo { + rdif_block::DeviceInfo { + name: Some("phytium-mci"), + ..rdif_block::DeviceInfo::new(self.capacity_blocks, BLOCK_SIZE) + } + } + + fn queue_limits(&self) -> rdif_block::QueueLimits { + rdif_block::QueueLimits { + dma_mask: u32::MAX as u64, + dma_alignment: BLOCK_SIZE, + max_blocks_per_request: u16::MAX as u32 + 1, + max_segments: 1, + max_segment_size: usize::MAX, + supported_flags: rdif_block::RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + } + } + + fn create_queue(&mut self) -> Option> { if self.queue_created { return None; } self.raw.as_ref().map(|dev| { self.queue_created = true; - alloc::boxed::Box::new(MciBlockQueue { - raw: dev.clone(), - capacity_blocks: self.capacity_blocks, - id: 0, - dma: axklib::dma::device_with_mask(u32::MAX as u64), - slot: BlockRequestSlot::default(), - pending: None, - completed: Vec::new(), - }) as _ + alloc::boxed::Box::new(MciBlockQueue::new(dev.clone(), self.capacity_blocks, 0)) as _ }) } - fn enable_irq(&mut self) { + fn enable_irq(&self) { if let Some(raw) = &self.raw { - let mut raw = raw.lock(); - if let Err(err) = SdioHost::enable_completion_irq(raw.host_mut()) { - warn!("phytium-mci: enable completion IRQ failed: {:?}", err); - return; - } - self.irq_enabled = true; + let mut enabled = false; + raw.with_mut(|raw| { + if let Err(err) = SdioHost::enable_completion_irq(raw.host_mut()) { + warn!("phytium-mci: enable completion IRQ failed: {:?}", err); + return; + } + enabled = true; + }); + self.irq_enabled.store(enabled, Ordering::Release); } } - fn disable_irq(&mut self) { + fn disable_irq(&self) { if let Some(raw) = &self.raw { - let mut raw = raw.lock(); - if let Err(err) = SdioHost::disable_completion_irq(raw.host_mut()) { - warn!("phytium-mci: disable completion IRQ failed: {:?}", err); - } + raw.with_mut(|raw| { + if let Err(err) = SdioHost::disable_completion_irq(raw.host_mut()) { + warn!("phytium-mci: disable completion IRQ failed: {:?}", err); + } + }); } - self.irq_enabled = false; + self.irq_enabled.store(false, Ordering::Release); } fn is_irq_enabled(&self) -> bool { - self.irq_enabled + self.irq_enabled.load(Ordering::Acquire) } - fn handle_irq(&mut self) -> rd_block::Event { - let Some(raw) = &self.raw else { - return rd_block::Event::none(); - }; - let irq_event = raw.lock().host_mut().handle_irq(); - block_event_from_mci_irq(irq_event) + fn irq_sources(&self) -> rdif_block::IrqSourceList { + alloc::vec![rdif_block::IrqSourceInfo::legacy( + rdif_block::IdList::from_bits(1), + )] + } + + fn take_irq_handler( + &mut self, + source_id: usize, + ) -> Option> { + if source_id != 0 { + return None; + } + if self.irq_handler_taken { + return None; + } + let raw = self.raw.as_ref()?.clone(); + self.irq_handler_taken = true; + Some(alloc::boxed::Box::new(MciBlockIrqHandler { raw })) } } -fn block_event_from_mci_irq(irq_event: phytium_mci_host::Event) -> rd_block::Event { +struct MciBlockIrqHandler { + raw: SharedDriver, +} + +impl rdif_block::IrqHandler for MciBlockIrqHandler { + fn handle_irq(&self) -> rdif_block::Event { + self.raw + .try_with_mut(|raw| block_event_from_mci_irq(raw.host_mut().handle_irq())) + .unwrap_or_else(rdif_block::Event::none) + } +} + +fn block_event_from_mci_irq(irq_event: phytium_mci_host::Event) -> rdif_block::Event { match irq_event { - phytium_mci_host::Event::None => rd_block::Event::none(), + phytium_mci_host::Event::None => rdif_block::Event::none(), phytium_mci_host::Event::CommandComplete | phytium_mci_host::Event::TransferComplete | phytium_mci_host::Event::ReceiveReady | phytium_mci_host::Event::TransmitReady | phytium_mci_host::Event::Error { .. } | phytium_mci_host::Event::Other { .. } => { - let mut event = rd_block::Event::none(); - event.queue.insert(0); + let mut event = rdif_block::Event::none(); + event.queues.insert(0); event } } } -impl rd_block::IQueue for MciBlockQueue { - fn num_blocks(&self) -> usize { - self.capacity_blocks as usize +// SAFETY: MciBlockQueue owns a single pending request slot and does not access +// request segments after that request completes or fails. +unsafe impl rdif_block::IQueue for MciBlockQueue { + fn id(&self) -> usize { + self.id } - fn block_size(&self) -> usize { - BLOCK_SIZE + fn info(&self) -> rdif_block::QueueInfo { + rdif_block::QueueInfo { + id: self.id, + device: rdif_block::DeviceInfo { + name: Some("phytium-mci"), + ..rdif_block::DeviceInfo::new(self.capacity_blocks, BLOCK_SIZE) + }, + limits: rdif_block::QueueLimits { + dma_mask: self.dma.dma_mask(), + dma_alignment: BLOCK_SIZE, + max_blocks_per_request: u16::MAX as u32 + 1, + max_segments: 1, + max_segment_size: usize::MAX, + supported_flags: rdif_block::RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + }, + } } - fn id(&self) -> usize { - self.id + fn submit_request( + &mut self, + request: rdif_block::Request<'_>, + ) -> Result { + self.submit_request_inner(request) } - fn buff_config(&self) -> rd_block::BuffConfig { - rd_block::BuffConfig { - dma_mask: self.dma.dma_mask(), - align: BLOCK_SIZE, - size: BLOCK_SIZE, + fn poll_request( + &mut self, + request: rdif_block::RequestId, + ) -> Result { + self.poll_request_inner(request) + } +} + +impl MciBlockQueue { + fn new(raw: SharedDriver, capacity_blocks: u64, id: usize) -> Self { + Self { + raw, + capacity_blocks, + id, + dma: axklib::dma::device_with_mask(u32::MAX as u64), + slot: BlockRequestSlot::default(), + pending: None, + completed: Vec::new(), } } - fn submit_request( + fn queue_info(&self) -> rdif_block::QueueInfo { + rdif_block::IQueue::info(self) + } + + fn submit_request_inner( &mut self, - request: rd_block::Request<'_>, - ) -> Result { + request: rdif_block::Request<'_>, + ) -> Result { + let info = self.queue_info(); + rdif_block::validate_request(info, &request)?; self.reap_pending_request()?; - let mut raw = self.raw.lock(); - let start_block = block_addr_for_card(request.block_id, raw.is_high_capacity())?; - match request.kind { - rd_block::RequestKind::Read(buffer) => { - if !buffer.len().is_multiple_of(BLOCK_SIZE) { - return Err(rd_block::BlkError::Other( - "read buffer is not block aligned".into(), - )); - } - let ptr = NonNull::new(buffer.virt).ok_or_else(|| { - rd_block::BlkError::Other("read buffer pointer is null".into()) - })?; - let size = NonZeroUsize::new(buffer.len()) - .ok_or_else(|| rd_block::BlkError::Other("read buffer is empty".into()))?; - let id = submit_read_request( + let raw = self.raw.clone(); + raw.with_mut(|raw| { + let start_block = block_addr_for_card(request.lba, raw.is_high_capacity())?; + let buffer = request + .segments + .first() + .copied() + .ok_or(rdif_block::BlkError::InvalidRequest)?; + if !buffer.len().is_multiple_of(BLOCK_SIZE) { + return Err(rdif_block::BlkError::Other("buffer is not block aligned")); + } + let ptr = NonNull::new(buffer.virt) + .ok_or(rdif_block::BlkError::Other("buffer pointer is null"))?; + let size = NonZeroUsize::new(buffer.len()) + .ok_or(rdif_block::BlkError::Other("buffer is empty"))?; + let id = match request.op { + rdif_block::RequestOp::Read => submit_read_request( raw.host_mut(), start_block, ptr, @@ -294,21 +385,8 @@ impl rd_block::IQueue for MciBlockQueue { &self.dma, &mut self.slot, &mut self.pending, - )?; - Ok(rd_block::RequestId::new(usize::from(id))) - } - rd_block::RequestKind::Write(items) => { - if !items.len().is_multiple_of(BLOCK_SIZE) { - return Err(rd_block::BlkError::Other( - "write buffer is not block aligned".into(), - )); - } - let ptr = NonNull::new(items.as_ptr() as *mut u8).ok_or_else(|| { - rd_block::BlkError::Other("write buffer pointer is null".into()) - })?; - let size = NonZeroUsize::new(items.len()) - .ok_or_else(|| rd_block::BlkError::Other("write buffer is empty".into()))?; - let id = submit_write_request( + )?, + rdif_block::RequestOp::Write => submit_write_request( raw.host_mut(), start_block, ptr, @@ -316,35 +394,44 @@ impl rd_block::IQueue for MciBlockQueue { &self.dma, &mut self.slot, &mut self.pending, - )?; - Ok(rd_block::RequestId::new(usize::from(id))) - } - } + )?, + rdif_block::RequestOp::Flush + | rdif_block::RequestOp::Discard + | rdif_block::RequestOp::WriteZeroes => { + return Err(rdif_block::BlkError::NotSupported); + } + }; + Ok(rdif_block::RequestId::new(usize::from(id))) + }) } - fn poll_request(&mut self, request: rd_block::RequestId) -> Result<(), rd_block::BlkError> { + fn poll_request_inner( + &mut self, + request: rdif_block::RequestId, + ) -> Result { if let Some(index) = self.completed.iter().position(|id| *id == request) { self.completed.swap_remove(index); - return Ok(()); + return Ok(rdif_block::RequestStatus::Complete); } self.poll_active_request(request) } -} -impl MciBlockQueue { fn poll_active_request( &mut self, - request: rd_block::RequestId, - ) -> Result<(), rd_block::BlkError> { - match self.raw.lock().host_mut().poll_block_request( - &mut self.pending, - RequestId::new(usize::from(request)), - &mut self.slot, - ) { - Ok(BlockPoll::Complete) => Ok(()), - Ok(BlockPoll::Pending) => Err(rd_block::BlkError::Retry), - Ok(_) => Err(rd_block::BlkError::Other( - "Phytium MCI returned an unknown poll state".into(), + request: rdif_block::RequestId, + ) -> Result { + let raw = self.raw.clone(); + match raw.with_mut(|raw| { + raw.host_mut().poll_block_request( + &mut self.pending, + RequestId::new(usize::from(request)), + &mut self.slot, + ) + }) { + Ok(BlockPoll::Complete) => Ok(rdif_block::RequestStatus::Complete), + Ok(BlockPoll::Pending) => Ok(rdif_block::RequestStatus::Pending), + Ok(_) => Err(rdif_block::BlkError::Other( + "Phytium MCI returned an unknown poll state", )), Err(err) => Err(map_dev_err_to_blk_err(err)), } @@ -354,17 +441,17 @@ impl MciBlockQueue { self.pending.as_ref().map(BlockRequest::id) } - fn reap_pending_request(&mut self) -> Result<(), rd_block::BlkError> { + fn reap_pending_request(&mut self) -> Result { let Some(active) = self.pending_id() else { - return Ok(()); + return Ok(rdif_block::RequestStatus::Complete); }; - let id = rd_block::RequestId::new(usize::from(active)); + let id = rdif_block::RequestId::new(usize::from(active)); match self.poll_active_request(id) { - Ok(()) => { + Ok(rdif_block::RequestStatus::Complete) => { self.completed.push(id); - Ok(()) + Ok(rdif_block::RequestStatus::Complete) } - Err(rd_block::BlkError::Retry) => Err(rd_block::BlkError::Retry), + Ok(rdif_block::RequestStatus::Pending) => Err(rdif_block::BlkError::Retry), Err(err) => Err(err), } } @@ -378,9 +465,9 @@ fn submit_read_request( dma: &DeviceDma, slot: &mut BlockRequestSlot, pending: &mut Option, -) -> Result { +) -> Result { if pending.is_some() { - return Err(rd_block::BlkError::Retry); + return Err(rdif_block::BlkError::Retry); } let request = match host.submit_read_blocks( start_block, @@ -421,9 +508,9 @@ fn submit_write_request( dma: &DeviceDma, slot: &mut BlockRequestSlot, pending: &mut Option, -) -> Result { +) -> Result { if pending.is_some() { - return Err(rd_block::BlkError::Retry); + return Err(rdif_block::BlkError::Retry); } let request = match host.submit_write_blocks( start_block, @@ -463,27 +550,27 @@ fn can_fallback_to_fifo(err: Error) -> bool { ) } -fn block_addr_for_card(block_id: usize, high_capacity: bool) -> Result { +fn block_addr_for_card(block_id: u64, high_capacity: bool) -> Result { let block_id = - u32::try_from(block_id).map_err(|_| rd_block::BlkError::InvalidBlockIndex(block_id))?; + u32::try_from(block_id).map_err(|_| rdif_block::BlkError::InvalidBlockIndex(block_id))?; if high_capacity { Ok(block_id) } else { block_id .checked_mul(BLOCK_SIZE as u32) - .ok_or(rd_block::BlkError::InvalidBlockIndex(block_id as usize)) + .ok_or(rdif_block::BlkError::InvalidBlockIndex(block_id as u64)) } } -fn map_dev_err_to_blk_err(err: Error) -> rd_block::BlkError { +fn map_dev_err_to_blk_err(err: Error) -> rdif_block::BlkError { match err { Error::NoCard | Error::UnsupportedCommand | Error::CardLocked => { - rd_block::BlkError::NotSupported + rdif_block::BlkError::NotSupported } Error::Misaligned | Error::InvalidArgument => { - rd_block::BlkError::Other("Phytium MCI request is not block aligned".into()) + rdif_block::BlkError::Other("Phytium MCI request is not block aligned") } - _ => rd_block::BlkError::Other("Phytium MCI I/O error".into()), + _ => rdif_block::BlkError::Io, } } diff --git a/drivers/ax-driver/src/block/rockchip/sdhci_rk3568.rs b/drivers/ax-driver/src/block/rockchip/sdhci_rk3568.rs index 1a8cfb68f9..fc086d6973 100644 --- a/drivers/ax-driver/src/block/rockchip/sdhci_rk3568.rs +++ b/drivers/ax-driver/src/block/rockchip/sdhci_rk3568.rs @@ -12,10 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use alloc::{format, string::ToString, sync::Arc, vec::Vec}; -use core::{num::NonZeroUsize, ptr::NonNull, time::Duration}; +use alloc::{format, string::ToString, vec::Vec}; +use core::{ + num::NonZeroUsize, + ptr::NonNull, + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; -use ax_kspin::SpinNoIrq; use dma_api::DeviceDma; use log::{info, warn}; use rdif_clk::ClockId; @@ -29,7 +33,7 @@ use sdmmc_protocol::{ use spin::Once; use crate::{ - block::{PlatformDeviceBlock, decode_fdt_irq}, + block::{PlatformDeviceBlock, SharedDriver, decode_fdt_irq}, mmio::iomap, }; @@ -160,12 +164,13 @@ fn probe(info: FdtInfo<'_>, plat_dev: PlatformDevice) -> Result<(), OnProbeError ); let irq_num = decode_fdt_irq(&info.interrupts()); - let raw = Arc::new(SpinNoIrq::new(card)); + let raw = SharedDriver::new(card); let dev = BlockDevice { raw: Some(raw.clone()), capacity_blocks: card_info.capacity_blocks.unwrap_or(0), - irq_enabled: false, + irq_enabled: AtomicBool::new(false), queue_created: false, + irq_handler_taken: false, }; plat_dev.register_block_with_irq(dev, irq_num); info!( @@ -366,20 +371,21 @@ fn clock_error() -> Error { } struct BlockDevice { - raw: Option>>, + raw: Option>, capacity_blocks: u64, - irq_enabled: bool, + irq_enabled: AtomicBool, queue_created: bool, + irq_handler_taken: bool, } struct BlockQueue { - raw: Arc>, + raw: SharedDriver, capacity_blocks: u64, id: usize, dma: DeviceDma, slot: BlockRequestSlot, pending: Option, - completed: Vec, + completed: Vec, } impl DriverGeneric for BlockDevice { @@ -388,123 +394,208 @@ impl DriverGeneric for BlockDevice { } } -impl rd_block::Interface for BlockDevice { - fn create_queue(&mut self) -> Option> { +impl rdif_block::Interface for BlockDevice { + fn device_info(&self) -> rdif_block::DeviceInfo { + rdif_block::DeviceInfo { + name: Some("rockchip-rk3568-sdhci"), + ..rdif_block::DeviceInfo::new(self.capacity_blocks, BLOCK_SIZE) + } + } + + fn queue_limits(&self) -> rdif_block::QueueLimits { + rdif_block::QueueLimits { + dma_mask: u32::MAX as u64, + dma_alignment: BLOCK_SIZE, + max_blocks_per_request: u16::MAX as u32 + 1, + max_segments: 1, + max_segment_size: usize::MAX, + supported_flags: rdif_block::RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + } + } + + fn create_queue(&mut self) -> Option> { if self.queue_created { return None; } self.raw.as_ref().map(|dev| { self.queue_created = true; - alloc::boxed::Box::new(BlockQueue { - raw: dev.clone(), - capacity_blocks: self.capacity_blocks, - id: 0, - dma: axklib::dma::device_with_mask(u32::MAX as u64), - slot: BlockRequestSlot::default(), - pending: None, - completed: Vec::new(), - }) as _ + alloc::boxed::Box::new(BlockQueue::new(dev.clone(), self.capacity_blocks, 0)) as _ }) } - fn enable_irq(&mut self) { + fn enable_irq(&self) { if let Some(raw) = &self.raw { - let mut raw = raw.lock(); - if let Err(err) = SdioHost::enable_completion_irq(raw.host_mut()) { - warn!( - "rockchip-rk3568-sdhci: enable completion IRQ failed: {:?}", - err - ); - return; - } - self.irq_enabled = true; + let mut enabled = false; + raw.with_mut(|raw| { + if let Err(err) = SdioHost::enable_completion_irq(raw.host_mut()) { + warn!( + "rockchip-rk3568-sdhci: enable completion IRQ failed: {:?}", + err + ); + return; + } + enabled = true; + }); + self.irq_enabled.store(enabled, Ordering::Release); } } - fn disable_irq(&mut self) { + fn disable_irq(&self) { if let Some(raw) = &self.raw { - let mut raw = raw.lock(); - if let Err(err) = SdioHost::disable_completion_irq(raw.host_mut()) { - warn!( - "rockchip-rk3568-sdhci: disable completion IRQ failed: {:?}", - err - ); - } + raw.with_mut(|raw| { + if let Err(err) = SdioHost::disable_completion_irq(raw.host_mut()) { + warn!( + "rockchip-rk3568-sdhci: disable completion IRQ failed: {:?}", + err + ); + } + }); } - self.irq_enabled = false; + self.irq_enabled.store(false, Ordering::Release); } fn is_irq_enabled(&self) -> bool { - self.irq_enabled + self.irq_enabled.load(Ordering::Acquire) } - fn handle_irq(&mut self) -> rd_block::Event { - let Some(raw) = &self.raw else { - return rd_block::Event::none(); - }; - let irq_event = raw.lock().host_mut().handle_irq(); - block_event_from_sdhci_irq(irq_event) + fn irq_sources(&self) -> rdif_block::IrqSourceList { + alloc::vec![rdif_block::IrqSourceInfo::legacy( + rdif_block::IdList::from_bits(1), + )] + } + + fn take_irq_handler( + &mut self, + source_id: usize, + ) -> Option> { + if source_id != 0 { + return None; + } + if self.irq_handler_taken { + return None; + } + let raw = self.raw.as_ref()?.clone(); + self.irq_handler_taken = true; + Some(alloc::boxed::Box::new(BlockIrqHandler { raw })) } } -fn block_event_from_sdhci_irq(irq_event: sdhci_host::Event) -> rd_block::Event { +struct BlockIrqHandler { + raw: SharedDriver, +} + +impl rdif_block::IrqHandler for BlockIrqHandler { + fn handle_irq(&self) -> rdif_block::Event { + self.raw + .try_with_mut(|raw| block_event_from_sdhci_irq(raw.host_mut().handle_irq())) + .unwrap_or_else(rdif_block::Event::none) + } +} + +fn block_event_from_sdhci_irq(irq_event: sdhci_host::Event) -> rdif_block::Event { match irq_event { - sdhci_host::Event::None => rd_block::Event::none(), + sdhci_host::Event::None => rdif_block::Event::none(), sdhci_host::Event::CommandComplete | sdhci_host::Event::TransferComplete | sdhci_host::Event::Error { .. } | sdhci_host::Event::Other { .. } => { - let mut event = rd_block::Event::none(); - event.queue.insert(0); + let mut event = rdif_block::Event::none(); + event.queues.insert(0); event } } } -impl rd_block::IQueue for BlockQueue { - fn num_blocks(&self) -> usize { - self.capacity_blocks as usize +// SAFETY: This queue has one pending request slot and does not retain segment +// access after the request is completed or failed. +unsafe impl rdif_block::IQueue for BlockQueue { + fn id(&self) -> usize { + self.id } - fn block_size(&self) -> usize { - BLOCK_SIZE + fn info(&self) -> rdif_block::QueueInfo { + rdif_block::QueueInfo { + id: self.id, + device: rdif_block::DeviceInfo { + name: Some("rockchip-rk3568-sdhci"), + ..rdif_block::DeviceInfo::new(self.capacity_blocks, BLOCK_SIZE) + }, + limits: rdif_block::QueueLimits { + dma_mask: self.dma.dma_mask(), + dma_alignment: BLOCK_SIZE, + max_blocks_per_request: u16::MAX as u32 + 1, + max_segments: 1, + max_segment_size: usize::MAX, + supported_flags: rdif_block::RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + }, + } } - fn id(&self) -> usize { - self.id + fn submit_request( + &mut self, + request: rdif_block::Request<'_>, + ) -> Result { + self.submit_request_inner(request) + } + + fn poll_request( + &mut self, + request: rdif_block::RequestId, + ) -> Result { + self.poll_request_inner(request) } +} - fn buff_config(&self) -> rd_block::BuffConfig { - rd_block::BuffConfig { - dma_mask: self.dma.dma_mask(), - align: BLOCK_SIZE, - size: BLOCK_SIZE, +impl BlockQueue { + fn new(raw: SharedDriver, capacity_blocks: u64, id: usize) -> Self { + Self { + raw, + capacity_blocks, + id, + dma: axklib::dma::device_with_mask(u32::MAX as u64), + slot: BlockRequestSlot::default(), + pending: None, + completed: Vec::new(), } } - fn submit_request( + fn queue_info(&self) -> rdif_block::QueueInfo { + rdif_block::IQueue::info(self) + } + + fn submit_request_inner( &mut self, - request: rd_block::Request<'_>, - ) -> Result { + request: rdif_block::Request<'_>, + ) -> Result { + let info = self.queue_info(); + rdif_block::validate_request(info, &request)?; self.reap_pending_request()?; - let mut raw = self.raw.lock(); - let start_block = block_addr_for_card(request.block_id, raw.is_high_capacity())?; - // Block I/O uses the host crate's submit/poll request API so - // completions can be driven by IRQ wakeups. Protocol data commands - // use the same submit/poll contract through SdioHost. - match request.kind { - rd_block::RequestKind::Read(buffer) => { - if !buffer.len().is_multiple_of(BLOCK_SIZE) { - return Err(rd_block::BlkError::Other( - "read buffer is not block aligned".into(), - )); - } - let ptr = NonNull::new(buffer.virt).ok_or_else(|| { - rd_block::BlkError::Other("read buffer pointer is null".into()) - })?; - let size = NonZeroUsize::new(buffer.len()) - .ok_or_else(|| rd_block::BlkError::Other("read buffer is empty".into()))?; - let id = submit_read_request( + let raw = self.raw.clone(); + raw.with_mut(|raw| { + let start_block = block_addr_for_card(request.lba, raw.is_high_capacity())?; + // Block I/O uses the host crate's submit/poll request API so + // completions can be driven by IRQ wakeups. Protocol data commands + // use the same submit/poll contract through SdioHost. + let buffer = request + .segments + .first() + .copied() + .ok_or(rdif_block::BlkError::InvalidRequest)?; + if !buffer.len().is_multiple_of(BLOCK_SIZE) { + return Err(rdif_block::BlkError::Other("buffer is not block aligned")); + } + let ptr = NonNull::new(buffer.virt) + .ok_or(rdif_block::BlkError::Other("buffer pointer is null"))?; + let size = NonZeroUsize::new(buffer.len()) + .ok_or(rdif_block::BlkError::Other("buffer is empty"))?; + let id = match request.op { + rdif_block::RequestOp::Read => submit_read_request( raw.host_mut(), start_block, ptr, @@ -512,21 +603,8 @@ impl rd_block::IQueue for BlockQueue { &self.dma, &mut self.slot, &mut self.pending, - )?; - Ok(rd_block::RequestId::new(usize::from(id))) - } - rd_block::RequestKind::Write(items) => { - if !items.len().is_multiple_of(BLOCK_SIZE) { - return Err(rd_block::BlkError::Other( - "write buffer is not block aligned".into(), - )); - } - let ptr = NonNull::new(items.as_ptr() as *mut u8).ok_or_else(|| { - rd_block::BlkError::Other("write buffer pointer is null".into()) - })?; - let size = NonZeroUsize::new(items.len()) - .ok_or_else(|| rd_block::BlkError::Other("write buffer is empty".into()))?; - let id = submit_write_request( + )?, + rdif_block::RequestOp::Write => submit_write_request( raw.host_mut(), start_block, ptr, @@ -534,35 +612,44 @@ impl rd_block::IQueue for BlockQueue { &self.dma, &mut self.slot, &mut self.pending, - )?; - Ok(rd_block::RequestId::new(usize::from(id))) - } - } + )?, + rdif_block::RequestOp::Flush + | rdif_block::RequestOp::Discard + | rdif_block::RequestOp::WriteZeroes => { + return Err(rdif_block::BlkError::NotSupported); + } + }; + Ok(rdif_block::RequestId::new(usize::from(id))) + }) } - fn poll_request(&mut self, request: rd_block::RequestId) -> Result<(), rd_block::BlkError> { + fn poll_request_inner( + &mut self, + request: rdif_block::RequestId, + ) -> Result { if let Some(index) = self.completed.iter().position(|id| *id == request) { self.completed.swap_remove(index); - return Ok(()); + return Ok(rdif_block::RequestStatus::Complete); } self.poll_active_request(request) } -} -impl BlockQueue { fn poll_active_request( &mut self, - request: rd_block::RequestId, - ) -> Result<(), rd_block::BlkError> { - match self.raw.lock().host_mut().poll_block_request( - &mut self.pending, - RequestId::new(usize::from(request)), - &mut self.slot, - ) { - Ok(BlockPoll::Complete) => Ok(()), - Ok(BlockPoll::Pending) => Err(rd_block::BlkError::Retry), - Ok(_) => Err(rd_block::BlkError::Other( - "SDHCI returned an unknown poll state".into(), + request: rdif_block::RequestId, + ) -> Result { + let raw = self.raw.clone(); + match raw.with_mut(|raw| { + raw.host_mut().poll_block_request( + &mut self.pending, + RequestId::new(usize::from(request)), + &mut self.slot, + ) + }) { + Ok(BlockPoll::Complete) => Ok(rdif_block::RequestStatus::Complete), + Ok(BlockPoll::Pending) => Ok(rdif_block::RequestStatus::Pending), + Ok(_) => Err(rdif_block::BlkError::Other( + "SDHCI returned an unknown poll state", )), Err(err) => Err(map_dev_err_to_blk_err(err)), } @@ -572,17 +659,17 @@ impl BlockQueue { self.pending.as_ref().map(BlockRequest::id) } - fn reap_pending_request(&mut self) -> Result<(), rd_block::BlkError> { + fn reap_pending_request(&mut self) -> Result { let Some(active) = self.pending_id() else { - return Ok(()); + return Ok(rdif_block::RequestStatus::Complete); }; - let id = rd_block::RequestId::new(usize::from(active)); + let id = rdif_block::RequestId::new(usize::from(active)); match self.poll_active_request(id) { - Ok(()) => { + Ok(rdif_block::RequestStatus::Complete) => { self.completed.push(id); - Ok(()) + Ok(rdif_block::RequestStatus::Complete) } - Err(rd_block::BlkError::Retry) => Err(rd_block::BlkError::Retry), + Ok(rdif_block::RequestStatus::Pending) => Err(rdif_block::BlkError::Retry), Err(err) => Err(err), } } @@ -600,9 +687,9 @@ fn submit_read_request( dma: &DeviceDma, slot: &mut BlockRequestSlot, pending: &mut Option, -) -> Result { +) -> Result { if pending.is_some() { - return Err(rd_block::BlkError::Retry); + return Err(rdif_block::BlkError::Retry); } let request = match host.submit_read_blocks( start_block, @@ -638,9 +725,9 @@ fn submit_write_request( dma: &DeviceDma, slot: &mut BlockRequestSlot, pending: &mut Option, -) -> Result { +) -> Result { if pending.is_some() { - return Err(rd_block::BlkError::Retry); + return Err(rdif_block::BlkError::Retry); } let request = match host.submit_write_blocks( start_block, @@ -683,27 +770,27 @@ fn can_fallback_to_fifo(err: Error) -> bool { ) } -fn block_addr_for_card(block_id: usize, high_capacity: bool) -> Result { +fn block_addr_for_card(block_id: u64, high_capacity: bool) -> Result { let block_id = - u32::try_from(block_id).map_err(|_| rd_block::BlkError::InvalidBlockIndex(block_id))?; + u32::try_from(block_id).map_err(|_| rdif_block::BlkError::InvalidBlockIndex(block_id))?; if high_capacity { Ok(block_id) } else { block_id .checked_mul(BLOCK_SIZE as u32) - .ok_or(rd_block::BlkError::InvalidBlockIndex(block_id as usize)) + .ok_or(rdif_block::BlkError::InvalidBlockIndex(block_id as u64)) } } -fn map_dev_err_to_blk_err(err: Error) -> rd_block::BlkError { +fn map_dev_err_to_blk_err(err: Error) -> rdif_block::BlkError { match err { Error::NoCard | Error::UnsupportedCommand | Error::CardLocked => { - rd_block::BlkError::NotSupported + rdif_block::BlkError::NotSupported } Error::Misaligned | Error::InvalidArgument => { - rd_block::BlkError::Other("SD/MMC request is not block aligned".into()) + rdif_block::BlkError::Other("SD/MMC request is not block aligned") } - _ => rd_block::BlkError::Other("SDHCI I/O error".into()), + _ => rdif_block::BlkError::Io, } } diff --git a/drivers/ax-driver/src/block/rockchip_mmc.rs b/drivers/ax-driver/src/block/rockchip_mmc.rs index 0f83268a64..2f9aec7de4 100644 --- a/drivers/ax-driver/src/block/rockchip_mmc.rs +++ b/drivers/ax-driver/src/block/rockchip_mmc.rs @@ -12,10 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use alloc::{format, string::ToString, sync::Arc, vec::Vec}; -use core::{num::NonZeroUsize, ptr::NonNull, time::Duration}; +use alloc::{format, string::ToString, vec::Vec}; +use core::{ + num::NonZeroUsize, + ptr::NonNull, + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; -use ax_kspin::SpinNoIrq; use dma_api::DeviceDma; use log::{info, warn}; use rdif_clk::ClockId; @@ -29,7 +33,7 @@ use sdmmc_protocol::{ use spin::Once; use crate::{ - block::{PlatformDeviceBlock, decode_fdt_irq}, + block::{PlatformDeviceBlock, SharedDriver, decode_fdt_irq}, mmio::iomap, }; @@ -112,12 +116,13 @@ fn probe(info: FdtInfo<'_>, plat_dev: PlatformDevice) -> Result<(), OnProbeError ); let irq_num = decode_fdt_irq(&info.interrupts()); - let raw = Arc::new(SpinNoIrq::new(card)); + let raw = SharedDriver::new(card); let dev = BlockDevice { raw: Some(raw.clone()), capacity_blocks: card_info.capacity_blocks.unwrap_or(0), - irq_enabled: false, + irq_enabled: AtomicBool::new(false), queue_created: false, + irq_handler_taken: false, }; plat_dev.register_block_with_irq(dev, irq_num); info!("rockchip-sdhci block device registered irq={:?}", irq_num); @@ -224,20 +229,21 @@ fn clock_error() -> Error { } struct BlockDevice { - raw: Option>>, + raw: Option>, capacity_blocks: u64, - irq_enabled: bool, + irq_enabled: AtomicBool, queue_created: bool, + irq_handler_taken: bool, } struct BlockQueue { - raw: Arc>, + raw: SharedDriver, capacity_blocks: u64, id: usize, dma: DeviceDma, slot: BlockRequestSlot, pending: Option, - completed: Vec, + completed: Vec, } impl DriverGeneric for BlockDevice { @@ -246,117 +252,202 @@ impl DriverGeneric for BlockDevice { } } -impl rd_block::Interface for BlockDevice { - fn create_queue(&mut self) -> Option> { +impl rdif_block::Interface for BlockDevice { + fn device_info(&self) -> rdif_block::DeviceInfo { + rdif_block::DeviceInfo { + name: Some("rockchip-sdhci"), + ..rdif_block::DeviceInfo::new(self.capacity_blocks, BLOCK_SIZE) + } + } + + fn queue_limits(&self) -> rdif_block::QueueLimits { + rdif_block::QueueLimits { + dma_mask: u32::MAX as u64, + dma_alignment: BLOCK_SIZE, + max_blocks_per_request: u16::MAX as u32 + 1, + max_segments: 1, + max_segment_size: usize::MAX, + supported_flags: rdif_block::RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + } + } + + fn create_queue(&mut self) -> Option> { if self.queue_created { return None; } self.raw.as_ref().map(|dev| { self.queue_created = true; - alloc::boxed::Box::new(BlockQueue { - raw: dev.clone(), - capacity_blocks: self.capacity_blocks, - id: 0, - dma: axklib::dma::device_with_mask(u32::MAX as u64), - slot: BlockRequestSlot::default(), - pending: None, - completed: Vec::new(), - }) as _ + alloc::boxed::Box::new(BlockQueue::new(dev.clone(), self.capacity_blocks, 0)) as _ }) } - fn enable_irq(&mut self) { + fn enable_irq(&self) { if let Some(raw) = &self.raw { - let mut raw = raw.lock(); - if let Err(err) = SdioHost::enable_completion_irq(raw.host_mut()) { - warn!("rockchip-sdhci: enable completion IRQ failed: {:?}", err); - return; - } - self.irq_enabled = true; + let mut enabled = false; + raw.with_mut(|raw| { + if let Err(err) = SdioHost::enable_completion_irq(raw.host_mut()) { + warn!("rockchip-sdhci: enable completion IRQ failed: {:?}", err); + return; + } + enabled = true; + }); + self.irq_enabled.store(enabled, Ordering::Release); } } - fn disable_irq(&mut self) { + fn disable_irq(&self) { if let Some(raw) = &self.raw { - let mut raw = raw.lock(); - if let Err(err) = SdioHost::disable_completion_irq(raw.host_mut()) { - warn!("rockchip-sdhci: disable completion IRQ failed: {:?}", err); - } + raw.with_mut(|raw| { + if let Err(err) = SdioHost::disable_completion_irq(raw.host_mut()) { + warn!("rockchip-sdhci: disable completion IRQ failed: {:?}", err); + } + }); } - self.irq_enabled = false; + self.irq_enabled.store(false, Ordering::Release); } fn is_irq_enabled(&self) -> bool { - self.irq_enabled + self.irq_enabled.load(Ordering::Acquire) } - fn handle_irq(&mut self) -> rd_block::Event { - let Some(raw) = &self.raw else { - return rd_block::Event::none(); - }; - let irq_event = raw.lock().host_mut().handle_irq(); - block_event_from_sdhci_irq(irq_event) + fn irq_sources(&self) -> rdif_block::IrqSourceList { + alloc::vec![rdif_block::IrqSourceInfo::legacy( + rdif_block::IdList::from_bits(1), + )] + } + + fn take_irq_handler( + &mut self, + source_id: usize, + ) -> Option> { + if source_id != 0 { + return None; + } + if self.irq_handler_taken { + return None; + } + let raw = self.raw.as_ref()?.clone(); + self.irq_handler_taken = true; + Some(alloc::boxed::Box::new(BlockIrqHandler { raw })) } } -fn block_event_from_sdhci_irq(irq_event: sdhci_host::Event) -> rd_block::Event { +struct BlockIrqHandler { + raw: SharedDriver, +} + +impl rdif_block::IrqHandler for BlockIrqHandler { + fn handle_irq(&self) -> rdif_block::Event { + self.raw + .try_with_mut(|raw| block_event_from_sdhci_irq(raw.host_mut().handle_irq())) + .unwrap_or_else(rdif_block::Event::none) + } +} + +fn block_event_from_sdhci_irq(irq_event: sdhci_host::Event) -> rdif_block::Event { match irq_event { - sdhci_host::Event::None => rd_block::Event::none(), + sdhci_host::Event::None => rdif_block::Event::none(), sdhci_host::Event::CommandComplete | sdhci_host::Event::TransferComplete | sdhci_host::Event::Error { .. } | sdhci_host::Event::Other { .. } => { - let mut event = rd_block::Event::none(); - event.queue.insert(0); + let mut event = rdif_block::Event::none(); + event.queues.insert(0); event } } } -impl rd_block::IQueue for BlockQueue { - fn num_blocks(&self) -> usize { - self.capacity_blocks as usize +// SAFETY: The SDHCI queue stores only one pending request at a time and keeps +// segment access bounded to the pending slot until completion/error is polled. +unsafe impl rdif_block::IQueue for BlockQueue { + fn id(&self) -> usize { + self.id } - fn block_size(&self) -> usize { - BLOCK_SIZE + fn info(&self) -> rdif_block::QueueInfo { + rdif_block::QueueInfo { + id: self.id, + device: rdif_block::DeviceInfo { + name: Some("rockchip-sdhci"), + ..rdif_block::DeviceInfo::new(self.capacity_blocks, BLOCK_SIZE) + }, + limits: rdif_block::QueueLimits { + dma_mask: self.dma.dma_mask(), + dma_alignment: BLOCK_SIZE, + max_blocks_per_request: u16::MAX as u32 + 1, + max_segments: 1, + max_segment_size: usize::MAX, + supported_flags: rdif_block::RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + }, + } } - fn id(&self) -> usize { - self.id + fn submit_request( + &mut self, + request: rdif_block::Request<'_>, + ) -> Result { + self.submit_request_inner(request) + } + + fn poll_request( + &mut self, + request: rdif_block::RequestId, + ) -> Result { + self.poll_request_inner(request) } +} - fn buff_config(&self) -> rd_block::BuffConfig { - rd_block::BuffConfig { - dma_mask: self.dma.dma_mask(), - align: BLOCK_SIZE, - size: BLOCK_SIZE, +impl BlockQueue { + fn new(raw: SharedDriver, capacity_blocks: u64, id: usize) -> Self { + Self { + raw, + capacity_blocks, + id, + dma: axklib::dma::device_with_mask(u32::MAX as u64), + slot: BlockRequestSlot::default(), + pending: None, + completed: Vec::new(), } } - fn submit_request( + fn queue_info(&self) -> rdif_block::QueueInfo { + rdif_block::IQueue::info(self) + } + + fn submit_request_inner( &mut self, - request: rd_block::Request<'_>, - ) -> Result { + request: rdif_block::Request<'_>, + ) -> Result { + let info = self.queue_info(); + rdif_block::validate_request(info, &request)?; self.reap_pending_request()?; - let mut raw = self.raw.lock(); - let start_block = block_addr_for_card(request.block_id, raw.is_high_capacity())?; - // Block I/O uses the host crate's submit/poll request API so - // completions can be driven by IRQ wakeups. Protocol data commands - // use the same submit/poll contract through SdioHost. - match request.kind { - rd_block::RequestKind::Read(buffer) => { - if !buffer.len().is_multiple_of(BLOCK_SIZE) { - return Err(rd_block::BlkError::Other( - "read buffer is not block aligned".into(), - )); - } - let ptr = NonNull::new(buffer.virt).ok_or_else(|| { - rd_block::BlkError::Other("read buffer pointer is null".into()) - })?; - let size = NonZeroUsize::new(buffer.len()) - .ok_or_else(|| rd_block::BlkError::Other("read buffer is empty".into()))?; - let id = submit_read_request( + let raw = self.raw.clone(); + raw.with_mut(|raw| { + let start_block = block_addr_for_card(request.lba, raw.is_high_capacity())?; + // Block I/O uses the host crate's submit/poll request API so + // completions can be driven by IRQ wakeups. Protocol data commands + // use the same submit/poll contract through SdioHost. + let buffer = request + .segments + .first() + .copied() + .ok_or(rdif_block::BlkError::InvalidRequest)?; + if !buffer.len().is_multiple_of(BLOCK_SIZE) { + return Err(rdif_block::BlkError::Other("buffer is not block aligned")); + } + let ptr = NonNull::new(buffer.virt) + .ok_or(rdif_block::BlkError::Other("buffer pointer is null"))?; + let size = NonZeroUsize::new(buffer.len()) + .ok_or(rdif_block::BlkError::Other("buffer is empty"))?; + let id = match request.op { + rdif_block::RequestOp::Read => submit_read_request( raw.host_mut(), start_block, ptr, @@ -364,21 +455,8 @@ impl rd_block::IQueue for BlockQueue { &self.dma, &mut self.slot, &mut self.pending, - )?; - Ok(rd_block::RequestId::new(usize::from(id))) - } - rd_block::RequestKind::Write(items) => { - if !items.len().is_multiple_of(BLOCK_SIZE) { - return Err(rd_block::BlkError::Other( - "write buffer is not block aligned".into(), - )); - } - let ptr = NonNull::new(items.as_ptr() as *mut u8).ok_or_else(|| { - rd_block::BlkError::Other("write buffer pointer is null".into()) - })?; - let size = NonZeroUsize::new(items.len()) - .ok_or_else(|| rd_block::BlkError::Other("write buffer is empty".into()))?; - let id = submit_write_request( + )?, + rdif_block::RequestOp::Write => submit_write_request( raw.host_mut(), start_block, ptr, @@ -386,35 +464,44 @@ impl rd_block::IQueue for BlockQueue { &self.dma, &mut self.slot, &mut self.pending, - )?; - Ok(rd_block::RequestId::new(usize::from(id))) - } - } + )?, + rdif_block::RequestOp::Flush + | rdif_block::RequestOp::Discard + | rdif_block::RequestOp::WriteZeroes => { + return Err(rdif_block::BlkError::NotSupported); + } + }; + Ok(rdif_block::RequestId::new(usize::from(id))) + }) } - fn poll_request(&mut self, request: rd_block::RequestId) -> Result<(), rd_block::BlkError> { + fn poll_request_inner( + &mut self, + request: rdif_block::RequestId, + ) -> Result { if let Some(index) = self.completed.iter().position(|id| *id == request) { self.completed.swap_remove(index); - return Ok(()); + return Ok(rdif_block::RequestStatus::Complete); } self.poll_active_request(request) } -} -impl BlockQueue { fn poll_active_request( &mut self, - request: rd_block::RequestId, - ) -> Result<(), rd_block::BlkError> { - match self.raw.lock().host_mut().poll_block_request( - &mut self.pending, - RequestId::new(usize::from(request)), - &mut self.slot, - ) { - Ok(BlockPoll::Complete) => Ok(()), - Ok(BlockPoll::Pending) => Err(rd_block::BlkError::Retry), - Ok(_) => Err(rd_block::BlkError::Other( - "SDHCI returned an unknown poll state".into(), + request: rdif_block::RequestId, + ) -> Result { + let raw = self.raw.clone(); + match raw.with_mut(|raw| { + raw.host_mut().poll_block_request( + &mut self.pending, + RequestId::new(usize::from(request)), + &mut self.slot, + ) + }) { + Ok(BlockPoll::Complete) => Ok(rdif_block::RequestStatus::Complete), + Ok(BlockPoll::Pending) => Ok(rdif_block::RequestStatus::Pending), + Ok(_) => Err(rdif_block::BlkError::Other( + "SDHCI returned an unknown poll state", )), Err(err) => Err(map_dev_err_to_blk_err(err)), } @@ -424,17 +511,17 @@ impl BlockQueue { self.pending.as_ref().map(BlockRequest::id) } - fn reap_pending_request(&mut self) -> Result<(), rd_block::BlkError> { + fn reap_pending_request(&mut self) -> Result { let Some(active) = self.pending_id() else { - return Ok(()); + return Ok(rdif_block::RequestStatus::Complete); }; - let id = rd_block::RequestId::new(usize::from(active)); + let id = rdif_block::RequestId::new(usize::from(active)); match self.poll_active_request(id) { - Ok(()) => { + Ok(rdif_block::RequestStatus::Complete) => { self.completed.push(id); - Ok(()) + Ok(rdif_block::RequestStatus::Complete) } - Err(rd_block::BlkError::Retry) => Err(rd_block::BlkError::Retry), + Ok(rdif_block::RequestStatus::Pending) => Err(rdif_block::BlkError::Retry), Err(err) => Err(err), } } @@ -448,9 +535,9 @@ fn submit_read_request( dma: &DeviceDma, slot: &mut BlockRequestSlot, pending: &mut Option, -) -> Result { +) -> Result { if pending.is_some() { - return Err(rd_block::BlkError::Retry); + return Err(rdif_block::BlkError::Retry); } let request = match host.submit_read_blocks( start_block, @@ -486,9 +573,9 @@ fn submit_write_request( dma: &DeviceDma, slot: &mut BlockRequestSlot, pending: &mut Option, -) -> Result { +) -> Result { if pending.is_some() { - return Err(rd_block::BlkError::Retry); + return Err(rdif_block::BlkError::Retry); } let request = match host.submit_write_blocks( start_block, @@ -523,27 +610,27 @@ fn can_fallback_to_fifo(err: Error) -> bool { ) } -fn block_addr_for_card(block_id: usize, high_capacity: bool) -> Result { +fn block_addr_for_card(block_id: u64, high_capacity: bool) -> Result { let block_id = - u32::try_from(block_id).map_err(|_| rd_block::BlkError::InvalidBlockIndex(block_id))?; + u32::try_from(block_id).map_err(|_| rdif_block::BlkError::InvalidBlockIndex(block_id))?; if high_capacity { Ok(block_id) } else { block_id .checked_mul(BLOCK_SIZE as u32) - .ok_or(rd_block::BlkError::InvalidBlockIndex(block_id as usize)) + .ok_or(rdif_block::BlkError::InvalidBlockIndex(block_id as u64)) } } -fn map_dev_err_to_blk_err(err: Error) -> rd_block::BlkError { +fn map_dev_err_to_blk_err(err: Error) -> rdif_block::BlkError { match err { Error::NoCard | Error::UnsupportedCommand | Error::CardLocked => { - rd_block::BlkError::NotSupported + rdif_block::BlkError::NotSupported } Error::Misaligned | Error::InvalidArgument => { - rd_block::BlkError::Other("SD/MMC request is not block aligned".into()) + rdif_block::BlkError::Other("SD/MMC request is not block aligned") } - _ => rd_block::BlkError::Other("SDHCI I/O error".into()), + _ => rdif_block::BlkError::Io, } } diff --git a/drivers/ax-driver/src/block/rockchip_sd.rs b/drivers/ax-driver/src/block/rockchip_sd.rs index 7c240b020a..1855bde650 100644 --- a/drivers/ax-driver/src/block/rockchip_sd.rs +++ b/drivers/ax-driver/src/block/rockchip_sd.rs @@ -12,10 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use alloc::{format, sync::Arc}; +use alloc::format; use core::time::Duration; -use ax_kspin::SpinNoIrq; use dwmmc_host::DwMmc; use log::{info, warn}; use rdif_clk::ClockId; @@ -27,7 +26,7 @@ use sdmmc_protocol::{ }; use crate::{ - block::{PlatformDeviceBlock, decode_fdt_irq}, + block::{PlatformDeviceBlock, SharedDriver, decode_fdt_irq}, mmio::iomap, soc::scmi, }; @@ -132,12 +131,13 @@ fn probe(info: FdtInfo<'_>, plat_dev: PlatformDevice) -> Result<(), OnProbeError } let irq_num = decode_fdt_irq(&info.interrupts()); - let raw = Arc::new(SpinNoIrq::new(sd)); + let raw = SharedDriver::new(sd); let dev = SdBlockDevice { raw: Some(raw.clone()), capacity_blocks: card_info.capacity_blocks.unwrap_or(0), - irq_enabled: false, + irq_enabled: core::sync::atomic::AtomicBool::new(false), queue_created: false, + irq_handler_taken: false, }; plat_dev.register_block_with_irq(dev, irq_num); info!("rockchip-sd block device registered irq={:?}", irq_num); diff --git a/drivers/ax-driver/src/block/rockchip_sd/block.rs b/drivers/ax-driver/src/block/rockchip_sd/block.rs index a78a59fdc2..92afb772f7 100644 --- a/drivers/ax-driver/src/block/rockchip_sd/block.rs +++ b/drivers/ax-driver/src/block/rockchip_sd/block.rs @@ -1,7 +1,10 @@ -use alloc::{sync::Arc, vec::Vec}; -use core::{num::NonZeroUsize, ptr::NonNull}; +use alloc::vec::Vec; +use core::{ + num::NonZeroUsize, + ptr::NonNull, + sync::atomic::{AtomicBool, Ordering}, +}; -use ax_kspin::SpinNoIrq; use dma_api::DeviceDma; use dwmmc_host::{BlockPoll, BlockRequest, BlockRequestSlot, DwMmc, RequestId}; use log::warn; @@ -9,22 +12,24 @@ use rdrive::DriverGeneric; use sdmmc_protocol::{BlockTransferMode, Error, sdio::SdioHost}; use super::{BLOCK_SIZE, RockchipDwMmc}; +use crate::block::SharedDriver; pub(super) struct SdBlockDevice { - pub(super) raw: Option>>, + pub(super) raw: Option>, pub(super) capacity_blocks: u64, - pub(super) irq_enabled: bool, + pub(super) irq_enabled: AtomicBool, pub(super) queue_created: bool, + pub(super) irq_handler_taken: bool, } struct SdBlockQueue { - raw: Arc>, + raw: SharedDriver, capacity_blocks: u64, id: usize, dma: DeviceDma, slot: BlockRequestSlot, pending: Option, - completed: Vec, + completed: Vec, } impl DriverGeneric for SdBlockDevice { @@ -33,116 +38,201 @@ impl DriverGeneric for SdBlockDevice { } } -impl rd_block::Interface for SdBlockDevice { - fn create_queue(&mut self) -> Option> { +impl rdif_block::Interface for SdBlockDevice { + fn device_info(&self) -> rdif_block::DeviceInfo { + rdif_block::DeviceInfo { + name: Some("rockchip-sd"), + ..rdif_block::DeviceInfo::new(self.capacity_blocks, BLOCK_SIZE) + } + } + + fn queue_limits(&self) -> rdif_block::QueueLimits { + rdif_block::QueueLimits { + dma_mask: u32::MAX as u64, + dma_alignment: BLOCK_SIZE, + max_blocks_per_request: u16::MAX as u32 + 1, + max_segments: 1, + max_segment_size: usize::MAX, + supported_flags: rdif_block::RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + } + } + + fn create_queue(&mut self) -> Option> { if self.queue_created { return None; } self.raw.as_ref().map(|dev| { self.queue_created = true; - alloc::boxed::Box::new(SdBlockQueue { - raw: dev.clone(), - capacity_blocks: self.capacity_blocks, - id: 0, - dma: axklib::dma::device_with_mask(u32::MAX as u64), - slot: BlockRequestSlot::default(), - pending: None, - completed: Vec::new(), - }) as _ + alloc::boxed::Box::new(SdBlockQueue::new(dev.clone(), self.capacity_blocks, 0)) as _ }) } - fn enable_irq(&mut self) { + fn enable_irq(&self) { if let Some(raw) = &self.raw { - let mut raw = raw.lock(); - if let Err(err) = SdioHost::enable_completion_irq(raw.host_mut()) { - warn!("rockchip-dwmmc: enable completion IRQ failed: {:?}", err); - return; - } - self.irq_enabled = true; + let mut enabled = false; + raw.with_mut(|raw| { + if let Err(err) = SdioHost::enable_completion_irq(raw.host_mut()) { + warn!("rockchip-dwmmc: enable completion IRQ failed: {:?}", err); + return; + } + enabled = true; + }); + self.irq_enabled.store(enabled, Ordering::Release); } } - fn disable_irq(&mut self) { + fn disable_irq(&self) { if let Some(raw) = &self.raw { - let mut raw = raw.lock(); - if let Err(err) = SdioHost::disable_completion_irq(raw.host_mut()) { - warn!("rockchip-dwmmc: disable completion IRQ failed: {:?}", err); - } + raw.with_mut(|raw| { + if let Err(err) = SdioHost::disable_completion_irq(raw.host_mut()) { + warn!("rockchip-dwmmc: disable completion IRQ failed: {:?}", err); + } + }); } - self.irq_enabled = false; + self.irq_enabled.store(false, Ordering::Release); } fn is_irq_enabled(&self) -> bool { - self.irq_enabled + self.irq_enabled.load(Ordering::Acquire) } - fn handle_irq(&mut self) -> rd_block::Event { - let Some(raw) = &self.raw else { - return rd_block::Event::none(); - }; - let irq_event = raw.lock().host_mut().handle_irq(); - block_event_from_dwmmc_irq(irq_event) + fn irq_sources(&self) -> rdif_block::IrqSourceList { + alloc::vec![rdif_block::IrqSourceInfo::legacy( + rdif_block::IdList::from_bits(1), + )] + } + + fn take_irq_handler( + &mut self, + source_id: usize, + ) -> Option> { + if source_id != 0 { + return None; + } + if self.irq_handler_taken { + return None; + } + let raw = self.raw.as_ref()?.clone(); + self.irq_handler_taken = true; + Some(alloc::boxed::Box::new(SdBlockIrqHandler { raw })) + } +} + +struct SdBlockIrqHandler { + raw: SharedDriver, +} + +impl rdif_block::IrqHandler for SdBlockIrqHandler { + fn handle_irq(&self) -> rdif_block::Event { + self.raw + .try_with_mut(|raw| block_event_from_dwmmc_irq(raw.host_mut().handle_irq())) + .unwrap_or_else(rdif_block::Event::none) } } -fn block_event_from_dwmmc_irq(irq_event: dwmmc_host::Event) -> rd_block::Event { +fn block_event_from_dwmmc_irq(irq_event: dwmmc_host::Event) -> rdif_block::Event { match irq_event { - dwmmc_host::Event::None => rd_block::Event::none(), + dwmmc_host::Event::None => rdif_block::Event::none(), dwmmc_host::Event::CommandComplete | dwmmc_host::Event::TransferComplete | dwmmc_host::Event::ReceiveReady | dwmmc_host::Event::TransmitReady | dwmmc_host::Event::Error { .. } | dwmmc_host::Event::Other { .. } => { - let mut event = rd_block::Event::none(); - event.queue.insert(0); + let mut event = rdif_block::Event::none(); + event.queues.insert(0); event } } } -impl rd_block::IQueue for SdBlockQueue { - fn num_blocks(&self) -> usize { - self.capacity_blocks as usize +// SAFETY: SdBlockQueue uses a single pending request slot and releases all +// segment access when the matching request completes or errors. +unsafe impl rdif_block::IQueue for SdBlockQueue { + fn id(&self) -> usize { + self.id + } + + fn info(&self) -> rdif_block::QueueInfo { + rdif_block::QueueInfo { + id: self.id, + device: rdif_block::DeviceInfo { + name: Some("rockchip-sd"), + ..rdif_block::DeviceInfo::new(self.capacity_blocks, BLOCK_SIZE) + }, + limits: rdif_block::QueueLimits { + dma_mask: self.dma.dma_mask(), + dma_alignment: BLOCK_SIZE, + max_blocks_per_request: u16::MAX as u32 + 1, + max_segments: 1, + max_segment_size: usize::MAX, + supported_flags: rdif_block::RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + }, + } } - fn block_size(&self) -> usize { - BLOCK_SIZE + fn submit_request( + &mut self, + request: rdif_block::Request<'_>, + ) -> Result { + self.submit_request_inner(request) } - fn id(&self) -> usize { - self.id + fn poll_request( + &mut self, + request: rdif_block::RequestId, + ) -> Result { + self.poll_request_inner(request) } +} - fn buff_config(&self) -> rd_block::BuffConfig { - rd_block::BuffConfig { - dma_mask: self.dma.dma_mask(), - align: BLOCK_SIZE, - size: BLOCK_SIZE, +impl SdBlockQueue { + fn new(raw: SharedDriver, capacity_blocks: u64, id: usize) -> Self { + Self { + raw, + capacity_blocks, + id, + dma: axklib::dma::device_with_mask(u32::MAX as u64), + slot: BlockRequestSlot::default(), + pending: None, + completed: Vec::new(), } } - fn submit_request( + fn queue_info(&self) -> rdif_block::QueueInfo { + rdif_block::IQueue::info(self) + } + + fn submit_request_inner( &mut self, - request: rd_block::Request<'_>, - ) -> Result { + request: rdif_block::Request<'_>, + ) -> Result { + let info = self.queue_info(); + rdif_block::validate_request(info, &request)?; self.reap_pending_request()?; - let mut raw = self.raw.lock(); - let start_block = block_addr_for_card(request.block_id, raw.is_high_capacity())?; - match request.kind { - rd_block::RequestKind::Read(buffer) => { - if !buffer.len().is_multiple_of(BLOCK_SIZE) { - return Err(rd_block::BlkError::Other( - "read buffer is not block aligned".into(), - )); - } - let ptr = NonNull::new(buffer.virt).ok_or_else(|| { - rd_block::BlkError::Other("read buffer pointer is null".into()) - })?; - let size = NonZeroUsize::new(buffer.len()) - .ok_or_else(|| rd_block::BlkError::Other("read buffer is empty".into()))?; - let id = submit_read_request( + let raw = self.raw.clone(); + raw.with_mut(|raw| { + let start_block = block_addr_for_card(request.lba, raw.is_high_capacity())?; + let buffer = request + .segments + .first() + .copied() + .ok_or(rdif_block::BlkError::InvalidRequest)?; + if !buffer.len().is_multiple_of(BLOCK_SIZE) { + return Err(rdif_block::BlkError::Other("buffer is not block aligned")); + } + let ptr = NonNull::new(buffer.virt) + .ok_or(rdif_block::BlkError::Other("buffer pointer is null"))?; + let size = NonZeroUsize::new(buffer.len()) + .ok_or(rdif_block::BlkError::Other("buffer is empty"))?; + let id = match request.op { + rdif_block::RequestOp::Read => submit_read_request( raw.host_mut(), start_block, ptr, @@ -150,21 +240,8 @@ impl rd_block::IQueue for SdBlockQueue { &self.dma, &mut self.slot, &mut self.pending, - )?; - Ok(rd_block::RequestId::new(usize::from(id))) - } - rd_block::RequestKind::Write(items) => { - if !items.len().is_multiple_of(BLOCK_SIZE) { - return Err(rd_block::BlkError::Other( - "write buffer is not block aligned".into(), - )); - } - let ptr = NonNull::new(items.as_ptr() as *mut u8).ok_or_else(|| { - rd_block::BlkError::Other("write buffer pointer is null".into()) - })?; - let size = NonZeroUsize::new(items.len()) - .ok_or_else(|| rd_block::BlkError::Other("write buffer is empty".into()))?; - let id = submit_write_request( + )?, + rdif_block::RequestOp::Write => submit_write_request( raw.host_mut(), start_block, ptr, @@ -172,35 +249,44 @@ impl rd_block::IQueue for SdBlockQueue { &self.dma, &mut self.slot, &mut self.pending, - )?; - Ok(rd_block::RequestId::new(usize::from(id))) - } - } + )?, + rdif_block::RequestOp::Flush + | rdif_block::RequestOp::Discard + | rdif_block::RequestOp::WriteZeroes => { + return Err(rdif_block::BlkError::NotSupported); + } + }; + Ok(rdif_block::RequestId::new(usize::from(id))) + }) } - fn poll_request(&mut self, request: rd_block::RequestId) -> Result<(), rd_block::BlkError> { + fn poll_request_inner( + &mut self, + request: rdif_block::RequestId, + ) -> Result { if let Some(index) = self.completed.iter().position(|id| *id == request) { self.completed.swap_remove(index); - return Ok(()); + return Ok(rdif_block::RequestStatus::Complete); } self.poll_active_request(request) } -} -impl SdBlockQueue { fn poll_active_request( &mut self, - request: rd_block::RequestId, - ) -> Result<(), rd_block::BlkError> { - match self.raw.lock().host_mut().poll_block_request( - &mut self.pending, - RequestId::new(usize::from(request)), - &mut self.slot, - ) { - Ok(BlockPoll::Complete) => Ok(()), - Ok(BlockPoll::Pending) => Err(rd_block::BlkError::Retry), - Ok(_) => Err(rd_block::BlkError::Other( - "DWMMC returned an unknown poll state".into(), + request: rdif_block::RequestId, + ) -> Result { + let raw = self.raw.clone(); + match raw.with_mut(|raw| { + raw.host_mut().poll_block_request( + &mut self.pending, + RequestId::new(usize::from(request)), + &mut self.slot, + ) + }) { + Ok(BlockPoll::Complete) => Ok(rdif_block::RequestStatus::Complete), + Ok(BlockPoll::Pending) => Ok(rdif_block::RequestStatus::Pending), + Ok(_) => Err(rdif_block::BlkError::Other( + "DWMMC returned an unknown poll state", )), Err(err) => Err(map_dev_err_to_blk_err(err)), } @@ -210,17 +296,17 @@ impl SdBlockQueue { self.pending.as_ref().map(BlockRequest::id) } - fn reap_pending_request(&mut self) -> Result<(), rd_block::BlkError> { + fn reap_pending_request(&mut self) -> Result { let Some(active) = self.pending_id() else { - return Ok(()); + return Ok(rdif_block::RequestStatus::Complete); }; - let id = rd_block::RequestId::new(usize::from(active)); + let id = rdif_block::RequestId::new(usize::from(active)); match self.poll_active_request(id) { - Ok(()) => { + Ok(rdif_block::RequestStatus::Complete) => { self.completed.push(id); - Ok(()) + Ok(rdif_block::RequestStatus::Complete) } - Err(rd_block::BlkError::Retry) => Err(rd_block::BlkError::Retry), + Ok(rdif_block::RequestStatus::Pending) => Err(rdif_block::BlkError::Retry), Err(err) => Err(err), } } @@ -234,9 +320,9 @@ fn submit_read_request( dma: &DeviceDma, slot: &mut BlockRequestSlot, pending: &mut Option, -) -> Result { +) -> Result { if pending.is_some() { - return Err(rd_block::BlkError::Retry); + return Err(rdif_block::BlkError::Retry); } let request = match host.submit_read_blocks( start_block, @@ -272,9 +358,9 @@ fn submit_write_request( dma: &DeviceDma, slot: &mut BlockRequestSlot, pending: &mut Option, -) -> Result { +) -> Result { if pending.is_some() { - return Err(rd_block::BlkError::Retry); + return Err(rdif_block::BlkError::Retry); } let request = match host.submit_write_blocks( start_block, @@ -309,26 +395,26 @@ fn can_fallback_to_fifo(err: Error) -> bool { ) } -fn block_addr_for_card(block_id: usize, high_capacity: bool) -> Result { +fn block_addr_for_card(block_id: u64, high_capacity: bool) -> Result { let block_id = - u32::try_from(block_id).map_err(|_| rd_block::BlkError::InvalidBlockIndex(block_id))?; + u32::try_from(block_id).map_err(|_| rdif_block::BlkError::InvalidBlockIndex(block_id))?; if high_capacity { Ok(block_id) } else { block_id .checked_mul(BLOCK_SIZE as u32) - .ok_or(rd_block::BlkError::InvalidBlockIndex(block_id as usize)) + .ok_or(rdif_block::BlkError::InvalidBlockIndex(block_id as u64)) } } -fn map_dev_err_to_blk_err(err: Error) -> rd_block::BlkError { +fn map_dev_err_to_blk_err(err: Error) -> rdif_block::BlkError { match err { Error::NoCard | Error::UnsupportedCommand | Error::CardLocked => { - rd_block::BlkError::NotSupported + rdif_block::BlkError::NotSupported } Error::Misaligned | Error::InvalidArgument => { - rd_block::BlkError::Other("SD/MMC request is not block aligned".into()) + rdif_block::BlkError::Other("SD/MMC request is not block aligned") } - _ => rd_block::BlkError::Other("DWMMC I/O error".into()), + _ => rdif_block::BlkError::Io, } } diff --git a/drivers/ax-driver/src/block/shared.rs b/drivers/ax-driver/src/block/shared.rs new file mode 100644 index 0000000000..6ced1744b4 --- /dev/null +++ b/drivers/ax-driver/src/block/shared.rs @@ -0,0 +1,90 @@ +use alloc::sync::Arc; +use core::{ + cell::UnsafeCell, + sync::atomic::{AtomicBool, Ordering}, +}; + +pub(crate) struct SharedDriver { + inner: Arc>, +} + +struct SharedDriverInner { + value: UnsafeCell, + borrowed: AtomicBool, +} + +struct SharedDriverGuard<'a, T> { + inner: &'a SharedDriverInner, +} + +// SAFETY: `SharedDriver` centralizes the `UnsafeCell` boundary. Access to the +// inner value is serialized by `borrowed`; IRQ users must use `try_with_mut`, +// which never spins or blocks on the callback path. +unsafe impl Send for SharedDriverInner {} + +// SAFETY: See the `Send` impl. +unsafe impl Sync for SharedDriverInner {} + +impl SharedDriver { + pub(crate) fn new(value: T) -> Self { + Self { + inner: Arc::new(SharedDriverInner { + value: UnsafeCell::new(value), + borrowed: AtomicBool::new(false), + }), + } + } + + pub(crate) fn with_mut(&self, f: impl FnOnce(&mut T) -> R) -> R { + let mut guard = self.inner.enter(); + f(guard.get_mut()) + } + + #[cfg(any( + feature = "phytium-mci", + feature = "rockchip-dwmmc", + feature = "rockchip-sdhci" + ))] + pub(crate) fn try_with_mut(&self, f: impl FnOnce(&mut T) -> R) -> Option { + let mut guard = self.inner.try_enter()?; + Some(f(guard.get_mut())) + } +} + +impl Clone for SharedDriver { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} + +impl SharedDriverInner { + fn enter(&self) -> SharedDriverGuard<'_, T> { + loop { + if let Some(guard) = self.try_enter() { + return guard; + } + core::hint::spin_loop(); + } + } + + fn try_enter(&self) -> Option> { + self.borrowed + .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) + .ok()?; + Some(SharedDriverGuard { inner: self }) + } +} + +impl SharedDriverGuard<'_, T> { + fn get_mut(&mut self) -> &mut T { + unsafe { &mut *self.inner.value.get() } + } +} + +impl Drop for SharedDriverGuard<'_, T> { + fn drop(&mut self) { + self.inner.borrowed.store(false, Ordering::Release); + } +} diff --git a/drivers/ax-driver/src/net/binding.rs b/drivers/ax-driver/src/net/binding.rs index 400bcf7f6e..10f36efa07 100644 --- a/drivers/ax-driver/src/net/binding.rs +++ b/drivers/ax-driver/src/net/binding.rs @@ -7,16 +7,16 @@ use rdrive::{Device, DriverGeneric, probe::pci::EndpointRc}; pub struct PlatformNetDevice { name: &'static str, - net: Option, irq_num: Option, + net: Option, } impl PlatformNetDevice { fn new(name: &'static str, net: rd_net::Net, irq_num: Option) -> Self { Self { name, - net: Some(net), irq_num, + net: Some(net), } } @@ -67,31 +67,7 @@ pub fn pci_legacy_irq(endpoint: &EndpointRc) -> Option { } } - if let Some(irq) = - crate::pci::legacy_irq_for_endpoint(endpoint.address(), endpoint.interrupt_pin()) - { - return Some(irq); - } - - let line = endpoint.interrupt_line(); - if line == 0 || line == u8::MAX { - return None; - } - Some(pci_legacy_line_to_irq(line)) -} - -const fn pci_legacy_line_to_irq(line: u8) -> usize { - const PCI_IRQ_BASE: usize = if cfg!(target_arch = "x86_64") || cfg!(target_arch = "riscv64") { - if cfg!(target_arch = "x86_64") { - 0x20 - } else { - 0 - } - } else { - 0 - }; - - PCI_IRQ_BASE + line as usize + crate::pci::endpoint_legacy_irq(endpoint) } pub trait PlatformDeviceNet { diff --git a/drivers/ax-driver/src/pci/mod.rs b/drivers/ax-driver/src/pci/mod.rs index 440ba7e553..f6a9e0eafb 100644 --- a/drivers/ax-driver/src/pci/mod.rs +++ b/drivers/ax-driver/src/pci/mod.rs @@ -109,6 +109,7 @@ pub const fn has_static_endpoint_drivers() -> bool { feature = "ixgbe", feature = "intel-net", feature = "realtek-rtl8125", + feature = "nvme", feature = "xhci-pci", feature = "virtio-blk", feature = "virtio-net", @@ -209,6 +210,32 @@ pub fn legacy_irq_for_address(address: PciAddress) -> Option { legacy_irq_for_endpoint(address, 1) } +pub fn endpoint_legacy_irq(endpoint: &rdrive::probe::pci::EndpointRc) -> Option { + if let Some(irq) = legacy_irq_for_endpoint(endpoint.address(), endpoint.interrupt_pin()) { + return Some(irq); + } + + let line = endpoint.interrupt_line(); + if line == 0 || line == u8::MAX { + return None; + } + Some(pci_legacy_line_to_irq(line)) +} + +const fn pci_legacy_line_to_irq(line: u8) -> usize { + const PCI_IRQ_BASE: usize = if cfg!(target_arch = "x86_64") || cfg!(target_arch = "riscv64") { + if cfg!(target_arch = "x86_64") { + 0x20 + } else { + 0 + } + } else { + 0 + }; + + PCI_IRQ_BASE + line as usize +} + pub fn register_legacy_irq_route(bus_start: u8, bus_end: u8, irq: usize) { register_legacy_irq_routes(bus_start, bus_end, &[irq]); } diff --git a/drivers/ax-driver/src/virtio/block.rs b/drivers/ax-driver/src/virtio/block.rs index 8081ce07a7..2dd81012f4 100644 --- a/drivers/ax-driver/src/virtio/block.rs +++ b/drivers/ax-driver/src/virtio/block.rs @@ -11,7 +11,10 @@ use virtio_drivers::{ transport::Transport, }; -use crate::{block::PlatformDeviceBlock, virtio::VirtIoHalImpl}; +use crate::{ + block::{PlatformDeviceBlock, SharedDriver}, + virtio::VirtIoHalImpl, +}; const VIRTIO_BLK_DMA_BUFFER_SIZE: usize = 32 * SECTOR_SIZE; @@ -64,8 +67,8 @@ pub fn register_transport( let dev = VirtIoBlkDevice::new(transport) .map_err(|err| OnProbeError::other(format!("failed to initialize virtio-blk: {err:?}")))?; plat_dev.register_block(BlockDevice { - dev: Some(dev), - irq_enabled: false, + dev: Some(SharedDriver::new(dev)), + queue_created: false, }); log::info!("registered virtio block device"); Ok(()) @@ -86,8 +89,8 @@ impl VirtIoBlkDevice { } struct BlockDevice { - dev: Option>, - irq_enabled: bool, + dev: Option>>, + queue_created: bool, } impl DriverGeneric for BlockDevice { @@ -96,92 +99,147 @@ impl DriverGeneric for BlockDevice { } } -impl rd_block::Interface for BlockDevice { - fn create_queue(&mut self) -> Option> { - self.dev - .take() - .map(|dev| alloc::boxed::Box::new(BlockQueue { raw: dev }) as _) +impl rdif_block::Interface for BlockDevice { + fn device_info(&self) -> rdif_block::DeviceInfo { + let blocks = self + .dev + .as_ref() + .map(|dev| dev.with_mut(|raw| raw.raw.capacity())) + .unwrap_or(0); + rdif_block::DeviceInfo { + name: Some("virtio-blk"), + ..rdif_block::DeviceInfo::new(blocks, SECTOR_SIZE) + } + } + + fn queue_limits(&self) -> rdif_block::QueueLimits { + rdif_block::QueueLimits { + dma_mask: u64::MAX, + dma_alignment: 0x1000, + max_blocks_per_request: (VIRTIO_BLK_DMA_BUFFER_SIZE / SECTOR_SIZE) as u32, + max_segments: 1, + max_segment_size: VIRTIO_BLK_DMA_BUFFER_SIZE, + supported_flags: rdif_block::RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + } } - fn enable_irq(&mut self) { - self.irq_enabled = true; + fn create_queue(&mut self) -> Option> { + if self.queue_created { + return None; + } + self.dev.as_ref().map(|dev| { + self.queue_created = true; + alloc::boxed::Box::new(BlockQueue { + id: 0, + raw: dev.clone(), + }) as _ + }) } - fn disable_irq(&mut self) { - self.irq_enabled = false; + fn enable_irq(&self) { + self.disable_irq(); } - fn is_irq_enabled(&self) -> bool { - self.irq_enabled + fn disable_irq(&self) { + if let Some(dev) = &self.dev { + dev.with_mut(|dev| dev.raw.disable_interrupts()); + } } - fn handle_irq(&mut self) -> rd_block::Event { - rd_block::Event::none() + fn is_irq_enabled(&self) -> bool { + false } } struct BlockQueue { - raw: VirtIoBlkDevice, + id: usize, + raw: SharedDriver>, } -impl rd_block::IQueue for BlockQueue { +// SAFETY: virtio-blk operations are submitted to the underlying synchronous +// driver and completed before `submit_request` returns. No request segment is +// retained after the call. +unsafe impl rdif_block::IQueue for BlockQueue { fn id(&self) -> usize { - 0 - } - - fn num_blocks(&self) -> usize { - self.raw.raw.capacity() as _ + self.id } - fn block_size(&self) -> usize { - SECTOR_SIZE - } - - fn buff_config(&self) -> rd_block::BuffConfig { - rd_block::BuffConfig { - dma_mask: u64::MAX, - align: 0x1000, - size: VIRTIO_BLK_DMA_BUFFER_SIZE, + fn info(&self) -> rdif_block::QueueInfo { + let blocks = self.raw.with_mut(|raw| raw.raw.capacity()); + rdif_block::QueueInfo { + id: self.id, + device: rdif_block::DeviceInfo { + name: Some("virtio-blk"), + ..rdif_block::DeviceInfo::new(blocks, SECTOR_SIZE) + }, + limits: rdif_block::QueueLimits { + dma_mask: u64::MAX, + dma_alignment: 0x1000, + max_blocks_per_request: (VIRTIO_BLK_DMA_BUFFER_SIZE / SECTOR_SIZE) as u32, + max_segments: 1, + max_segment_size: VIRTIO_BLK_DMA_BUFFER_SIZE, + supported_flags: rdif_block::RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + }, } } fn submit_request( &mut self, - request: rd_block::Request<'_>, - ) -> Result { - match request.kind { - rd_block::RequestKind::Read(mut buffer) => { + request: rdif_block::Request<'_>, + ) -> Result { + rdif_block::validate_request(self.info(), &request)?; + match request.op { + rdif_block::RequestOp::Read => { + let mut segment = request + .segments + .first() + .copied() + .ok_or(rdif_block::BlkError::InvalidRequest)?; self.raw - .raw - .read_blocks(request.block_id, &mut buffer) + .with_mut(|raw| raw.raw.read_blocks(request.lba as usize, &mut segment)) .map_err(map_virtio_err_to_blk_err)?; } - rd_block::RequestKind::Write(items) => { + rdif_block::RequestOp::Write => { + let segment = request + .segments + .first() + .ok_or(rdif_block::BlkError::InvalidRequest)?; self.raw - .raw - .write_blocks(request.block_id, items) + .with_mut(|raw| raw.raw.write_blocks(request.lba as usize, segment)) .map_err(map_virtio_err_to_blk_err)?; } + rdif_block::RequestOp::Flush + | rdif_block::RequestOp::Discard + | rdif_block::RequestOp::WriteZeroes => return Err(rdif_block::BlkError::NotSupported), } - Ok(rd_block::RequestId::new(0)) + Ok(rdif_block::RequestId::new(0)) } - fn poll_request(&mut self, _request: rd_block::RequestId) -> Result<(), rd_block::BlkError> { - Ok(()) + fn poll_request( + &mut self, + _request: rdif_block::RequestId, + ) -> Result { + Ok(rdif_block::RequestStatus::Complete) } } -fn map_virtio_err_to_blk_err(err: VirtIoError) -> rd_block::BlkError { +fn map_virtio_err_to_blk_err(err: VirtIoError) -> rdif_block::BlkError { match err { - VirtIoError::QueueFull | VirtIoError::NotReady => rd_block::BlkError::Retry, + VirtIoError::QueueFull | VirtIoError::NotReady => rdif_block::BlkError::Retry, VirtIoError::WrongToken | VirtIoError::ConfigSpaceTooSmall - | VirtIoError::ConfigSpaceMissing => rd_block::BlkError::Other("bad internal state".into()), - VirtIoError::AlreadyUsed => rd_block::BlkError::Other("already exists".into()), - VirtIoError::InvalidParam => rd_block::BlkError::Other("invalid parameter".into()), - VirtIoError::DmaError => rd_block::BlkError::NoMemory, - VirtIoError::IoError => rd_block::BlkError::Other("I/O error".into()), - VirtIoError::Unsupported => rd_block::BlkError::NotSupported, - VirtIoError::SocketDeviceError(_) => rd_block::BlkError::Other("socket error".into()), + | VirtIoError::ConfigSpaceMissing => rdif_block::BlkError::Other("bad internal state"), + VirtIoError::AlreadyUsed => rdif_block::BlkError::Other("already exists"), + VirtIoError::InvalidParam => rdif_block::BlkError::InvalidRequest, + VirtIoError::DmaError => rdif_block::BlkError::NoMemory, + VirtIoError::IoError => rdif_block::BlkError::Io, + VirtIoError::Unsupported => rdif_block::BlkError::NotSupported, + VirtIoError::SocketDeviceError(_) => rdif_block::BlkError::Other("socket error"), } } diff --git a/drivers/blk/nvme-driver/Cargo.toml b/drivers/blk/nvme-driver/Cargo.toml index bd5c4657b2..c237a4f42b 100644 --- a/drivers/blk/nvme-driver/Cargo.toml +++ b/drivers/blk/nvme-driver/Cargo.toml @@ -14,7 +14,7 @@ version = "0.4.2" dma-api.workspace = true log = "0.4" mmio-api.workspace = true -rd-block.workspace = true +rdif-block.workspace = true spin.workspace = true tock-registers = "0.10" diff --git a/drivers/blk/nvme-driver/README.md b/drivers/blk/nvme-driver/README.md index 6edaa5824b..f2be799963 100644 --- a/drivers/blk/nvme-driver/README.md +++ b/drivers/blk/nvme-driver/README.md @@ -1,26 +1,42 @@ -# NVME Driver +# NVMe Driver -nvme driver 1.4 +Portable NVMe 1.4 block driver for the `rdif-block` capability boundary. -## example run +## RDIF Submit/Poll Model -install qemu. +The RDIF data path is queue-local and non-blocking: -```shell -cargo install ostool -./img.sh +- `submit_request()` validates the LBA request, allocates a queue-local CID, builds PRP entries, writes one SQE, rings the submission doorbell, and returns `RequestId`. +- `poll_request()` drains CQEs without spinning, updates the matching CID slot, rings the completion doorbell, and reports `Pending` or `Complete`. +- `RequestId` is the NVMe CID for the same IO queue. It must not be used on another queue. +- Queue-full or CID exhaustion is reported as `BlkError::Retry`; incomplete commands are reported as `RequestStatus::Pending`. -# run test with qemu -cargo test --test tests -- --show-output -``` +Controller/admin initialization still uses the driver's internal admin queue flow. The public block data path does not call synchronous read/write helpers and does not spin for IO completion inside `submit_request()`. + +## Queues, PRP, And CID + +Each RDIF queue owns one hardware IO queue pair: SQ, CQ, CID slots, PRP list pages, and doorbell access. Request address fields are device-native `lba` and `block_count`; Linux-style 512-byte sector translation belongs to OS glue above `rdif-block`. + +Read and write requests use NVMe PRP: + +- `prp1` points at the first DMA page fragment. +- `prp2` is either the second page or a PRP-list page. +- The current implementation supports one PRP-list page per request. -## hardware test +Flush maps to NVMe NVM Flush. Discard and write-zeroes are reported as unsupported until the command set implementation grows those operations. -1. 主机连接开发板串口 -2. 开发板插入网线,并且主机与开发板应处于同一网段 -3. 准备开发板设备树文件 `*.dtb` +## IRQ Sources + +`rdif-block` supports multiple IRQ sources via `Interface::irq_sources()` and `take_irq_handler(source_id)`. The NVMe driver exposes legacy source `0`; its event mask covers all created IO queues. This keeps today's INTx/legacy flow working while leaving room for future MSI-X source-per-vector wiring. + +The IRQ handler only returns queue events. It does not complete requests, wake tasks, or take OS locks. Runtime/OS glue polls the indicated queues after receiving an event. + +## QEMU Smoke Test + +The StarryOS NVMe rootfs test boots with an NVMe disk and installs curl inside the guest: ```shell -cargo install ostool -cargo test --test tests -- --show-output --uboot +cargo xtask starry test qemu --arch x86_64 -c nvme-rootfs-apk-curl ``` + +The same case is defined for `aarch64`, `riscv64`, and `loongarch64`. diff --git a/drivers/blk/nvme-driver/qemu.toml b/drivers/blk/nvme-driver/qemu.toml deleted file mode 100644 index 262ef0ad62..0000000000 --- a/drivers/blk/nvme-driver/qemu.toml +++ /dev/null @@ -1,15 +0,0 @@ -args = [ - "-nographic", - "-cpu", - "cortex-a53", - "-machine", - "virt,gic-version=3,virtualization=on", - "-drive", - "file=../../../target/nvme.img,format=raw,if=none,id=nvm", - "-device", - "nvme,serial=deadbeef,drive=nvm", -] -uefi = false -to_bin = true -success_regex = ["All tests passed"] -fail_regex = ["Panicked:", "thread 'main' has overflowed its stack"] diff --git a/drivers/blk/nvme-driver/src/block.rs b/drivers/blk/nvme-driver/src/block.rs index 5288bbf404..024411a460 100644 --- a/drivers/blk/nvme-driver/src/block.rs +++ b/drivers/blk/nvme-driver/src/block.rs @@ -1,61 +1,134 @@ -use alloc::{boxed::Box, collections::BTreeSet, sync::Arc}; -use core::any::Any; +use alloc::{boxed::Box, sync::Arc, vec, vec::Vec}; +use core::{ + any::Any, + cell::UnsafeCell, + sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}, +}; + +use dma_api::CoherentArray; +use rdif_block::{ + BlkError, DeviceInfo, DriverGeneric, Event, IQueue, IdList, Interface, IrqHandler, + IrqSourceInfo, IrqSourceList, QueueInfo, QueueLimits, Request, RequestFlags, RequestId, + RequestOp, RequestStatus, validate_request, +}; -use rd_block::{ - BlkError, Block as RdBlock, BuffConfig, DriverGeneric, Event, IQueue, IdList, Interface, - Request, RequestId, RequestKind, +use crate::{ + Namespace, Nvme, + err::{Error as NvmeError, Result as NvmeResult}, + queue::{CommandSet, NvmeQueue as HardwareQueue}, }; -use spin::Mutex; -use crate::{Namespace, Nvme, err::Result}; +const MAX_PRP_LIST_PAGES: usize = 1; +const DEFAULT_QUEUE_DEPTH: usize = 64; struct NvmeBlockInner { nvme: Nvme, namespace: Namespace, - irq_enabled: bool, - pending_irq: IdList, - completed: BTreeSet, - next_request_id: usize, - next_queue_id: usize, } pub struct NvmeBlockDriver { name: &'static str, - inner: Arc>, + inner: Arc, + queue_depth: usize, + irq_handler_taken: bool, +} + +struct NvmeBlockOwner { + inner: UnsafeCell, + next_queue_id: AtomicUsize, + irq_enabled: AtomicBool, + pending_irq: AtomicU64, + created_queues: AtomicU64, } impl NvmeBlockDriver { - pub fn from_nvme(mut nvme: Nvme) -> Result { + pub fn from_nvme(mut nvme: Nvme) -> NvmeResult { let namespace = nvme .namespace_list()? .into_iter() .next() - .ok_or(crate::err::Error::Unknown("no active namespace found"))?; + .ok_or(NvmeError::Unknown("no active namespace found"))?; Ok(Self::with_namespace("nvme", nvme, namespace)) } + pub fn from_nvme_with_queue_depth(mut nvme: Nvme, queue_depth: usize) -> NvmeResult { + let namespace = nvme + .namespace_list()? + .into_iter() + .next() + .ok_or(NvmeError::Unknown("no active namespace found"))?; + + Ok(Self::with_namespace_and_queue_depth( + "nvme", + nvme, + namespace, + queue_depth, + )) + } + pub fn with_namespace(name: &'static str, nvme: Nvme, namespace: Namespace) -> Self { + Self::with_namespace_and_queue_depth(name, nvme, namespace, DEFAULT_QUEUE_DEPTH) + } + + pub fn with_namespace_and_queue_depth( + name: &'static str, + nvme: Nvme, + namespace: Namespace, + queue_depth: usize, + ) -> Self { Self { name, - inner: Arc::new(Mutex::new(NvmeBlockInner { - nvme, - namespace, - irq_enabled: true, - pending_irq: IdList::none(), - completed: BTreeSet::new(), - next_request_id: 1, - next_queue_id: 0, - })), + inner: Arc::new(NvmeBlockOwner { + inner: UnsafeCell::new(NvmeBlockInner { nvme, namespace }), + next_queue_id: AtomicUsize::new(0), + irq_enabled: AtomicBool::new(true), + pending_irq: AtomicU64::new(0), + created_queues: AtomicU64::new(0), + }), + queue_depth: queue_depth.max(1), + irq_handler_taken: false, } } pub fn namespace(&self) -> Namespace { - self.inner.lock().namespace + self.inner.with_mut(|inner| inner.namespace) + } + + pub fn into_interface(self) -> Self { + self + } + + fn device_info_for(&self) -> DeviceInfo { + self.inner + .with_mut(|inner| device_info(self.name, inner.namespace)) + } + + fn limits_for(&self) -> QueueLimits { + self.inner.with_mut(|inner| { + limits( + inner.nvme.dma_mask(), + inner.nvme.page_size(), + inner.namespace, + ) + }) } +} + +// SAFETY: RDIF queue ownership removes task-side sharing of an IO queue. The +// exported IRQ handler only touches atomics and never borrows the controller. +// The owner keeps the controller and MMIO mapping alive until all queues and +// handlers are dropped. +unsafe impl Send for NvmeBlockOwner {} - pub fn into_block(self, dma_op: &'static dyn dma_api::DmaOp) -> RdBlock { - RdBlock::new(self, dma_op) +// SAFETY: Mutable controller access is scoped through `with_mut` during queue +// creation and namespace queries. Runtime IRQ callbacks use only atomics. +unsafe impl Sync for NvmeBlockOwner {} + +impl NvmeBlockOwner { + fn with_mut(&self, f: impl FnOnce(&mut NvmeBlockInner) -> R) -> R { + let inner = unsafe { &mut *self.inner.get() }; + f(inner) } } @@ -74,121 +147,375 @@ impl DriverGeneric for NvmeBlockDriver { } impl Interface for NvmeBlockDriver { + fn device_info(&self) -> DeviceInfo { + self.device_info_for() + } + + fn queue_limits(&self) -> QueueLimits { + self.limits_for() + } + fn create_queue(&mut self) -> Option> { - let mut inner = self.inner.lock(); - let queue_id = inner.next_queue_id; - inner.next_queue_id += 1; + let id = self.inner.next_queue_id.fetch_add(1, Ordering::Relaxed); + if id >= u64::BITS as usize { + return None; + } - Some(Box::new(NvmeRequestQueue { - id: queue_id, - inner: self.inner.clone(), - })) + let queue = self.inner.with_mut(|inner| { + let queue = inner.nvme.take_io_queue(id)?; + let depth = self.queue_depth.min(queue.depth().saturating_sub(1).max(1)); + let prp_lists = alloc_prp_lists(&inner.nvme, depth).ok()?; + Some(NvmeBlockQueue::new( + id, + depth, + self.name, + inner.namespace, + inner.nvme.dma_mask(), + inner.nvme.page_size(), + queue, + prp_lists, + Arc::clone(&self.inner), + )) + })?; + + self.inner + .created_queues + .fetch_or(1 << id, Ordering::AcqRel); + Some(Box::new(queue)) } - fn enable_irq(&mut self) { - self.inner.lock().irq_enabled = true; + fn enable_irq(&self) { + self.inner.irq_enabled.store(true, Ordering::Release); } - fn disable_irq(&mut self) { - self.inner.lock().irq_enabled = false; + fn disable_irq(&self) { + self.inner.irq_enabled.store(false, Ordering::Release); } fn is_irq_enabled(&self) -> bool { - self.inner.lock().irq_enabled + self.inner.irq_enabled.load(Ordering::Acquire) } - fn handle_irq(&mut self) -> Event { - let mut inner = self.inner.lock(); - if !inner.irq_enabled { - return Event::none(); + fn irq_sources(&self) -> IrqSourceList { + let queues = IdList::from_bits(self.inner.created_queues.load(Ordering::Acquire)); + vec![IrqSourceInfo::legacy(queues)] + } + + fn take_irq_handler(&mut self, source_id: usize) -> Option> { + if source_id != 0 || self.irq_handler_taken { + return None; } + self.irq_handler_taken = true; + Some(Box::new(NvmeIrqHandler { + inner: Arc::clone(&self.inner), + })) + } +} + +struct NvmeIrqHandler { + inner: Arc, +} - let mut event = Event::none(); - core::mem::swap(&mut event.queue, &mut inner.pending_irq); - event +impl IrqHandler for NvmeIrqHandler { + fn handle_irq(&self) -> Event { + if !self.inner.irq_enabled.load(Ordering::Acquire) { + return Event::none(); + } + let pending = self.inner.pending_irq.swap(0, Ordering::AcqRel); + if pending == 0 { + Event::from_queue_bits(self.inner.created_queues.load(Ordering::Acquire)) + } else { + Event::from_queue_bits(pending) + } } } -struct NvmeRequestQueue { +struct NvmeBlockQueue { id: usize, - inner: Arc>, + name: &'static str, + namespace: Namespace, + dma_mask: u64, + page_size: usize, + queue: HardwareQueue, + slots: Vec, + free_cids: Vec, + free_prp_lists: Vec>, + owner: Arc, } -impl IQueue for NvmeRequestQueue { - fn id(&self) -> usize { - self.id +struct RequestSlot { + state: SlotState, + prp_list: Option>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SlotState { + Free, + Pending, + Complete, + Failed, +} + +struct PrpMapping { + prp1: u64, + prp2: u64, + prp_list: Option>, +} + +impl NvmeBlockQueue { + #[allow(clippy::too_many_arguments)] + fn new( + id: usize, + depth: usize, + name: &'static str, + namespace: Namespace, + dma_mask: u64, + page_size: usize, + queue: HardwareQueue, + prp_lists: Vec>, + owner: Arc, + ) -> Self { + let mut slots = Vec::with_capacity(depth + 1); + slots.resize_with(depth + 1, || RequestSlot { + state: SlotState::Free, + prp_list: None, + }); + let free_cids = (1..=depth).rev().collect(); + + Self { + id, + name, + namespace, + dma_mask, + page_size, + queue, + slots, + free_cids, + free_prp_lists: prp_lists, + owner, + } } - fn num_blocks(&self) -> usize { - self.inner.lock().namespace.lba_count + fn queue_info(&self) -> QueueInfo { + QueueInfo { + id: self.id, + device: device_info(self.name, self.namespace), + limits: limits(self.dma_mask, self.page_size, self.namespace), + } } - fn block_size(&self) -> usize { - self.inner.lock().namespace.lba_size + fn alloc_cid(&mut self) -> Result { + self.free_cids.pop().ok_or(BlkError::Retry) } - fn buff_config(&self) -> BuffConfig { - let inner = self.inner.lock(); - BuffConfig { - dma_mask: inner.nvme.dma_mask(), - align: inner.namespace.lba_size, - size: inner.namespace.lba_size, + fn free_cid(&mut self, cid: usize) { + if cid < self.slots.len() { + if let Some(prp_list) = self.slots[cid].prp_list.take() { + self.free_prp_lists.push(prp_list); + } + self.slots[cid].state = SlotState::Free; + self.free_cids.push(cid); } } - fn submit_request( - &mut self, - request: Request<'_>, - ) -> core::result::Result { - let mut inner = self.inner.lock(); - let namespace = inner.namespace; + fn build_command(&mut self, cid: usize, request: &Request<'_>) -> Result { + let cid = u16::try_from(cid).map_err(|_| BlkError::InvalidRequest)?; + match request.op { + RequestOp::Read | RequestOp::Write => { + let prp = self.build_prp_mapping(request)?; + let command = match request.op { + RequestOp::Read => CommandSet::nvm_cmd_read_with_cid( + self.namespace.id, + prp.prp1, + prp.prp2, + request.lba, + request.block_count, + cid, + ), + RequestOp::Write => CommandSet::nvm_cmd_write_with_cid( + self.namespace.id, + prp.prp1, + prp.prp2, + request.lba, + request.block_count, + cid, + ), + _ => unreachable!(), + }; + self.slots[usize::from(cid)].prp_list = prp.prp_list; + Ok(command) + } + RequestOp::Flush => Ok(CommandSet::nvm_cmd_flush_with_cid(self.namespace.id, cid)), + RequestOp::Discard | RequestOp::WriteZeroes => Err(BlkError::NotSupported), + } + } - if request.block_id >= namespace.lba_count { - return Err(BlkError::InvalidBlockIndex(request.block_id)); + fn build_prp_mapping(&mut self, request: &Request<'_>) -> Result { + let mut pages = Vec::new(); + for segment in request.segments.iter() { + push_prp_pages(&mut pages, segment.bus, segment.len, self.page_size)?; } + let prp1 = *pages.first().ok_or(BlkError::InvalidRequest)?; + let prp2 = match pages.len() { + 1 => 0, + 2 => pages[1], + _ => { + let list_entries = self.page_size / core::mem::size_of::(); + if pages.len() - 1 > list_entries * MAX_PRP_LIST_PAGES { + return Err(BlkError::InvalidRequest); + } + let mut list = self.free_prp_lists.pop().ok_or(BlkError::Retry)?; + for entry in 0..list_entries { + list.set(entry, 0); + } + for (entry, addr) in pages[1..].iter().copied().enumerate() { + list.set(entry, addr); + } + let addr = list.dma_addr().as_u64(); + return Ok(PrpMapping { + prp1, + prp2: addr, + prp_list: Some(list), + }); + } + }; + Ok(PrpMapping { + prp1, + prp2, + prp_list: None, + }) + } - let req_id = RequestId::new(inner.next_request_id); - inner.next_request_id += 1; + fn drain_completions(&mut self) { + while let Some(completion) = self.queue.poll_completion() { + let cid = usize::from(completion.command_id); + if let Some(slot) = self.slots.get_mut(cid) { + slot.state = if completion.status.is_success() { + SlotState::Complete + } else { + SlotState::Failed + }; + } + } + } - match request.kind { - RequestKind::Read(buffer) => { - if buffer.size < namespace.lba_size { - return Err(BlkError::NotSupported); - } + fn insert_pending_irq(&self) { + if self.owner.irq_enabled.load(Ordering::Acquire) && self.id < u64::BITS as usize { + self.owner + .pending_irq + .fetch_or(1 << self.id, Ordering::AcqRel); + } + } +} + +// SAFETY: NVMe queues may access submitted request DMA segments until the +// matching completion is reclaimed by `poll_request`. Slots are freed only +// after completion/error, and no segment pointers are accessed after that. +unsafe impl IQueue for NvmeBlockQueue { + fn id(&self) -> usize { + self.id + } + + fn info(&self) -> QueueInfo { + self.queue_info() + } - let slice = - unsafe { core::slice::from_raw_parts_mut(buffer.virt, namespace.lba_size) }; - inner - .nvme - .block_read_sync(&namespace, request.block_id as u64, slice) - .map_err(|err| BlkError::Other(Box::new(err)))?; + fn submit_request(&mut self, request: Request<'_>) -> Result { + let info = self.queue_info(); + validate_request(info, &request)?; + self.drain_completions(); + + let cid = self.alloc_cid()?; + let command = match self.build_command(cid, &request) { + Ok(command) => command, + Err(err) => { + self.free_cid(cid); + return Err(err); } - RequestKind::Write(buffer) => { - if buffer.len() != namespace.lba_size { - return Err(BlkError::NotSupported); - } + }; + self.slots[cid].state = SlotState::Pending; + self.queue.submit_io_data(command); + self.insert_pending_irq(); + Ok(RequestId::new(cid)) + } - inner - .nvme - .block_write_sync(&namespace, request.block_id as u64, buffer) - .map_err(|err| BlkError::Other(Box::new(err)))?; + fn poll_request(&mut self, request: RequestId) -> Result { + self.drain_completions(); + + let cid = usize::from(request); + match self.slots.get(cid).map(|slot| slot.state) { + Some(SlotState::Pending) => Ok(RequestStatus::Pending), + Some(SlotState::Complete) => { + self.free_cid(cid); + Ok(RequestStatus::Complete) + } + Some(SlotState::Failed) => { + self.free_cid(cid); + Err(BlkError::Io) } + Some(SlotState::Free) | None => Err(BlkError::InvalidRequest), } + } +} - inner.completed.insert(req_id); - if inner.irq_enabled { - inner.pending_irq.insert(self.id); - } +fn alloc_prp_lists(nvme: &Nvme, depth: usize) -> NvmeResult>> { + let mut lists = Vec::with_capacity(depth); + for _ in 0..depth { + lists.push(nvme.alloc_prp_list()?); + } + Ok(lists) +} - Ok(req_id) +fn push_prp_pages( + pages: &mut Vec, + mut addr: u64, + mut len: usize, + page_size: usize, +) -> Result<(), BlkError> { + if page_size == 0 || len == 0 { + return Err(BlkError::InvalidRequest); } - fn poll_request(&mut self, request: RequestId) -> core::result::Result<(), BlkError> { - let mut inner = self.inner.lock(); - if inner.completed.remove(&request) { - Ok(()) - } else { - Err(BlkError::Retry) + while len > 0 { + pages.push(addr); + let offset = addr as usize % page_size; + let chunk = page_size.saturating_sub(offset).min(len); + if chunk == 0 { + return Err(BlkError::InvalidRequest); } + addr = addr + .checked_add(chunk as u64) + .ok_or(BlkError::InvalidRequest)?; + len -= chunk; + } + Ok(()) +} + +fn device_info(name: &'static str, namespace: Namespace) -> DeviceInfo { + DeviceInfo { + name: Some(name), + model: Some("nvme"), + ..DeviceInfo::new(namespace.lba_count as u64, namespace.lba_size) + } +} + +fn limits(dma_mask: u64, page_size: usize, namespace: Namespace) -> QueueLimits { + let prp_entries = page_size / core::mem::size_of::(); + let max_bytes = page_size.saturating_mul(prp_entries + 1); + let max_blocks = max_bytes + .checked_div(namespace.lba_size.max(1)) + .unwrap_or(1) + .max(1) + .min(u16::MAX as usize + 1) as u32; + QueueLimits { + dma_mask, + dma_alignment: namespace.lba_size.max(1), + max_blocks_per_request: max_blocks, + max_segments: prp_entries + 1, + max_segment_size: max_bytes, + supported_flags: RequestFlags::NONE, + supports_flush: true, + supports_discard: false, + supports_write_zeroes: false, } } diff --git a/drivers/blk/nvme-driver/src/nvme.rs b/drivers/blk/nvme-driver/src/nvme.rs index 2167979dce..079197603a 100644 --- a/drivers/blk/nvme-driver/src/nvme.rs +++ b/drivers/blk/nvme-driver/src/nvme.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; use core::ptr::NonNull; -use dma_api::{DeviceDma, DmaDirection, DmaOp}; +use dma_api::{CoherentArray, DeviceDma, DmaDirection, DmaOp}; use log::{debug, info}; use mmio_api::{Mmio, MmioAddr, MmioOp}; @@ -20,10 +20,11 @@ pub struct Nvme { _mmio: Option, dma: DeviceDma, admin_queue: NvmeQueue, - io_queues: Vec, + io_queues: Vec>, num_ns: usize, sqes: u32, cqes: u32, + page_size: usize, } #[derive(Debug, Clone, Copy)] @@ -71,6 +72,7 @@ impl Nvme { num_ns: 0, sqes: 6, cqes: 4, + page_size: config.page_size, }; let version = s.version(); @@ -199,7 +201,7 @@ impl Nvme { io_queue.cq.len() as _, io_queue.cq.bus_addr(), true, - false, + true, 0, ); self.admin_queue.command_sync(data)?; @@ -216,12 +218,33 @@ impl Nvme { self.admin_queue.command_sync(data)?; - self.io_queues.push(io_queue); + self.io_queues.push(Some(io_queue)); } Ok(()) } + pub fn io_queue_count(&self) -> usize { + self.io_queues.len() + } + + pub fn page_size(&self) -> usize { + self.page_size + } + + pub(crate) fn take_io_queue(&mut self, index: usize) -> Option { + self.io_queues.get_mut(index)?.take() + } + + pub(crate) fn alloc_prp_list(&self) -> Result> { + self.dma + .coherent_array_zero_with_align( + self.page_size / core::mem::size_of::(), + self.page_size, + ) + .map_err(Into::into) + } + pub fn get_identfy(&mut self, mut want: T) -> Result { let cmd = want.command_set_mut(); @@ -271,7 +294,11 @@ impl Nvme { blk_num as _, ); - self.io_queues[0].command_sync(cmd)?; + self.io_queues + .get_mut(0) + .and_then(Option::as_mut) + .ok_or(Error::Unknown("missing IO queue"))? + .command_sync(cmd)?; Ok(()) } @@ -302,7 +329,11 @@ impl Nvme { blk_num as _, ); - self.io_queues[0].command_sync(cmd)?; + self.io_queues + .get_mut(0) + .and_then(Option::as_mut) + .ok_or(Error::Unknown("missing IO queue"))? + .command_sync(cmd)?; dma_buff.sync_for_cpu_all(); for (index, value) in dma_buff.iter().enumerate() { diff --git a/drivers/blk/nvme-driver/src/queue.rs b/drivers/blk/nvme-driver/src/queue.rs index 00d0c76d16..929f90c508 100644 --- a/drivers/blk/nvme-driver/src/queue.rs +++ b/drivers/blk/nvme-driver/src/queue.rs @@ -76,7 +76,12 @@ pub struct CommandSet { impl CommandSet { pub fn cdw0_from_opcode(opcode: command::Opcode) -> u32 { - (CommandDword0::Opcode.val(opcode.as_u32()) + CommandDword0::CommandId.val(next_id())).value + Self::cdw0_from_opcode_with_cid(opcode, next_id() as u16) + } + + pub fn cdw0_from_opcode_with_cid(opcode: command::Opcode, cid: u16) -> u32 { + (CommandDword0::Opcode.val(opcode.as_u32()) + CommandDword0::CommandId.val(cid as u32)) + .value } pub fn set_features(feature: Feature) -> Self { @@ -146,16 +151,28 @@ impl CommandSet { } } - pub fn nvm_cmd_read(nsid: u32, paddr: u64, starting_lba: u64, blk_num: u16) -> Self { - let cdw0 = Self::cdw0_from_opcode(command::Opcode::NVM_READ); + pub fn nvm_cmd_read(nsid: u32, paddr: u64, starting_lba: u64, blk_num: u32) -> Self { + Self::nvm_cmd_read_with_cid(nsid, paddr, 0, starting_lba, blk_num, next_id() as u16) + } + + pub fn nvm_cmd_read_with_cid( + nsid: u32, + prp1: u64, + prp2: u64, + starting_lba: u64, + block_count: u32, + cid: u16, + ) -> Self { + let cdw0 = Self::cdw0_from_opcode_with_cid(command::Opcode::NVM_READ, cid); let low = (starting_lba & 0xFFFFFFFF) as u32; let high = (starting_lba >> 32) as u32; - let cdw12 = blk_num as u32; + let cdw12 = block_count.saturating_sub(1); CommandSet { nsid, cdw0, - prp1: paddr, + prp1, + prp2, cdw10: low, cdw11: high, cdw12, @@ -163,22 +180,44 @@ impl CommandSet { } } - pub fn nvm_cmd_write(nsid: u32, paddr: u64, starting_lba: u64, blk_num: u16) -> Self { - let cdw0 = Self::cdw0_from_opcode(command::Opcode::NVM_WRITE); + pub fn nvm_cmd_write(nsid: u32, paddr: u64, starting_lba: u64, blk_num: u32) -> Self { + Self::nvm_cmd_write_with_cid(nsid, paddr, 0, starting_lba, blk_num, next_id() as u16) + } + + pub fn nvm_cmd_write_with_cid( + nsid: u32, + prp1: u64, + prp2: u64, + starting_lba: u64, + block_count: u32, + cid: u16, + ) -> Self { + let cdw0 = Self::cdw0_from_opcode_with_cid(command::Opcode::NVM_WRITE, cid); let low = (starting_lba & 0xFFFFFFFF) as u32; let high = (starting_lba >> 32) as u32; - let cdw12 = blk_num as u32; + let cdw12 = block_count.saturating_sub(1); CommandSet { nsid, cdw0, - prp1: paddr, + prp1, + prp2, cdw10: low, cdw11: high, cdw12, ..Default::default() } } + + pub fn nvm_cmd_flush_with_cid(nsid: u32, cid: u16) -> Self { + let cdw0 = Self::cdw0_from_opcode_with_cid(command::Opcode::NVM_FLUSH, cid); + + CommandSet { + nsid, + cdw0, + ..Default::default() + } + } } impl Submission for CommandSet { @@ -189,7 +228,7 @@ impl Submission for CommandSet { #[repr(C)] #[derive(Debug, Copy, Clone, Default)] -struct NvmeCompletion { +pub(crate) struct NvmeCompletion { pub result: u64, pub sq_head: u16, pub sq_id: u16, @@ -199,14 +238,14 @@ struct NvmeCompletion { #[repr(transparent)] #[derive(Debug, Copy, Clone, Default)] -struct CompletionStatus(pub u16); +pub(crate) struct CompletionStatus(pub u16); impl CompletionStatus { pub fn phase(&self) -> bool { self.0 & 1 > 0 } - fn is_success(&self) -> bool { + pub(crate) fn is_success(&self) -> bool { self.0 & (1 << 1) == 0 } @@ -222,6 +261,11 @@ pub struct NvmeQueue { pub reg: NonNull, } +// SAFETY: An `NvmeQueue` is owned by exactly one RDIF queue after creation. +// Moving that owner between threads does not create aliasing; register access +// still happens through `&mut self` queue methods. +unsafe impl Send for NvmeQueue {} + impl NvmeQueue { pub fn new( qid: u32, @@ -251,6 +295,21 @@ impl NvmeQueue { self.reg().write_sq_y_tail_doolbell(self.qid as _, tail); } + pub(crate) fn submit_io_data(&mut self, data: CommandSet) { + self.submit_admin_data(data); + } + + pub(crate) fn poll_completion(&mut self) -> Option { + let complete = self.cq.take_complete()?; + self.reg() + .write_cq_y_head_doolbell(self.qid as _, self.cq.head); + Some(complete) + } + + pub(crate) fn depth(&self) -> usize { + self.sq.len().min(self.cq.len()) + } + pub fn command_sync(&mut self, data: CommandSet) -> Result<()> { self.submit_admin_data(data); let complete = self.cq.spin_for_complete(); @@ -328,21 +387,26 @@ impl CompleteQueue { fn spin_for_complete(&mut self) -> NvmeCompletion { loop { - if let Some(e) = self.complete() { - let next_head = self.head + 1; - if next_head >= self.queue.len() as u32 { - self.head = 0; - self.phase = !self.phase; - } else { - self.head = next_head; - } - + if let Some(e) = self.take_complete() { return e; } spin_loop(); } } + fn take_complete(&mut self) -> Option { + let complete = self.complete()?; + let next_head = self.head + 1; + if next_head >= self.queue.len() as u32 { + self.head = 0; + self.phase = !self.phase; + } else { + self.head = next_head; + } + + Some(complete) + } + pub fn len(&self) -> usize { self.queue.len() } diff --git a/drivers/blk/nvme-driver/tests/test.rs b/drivers/blk/nvme-driver/tests/test.rs index 1fe13fdd6b..0c5215ef8d 100644 --- a/drivers/blk/nvme-driver/tests/test.rs +++ b/drivers/blk/nvme-driver/tests/test.rs @@ -22,6 +22,7 @@ mod tests { CommandRegister, DeviceType, PciMem32, PciMem64, PcieController, PcieGeneric, enumerate_by_controller, }; + use rdif_block::{Interface, Request, RequestFlags, RequestOp, RequestStatus, Segment}; #[test] fn test_framework_boot() { @@ -59,11 +60,11 @@ mod tests { let ns = namespace_list[0]; let mut block = - NvmeBlockDriver::with_namespace("nvme", nvme, ns).into_block(kernel_dma_op()); + NvmeBlockDriver::with_namespace_and_queue_depth("nvme", nvme, ns, 64).into_interface(); let mut queue = block.create_queue().unwrap(); - assert_eq!(queue.block_size(), ns.lba_size); - assert_eq!(queue.num_blocks(), ns.lba_count); + assert_eq!(queue.info().device.logical_block_size, ns.lba_size); + assert_eq!(queue.info().device.num_blocks, ns.lba_count as u64); for block in 0..128 { let mut write_buf = vec![0u8; ns.lba_size]; @@ -72,11 +73,10 @@ mod tests { write_buf[..message_bytes.len()].copy_from_slice(message_bytes); - let write_result = queue.write_blocks_blocking(block, &write_buf); - assert!(write_result.into_iter().all(|entry| entry.is_ok())); + submit(&mut *queue, RequestOp::Write, block, &mut write_buf); - let mut read_result = queue.read_blocks_blocking(block, 1).into_iter(); - let read_buf = read_result.next().unwrap().unwrap(); + let mut read_buf = vec![0u8; ns.lba_size]; + submit(&mut *queue, RequestOp::Read, block, &mut read_buf); assert_eq!(&read_buf[..message_bytes.len()], message_bytes); @@ -88,6 +88,26 @@ mod tests { println!("nvme io ok"); } + fn submit(queue: &mut dyn rdif_block::IQueue, op: RequestOp, lba: usize, data: &mut [u8]) { + let block_size = queue.info().device.logical_block_size; + let segment = unsafe { + Segment::from_raw_parts(data.as_mut_ptr(), data.as_mut_ptr() as u64, data.len()) + }; + let mut segments = [segment]; + let id = queue + .submit_request(Request { + op, + lba: lba as u64, + block_count: (data.len() / block_size) as u32, + segments: &mut segments, + flags: RequestFlags::NONE, + }) + .expect("submit should succeed"); + while queue.poll_request(id).expect("poll should succeed") == RequestStatus::Pending { + core::hint::spin_loop(); + } + } + fn get_nvme() -> Nvme { let PlatformDescriptor::DeviceTree(dtb) = get_platform_descriptor() else { panic!("device tree not found"); diff --git a/drivers/blk/phytium-mci-host/src/lib.rs b/drivers/blk/phytium-mci-host/src/lib.rs index f136a0504e..b5542038bb 100644 --- a/drivers/blk/phytium-mci-host/src/lib.rs +++ b/drivers/blk/phytium-mci-host/src/lib.rs @@ -10,7 +10,7 @@ //! timing tables, 1-bit / 4-bit / 8-bit bus selection, command response //! decoding, FIFO block transfers, and stable IRQ event extraction. //! - **Out of scope for this crate**: FDT/ACPI probe, MMIO remapping, IRQ -//! registration, pad-controller programming, OS sleeps/wakeups, and rd-block +//! registration, pad-controller programming, OS sleeps/wakeups, and rdif-block //! registration. //! - **Implemented for block I/O**: IDMAC descriptor setup, DMA buffer mapping, //! DMA block read/write polling, and FIFO fallback at the platform adapter. diff --git a/drivers/blk/ramdisk/Cargo.toml b/drivers/blk/ramdisk/Cargo.toml index 5cfe7ce6ff..c84a3a9474 100644 --- a/drivers/blk/ramdisk/Cargo.toml +++ b/drivers/blk/ramdisk/Cargo.toml @@ -15,4 +15,3 @@ spin = { workspace = true } [dev-dependencies] dma-api = { workspace = true } -rd-block = { workspace = true } diff --git a/drivers/blk/ramdisk/examples/ramdisk.rs b/drivers/blk/ramdisk/examples/ramdisk.rs index a452c38729..f4c09b99fa 100644 --- a/drivers/blk/ramdisk/examples/ramdisk.rs +++ b/drivers/blk/ramdisk/examples/ramdisk.rs @@ -1,95 +1,40 @@ -use std::{ - alloc::{Layout, alloc_zeroed, dealloc}, - num::NonZeroUsize, - ptr::NonNull, - time::Duration, -}; - use ramdisk::RamDisk; -use rd_block::Block; - -struct ExampleDmaOp; - -impl dma_api::DmaOp for ExampleDmaOp { - fn page_size(&self) -> usize { - 4096 - } - - unsafe fn alloc_contiguous( - &self, - _constraints: dma_api::DmaConstraints, - layout: Layout, - ) -> Option { - let ptr = unsafe { alloc_zeroed(layout) }; - let ptr = NonNull::new(ptr)?; - let dma_addr = (ptr.as_ptr() as usize as u64).into(); - Some(unsafe { dma_api::DmaAllocHandle::new(ptr, dma_addr, layout) }) - } - - unsafe fn dealloc_contiguous(&self, handle: dma_api::DmaAllocHandle) { - unsafe { dealloc(handle.as_ptr().as_ptr(), handle.layout()) } - } - - unsafe fn alloc_coherent( - &self, - _constraints: dma_api::DmaConstraints, - layout: Layout, - ) -> Option { - let ptr = unsafe { alloc_zeroed(layout) }; - let ptr = NonNull::new(ptr)?; - let dma_addr = (ptr.as_ptr() as usize as u64).into(); - Some(unsafe { dma_api::DmaAllocHandle::new(ptr, dma_addr, layout) }) - } - - unsafe fn dealloc_coherent(&self, handle: dma_api::DmaAllocHandle) { - unsafe { dealloc(handle.as_ptr().as_ptr(), handle.layout()) } - } - - unsafe fn map_streaming( - &self, - constraints: dma_api::DmaConstraints, - addr: NonNull, - size: NonZeroUsize, - _direction: dma_api::DmaDirection, - ) -> Result { - let layout = Layout::from_size_align(size.get(), constraints.align.max(1))?; - let dma_addr = (addr.as_ptr() as usize as u64).into(); - Ok(unsafe { dma_api::DmaMapHandle::new(addr, dma_addr, layout, None) }) - } - - unsafe fn unmap_streaming(&self, _handle: dma_api::DmaMapHandle) {} -} - -static DMA_OP: ExampleDmaOp = ExampleDmaOp; +use rdif_block::{Interface, Request, RequestFlags, RequestOp, RequestStatus, Segment}; fn main() { - let mut block = Block::new(RamDisk::new(16, 1024), &DMA_OP); + let mut block = RamDisk::new(16, 1024); let mut queue = block.create_queue().expect("queue must be created"); - let irq = block.irq_handler(); - std::thread::spawn(move || { - loop { - irq.handle(); - std::thread::sleep(Duration::from_millis(10)); - } - }); + let mut read = vec![0; queue.info().device.logical_block_size * 2]; + submit(&mut *queue, RequestOp::Read, 3, &mut read); + println!("read: {:?}", read); - let result = queue.read_blocks_blocking(3, 2); - for block in result { - println!("read: {:?}", block.expect("read should succeed")); - } - - let size = queue.block_size(); + let size = queue.info().device.logical_block_size; let mut data = vec![0xAAu8; size]; data.extend(vec![0xBBu8; size]); - let result = queue.write_blocks_blocking(3, &data); - for write in result { - println!("write: {:?}", write); - } + submit(&mut *queue, RequestOp::Write, 3, &mut data); + + let mut after = vec![0; queue.info().device.logical_block_size * 2]; + submit(&mut *queue, RequestOp::Read, 3, &mut after); + println!("after write: {:?}", after); +} - let result = queue.read_blocks_blocking(3, 2); - for block in result { - println!("after write: {:?}", block.expect("read should succeed")); +fn submit(queue: &mut dyn rdif_block::IQueue, op: RequestOp, lba: u64, data: &mut [u8]) { + let block_size = queue.info().device.logical_block_size; + let segment = + unsafe { Segment::from_raw_parts(data.as_mut_ptr(), data.as_mut_ptr() as u64, data.len()) }; + let mut segments = [segment]; + let id = queue + .submit_request(Request { + op, + lba, + block_count: (data.len() / block_size) as u32, + segments: &mut segments, + flags: RequestFlags::NONE, + }) + .expect("submit should succeed"); + while queue.poll_request(id).expect("poll should succeed") == RequestStatus::Pending { + std::hint::spin_loop(); } } diff --git a/drivers/blk/ramdisk/src/lib.rs b/drivers/blk/ramdisk/src/lib.rs index b27148d4bc..ea59e0d082 100644 --- a/drivers/blk/ramdisk/src/lib.rs +++ b/drivers/blk/ramdisk/src/lib.rs @@ -3,28 +3,36 @@ extern crate alloc; use alloc::{boxed::Box, sync::Arc, vec::Vec}; +use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use rdif_block::{ - BlkError, BuffConfig, DriverGeneric, Event, IQueue, IdList, Interface, Request, RequestId, - RequestKind, + BlkError, DeviceInfo, DriverGeneric, Event, IQueue, IdList, Interface, IrqHandler, + IrqSourceInfo, IrqSourceList, QueueInfo, QueueLimits, Request, RequestId, RequestOp, + RequestStatus, validate_request, }; use spin::Mutex; +const PREFERRED_TRANSFER_SIZE: usize = 16 * 1024; + struct RamInner { storage: Vec, - completed_reads: Vec, - completed_writes: Vec, - irq_rx: IdList, - irq_enabled: bool, + completed: Vec, next_req_id: usize, next_queue_id: usize, } +struct RamIrqState { + enabled: AtomicBool, + handler_taken: AtomicBool, + queues: AtomicU64, +} + pub struct RamDisk { name: &'static str, block_size: usize, num_blocks: usize, inner: Arc>, + irq: Arc, } impl RamDisk { @@ -43,19 +51,22 @@ impl RamDisk { let inner = RamInner { storage, - completed_reads: Vec::new(), - completed_writes: Vec::new(), - irq_rx: IdList::none(), - irq_enabled: true, + completed: Vec::new(), next_req_id: 1, next_queue_id: 0, }; + let irq = RamIrqState { + enabled: AtomicBool::new(true), + handler_taken: AtomicBool::new(false), + queues: AtomicU64::new(0), + }; Self { name, block_size, num_blocks, inner: Arc::new(Mutex::new(inner)), + irq: Arc::new(irq), } } @@ -70,6 +81,28 @@ impl RamDisk { pub fn storage_len(&self) -> usize { self.inner.lock().storage.len() } + + fn device_info_for(&self) -> DeviceInfo { + DeviceInfo { + name: Some(self.name), + ..DeviceInfo::new(self.num_blocks as u64, self.block_size) + } + } + + fn limits_for(&self) -> QueueLimits { + let mut limits = QueueLimits::simple(self.block_size, u64::MAX); + let transfer_size = align_down( + PREFERRED_TRANSFER_SIZE.max(self.block_size), + self.block_size, + ); + limits.max_blocks_per_request = (transfer_size / self.block_size).max(1) as u32; + limits.max_segment_size = transfer_size; + limits + } +} + +fn align_down(value: usize, align: usize) -> usize { + value / align * align } impl DriverGeneric for RamDisk { @@ -87,127 +120,173 @@ impl DriverGeneric for RamDisk { } impl Interface for RamDisk { + fn device_info(&self) -> DeviceInfo { + self.device_info_for() + } + + fn queue_limits(&self) -> QueueLimits { + self.limits_for() + } + fn create_queue(&mut self) -> Option> { let mut guard = self.inner.lock(); let id = guard.next_queue_id; guard.next_queue_id += 1; + if id >= 64 { + return None; + } + Some(Box::new(RamQueue { id, - block_size: self.block_size, - num_blocks: self.num_blocks, + device: self.device_info_for(), + limits: self.limits_for(), inner: Arc::clone(&self.inner), + irq: Arc::clone(&self.irq), })) } - fn enable_irq(&mut self) { - self.inner.lock().irq_enabled = true; + fn enable_irq(&self) { + self.irq.enabled.store(true, Ordering::Release); } - fn disable_irq(&mut self) { - self.inner.lock().irq_enabled = false; + fn disable_irq(&self) { + self.irq.enabled.store(false, Ordering::Release); } fn is_irq_enabled(&self) -> bool { - self.inner.lock().irq_enabled + self.irq.enabled.load(Ordering::Acquire) } - fn handle_irq(&mut self) -> Event { - let mut guard = self.inner.lock(); - if !guard.irq_enabled { + fn irq_sources(&self) -> IrqSourceList { + alloc::vec![IrqSourceInfo::legacy(IdList::from_bits(u64::MAX))] + } + + fn take_irq_handler(&mut self, source_id: usize) -> Option> { + if source_id != 0 { + return None; + } + self.irq + .handler_taken + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .ok()?; + + Some(Box::new(RamIrqHandler { + irq: Arc::clone(&self.irq), + })) + } +} + +struct RamIrqHandler { + irq: Arc, +} + +impl IrqHandler for RamIrqHandler { + fn handle_irq(&self) -> Event { + if !self.irq.enabled.load(Ordering::Acquire) { return Event::none(); } - let mut ev = Event::none(); - core::mem::swap(&mut ev.queue, &mut guard.irq_rx); - ev + Event::from_queue_bits(self.irq.queues.swap(0, Ordering::AcqRel)) } } struct RamQueue { id: usize, - block_size: usize, - num_blocks: usize, + device: DeviceInfo, + limits: QueueLimits, inner: Arc>, + irq: Arc, } -impl IQueue for RamQueue { +// SAFETY: ramdisk copies data synchronously during `submit_request`; it stores +// only the completed request ID and never retains request segment pointers. +unsafe impl IQueue for RamQueue { fn id(&self) -> usize { self.id } - fn num_blocks(&self) -> usize { - self.num_blocks - } - - fn block_size(&self) -> usize { - self.block_size - } - - fn buff_config(&self) -> BuffConfig { - BuffConfig { - dma_mask: !0u64, - align: 1, - size: self.block_size, + fn info(&self) -> QueueInfo { + QueueInfo { + id: self.id, + device: self.device, + limits: self.limits, } } fn submit_request(&mut self, request: Request<'_>) -> Result { - let block_id = request.block_id; - if block_id >= self.num_blocks { - return Err(BlkError::InvalidBlockIndex(block_id)); - } + validate_request(self.info(), &request)?; let mut guard = self.inner.lock(); let req_id = RequestId::new(guard.next_req_id); guard.next_req_id += 1; - let offset = block_id * self.block_size; - match request.kind { - RequestKind::Read(buff) => { - if buff.size < self.block_size { - return Err(BlkError::NotSupported); - } - - unsafe { - core::ptr::copy_nonoverlapping( - guard.storage.as_ptr().add(offset), - buff.virt, - self.block_size, - ); - } - guard.completed_reads.push(req_id); + match request.op { + RequestOp::Read => { + copy_from_storage(&guard.storage, self.device.logical_block_size, &request)?; } - RequestKind::Write(slice) => { - if slice.len() != self.block_size { + RequestOp::Write => { + if self.device.read_only { return Err(BlkError::NotSupported); } - - unsafe { - core::ptr::copy_nonoverlapping( - slice.as_ptr(), - guard.storage.as_mut_ptr().add(offset), - self.block_size, - ); - } - guard.completed_writes.push(req_id); + copy_to_storage(&mut guard.storage, self.device.logical_block_size, &request)?; } + RequestOp::Flush => {} + RequestOp::Discard | RequestOp::WriteZeroes => return Err(BlkError::NotSupported), } - guard.irq_rx.insert(self.id); + guard.completed.push(req_id); + insert_irq_bit(&self.irq.queues, self.id); Ok(req_id) } - fn poll_request(&mut self, request: RequestId) -> Result<(), BlkError> { + fn poll_request(&mut self, request: RequestId) -> Result { let mut guard = self.inner.lock(); - if let Some(pos) = guard.completed_reads.iter().position(|r| *r == request) { - guard.completed_reads.remove(pos); - Ok(()) - } else if let Some(pos) = guard.completed_writes.iter().position(|r| *r == request) { - guard.completed_writes.remove(pos); - Ok(()) + if let Some(pos) = guard.completed.iter().position(|r| *r == request) { + guard.completed.remove(pos); + Ok(RequestStatus::Complete) } else { - Err(BlkError::Retry) + Ok(RequestStatus::Pending) + } + } +} + +fn copy_from_storage( + storage: &[u8], + block_size: usize, + request: &Request<'_>, +) -> Result<(), BlkError> { + let mut offset = request.lba as usize * block_size; + for segment in request.segments.iter() { + unsafe { + core::ptr::copy_nonoverlapping(storage.as_ptr().add(offset), segment.virt, segment.len); + } + offset += segment.len; + } + Ok(()) +} + +fn copy_to_storage( + storage: &mut [u8], + block_size: usize, + request: &Request<'_>, +) -> Result<(), BlkError> { + let mut offset = request.lba as usize * block_size; + for segment in request.segments.iter() { + unsafe { + core::ptr::copy_nonoverlapping( + segment.virt, + storage.as_mut_ptr().add(offset), + segment.len, + ); } + offset += segment.len; + } + Ok(()) +} + +fn insert_irq_bit(bits: &AtomicU64, id: usize) { + if id < u64::BITS as usize { + bits.fetch_or(1 << id, Ordering::AcqRel); } } diff --git a/drivers/blk/rd-block-volume/Cargo.toml b/drivers/blk/rd-block-volume/Cargo.toml deleted file mode 100644 index 5c2c7d2562..0000000000 --- a/drivers/blk/rd-block-volume/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "rd-block-volume" -version = "0.1.0" -edition.workspace = true -authors.workspace = true -license.workspace = true -repository.workspace = true -categories = ["embedded", "no-std"] -keywords = ["os", "driver", "block", "partition"] -description = "no_std block volume and partition scanner for rd-block style adapters." - -[dependencies] diff --git a/drivers/blk/rd-block/CHANGELOG.md b/drivers/blk/rd-block/CHANGELOG.md deleted file mode 100644 index 25412f62db..0000000000 --- a/drivers/blk/rd-block/CHANGELOG.md +++ /dev/null @@ -1,20 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -## [0.1.1](https://github.com/drivercraft/sparreal-os/compare/rd-block-v0.1.0...rd-block-v0.1.1) - 2026-03-05 - -### Other - -- ✨ feat: 简化 rdif_block 的导入方式 ([#38](https://github.com/drivercraft/sparreal-os/pull/38)) - -## [0.1.0](https://github.com/drivercraft/sparreal-os/releases/tag/rd-block-v0.1.0) - 2026-03-05 - -### Other - -- ✨ feat: 重构设备驱动接口,移除 open/close 方法,添加 name 方法 ([#25](https://github.com/drivercraft/sparreal-os/pull/25)) diff --git a/drivers/blk/rd-block/Cargo.toml b/drivers/blk/rd-block/Cargo.toml deleted file mode 100644 index e73382e407..0000000000 --- a/drivers/blk/rd-block/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -authors = ["周睿 "] -categories = ["embedded", "no-std"] -description = "Driver Interface block definition." -edition.workspace = true -keywords = ["os", "driver"] -license = "MIT" -name = "rd-block" -repository.workspace = true -version = "0.1.2" - -[dependencies] -dma-api = {workspace = true} -futures = {version = "0.3", features = ["alloc"], default-features = false} -rdif-block = {workspace = true} -spin_on = "0.1" diff --git a/drivers/blk/rd-block/src/lib.rs b/drivers/blk/rd-block/src/lib.rs deleted file mode 100644 index a1a3601c21..0000000000 --- a/drivers/blk/rd-block/src/lib.rs +++ /dev/null @@ -1,645 +0,0 @@ -#![no_std] - -extern crate alloc; -#[cfg(test)] -extern crate std; - -use alloc::{ - boxed::Box, - collections::{btree_map::BTreeMap, btree_set::BTreeSet}, - sync::Arc, - vec::Vec, -}; -use core::{ - alloc::Layout, - any::Any, - cell::UnsafeCell, - fmt::Debug, - ops::{Deref, DerefMut}, - task::Poll, -}; - -use dma_api::{ContiguousBuffer, ContiguousBufferPool, DeviceDma, DmaDirection, DmaOp}; -use futures::task::AtomicWaker; -pub use rdif_block::*; - -pub struct Block { - inner: Arc, -} - -struct QueueWakerMap(UnsafeCell>>); - -impl QueueWakerMap { - fn new() -> Self { - Self(UnsafeCell::new(BTreeMap::new())) - } - - fn register(&self, queue_id: usize) -> Arc { - let waker = Arc::new(AtomicWaker::new()); - unsafe { &mut *self.0.get() }.insert(queue_id, waker.clone()); - waker - } - - fn wake(&self, queue_id: usize) { - if let Some(waker) = unsafe { &*self.0.get() }.get(&queue_id) { - waker.wake(); - } - } -} - -struct BlockInner { - interface: UnsafeCell>, - dma_op: &'static dyn DmaOp, - queue_waker_map: QueueWakerMap, -} - -unsafe impl Send for BlockInner {} -unsafe impl Sync for BlockInner {} - -struct IrqGuard<'a> { - enabled: bool, - inner: &'a Block, -} - -impl<'a> Drop for IrqGuard<'a> { - fn drop(&mut self) { - if self.enabled { - self.inner.interface().enable_irq(); - } - } -} - -impl DriverGeneric for Block { - fn name(&self) -> &str { - self.interface().name() - } - - fn raw_any(&self) -> Option<&dyn Any> { - Some(self) - } - - fn raw_any_mut(&mut self) -> Option<&mut dyn Any> { - Some(self) - } -} - -impl Block { - pub fn new(interface: impl Interface, dma_op: &'static dyn DmaOp) -> Self { - Self { - inner: Arc::new(BlockInner { - interface: UnsafeCell::new(Box::new(interface)), - dma_op, - queue_waker_map: QueueWakerMap::new(), - }), - } - } - - pub fn typed_ref(&self) -> Option<&T> { - self.interface().raw_any()?.downcast_ref::() - } - - pub fn typed_mut(&mut self) -> Option<&mut T> { - self.interface().raw_any_mut()?.downcast_mut::() - } - - #[allow(clippy::mut_from_ref)] - fn interface(&self) -> &mut dyn Interface { - unsafe { &mut **self.inner.interface.get() } - } - - fn irq_guard(&self) -> IrqGuard<'_> { - let enabled = self.interface().is_irq_enabled(); - if enabled { - self.interface().disable_irq(); - } - IrqGuard { - enabled, - inner: self, - } - } - - pub fn create_queue_with_capacity(&mut self, capacity: usize) -> Option { - let irq_guard = self.irq_guard(); - let queue = self.interface().create_queue()?; - let queue_id = queue.id(); - let config = queue.buff_config(); - let block_size = queue.block_size(); - if block_size == 0 || config.size < block_size { - return None; - } - let layout = Layout::from_size_align(config.size, config.align).ok()?; - let dma = DeviceDma::new(config.dma_mask, self.inner.dma_op); - let pool = dma.contiguous_buffer_pool(layout, DmaDirection::FromDevice, capacity); - let waker = self.inner.queue_waker_map.register(queue_id); - drop(irq_guard); - - Some(CmdQueue::new(queue, waker, pool, config.size)) - } - - pub fn create_queue(&mut self) -> Option { - self.create_queue_with_capacity(32) - } - - pub fn irq_handler(&self) -> IrqHandler { - IrqHandler { - inner: self.inner.clone(), - } - } -} - -pub struct IrqHandler { - inner: Arc, -} - -unsafe impl Sync for IrqHandler {} - -impl IrqHandler { - pub fn handle(&self) { - let iface = unsafe { &mut **self.inner.interface.get() }; - let event = iface.handle_irq(); - for id in event.queue.iter() { - self.inner.queue_waker_map.wake(id); - } - } -} - -pub struct CmdQueue { - interface: Box, - waker: Arc, - pool: ContiguousBufferPool, - buffer_size: usize, -} - -impl CmdQueue { - fn new( - interface: Box, - waker: Arc, - pool: ContiguousBufferPool, - buffer_size: usize, - ) -> Self { - Self { - interface, - waker, - pool, - buffer_size, - } - } - - pub fn id(&self) -> usize { - self.interface.id() - } - - pub fn num_blocks(&self) -> usize { - self.interface.num_blocks() - } - - pub fn block_size(&self) -> usize { - self.interface.block_size() - } - - pub fn max_blocks_per_request(&self) -> usize { - let block_size = self.block_size(); - debug_assert!(block_size > 0); - debug_assert!(self.buffer_size >= block_size); - self.buffer_size / block_size - } - - pub fn read_blocks( - &mut self, - blk_id: usize, - blk_count: usize, - ) -> impl core::future::Future>> { - let block_size = self.block_size(); - let request_ls = block_ranges(blk_id, blk_count, self.max_blocks_per_request(), block_size); - ReadFuture::new(self, request_ls) - } - - pub fn read_blocks_blocking( - &mut self, - blk_id: usize, - blk_count: usize, - ) -> Vec> { - spin_on::spin_on(self.read_blocks(blk_id, blk_count)) - } - - pub async fn write_blocks( - &mut self, - start_blk_id: usize, - data: &[u8], - ) -> Vec> { - let block_size = self.block_size(); - assert_eq!(data.len() % block_size, 0); - let max_blocks = self.max_blocks_per_request(); - let max_bytes = max_blocks * block_size; - let mut block_vecs = Vec::new(); - for (i, chunk) in data.chunks(max_bytes).enumerate() { - let blk_id = start_blk_id + i * max_blocks; - block_vecs.push((blk_id, chunk)); - } - WriteFuture::new(self, block_vecs).await - } - - pub fn write_blocks_blocking( - &mut self, - start_blk_id: usize, - data: &[u8], - ) -> Vec> { - spin_on::spin_on(self.write_blocks(start_blk_id, data)) - } -} - -pub struct BlockData { - block_id: usize, - data: ContiguousBuffer, - len: usize, -} - -pub struct ReadFuture<'a> { - queue: &'a mut CmdQueue, - req_ls: Vec<(usize, usize)>, - requested: BTreeMap>, - map: BTreeMap, - results: BTreeMap>, -} - -impl<'a> ReadFuture<'a> { - fn new(queue: &'a mut CmdQueue, req_ls: Vec<(usize, usize)>) -> Self { - Self { - queue, - req_ls, - requested: BTreeMap::new(), - map: BTreeMap::new(), - results: BTreeMap::new(), - } - } -} - -impl<'a> core::future::Future for ReadFuture<'a> { - type Output = Vec>; - - fn poll( - self: core::pin::Pin<&mut Self>, - cx: &mut core::task::Context<'_>, - ) -> Poll { - let this = self.get_mut(); - - for &(blk_id, len) in &this.req_ls { - if this.results.contains_key(&blk_id) { - continue; - } - - if this.requested.contains_key(&blk_id) { - continue; - } - - match this.queue.pool.alloc() { - Ok(buff) => { - let kind = RequestKind::Read(Buffer { - virt: buff.as_ptr().as_ptr(), - bus: buff.dma_addr().as_u64(), - size: len, - }); - - match this.queue.interface.submit_request(Request { - block_id: blk_id, - kind, - }) { - Ok(req_id) => { - buff.sync_for_device_all(); - this.map.insert(blk_id, req_id); - this.requested.insert(blk_id, Some((buff, len))); - } - Err(BlkError::Retry) => { - this.queue.waker.register(cx.waker()); - return Poll::Pending; - } - Err(e) => { - this.results.insert(blk_id, Err(e)); - } - } - } - Err(e) => { - this.results.insert(blk_id, Err(e.into())); - } - } - } - - for (blk_id, buff) in &mut this.requested { - if this.results.contains_key(blk_id) { - continue; - } - - let req_id = this.map[blk_id]; - - match this.queue.interface.poll_request(req_id) { - Ok(_) => { - let (data, len) = buff - .take() - .expect("DMA read buffer should exist until completion"); - data.sync_for_cpu_all(); - this.results.insert( - *blk_id, - Ok(BlockData { - block_id: *blk_id, - data, - len, - }), - ); - } - Err(BlkError::Retry) => { - this.queue.waker.register(cx.waker()); - return Poll::Pending; - } - Err(e) => { - this.results.insert(*blk_id, Err(e)); - } - } - } - - let mut out = Vec::with_capacity(this.req_ls.len()); - for (blk_id, _) in &this.req_ls { - let result = this - .results - .remove(blk_id) - .expect("all blocks should have completion results"); - out.push(result); - } - Poll::Ready(out) - } -} - -pub struct WriteFuture<'a, 'b> { - queue: &'a mut CmdQueue, - req_ls: Vec<(usize, &'b [u8])>, - requested: BTreeSet, - map: BTreeMap, - results: BTreeMap>, -} - -impl<'a, 'b> WriteFuture<'a, 'b> { - fn new(queue: &'a mut CmdQueue, req_ls: Vec<(usize, &'b [u8])>) -> Self { - Self { - queue, - req_ls, - requested: BTreeSet::new(), - map: BTreeMap::new(), - results: BTreeMap::new(), - } - } -} - -impl<'a, 'b> core::future::Future for WriteFuture<'a, 'b> { - type Output = Vec>; - - fn poll( - self: core::pin::Pin<&mut Self>, - cx: &mut core::task::Context<'_>, - ) -> core::task::Poll { - let this = self.get_mut(); - for &(blk_id, buff) in &this.req_ls { - if this.results.contains_key(&blk_id) { - continue; - } - - if this.requested.contains(&blk_id) { - continue; - } - - match this.queue.interface.submit_request(Request { - block_id: blk_id, - kind: RequestKind::Write(buff), - }) { - Ok(req_id) => { - this.map.insert(blk_id, req_id); - this.requested.insert(blk_id); - } - Err(BlkError::Retry) => { - this.queue.waker.register(cx.waker()); - return Poll::Pending; - } - Err(e) => { - this.results.insert(blk_id, Err(e)); - } - } - } - - for blk_id in &this.requested { - if this.results.contains_key(blk_id) { - continue; - } - - let req_id = this.map[blk_id]; - - match this.queue.interface.poll_request(req_id) { - Ok(_) => { - this.results.insert(*blk_id, Ok(())); - } - Err(BlkError::Retry) => { - this.queue.waker.register(cx.waker()); - return Poll::Pending; - } - Err(e) => { - this.results.insert(*blk_id, Err(e)); - } - } - } - - let mut out = Vec::with_capacity(this.req_ls.len()); - for (blk_id, _) in &this.req_ls { - let result = this - .results - .remove(blk_id) - .expect("all blocks should have completion results"); - out.push(result); - } - Poll::Ready(out) - } -} - -impl Debug for BlockData { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("BlockData") - .field("block_id", &self.block_id) - .field("data", &self.deref()) - .finish() - } -} - -impl BlockData { - pub fn block_id(&self) -> usize { - self.block_id - } -} - -impl Deref for BlockData { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - unsafe { core::slice::from_raw_parts(self.data.as_ptr().as_ptr(), self.len) } - } -} - -impl DerefMut for BlockData { - fn deref_mut(&mut self) -> &mut Self::Target { - unsafe { core::slice::from_raw_parts_mut(self.data.as_ptr().as_ptr(), self.len) } - } -} - -fn block_ranges( - start_blk_id: usize, - block_count: usize, - max_blocks: usize, - block_size: usize, -) -> Vec<(usize, usize)> { - let max_blocks = max_blocks.max(1); - let mut out = Vec::new(); - let mut blk_id = start_blk_id; - let mut remaining = block_count; - while remaining > 0 { - let count = remaining.min(max_blocks); - out.push((blk_id, count * block_size)); - blk_id += count; - remaining -= count; - } - out -} - -#[cfg(test)] -mod tests { - use core::{alloc::Layout, num::NonZeroUsize, ptr::NonNull}; - - use dma_api::{DmaAllocHandle, DmaConstraints, DmaError, DmaMapHandle}; - - use super::*; - - struct TestDma; - - static TEST_DMA: TestDma = TestDma; - - impl DmaOp for TestDma { - fn page_size(&self) -> usize { - 4096 - } - - unsafe fn alloc_contiguous( - &self, - _constraints: DmaConstraints, - layout: Layout, - ) -> Option { - let ptr = unsafe { std::alloc::alloc_zeroed(layout) }; - let ptr = NonNull::new(ptr)?; - Some(unsafe { DmaAllocHandle::new(ptr, (ptr.as_ptr() as u64).into(), layout) }) - } - - unsafe fn dealloc_contiguous(&self, handle: DmaAllocHandle) { - unsafe { std::alloc::dealloc(handle.as_ptr().as_ptr(), handle.layout()) }; - } - - unsafe fn alloc_coherent( - &self, - _constraints: DmaConstraints, - layout: Layout, - ) -> Option { - let ptr = unsafe { std::alloc::alloc_zeroed(layout) }; - let ptr = NonNull::new(ptr)?; - Some(unsafe { DmaAllocHandle::new(ptr, (ptr.as_ptr() as u64).into(), layout) }) - } - - unsafe fn dealloc_coherent(&self, handle: DmaAllocHandle) { - unsafe { std::alloc::dealloc(handle.as_ptr().as_ptr(), handle.layout()) }; - } - - unsafe fn map_streaming( - &self, - constraints: DmaConstraints, - addr: NonNull, - size: NonZeroUsize, - _direction: DmaDirection, - ) -> Result { - let layout = Layout::from_size_align(size.get(), constraints.align.max(1))?; - Ok(unsafe { DmaMapHandle::new(addr, (addr.as_ptr() as u64).into(), layout, None) }) - } - - unsafe fn unmap_streaming(&self, _handle: DmaMapHandle) {} - } - - struct TestBlock { - block_size: usize, - buffer_size: usize, - } - - impl DriverGeneric for TestBlock { - fn name(&self) -> &str { - "test-block" - } - } - - impl Interface for TestBlock { - fn create_queue(&mut self) -> Option> { - Some(Box::new(TestQueue { - block_size: self.block_size, - buffer_size: self.buffer_size, - })) - } - - fn enable_irq(&mut self) {} - - fn disable_irq(&mut self) {} - - fn is_irq_enabled(&self) -> bool { - false - } - - fn handle_irq(&mut self) -> Event { - Event::none() - } - } - - struct TestQueue { - block_size: usize, - buffer_size: usize, - } - - impl IQueue for TestQueue { - fn id(&self) -> usize { - 0 - } - - fn num_blocks(&self) -> usize { - 1 - } - - fn block_size(&self) -> usize { - self.block_size - } - - fn buff_config(&self) -> BuffConfig { - BuffConfig { - dma_mask: u64::MAX, - align: 8, - size: self.buffer_size, - } - } - - fn submit_request(&mut self, _request: Request<'_>) -> Result { - Ok(RequestId::new(0)) - } - - fn poll_request(&mut self, _request: RequestId) -> Result<(), BlkError> { - Ok(()) - } - } - - #[test] - fn create_queue_rejects_buffer_smaller_than_block_size() { - let mut block = Block::new( - TestBlock { - block_size: 512, - buffer_size: 256, - }, - &TEST_DMA, - ); - - assert!(block.create_queue_with_capacity(1).is_none()); - } -} diff --git a/drivers/blk/sdhci-host/src/command.rs b/drivers/blk/sdhci-host/src/command.rs index 43c401e4c2..aeadd17be1 100644 --- a/drivers/blk/sdhci-host/src/command.rs +++ b/drivers/blk/sdhci-host/src/command.rs @@ -165,6 +165,11 @@ impl Sdhci { } } + pub(crate) fn clear_cached_irq_status(&mut self) { + self.irq_pending_normal = 0; + self.irq_pending_error = 0; + } + pub(crate) fn take_data_irq_status(&mut self) -> (u16, u16) { let normal_hw = self.read_u16(REG_NORMAL_INT_STATUS); let error_hw = if normal_hw & NORMAL_INT_ERROR != 0 { @@ -287,6 +292,7 @@ impl Sdhci { self.write_u16(REG_NORMAL_INT_STATUS, NORMAL_INT_CLEAR_ALL); self.write_u16(REG_ERROR_INT_STATUS, ERROR_INT_CLEAR_ALL); + self.clear_cached_irq_status(); if let Some(d) = data { self.configure_data_phase(d.direction, d.block_size, d.block_count, use_dma); @@ -436,10 +442,15 @@ fn read_r2(host: &Sdhci) -> [u8; 16] { #[cfg(test)] mod tests { - use sdmmc_protocol::DataDirection; + use core::ptr::NonNull; + + use sdmmc_protocol::{DataDirection, cmd::cmd17}; use super::*; + #[repr(align(4))] + struct FakeRegs([u8; 0x100]); + #[test] fn multi_block_transfer_mode_leaves_stop_command_to_request_state_machine() { let mode = transfer_mode(DataDirection::Read, 4, false); @@ -470,4 +481,23 @@ mod tests { "transfer completion belongs to the data-complete poll step" ); } + + #[test] + fn new_command_discards_cached_irq_status_from_previous_request() { + let mut regs = FakeRegs([0; 0x100]); + let base = NonNull::new(regs.0.as_mut_ptr()).unwrap(); + let mut host = unsafe { Sdhci::new(base) }; + host.irq_pending_normal = NORMAL_INT_CMD_COMPLETE | NORMAL_INT_XFER_COMPLETE; + host.irq_pending_error = ERROR_INT_DATA_TIMEOUT; + host.pending_data = Some(crate::host::PendingData { + direction: DataDirection::Read, + block_size: 512, + block_count: 1, + }); + + host.submit_command(&cmd17(0)).unwrap(); + + assert_eq!(host.irq_pending_normal, 0); + assert_eq!(host.irq_pending_error, 0); + } } diff --git a/drivers/blk/sdmmc-protocol/src/block.rs b/drivers/blk/sdmmc-protocol/src/block.rs index e2301612a1..5d918ddb31 100644 --- a/drivers/blk/sdmmc-protocol/src/block.rs +++ b/drivers/blk/sdmmc-protocol/src/block.rs @@ -1,6 +1,6 @@ //! Block request state shared by SD/MMC host controller backends. //! -//! The protocol crate intentionally does not know about `rd-block` or any +//! The protocol crate intentionally does not know about a block runtime or any //! executor. These types describe the portable queue contract that host //! drivers expose upward: submit one block transfer, advance it by polling or //! IRQ wakeups, and keep the concrete FIFO/DMA engine visible. diff --git a/drivers/interface/rdif-block/src/error.rs b/drivers/interface/rdif-block/src/error.rs new file mode 100644 index 0000000000..f4320160bb --- /dev/null +++ b/drivers/interface/rdif-block/src/error.rs @@ -0,0 +1,53 @@ +use crate::io; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BlkError { + NotSupported, + Retry, + NoMemory, + InvalidBlockIndex(u64), + InvalidRequest, + Io, + Other(&'static str), +} + +impl core::fmt::Display for BlkError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + BlkError::NotSupported => f.write_str("operation not supported"), + BlkError::Retry => f.write_str("operation should be retried"), + BlkError::NoMemory => f.write_str("insufficient memory"), + BlkError::InvalidBlockIndex(index) => write!(f, "invalid block index: {index}"), + BlkError::InvalidRequest => f.write_str("invalid block request"), + BlkError::Io => f.write_str("block I/O error"), + BlkError::Other(msg) => f.write_str(msg), + } + } +} + +impl core::error::Error for BlkError {} + +impl From for io::ErrorKind { + fn from(value: BlkError) -> Self { + match value { + BlkError::NotSupported => io::ErrorKind::Unsupported, + BlkError::Retry => io::ErrorKind::Interrupted, + BlkError::NoMemory => io::ErrorKind::OutOfMemory, + BlkError::InvalidBlockIndex(_) => io::ErrorKind::NotAvailable, + BlkError::InvalidRequest => io::ErrorKind::InvalidParameter { + name: "block request", + }, + BlkError::Io => io::ErrorKind::Other("block I/O error".into()), + BlkError::Other(msg) => io::ErrorKind::Other(msg.into()), + } + } +} + +impl From for BlkError { + fn from(value: dma_api::DmaError) -> Self { + match value { + dma_api::DmaError::NoMemory => BlkError::NoMemory, + _ => BlkError::Io, + } + } +} diff --git a/drivers/interface/rdif-block/src/info.rs b/drivers/interface/rdif-block/src/info.rs new file mode 100644 index 0000000000..01d382e754 --- /dev/null +++ b/drivers/interface/rdif-block/src/info.rs @@ -0,0 +1,60 @@ +use crate::request::RequestFlags; + +#[derive(Debug, Clone, Copy)] +pub struct DeviceInfo { + pub num_blocks: u64, + pub logical_block_size: usize, + pub read_only: bool, + pub name: Option<&'static str>, + pub vendor: Option<&'static str>, + pub model: Option<&'static str>, +} + +impl DeviceInfo { + pub const fn new(num_blocks: u64, logical_block_size: usize) -> Self { + Self { + num_blocks, + logical_block_size, + read_only: false, + name: None, + vendor: None, + model: None, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct QueueLimits { + pub dma_mask: u64, + pub dma_alignment: usize, + pub max_blocks_per_request: u32, + pub max_segments: usize, + pub max_segment_size: usize, + pub supported_flags: RequestFlags, + pub supports_flush: bool, + pub supports_discard: bool, + pub supports_write_zeroes: bool, +} + +impl QueueLimits { + pub const fn simple(logical_block_size: usize, dma_mask: u64) -> Self { + Self { + dma_mask, + dma_alignment: logical_block_size, + max_blocks_per_request: 1, + max_segments: 1, + max_segment_size: logical_block_size, + supported_flags: RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct QueueInfo { + pub id: usize, + pub device: DeviceInfo, + pub limits: QueueLimits, +} diff --git a/drivers/interface/rdif-block/src/interface.rs b/drivers/interface/rdif-block/src/interface.rs new file mode 100644 index 0000000000..5914df807a --- /dev/null +++ b/drivers/interface/rdif-block/src/interface.rs @@ -0,0 +1,104 @@ +use alloc::{boxed::Box, vec::Vec}; + +use crate::{ + BlkError, DeviceInfo, DriverGeneric, IrqHandler, IrqSourceList, QueueInfo, QueueLimits, + Request, RequestId, RequestStatus, +}; + +pub trait Interface: DriverGeneric { + fn device_info(&self) -> DeviceInfo; + + fn queue_limits(&self) -> QueueLimits; + + fn create_queue(&mut self) -> Option>; + + fn enable_irq(&self) {} + + fn disable_irq(&self) {} + + fn is_irq_enabled(&self) -> bool { + false + } + + fn irq_sources(&self) -> IrqSourceList { + Vec::new() + } + + fn take_irq_handler(&mut self, _source_id: usize) -> Option> { + None + } +} + +/// A request queue for one block device hardware/software queue. +/// +/// # Safety +/// +/// Implementers may access `Request` segments after `submit_request` returns +/// and until the matching `poll_request` returns `RequestStatus::Complete` or +/// an error. They must not access any segment before `submit_request` is called +/// or after completion/error has been reported, and request IDs must not alias +/// two concurrently pending requests in a way that extends this lifetime. +pub unsafe trait IQueue: Send + 'static { + fn id(&self) -> usize; + + fn info(&self) -> QueueInfo; + + fn submit_request(&mut self, request: Request<'_>) -> Result; + + fn poll_request(&mut self, request: RequestId) -> Result; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{DeviceInfo, Event, QueueLimits, RequestOp}; + + struct NoopIrq; + + impl IrqHandler for NoopIrq { + fn handle_irq(&self) -> Event { + let mut event = Event::none(); + event.queues.insert(1); + event + } + } + + struct Queue; + + // SAFETY: This test queue never stores request segments beyond + // `submit_request` and reports completion immediately. + unsafe impl IQueue for Queue { + fn id(&self) -> usize { + 1 + } + + fn info(&self) -> QueueInfo { + QueueInfo { + id: 1, + device: DeviceInfo::new(8, 512), + limits: QueueLimits::simple(512, u64::MAX), + } + } + + fn submit_request(&mut self, request: Request<'_>) -> Result { + assert!(matches!(request.op, RequestOp::Read | RequestOp::Write)); + Ok(RequestId::new(1)) + } + + fn poll_request(&mut self, _request: RequestId) -> Result { + Ok(RequestStatus::Complete) + } + } + + #[test] + fn block_api_uses_unified_queue_and_irq_events() { + fn assert_queue() {} + fn assert_irq_handler() {} + + assert_queue::(); + assert_irq_handler::(); + + let event = NoopIrq.handle_irq(); + assert!(event.queues.contains(1)); + } +} diff --git a/drivers/interface/rdif-block/src/irq.rs b/drivers/interface/rdif-block/src/irq.rs new file mode 100644 index 0000000000..b52ba64aa4 --- /dev/null +++ b/drivers/interface/rdif-block/src/irq.rs @@ -0,0 +1,95 @@ +use alloc::vec::Vec; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct IrqSourceInfo { + pub id: usize, + pub queues: IdList, +} + +impl IrqSourceInfo { + pub const fn new(id: usize, queues: IdList) -> Self { + Self { id, queues } + } + + pub const fn legacy(queues: IdList) -> Self { + Self { id: 0, queues } + } +} + +pub type IrqSourceList = Vec; + +pub trait IrqHandler: Send + Sync + 'static { + fn handle_irq(&self) -> Event; +} + +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct IdList(u64); + +impl IdList { + pub const fn none() -> Self { + Self(0) + } + + pub const fn from_bits(bits: u64) -> Self { + Self(bits) + } + + pub const fn bits(self) -> u64 { + self.0 + } + + pub fn contains(&self, id: usize) -> bool { + id < 64 && (self.0 & (1 << id)) != 0 + } + + pub fn insert(&mut self, id: usize) { + if id < 64 { + self.0 |= 1 << id; + } + } + + pub fn remove(&mut self, id: usize) { + if id < 64 { + self.0 &= !(1 << id); + } + } + + pub fn iter(&self) -> impl Iterator { + (0..64).filter(move |i| self.contains(*i)) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Event { + pub queues: IdList, +} + +impl Event { + pub const fn none() -> Self { + Self { + queues: IdList::none(), + } + } + + pub const fn from_queue_bits(bits: u64) -> Self { + Self { + queues: IdList::from_bits(bits), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn irq_source_lists_queue_masks() { + let mut queues = IdList::none(); + queues.insert(2); + let source = IrqSourceInfo::legacy(queues); + + assert_eq!(source.id, 0); + assert!(source.queues.contains(2)); + } +} diff --git a/drivers/interface/rdif-block/src/lib.rs b/drivers/interface/rdif-block/src/lib.rs index 3dcf3f9681..13c7125590 100644 --- a/drivers/interface/rdif-block/src/lib.rs +++ b/drivers/interface/rdif-block/src/lib.rs @@ -2,261 +2,24 @@ extern crate alloc; -use alloc::boxed::Box; -use core::ops::{Deref, DerefMut}; +mod error; +mod info; +mod interface; +mod irq; +mod planner; +mod request; pub use dma_api; +pub use error::BlkError; +pub use info::{DeviceInfo, QueueInfo, QueueLimits}; +pub use interface::{IQueue, Interface}; +pub use irq::{Event, IdList, IrqHandler, IrqSourceInfo, IrqSourceList}; +pub use planner::{ + TransferChunk, TransferPlan, TransferPlanner, TransferRuntimeCaps, TransferSegment, + TransferSegments, +}; pub use rdif_base::{DriverGeneric, KError, io}; - -/// Configuration for DMA buffer allocation. -/// -/// This structure specifies the requirements for DMA buffers used in -/// block device operations. The configuration ensures that buffers -/// meet the hardware's alignment and addressing constraints. -pub struct BuffConfig { - /// DMA addressing mask for the device. - /// - /// This mask defines the addressable memory range for DMA operations. - /// For example, a 32-bit device would use `0xFFFFFFFF`. - pub dma_mask: u64, - - /// Required alignment for buffer addresses. - /// - /// Buffers must be aligned to this boundary (in bytes) for optimal - /// performance and hardware compatibility. Common values are 512 or 4096. - pub align: usize, - - /// Size of each buffer in bytes. - /// - /// This typically matches the device's block size to ensure efficient - /// data transfer and avoid partial block operations. - pub size: usize, -} - -/// Errors that can occur during block device operations. -/// -/// These errors provide detailed information about what went wrong during -/// block device operations and how the caller should respond. -#[derive(thiserror::Error, Debug)] -pub enum BlkError { - /// The requested operation is not supported by the device. - /// - /// This error occurs when attempting to perform an operation that the - /// hardware or driver does not support. For example, trying to write - /// to a read-only device. - /// - /// **Recovery**: Check device capabilities and use only supported operations. - #[error("Operation not supported")] - NotSupported, - - /// The operation should be retried later. - /// - /// This error indicates that the operation failed due to temporary conditions - /// and should be retried. This commonly occurs when: - /// - The device queue is full - /// - The device is temporarily busy - /// - Resource contention prevents immediate completion - /// - /// **Recovery**: Wait a short time and retry the operation. Consider implementing - /// exponential backoff for repeated retries. - #[error("Operation should be retried")] - Retry, - - /// Insufficient memory to complete the operation. - /// - /// This error occurs when there is not enough memory available to: - /// - Allocate DMA buffers - /// - Create internal data structures - /// - Complete the requested operation - /// - /// **Recovery**: Free unused resources or wait for memory to become available. - /// Consider reducing the number of concurrent operations. - #[error("Insufficient memory")] - NoMemory, - - /// The specified block index is invalid or out of range. - /// - /// This error occurs when: - /// - The block index exceeds the device's capacity - /// - The block index is negative (in languages that allow it) - /// - The block has been marked as bad or unusable - /// - /// **Recovery**: Verify that the block index is within the valid range - /// (0 to `num_blocks() - 1`) and that the block is accessible. - #[error("Invalid block index: {0} (check device capacity and block accessibility)")] - InvalidBlockIndex(usize), - - /// An unspecified error occurred. - /// - /// This error wraps other error types that don't fit into the specific - /// categories above. The wrapped error provides additional context about - /// what went wrong. - /// - /// **Recovery**: Examine the wrapped error for specific recovery instructions. - /// This often indicates a lower-level hardware or system error. - #[error("Other error: {0}")] - Other(Box), -} - -impl From for io::ErrorKind { - fn from(value: BlkError) -> Self { - match value { - BlkError::NotSupported => io::ErrorKind::Unsupported, - BlkError::Retry => io::ErrorKind::Interrupted, - BlkError::NoMemory => io::ErrorKind::OutOfMemory, - BlkError::InvalidBlockIndex(_) => io::ErrorKind::NotAvailable, - BlkError::Other(e) => io::ErrorKind::Other(e), - } - } -} - -impl From for BlkError { - fn from(value: dma_api::DmaError) -> Self { - match value { - dma_api::DmaError::NoMemory => BlkError::NoMemory, - e => BlkError::Other(Box::new(e)), - } - } -} - -/// Operations that require a block storage device driver to implement. -/// -/// This trait defines the core interface that all block device drivers -/// must implement to work with the rdrive framework. It provides methods -/// for queue management, interrupt handling, and device lifecycle operations. -pub trait Interface: DriverGeneric { - fn create_queue(&mut self) -> Option>; - - /// Enable interrupts for the device. - /// - /// After calling this method, the device will generate interrupts - /// for completed operations and other events. - fn enable_irq(&mut self); - - /// Disable interrupts for the device. - /// - /// After calling this method, the device will not generate interrupts. - /// This is useful during critical sections or device shutdown. - fn disable_irq(&mut self); - - /// Check if interrupts are currently enabled. - /// - /// Returns `true` if interrupts are enabled, `false` otherwise. - fn is_irq_enabled(&self) -> bool; - - /// Handles an IRQ from the device, returning an event if applicable. - /// - /// This method should be called from the device's interrupt handler. - /// It processes the interrupt and returns information about which - /// queues have completed operations. - fn handle_irq(&mut self) -> Event; -} - -#[repr(transparent)] -#[derive(Debug, Clone, Copy)] -pub struct IdList(u64); - -impl IdList { - pub const fn none() -> Self { - Self(0) - } - - pub fn contains(&self, id: usize) -> bool { - (self.0 & (1 << id)) != 0 - } - - pub fn insert(&mut self, id: usize) { - self.0 |= 1 << id; - } - - pub fn remove(&mut self, id: usize) { - self.0 &= !(1 << id); - } - - pub fn iter(&self) -> impl Iterator { - (0..64).filter(move |i| self.contains(*i)) - } -} - -#[derive(Debug, Clone, Copy)] -pub struct Event { - /// Bitmask of queue IDs that have events. - pub queue: IdList, -} - -impl Event { - pub const fn none() -> Self { - Self { - queue: IdList::none(), - } - } -} - -#[repr(transparent)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct RequestId(usize); - -impl RequestId { - pub fn new(id: usize) -> Self { - Self(id) - } -} - -impl From for usize { - fn from(value: RequestId) -> Self { - value.0 - } -} - -#[derive(Clone, Copy)] -pub struct Buffer { - pub virt: *mut u8, - pub bus: u64, - pub size: usize, -} - -impl Deref for Buffer { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - unsafe { core::slice::from_raw_parts(self.virt, self.size) } - } -} - -impl DerefMut for Buffer { - fn deref_mut(&mut self) -> &mut Self::Target { - unsafe { core::slice::from_raw_parts_mut(self.virt, self.size) } - } -} - -/// Read queue trait for block devices. -pub trait IQueue: Send + 'static { - /// Get the queue identifier. - fn id(&self) -> usize; - - /// Get the total number of blocks available. - fn num_blocks(&self) -> usize; - - /// Get the size of each block in bytes. - fn block_size(&self) -> usize; - - /// Get the buffer configuration for this queue. - fn buff_config(&self) -> BuffConfig; - - fn submit_request(&mut self, request: Request<'_>) -> Result; - - /// Poll the status of a previously submitted request. - fn poll_request(&mut self, request: RequestId) -> Result<(), BlkError>; -} - -#[derive(Clone)] -pub struct Request<'a> { - pub block_id: usize, - pub kind: RequestKind<'a>, -} - -#[derive(Clone)] -pub enum RequestKind<'a> { - Read(Buffer), - Write(&'a [u8]), -} +pub use request::{ + Buffer, Request, RequestFlags, RequestId, RequestOp, RequestStatus, Segment, validate_request, + validate_request_shape, +}; diff --git a/drivers/interface/rdif-block/src/planner.rs b/drivers/interface/rdif-block/src/planner.rs new file mode 100644 index 0000000000..1a6776d922 --- /dev/null +++ b/drivers/interface/rdif-block/src/planner.rs @@ -0,0 +1,424 @@ +use crate::{BlkError, DeviceInfo, QueueLimits}; + +#[derive(Debug, Clone, Copy)] +pub struct TransferRuntimeCaps { + pub max_transfer_bytes: usize, + pub max_segments: usize, +} + +impl TransferRuntimeCaps { + pub const fn new(max_transfer_bytes: usize, max_segments: usize) -> Self { + Self { + max_transfer_bytes, + max_segments, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TransferSegment { + /// Segment byte offset relative to the containing transfer chunk. + pub byte_offset: usize, + pub byte_len: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TransferChunk { + pub lba: u64, + pub block_count: u32, + pub byte_offset: usize, + pub byte_len: usize, + max_segment_size: usize, +} + +impl TransferChunk { + pub fn segments(self) -> TransferSegments { + TransferSegments { + remaining_len: self.byte_len, + byte_offset: 0, + max_segment_size: self.max_segment_size, + } + } +} + +pub struct TransferSegments { + remaining_len: usize, + byte_offset: usize, + max_segment_size: usize, +} + +impl Iterator for TransferSegments { + type Item = TransferSegment; + + fn next(&mut self) -> Option { + if self.remaining_len == 0 { + return None; + } + + let byte_len = self.remaining_len.min(self.max_segment_size); + let segment = TransferSegment { + byte_offset: self.byte_offset, + byte_len, + }; + self.byte_offset += byte_len; + self.remaining_len -= byte_len; + Some(segment) + } +} + +impl ExactSizeIterator for TransferSegments { + fn len(&self) -> usize { + self.remaining_len.div_ceil(self.max_segment_size) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct TransferPlanner { + device: DeviceInfo, + limits: QueueLimits, + max_chunk_size: usize, +} + +impl TransferPlanner { + pub fn new( + device: DeviceInfo, + limits: QueueLimits, + caps: TransferRuntimeCaps, + ) -> Result { + let max_chunk_size = planned_transfer_size(device, limits, caps)?; + + Ok(Self { + device, + limits, + max_chunk_size, + }) + } + + pub const fn chunk_size(&self) -> usize { + self.max_chunk_size + } + + pub fn plan(&self, lba: u64, byte_len: usize) -> Result { + TransferPlan::new(self.device, self.limits, self.max_chunk_size, lba, byte_len) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct TransferPlan { + next_lba: u64, + byte_offset: usize, + remaining_bytes: usize, + block_size: usize, + max_chunk_size: usize, + max_segment_size: usize, +} + +impl TransferPlan { + fn new( + device: DeviceInfo, + limits: QueueLimits, + max_chunk_size: usize, + lba: u64, + byte_len: usize, + ) -> Result { + let block_size = device.logical_block_size; + if block_size == 0 || byte_len == 0 || !byte_len.is_multiple_of(block_size) { + return Err(BlkError::InvalidRequest); + } + + let block_count = byte_len / block_size; + let block_count_u64 = u64::try_from(block_count).map_err(|_| BlkError::InvalidRequest)?; + if lba >= device.num_blocks + || lba + .checked_add(block_count_u64) + .is_none_or(|end| end > device.num_blocks) + { + return Err(BlkError::InvalidBlockIndex(lba)); + } + + Ok(Self { + next_lba: lba, + byte_offset: 0, + remaining_bytes: byte_len, + block_size, + max_chunk_size, + max_segment_size: limits.max_segment_size, + }) + } +} + +fn planned_transfer_size( + device: DeviceInfo, + limits: QueueLimits, + caps: TransferRuntimeCaps, +) -> Result { + let block_size = device.logical_block_size; + let max_segments = limits.max_segments.min(caps.max_segments); + if block_size == 0 + || limits.max_blocks_per_request == 0 + || max_segments == 0 + || limits.max_segment_size == 0 + || caps.max_transfer_bytes == 0 + { + return Err(BlkError::InvalidRequest); + } + + let max_by_blocks = block_size.saturating_mul(limits.max_blocks_per_request as usize); + let max_by_segments = limits.max_segment_size.saturating_mul(max_segments); + let max_chunk_size = [max_by_blocks, max_by_segments, caps.max_transfer_bytes] + .into_iter() + .min() + .ok_or(BlkError::InvalidRequest)?; + let max_chunk_size = align_down(max_chunk_size, block_size); + if max_chunk_size < block_size { + return Err(BlkError::InvalidRequest); + } + Ok(max_chunk_size) +} + +impl Iterator for TransferPlan { + type Item = TransferChunk; + + fn next(&mut self) -> Option { + if self.remaining_bytes == 0 { + return None; + } + + let byte_len = self.remaining_bytes.min(self.max_chunk_size); + let block_count = byte_len / self.block_size; + let block_count_u32 = block_count as u32; + let chunk = TransferChunk { + lba: self.next_lba, + block_count: block_count_u32, + byte_offset: self.byte_offset, + byte_len, + max_segment_size: self.max_segment_size, + }; + + self.next_lba += block_count as u64; + self.byte_offset += byte_len; + self.remaining_bytes -= byte_len; + Some(chunk) + } +} + +fn align_down(value: usize, align: usize) -> usize { + value / align * align +} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + + use super::*; + use crate::{QueueInfo, RequestFlags}; + + fn queue_info_with(limits: QueueLimits) -> QueueInfo { + QueueInfo { + id: 0, + device: DeviceInfo::new(64, 512), + limits, + } + } + + fn queue_limits( + max_blocks_per_request: u32, + max_segments: usize, + max_segment_size: usize, + ) -> QueueLimits { + QueueLimits { + dma_mask: u64::MAX, + dma_alignment: 512, + max_blocks_per_request, + max_segments, + max_segment_size, + supported_flags: RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + } + } + + fn test_runtime_caps() -> TransferRuntimeCaps { + TransferRuntimeCaps { + max_transfer_bytes: 16 * 1024, + max_segments: 16, + } + } + + fn chunk_summary(chunks: &[TransferChunk]) -> Vec<(u64, u32, usize, usize, usize)> { + chunks + .iter() + .map(|chunk| { + let segments = chunk.segments(); + ( + chunk.lba, + chunk.block_count, + chunk.byte_offset, + chunk.byte_len, + segments.len(), + ) + }) + .collect() + } + + #[test] + fn simple_limits_allow_single_block_transfers() { + let info = queue_info_with(QueueLimits::simple(512, u64::MAX)); + let planner = TransferPlanner::new(info.device, info.limits, test_runtime_caps()).unwrap(); + let plan = planner.plan(0, 2048).unwrap(); + let chunks: Vec<_> = plan.collect(); + + assert_eq!(planner.chunk_size(), 512); + assert_eq!( + chunk_summary(&chunks), + [ + (0, 1, 0, 512, 1), + (1, 1, 512, 512, 1), + (2, 1, 1024, 512, 1), + (3, 1, 1536, 512, 1), + ] + ); + } + + #[test] + fn transfer_plan_chunks_by_runtime_cap() { + let info = queue_info_with(queue_limits(16, 4, 4096)); + let planner = TransferPlanner::new( + info.device, + info.limits, + TransferRuntimeCaps { + max_transfer_bytes: 2048, + max_segments: 16, + }, + ) + .unwrap(); + let plan = planner.plan(4, 5120).unwrap(); + let chunks: Vec<_> = plan.collect(); + + assert_eq!(planner.chunk_size(), 2048); + assert_eq!( + chunk_summary(&chunks), + [ + (4, 4, 0, 2048, 1), + (8, 4, 2048, 2048, 1), + (12, 2, 4096, 1024, 1), + ] + ); + } + + #[test] + fn transfer_chunk_segments_split_by_hard_segment_size() { + let info = queue_info_with(queue_limits(16, 4, 1024)); + let planner = TransferPlanner::new(info.device, info.limits, test_runtime_caps()).unwrap(); + let mut plan = planner.plan(0, 4096).unwrap(); + let chunk = plan.next().unwrap(); + let segment_iter = chunk.segments(); + assert_eq!(segment_iter.len(), 4); + let segments: Vec<_> = segment_iter.collect(); + + assert_eq!( + segments, + [ + TransferSegment { + byte_offset: 0, + byte_len: 1024, + }, + TransferSegment { + byte_offset: 1024, + byte_len: 1024, + }, + TransferSegment { + byte_offset: 2048, + byte_len: 1024, + }, + TransferSegment { + byte_offset: 3072, + byte_len: 1024, + }, + ] + ); + assert!(plan.next().is_none()); + } + + #[test] + fn transfer_plan_clamps_to_hard_block_count() { + let info = queue_info_with(queue_limits(4, 8, 4096)); + let planner = TransferPlanner::new(info.device, info.limits, test_runtime_caps()).unwrap(); + let plan = planner.plan(0, 5120).unwrap(); + let chunks: Vec<_> = plan.collect(); + + assert_eq!(planner.chunk_size(), 2048); + assert_eq!( + chunks + .iter() + .map(|chunk| chunk.byte_len) + .collect::>(), + [2048, 2048, 1024] + ); + } + + #[test] + fn transfer_plan_clamps_to_runtime_limits() { + let info = queue_info_with(queue_limits(16, 8, 2048)); + let planner = TransferPlanner::new( + info.device, + info.limits, + TransferRuntimeCaps { + max_transfer_bytes: 4096, + max_segments: 1, + }, + ) + .unwrap(); + let plan = planner.plan(0, 4096).unwrap(); + let chunks: Vec<_> = plan.collect(); + + assert_eq!(planner.chunk_size(), 2048); + assert_eq!( + chunks + .iter() + .map(|chunk| chunk.byte_len) + .collect::>(), + [2048, 2048] + ); + } + + #[test] + fn transfer_planner_rejects_too_small_runtime_cap() { + let info = queue_info_with(queue_limits(16, 8, 2048)); + + assert_eq!( + TransferPlanner::new( + info.device, + info.limits, + TransferRuntimeCaps { + max_transfer_bytes: 511, + max_segments: 1, + }, + ) + .unwrap_err(), + BlkError::InvalidRequest + ); + } + + #[test] + fn transfer_planner_does_not_depend_on_queue_identity() { + let mut info = queue_info_with(queue_limits(16, 8, 2048)); + let first = TransferPlanner::new(info.device, info.limits, test_runtime_caps()).unwrap(); + info.id = 7; + let second = TransferPlanner::new(info.device, info.limits, test_runtime_caps()).unwrap(); + + assert_eq!(first.chunk_size(), second.chunk_size()); + } + + #[test] + fn transfer_planner_checks_range_when_creating_plan() { + let info = queue_info_with(QueueLimits::simple(512, u64::MAX)); + let planner = TransferPlanner::new(info.device, info.limits, test_runtime_caps()).unwrap(); + + assert_eq!( + planner.plan(63, 1024).unwrap_err(), + BlkError::InvalidBlockIndex(63) + ); + } +} diff --git a/drivers/interface/rdif-block/src/request.rs b/drivers/interface/rdif-block/src/request.rs new file mode 100644 index 0000000000..c6b33180c2 --- /dev/null +++ b/drivers/interface/rdif-block/src/request.rs @@ -0,0 +1,436 @@ +use core::{ + marker::PhantomData, + ops::{Deref, DerefMut}, +}; + +use crate::{BlkError, DeviceInfo, QueueInfo, QueueLimits}; + +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RequestId(usize); + +impl RequestId { + pub const fn new(id: usize) -> Self { + Self(id) + } +} + +impl From for usize { + fn from(value: RequestId) -> Self { + value.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RequestStatus { + Pending, + Complete, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RequestOp { + Read, + Write, + Flush, + Discard, + WriteZeroes, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RequestFlags(u32); + +impl RequestFlags { + pub const NONE: Self = Self(0); + pub const FUA: Self = Self(1 << 0); + pub const PREFLUSH: Self = Self(1 << 1); + pub const SYNC: Self = Self(1 << 2); + pub const META: Self = Self(1 << 3); + pub const POLLED: Self = Self(1 << 4); + pub const NOWAIT: Self = Self(1 << 5); + pub const ALL_KNOWN: Self = Self( + Self::FUA.bits() + | Self::PREFLUSH.bits() + | Self::SYNC.bits() + | Self::META.bits() + | Self::POLLED.bits() + | Self::NOWAIT.bits(), + ); + + pub const fn bits(self) -> u32 { + self.0 + } + + pub const fn is_empty(self) -> bool { + self.0 == 0 + } + + pub const fn contains(self, other: Self) -> bool { + (self.0 & other.0) == other.0 + } + + pub const fn intersects(self, other: Self) -> bool { + (self.0 & other.0) != 0 + } + + pub const fn unsupported_by(self, supported: Self) -> Self { + Self(self.0 & !supported.0) + } +} + +impl core::ops::BitOr for RequestFlags { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } +} + +impl core::ops::BitOrAssign for RequestFlags { + fn bitor_assign(&mut self, rhs: Self) { + self.0 |= rhs.0; + } +} + +impl Default for RequestFlags { + fn default() -> Self { + Self::NONE + } +} + +#[derive(Clone, Copy)] +pub struct Segment<'a> { + pub virt: *mut u8, + pub bus: u64, + pub len: usize, + _marker: PhantomData<&'a mut [u8]>, +} + +impl<'a> Segment<'a> { + /// Creates a block I/O segment from caller-owned CPU and DMA addresses. + /// + /// # Safety + /// + /// `virt` must be valid for reads and writes of `len` bytes for the + /// whole request lifetime, and `bus` must be the DMA/bus address for the + /// same storage. The caller must keep the buffer and DMA mapping alive + /// until `poll_request` reports `RequestStatus::Complete`. + pub unsafe fn from_raw_parts(virt: *mut u8, bus: u64, len: usize) -> Self { + Self { + virt, + bus, + len, + _marker: PhantomData, + } + } +} + +impl Deref for Segment<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + unsafe { core::slice::from_raw_parts(self.virt, self.len) } + } +} + +impl DerefMut for Segment<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { core::slice::from_raw_parts_mut(self.virt, self.len) } + } +} + +pub type Buffer<'a> = Segment<'a>; + +pub struct Request<'a> { + pub op: RequestOp, + pub lba: u64, + pub block_count: u32, + pub segments: &'a mut [Segment<'a>], + pub flags: RequestFlags, +} + +impl Request<'_> { + pub fn data_len(&self) -> usize { + self.segments.iter().map(|segment| segment.len).sum() + } + + pub fn is_data_op(&self) -> bool { + matches!(self.op, RequestOp::Read | RequestOp::Write) + } +} + +pub fn validate_request(info: QueueInfo, request: &Request<'_>) -> Result<(), BlkError> { + validate_request_flags(info, request)?; + validate_request_shape(info.device, info.limits, request) +} + +pub fn validate_request_shape( + info: DeviceInfo, + limits: QueueLimits, + request: &Request<'_>, +) -> Result<(), BlkError> { + if request.block_count == 0 && !matches!(request.op, RequestOp::Flush) { + return Err(BlkError::InvalidRequest); + } + + if request.lba >= info.num_blocks + || request + .lba + .checked_add(request.block_count as u64) + .is_none_or(|end| end > info.num_blocks) + { + return Err(BlkError::InvalidBlockIndex(request.lba)); + } + + match request.op { + RequestOp::Read | RequestOp::Write => { + let expected = request + .block_count + .checked_mul(info.logical_block_size as u32) + .map(|len| len as usize) + .ok_or(BlkError::InvalidRequest)?; + if request.segments.is_empty() + || request.segments.len() > limits.max_segments + || request.data_len() != expected + { + return Err(BlkError::InvalidRequest); + } + if request + .segments + .iter() + .any(|segment| segment.len > limits.max_segment_size) + { + return Err(BlkError::InvalidRequest); + } + } + RequestOp::Flush => { + if !request.segments.is_empty() || request.block_count != 0 { + return Err(BlkError::InvalidRequest); + } + if !limits.supports_flush { + return Err(BlkError::NotSupported); + } + } + RequestOp::Discard => { + if !request.segments.is_empty() { + return Err(BlkError::InvalidRequest); + } + if !limits.supports_discard { + return Err(BlkError::NotSupported); + } + } + RequestOp::WriteZeroes => { + if !request.segments.is_empty() { + return Err(BlkError::InvalidRequest); + } + if !limits.supports_write_zeroes { + return Err(BlkError::NotSupported); + } + } + } + + if request.block_count > limits.max_blocks_per_request { + return Err(BlkError::InvalidRequest); + } + + Ok(()) +} + +fn validate_request_flags(info: QueueInfo, request: &Request<'_>) -> Result<(), BlkError> { + let unknown = request.flags.unsupported_by(RequestFlags::ALL_KNOWN); + if !unknown.is_empty() { + return Err(BlkError::InvalidRequest); + } + + let unsupported = request.flags.unsupported_by(info.limits.supported_flags); + if !unsupported.is_empty() { + return Err(BlkError::NotSupported); + } + + if request.flags.intersects(RequestFlags::PREFLUSH) && !info.limits.supports_flush { + return Err(BlkError::NotSupported); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_status_distinguishes_pending_from_errors() { + assert_eq!(RequestStatus::Pending, RequestStatus::Pending); + assert_ne!(RequestStatus::Pending, RequestStatus::Complete); + } + + #[test] + fn segment_carries_cpu_and_dma_addresses() { + let mut bytes = [0x5a_u8; 4]; + let segment = unsafe { Segment::from_raw_parts(bytes.as_mut_ptr(), 0x1000, bytes.len()) }; + + assert_eq!(segment.bus, 0x1000); + assert_eq!(&*segment, &[0x5a; 4]); + } + + #[test] + fn request_shape_checks_lba_and_segments() { + let info = DeviceInfo::new(8, 512); + let limits = QueueLimits { + max_blocks_per_request: 8, + max_segment_size: 1024, + ..QueueLimits::simple(512, u64::MAX) + }; + let mut bytes = [0_u8; 1024]; + let segment = unsafe { Segment::from_raw_parts(bytes.as_mut_ptr(), 0x1000, bytes.len()) }; + let mut segments = [segment]; + let request = Request { + op: RequestOp::Read, + lba: 1, + block_count: 2, + segments: &mut segments, + flags: RequestFlags::NONE, + }; + + assert_eq!(validate_request_shape(info, limits, &request), Ok(())); + } + + #[test] + fn request_shape_rejects_wrong_segment_size() { + let info = DeviceInfo::new(8, 512); + let limits = QueueLimits::simple(512, u64::MAX); + let mut bytes = [0_u8; 512]; + let segment = unsafe { Segment::from_raw_parts(bytes.as_mut_ptr(), 0x1000, bytes.len()) }; + let mut segments = [segment]; + let request = Request { + op: RequestOp::Write, + lba: 1, + block_count: 2, + segments: &mut segments, + flags: RequestFlags::NONE, + }; + + assert_eq!( + validate_request_shape(info, limits, &request), + Err(BlkError::InvalidRequest) + ); + } + + fn queue_info_with(limits: QueueLimits) -> QueueInfo { + QueueInfo { + id: 0, + device: DeviceInfo::new(64, 512), + limits, + } + } + + #[test] + fn request_validation_rejects_unsupported_flags() { + let info = queue_info_with(QueueLimits::simple(512, u64::MAX)); + let mut bytes = [0_u8; 512]; + let segment = unsafe { Segment::from_raw_parts(bytes.as_mut_ptr(), 0x1000, bytes.len()) }; + let mut segments = [segment]; + let request = Request { + op: RequestOp::Write, + lba: 0, + block_count: 1, + segments: &mut segments, + flags: RequestFlags::FUA, + }; + + assert_eq!( + validate_request(info, &request), + Err(BlkError::NotSupported) + ); + } + + #[test] + fn request_validation_rejects_unknown_flags() { + let info = queue_info_with(QueueLimits::simple(512, u64::MAX)); + let mut bytes = [0_u8; 512]; + let segment = unsafe { Segment::from_raw_parts(bytes.as_mut_ptr(), 0x1000, bytes.len()) }; + let mut segments = [segment]; + let request = Request { + op: RequestOp::Read, + lba: 0, + block_count: 1, + segments: &mut segments, + flags: RequestFlags(1 << 24), + }; + + assert_eq!( + validate_request(info, &request), + Err(BlkError::InvalidRequest) + ); + } + + #[test] + fn request_validation_accepts_supported_flags() { + let mut limits = QueueLimits::simple(512, u64::MAX); + limits.supported_flags = RequestFlags::FUA; + let info = queue_info_with(limits); + let mut bytes = [0_u8; 512]; + let segment = unsafe { Segment::from_raw_parts(bytes.as_mut_ptr(), 0x1000, bytes.len()) }; + let mut segments = [segment]; + let request = Request { + op: RequestOp::Write, + lba: 0, + block_count: 1, + segments: &mut segments, + flags: RequestFlags::FUA, + }; + + assert_eq!(validate_request(info, &request), Ok(())); + } + + #[test] + fn preflush_flag_requires_flush_support() { + let mut limits = QueueLimits::simple(512, u64::MAX); + limits.supported_flags = RequestFlags::PREFLUSH; + let info = queue_info_with(limits); + let mut bytes = [0_u8; 512]; + let segment = unsafe { Segment::from_raw_parts(bytes.as_mut_ptr(), 0x1000, bytes.len()) }; + let mut segments = [segment]; + let request = Request { + op: RequestOp::Write, + lba: 0, + block_count: 1, + segments: &mut segments, + flags: RequestFlags::PREFLUSH, + }; + + assert_eq!( + validate_request(info, &request), + Err(BlkError::NotSupported) + ); + } + + #[test] + fn request_validation_rejects_transfer_larger_than_hard_block_limit() { + let info = queue_info_with(QueueLimits { + dma_mask: u64::MAX, + dma_alignment: 512, + max_blocks_per_request: 2, + max_segments: 1, + max_segment_size: 4096, + supported_flags: RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + }); + let mut bytes = [0_u8; 1536]; + let segment = unsafe { Segment::from_raw_parts(bytes.as_mut_ptr(), 0x1000, bytes.len()) }; + let mut segments = [segment]; + let request = Request { + op: RequestOp::Write, + lba: 0, + block_count: 3, + segments: &mut segments, + flags: RequestFlags::NONE, + }; + + assert_eq!( + validate_request(info, &request), + Err(BlkError::InvalidRequest) + ); + } +} diff --git a/drivers/usb/usb-host/src/backend/kmod/xhci/ring.rs b/drivers/usb/usb-host/src/backend/kmod/xhci/ring.rs index c34029cf6b..d197fe5e72 100644 --- a/drivers/usb/usb-host/src/backend/kmod/xhci/ring.rs +++ b/drivers/usb/usb-host/src/backend/kmod/xhci/ring.rs @@ -21,7 +21,7 @@ const DEFAULT_RING_PAGES: usize = 2; pub struct TrbData([u32; TRB_LEN]); impl TrbData { - pub fn to_raw(&self) -> [u32; TRB_LEN] { + pub fn to_raw(self) -> [u32; TRB_LEN] { self.0 } } diff --git a/os/arceos/api/arceos_posix_api/src/imp/pthread/mod.rs b/os/arceos/api/arceos_posix_api/src/imp/pthread/mod.rs index 0f2a39c529..32d778292b 100644 --- a/os/arceos/api/arceos_posix_api/src/imp/pthread/mod.rs +++ b/os/arceos/api/arceos_posix_api/src/imp/pthread/mod.rs @@ -2,6 +2,7 @@ use alloc::{boxed::Box, collections::BTreeMap, sync::Arc}; use core::{ cell::UnsafeCell, ffi::{c_int, c_void}, + sync::atomic::{AtomicBool, Ordering}, }; use ax_errno::{LinuxError, LinuxResult}; @@ -52,8 +53,13 @@ impl Pthread { result: UnsafeCell::new(core::ptr::null_mut()), }); let their_packet = my_packet.clone(); + let registered = Arc::new(AtomicBool::new(false)); + let child_registered = registered.clone(); let main = move || { + while !child_registered.load(Ordering::Acquire) { + ax_task::yield_now(); + } let arg = arg_wrapper; let ret = start_routine(arg.0); unsafe { *their_packet.result.get() = ret }; @@ -68,6 +74,7 @@ impl Pthread { }; let ptr = Box::into_raw(Box::new(thread)) as *mut c_void; TID_TO_PTHREAD.write().insert(tid, ForceSendSync(ptr)); + registered.store(true, Ordering::Release); Ok(ptr) } diff --git a/os/arceos/api/axfeat/Cargo.toml b/os/arceos/api/axfeat/Cargo.toml index 212b137d33..970e8b520a 100644 --- a/os/arceos/api/axfeat/Cargo.toml +++ b/os/arceos/api/axfeat/Cargo.toml @@ -74,8 +74,8 @@ fs = [ "ax-runtime/fs", ] # TODO: try to remove "paging" fs-ng = ["paging", "dep:ax-fs-ng", "ax-runtime/fs-ng"] -fs-ng-ext4 = ["fs-ng", "ax-fs-ng/ext4"] -fs-ng-fat = ["fs-ng", "ax-fs-ng/fat"] +fs-ng-ext4 = ["fs-ng", "ax-fs-ng/ext4", "ax-runtime/fs-ng-ext4"] +fs-ng-fat = ["fs-ng", "ax-fs-ng/fat", "ax-runtime/fs-ng-fat"] fs-ng-times = ["fs-ng", "ax-fs-ng/times"] # Networking diff --git a/os/arceos/modules/axfs-ng/Cargo.toml b/os/arceos/modules/axfs-ng/Cargo.toml index 2262c6790c..1410daabbd 100644 --- a/os/arceos/modules/axfs-ng/Cargo.toml +++ b/os/arceos/modules/axfs-ng/Cargo.toml @@ -30,7 +30,6 @@ intrusive-collections = "0.9" ax-kspin = { workspace = true } log = { workspace = true } lru = "0.16" -rd-block-volume = { workspace = true } scope-local = { workspace = true } slab = { version = "0.4", default-features = false } spin = { workspace = true } diff --git a/os/arceos/modules/axfs-ng/src/block.rs b/os/arceos/modules/axfs-ng/src/block.rs index 3cd8124563..5142364399 100644 --- a/os/arceos/modules/axfs-ng/src/block.rs +++ b/os/arceos/modules/axfs-ng/src/block.rs @@ -3,7 +3,6 @@ use alloc::boxed::Box; #[cfg(any(feature = "ext4", feature = "fat"))] use ax_errno::AxError; use ax_errno::AxResult; -use rd_block_volume::{BlockReader, Error as VolumeError}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct BlockRegion { @@ -132,29 +131,3 @@ impl FsBlockDevice for RegionBlockDevice { self.inner.flush() } } - -pub struct VolumeReader<'a, T: FsBlockDevice + ?Sized> { - inner: &'a mut T, -} - -impl<'a, T: FsBlockDevice + ?Sized> VolumeReader<'a, T> { - pub const fn new(inner: &'a mut T) -> Self { - Self { inner } - } -} - -impl BlockReader for VolumeReader<'_, T> { - fn block_size(&self) -> usize { - self.inner.block_size() - } - - fn num_blocks(&self) -> u64 { - self.inner.num_blocks() - } - - fn read_block(&mut self, block: u64, buf: &mut [u8]) -> rd_block_volume::Result<()> { - self.inner - .read_block(block, buf) - .map_err(|_| VolumeError::Reader) - } -} diff --git a/os/arceos/modules/axfs-ng/src/lib.rs b/os/arceos/modules/axfs-ng/src/lib.rs index 0b9344f4df..956173b0b7 100644 --- a/os/arceos/modules/axfs-ng/src/lib.rs +++ b/os/arceos/modules/axfs-ng/src/lib.rs @@ -12,53 +12,30 @@ extern crate alloc; #[macro_use] extern crate log; -use alloc::{boxed::Box, vec::Vec}; +use alloc::boxed::Box; mod block; mod fs; mod highlevel; -mod root; pub use block::{BlockRegion, FsBlockDevice}; /// Create a filesystem from a dynamic (boxed) block device. #[cfg(feature = "ext4")] pub use fs::new_from_dyn as new_filesystem_from_dyn; pub use highlevel::*; -use root::FilesystemKind; -/// Initializes the filesystem subsystem by selecting a root device from the -/// available block devices and optional boot arguments. -pub fn init_filesystems(block_devs: Vec>, bootargs: Option<&str>) { - info!("Initialize filesystem subsystem..."); +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum FilesystemKind { + Ext4, + Fat, +} - let root_spec = root::parse_root_spec(bootargs); - let mut disks = root::collect_disks(block_devs); - let candidates = root::collect_root_candidates(&disks); - let (selected_disk_index, selected_partition) = - root::select_root_candidate(&candidates, &root_spec).unwrap_or_else(|| { - panic!("failed to determine root device from available block devices") - }); - let selected_disk_pos = disks - .iter() - .position(|disk| disk.disk_index == selected_disk_index) - .unwrap_or_else(|| panic!("selected root disk disappeared during initialization")); - let selected = disks.swap_remove(selected_disk_pos); - let (description, region) = { - let selected_partition_info = selected_partition.and_then(|part_index| { - selected - .partitions - .iter() - .find(|partition| partition.info.index == part_index) - }); - ( - root::describe_selection(selected.disk_index, selected_partition_info), - selected_partition_info - .map_or_else(|| full_region(&selected.dev), |part| part.info.region), - ) - }; +/// Initializes the filesystem subsystem from a runtime-selected block region. +pub fn init_filesystem(dev: Box, region: BlockRegion, description: &str) { + info!("Initialize filesystem subsystem..."); info!(" selected root device: {}", description); - let fs = fs::new_default(selected.dev, region).unwrap_or_else(|err| { + let fs = fs::new_default(dev, region).unwrap_or_else(|err| { panic!( "failed to initialize filesystem on {}: {err:?}", description @@ -70,7 +47,10 @@ pub fn init_filesystems(block_devs: Vec>, bootargs: Optio ROOT_FS_CONTEXT.call_once(|| FsContext::new(mp.root_location())); } -fn detect_filesystem(dev: &mut dyn FsBlockDevice, region: BlockRegion) -> Option { +pub fn detect_filesystem( + dev: &mut dyn FsBlockDevice, + region: BlockRegion, +) -> Option { #[cfg(not(any(feature = "ext4", feature = "fat")))] let _ = (&mut *dev, region); @@ -163,7 +143,3 @@ fn region_has_magic_u16( u16::from_le_bytes([buf[within_block], buf[within_block + 1]]) == magic } - -fn full_region(dev: &dyn FsBlockDevice) -> BlockRegion { - BlockRegion::from_num_blocks(dev.num_blocks()) -} diff --git a/os/arceos/modules/axruntime/Cargo.toml b/os/arceos/modules/axruntime/Cargo.toml index 8c4729fda0..f9f916d094 100644 --- a/os/arceos/modules/axruntime/Cargo.toml +++ b/os/arceos/modules/axruntime/Cargo.toml @@ -12,7 +12,7 @@ default = [] dma = ["paging"] ipi = ["dep:ax-ipi"] -irq = ["ax-hal/irq", "ax-task?/irq", "dep:ax-percpu"] +irq = ["ax-hal/irq", "ax-task?/irq", "dep:ax-percpu", "ax-driver/irq"] multitask = ["ax-task/multitask"] paging = ["ax-hal/paging", "dep:ax-mm", "dep:axklib"] rtc = [] @@ -35,7 +35,6 @@ display = ["dep:ax-display", "ax-driver/display"] fs = [ "dep:ax-errno", "dep:ax-fs", - "dep:rd-block", "dep:spin", "dep:axklib", "ax-driver/block", @@ -43,11 +42,12 @@ fs = [ fs-ng = [ "dep:ax-errno", "dep:ax-fs-ng", - "dep:rd-block", "dep:spin", "dep:axklib", "ax-driver/block", ] +fs-ng-ext4 = ["fs-ng", "ax-fs-ng/ext4"] +fs-ng-fat = ["fs-ng", "ax-fs-ng/fat"] input = ["dep:ax-input", "ax-driver/input"] net = ["dep:ax-net", "ax-driver/net"] net-ng = ["dep:ax-net-ng", "dep:rd-net", "dep:spin", "dep:axklib", "ax-driver/net"] @@ -81,7 +81,6 @@ axpanic = {workspace = true} cfg-if = {workspace = true} chrono = {version = "0.4", default-features = false} indoc = "2" -rd-block = {workspace = true, optional = true} -rd-net = {workspace = true, optional = true} +rd-net = { workspace = true, optional = true } rdrive.workspace = true spin = {workspace = true, optional = true} diff --git a/os/arceos/modules/axruntime/src/block/mod.rs b/os/arceos/modules/axruntime/src/block/mod.rs new file mode 100644 index 0000000000..dd452f7d25 --- /dev/null +++ b/os/arceos/modules/axruntime/src/block/mod.rs @@ -0,0 +1,108 @@ +#[cfg(all(feature = "fs-ng", any(not(feature = "plat-dyn"), target_os = "none")))] +use alloc::vec::Vec; + +#[cfg(all(feature = "fs-ng", any(not(feature = "plat-dyn"), target_os = "none")))] +mod root; +#[cfg(any( + all(feature = "fs-ng", any(not(feature = "plat-dyn"), target_os = "none")), + test +))] +pub(crate) mod volume; + +#[cfg(all(feature = "fs-ng", any(not(feature = "plat-dyn"), target_os = "none")))] +struct FsNgBlockDevice(ax_driver::block::Block); + +#[cfg(all(feature = "fs-ng", any(not(feature = "plat-dyn"), target_os = "none")))] +impl ax_fs_ng::FsBlockDevice for FsNgBlockDevice { + fn name(&self) -> &str { + self.0.name() + } + + fn num_blocks(&self) -> u64 { + self.0.num_blocks() + } + + fn block_size(&self) -> usize { + self.0.block_size() + } + + fn read_block(&mut self, block_id: u64, buf: &mut [u8]) -> ax_errno::AxResult { + self.0.read_block(block_id, buf) + } + + fn write_block(&mut self, block_id: u64, buf: &[u8]) -> ax_errno::AxResult { + self.0.write_block(block_id, buf) + } + + fn flush(&mut self) -> ax_errno::AxResult { + self.0.flush() + } +} + +#[cfg(any( + all( + feature = "fs", + not(feature = "fs-ng"), + any(not(feature = "plat-dyn"), target_os = "none") + ), + all(feature = "fs-ng", any(not(feature = "plat-dyn"), target_os = "none")) +))] +pub(crate) fn register_irq_handlers(blocks: &mut [ax_driver::block::Block]) { + // Block queues are driven through the rdif-block polled path for now. Avoid + // installing block handlers on shared legacy IRQ lines used by net devices. + let _ = blocks; +} + +#[cfg(all(feature = "fs-ng", feature = "plat-dyn"))] +pub(crate) fn init_dyn_fs_ng(bootargs: Option<&str>) { + #[cfg(target_os = "none")] + init_fs_ng_from_blocks(take_block_devices(), bootargs); + + #[cfg(not(target_os = "none"))] + let _ = bootargs; +} + +#[cfg(all(feature = "fs-ng", not(feature = "plat-dyn")))] +pub(crate) fn init_static_fs_ng() { + init_fs_ng_from_blocks(take_block_devices(), None); +} + +#[cfg(all(feature = "fs-ng", any(not(feature = "plat-dyn"), target_os = "none")))] +fn take_block_devices() -> Vec { + let mut devices = ax_driver::block::take_block_devices(); + register_irq_handlers(&mut devices); + devices +} + +#[cfg(all(feature = "fs-ng", any(not(feature = "plat-dyn"), target_os = "none")))] +fn init_fs_ng_from_blocks(blocks: Vec, bootargs: Option<&str>) { + let block_devs = blocks.into_iter().map(|dev| { + alloc::boxed::Box::new(FsNgBlockDevice(dev)) + as alloc::boxed::Box + }); + let root_spec = root::parse_root_spec(bootargs); + let mut disks = root::collect_disks(block_devs); + let candidates = root::collect_root_candidates(&disks); + let (selected_disk_index, selected_partition) = + root::select_root_candidate(&candidates, &root_spec).unwrap_or_else(|| { + panic!("failed to determine root device from available block devices") + }); + let selected_disk_pos = disks + .iter() + .position(|disk| disk.disk_index == selected_disk_index) + .unwrap_or_else(|| panic!("selected root disk disappeared during initialization")); + let selected = disks.swap_remove(selected_disk_pos); + let selected_partition_info = selected_partition.and_then(|part_index| { + selected + .partitions + .iter() + .find(|partition| partition.info.index == part_index) + }); + let description = root::describe_selection(selected.disk_index, selected_partition_info); + let region = selected_partition_info.map_or_else( + || ax_fs_ng::BlockRegion::from_num_blocks(selected.dev.num_blocks()), + |part| part.info.region, + ); + + ax_fs_ng::init_filesystem(selected.dev, region, &description); +} diff --git a/os/arceos/modules/axfs-ng/src/root.rs b/os/arceos/modules/axruntime/src/block/root.rs similarity index 90% rename from os/arceos/modules/axfs-ng/src/root.rs rename to os/arceos/modules/axruntime/src/block/root.rs index cf64221ea9..2216e7acb1 100644 --- a/os/arceos/modules/axfs-ng/src/root.rs +++ b/os/arceos/modules/axruntime/src/block/root.rs @@ -5,9 +5,38 @@ use alloc::{ vec::Vec, }; -use rd_block_volume::{BlockVolume, DiskId, PartitionTableKind as VolumeTableKind, scan_volumes}; +use ax_fs_ng::{BlockRegion, FilesystemKind, FsBlockDevice}; -use crate::block::{BlockRegion, FsBlockDevice, VolumeReader}; +use super::volume::{ + BlockReader, BlockVolume, DiskId, Error as VolumeError, PartitionTableKind as VolumeTableKind, + scan_volumes, +}; + +struct VolumeReader<'a, T: FsBlockDevice + ?Sized> { + inner: &'a mut T, +} + +impl<'a, T: FsBlockDevice + ?Sized> VolumeReader<'a, T> { + const fn new(inner: &'a mut T) -> Self { + Self { inner } + } +} + +impl BlockReader for VolumeReader<'_, T> { + fn block_size(&self) -> usize { + self.inner.block_size() + } + + fn num_blocks(&self) -> u64 { + self.inner.num_blocks() + } + + fn read_block(&mut self, block: u64, buf: &mut [u8]) -> super::volume::Result<()> { + self.inner + .read_block(block, buf) + .map_err(|_| VolumeError::Reader) + } +} #[derive(Debug, Default)] pub(crate) struct RootSpec { @@ -51,14 +80,6 @@ pub(crate) enum PartitionTableKind { Mbr, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum FilesystemKind { - #[cfg(feature = "ext4")] - Ext4, - #[cfg(feature = "fat")] - Fat, -} - impl RootCandidate { pub(crate) fn description(&self) -> String { if let Some(partition) = &self.partition { @@ -107,13 +128,13 @@ fn collect_partitions( for volume in volumes { if volume.table_kind == VolumeTableKind::Raw { let region = region_from_volume(&volume); - let raw_fs = super::detect_filesystem(dev, region); + let raw_fs = ax_fs_ng::detect_filesystem(dev, region); info!(" raw device fs={:?}", raw_fs); continue; } let info = partition_info_from_volume(&volume); - let filesystem = super::detect_filesystem(dev, info.region); + let filesystem = ax_fs_ng::detect_filesystem(dev, info.region); info!( " partition {} name={:?} fs={:?} lba {}..{}", info.index + 1, @@ -342,39 +363,7 @@ fn supported_filesystem_partition_matches( } fn supported_default_root_partition(partition: &DetectedPartition) -> bool { - if !partition.filesystem.is_some() { - return false; - } - match partition.info.table_kind { - PartitionTableKind::Mbr => { - #[cfg(feature = "ext4")] - { - partition.filesystem == Some(FilesystemKind::Ext4) - } - #[cfg(all(not(feature = "ext4"), feature = "fat"))] - { - partition.filesystem == Some(FilesystemKind::Fat) - } - #[cfg(all(not(feature = "ext4"), not(feature = "fat")))] - { - false - } - } - PartitionTableKind::Gpt | PartitionTableKind::Raw => { - #[cfg(feature = "ext4")] - { - partition.filesystem == Some(FilesystemKind::Ext4) - } - #[cfg(all(not(feature = "ext4"), feature = "fat"))] - { - partition.filesystem == Some(FilesystemKind::Fat) - } - #[cfg(all(not(feature = "ext4"), not(feature = "fat")))] - { - false - } - } - } + partition.filesystem.is_some() } pub(crate) fn describe_selection( @@ -488,9 +477,7 @@ fn parse_sd_path(path: &str) -> RootSpec { const fn filesystem_name(fs: FilesystemKind) -> &'static str { match fs { - #[cfg(feature = "ext4")] FilesystemKind::Ext4 => "ext4", - #[cfg(feature = "fat")] FilesystemKind::Fat => "fat", } } @@ -521,7 +508,6 @@ mod tests { } #[test] - #[cfg(feature = "ext4")] fn default_root_uses_only_supported_mbr_filesystem_partition_without_boot_flag() { let candidates = [ mbr_partition(0, None, false), @@ -532,7 +518,6 @@ mod tests { } #[test] - #[cfg(feature = "ext4")] fn default_root_prefers_only_bootable_mbr_filesystem_partition() { let candidates = [ mbr_partition(0, Some(FilesystemKind::Ext4), false), diff --git a/drivers/blk/rd-block-volume/src/error.rs b/os/arceos/modules/axruntime/src/block/volume/error.rs similarity index 100% rename from drivers/blk/rd-block-volume/src/error.rs rename to os/arceos/modules/axruntime/src/block/volume/error.rs diff --git a/drivers/blk/rd-block-volume/src/gpt.rs b/os/arceos/modules/axruntime/src/block/volume/gpt.rs similarity index 99% rename from drivers/blk/rd-block-volume/src/gpt.rs rename to os/arceos/modules/axruntime/src/block/volume/gpt.rs index 3c0bc108f2..467117ddbc 100644 --- a/drivers/blk/rd-block-volume/src/gpt.rs +++ b/os/arceos/modules/axruntime/src/block/volume/gpt.rs @@ -1,6 +1,6 @@ use alloc::{format, string::String, vec::Vec}; -use crate::{ +use super::{ BlockReader, BlockRegion, BlockVolume, DiskId, Error, PartitionId, PartitionLabel, PartitionTableKind, PartitionUuid, Result, mbr, }; diff --git a/drivers/blk/rd-block-volume/src/mbr.rs b/os/arceos/modules/axruntime/src/block/volume/mbr.rs similarity index 99% rename from drivers/blk/rd-block-volume/src/mbr.rs rename to os/arceos/modules/axruntime/src/block/volume/mbr.rs index 78d68d6731..ce638f5d31 100644 --- a/drivers/blk/rd-block-volume/src/mbr.rs +++ b/os/arceos/modules/axruntime/src/block/volume/mbr.rs @@ -1,6 +1,6 @@ use alloc::{collections::BTreeSet, format, vec::Vec}; -use crate::{ +use super::{ BlockReader, BlockRegion, BlockVolume, DiskId, Error, PartitionId, PartitionTableKind, PartitionUuid, Result, }; diff --git a/drivers/blk/rd-block-volume/src/lib.rs b/os/arceos/modules/axruntime/src/block/volume/mod.rs similarity index 89% rename from drivers/blk/rd-block-volume/src/lib.rs rename to os/arceos/modules/axruntime/src/block/volume/mod.rs index bff977ea52..e83546f0fc 100644 --- a/drivers/blk/rd-block-volume/src/lib.rs +++ b/os/arceos/modules/axruntime/src/block/volume/mod.rs @@ -1,12 +1,10 @@ -#![no_std] - -extern crate alloc; - mod error; mod gpt; mod mbr; mod reader; mod scan; +#[cfg(test)] +mod tests; mod types; pub use error::{Error, Result}; diff --git a/drivers/blk/rd-block-volume/src/reader.rs b/os/arceos/modules/axruntime/src/block/volume/reader.rs similarity index 97% rename from drivers/blk/rd-block-volume/src/reader.rs rename to os/arceos/modules/axruntime/src/block/volume/reader.rs index 508882f716..56e44b58ff 100644 --- a/drivers/blk/rd-block-volume/src/reader.rs +++ b/os/arceos/modules/axruntime/src/block/volume/reader.rs @@ -1,4 +1,4 @@ -use crate::{Error, Result}; +use super::{Error, Result}; pub trait BlockReader { fn block_size(&self) -> usize; diff --git a/drivers/blk/rd-block-volume/src/scan.rs b/os/arceos/modules/axruntime/src/block/volume/scan.rs similarity index 98% rename from drivers/blk/rd-block-volume/src/scan.rs rename to os/arceos/modules/axruntime/src/block/volume/scan.rs index 9acbfcf619..e1635dd022 100644 --- a/drivers/blk/rd-block-volume/src/scan.rs +++ b/os/arceos/modules/axruntime/src/block/volume/scan.rs @@ -1,6 +1,6 @@ use alloc::vec::Vec; -use crate::{ +use super::{ BlockReader, BlockRegion, BlockVolume, DiskId, Error, PartitionId, PartitionTableKind, Result, gpt, mbr, }; diff --git a/drivers/blk/rd-block-volume/tests/scan.rs b/os/arceos/modules/axruntime/src/block/volume/tests.rs similarity index 98% rename from drivers/blk/rd-block-volume/tests/scan.rs rename to os/arceos/modules/axruntime/src/block/volume/tests.rs index cbf2faaad3..6bd2922032 100644 --- a/drivers/blk/rd-block-volume/tests/scan.rs +++ b/os/arceos/modules/axruntime/src/block/volume/tests.rs @@ -1,4 +1,4 @@ -use rd_block_volume::{ +use super::{ BlockReader, BlockRegion, DiskId, Error, PartitionId, PartitionTableKind, Result, scan_volumes, }; @@ -79,6 +79,11 @@ fn raw_disk_fallback_covers_entire_reader() { assert_eq!(volumes[0].partlabel, None); } +#[test] +fn reader_error_variant_is_available_to_runtime_adapters() { + assert_eq!(Error::Reader, Error::Reader); +} + #[test] fn scans_mbr_primary_partition() { let mut reader = MemReader::new(128); diff --git a/drivers/blk/rd-block-volume/src/types.rs b/os/arceos/modules/axruntime/src/block/volume/types.rs similarity index 93% rename from drivers/blk/rd-block-volume/src/types.rs rename to os/arceos/modules/axruntime/src/block/volume/types.rs index 5bd09b2e57..4381bf2cab 100644 --- a/drivers/blk/rd-block-volume/src/types.rs +++ b/os/arceos/modules/axruntime/src/block/volume/types.rs @@ -19,10 +19,6 @@ impl BlockRegion { num_blocks, } } - - pub const fn is_empty(self) -> bool { - self.num_blocks == 0 - } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/os/arceos/modules/axruntime/src/devices.rs b/os/arceos/modules/axruntime/src/devices.rs index f218302dfe..adc36ca37b 100644 --- a/os/arceos/modules/axruntime/src/devices.rs +++ b/os/arceos/modules/axruntime/src/devices.rs @@ -16,7 +16,9 @@ pub(crate) fn take_dyn_fs_block_devices() if !rdrive::is_initialized() { return alloc::vec::Vec::new(); } - ax_driver::block::take_block_devices() + let mut devices = ax_driver::block::take_block_devices(); + crate::block::register_irq_handlers(&mut devices); + devices .into_iter() .map(|dev| { alloc::boxed::Box::new(FsBlockDevice(dev)) @@ -32,7 +34,9 @@ pub(crate) fn take_dyn_fs_block_devices() #[cfg(all(feature = "fs", not(feature = "fs-ng"), not(feature = "plat-dyn")))] pub(crate) fn take_static_fs_block_devices() -> alloc::vec::Vec> { - ax_driver::block::take_block_devices() + let mut devices = ax_driver::block::take_block_devices(); + crate::block::register_irq_handlers(&mut devices); + devices .into_iter() .map(|dev| { alloc::boxed::Box::new(FsBlockDevice(dev)) @@ -41,39 +45,6 @@ pub(crate) fn take_static_fs_block_devices() .collect() } -#[cfg(all(feature = "fs-ng", feature = "plat-dyn"))] -pub(crate) fn take_dyn_fs_ng_block_devices() --> alloc::vec::Vec> { - #[cfg(target_os = "none")] - { - if !rdrive::is_initialized() { - return alloc::vec::Vec::new(); - } - ax_driver::block::take_block_devices() - .into_iter() - .map(|dev| { - alloc::boxed::Box::new(FsBlockDevice(dev)) - as alloc::boxed::Box - }) - .collect() - } - - #[cfg(not(target_os = "none"))] - alloc::vec::Vec::new() -} - -#[cfg(all(feature = "fs-ng", not(feature = "plat-dyn")))] -pub(crate) fn take_static_fs_ng_block_devices() --> alloc::vec::Vec> { - ax_driver::block::take_block_devices() - .into_iter() - .map(|dev| { - alloc::boxed::Box::new(FsBlockDevice(dev)) - as alloc::boxed::Box - }) - .collect() -} - #[cfg(all(feature = "display", feature = "plat-dyn"))] pub(crate) fn init_dyn_display() { #[cfg(target_os = "none")] @@ -257,7 +228,8 @@ fn take_dyn_net_ng_drivers() -> alloc::vec::Vec &str { - self.0.name() - } - - fn num_blocks(&self) -> u64 { - self.0.num_blocks() - } - - fn block_size(&self) -> usize { - self.0.block_size() - } - - fn read_block(&mut self, block_id: u64, buf: &mut [u8]) -> ax_errno::AxResult { - self.0.read_block(block_id, buf) - } - - fn write_block(&mut self, block_id: u64, buf: &[u8]) -> ax_errno::AxResult { - self.0.write_block(block_id, buf) - } - - fn flush(&mut self) -> ax_errno::AxResult { - self.0.flush() - } -} diff --git a/os/arceos/modules/axruntime/src/lib.rs b/os/arceos/modules/axruntime/src/lib.rs index 99bb2ce06f..118d2bdf16 100644 --- a/os/arceos/modules/axruntime/src/lib.rs +++ b/os/arceos/modules/axruntime/src/lib.rs @@ -47,6 +47,8 @@ mod mp; #[cfg(feature = "paging")] mod klib; +#[cfg(any(feature = "fs", feature = "fs-ng", test))] +mod block; mod devices; mod registers; @@ -271,12 +273,9 @@ pub fn rust_main(cpu_id: usize, arg: usize) -> ! { cfg_if::cfg_if! { if #[cfg(all(feature = "fs-ng", feature = "plat-dyn"))] { - ax_fs_ng::init_filesystems( - devices::take_dyn_fs_ng_block_devices(), - ax_hal::dtb::get_chosen_bootargs(), - ); + block::init_dyn_fs_ng(ax_hal::dtb::get_chosen_bootargs()); } else if #[cfg(all(feature = "fs-ng", not(feature = "plat-dyn")))] { - ax_fs_ng::init_filesystems(devices::take_static_fs_ng_block_devices(), None); + block::init_static_fs_ng(); } else if #[cfg(all(feature = "fs", feature = "plat-dyn"))] { ax_fs::init_filesystems( devices::take_dyn_fs_block_devices(), diff --git a/os/axvisor/Cargo.toml b/os/axvisor/Cargo.toml index 2dc8d04429..72d9a5e4ef 100644 --- a/os/axvisor/Cargo.toml +++ b/os/axvisor/Cargo.toml @@ -50,7 +50,7 @@ sdmmc = [ "ax-driver/phytium-mci", ] rockchip-pm = ["ax-driver/rockchip-pm"] -serial = ["ax-driver/serial"] +serial = [] [dependencies] spin = { workspace = true } diff --git a/platforms/ax-plat-riscv64-sg2002/Cargo.toml b/platforms/ax-plat-riscv64-sg2002/Cargo.toml index 346c3d8333..209c21e3d2 100644 --- a/platforms/ax-plat-riscv64-sg2002/Cargo.toml +++ b/platforms/ax-plat-riscv64-sg2002/Cargo.toml @@ -29,7 +29,7 @@ ax-kspin = { workspace = true } ax-lazyinit = { workspace = true } ax-plat = { workspace = true } axklib = { workspace = true, features = ["tlsf"] } -rd-block.workspace = true +rdif-block.workspace = true sg200x-bsp.workspace = true some-serial.workspace = true spin.workspace = true diff --git a/platforms/ax-plat-riscv64-sg2002/src/drivers/cvsd.rs b/platforms/ax-plat-riscv64-sg2002/src/drivers/cvsd.rs index f3d99d5851..09401b5f15 100644 --- a/platforms/ax-plat-riscv64-sg2002/src/drivers/cvsd.rs +++ b/platforms/ax-plat-riscv64-sg2002/src/drivers/cvsd.rs @@ -1,9 +1,15 @@ use alloc::{boxed::Box, format, sync::Arc}; +use core::{ + cell::UnsafeCell, + sync::atomic::{AtomicBool, Ordering}, +}; use ax_driver::{PlatformDevice, block::PlatformDeviceBlock, probe::OnProbeError}; -use rd_block::{BlkError, BuffConfig, DriverGeneric, Event, IQueue, Interface, Request, RequestId}; +use rdif_block::{ + BlkError, DeviceInfo, DriverGeneric, IQueue, Interface, QueueInfo, QueueLimits, Request, + RequestFlags, RequestId, RequestOp, RequestStatus, validate_request, +}; use sg200x_bsp::sdmmc::Sdmmc; -use spin::Mutex; use crate::config::devices; @@ -65,6 +71,76 @@ struct CvsdDriver(Sdmmc); // contexts is sound. unsafe impl Send for CvsdDriver {} +struct SharedCvsdDriver { + inner: Arc, +} + +struct SharedCvsdDriverInner { + driver: UnsafeCell, + borrowed: AtomicBool, +} + +struct SharedCvsdDriverGuard<'a> { + inner: &'a SharedCvsdDriverInner, +} + +// SAFETY: Access to the `UnsafeCell` is serialized by `borrowed` and scoped to +// `with_mut`. CVSD has no exported hard-IRQ handler, so callers only use this +// from task-side queue methods. +unsafe impl Send for SharedCvsdDriverInner {} + +// SAFETY: See the `Send` impl. +unsafe impl Sync for SharedCvsdDriverInner {} + +impl SharedCvsdDriver { + fn new(driver: CvsdDriver) -> Self { + Self { + inner: Arc::new(SharedCvsdDriverInner { + driver: UnsafeCell::new(driver), + borrowed: AtomicBool::new(false), + }), + } + } + + fn with_mut(&self, f: impl FnOnce(&mut CvsdDriver) -> R) -> R { + let mut guard = self.inner.enter(); + f(guard.get_mut()) + } +} + +impl Clone for SharedCvsdDriver { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} + +impl SharedCvsdDriverInner { + fn enter(&self) -> SharedCvsdDriverGuard<'_> { + while self + .borrowed + .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) + .is_err() + { + core::hint::spin_loop(); + } + SharedCvsdDriverGuard { inner: self } + } +} + +impl SharedCvsdDriverGuard<'_> { + fn get_mut(&mut self) -> &mut CvsdDriver { + unsafe { &mut *self.inner.driver.get() } + } +} + +impl Drop for SharedCvsdDriverGuard<'_> { + fn drop(&mut self) { + self.inner.borrowed.store(false, Ordering::Release); + } +} + impl CvsdDriver { fn new(sdmmc: usize, syscon: usize) -> Result { let sdmmc = unsafe { Sdmmc::new(sdmmc, syscon) }; @@ -80,8 +156,8 @@ impl CvsdDriver { fn checked_lba(block_id: u64, offset: usize) -> Result { let lba = block_id .checked_add(offset as u64) - .ok_or(BlkError::InvalidBlockIndex(block_id as usize))?; - u32::try_from(lba).map_err(|_| BlkError::InvalidBlockIndex(block_id as usize)) + .ok_or(BlkError::InvalidBlockIndex(block_id))?; + u32::try_from(lba).map_err(|_| BlkError::InvalidBlockIndex(block_id)) } fn read_blocks(&mut self, block_id: u64, buf: &mut [u8]) -> Result<(), BlkError> { @@ -92,7 +168,7 @@ impl CvsdDriver { for (i, block) in buf.chunks_exact_mut(BLOCK_SIZE).enumerate() { self.0 .read_block(Self::checked_lba(block_id, i)?, block) - .map_err(|_| BlkError::Other("CVSD read failed".into()))?; + .map_err(|_| BlkError::Other("CVSD read failed"))?; } Ok(()) } @@ -105,24 +181,22 @@ impl CvsdDriver { for (i, block) in buf.chunks_exact(BLOCK_SIZE).enumerate() { self.0 .write_block(Self::checked_lba(block_id, i)?, block) - .map_err(|_| BlkError::Other("CVSD write failed".into()))?; + .map_err(|_| BlkError::Other("CVSD write failed"))?; } Ok(()) } } struct CvsdBlock { - inner: Arc>, + inner: SharedCvsdDriver, queue_created: bool, - irq_enabled: bool, } impl CvsdBlock { fn new(driver: CvsdDriver) -> Self { Self { - inner: Arc::new(Mutex::new(driver)), + inner: SharedCvsdDriver::new(driver), queue_created: false, - irq_enabled: false, } } } @@ -134,6 +208,30 @@ impl DriverGeneric for CvsdBlock { } impl Interface for CvsdBlock { + fn device_info(&self) -> DeviceInfo { + DeviceInfo { + name: Some(DEVICE_NAME), + ..DeviceInfo::new( + self.inner.with_mut(|driver| driver.num_blocks()), + BLOCK_SIZE, + ) + } + } + + fn queue_limits(&self) -> QueueLimits { + QueueLimits { + dma_mask: u64::MAX, + dma_alignment: 0x1000, + max_blocks_per_request: 1, + max_segments: 1, + max_segment_size: BLOCK_SIZE, + supported_flags: RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + } + } + fn create_queue(&mut self) -> Option> { if self.queue_created { return None; @@ -141,68 +239,71 @@ impl Interface for CvsdBlock { self.queue_created = true; Some(Box::new(CvsdQueue { id: 0, - inner: Arc::clone(&self.inner), + inner: self.inner.clone(), })) } - - fn enable_irq(&mut self) { - self.irq_enabled = true; - } - - fn disable_irq(&mut self) { - self.irq_enabled = false; - } - - fn is_irq_enabled(&self) -> bool { - self.irq_enabled - } - - fn handle_irq(&mut self) -> Event { - Event::none() - } } struct CvsdQueue { id: usize, - inner: Arc>, + inner: SharedCvsdDriver, } -impl IQueue for CvsdQueue { +// SAFETY: CVSD operations complete synchronously inside `submit_request`; the +// queue never retains segment pointers beyond the call. +unsafe impl IQueue for CvsdQueue { fn id(&self) -> usize { self.id } - fn num_blocks(&self) -> usize { - self.inner.lock().num_blocks() as usize - } - - fn block_size(&self) -> usize { - BLOCK_SIZE - } - - fn buff_config(&self) -> BuffConfig { - BuffConfig { - dma_mask: u64::MAX, - align: 0x1000, - size: self.block_size(), + fn info(&self) -> QueueInfo { + QueueInfo { + id: self.id, + device: DeviceInfo { + name: Some(DEVICE_NAME), + ..DeviceInfo::new( + self.inner.with_mut(|driver| driver.num_blocks()), + BLOCK_SIZE, + ) + }, + limits: QueueLimits { + dma_mask: u64::MAX, + dma_alignment: 0x1000, + max_blocks_per_request: 1, + max_segments: 1, + max_segment_size: BLOCK_SIZE, + supported_flags: RequestFlags::NONE, + supports_flush: false, + supports_discard: false, + supports_write_zeroes: false, + }, } } fn submit_request(&mut self, request: Request<'_>) -> Result { - match request.kind { - rd_block::RequestKind::Read(mut buffer) => self - .inner - .lock() - .read_blocks(request.block_id as u64, &mut buffer)?, - rd_block::RequestKind::Write(items) => self - .inner - .lock() - .write_blocks(request.block_id as u64, items)?, + validate_request(self.info(), &request)?; + match request.op { + RequestOp::Read => { + let segment = request + .segments + .first_mut() + .ok_or(BlkError::InvalidRequest)?; + self.inner + .with_mut(|driver| driver.read_blocks(request.lba, segment))?; + } + RequestOp::Write => { + let segment = request.segments.first().ok_or(BlkError::InvalidRequest)?; + self.inner + .with_mut(|driver| driver.write_blocks(request.lba, segment))?; + } + RequestOp::Flush | RequestOp::Discard | RequestOp::WriteZeroes => { + return Err(BlkError::NotSupported); + } } Ok(RequestId::new(0)) } - fn poll_request(&mut self, _request: RequestId) -> Result<(), BlkError> { - Ok(()) + fn poll_request(&mut self, _request: RequestId) -> Result { + Ok(RequestStatus::Complete) } } diff --git a/scripts/axbuild/src/axvisor/test.rs b/scripts/axbuild/src/axvisor/test.rs index 9f4f53b030..02c96f54b5 100644 --- a/scripts/axbuild/src/axvisor/test.rs +++ b/scripts/axbuild/src/axvisor/test.rs @@ -641,6 +641,7 @@ impl Axvisor { fn qemu_test_request(mut request: ResolvedAxvisorRequest) -> ResolvedAxvisorRequest { request.smp = None; + request.vmconfigs.clear(); request } @@ -736,6 +737,7 @@ fn axvisor_case_asset_config() -> test_case::CaseAssetConfig { grouped_runner: test_case::GroupedCaseRunnerConfig { runner_name: "axvisor-run-case-tests".to_string(), runner_path: "/usr/bin/axvisor-run-case-tests".to_string(), + autorun_profile_script: None, begin_marker: "AXVISOR_GROUPED_TEST_BEGIN".to_string(), passed_marker: "AXVISOR_GROUPED_TEST_PASSED".to_string(), failed_marker: "AXVISOR_GROUPED_TEST_FAILED".to_string(), @@ -948,6 +950,22 @@ mod tests { assert_eq!(request.smp, None); } + #[test] + fn qemu_test_request_ignores_inherited_vmconfigs() { + let mut request = axvisor_request( + PathBuf::from("/tmp/build-x86_64-unknown-none.toml"), + "x86_64", + "x86_64-unknown-none", + ); + request + .vmconfigs + .push(PathBuf::from("tmp/old-axvisor-vm.toml")); + + let request = Axvisor::qemu_test_request(request); + + assert!(request.vmconfigs.is_empty()); + } + #[test] fn discovers_only_cases_with_matching_qemu_config() { let root = tempdir().unwrap(); diff --git a/scripts/axbuild/src/rootfs/qemu.rs b/scripts/axbuild/src/rootfs/qemu.rs index 7d47efc0e0..9dc7ec9abb 100644 --- a/scripts/axbuild/src/rootfs/qemu.rs +++ b/scripts/axbuild/src/rootfs/qemu.rs @@ -107,6 +107,38 @@ fn drive_file_value(drive_arg: &str) -> Option<&str> { .find_map(|part| part.strip_prefix("file=")) } +fn drive_id_value(drive_arg: &str) -> Option<&str> { + drive_arg + .split(',') + .find_map(|part| part.strip_prefix("id=")) +} + +fn drive_ref_value(device_arg: &str) -> Option<&str> { + device_arg + .split(',') + .find_map(|part| part.strip_prefix("drive=")) +} + +fn replace_drive_file_arg(drive_arg: &str, rootfs_path: &Path) -> String { + let mut replaced = false; + let mut parts = drive_arg + .split(',') + .map(|part| { + if part.starts_with("file=") { + replaced = true; + format!("file={}", rootfs_path.display()) + } else { + part.to_string() + } + }) + .collect::>(); + + if !replaced { + parts.push(format!("file={}", rootfs_path.display())); + } + parts.join(",") +} + /// Replaces an existing `disk0` drive argument or inserts one next to the /// matching block-device declaration. fn replace_drive_arg(args: &mut Vec, rootfs_path: &Path) { @@ -146,6 +178,8 @@ fn ensure_disk_boot_net_args(qemu: &mut QemuConfig, disk_img: &Path) { let mut has_drive = false; let mut has_net_device = false; let mut has_netdev = false; + let mut device_drive_ids = Vec::new(); + let mut custom_rootfs_drives = Vec::new(); let mut index = 0; while index < args.len() { @@ -157,6 +191,9 @@ fn ensure_disk_boot_net_args(qemu: &mut QemuConfig, disk_img: &Path) { } else if wiring.net_device_matches(value) { has_net_device = true; } + if let Some(drive_id) = drive_ref_value(value) { + device_drive_ids.push(drive_id.to_string()); + } index += 2; } "-drive" if index + 1 < args.len() => { @@ -164,6 +201,10 @@ fn ensure_disk_boot_net_args(qemu: &mut QemuConfig, disk_img: &Path) { if value.starts_with(&drive_prefix) { *value = disk_value.clone(); has_drive = true; + } else if let (Some(drive_id), Some(_)) = + (drive_id_value(value), drive_file_value(value)) + { + custom_rootfs_drives.push((index + 1, drive_id.to_string())); } index += 2; } @@ -178,6 +219,16 @@ fn ensure_disk_boot_net_args(qemu: &mut QemuConfig, disk_img: &Path) { } } + if !has_drive + && let Some((value_index, _)) = custom_rootfs_drives + .iter() + .find(|(_, drive_id)| device_drive_ids.iter().any(|id| id == drive_id)) + { + args[*value_index] = replace_drive_file_arg(&args[*value_index], disk_img); + has_blk_device = true; + has_drive = true; + } + if !has_blk_device { args.push("-device".to_string()); args.push(wiring.default_block_device.to_string()); @@ -186,6 +237,16 @@ fn ensure_disk_boot_net_args(qemu: &mut QemuConfig, disk_img: &Path) { args.push("-drive".to_string()); args.push(disk_value); } + let has_custom_rootfs_device = has_blk_device + && device_drive_ids.iter().any(|id| { + id != wiring.disk_id + && custom_rootfs_drives + .iter() + .any(|(_, drive_id)| drive_id == id) + }); + if has_custom_rootfs_device && !has_net_device && !has_netdev { + return; + } if !has_net_device { args.push("-device".to_string()); args.push(wiring.default_net_device.to_string()); @@ -329,4 +390,64 @@ mod tests { ] ); } + + #[test] + fn ensure_disk_boot_net_accepts_custom_rootfs_drive_device() { + let rootfs = Path::new("/tmp/new-rootfs.img"); + let mut qemu = QemuConfig { + args: vec![ + "-drive".to_string(), + "id=nvm,if=none,format=raw,file=/tmp/old-rootfs.img".to_string(), + "-device".to_string(), + "nvme,serial=starry-nvme-rootfs,drive=nvm".to_string(), + "-device".to_string(), + "virtio-net-pci,netdev=net0".to_string(), + "-netdev".to_string(), + "user,id=net0".to_string(), + ], + ..Default::default() + }; + + patch_rootfs(&mut qemu, rootfs, RootfsPatchMode::EnsureDiskBootNet); + + assert_eq!( + qemu.args, + vec![ + "-drive".to_string(), + "id=nvm,if=none,format=raw,file=/tmp/new-rootfs.img".to_string(), + "-device".to_string(), + "nvme,serial=starry-nvme-rootfs,drive=nvm".to_string(), + "-device".to_string(), + "virtio-net-pci,netdev=net0".to_string(), + "-netdev".to_string(), + "user,id=net0".to_string(), + ] + ); + } + + #[test] + fn ensure_disk_boot_net_does_not_add_network_for_custom_rootfs_without_network() { + let rootfs = Path::new("/tmp/new-rootfs.img"); + let mut qemu = QemuConfig { + args: vec![ + "-drive".to_string(), + "id=nvm,if=none,format=raw,file=/tmp/old-rootfs.img".to_string(), + "-device".to_string(), + "nvme,serial=starry-nvme-rootfs,drive=nvm".to_string(), + ], + ..Default::default() + }; + + patch_rootfs(&mut qemu, rootfs, RootfsPatchMode::EnsureDiskBootNet); + + assert_eq!( + qemu.args, + vec![ + "-drive".to_string(), + "id=nvm,if=none,format=raw,file=/tmp/new-rootfs.img".to_string(), + "-device".to_string(), + "nvme,serial=starry-nvme-rootfs,drive=nvm".to_string(), + ] + ); + } } diff --git a/scripts/axbuild/src/starry/test.rs b/scripts/axbuild/src/starry/test.rs index 76795e87b4..4e3a006d2e 100644 --- a/scripts/axbuild/src/starry/test.rs +++ b/scripts/axbuild/src/starry/test.rs @@ -915,6 +915,7 @@ pub(crate) fn starry_case_asset_config() -> case::CaseAssetConfig { grouped_runner: case::GroupedCaseRunnerConfig { runner_name: "starry-run-case-tests".to_string(), runner_path: "/usr/bin/starry-run-case-tests".to_string(), + autorun_profile_script: Some("99-starry-run-case-tests.sh".to_string()), begin_marker: "STARRY_GROUPED_TEST_BEGIN".to_string(), passed_marker: "STARRY_GROUPED_TEST_PASSED".to_string(), failed_marker: "STARRY_GROUPED_TEST_FAILED".to_string(), @@ -1247,78 +1248,642 @@ mod tests { } #[test] - fn apk_curl_qemu_configs_bound_guest_network_commands() { + fn inotifywait_qemu_case_installs_tool_before_boot() { let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); - let case_dir = workspace_root.join("test-suit/starryos/normal/qemu-smp1/apk-curl"); + let case_dir = workspace_root.join("test-suit/starryos/normal/qemu-smp1/inotifywait"); + let config_path = case_dir.join("qemu-x86_64.toml"); + let cmake_path = case_dir.join("c/CMakeLists.txt"); + let script_path = case_dir.join("c/inotifywait-tests.sh"); + let prebuild_path = case_dir.join("c/prebuild.sh"); + + assert!( + script_path.is_file(), + "{} must be installed through the case C pipeline", + script_path.display() + ); + assert!( + cmake_path.is_file(), + "{} must install inotifywait assets through the host CMake install phase", + cmake_path.display() + ); + assert!( + prebuild_path.is_file(), + "{} must install inotify-tools into the staging root before CMake install", + prebuild_path.display() + ); + + let script = fs::read_to_string(&script_path).unwrap(); + for guest_apk_command in ["apk update", "apk add"] { + assert!( + !script.contains(guest_apk_command), + "{} must not run `{guest_apk_command}` after StarryOS boots", + script_path.display() + ); + } + assert!( + script.contains("command -v inotifywait"), + "{} must still exercise the inotifywait userspace tool", + script_path.display() + ); + + let prebuild = fs::read_to_string(&prebuild_path).unwrap(); + assert!( + prebuild.contains("apk add") && prebuild.contains("inotify-tools"), + "{} must install the inotify-tools package during case asset preparation", + prebuild_path.display() + ); + for host_overlay_command in ["STARRY_CASE_OVERLAY_DIR", "cp ", "chmod ", "mkdir "] { + assert!( + !prebuild.contains(host_overlay_command), + "{} must not manipulate host overlay paths from the guest prebuild shell", + prebuild_path.display() + ); + } + assert!( + prebuild.contains("STARRY_STAGING_ROOT/usr/bin/inotifywait"), + "{} must verify that apk installed the inotifywait tool in the staging root", + prebuild_path.display() + ); + + let cmake = fs::read_to_string(&cmake_path).unwrap(); + assert!( + cmake.contains("install(PROGRAMS inotifywait-tests.sh") + && cmake.contains("${STARRY_STAGING_ROOT}/usr/bin/inotifywait") + && cmake.contains("DESTINATION usr/bin"), + "{} must copy both test script and inotifywait through CMake install", + cmake_path.display() + ); + + let content = fs::read_to_string(&config_path).unwrap(); + let config: toml::Value = toml::from_str(&content).unwrap(); + let timeout = config + .get("timeout") + .and_then(toml::Value::as_integer) + .unwrap_or_default(); + assert!( + timeout <= 180, + "{} must fail quickly because apk setup happens before QEMU boot", + config_path.display() + ); + + let success_regex = config + .get("success_regex") + .and_then(toml::Value::as_array) + .unwrap(); + assert!( + success_regex + .iter() + .filter_map(toml::Value::as_str) + .any(|regex| regex.contains("INOTIFYWAIT_TEST_PASSED")), + "{} must require the inotifywait test pass marker", + config_path.display() + ); + } + + #[test] + fn procps_qemu_case_installs_tools_before_boot() { + let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let case_dir = workspace_root.join("test-suit/starryos/normal/qemu-smp1/procps"); + let cmake_path = case_dir.join("c/CMakeLists.txt"); + let script_path = case_dir.join("c/procps-test.sh"); + let prebuild_path = case_dir.join("c/prebuild.sh"); + + assert!( + script_path.is_file(), + "{} must be installed through the case C pipeline", + script_path.display() + ); + assert!( + cmake_path.is_file(), + "{} must install procps assets through the host CMake install phase", + cmake_path.display() + ); + assert!( + prebuild_path.is_file(), + "{} must install procps into the staging root before CMake install", + prebuild_path.display() + ); + assert!( + !case_dir.join("sh").exists(), + "{} must not keep the old shell pipeline that cannot prebuild packages", + case_dir.join("sh").display() + ); + + let script = fs::read_to_string(&script_path).unwrap(); + for guest_apk_command in ["apk update", "apk add", "apk info"] { + assert!( + !script.contains(guest_apk_command), + "{} must not run `{guest_apk_command}` after StarryOS boots", + script_path.display() + ); + } + assert!( + script.contains("PROCPS_TEST_PASSED") && script.contains("command -v pmap"), + "{} must still exercise the installed procps tools", + script_path.display() + ); + + let prebuild = fs::read_to_string(&prebuild_path).unwrap(); + assert!( + prebuild.contains("apk add") && prebuild.contains("procps"), + "{} must install procps during case asset preparation", + prebuild_path.display() + ); + for tool in ["ps", "free", "uptime", "pgrep", "pmap"] { + assert!( + prebuild.contains(tool), + "{} must verify that apk installed the {tool} tool in the staging root", + prebuild_path.display() + ); + } + + let cmake = fs::read_to_string(&cmake_path).unwrap(); + assert!( + cmake.contains("install(PROGRAMS procps-test.sh") + && cmake.contains("STARRY_STAGING_ROOT") + && cmake.contains("ps") + && cmake.contains("pmap"), + "{} must install both the procps test script and staging-root tools", + cmake_path.display() + ); for arch in ["aarch64", "loongarch64", "riscv64", "x86_64"] { - let path = case_dir.join(format!("qemu-{arch}.toml")); - let content = fs::read_to_string(&path).unwrap(); + let config_path = case_dir.join(format!("qemu-{arch}.toml")); + let content = fs::read_to_string(&config_path).unwrap(); let config: toml::Value = toml::from_str(&content).unwrap(); - let shell_init_cmd = config - .get("shell_init_cmd") - .and_then(toml::Value::as_str) + let timeout = config + .get("timeout") + .and_then(toml::Value::as_integer) .unwrap_or_default(); + assert!( + timeout <= 180, + "{} must fail quickly because procps setup happens before QEMU boot", + config_path.display() + ); + } + } + #[test] + fn apk_add_fs_equivalence_qemu_case_covers_package_fs_ops() { + let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let case_dir = + workspace_root.join("test-suit/starryos/normal/qemu-smp1/apk-add-fs-equivalence"); + let cmake_path = case_dir.join("c/CMakeLists.txt"); + let source_path = case_dir.join("c/src/main.c"); + + assert!( + cmake_path.is_file(), + "{} must build the filesystem equivalence probe through the C pipeline", + cmake_path.display() + ); + assert!( + source_path.is_file(), + "{} must contain the filesystem equivalence probe source", + source_path.display() + ); + assert!( + !case_dir.join("sh").exists() && !case_dir.join("python").exists(), + "{} must stay a single C pipeline case", + case_dir.display() + ); + + let source = fs::read_to_string(&source_path).unwrap(); + for forbidden in [ + "apk update", + "apk add", + "apt update", + "apt install", + "curl ", + ] { assert!( - shell_init_cmd.contains("fetch_timeout=60"), - "{} must keep apk/curl network waits short enough for CI retries", - path.display() + !source.contains(forbidden), + "{} must not depend on package managers or network clients", + source_path.display() ); - for mirror in [ - "https://mirrors.cernet.edu.cn/alpine", - "https://dl-cdn.alpinelinux.org/alpine", - ] { - assert!( - shell_init_cmd.contains(mirror), - "{} must retry apk-curl through fallback mirror {mirror}", - path.display() - ); - } + } + for forbidden in ["http://", "https://"] { assert!( - shell_init_cmd.contains("APK_CURL_REPO_"), - "{} must report which apk mirror is being tried", - path.display() + !source.contains(forbidden), + "{} must not access external network resources", + source_path.display() ); + } + for required in [ + "mkdir(", + "mkdirat(", + "stat(", + "lstat(", + "fstatat(", + "opendir(", + "readdir(", + "open(", + "O_CREAT", + "O_TRUNC", + "O_EXCL", + "write(", + "read(", + "pread(", + "pwrite(", + "payload_checksum_update(", + "rename(", + "unlink(", + "chmod(", + "fchmod(", + "chown(", + "fchown(", + "lchown(", + "truncate(", + "ftruncate(", + "utimensat(", + "symlink(", + "readlink(", + "link(", + "fsync(", + "fdatasync(", + "sync()", + "syncfs(", + "APK_ADD_FS_EQUIV_LARGE_PAYLOAD_WRITE_BYTES", + "APK_ADD_FS_EQUIV_LARGE_PAYLOAD_READ_BYTES", + "read_checksum == write_checksum", + "APK_ADD_FS_EQUIV_TEST_PASSED", + "APK_ADD_FS_EQUIV_TEST_FAILED", + ] { assert!( - shell_init_cmd - .contains("timeout \"$fetch_timeout\" apk --timeout \"$fetch_timeout\" update"), - "{} must bound apk update so CI can retry mirror stalls", - path.display() + source.contains(required), + "{} must cover `{required}`", + source_path.display() ); + } + for simulated_path in ["/usr/bin", "/usr/lib", "/lib/apk/db", "/var/lib/dpkg"] { assert!( - shell_init_cmd.contains( - "timeout \"$fetch_timeout\" apk --timeout \"$fetch_timeout\" add curl" - ), - "{} must bound apk add so CI can retry mirror stalls", - path.display() + source.contains(simulated_path), + "{} must simulate package install path `{simulated_path}`", + source_path.display() + ); + } + + let cmake = fs::read_to_string(&cmake_path).unwrap(); + assert!( + cmake.contains("project(apk-add-fs-equivalence C)") + && cmake.contains("add_executable(apk-add-fs-equivalence") + && cmake.contains("install(TARGETS apk-add-fs-equivalence") + && cmake.contains("DESTINATION usr/bin"), + "{} must install the C probe into the guest image", + cmake_path.display() + ); + + for arch in ["x86_64", "riscv64"] { + let config_path = case_dir.join(format!("qemu-{arch}.toml")); + assert!( + config_path.is_file(), + "{} must exist after local validation for {arch}", + config_path.display() ); + let content = fs::read_to_string(&config_path).unwrap(); + let config: toml::Value = toml::from_str(&content).unwrap(); + let args = config.get("args").and_then(toml::Value::as_array).unwrap(); + let args = args + .iter() + .filter_map(toml::Value::as_str) + .collect::>(); + assert!( - shell_init_cmd.contains("curl --connect-timeout 10 --max-time 30"), - "{} must bound the external curl probe", - path.display() + args.iter() + .any(|arg| arg.contains("virtio-blk-pci,drive=disk0")), + "{} must exercise virtio-blk", + config_path.display() + ); + assert!( + args.iter().any(|arg| { + arg.contains(&format!("rootfs-{arch}-alpine.img")) + && arg.contains("tmp/axbuild/rootfs") + }), + "{} must use the managed Alpine rootfs for {arch}", + config_path.display() + ); + + assert_eq!( + config + .get("shell_init_cmd") + .and_then(toml::Value::as_str) + .unwrap(), + "/usr/bin/apk-add-fs-equivalence" + ); + let success_regex = config + .get("success_regex") + .and_then(toml::Value::as_array) + .unwrap(); + assert!( + success_regex + .iter() + .filter_map(toml::Value::as_str) + .any(|regex| regex.contains("APK_ADD_FS_EQUIV_TEST_PASSED")), + "{} must require the pass marker", + config_path.display() + ); + assert!( + success_regex + .iter() + .filter_map(toml::Value::as_str) + .all(|regex| !regex.contains("APK_ADD_FS_EQUIV_LARGE_PAYLOAD")), + "{} must not use intermediate payload diagnostics as success markers", + config_path.display() + ); + let fail_regex = config + .get("fail_regex") + .and_then(toml::Value::as_array) + .unwrap(); + assert!( + fail_regex + .iter() + .filter_map(toml::Value::as_str) + .any(|regex| regex.contains("APK_ADD_FS_EQUIV_TEST_FAILED")), + "{} must fail on the probe failure marker", + config_path.display() ); let timeout = config .get("timeout") .and_then(toml::Value::as_integer) .unwrap_or_default(); assert!( - timeout <= 600, - "{} must not leave apk-curl with the old long QEMU timeout", - path.display() + timeout <= 180, + "{} must stay a focused diagnostic case", + config_path.display() + ); + } + } + + #[test] + fn apk_net_equivalence_qemu_case_covers_apk_like_network_ops() { + let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let case_dir = + workspace_root.join("test-suit/starryos/normal/qemu-smp1/apk-net-equivalence"); + let cmake_path = case_dir.join("c/CMakeLists.txt"); + let source_path = case_dir.join("c/src/main.c"); + + assert!( + cmake_path.is_file(), + "{} must build the network equivalence probe through the C pipeline", + cmake_path.display() + ); + assert!( + source_path.is_file(), + "{} must contain the network equivalence probe source", + source_path.display() + ); + assert!( + !case_dir.join("sh").exists() && !case_dir.join("python").exists(), + "{} must stay a single C pipeline case", + case_dir.display() + ); + + let source = fs::read_to_string(&source_path).unwrap(); + for forbidden in ["apk update", "apk add", "curl ", "http://", "https://"] { + assert!( + !source.contains(forbidden), + "{} must not depend on package managers, curl, or external URLs", + source_path.display() + ); + } + for required in [ + "socket(", + "bind(", + "getsockname(", + "sendto(", + "recvfrom(", + "listen(", + "accept(", + "connect(", + "send(", + "recv(", + "GET /alpine/APKINDEX.tar.gz", + "GET /alpine/main/x86_64/fake-package.apk", + "Host: apk.local", + "Content-Length:", + "APK_NET_EQUIV_TEST_PASSED", + "APK_NET_EQUIV_TEST_FAILED", + ] { + assert!( + source.contains(required), + "{} must cover `{required}`", + source_path.display() + ); + } + + let cmake = fs::read_to_string(&cmake_path).unwrap(); + assert!( + cmake.contains("project(apk-net-equivalence C)") + && cmake.contains("add_executable(apk-net-equivalence") + && cmake.contains("install(TARGETS apk-net-equivalence") + && cmake.contains("DESTINATION usr/bin"), + "{} must install the C probe into the guest image", + cmake_path.display() + ); + + for arch in ["x86_64", "riscv64"] { + let config_path = case_dir.join(format!("qemu-{arch}.toml")); + assert!( + config_path.is_file(), + "{} must exist after local validation for {arch}", + config_path.display() + ); + let content = fs::read_to_string(&config_path).unwrap(); + let config: toml::Value = toml::from_str(&content).unwrap(); + let args = config.get("args").and_then(toml::Value::as_array).unwrap(); + let args = args + .iter() + .filter_map(toml::Value::as_str) + .collect::>(); + + assert!( + args.iter() + .any(|arg| arg.contains("virtio-blk-pci,drive=disk0")), + "{} must exercise virtio-blk", + config_path.display() + ); + assert!( + args.iter() + .any(|arg| arg.contains("virtio-net-pci,netdev=net0")), + "{} must exercise virtio-net", + config_path.display() + ); + assert!( + args.iter().any(|arg| { + arg.contains(&format!("rootfs-{arch}-alpine.img")) + && arg.contains("tmp/axbuild/rootfs") + }), + "{} must use the managed Alpine rootfs for {arch}", + config_path.display() + ); + + assert_eq!( + config + .get("shell_init_cmd") + .and_then(toml::Value::as_str) + .unwrap(), + "/usr/bin/apk-net-equivalence" + ); + let success_regex = config + .get("success_regex") + .and_then(toml::Value::as_array) + .unwrap(); + assert!( + success_regex + .iter() + .filter_map(toml::Value::as_str) + .any(|regex| regex.contains("APK_NET_EQUIV_TEST_PASSED")), + "{} must require the pass marker", + config_path.display() + ); + let fail_regex = config + .get("fail_regex") + .and_then(toml::Value::as_array) + .unwrap(); + assert!( + fail_regex + .iter() + .filter_map(toml::Value::as_str) + .any(|regex| regex.contains("APK_NET_EQUIV_TEST_FAILED")), + "{} must fail on the probe failure marker", + config_path.display() + ); + let timeout = config + .get("timeout") + .and_then(toml::Value::as_integer) + .unwrap_or_default(); + assert!( + timeout <= 180, + "{} must stay a focused diagnostic case", + config_path.display() + ); + } + } + + #[test] + fn apk_curl_qemu_case_tries_cernet_before_upstream() { + let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let case_dir = workspace_root.join("test-suit/starryos/normal/qemu-smp1/apk-curl"); + + for arch in ["aarch64", "loongarch64", "riscv64", "x86_64"] { + let config_path = case_dir.join(format!("qemu-{arch}.toml")); + let content = fs::read_to_string(&config_path).unwrap(); + let config: toml::Value = toml::from_str(&content).unwrap(); + let script = config + .get("shell_init_cmd") + .and_then(toml::Value::as_str) + .unwrap(); + + assert!( + script.contains("apk --timeout \"$fetch_timeout\" add curl"), + "{} must install curl dynamically to exercise the apk add path", + config_path.display() + ); + assert!( + script.contains("mirrors.cernet.edu.cn") + && script.contains("dl-cdn.alpinelinux.org"), + "{} must provide Cernet first and upstream as a fallback", + config_path.display() + ); + let cernet_index = script.find("mirrors.cernet.edu.cn").unwrap(); + let upstream_index = script.find("dl-cdn.alpinelinux.org").unwrap(); + assert!( + cernet_index < upstream_index, + "{} must try Cernet before upstream", + config_path.display() + ); + assert!( + !script.contains("mirrors.aliyun.com") + && !script.contains("mirrors.tuna.tsinghua.edu.cn") + && !script.contains("mirrors.ustc.edu.cn"), + "{} must avoid mirrors that repeatedly timeout in QEMU", + config_path.display() + ); + assert!( + !script.contains("__original__"), + "{} must use explicit mirror attempts so the selected repository is diagnosable", + config_path.display() + ); + assert!( + script.contains("APK_CURL_REPO_$label") + && script.contains("APK_CURL_TEST_PASSED") + && script.contains("APK_CURL_TEST_FAILED"), + "{} must keep clear pass/fail diagnostics", + config_path.display() + ); + } + } + + #[test] + fn dhcp_qemu_case_checks_local_dhcp_state_without_external_apk_fetch() { + let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let build_group_dir = workspace_root.join("test-suit/starryos/normal/qemu-dhcp"); + let case_dir = workspace_root.join("test-suit/starryos/normal/qemu-dhcp/dhcp"); + + for (arch, target) in [ + ("aarch64", "aarch64-unknown-none-softfloat"), + ("loongarch64", "loongarch64-unknown-none-softfloat"), + ("riscv64", "riscv64gc-unknown-none-elf"), + ("x86_64", "x86_64-unknown-none"), + ] { + let build_config_path = build_group_dir.join(format!("build-{target}.toml")); + let build_content = fs::read_to_string(&build_config_path).unwrap(); + let build_config: toml::Value = toml::from_str(&build_content).unwrap(); + assert_eq!( + build_config.get("target").and_then(toml::Value::as_str), + Some(target), + "{} must target {target}", + build_config_path.display() + ); + assert!( + build_config + .get("env") + .and_then(toml::Value::as_table) + .is_some_and(toml::map::Map::is_empty), + "{} must leave AX_IP/AX_GW unset so Starry exercises DHCP", + build_config_path.display() + ); + + let config_path = case_dir.join(format!("qemu-{arch}.toml")); + let content = fs::read_to_string(&config_path).unwrap(); + let config: toml::Value = toml::from_str(&content).unwrap(); + let script = config + .get("shell_init_cmd") + .and_then(toml::Value::as_str) + .unwrap(); + + assert!( + !script.contains("apk update") + && !script.contains("apk --timeout") + && !script.contains("http://") + && !script.contains("https://"), + "{} must not depend on external APK repositories; DHCP is already local to the \ + QEMU user network", + config_path.display() ); for marker in [ - "APK_CURL_UPDATE_BEGIN", - "APK_CURL_UPDATE_DONE", - "APK_CURL_ADD_BEGIN", - "APK_CURL_ADD_DONE", - "APK_CURL_PROBE_BEGIN", - "APK_CURL_PROBE_DONE", + "DHCP_PROBE_BEGIN", + "DHCP_ADDR_OK", + "DHCP_RESOLVER_OK", + "DHCP_TEST_DONE", + "DHCP_TEST_FAILED", ] { assert!( - shell_init_cmd.contains(marker), - "{} must mark apk-curl progress with {marker}", - path.display() + script.contains(marker), + "{} must print the diagnostic marker `{marker}`", + config_path.display() + ); + } + for expected in [ + "ifconfig eth0", + "ip addr show", + "/etc/resolv.conf", + "10.0.2.15", + "10.0.2.3", + ] { + assert!( + script.contains(expected), + "{} must check `{expected}` in the local QEMU DHCP state", + config_path.display() ); } @@ -1330,20 +1895,124 @@ mod tests { fail_regex .iter() .filter_map(toml::Value::as_str) - .any(|regex| regex.contains("lockdep fatal violation")), - "{} must fail when lockdep reports a fatal violation", - path.display() + .any(|regex| regex.contains("DHCP_TEST_FAILED")), + "{} must fail explicitly on the DHCP probe failure marker", + config_path.display() + ); + let timeout = config + .get("timeout") + .and_then(toml::Value::as_integer) + .unwrap_or_default(); + assert!( + timeout <= 120, + "{} must stay a focused local DHCP diagnostic case", + config_path.display() + ); + } + } + + #[test] + fn lua_qemu_case_installs_lua_before_boot() { + let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let case_dir = workspace_root.join("test-suit/starryos/normal/qemu-smp1/lua"); + let cmake_path = case_dir.join("c/CMakeLists.txt"); + let script_path = case_dir.join("c/lua-app-tests.sh"); + let prebuild_path = case_dir.join("c/prebuild.sh"); + + assert!( + script_path.is_file(), + "{} must be installed through the case C pipeline", + script_path.display() + ); + assert!( + cmake_path.is_file(), + "{} must install Lua assets through the host CMake install phase", + cmake_path.display() + ); + assert!( + prebuild_path.is_file(), + "{} must install Lua packages into the staging root before QEMU boot", + prebuild_path.display() + ); + assert!( + !case_dir.join("sh").exists(), + "{} must not keep the old shell pipeline that cannot prebuild packages", + case_dir.join("sh").display() + ); + + let script = fs::read_to_string(&script_path).unwrap(); + for guest_apk_command in ["apk update", "apk add"] { + assert!( + !script.contains(guest_apk_command), + "{} must not run `{guest_apk_command}` after StarryOS boots", + script_path.display() + ); + } + assert!( + script.contains("lua5.4 /usr/bin/lua-main.lua alpha beta") + && script.contains("LUA_APP_TEST_FAILED"), + "{} must still exercise the Lua runtime and report failures", + script_path.display() + ); + + let prebuild = fs::read_to_string(&prebuild_path).unwrap(); + assert!( + prebuild.contains("apk add") + && prebuild.contains("lua5.4") + && prebuild.contains("lua5.4-cjson"), + "{} must install Lua packages during case asset preparation", + prebuild_path.display() + ); + for staged_path in [ + "STARRY_STAGING_ROOT/usr/bin/lua5.4", + "STARRY_STAGING_ROOT/usr/lib/lua/5.4/cjson.so", + ] { + assert!( + prebuild.contains(staged_path), + "{} must verify {} exists in the staging root", + prebuild_path.display(), + staged_path + ); + } + + let cmake = fs::read_to_string(&cmake_path).unwrap(); + assert!( + cmake.contains("install(PROGRAMS lua-app-tests.sh") + && cmake.contains("${STARRY_STAGING_ROOT}/usr/bin/lua5.4") + && cmake.contains("${STARRY_STAGING_ROOT}/usr/lib/lua/5.4/cjson.so"), + "{} must install the Lua interpreter, cjson module, and test scripts", + cmake_path.display() + ); + + for arch in ["aarch64", "riscv64", "x86_64"] { + let config_path = case_dir.join(format!("qemu-{arch}.toml")); + let content = fs::read_to_string(&config_path).unwrap(); + let config: toml::Value = toml::from_str(&content).unwrap(); + let timeout = config + .get("timeout") + .and_then(toml::Value::as_integer) + .unwrap_or_default(); + assert!( + timeout <= 180, + "{} must fail quickly because Lua setup happens before QEMU boot", + config_path.display() ); } } #[test] - fn bug_ext4_dir_ops_qemu_configs_fail_on_lockdep_fatal() { + fn bug_ext4_dir_ops_stays_in_bugfix_grouped_qemu_configs() { let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); - let case_dir = workspace_root.join("test-suit/starryos/normal/qemu-smp1/bugfix"); + let case_dir = + workspace_root.join("test-suit/starryos/normal/qemu-smp1/bugfix/bug-ext4-dir-ops"); + assert!( + case_dir.join("c/CMakeLists.txt").is_file(), + "{} must remain a grouped C test case", + case_dir.display() + ); for arch in ["aarch64", "loongarch64", "riscv64", "x86_64"] { - let path = case_dir.join(format!("qemu-{arch}.toml")); + let path = case_dir.parent().unwrap().join(format!("qemu-{arch}.toml")); let content = fs::read_to_string(&path).unwrap(); let config: toml::Value = toml::from_str(&content).unwrap(); let test_commands = config @@ -1367,13 +2036,173 @@ mod tests { fail_regex .iter() .filter_map(toml::Value::as_str) - .any(|regex| regex.contains("lockdep fatal violation")), - "{} must fail when lockdep reports a fatal violation", + .any(|regex| regex.contains("STARRY_GROUPED_TEST_FAILED")), + "{} must fail when a grouped bugfix command fails", path.display() ); } } + #[test] + fn zombie_bugfix_commands_are_isolated_from_large_bugfix_group() { + let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let qemu_smp1 = workspace_root.join("test-suit/starryos/normal/qemu-smp1"); + let zombie_case_dir = qemu_smp1.join("zombie-bugfix"); + let zombie_commands = [ + "/usr/bin/bug-kill-zombie-esrch", + "/usr/bin/bug-kill-zombie-perm", + "/usr/bin/bug-zombie-syscalls", + "/usr/bin/bug-waitid-basic", + ]; + + for command in zombie_commands { + let name = command.trim_start_matches("/usr/bin/"); + assert!( + zombie_case_dir + .join(name) + .join("c/CMakeLists.txt") + .is_file(), + "{} must be built in the isolated zombie-bugfix case", + command + ); + } + + for arch in ["aarch64", "loongarch64", "riscv64", "x86_64"] { + let bugfix_path = qemu_smp1.join("bugfix").join(format!("qemu-{arch}.toml")); + let bugfix_content = fs::read_to_string(&bugfix_path).unwrap(); + let bugfix_config: toml::Value = toml::from_str(&bugfix_content).unwrap(); + let bugfix_commands = bugfix_config + .get("test_commands") + .and_then(toml::Value::as_array) + .unwrap(); + for command in zombie_commands { + assert!( + !bugfix_commands + .iter() + .filter_map(toml::Value::as_str) + .any(|candidate| candidate == command), + "{} must not keep {} in the long bugfix guest session", + bugfix_path.display(), + command + ); + } + + let zombie_path = zombie_case_dir.join(format!("qemu-{arch}.toml")); + let zombie_content = fs::read_to_string(&zombie_path).unwrap(); + let zombie_config: toml::Value = toml::from_str(&zombie_content).unwrap(); + let isolated_commands = zombie_config + .get("test_commands") + .and_then(toml::Value::as_array) + .unwrap(); + for command in zombie_commands { + assert!( + isolated_commands + .iter() + .filter_map(toml::Value::as_str) + .any(|candidate| candidate == command), + "{} must include {}", + zombie_path.display(), + command + ); + } + } + } + + #[test] + fn tty_bugfix_commands_are_isolated_from_large_bugfix_group() { + let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let qemu_smp1 = workspace_root.join("test-suit/starryos/normal/qemu-smp1"); + let tty_case_dir = qemu_smp1.join("tty-bugfix"); + let tty_commands = [ + "/usr/bin/bug-raw-terminal-polling", + "/usr/bin/bug-tty-cursor-report", + ]; + + for command in tty_commands { + let name = command.trim_start_matches("/usr/bin/"); + assert!( + tty_case_dir.join(name).join("c/CMakeLists.txt").is_file(), + "{} must be built in the isolated tty-bugfix case", + command + ); + } + + for arch in ["aarch64", "loongarch64", "riscv64", "x86_64"] { + let bugfix_path = qemu_smp1.join("bugfix").join(format!("qemu-{arch}.toml")); + let bugfix_content = fs::read_to_string(&bugfix_path).unwrap(); + let bugfix_config: toml::Value = toml::from_str(&bugfix_content).unwrap(); + let bugfix_commands = bugfix_config + .get("test_commands") + .and_then(toml::Value::as_array) + .unwrap(); + for command in tty_commands { + assert!( + !bugfix_commands + .iter() + .filter_map(toml::Value::as_str) + .any(|candidate| candidate == command), + "{} must not keep {} in the long bugfix guest session", + bugfix_path.display(), + command + ); + } + + let tty_path = tty_case_dir.join(format!("qemu-{arch}.toml")); + let tty_content = fs::read_to_string(&tty_path).unwrap(); + let tty_config: toml::Value = toml::from_str(&tty_content).unwrap(); + let isolated_commands = tty_config + .get("test_commands") + .and_then(toml::Value::as_array) + .unwrap(); + for command in tty_commands { + assert!( + isolated_commands + .iter() + .filter_map(toml::Value::as_str) + .any(|candidate| candidate == command), + "{} must include {}", + tty_path.display(), + command + ); + } + } + } + + #[test] + fn busybox_guest_script_reports_case_start_and_bounds_nologin() { + let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let script_path = + workspace_root.join("test-suit/starryos/normal/qemu-smp1/busybox/sh/busybox-tests.sh"); + let script = fs::read_to_string(&script_path).unwrap(); + + assert!( + script.contains("echo \"START: $BB_CASE_NAME\""), + "{} must print case start markers so CI timeout logs identify the hanging BusyBox \ + applet", + script_path.display() + ); + assert!( + script.contains("timeout 2 busybox nologin"), + "{} must run nologin in the foreground under a timeout", + script_path.display() + ); + assert!( + !script.contains("busybox nologin >/tmp/bb_nologin.out 2>&1 &"), + "{} must not leave the nologin probe as a background child", + script_path.display() + ); + } + + #[test] + fn starry_grouped_cases_install_profile_autorun() { + let config = starry_case_asset_config(); + + assert_eq!( + config.grouped_runner.autorun_profile_script.as_deref(), + Some("99-starry-run-case-tests.sh") + ); + } + fn prepared_qemu_case(name: &str, build_config_path: PathBuf) -> PreparedStarryQemuCase { PreparedStarryQemuCase { case: TestQemuCase { @@ -1457,6 +2286,45 @@ mod tests { ); } + #[test] + fn grouped_case_skips_arch_specific_subcases_for_other_arches() { + let root = tempdir().unwrap(); + write_qemu_build_config( + root.path(), + "normal", + "default", + "riscv64gc-unknown-none-elf", + ); + write_grouped_qemu_test_config(root.path(), "normal", "default", "syscall", "riscv64"); + + let case_dir = root + .path() + .join("test-suit/starryos/normal/default/syscall"); + fs::create_dir_all(case_dir.join("alpha/c")).unwrap(); + fs::create_dir_all(case_dir.join("x86-only/c")).unwrap(); + fs::write(case_dir.join("x86-only/qemu-x86_64.toml"), "timeout = 1\n").unwrap(); + + let cases = discover_qemu_cases( + root.path(), + "riscv64", + "riscv64gc-unknown-none-elf", + None, + "normal", + ) + .unwrap(); + + assert_eq!(cases.len(), 1); + assert_eq!( + cases[0] + .case + .subcases + .iter() + .map(|subcase| subcase.name.as_str()) + .collect::>(), + vec!["alpha"] + ); + } + #[test] fn grouped_case_loads_with_both_shell_init_cmd_and_test_commands_present() { // The mutual-exclusion check has been moved from the initial TOML parse diff --git a/scripts/axbuild/src/support/git.rs b/scripts/axbuild/src/support/git.rs index 3d2f4fd0db..6c462e6b8b 100644 --- a/scripts/axbuild/src/support/git.rs +++ b/scripts/axbuild/src/support/git.rs @@ -1,11 +1,17 @@ use std::{ collections::{BTreeMap, BTreeSet}, + fs, path::{Path, PathBuf}, process::Command, }; use anyhow::{Context, bail}; use cargo_metadata::{Metadata, Package, PackageId}; +use toml::Value; + +const ROOT_MANIFEST: &str = "Cargo.toml"; +const WORKSPACE_TABLE: &str = "workspace"; +const WORKSPACE_DEPENDENCIES_TABLE: &str = "dependencies"; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum IncrementalPackageSelection { @@ -27,11 +33,21 @@ pub(crate) fn select_incremental_packages( }); } }; - select_incremental_packages_for_paths( + let root_manifest_change = if changed_paths + .iter() + .any(|path| path == Path::new(ROOT_MANIFEST)) + { + Some(root_manifest_change_since(workspace_root, since)?) + } else { + None + }; + + select_incremental_packages_for_paths_with_root_manifest_change( workspace_root, metadata, workspace_packages, changed_paths, + root_manifest_change, ) } @@ -111,27 +127,48 @@ fn git_safe_directory_args(workspace_root: &Path) -> [String; 2] { ] } -pub(crate) fn select_incremental_packages_for_paths( +#[cfg(test)] +fn select_incremental_packages_for_paths( + workspace_root: &Path, + metadata: &Metadata, + workspace_packages: &[Package], + changed_paths: I, +) -> anyhow::Result +where + I: IntoIterator, +{ + select_incremental_packages_for_paths_with_root_manifest_change( + workspace_root, + metadata, + workspace_packages, + changed_paths, + None, + ) +} + +fn select_incremental_packages_for_paths_with_root_manifest_change( workspace_root: &Path, metadata: &Metadata, workspace_packages: &[Package], changed_paths: I, + root_manifest_change: Option, ) -> anyhow::Result where I: IntoIterator, { let package_index = PackagePathIndex::new(workspace_root, workspace_packages)?; - let changed_packages = match package_index.changed_packages(changed_paths)? { - ChangedPackages::Packages(packages) => packages, - ChangedPackages::Full { path } => { - return Ok(IncrementalPackageSelection::Full { - reason: format!( - "changed path `{}` is outside any workspace package", - path.display() - ), - }); - } - }; + let changed_packages = + match package_index.changed_packages(changed_paths, root_manifest_change)? { + ChangedPackages::Packages(packages) => packages, + ChangedPackages::Full { path } => { + return Ok(IncrementalPackageSelection::Full { + reason: format!( + "changed path `{}` is outside any workspace package", + path.display() + ), + }); + } + }; let affected = affected_workspace_packages(metadata, workspace_packages, &changed_packages); Ok(IncrementalPackageSelection::Packages(affected)) @@ -147,6 +184,12 @@ enum GlobalClippyInput { Soft, } +#[derive(Debug, Clone, PartialEq, Eq)] +enum RootManifestChange { + Hard, + LocalWorkspaceDependencies(BTreeSet), +} + struct PackagePathIndex { packages: Vec, } @@ -206,7 +249,11 @@ impl PackagePathIndex { Ok(Self { packages }) } - fn changed_packages(&self, changed_paths: I) -> anyhow::Result + fn changed_packages( + &self, + changed_paths: I, + root_manifest_change: Option, + ) -> anyhow::Result where I: IntoIterator, { @@ -217,6 +264,19 @@ impl PackagePathIndex { if path.as_os_str().is_empty() { continue; } + if path == Path::new(ROOT_MANIFEST) { + match root_manifest_change + .clone() + .unwrap_or(RootManifestChange::Hard) + { + RootManifestChange::Hard => return Ok(ChangedPackages::Full { path }), + RootManifestChange::LocalWorkspaceDependencies(dependencies) => { + packages.extend(dependencies); + soft_global_inputs.push(path); + } + } + continue; + } let Some(package) = self.package_for_path(&path) else { match global_clippy_input(&path) { Some(GlobalClippyInput::Hard) => return Ok(ChangedPackages::Full { path }), @@ -250,26 +310,125 @@ fn global_clippy_input(path: &Path) -> Option { // *only* change, however, transitive-dep/proc-macro/build-script changes // can still break compilation, so fall back to Full in that case. Some(GlobalClippyInput::Soft) - } else if path == Path::new("Cargo.toml") - || path == Path::new("rust-toolchain") + } else if path == Path::new("rust-toolchain") || path == Path::new("rust-toolchain.toml") || path == Path::new("clippy.toml") || path == Path::new(".clippy.toml") || path.starts_with(".cargo") || path.starts_with("os/arceos/configs") { - // Hard: root Cargo.toml is not limited to [workspace.members]; it also - // carries [workspace.dependencies], [workspace.package], [patch], and - // [profile] sections. A workspace-dep bump alongside any code change - // would otherwise leave all other consumers unchecked. We cannot - // distinguish "only added a member" from "bumped a global dep" without - // parsing diff hunks, so Hard is the only sound choice here. Some(GlobalClippyInput::Hard) } else { None } } +fn root_manifest_change_since( + workspace_root: &Path, + since: &str, +) -> anyhow::Result { + let old_manifest = git_show_file(workspace_root, since, ROOT_MANIFEST).with_context(|| { + format!("failed to read `{ROOT_MANIFEST}` from `{since}` for incremental clippy") + })?; + let new_manifest = + fs::read_to_string(workspace_root.join(ROOT_MANIFEST)).with_context(|| { + format!( + "failed to read current `{}`", + workspace_root.join(ROOT_MANIFEST).display() + ) + })?; + + classify_root_manifest_change(&old_manifest, &new_manifest).with_context(|| { + format!("failed to classify `{ROOT_MANIFEST}` changes for incremental clippy") + }) +} + +fn git_show_file(workspace_root: &Path, rev: &str, path: &str) -> anyhow::Result { + let spec = format!("{rev}:{path}"); + let output = Command::new("git") + .args(git_safe_directory_args(workspace_root)) + .arg("-C") + .arg(workspace_root) + .args(["show", spec.as_str()]) + .output() + .with_context(|| format!("failed to run git show `{spec}`"))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + bail!( + "git show `{spec}` exited with status {}{}", + output.status, + if stderr.is_empty() { + String::new() + } else { + format!(": {stderr}") + } + ); + } + + String::from_utf8(output.stdout).context("git show output was not valid UTF-8") +} + +fn classify_root_manifest_change( + old_manifest: &str, + new_manifest: &str, +) -> anyhow::Result { + let old: Value = toml::from_str(old_manifest).context("failed to parse old root Cargo.toml")?; + let new: Value = toml::from_str(new_manifest).context("failed to parse new root Cargo.toml")?; + + if without_workspace_dependencies(old.clone()) != without_workspace_dependencies(new.clone()) { + return Ok(RootManifestChange::Hard); + } + + let old_dependencies = workspace_dependencies(&old); + let new_dependencies = workspace_dependencies(&new); + let mut changed = BTreeSet::new(); + for dependency in old_dependencies + .keys() + .chain(new_dependencies.keys()) + .collect::>() + { + let old_dependency = old_dependencies.get(dependency.as_str()); + let new_dependency = new_dependencies.get(dependency.as_str()); + if old_dependency == new_dependency { + continue; + } + if !old_dependency.is_some_and(is_local_workspace_dependency) + && !new_dependency.is_some_and(is_local_workspace_dependency) + { + return Ok(RootManifestChange::Hard); + } + changed.insert(dependency.to_string()); + } + + Ok(RootManifestChange::LocalWorkspaceDependencies(changed)) +} + +fn without_workspace_dependencies(mut manifest: Value) -> Value { + if let Some(workspace) = manifest + .get_mut(WORKSPACE_TABLE) + .and_then(Value::as_table_mut) + { + workspace.remove(WORKSPACE_DEPENDENCIES_TABLE); + } + manifest +} + +fn workspace_dependencies(manifest: &Value) -> toml::Table { + manifest + .get(WORKSPACE_TABLE) + .and_then(|workspace| workspace.get(WORKSPACE_DEPENDENCIES_TABLE)) + .and_then(Value::as_table) + .cloned() + .unwrap_or_default() +} + +fn is_local_workspace_dependency(dependency: &Value) -> bool { + dependency + .as_table() + .and_then(|table| table.get("path")) + .is_some_and(Value::is_str) +} + fn normalize_git_path(path: PathBuf) -> anyhow::Result { if path.is_absolute() { bail!( @@ -605,6 +764,77 @@ mod tests { )); } + #[test] + fn root_cargo_toml_workspace_dependency_change_keeps_incremental_package_selection() { + let (root, metadata, workspace_packages) = test_workspace(); + let selected = select_incremental_packages_for_paths_with_root_manifest_change( + root.path(), + &metadata, + &workspace_packages, + [ + PathBuf::from("Cargo.toml"), + PathBuf::from("crates/beta/Cargo.toml"), + ], + Some(RootManifestChange::LocalWorkspaceDependencies( + BTreeSet::from(["beta".to_string()]), + )), + ) + .unwrap(); + + assert_eq!( + selected, + IncrementalPackageSelection::Packages(vec!["beta".into(), "gamma".into()]) + ); + } + + #[test] + fn root_manifest_classifier_accepts_local_workspace_dependency_removal() { + let old_manifest = r#" + [workspace] + members = ["crates/alpha"] + + [workspace.dependencies] + alpha = { version = "0.1.0", path = "crates/alpha" } + beta = { version = "0.1.0", path = "crates/beta" } + "#; + let new_manifest = r#" + [workspace] + members = ["crates/alpha"] + + [workspace.dependencies] + alpha = { version = "0.1.0", path = "crates/alpha" } + "#; + + let change = classify_root_manifest_change(old_manifest, new_manifest).unwrap(); + + assert_eq!( + change, + RootManifestChange::LocalWorkspaceDependencies(BTreeSet::from(["beta".to_string()])) + ); + } + + #[test] + fn root_manifest_classifier_keeps_external_dependency_changes_hard() { + let old_manifest = r#" + [workspace] + members = ["crates/alpha"] + + [workspace.dependencies] + anyhow = "1.0" + "#; + let new_manifest = r#" + [workspace] + members = ["crates/alpha"] + + [workspace.dependencies] + anyhow = "2.0" + "#; + + let change = classify_root_manifest_change(old_manifest, new_manifest).unwrap(); + + assert_eq!(change, RootManifestChange::Hard); + } + #[test] fn global_config_file_falls_back_to_full_run() { let (root, metadata, workspace_packages) = test_workspace(); diff --git a/scripts/axbuild/src/test/build.rs b/scripts/axbuild/src/test/build.rs index 23ec75a500..e634a8588b 100644 --- a/scripts/axbuild/src/test/build.rs +++ b/scripts/axbuild/src/test/build.rs @@ -1428,6 +1428,7 @@ mod tests { grouped_runner: case_assets::GroupedCaseRunnerConfig { runner_name: "suite-run-case-tests".to_string(), runner_path: "/usr/bin/suite-run-case-tests".to_string(), + autorun_profile_script: None, begin_marker: "SUITE_GROUPED_TEST_BEGIN".to_string(), passed_marker: "SUITE_GROUPED_TEST_PASSED".to_string(), failed_marker: "SUITE_GROUPED_TEST_FAILED".to_string(), diff --git a/scripts/axbuild/src/test/case.rs b/scripts/axbuild/src/test/case.rs index fa846ac08a..678ed6dcc4 100644 --- a/scripts/axbuild/src/test/case.rs +++ b/scripts/axbuild/src/test/case.rs @@ -77,6 +77,7 @@ pub(crate) struct TestQemuSubcase { pub(crate) struct GroupedCaseRunnerConfig { pub(crate) runner_name: String, pub(crate) runner_path: String, + pub(crate) autorun_profile_script: Option, pub(crate) begin_marker: String, pub(crate) passed_marker: String, pub(crate) failed_marker: String, @@ -594,6 +595,9 @@ fn case_asset_cache_key( if pipeline == CasePipeline::Rust { hash_token(&mut hasher, RUST_PIPELINE_CACHE_VERSION); } + if pipeline == CasePipeline::Grouped { + hash_grouped_runner_config(&mut hasher, &config.grouped_runner); + } hash_rootfs_fingerprint(&mut hasher, shared_rootfs)?; hash_tree(&mut hasher, &case.case_dir)?; @@ -604,6 +608,25 @@ fn case_asset_cache_key( Ok(format!("{:x}", hasher.finalize())) } +fn hash_grouped_runner_config(hasher: &mut Sha256, config: &GroupedCaseRunnerConfig) { + hash_token(hasher, &config.runner_name); + hash_token(hasher, &config.runner_path); + match &config.autorun_profile_script { + Some(script_name) => { + hash_token(hasher, "autorun_profile_script"); + hash_token(hasher, script_name); + } + None => hash_token(hasher, "no_autorun_profile_script"), + } + hash_token(hasher, &config.begin_marker); + hash_token(hasher, &config.passed_marker); + hash_token(hasher, &config.failed_marker); + hash_token(hasher, &config.all_passed_marker); + hash_token(hasher, &config.all_failed_marker); + hash_token(hasher, &config.success_regex); + hash_token(hasher, &config.fail_regex); +} + fn hash_tree(hasher: &mut Sha256, root: &Path) -> anyhow::Result<()> { let mut files = WalkDir::new(root) .follow_links(false) @@ -697,7 +720,7 @@ pub(crate) fn apply_grouped_qemu_config( return; } - qemu.shell_init_cmd = Some(config.runner_path.clone()); + qemu.shell_init_cmd = Some(grouped_runner_shell_init_cmd(config)); qemu.success_regex = vec![config.success_regex.clone()]; if !qemu .fail_regex @@ -708,6 +731,15 @@ pub(crate) fn apply_grouped_qemu_config( } } +fn grouped_runner_shell_init_cmd(config: &GroupedCaseRunnerConfig) -> String { + let runner = shell_single_quote(&config.runner_path); + if config.autorun_profile_script.is_some() { + format!(r#"[ "${{AXBUILD_GROUPED_AUTORUN_DONE:-0}}" = "1" ] || {runner}"#) + } else { + config.runner_path.clone() + } +} + pub(crate) async fn run_qemu_with_prepared_case_assets( app: &mut AppContext, cargo: &Cargo, @@ -777,7 +809,37 @@ pub(crate) fn write_grouped_case_runner_script( '%s\\n' {all_failed}\nexit 1\n" )); - write_executable_script(&runner_path, &body) + write_executable_script(&runner_path, &body)?; + if let Some(script_name) = &config.autorun_profile_script { + write_grouped_case_autorun_profile_script(overlay_dir, script_name, &config.runner_path)?; + } + Ok(()) +} + +fn write_grouped_case_autorun_profile_script( + overlay_dir: &Path, + script_name: &str, + runner_path: &str, +) -> anyhow::Result<()> { + ensure!( + !script_name.is_empty() && !script_name.contains('/') && script_name.ends_with(".sh"), + "invalid grouped qemu autorun profile script name `{script_name}`" + ); + + let dest_dir = overlay_dir.join("etc/profile.d"); + fs::create_dir_all(&dest_dir) + .with_context(|| format!("failed to create {}", dest_dir.display()))?; + let script_path = dest_dir.join(script_name); + let runner = shell_single_quote(runner_path); + let body = format!( + "case \"$-\" in\n\t*i*) ;;\n\t*) return 0 2>/dev/null || exit 0 ;;\nesac\n\nif [ \ + \"${{AXBUILD_GROUPED_AUTORUN_DONE:-0}}\" = \"1\" ]; then\n\treturn 0 2>/dev/null || exit \ + 0\nfi\nexport AXBUILD_GROUPED_AUTORUN_DONE=1\n\nif [ -x {runner} ]; \ + then\n\t{runner}\nfi\n" + ); + fs::write(&script_path, body) + .with_context(|| format!("failed to write {}", script_path.display()))?; + make_executable(&script_path) } /// Prepares overlay assets for a shell-based QEMU test case. @@ -907,6 +969,7 @@ mod tests { grouped_runner: GroupedCaseRunnerConfig { runner_name: "suite-run-case-tests".to_string(), runner_path: "/usr/bin/suite-run-case-tests".to_string(), + autorun_profile_script: None, begin_marker: "SUITE_GROUPED_TEST_BEGIN".to_string(), passed_marker: "SUITE_GROUPED_TEST_PASSED".to_string(), failed_marker: "SUITE_GROUPED_TEST_FAILED".to_string(), @@ -1002,6 +1065,71 @@ mod tests { assert!(content.contains("SUITE_GROUPED_TESTS_PASSED")); } + #[test] + fn grouped_runner_can_install_interactive_profile_autorun() { + let root = tempdir().unwrap(); + let overlay = root.path().join("overlay"); + let commands = vec!["/usr/bin/alpha".to_string()]; + let mut config = fake_config(); + config.grouped_runner.autorun_profile_script = Some("99-suite-run-case-tests.sh".into()); + + write_grouped_case_runner_script(&overlay, &commands, &config.grouped_runner).unwrap(); + + let profile = overlay.join("etc/profile.d/99-suite-run-case-tests.sh"); + let content = fs::read_to_string(&profile).unwrap(); + assert!(content.contains("case \"$-\" in")); + assert!(content.contains("AXBUILD_GROUPED_AUTORUN_DONE")); + assert!(content.contains("/usr/bin/suite-run-case-tests")); + assert!(!content.contains("set -u")); + } + + #[test] + fn grouped_runner_shell_init_skips_when_profile_autorun_already_ran() { + let mut config = fake_config(); + config.grouped_runner.autorun_profile_script = Some("99-suite-run-case-tests.sh".into()); + let mut qemu = QemuConfig::default(); + let mut case = fake_case(tempdir().unwrap().path(), "grouped"); + case.test_commands = vec!["/usr/bin/alpha".to_string()]; + + apply_grouped_qemu_config(&mut qemu, &case, &config.grouped_runner); + + let command = qemu.shell_init_cmd.as_deref().unwrap(); + assert!(command.contains("AXBUILD_GROUPED_AUTORUN_DONE")); + assert!(command.contains("/usr/bin/suite-run-case-tests")); + } + + #[test] + fn grouped_cache_key_tracks_runner_autorun_config() { + let root = tempdir().unwrap(); + let shared_img = root.path().join("rootfs.img"); + fs::write(&shared_img, b"rootfs").unwrap(); + let case = fake_case(root.path(), "grouped"); + let mut config = fake_config(); + + let without_autorun = case_asset_cache_key( + "x86_64", + "x86_64-unknown-none", + CasePipeline::Grouped, + &case, + &shared_img, + &config, + ) + .unwrap(); + + config.grouped_runner.autorun_profile_script = Some("99-suite-run-case-tests.sh".into()); + let with_autorun = case_asset_cache_key( + "x86_64", + "x86_64-unknown-none", + CasePipeline::Grouped, + &case, + &shared_img, + &config, + ) + .unwrap(); + + assert_ne!(without_autorun, with_autorun); + } + #[test] fn save_rootfs_cache_image_is_noop_in_ci() { let _lock = ENV_LOCK.lock().unwrap(); diff --git a/scripts/axbuild/src/test/qemu.rs b/scripts/axbuild/src/test/qemu.rs index b82f3f42a9..30da134432 100644 --- a/scripts/axbuild/src/test/qemu.rs +++ b/scripts/axbuild/src/test/qemu.rs @@ -715,7 +715,11 @@ pub(crate) fn load_test_qemu_case_fields( ) -> anyhow::Result { let test_commands = load_qemu_case_test_commands(&qemu_config_path, suite_name)?; let subcases = if discover_subcases && !test_commands.is_empty() { - discover_qemu_subcases(&case_dir)? + let arch = qemu_config_path + .file_stem() + .and_then(|stem| stem.to_str()) + .and_then(|stem| stem.strip_prefix("qemu-")); + discover_qemu_subcases(&case_dir, arch)? } else { Vec::new() }; @@ -740,7 +744,10 @@ fn load_qemu_case_test_commands( normalize_qemu_test_commands(qemu_config_path, config.test_commands, suite_name) } -fn discover_qemu_subcases(case_dir: &Path) -> anyhow::Result> { +fn discover_qemu_subcases( + case_dir: &Path, + arch: Option<&str>, +) -> anyhow::Result> { let mut subcases = Vec::new(); for entry in fs::read_dir(case_dir).with_context(|| format!("failed to read {}", case_dir.display()))? @@ -751,6 +758,13 @@ fn discover_qemu_subcases(case_dir: &Path) -> anyhow::Result bool { - for _ in 0..200_000 { - if DONE.load(Ordering::Acquire) { - return true; +fn wake_sleep_queue_after_waiter_enqueued() { + for _ in 0..WAITER_ENQUEUE_RETRIES { + let woke_waiter = Cell::new(false); + // Publish GO only while waking an actual waiter, so an early wake cannot + // satisfy the condition before the sleeper blocks on SLEEP_WQ. + api::ax_wait_queue_wake_one_with(&SLEEP_WQ, |task_id| { + if task_id != 0 { + GO.store(true, Ordering::Release); + woke_waiter.set(true); + } + }); + if woke_waiter.get() { + return; } - spin_loop(); + thread::yield_now(); } - false + panic!("sleeper did not enter wait queue"); } #[cfg(all(feature = "ax-std", target_arch = "aarch64"))] @@ -106,17 +118,19 @@ fn run_remote_wakeup_test() { api::ax_wait_queue_wait_until(&READY_WQ, || READY.load(Ordering::Acquire), None); assert_eq!(SLEEPER_CPU.load(Ordering::Acquire), sleeper_cpu); - // Give the sleeper a stable chance to enter the wait queue before the wake. - thread::sleep(Duration::from_millis(10)); assert_eq!(this_cpu_id(), waker_cpu); - GO.store(true, Ordering::Release); - api::ax_wait_queue_wake(&SLEEP_WQ, 1); + wake_sleep_queue_after_waiter_enqueued(); + // Block instead of spinning; single-threaded TCG can otherwise let the + // waker consume the emulation window that the remote CPU needs to run. assert!( - wait_for_fast_progress(), - "remote wait-queue wakeup did not make prompt progress" + !api::ax_wait_queue_wait_until( + &DONE_WQ, + || DONE.load(Ordering::Acquire), + Some(Duration::from_millis(5)), + ), + "remote wait-queue wakeup did not make bounded progress" ); - api::ax_wait_queue_wait_until(&DONE_WQ, || DONE.load(Ordering::Acquire), None); sleeper.join().unwrap(); println!("wait_queue_remote_wake: test OK!"); diff --git a/test-suit/starryos/apps/qemu-nvme-smp1/build-aarch64-unknown-none-softfloat.toml b/test-suit/starryos/apps/qemu-nvme-smp1/build-aarch64-unknown-none-softfloat.toml new file mode 100644 index 0000000000..4fb4ab9607 --- /dev/null +++ b/test-suit/starryos/apps/qemu-nvme-smp1/build-aarch64-unknown-none-softfloat.toml @@ -0,0 +1,12 @@ +env = {} +features = [ + "ax-hal/aarch64-qemu-virt", + "qemu", + "ax-driver/nvme", + "ax-driver/virtio-gpu", + "ax-driver/virtio-input", + "ax-driver/virtio-socket", +] +log = "Warn" +plat_dyn = false +target = "aarch64-unknown-none-softfloat" diff --git a/test-suit/starryos/apps/qemu-nvme-smp1/build-loongarch64-unknown-none-softfloat.toml b/test-suit/starryos/apps/qemu-nvme-smp1/build-loongarch64-unknown-none-softfloat.toml new file mode 100644 index 0000000000..42ab29c4a7 --- /dev/null +++ b/test-suit/starryos/apps/qemu-nvme-smp1/build-loongarch64-unknown-none-softfloat.toml @@ -0,0 +1,12 @@ +target = "loongarch64-unknown-none-softfloat" +env = {} +log = "Warn" +features = [ + "ax-hal/loongarch64-qemu-virt", + "qemu", + "ax-driver/nvme", + "ax-driver/virtio-gpu", + "ax-driver/virtio-input", + "ax-driver/virtio-socket", +] +plat_dyn = false diff --git a/test-suit/starryos/apps/qemu-nvme-smp1/build-riscv64gc-unknown-none-elf.toml b/test-suit/starryos/apps/qemu-nvme-smp1/build-riscv64gc-unknown-none-elf.toml new file mode 100644 index 0000000000..2a7a42977e --- /dev/null +++ b/test-suit/starryos/apps/qemu-nvme-smp1/build-riscv64gc-unknown-none-elf.toml @@ -0,0 +1,12 @@ +env = {} +features = [ + "ax-hal/riscv64-qemu-virt", + "qemu", + "ax-driver/nvme", + "ax-driver/virtio-gpu", + "ax-driver/virtio-input", + "ax-driver/virtio-socket", +] +log = "Warn" +plat_dyn = false +target = "riscv64gc-unknown-none-elf" diff --git a/test-suit/starryos/apps/qemu-nvme-smp1/build-x86_64-unknown-none.toml b/test-suit/starryos/apps/qemu-nvme-smp1/build-x86_64-unknown-none.toml new file mode 100644 index 0000000000..9c149a8b29 --- /dev/null +++ b/test-suit/starryos/apps/qemu-nvme-smp1/build-x86_64-unknown-none.toml @@ -0,0 +1,12 @@ +target = "x86_64-unknown-none" +env = {} +log = "Warn" +features = [ + "ax-hal/x86-pc", + "qemu", + "ax-driver/nvme", + "ax-driver/virtio-gpu", + "ax-driver/virtio-input", + "ax-driver/virtio-socket", +] +plat_dyn = false diff --git a/test-suit/starryos/apps/qemu-nvme-smp1/nvme-rootfs-rw-20m/qemu-aarch64.toml b/test-suit/starryos/apps/qemu-nvme-smp1/nvme-rootfs-rw-20m/qemu-aarch64.toml new file mode 100644 index 0000000000..c19ae0c738 --- /dev/null +++ b/test-suit/starryos/apps/qemu-nvme-smp1/nvme-rootfs-rw-20m/qemu-aarch64.toml @@ -0,0 +1,35 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "cortex-a53", + "-drive", + "id=nvm,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", + "nvme,serial=starry-nvme-rootfs,drive=nvm", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = ''' +fail() { + echo 'NVME_ROOTFS_RW_20M_TEST_FAILED' + exit 1 +} +mkdir -p /root/nvme-rootfs-rw-20m +src=/root/nvme-rootfs-rw-20m/source.bin +copy=/root/nvme-rootfs-rw-20m/copy.bin +(yes 'nvme-rdif-block-submit-poll-0123456789abcdef' | head -c 20971520 > "$src") || fail +sync || fail +cp "$src" "$copy" || fail +sync || fail +size=$(wc -c < "$copy" | tr -d ' ') +[ "$size" = "20971520" ] || fail +hash=$(sha256sum "$copy" | awk '{print $1}') +[ "$hash" = "2c05845aa65c70c952edbcf0838d340ec5552bafba6dd8571b245a090e891b85" ] || fail +echo 'NVME_ROOTFS_RW_20M_TEST_PASSED' +''' +success_regex = ["(?m)^NVME_ROOTFS_RW_20M_TEST_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NVME_ROOTFS_RW_20M_TEST_FAILED\\s*$"] +timeout = 300 diff --git a/test-suit/starryos/apps/qemu-nvme-smp1/nvme-rootfs-rw-20m/qemu-loongarch64.toml b/test-suit/starryos/apps/qemu-nvme-smp1/nvme-rootfs-rw-20m/qemu-loongarch64.toml new file mode 100644 index 0000000000..5432794245 --- /dev/null +++ b/test-suit/starryos/apps/qemu-nvme-smp1/nvme-rootfs-rw-20m/qemu-loongarch64.toml @@ -0,0 +1,37 @@ +args = [ + "-machine", + "virt", + "-cpu", + "la464", + "-nographic", + "-m", + "512M", + "-drive", + "id=nvm,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-loongarch64-alpine.img", + "-device", + "nvme,serial=starry-nvme-rootfs,drive=nvm", +] +to_bin = true +uefi = false +shell_prefix = "root@starry:" +shell_init_cmd = ''' +fail() { + echo 'NVME_ROOTFS_RW_20M_TEST_FAILED' + exit 1 +} +mkdir -p /root/nvme-rootfs-rw-20m +src=/root/nvme-rootfs-rw-20m/source.bin +copy=/root/nvme-rootfs-rw-20m/copy.bin +(yes 'nvme-rdif-block-submit-poll-0123456789abcdef' | head -c 20971520 > "$src") || fail +sync || fail +cp "$src" "$copy" || fail +sync || fail +size=$(wc -c < "$copy" | tr -d ' ') +[ "$size" = "20971520" ] || fail +hash=$(sha256sum "$copy" | awk '{print $1}') +[ "$hash" = "2c05845aa65c70c952edbcf0838d340ec5552bafba6dd8571b245a090e891b85" ] || fail +echo 'NVME_ROOTFS_RW_20M_TEST_PASSED' +''' +success_regex = ["(?m)^NVME_ROOTFS_RW_20M_TEST_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NVME_ROOTFS_RW_20M_TEST_FAILED\\s*$"] +timeout = 300 diff --git a/test-suit/starryos/apps/qemu-nvme-smp1/nvme-rootfs-rw-20m/qemu-riscv64.toml b/test-suit/starryos/apps/qemu-nvme-smp1/nvme-rootfs-rw-20m/qemu-riscv64.toml new file mode 100644 index 0000000000..10d264e0a4 --- /dev/null +++ b/test-suit/starryos/apps/qemu-nvme-smp1/nvme-rootfs-rw-20m/qemu-riscv64.toml @@ -0,0 +1,35 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "rv64", + "-drive", + "id=nvm,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", + "-device", + "nvme,serial=starry-nvme-rootfs,drive=nvm", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = ''' +fail() { + echo 'NVME_ROOTFS_RW_20M_TEST_FAILED' + exit 1 +} +mkdir -p /root/nvme-rootfs-rw-20m +src=/root/nvme-rootfs-rw-20m/source.bin +copy=/root/nvme-rootfs-rw-20m/copy.bin +(yes 'nvme-rdif-block-submit-poll-0123456789abcdef' | head -c 20971520 > "$src") || fail +sync || fail +cp "$src" "$copy" || fail +sync || fail +size=$(wc -c < "$copy" | tr -d ' ') +[ "$size" = "20971520" ] || fail +hash=$(sha256sum "$copy" | awk '{print $1}') +[ "$hash" = "2c05845aa65c70c952edbcf0838d340ec5552bafba6dd8571b245a090e891b85" ] || fail +echo 'NVME_ROOTFS_RW_20M_TEST_PASSED' +''' +success_regex = ["(?m)^NVME_ROOTFS_RW_20M_TEST_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NVME_ROOTFS_RW_20M_TEST_FAILED\\s*$"] +timeout = 300 diff --git a/test-suit/starryos/apps/qemu-nvme-smp1/nvme-rootfs-rw-20m/qemu-x86_64.toml b/test-suit/starryos/apps/qemu-nvme-smp1/nvme-rootfs-rw-20m/qemu-x86_64.toml new file mode 100644 index 0000000000..5c4902295a --- /dev/null +++ b/test-suit/starryos/apps/qemu-nvme-smp1/nvme-rootfs-rw-20m/qemu-x86_64.toml @@ -0,0 +1,35 @@ +args = [ + "-machine", + "q35", + "-nographic", + "-m", + "512M", + "-drive", + "id=nvm,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", + "nvme,serial=starry-nvme-rootfs,drive=nvm", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = ''' +fail() { + echo 'NVME_ROOTFS_RW_20M_TEST_FAILED' + exit 1 +} +mkdir -p /root/nvme-rootfs-rw-20m +src=/root/nvme-rootfs-rw-20m/source.bin +copy=/root/nvme-rootfs-rw-20m/copy.bin +(yes 'nvme-rdif-block-submit-poll-0123456789abcdef' | head -c 20971520 > "$src") || fail +sync || fail +cp "$src" "$copy" || fail +sync || fail +size=$(wc -c < "$copy" | tr -d ' ') +[ "$size" = "20971520" ] || fail +hash=$(sha256sum "$copy" | awk '{print $1}') +[ "$hash" = "2c05845aa65c70c952edbcf0838d340ec5552bafba6dd8571b245a090e891b85" ] || fail +echo 'NVME_ROOTFS_RW_20M_TEST_PASSED' +''' +success_regex = ["(?m)^NVME_ROOTFS_RW_20M_TEST_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NVME_ROOTFS_RW_20M_TEST_FAILED\\s*$"] +timeout = 300 diff --git a/test-suit/starryos/normal/qemu-dhcp/build-aarch64-unknown-none-softfloat.toml b/test-suit/starryos/normal/qemu-dhcp/build-aarch64-unknown-none-softfloat.toml new file mode 100644 index 0000000000..24f737731e --- /dev/null +++ b/test-suit/starryos/normal/qemu-dhcp/build-aarch64-unknown-none-softfloat.toml @@ -0,0 +1,16 @@ +env = {} +features = [ + "ax-feat/display", + "ax-feat/rtc", + "ax-driver/rtc", + "ax-driver/virtio-blk", + "ax-driver/virtio-net", + "ax-driver/virtio-gpu", + "ax-driver/virtio-input", + "ax-driver/virtio-socket", + "starry-kernel/input", + "starry-kernel/vsock", +] +log = "Warn" +plat_dyn = true +target = "aarch64-unknown-none-softfloat" diff --git a/test-suit/starryos/normal/qemu-dhcp/build-loongarch64-unknown-none-softfloat.toml b/test-suit/starryos/normal/qemu-dhcp/build-loongarch64-unknown-none-softfloat.toml new file mode 100644 index 0000000000..664004de9a --- /dev/null +++ b/test-suit/starryos/normal/qemu-dhcp/build-loongarch64-unknown-none-softfloat.toml @@ -0,0 +1,14 @@ +target = "loongarch64-unknown-none-softfloat" +env = {} +log = "Warn" +features = [ + "ax-hal/loongarch64-qemu-virt", + "qemu", + "ax-driver/plat-static", + "ax-driver/virtio-blk", + "ax-driver/virtio-net", + "ax-driver/virtio-gpu", + "ax-driver/virtio-input", + "ax-driver/virtio-socket", +] +plat_dyn = false diff --git a/test-suit/starryos/normal/qemu-dhcp/build-riscv64gc-unknown-none-elf.toml b/test-suit/starryos/normal/qemu-dhcp/build-riscv64gc-unknown-none-elf.toml new file mode 100644 index 0000000000..79c06f9aa5 --- /dev/null +++ b/test-suit/starryos/normal/qemu-dhcp/build-riscv64gc-unknown-none-elf.toml @@ -0,0 +1,15 @@ +env = {} +features = [ + "ax-feat/rtc", + "ax-driver/rtc", + "ax-driver/serial", + "ax-driver/virtio-blk", + "ax-driver/virtio-net", + "ax-driver/virtio-gpu", + "ax-driver/virtio-input", + "ax-driver/virtio-socket", + "starry-kernel/input", +] +log = "Warn" +plat_dyn = true +target = "riscv64gc-unknown-none-elf" diff --git a/test-suit/starryos/normal/qemu-dhcp/dhcp/qemu-aarch64.toml b/test-suit/starryos/normal/qemu-dhcp/dhcp/qemu-aarch64.toml new file mode 100644 index 0000000000..9f7655f754 --- /dev/null +++ b/test-suit/starryos/normal/qemu-dhcp/dhcp/qemu-aarch64.toml @@ -0,0 +1,46 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "cortex-a53", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = ''' +echo 'DHCP_PROBE_BEGIN' + +ifconfig_out="$(ifconfig eth0 2>&1 || true)" +echo "$ifconfig_out" +ip_addr_out="$(ip addr show eth0 2>&1 || true)" +echo "$ip_addr_out" +if echo "$ifconfig_out" "$ip_addr_out" | grep -qF '10.0.2.15'; then + echo 'DHCP_ADDR_OK' +else + echo 'DHCP_TEST_FAILED' + exit 1 +fi + +resolver_out="$(cat /etc/resolv.conf 2>&1 || true)" +echo "$resolver_out" +if echo "$resolver_out" | grep -qF '10.0.2.3'; then + echo 'DHCP_RESOLVER_OK' +else + echo 'DHCP_TEST_FAILED' + exit 1 +fi + +echo 'DHCP_TEST_DONE' +''' +success_regex = ["(?m)^DHCP_TEST_DONE\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", "(?m)^DHCP_TEST_FAILED\\s*$"] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-dhcp/dhcp/qemu-loongarch64.toml b/test-suit/starryos/normal/qemu-dhcp/dhcp/qemu-loongarch64.toml new file mode 100644 index 0000000000..9bb45d9aee --- /dev/null +++ b/test-suit/starryos/normal/qemu-dhcp/dhcp/qemu-loongarch64.toml @@ -0,0 +1,48 @@ +args = [ + "-machine", + "virt", + "-cpu", + "la464", + "-nographic", + "-m", + "512M", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-loongarch64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = ''' +echo 'DHCP_PROBE_BEGIN' + +ifconfig_out="$(ifconfig eth0 2>&1 || true)" +echo "$ifconfig_out" +ip_addr_out="$(ip addr show eth0 2>&1 || true)" +echo "$ip_addr_out" +if echo "$ifconfig_out" "$ip_addr_out" | grep -qF '10.0.2.15'; then + echo 'DHCP_ADDR_OK' +else + echo 'DHCP_TEST_FAILED' + exit 1 +fi + +resolver_out="$(cat /etc/resolv.conf 2>&1 || true)" +echo "$resolver_out" +if echo "$resolver_out" | grep -qF '10.0.2.3'; then + echo 'DHCP_RESOLVER_OK' +else + echo 'DHCP_TEST_FAILED' + exit 1 +fi + +echo 'DHCP_TEST_DONE' +''' +success_regex = ["(?m)^DHCP_TEST_DONE\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", "(?m)^DHCP_TEST_FAILED\\s*$"] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-dhcp/dhcp/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-dhcp/dhcp/qemu-riscv64.toml new file mode 100644 index 0000000000..dd6ef48932 --- /dev/null +++ b/test-suit/starryos/normal/qemu-dhcp/dhcp/qemu-riscv64.toml @@ -0,0 +1,46 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "rv64", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = ''' +echo 'DHCP_PROBE_BEGIN' + +ifconfig_out="$(ifconfig eth0 2>&1 || true)" +echo "$ifconfig_out" +ip_addr_out="$(ip addr show eth0 2>&1 || true)" +echo "$ip_addr_out" +if echo "$ifconfig_out" "$ip_addr_out" | grep -qF '10.0.2.15'; then + echo 'DHCP_ADDR_OK' +else + echo 'DHCP_TEST_FAILED' + exit 1 +fi + +resolver_out="$(cat /etc/resolv.conf 2>&1 || true)" +echo "$resolver_out" +if echo "$resolver_out" | grep -qF '10.0.2.3'; then + echo 'DHCP_RESOLVER_OK' +else + echo 'DHCP_TEST_FAILED' + exit 1 +fi + +echo 'DHCP_TEST_DONE' +''' +success_regex = ["(?m)^DHCP_TEST_DONE\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", "(?m)^DHCP_TEST_FAILED\\s*$"] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-dhcp/dhcp/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-dhcp/dhcp/qemu-x86_64.toml index 8c595ffabb..209722febd 100644 --- a/test-suit/starryos/normal/qemu-dhcp/dhcp/qemu-x86_64.toml +++ b/test-suit/starryos/normal/qemu-dhcp/dhcp/qemu-x86_64.toml @@ -15,10 +15,30 @@ uefi = false to_bin = false shell_prefix = "root@starry:" shell_init_cmd = ''' -apk update && \ -echo 'DHCP_TEST_DONE' || \ -echo 'DHCP_TEST_FAILED' +echo 'DHCP_PROBE_BEGIN' + +ifconfig_out="$(ifconfig eth0 2>&1 || true)" +echo "$ifconfig_out" +ip_addr_out="$(ip addr show eth0 2>&1 || true)" +echo "$ip_addr_out" +if echo "$ifconfig_out" "$ip_addr_out" | grep -qF '10.0.2.15'; then + echo 'DHCP_ADDR_OK' +else + echo 'DHCP_TEST_FAILED' + exit 1 +fi + +resolver_out="$(cat /etc/resolv.conf 2>&1 || true)" +echo "$resolver_out" +if echo "$resolver_out" | grep -qF '10.0.2.3'; then + echo 'DHCP_RESOLVER_OK' +else + echo 'DHCP_TEST_FAILED' + exit 1 +fi + +echo 'DHCP_TEST_DONE' ''' success_regex = ["(?m)^DHCP_TEST_DONE\\s*$"] -fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^DHCP_TEST_FAILED\\s*$"] -timeout = 300 +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", "(?m)^DHCP_TEST_FAILED\\s*$"] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/apk-add-fs-equivalence/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/apk-add-fs-equivalence/c/CMakeLists.txt new file mode 100644 index 0000000000..eb43d4103e --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/apk-add-fs-equivalence/c/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.20) + +project(apk-add-fs-equivalence C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +add_executable(apk-add-fs-equivalence src/main.c) +target_compile_options(apk-add-fs-equivalence PRIVATE -Wall -Wextra -Werror) + +install(TARGETS apk-add-fs-equivalence RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/apk-add-fs-equivalence/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/apk-add-fs-equivalence/c/src/main.c new file mode 100644 index 0000000000..426a066fe1 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/apk-add-fs-equivalence/c/src/main.c @@ -0,0 +1,507 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BASE_ROOT "/root" +#define TEST_NAME "apk-add-fs-equivalence" +#define APK_PAYLOAD_CHUNK_SIZE 16384 +#define APK_PAYLOAD_CHUNKS 128 +#define APK_PAYLOAD_BYTES (APK_PAYLOAD_CHUNK_SIZE * APK_PAYLOAD_CHUNKS) +#define APK_PAYLOAD_BUDGET_MS 30000 + +static void fail_at(const char *file, int line, const char *message) +{ + printf("APK_ADD_FS_EQUIV_TEST_FAILED: %s:%d: %s (errno=%d %s)\n", + file, line, message, errno, strerror(errno)); + fflush(stdout); + exit(1); +} + +#define CHECK(cond, message) \ + do { \ + if (!(cond)) { \ + fail_at(__FILE__, __LINE__, (message)); \ + } \ + } while (0) + +#define CHECK_CHOWN(call, message) \ + do { \ + if ((call) != 0) { \ + int saved_errno = errno; \ + if (!(saved_errno == EPERM && geteuid() != 0)) { \ + errno = saved_errno; \ + fail_at(__FILE__, __LINE__, (message)); \ + } \ + } \ + } while (0) + +static void path_join(char *out, size_t out_len, const char *base, const char *rel) +{ + int n = snprintf(out, out_len, "%s/%s", base, rel); + CHECK(n > 0 && (size_t)n < out_len, "path too long"); +} + +static void make_dir(const char *path, mode_t mode) +{ + if (mkdir(path, mode) == 0) { + return; + } + CHECK(errno == EEXIST, "mkdir failed"); +} + +static void make_child_dir(const char *base, const char *rel, mode_t mode) +{ + char path[PATH_MAX]; + path_join(path, sizeof(path), base, rel); + make_dir(path, mode); +} + +static void write_all(int fd, const void *buf, size_t len) +{ + const char *p = (const char *)buf; + while (len > 0) { + ssize_t n = write(fd, p, len); + CHECK(n > 0, "write failed"); + p += n; + len -= (size_t)n; + } +} + +static void pwrite_all(int fd, const void *buf, size_t len, off_t offset) +{ + const char *p = (const char *)buf; + while (len > 0) { + ssize_t n = pwrite(fd, p, len, offset); + CHECK(n > 0, "pwrite failed"); + p += n; + offset += n; + len -= (size_t)n; + } +} + +static void read_exact_at(int fd, void *buf, size_t len, off_t offset) +{ + char *p = (char *)buf; + while (len > 0) { + ssize_t n = pread(fd, p, len, offset); + CHECK(n > 0, "pread failed"); + p += n; + offset += n; + len -= (size_t)n; + } +} + +static long monotonic_ms(void) +{ + struct timespec ts; + CHECK(clock_gettime(CLOCK_MONOTONIC, &ts) == 0, "clock_gettime failed"); + return ts.tv_sec * 1000L + ts.tv_nsec / 1000000L; +} + +static void read_file(const char *path, char *buf, size_t buf_len) +{ + int fd = open(path, O_RDONLY); + CHECK(fd >= 0, "open for read failed"); + ssize_t n = read(fd, buf, buf_len - 1); + CHECK(n >= 0, "read failed"); + buf[n] = '\0'; + CHECK(close(fd) == 0, "close after read failed"); +} + +static void write_file_atomic(const char *tmp_path, const char *final_path, + const char *content, mode_t mode) +{ + int fd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC, mode); + CHECK(fd >= 0, "open temp file failed"); + write_all(fd, content, strlen(content)); + CHECK(fchmod(fd, mode) == 0, "fchmod temp file failed"); + CHECK_CHOWN(fchown(fd, geteuid(), getegid()), "fchown temp file failed"); + CHECK(fdatasync(fd) == 0, "fdatasync temp file failed"); + CHECK(fsync(fd) == 0, "fsync temp file failed"); + CHECK(close(fd) == 0, "close temp file failed"); + CHECK(chmod(tmp_path, mode) == 0, "chmod temp file failed"); + CHECK_CHOWN(chown(tmp_path, geteuid(), getegid()), "chown temp file failed"); + + struct timespec times[2] = { + {.tv_sec = 1700000000, .tv_nsec = 0}, + {.tv_sec = 1700000001, .tv_nsec = 0}, + }; + CHECK(utimensat(AT_FDCWD, tmp_path, times, 0) == 0, "utimensat temp file failed"); + CHECK(rename(tmp_path, final_path) == 0, "rename temp to final failed"); + CHECK(access(tmp_path, F_OK) != 0 && errno == ENOENT, "renamed temp file still exists"); +} + +static void verify_file_content(const char *path, const char *expected) +{ + char buf[512]; + memset(buf, 0, sizeof(buf)); + read_file(path, buf, sizeof(buf)); + CHECK(strcmp(buf, expected) == 0, "file content mismatch"); +} + +static void rm_rf(const char *path) +{ + struct stat st; + if (lstat(path, &st) != 0) { + return; + } + + if (S_ISDIR(st.st_mode)) { + DIR *dir = opendir(path); + if (dir) { + struct dirent *ent; + while ((ent = readdir(dir)) != NULL) { + if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) { + continue; + } + char child[PATH_MAX]; + path_join(child, sizeof(child), path, ent->d_name); + rm_rf(child); + } + closedir(dir); + } + rmdir(path); + } else { + unlink(path); + } +} + +static int count_regular_entries(const char *path) +{ + DIR *dir = opendir(path); + CHECK(dir != NULL, "opendir failed"); + + int count = 0; + struct dirent *ent; + while ((ent = readdir(dir)) != NULL) { + if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) { + continue; + } + count++; + } + CHECK(closedir(dir) == 0, "closedir failed"); + return count; +} + +static void create_package_tree(const char *base) +{ + make_dir(base, 0755); + int basefd = open(base, O_RDONLY | O_DIRECTORY); + CHECK(basefd >= 0, "open base dir failed"); + CHECK(mkdirat(basefd, "usr", 0755) == 0, "mkdirat usr failed"); + CHECK(fstatat(basefd, "usr", &(struct stat){0}, 0) == 0, "fstatat usr failed"); + CHECK(close(basefd) == 0, "close base dir failed"); + + make_child_dir(base, "usr/bin", 0755); + make_child_dir(base, "usr/lib", 0755); + make_child_dir(base, "usr/share", 0755); + make_child_dir(base, "usr/share/doc", 0755); + make_child_dir(base, "usr/share/doc/pkg", 0755); + make_child_dir(base, "lib", 0755); + make_child_dir(base, "lib/apk", 0755); + make_child_dir(base, "lib/apk/db", 0755); + make_child_dir(base, "var", 0755); + make_child_dir(base, "var/cache", 0755); + make_child_dir(base, "var/cache/apk", 0755); + make_child_dir(base, "var/lib", 0755); + make_child_dir(base, "var/lib/dpkg", 0755); + make_child_dir(base, "var/lib/dpkg/info", 0755); + make_child_dir(base, "etc", 0755); + make_child_dir(base, "etc/apk", 0755); +} + +static void test_lock_file(const char *base) +{ + char lock[PATH_MAX]; + path_join(lock, sizeof(lock), base, "lib/apk/db/lock"); + + int fd = open(lock, O_RDWR | O_CREAT | O_EXCL, 0600); + CHECK(fd >= 0, "create exclusive lock file failed"); + write_all(fd, "locked\n", 7); + CHECK(fsync(fd) == 0, "fsync lock failed"); + + int second = open(lock, O_RDWR | O_CREAT | O_EXCL, 0600); + CHECK(second < 0 && errno == EEXIST, "O_EXCL lock did not reject existing file"); + CHECK(close(fd) == 0, "close lock failed"); +} + +static void test_payload_replace(const char *base) +{ + char tmp[PATH_MAX], final[PATH_MAX], link_path[PATH_MAX], hard_path[PATH_MAX]; + path_join(tmp, sizeof(tmp), base, "usr/bin/pkg-tool.apk-new"); + path_join(final, sizeof(final), base, "usr/bin/pkg-tool"); + path_join(link_path, sizeof(link_path), base, "usr/bin/pkg-tool-link"); + path_join(hard_path, sizeof(hard_path), base, "usr/bin/pkg-tool-hardlink"); + + write_file_atomic(tmp, final, "#!/bin/sh\necho package-fs\n", 0755); + verify_file_content(final, "#!/bin/sh\necho package-fs\n"); + + struct stat st; + CHECK(stat(final, &st) == 0, "stat final payload failed"); + CHECK((st.st_mode & 0777) == 0755, "payload mode mismatch"); + CHECK(st.st_uid == geteuid() && st.st_gid == getegid(), "payload owner mismatch"); + CHECK(st.st_mtime == 1700000001, "payload mtime mismatch"); + + CHECK(symlink("pkg-tool", link_path) == 0, "symlink failed"); + char target[64]; + ssize_t n = readlink(link_path, target, sizeof(target) - 1); + CHECK(n > 0, "readlink failed"); + target[n] = '\0'; + CHECK(strcmp(target, "pkg-tool") == 0, "symlink target mismatch"); + CHECK(lstat(link_path, &st) == 0 && S_ISLNK(st.st_mode), "lstat symlink failed"); + CHECK_CHOWN(lchown(link_path, geteuid(), getegid()), "lchown symlink failed"); + + CHECK(link(final, hard_path) == 0, "hard link failed"); + CHECK(stat(hard_path, &st) == 0, "stat hard link failed"); + CHECK(st.st_nlink >= 2, "hard link count mismatch"); + verify_file_content(hard_path, "#!/bin/sh\necho package-fs\n"); +} + +static void test_database_rewrite(const char *base) +{ + char tmp[PATH_MAX], installed[PATH_MAX], status[PATH_MAX], status_tmp[PATH_MAX]; + path_join(tmp, sizeof(tmp), base, "lib/apk/db/installed.new"); + path_join(installed, sizeof(installed), base, "lib/apk/db/installed"); + path_join(status, sizeof(status), base, "var/lib/dpkg/status"); + path_join(status_tmp, sizeof(status_tmp), base, "var/lib/dpkg/status.dpkg-new"); + + write_file_atomic(tmp, installed, + "P:pkg-tool\nV:1.0-r0\nF:usr/bin/pkg-tool\n", 0644); + verify_file_content(installed, "P:pkg-tool\nV:1.0-r0\nF:usr/bin/pkg-tool\n"); + + write_file_atomic(tmp, installed, + "P:pkg-tool\nV:1.1-r0\nF:usr/bin/pkg-tool\n", 0644); + verify_file_content(installed, "P:pkg-tool\nV:1.1-r0\nF:usr/bin/pkg-tool\n"); + + int fd = open(status, O_WRONLY | O_CREAT | O_TRUNC, 0644); + CHECK(fd >= 0, "open dpkg status failed"); + const char *tool_status = "Package: pkg-tool\nStatus: install ok installed\n\n"; + write_all(fd, tool_status, strlen(tool_status)); + CHECK(close(fd) == 0, "close dpkg status failed"); + + fd = open(status, O_WRONLY | O_APPEND); + CHECK(fd >= 0, "open dpkg status append failed"); + const char *lib_status = "Package: pkg-lib\nStatus: install ok installed\n\n"; + write_all(fd, lib_status, strlen(lib_status)); + CHECK(close(fd) == 0, "close dpkg status append failed"); + + CHECK(truncate(status, (off_t)strlen(tool_status)) == 0, "truncate dpkg status failed"); + verify_file_content(status, tool_status); + + write_file_atomic(status_tmp, status, + "Package: pkg-tool\nStatus: install ok unpacked\n\n", 0644); + verify_file_content(status, "Package: pkg-tool\nStatus: install ok unpacked\n\n"); +} + +static void test_sparse_library_io(const char *base) +{ + char path[PATH_MAX]; + path_join(path, sizeof(path), base, "usr/lib/libpkgpayload.so"); + + int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644); + CHECK(fd >= 0, "open library payload failed"); + const char *head = "ELF-package-head"; + const char *tail = "ELF-package-tail"; + pwrite_all(fd, head, strlen(head), 0); + pwrite_all(fd, tail, strlen(tail), 4096); + CHECK(ftruncate(fd, 8192) == 0, "ftruncate grow failed"); + CHECK(fdatasync(fd) == 0, "fdatasync library failed"); + CHECK(fsync(fd) == 0, "fsync library failed"); + + char buf[64]; + memset(buf, 0, sizeof(buf)); + read_exact_at(fd, buf, strlen(head), 0); + CHECK(strcmp(buf, head) == 0, "pread head mismatch"); + memset(buf, 0, sizeof(buf)); + read_exact_at(fd, buf, strlen(tail), 4096); + CHECK(strcmp(buf, tail) == 0, "pread tail mismatch"); + + CHECK(ftruncate(fd, 64) == 0, "ftruncate shrink failed"); + struct stat st; + CHECK(fstat(fd, &st) == 0, "fstat library failed"); + CHECK(st.st_size == 64, "library shrink size mismatch"); + CHECK(close(fd) == 0, "close library failed"); +} + +static void test_many_small_files(const char *base) +{ + char dir[PATH_MAX]; + path_join(dir, sizeof(dir), base, "usr/share/doc/pkg"); + + for (int i = 0; i < 64; i++) { + char path[PATH_MAX]; + char data[128]; + int n = snprintf(path, sizeof(path), "%s/file-%02d.txt", dir, i); + CHECK(n > 0 && (size_t)n < sizeof(path), "small file path too long"); + n = snprintf(data, sizeof(data), "payload-file-%02d\n", i); + CHECK(n > 0 && (size_t)n < sizeof(data), "small file data too long"); + + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + CHECK(fd >= 0, "open small file failed"); + write_all(fd, data, strlen(data)); + if (i % 8 == 0) { + CHECK(fdatasync(fd) == 0, "fdatasync small file failed"); + } + CHECK(close(fd) == 0, "close small file failed"); + + char readback[128]; + memset(readback, 0, sizeof(readback)); + read_file(path, readback, sizeof(readback)); + CHECK(strcmp(readback, data) == 0, "small file content mismatch"); + } + + CHECK(count_regular_entries(dir) == 64, "readdir small file count mismatch"); +} + +static unsigned char payload_byte(size_t chunk, size_t offset) +{ + return (unsigned char)((chunk * 31 + offset * 17 + 0x5a) & 0xff); +} + +static void fill_payload_chunk(unsigned char *buf, size_t chunk) +{ + for (size_t i = 0; i < APK_PAYLOAD_CHUNK_SIZE; i++) { + buf[i] = payload_byte(chunk, i); + } +} + +static void verify_payload_chunk(const unsigned char *buf, size_t chunk) +{ + for (size_t i = 0; i < APK_PAYLOAD_CHUNK_SIZE; i++) { + CHECK(buf[i] == payload_byte(chunk, i), "large payload content mismatch"); + } +} + +static unsigned long long payload_checksum_update(unsigned long long checksum, + const unsigned char *buf, + size_t len) +{ + for (size_t i = 0; i < len; i++) { + checksum = (checksum * 1315423911ULL) ^ (unsigned long long)buf[i] ^ i; + } + return checksum; +} + +static void test_large_apk_payload_io(const char *base) +{ + char tmp[PATH_MAX], final[PATH_MAX]; + unsigned char buf[APK_PAYLOAD_CHUNK_SIZE]; + size_t written = 0; + size_t readback = 0; + unsigned long long write_checksum = 0; + unsigned long long read_checksum = 0; + + path_join(tmp, sizeof(tmp), base, "var/cache/apk/large-package.apk-new"); + path_join(final, sizeof(final), base, "usr/lib/large-package.dat"); + + long start_ms = monotonic_ms(); + int fd = open(tmp, O_RDWR | O_CREAT | O_TRUNC, 0644); + CHECK(fd >= 0, "open large apk payload failed"); + + for (size_t chunk = 0; chunk < APK_PAYLOAD_CHUNKS; chunk++) { + fill_payload_chunk(buf, chunk); + write_checksum = payload_checksum_update(write_checksum, buf, sizeof(buf)); + write_all(fd, buf, sizeof(buf)); + written += sizeof(buf); + } + CHECK(fdatasync(fd) == 0, "fdatasync large apk payload failed"); + CHECK(fsync(fd) == 0, "fsync large apk payload failed"); + CHECK(close(fd) == 0, "close large apk payload failed"); + CHECK(rename(tmp, final) == 0, "rename large apk payload failed"); + + fd = open(final, O_RDONLY); + CHECK(fd >= 0, "open large apk payload readback failed"); + for (size_t chunk = 0; chunk < APK_PAYLOAD_CHUNKS; chunk++) { + read_exact_at(fd, buf, sizeof(buf), (off_t)(chunk * APK_PAYLOAD_CHUNK_SIZE)); + verify_payload_chunk(buf, chunk); + read_checksum = payload_checksum_update(read_checksum, buf, sizeof(buf)); + readback += sizeof(buf); + } + CHECK(close(fd) == 0, "close large apk payload readback failed"); + CHECK(written == APK_PAYLOAD_BYTES, "large payload written byte count mismatch"); + CHECK(readback == APK_PAYLOAD_BYTES, "large payload readback byte count mismatch"); + CHECK(readback == written, "large payload readback length differs from written length"); + CHECK(read_checksum == write_checksum, "large payload checksum mismatch"); + + long elapsed_ms = monotonic_ms() - start_ms; + printf("APK_ADD_FS_EQUIV_LARGE_PAYLOAD_WRITE_BYTES=%zu CHECKSUM=%llu\n", + written, write_checksum); + printf("APK_ADD_FS_EQUIV_LARGE_PAYLOAD_READ_BYTES=%zu CHECKSUM=%llu\n", + readback, read_checksum); + printf("APK_ADD_FS_EQUIV_LARGE_PAYLOAD_MS=%ld\n", elapsed_ms); + CHECK(elapsed_ms < APK_PAYLOAD_BUDGET_MS, "large apk payload I/O exceeded budget"); +} + +static void test_cross_directory_rename_and_cleanup(const char *base) +{ + char tmp[PATH_MAX], final[PATH_MAX], empty_dir[PATH_MAX], apk_db[PATH_MAX]; + path_join(tmp, sizeof(tmp), base, "var/cache/apk/pkg-tool.meta.tmp"); + path_join(final, sizeof(final), base, "usr/lib/pkg-tool.meta"); + path_join(empty_dir, sizeof(empty_dir), base, "var/cache/apk/empty"); + path_join(apk_db, sizeof(apk_db), base, "lib/apk/db"); + + int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644); + CHECK(fd >= 0, "open cross-dir temp failed"); + write_all(fd, "cached package metadata\n", 24); + CHECK(fsync(fd) == 0, "fsync cross-dir temp failed"); + CHECK(close(fd) == 0, "close cross-dir temp failed"); + CHECK(rename(tmp, final) == 0, "cross-directory rename failed"); + verify_file_content(final, "cached package metadata\n"); + + make_dir(empty_dir, 0755); + CHECK(rmdir(empty_dir) == 0, "rmdir empty cache dir failed"); + CHECK(unlink(final) == 0, "unlink renamed metadata failed"); + + int dirfd = open(apk_db, O_RDONLY | O_DIRECTORY); + CHECK(dirfd >= 0, "open apk db dir failed"); + CHECK(fsync(dirfd) == 0, "fsync apk db dir failed"); + CHECK(fdatasync(dirfd) == 0, "fdatasync apk db dir failed"); + CHECK(syncfs(dirfd) == 0, "syncfs apk db dir failed"); + CHECK(close(dirfd) == 0, "close apk db dir failed"); + sync(); +} + +int main(void) +{ + const char *base_root = getenv("STARRY_APK_ADD_FS_BASE"); + if (base_root == NULL || base_root[0] == '\0') { + base_root = BASE_ROOT; + } + const char *guest_paths[] = { + "/usr/bin", + "/usr/lib", + "/lib/apk/db", + "/var/lib/dpkg", + }; + for (size_t i = 0; i < sizeof(guest_paths) / sizeof(guest_paths[0]); i++) { + printf("simulate package path: %s\n", guest_paths[i]); + } + + char base[PATH_MAX]; + int n = snprintf(base, sizeof(base), "%s/%s-%ld", base_root, TEST_NAME, (long)getpid()); + CHECK(n > 0 && (size_t)n < sizeof(base), "base path too long"); + + rm_rf(base); + create_package_tree(base); + test_lock_file(base); + test_payload_replace(base); + test_database_rewrite(base); + test_sparse_library_io(base); + test_many_small_files(base); + test_large_apk_payload_io(base); + test_cross_directory_rename_and_cleanup(base); + rm_rf(base); + + printf("APK_ADD_FS_EQUIV_TEST_PASSED\n"); + fflush(stdout); + return 0; +} diff --git a/test-suit/starryos/normal/qemu-smp1/apk-add-fs-equivalence/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/apk-add-fs-equivalence/qemu-riscv64.toml new file mode 100644 index 0000000000..493671ce90 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/apk-add-fs-equivalence/qemu-riscv64.toml @@ -0,0 +1,22 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "rv64", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/apk-add-fs-equivalence" +success_regex = ["(?m)^APK_ADD_FS_EQUIV_TEST_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^APK_ADD_FS_EQUIV_TEST_FAILED:"] +timeout = 180 diff --git a/test-suit/starryos/normal/qemu-smp1/apk-add-fs-equivalence/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/apk-add-fs-equivalence/qemu-x86_64.toml new file mode 100644 index 0000000000..c27af3339b --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/apk-add-fs-equivalence/qemu-x86_64.toml @@ -0,0 +1,20 @@ +args = [ + "-nographic", + "-m", + "512M", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/apk-add-fs-equivalence" +success_regex = ["(?m)^APK_ADD_FS_EQUIV_TEST_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^APK_ADD_FS_EQUIV_TEST_FAILED:"] +timeout = 180 diff --git a/test-suit/starryos/normal/qemu-smp1/apk-net-equivalence/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/apk-net-equivalence/c/CMakeLists.txt new file mode 100644 index 0000000000..216cc45efc --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/apk-net-equivalence/c/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.20) + +project(apk-net-equivalence C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +add_executable(apk-net-equivalence src/main.c) +target_compile_options(apk-net-equivalence PRIVATE -Wall -Wextra -Werror) + +install(TARGETS apk-net-equivalence RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/apk-net-equivalence/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/apk-net-equivalence/c/src/main.c new file mode 100644 index 0000000000..134b60233e --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/apk-net-equivalence/c/src/main.c @@ -0,0 +1,272 @@ +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DNS_QUERY "apk.local A dl-cdn.alpinelinux.org" +#define DNS_REPLY "apk.local A 127.0.0.1" +#define INDEX_PATH "/alpine/APKINDEX.tar.gz" +#define PACKAGE_PATH "/alpine/main/x86_64/fake-package.apk" +#define INDEX_REQUEST_LINE "GET /alpine/APKINDEX.tar.gz HTTP/1.1" +#define PACKAGE_REQUEST_LINE "GET /alpine/main/x86_64/fake-package.apk HTTP/1.1" +#define INDEX_BODY "P:fake-package\nV:1.0-r0\nA:x86_64\n" +#define PACKAGE_BODY "fake apk payload\n" + +static void fail_at(const char *file, int line, const char *message) +{ + printf("APK_NET_EQUIV_TEST_FAILED: %s:%d: %s (errno=%d %s)\n", + file, line, message, errno, strerror(errno)); + fflush(stdout); + exit(1); +} + +#define CHECK(cond, message) \ + do { \ + if (!(cond)) { \ + fail_at(__FILE__, __LINE__, (message)); \ + } \ + } while (0) + +static void write_all(int fd, const void *buf, size_t len) +{ + const char *p = (const char *)buf; + while (len > 0) { + ssize_t n = write(fd, p, len); + CHECK(n > 0, "write failed"); + p += n; + len -= (size_t)n; + } +} + +static void read_all(int fd, char *buf, size_t buf_len) +{ + size_t used = 0; + while (used + 1 < buf_len) { + ssize_t n = recv(fd, buf + used, buf_len - used - 1, 0); + CHECK(n >= 0, "recv failed"); + if (n == 0) { + break; + } + used += (size_t)n; + } + buf[used] = '\0'; +} + +static void set_socket_timeout(int fd) +{ + struct timeval tv = { + .tv_sec = 5, + .tv_usec = 0, + }; + CHECK(setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) == 0, + "setsockopt SO_RCVTIMEO failed"); + CHECK(setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) == 0, + "setsockopt SO_SNDTIMEO failed"); +} + +static void wait_child(pid_t pid) +{ + int status = 0; + CHECK(waitpid(pid, &status, 0) == pid, "waitpid failed"); + CHECK(WIFEXITED(status), "child did not exit normally"); + CHECK(WEXITSTATUS(status) == 0, "child reported failure"); +} + +static void test_dns_like_udp(void) +{ + int server = socket(AF_INET, SOCK_DGRAM, 0); + int client = socket(AF_INET, SOCK_DGRAM, 0); + CHECK(server >= 0 && client >= 0, "create UDP sockets failed"); + set_socket_timeout(server); + set_socket_timeout(client); + + struct sockaddr_in server_addr = { + .sin_family = AF_INET, + .sin_port = 0, + .sin_addr = { .s_addr = htonl(INADDR_LOOPBACK) }, + }; + CHECK(bind(server, (struct sockaddr *)&server_addr, sizeof(server_addr)) == 0, + "bind UDP DNS server failed"); + + socklen_t server_len = sizeof(server_addr); + CHECK(getsockname(server, (struct sockaddr *)&server_addr, &server_len) == 0, + "getsockname UDP DNS server failed"); + CHECK(ntohs(server_addr.sin_port) != 0, "UDP DNS server did not get a port"); + + ssize_t sent = sendto(client, DNS_QUERY, sizeof(DNS_QUERY), 0, + (struct sockaddr *)&server_addr, sizeof(server_addr)); + CHECK(sent == (ssize_t)sizeof(DNS_QUERY), "sendto DNS query failed"); + + char query[128] = {0}; + struct sockaddr_in peer = {0}; + socklen_t peer_len = sizeof(peer); + ssize_t n = recvfrom(server, query, sizeof(query), 0, + (struct sockaddr *)&peer, &peer_len); + CHECK(n == (ssize_t)sizeof(DNS_QUERY), "recvfrom DNS query size mismatch"); + CHECK(memcmp(query, DNS_QUERY, sizeof(DNS_QUERY)) == 0, + "DNS query payload mismatch"); + CHECK(peer.sin_family == AF_INET, "DNS client peer family mismatch"); + CHECK(ntohl(peer.sin_addr.s_addr) == INADDR_LOOPBACK, + "DNS client peer address mismatch"); + CHECK(ntohs(peer.sin_port) != 0, "DNS client peer port missing"); + + sent = sendto(server, DNS_REPLY, sizeof(DNS_REPLY), 0, + (struct sockaddr *)&peer, peer_len); + CHECK(sent == (ssize_t)sizeof(DNS_REPLY), "sendto DNS reply failed"); + + char reply[128] = {0}; + n = recvfrom(client, reply, sizeof(reply), 0, NULL, NULL); + CHECK(n == (ssize_t)sizeof(DNS_REPLY), "recvfrom DNS reply size mismatch"); + CHECK(memcmp(reply, DNS_REPLY, sizeof(DNS_REPLY)) == 0, + "DNS reply payload mismatch"); + + CHECK(close(client) == 0, "close UDP client failed"); + CHECK(close(server) == 0, "close UDP server failed"); +} + +static const char *body_for_path(const char *path) +{ + if (strcmp(path, INDEX_PATH) == 0) { + return INDEX_BODY; + } + if (strcmp(path, PACKAGE_PATH) == 0) { + return PACKAGE_BODY; + } + return NULL; +} + +static void handle_http_client(int accepted) +{ + set_socket_timeout(accepted); + + char request[512] = {0}; + ssize_t n = recv(accepted, request, sizeof(request) - 1, 0); + CHECK(n > 0, "recv HTTP request failed"); + request[n] = '\0'; + + char path[128] = {0}; + CHECK(sscanf(request, "GET %127s HTTP/1.1", path) == 1, + "HTTP request line parse failed"); + CHECK(strstr(request, INDEX_REQUEST_LINE) != NULL || + strstr(request, PACKAGE_REQUEST_LINE) != NULL, + "HTTP request line mismatch"); + CHECK(strstr(request, "Host: apk.local") != NULL, "HTTP Host header missing"); + CHECK(strstr(request, "Connection: close") != NULL, + "HTTP Connection header missing"); + + const char *body = body_for_path(path); + CHECK(body != NULL, "unexpected HTTP apk path"); + + char response[512]; + int len = snprintf(response, sizeof(response), + "HTTP/1.1 200 OK\r\n" + "Content-Length: %zu\r\n" + "Connection: close\r\n" + "\r\n" + "%s", + strlen(body), body); + CHECK(len > 0 && (size_t)len < sizeof(response), "HTTP response too long"); + write_all(accepted, response, (size_t)len); +} + +static void http_server_main(int listen_fd) +{ + for (int i = 0; i < 2; i++) { + int accepted = accept(listen_fd, NULL, NULL); + CHECK(accepted >= 0, "accept HTTP client failed"); + handle_http_client(accepted); + CHECK(close(accepted) == 0, "close HTTP client failed"); + } + CHECK(close(listen_fd) == 0, "close HTTP listener failed"); + _exit(0); +} + +static void fetch_http_path(const struct sockaddr_in *server_addr, + const char *path, + const char *expected_body) +{ + int fd = socket(AF_INET, SOCK_STREAM, 0); + CHECK(fd >= 0, "create HTTP client socket failed"); + set_socket_timeout(fd); + + CHECK(connect(fd, (const struct sockaddr *)server_addr, sizeof(*server_addr)) == 0, + "connect HTTP client failed"); + + char request[256]; + int request_len = snprintf(request, sizeof(request), + "GET %s HTTP/1.1\r\n" + "Host: apk.local\r\n" + "Connection: close\r\n" + "\r\n", + path); + CHECK(request_len > 0 && (size_t)request_len < sizeof(request), + "HTTP request too long"); + ssize_t sent = send(fd, request, (size_t)request_len, 0); + CHECK(sent == request_len, "send HTTP request failed"); + + char response[1024]; + read_all(fd, response, sizeof(response)); + CHECK(strstr(response, "HTTP/1.1 200 OK") != NULL, + "HTTP status line missing"); + CHECK(strstr(response, "Content-Length:") != NULL, + "Content-Length: header missing"); + CHECK(strstr(response, expected_body) != NULL, "HTTP body mismatch"); + + CHECK(close(fd) == 0, "close HTTP client failed"); +} + +static void test_apk_like_http_fetches(void) +{ + int listen_fd = socket(AF_INET, SOCK_STREAM, 0); + CHECK(listen_fd >= 0, "create HTTP listener socket failed"); + set_socket_timeout(listen_fd); + + int one = 1; + CHECK(setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) == 0, + "setsockopt SO_REUSEADDR failed"); + + struct sockaddr_in server_addr = { + .sin_family = AF_INET, + .sin_port = 0, + .sin_addr = { .s_addr = htonl(INADDR_LOOPBACK) }, + }; + CHECK(bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == 0, + "bind HTTP server failed"); + CHECK(listen(listen_fd, 2) == 0, "listen HTTP server failed"); + + socklen_t server_len = sizeof(server_addr); + CHECK(getsockname(listen_fd, (struct sockaddr *)&server_addr, &server_len) == 0, + "getsockname HTTP server failed"); + CHECK(ntohs(server_addr.sin_port) != 0, "HTTP server did not get a port"); + + pid_t server_pid = fork(); + CHECK(server_pid >= 0, "fork HTTP server failed"); + if (server_pid == 0) { + http_server_main(listen_fd); + } + + CHECK(close(listen_fd) == 0, "close parent HTTP listener failed"); + + fetch_http_path(&server_addr, INDEX_PATH, INDEX_BODY); + fetch_http_path(&server_addr, PACKAGE_PATH, PACKAGE_BODY); + wait_child(server_pid); +} + +int main(void) +{ + signal(SIGPIPE, SIG_IGN); + test_dns_like_udp(); + test_apk_like_http_fetches(); + printf("APK_NET_EQUIV_TEST_PASSED\n"); + return 0; +} diff --git a/test-suit/starryos/normal/qemu-smp1/apk-net-equivalence/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/apk-net-equivalence/qemu-riscv64.toml new file mode 100644 index 0000000000..9ea83228f9 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/apk-net-equivalence/qemu-riscv64.toml @@ -0,0 +1,22 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "rv64", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/apk-net-equivalence" +success_regex = ["(?m)^APK_NET_EQUIV_TEST_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", "(?m)^APK_NET_EQUIV_TEST_FAILED:"] +timeout = 180 diff --git a/test-suit/starryos/normal/qemu-smp1/apk-net-equivalence/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/apk-net-equivalence/qemu-x86_64.toml new file mode 100644 index 0000000000..21b1ea69ab --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/apk-net-equivalence/qemu-x86_64.toml @@ -0,0 +1,20 @@ +args = [ + "-nographic", + "-m", + "512M", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/apk-net-equivalence" +success_regex = ["(?m)^APK_NET_EQUIV_TEST_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", "(?m)^APK_NET_EQUIV_TEST_FAILED:"] +timeout = 180 diff --git a/test-suit/starryos/normal/qemu-smp1/apt/qemu-aarch64.toml b/test-suit/starryos/normal/qemu-smp1/apt/qemu-aarch64.toml index 7c2fd0df28..5ba41cbafa 100644 --- a/test-suit/starryos/normal/qemu-smp1/apt/qemu-aarch64.toml +++ b/test-suit/starryos/normal/qemu-smp1/apt/qemu-aarch64.toml @@ -20,6 +20,7 @@ shell_init_cmd = ''' rm -f /etc/apt/sources.list.d/*.sources && \ apt_hello_test() { \ printf 'deb %s trixie main\n' "$1" > /etc/apt/sources.list && \ + echo "APT_HELLO_REPO: $1" && \ apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends hello && \ hello; \ diff --git a/test-suit/starryos/normal/qemu-smp1/apt/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/apt/qemu-riscv64.toml index 34289b0222..cabf74bef8 100644 --- a/test-suit/starryos/normal/qemu-smp1/apt/qemu-riscv64.toml +++ b/test-suit/starryos/normal/qemu-smp1/apt/qemu-riscv64.toml @@ -20,6 +20,7 @@ shell_init_cmd = ''' rm -f /etc/apt/sources.list.d/*.sources && \ apt_hello_test() { \ printf 'deb %s trixie main\n' "$1" > /etc/apt/sources.list && \ + echo "APT_HELLO_REPO: $1" && \ apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends hello && \ hello; \ diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-tcp-nonblocking-connect-so-error/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/bugfix/bug-tcp-nonblocking-connect-so-error/c/src/main.c index 7d53db5396..58a59b4971 100644 --- a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-tcp-nonblocking-connect-so-error/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/bugfix/bug-tcp-nonblocking-connect-so-error/c/src/main.c @@ -51,9 +51,11 @@ static int make_listener(void) { return fd; } -static void server_process(void) { +static void server_process(int ready_fd) { int listener = make_listener(); if (listener < 0) _exit(10); + if (write(ready_fd, "R", 1) != 1) _exit(15); + close(ready_fd); int fd = accept(listener, NULL, NULL); if (fd < 0) _exit(11); @@ -72,13 +74,29 @@ int main(void) { setvbuf(stdout, NULL, _IONBF, 0); printf("=== bug-tcp-nonblocking-connect-so-error ===\n"); + int ready_pipe[2]; + CHECK(pipe(ready_pipe) == 0, "create server ready pipe"); + pid_t server = fork(); - CHECK(server >= 0, "fork server"); if (server == 0) { - server_process(); + close(ready_pipe[0]); + server_process(ready_pipe[1]); } - - sleep(1); + close(ready_pipe[1]); + CHECK(server > 0, "fork server"); + + struct pollfd ready_pfd; + memset(&ready_pfd, 0, sizeof(ready_pfd)); + ready_pfd.fd = ready_pipe[0]; + ready_pfd.events = POLLIN; + int ret = poll(&ready_pfd, 1, 3000); + CHECK(ret == 1 && (ready_pfd.revents & POLLIN) != 0, + "server reports listener ready"); + + char ready = 0; + CHECK(read(ready_pipe[0], &ready, 1) == 1 && ready == 'R', + "read server ready signal"); + close(ready_pipe[0]); int fd = socket(AF_INET, SOCK_STREAM, 0); CHECK(fd >= 0, "create client socket"); @@ -88,7 +106,7 @@ int main(void) { CHECK(fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0, "set O_NONBLOCK"); struct sockaddr_in addr = loopback_addr(); - int ret = connect(fd, (struct sockaddr *)&addr, sizeof(addr)); + ret = connect(fd, (struct sockaddr *)&addr, sizeof(addr)); CHECK(ret == 0 || errno == EINPROGRESS, "nonblocking connect starts"); struct pollfd pfd; diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-aarch64.toml b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-aarch64.toml index 4bfffbcf74..ca237991a7 100644 --- a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-aarch64.toml +++ b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-aarch64.toml @@ -63,14 +63,9 @@ test_commands = [ "/usr/bin/bug-ext4-dir-ops", "/usr/bin/bug-tcp-concurrent-connect", "/usr/bin/bug-tcp-nonblocking-connect-so-error", - "/usr/bin/bug-kill-zombie-esrch", - "/usr/bin/bug-kill-zombie-perm", - "/usr/bin/bug-raw-terminal-polling", - "/usr/bin/bug-tty-cursor-report", "/usr/bin/bug-rename-replace", "/usr/bin/bug-rename-file-into-child-dir", "/usr/bin/bug-tmpfs-hardlink-cache", - "/usr/bin/bug-zombie-syscalls", "/usr/bin/bug-starry-setuid-setgid-no-uidvalid", "/usr/bin/bug-open-nofollow-sym", "/usr/bin/bug-open-creat-directory-einval", @@ -93,7 +88,6 @@ test_commands = [ "/usr/bin/bug-open-unix-socket-no-enxio", "/usr/bin/bug-open-fifo-wronly-no-reader-no-enxio", "/usr/bin/bug-prctl-set-vma-anon-name", - "/usr/bin/bug-waitid-basic", ] success_regex = ["(?m)^STARRY_GROUPED_TESTS_PASSED\\s*$"] fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", '(?m)^STARRY_GROUPED_TEST_FAILED:'] diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-loongarch64.toml b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-loongarch64.toml index 7b92c33e8f..43977a90c2 100644 --- a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-loongarch64.toml +++ b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-loongarch64.toml @@ -64,14 +64,9 @@ test_commands = [ "/usr/bin/bug-ext4-dir-ops", "/usr/bin/bug-tcp-concurrent-connect", "/usr/bin/bug-tcp-nonblocking-connect-so-error", - "/usr/bin/bug-kill-zombie-esrch", - "/usr/bin/bug-kill-zombie-perm", - "/usr/bin/bug-raw-terminal-polling", - "/usr/bin/bug-tty-cursor-report", "/usr/bin/bug-rename-replace", "/usr/bin/bug-rename-file-into-child-dir", "/usr/bin/bug-tmpfs-hardlink-cache", - "/usr/bin/bug-zombie-syscalls", "/usr/bin/bug-starry-setuid-setgid-no-uidvalid", "/usr/bin/bug-open-nofollow-sym", "/usr/bin/bug-open-creat-directory-einval", @@ -94,7 +89,6 @@ test_commands = [ "/usr/bin/bug-open-unix-socket-no-enxio", "/usr/bin/bug-open-fifo-wronly-no-reader-no-enxio", "/usr/bin/bug-prctl-set-vma-anon-name", - "/usr/bin/bug-waitid-basic", ] success_regex = ["(?m)^STARRY_GROUPED_TESTS_PASSED\\s*$"] fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", '(?m)^STARRY_GROUPED_TEST_FAILED:'] diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-riscv64.toml index dd019dcaa5..26916310cf 100644 --- a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-riscv64.toml +++ b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-riscv64.toml @@ -71,14 +71,9 @@ test_commands = [ "/usr/bin/bug-ext4-dir-ops", "/usr/bin/bug-tcp-concurrent-connect", "/usr/bin/bug-tcp-nonblocking-connect-so-error", - "/usr/bin/bug-kill-zombie-esrch", - "/usr/bin/bug-kill-zombie-perm", - "/usr/bin/bug-raw-terminal-polling", - "/usr/bin/bug-tty-cursor-report", "/usr/bin/bug-rename-replace", "/usr/bin/bug-rename-file-into-child-dir", "/usr/bin/bug-tmpfs-hardlink-cache", - "/usr/bin/bug-zombie-syscalls", "/usr/bin/bug-starry-setuid-setgid-no-uidvalid", "/usr/bin/bug-riscv-hwprobe", "/usr/bin/bug-open-nofollow-sym", @@ -102,7 +97,6 @@ test_commands = [ "/usr/bin/bug-open-unix-socket-no-enxio", "/usr/bin/bug-open-fifo-wronly-no-reader-no-enxio", "/usr/bin/bug-prctl-set-vma-anon-name", - "/usr/bin/bug-waitid-basic", ] success_regex = ["(?m)^STARRY_GROUPED_TESTS_PASSED\\s*$"] fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", '(?m)^STARRY_GROUPED_TEST_FAILED:'] diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-x86_64.toml index e8b1a476ed..f210c02cf7 100644 --- a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-x86_64.toml +++ b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-x86_64.toml @@ -69,14 +69,9 @@ test_commands = [ "/usr/bin/bug-ext4-dir-ops", "/usr/bin/bug-tcp-concurrent-connect", "/usr/bin/bug-tcp-nonblocking-connect-so-error", - "/usr/bin/bug-kill-zombie-esrch", - "/usr/bin/bug-kill-zombie-perm", - "/usr/bin/bug-raw-terminal-polling", - "/usr/bin/bug-tty-cursor-report", "/usr/bin/bug-rename-replace", "/usr/bin/bug-rename-file-into-child-dir", "/usr/bin/bug-tmpfs-hardlink-cache", - "/usr/bin/bug-zombie-syscalls", "/usr/bin/bug-starry-setuid-setgid-no-uidvalid", "/usr/bin/bug-open-nofollow-sym", "/usr/bin/bug-open-creat-directory-einval", @@ -99,7 +94,6 @@ test_commands = [ "/usr/bin/bug-open-unix-socket-no-enxio", "/usr/bin/bug-open-fifo-wronly-no-reader-no-enxio", "/usr/bin/bug-prctl-set-vma-anon-name", - "/usr/bin/bug-waitid-basic", ] success_regex = ["(?m)^STARRY_GROUPED_TESTS_PASSED\\s*$"] fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", '(?m)^STARRY_GROUPED_TEST_FAILED:'] diff --git a/test-suit/starryos/normal/qemu-smp1/busybox/sh/busybox-tests.sh b/test-suit/starryos/normal/qemu-smp1/busybox/sh/busybox-tests.sh index 1701952db0..c88123a792 100644 --- a/test-suit/starryos/normal/qemu-smp1/busybox/sh/busybox-tests.sh +++ b/test-suit/starryos/normal/qemu-smp1/busybox/sh/busybox-tests.sh @@ -23,6 +23,7 @@ bb_now_ms() { bb_case_start() { BB_CASE_NAME=$1 BB_CASE_START_MS=$(bb_now_ms) + echo "START: $BB_CASE_NAME" } bb_case_print_time() { @@ -638,7 +639,7 @@ _t=$({ timeout 10 sh -c "busybox nmeter -h 2>&1"; } 2>&1) if echo "$_t" | grep -qF "Usage: nmeter"; then echo "PASS: busybox_nmeter"; bb_case_pass; else echo "FAIL_DETAIL: busybox_nmeter"; bb_case_fail; fi bb_case_start "busybox_nologin" -_t=$({ timeout 2 sh -c 'busybox rm -f /tmp/bb_nologin.out; busybox nologin >/tmp/bb_nologin.out 2>&1 & _pid=$!; busybox usleep 200000; busybox kill -9 "$_pid" 2>/dev/null || true; busybox cat /tmp/bb_nologin.out 2>/dev/null; busybox rm -f /tmp/bb_nologin.out'; } 2>&1) +_t=$({ timeout 2 busybox nologin 2>&1 || true; } 2>&1) if echo "$_t" | grep -qF "This account is not available"; then echo "PASS: busybox_nologin"; bb_case_pass; else echo "FAIL_DETAIL: busybox_nologin"; bb_case_fail; fi bb_case_start "busybox_nproc" @@ -1092,10 +1093,10 @@ busybox mkdir -p /tmp/bb_wget_root busybox printf "\r\n" busybox printf "busybox wget local ok\n" } > /tmp/bb_wget_root/response.http -busybox nc -l -p 18080 -w 10 < /tmp/bb_wget_root/response.http & +busybox nc -l -p 18080 -w 10 < /tmp/bb_wget_root/response.http >/tmp/bb_wget_nc.out 2>&1 & server_pid=$! busybox sleep 1 -busybox wget -O /tmp/bb_wget.html http://127.0.0.1:18080/index.html 2>&1 +timeout 10 busybox wget -O /tmp/bb_wget.html http://127.0.0.1:18080/index.html 2>&1 wget_status=$? busybox kill "$server_pid" 2>/dev/null || true busybox test "$wget_status" -eq 0 && diff --git a/test-suit/starryos/normal/qemu-smp1/inotifywait/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/inotifywait/c/CMakeLists.txt new file mode 100644 index 0000000000..ef8bc2f835 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/inotifywait/c/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.20) + +project(inotifywait-test NONE) + +install(PROGRAMS inotifywait-tests.sh DESTINATION usr/bin) +install(PROGRAMS ${STARRY_STAGING_ROOT}/usr/bin/inotifywait DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/inotifywait/sh/inotifywait-tests.sh b/test-suit/starryos/normal/qemu-smp1/inotifywait/c/inotifywait-tests.sh similarity index 95% rename from test-suit/starryos/normal/qemu-smp1/inotifywait/sh/inotifywait-tests.sh rename to test-suit/starryos/normal/qemu-smp1/inotifywait/c/inotifywait-tests.sh index a73b034ac1..41ba644452 100644 --- a/test-suit/starryos/normal/qemu-smp1/inotifywait/sh/inotifywait-tests.sh +++ b/test-suit/starryos/normal/qemu-smp1/inotifywait/c/inotifywait-tests.sh @@ -6,9 +6,6 @@ fail() { exit 1 } -apk update || fail "apk update" -apk add inotify-tools || fail "apk add inotify-tools" - command -v inotifywait >/dev/null || fail "inotifywait missing" workdir=/tmp/starry-inotifywait diff --git a/test-suit/starryos/normal/qemu-smp1/inotifywait/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/inotifywait/c/prebuild.sh new file mode 100644 index 0000000000..23ebed7877 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/inotifywait/c/prebuild.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -eu + +apk add inotify-tools + +test -x "$STARRY_STAGING_ROOT/usr/bin/inotifywait" diff --git a/test-suit/starryos/normal/qemu-smp1/inotifywait/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/inotifywait/qemu-x86_64.toml index 484026af24..b0d2bf5886 100644 --- a/test-suit/starryos/normal/qemu-smp1/inotifywait/qemu-x86_64.toml +++ b/test-suit/starryos/normal/qemu-smp1/inotifywait/qemu-x86_64.toml @@ -17,4 +17,4 @@ shell_prefix = "root@starry:" shell_init_cmd = "/usr/bin/inotifywait-tests.sh" success_regex = ["(?m)^INOTIFYWAIT_TEST_PASSED\\s*$"] fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^INOTIFYWAIT_TEST_FAILED:"] -timeout = 600 +timeout = 180 diff --git a/test-suit/starryos/normal/qemu-smp1/lua-luarocks/sh/lua-luarocks-tests.sh b/test-suit/starryos/normal/qemu-smp1/lua-luarocks/sh/lua-luarocks-tests.sh index 7982069e25..38f2c3bb58 100644 --- a/test-suit/starryos/normal/qemu-smp1/lua-luarocks/sh/lua-luarocks-tests.sh +++ b/test-suit/starryos/normal/qemu-smp1/lua-luarocks/sh/lua-luarocks-tests.sh @@ -1,13 +1,23 @@ #!/bin/sh set -eu -apk update -apk add lua5.4 luarocks5.4 - -luarocks-5.4 install inspect -test -f /usr/local/share/lua/5.4/inspect.lua - -lua5.4 /usr/bin/lua-luarocks-main.lua || { +fail() { echo "LUA_LUAROCKS_TEST_FAILED" exit 1 } + +retry() { + for _ in 1 2 3; do + "$@" && return 0 + sleep 1 + done + return 1 +} + +retry apk update || fail +retry apk add lua5.4 luarocks5.4 || fail + +retry luarocks-5.4 install inspect || fail +test -f /usr/local/share/lua/5.4/inspect.lua || fail + +lua5.4 /usr/bin/lua-luarocks-main.lua || fail diff --git a/test-suit/starryos/normal/qemu-smp1/lua/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/lua/c/CMakeLists.txt new file mode 100644 index 0000000000..144522369c --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/lua/c/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.20) + +project(lua-app-test NONE) + +install(PROGRAMS lua-app-tests.sh DESTINATION usr/bin) +install(FILES lua-main.lua lua-secondary.lua starry_lua_helper.lua DESTINATION usr/bin) +install(PROGRAMS ${STARRY_STAGING_ROOT}/usr/bin/lua5.4 DESTINATION usr/bin) +install(FILES ${STARRY_STAGING_ROOT}/usr/lib/lua/5.4/cjson.so DESTINATION usr/lib/lua/5.4) diff --git a/test-suit/starryos/normal/qemu-smp1/lua/sh/lua-app-tests.sh b/test-suit/starryos/normal/qemu-smp1/lua/c/lua-app-tests.sh similarity index 72% rename from test-suit/starryos/normal/qemu-smp1/lua/sh/lua-app-tests.sh rename to test-suit/starryos/normal/qemu-smp1/lua/c/lua-app-tests.sh index 322c7af2af..4446114831 100644 --- a/test-suit/starryos/normal/qemu-smp1/lua/sh/lua-app-tests.sh +++ b/test-suit/starryos/normal/qemu-smp1/lua/c/lua-app-tests.sh @@ -1,9 +1,6 @@ #!/bin/sh set -eu -apk update -apk add lua5.4 lua5.4-cjson - lua5.4 /usr/bin/lua-main.lua alpha beta || { echo "LUA_APP_TEST_FAILED" exit 1 diff --git a/test-suit/starryos/normal/qemu-smp1/lua/sh/lua-main.lua b/test-suit/starryos/normal/qemu-smp1/lua/c/lua-main.lua similarity index 100% rename from test-suit/starryos/normal/qemu-smp1/lua/sh/lua-main.lua rename to test-suit/starryos/normal/qemu-smp1/lua/c/lua-main.lua diff --git a/test-suit/starryos/normal/qemu-smp1/lua/sh/lua-secondary.lua b/test-suit/starryos/normal/qemu-smp1/lua/c/lua-secondary.lua similarity index 100% rename from test-suit/starryos/normal/qemu-smp1/lua/sh/lua-secondary.lua rename to test-suit/starryos/normal/qemu-smp1/lua/c/lua-secondary.lua diff --git a/test-suit/starryos/normal/qemu-smp1/lua/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/lua/c/prebuild.sh new file mode 100644 index 0000000000..657ca7925a --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/lua/c/prebuild.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -eu + +apk add lua5.4 lua5.4-cjson + +test -x "$STARRY_STAGING_ROOT/usr/bin/lua5.4" +test -f "$STARRY_STAGING_ROOT/usr/lib/lua/5.4/cjson.so" diff --git a/test-suit/starryos/normal/qemu-smp1/lua/sh/starry_lua_helper.lua b/test-suit/starryos/normal/qemu-smp1/lua/c/starry_lua_helper.lua similarity index 100% rename from test-suit/starryos/normal/qemu-smp1/lua/sh/starry_lua_helper.lua rename to test-suit/starryos/normal/qemu-smp1/lua/c/starry_lua_helper.lua diff --git a/test-suit/starryos/normal/qemu-smp1/lua/qemu-aarch64.toml b/test-suit/starryos/normal/qemu-smp1/lua/qemu-aarch64.toml index 180ab320ef..2ff34f95cc 100644 --- a/test-suit/starryos/normal/qemu-smp1/lua/qemu-aarch64.toml +++ b/test-suit/starryos/normal/qemu-smp1/lua/qemu-aarch64.toml @@ -19,4 +19,4 @@ shell_prefix = "root@starry:" shell_init_cmd = "/usr/bin/lua-app-tests.sh" success_regex = ["(?m)^LUA_APP_TEST_PASSED\\s*$"] fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^LUA_APP_TEST_FAILED\\s*$"] -timeout = 600 +timeout = 180 diff --git a/test-suit/starryos/normal/qemu-smp1/lua/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/lua/qemu-riscv64.toml index 9c6594c38c..27e8c4dd21 100644 --- a/test-suit/starryos/normal/qemu-smp1/lua/qemu-riscv64.toml +++ b/test-suit/starryos/normal/qemu-smp1/lua/qemu-riscv64.toml @@ -19,4 +19,4 @@ shell_prefix = "root@starry:" shell_init_cmd = "/usr/bin/lua-app-tests.sh" success_regex = ["(?m)^LUA_APP_TEST_PASSED\\s*$"] fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^LUA_APP_TEST_FAILED\\s*$"] -timeout = 600 +timeout = 180 diff --git a/test-suit/starryos/normal/qemu-smp1/lua/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/lua/qemu-x86_64.toml index eaa23f3a50..d876fcc85a 100644 --- a/test-suit/starryos/normal/qemu-smp1/lua/qemu-x86_64.toml +++ b/test-suit/starryos/normal/qemu-smp1/lua/qemu-x86_64.toml @@ -17,4 +17,4 @@ shell_prefix = "root@starry:" shell_init_cmd = "/usr/bin/lua-app-tests.sh" success_regex = ["(?m)^LUA_APP_TEST_PASSED\\s*$"] fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^LUA_APP_TEST_FAILED\\s*$"] -timeout = 600 +timeout = 180 diff --git a/test-suit/starryos/normal/qemu-smp1/procps/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/procps/c/CMakeLists.txt new file mode 100644 index 0000000000..c009e78b40 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/procps/c/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.20) + +project(procps-test NONE) + +install(PROGRAMS procps-test.sh DESTINATION usr/bin) + +set(PROCPS_TOOLS + ps + free + uptime + pgrep + pmap +) + +foreach(tool IN LISTS PROCPS_TOOLS) + if(EXISTS "${STARRY_STAGING_ROOT}/usr/bin/${tool}") + install(PROGRAMS "${STARRY_STAGING_ROOT}/usr/bin/${tool}" DESTINATION usr/bin) + elseif(EXISTS "${STARRY_STAGING_ROOT}/bin/${tool}") + install(PROGRAMS "${STARRY_STAGING_ROOT}/bin/${tool}" DESTINATION usr/bin) + else() + message(FATAL_ERROR "procps tool ${tool} not found in staging root") + endif() +endforeach() diff --git a/test-suit/starryos/normal/qemu-smp1/procps/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/procps/c/prebuild.sh new file mode 100644 index 0000000000..f6e6625fee --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/procps/c/prebuild.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -eu + +apk add procps + +for tool in ps free uptime pgrep pmap; do + if [ ! -x "$STARRY_STAGING_ROOT/usr/bin/$tool" ] \ + && [ ! -x "$STARRY_STAGING_ROOT/bin/$tool" ]; then + echo "missing procps tool: $tool" >&2 + exit 1 + fi +done diff --git a/test-suit/starryos/normal/qemu-smp1/procps/sh/procps-test.sh b/test-suit/starryos/normal/qemu-smp1/procps/c/procps-test.sh similarity index 69% rename from test-suit/starryos/normal/qemu-smp1/procps/sh/procps-test.sh rename to test-suit/starryos/normal/qemu-smp1/procps/c/procps-test.sh index 47483dd5cf..db1389d8b2 100644 --- a/test-suit/starryos/normal/qemu-smp1/procps/sh/procps-test.sh +++ b/test-suit/starryos/normal/qemu-smp1/procps/c/procps-test.sh @@ -1,6 +1,7 @@ #!/bin/sh -echo "=== install procps ===" -apk add procps || echo " WARN | apk add procps failed (network issue), using busybox fallback" +set -u + +echo "=== procps tool test ===" PASS=0 FAIL=0 @@ -20,6 +21,22 @@ run_test() { fi } +require_tool() { + TOOL="$1" + if command -v "$TOOL" >/dev/null 2>&1; then + PASS=$((PASS + 1)) + echo " PASS | $TOOL available" + else + FAIL=$((FAIL + 1)) + echo " FAIL | $TOOL available" + fi +} + +echo "=== verify procps tools ===" +for tool in ps free uptime pgrep pmap; do + require_tool "$tool" +done + echo "=== test ps ===" run_test "ps aux" ps aux run_test "ps -ef" ps -ef @@ -36,14 +53,14 @@ echo "=== test pgrep ===" run_test "pgrep -l sh" pgrep -l sh echo "=== test pmap ===" -if apk info -e procps >/dev/null 2>&1; then +if command -v pmap >/dev/null 2>&1; then if pmap 1 >/dev/null 2>&1; then run_test "pmap 1" pmap 1 else - echo " SKIP | pmap (procps installed but pmap tool not working on this arch)" + echo " SKIP | pmap (tool installed but pmap is unsupported on this arch)" fi else - echo " SKIP | pmap (procps-ng not installed)" + echo " SKIP | pmap (procps tool missing)" fi echo "=== test /proc entries ===" @@ -61,6 +78,8 @@ echo "=== results: PASS=$PASS FAIL=$FAIL ===" if [ $FAIL -eq 0 ]; then echo "PROCPS_TEST_PASSED" + exit 0 else echo "PROCPS_TEST_FAILED" + exit 1 fi diff --git a/test-suit/starryos/normal/qemu-smp1/procps/qemu-aarch64.toml b/test-suit/starryos/normal/qemu-smp1/procps/qemu-aarch64.toml index 954a3523a9..8dc26f84a7 100644 --- a/test-suit/starryos/normal/qemu-smp1/procps/qemu-aarch64.toml +++ b/test-suit/starryos/normal/qemu-smp1/procps/qemu-aarch64.toml @@ -13,4 +13,4 @@ shell_prefix = "root@starry:" shell_init_cmd = "/usr/bin/procps-test.sh" success_regex = ["(?m)^PROCPS_TEST_PASSED\\s*$"] fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^PROCPS_TEST_FAILED\\s*$"] -timeout = 300 +timeout = 180 diff --git a/test-suit/starryos/normal/qemu-smp1/procps/qemu-loongarch64.toml b/test-suit/starryos/normal/qemu-smp1/procps/qemu-loongarch64.toml index a0e346961c..62c85f6ba6 100644 --- a/test-suit/starryos/normal/qemu-smp1/procps/qemu-loongarch64.toml +++ b/test-suit/starryos/normal/qemu-smp1/procps/qemu-loongarch64.toml @@ -14,4 +14,4 @@ shell_prefix = "root@starry:" shell_init_cmd = "/usr/bin/procps-test.sh" success_regex = ["(?m)^PROCPS_TEST_PASSED\\s*$"] fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^PROCPS_TEST_FAILED\\s*$"] -timeout = 300 +timeout = 180 diff --git a/test-suit/starryos/normal/qemu-smp1/procps/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/procps/qemu-riscv64.toml index 26bad69e0f..ed25ab73ce 100644 --- a/test-suit/starryos/normal/qemu-smp1/procps/qemu-riscv64.toml +++ b/test-suit/starryos/normal/qemu-smp1/procps/qemu-riscv64.toml @@ -13,4 +13,4 @@ shell_prefix = "root@starry:" shell_init_cmd = "/usr/bin/procps-test.sh" success_regex = ["(?m)^PROCPS_TEST_PASSED\\s*$"] fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^PROCPS_TEST_FAILED\\s*$"] -timeout = 300 +timeout = 180 diff --git a/test-suit/starryos/normal/qemu-smp1/procps/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/procps/qemu-x86_64.toml index b3c7dea02d..faf1c72dfd 100644 --- a/test-suit/starryos/normal/qemu-smp1/procps/qemu-x86_64.toml +++ b/test-suit/starryos/normal/qemu-smp1/procps/qemu-x86_64.toml @@ -12,4 +12,4 @@ shell_prefix = "root@starry:" shell_init_cmd = "/usr/bin/procps-test.sh" success_regex = ["(?m)^PROCPS_TEST_PASSED\\s*$"] fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^PROCPS_TEST_FAILED\\s*$"] -timeout = 600 +timeout = 180 diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-raw-msg-peek/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/syscall/test-raw-msg-peek/c/prebuild.sh index 74486050a6..dd8d15f56b 100644 --- a/test-suit/starryos/normal/qemu-smp1/syscall/test-raw-msg-peek/c/prebuild.sh +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-raw-msg-peek/c/prebuild.sh @@ -6,4 +6,70 @@ if [ -z "${STARRY_STAGING_ROOT:-}" ]; then exit 0 fi -apk add binutils gcc musl-dev +case "$STARRY_STAGING_ROOT" in + /*) root=$STARRY_STAGING_ROOT ;; + *) root=$(pwd -P)/$STARRY_STAGING_ROOT ;; +esac +run_dir=${root%/staging-root} +case_dir=${run_dir%/runs/*} +apk_cache_dir=$case_dir/cache/apk-cache + +find_qemu_runner() { + for candidate in "$@"; do + if [ -z "$candidate" ]; then + continue + fi + if command -v "$candidate" >/dev/null 2>&1; then + command -v "$candidate" + return 0 + fi + if [ -x "$candidate" ]; then + printf '%s\n' "$candidate" + return 0 + fi + done + return 1 +} + +case "$root" in + *aarch64*) + qemu_runner=$(find_qemu_runner \ + /usr/bin/qemu-aarch64-static \ + qemu-aarch64-static \ + qemu-aarch64) + ;; + *x86_64*) + qemu_runner=$(find_qemu_runner \ + /usr/bin/qemu-x86_64-static \ + qemu-x86_64-static \ + qemu-x86_64) + ;; + *riscv64*) + qemu_runner=$(find_qemu_runner \ + /usr/bin/qemu-riscv64-static \ + qemu-riscv64-static \ + qemu-riscv64) + ;; + *loongarch64*) + qemu_runner=$(find_qemu_runner \ + /usr/bin/qemu-loongarch64-static \ + /usr/local/bin/qemu-loongarch64 \ + qemu-loongarch64-static \ + qemu-loongarch64) + ;; + *) + echo "unsupported staging root target: $root" >&2 + exit 1 + ;; +esac + +"$qemu_runner" -L "$root" "$root/sbin/apk" \ + --root="$root" \ + --repositories-file="$root/etc/apk/repositories" \ + --keys-dir="$root/etc/apk/keys" \ + --cache-dir="$apk_cache_dir" \ + --update-cache \ + --timeout=60 \ + --no-interactive \ + --force-no-chroot \ + add --scripts=no binutils gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-raw-terminal-polling/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/tty-bugfix/bug-raw-terminal-polling/c/CMakeLists.txt similarity index 100% rename from test-suit/starryos/normal/qemu-smp1/bugfix/bug-raw-terminal-polling/c/CMakeLists.txt rename to test-suit/starryos/normal/qemu-smp1/tty-bugfix/bug-raw-terminal-polling/c/CMakeLists.txt diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-raw-terminal-polling/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/tty-bugfix/bug-raw-terminal-polling/c/src/main.c similarity index 100% rename from test-suit/starryos/normal/qemu-smp1/bugfix/bug-raw-terminal-polling/c/src/main.c rename to test-suit/starryos/normal/qemu-smp1/tty-bugfix/bug-raw-terminal-polling/c/src/main.c diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-tty-cursor-report/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/tty-bugfix/bug-tty-cursor-report/c/CMakeLists.txt similarity index 100% rename from test-suit/starryos/normal/qemu-smp1/bugfix/bug-tty-cursor-report/c/CMakeLists.txt rename to test-suit/starryos/normal/qemu-smp1/tty-bugfix/bug-tty-cursor-report/c/CMakeLists.txt diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-tty-cursor-report/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/tty-bugfix/bug-tty-cursor-report/c/src/main.c similarity index 100% rename from test-suit/starryos/normal/qemu-smp1/bugfix/bug-tty-cursor-report/c/src/main.c rename to test-suit/starryos/normal/qemu-smp1/tty-bugfix/bug-tty-cursor-report/c/src/main.c diff --git a/test-suit/starryos/normal/qemu-smp1/tty-bugfix/qemu-aarch64.toml b/test-suit/starryos/normal/qemu-smp1/tty-bugfix/qemu-aarch64.toml new file mode 100644 index 0000000000..1e35fff639 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/tty-bugfix/qemu-aarch64.toml @@ -0,0 +1,25 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "cortex-a53", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +test_commands = [ + "/usr/bin/bug-raw-terminal-polling", + "/usr/bin/bug-tty-cursor-report", +] +success_regex = ["(?m)^STARRY_GROUPED_TESTS_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", '(?m)^STARRY_GROUPED_TEST_FAILED:'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/tty-bugfix/qemu-loongarch64.toml b/test-suit/starryos/normal/qemu-smp1/tty-bugfix/qemu-loongarch64.toml new file mode 100644 index 0000000000..e229cbfcfe --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/tty-bugfix/qemu-loongarch64.toml @@ -0,0 +1,27 @@ +args = [ + "-machine", + "virt", + "-cpu", + "la464", + "-nographic", + "-m", + "512M", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-loongarch64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +test_commands = [ + "/usr/bin/bug-raw-terminal-polling", + "/usr/bin/bug-tty-cursor-report", +] +success_regex = ["(?m)^STARRY_GROUPED_TESTS_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", '(?m)^STARRY_GROUPED_TEST_FAILED:'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/tty-bugfix/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/tty-bugfix/qemu-riscv64.toml new file mode 100644 index 0000000000..ea0e0f463e --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/tty-bugfix/qemu-riscv64.toml @@ -0,0 +1,25 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "rv64", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +test_commands = [ + "/usr/bin/bug-raw-terminal-polling", + "/usr/bin/bug-tty-cursor-report", +] +success_regex = ["(?m)^STARRY_GROUPED_TESTS_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", '(?m)^STARRY_GROUPED_TEST_FAILED:'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/tty-bugfix/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/tty-bugfix/qemu-x86_64.toml new file mode 100644 index 0000000000..31f00b841d --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/tty-bugfix/qemu-x86_64.toml @@ -0,0 +1,23 @@ +args = [ + "-nographic", + "-m", + "512M", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +test_commands = [ + "/usr/bin/bug-raw-terminal-polling", + "/usr/bin/bug-tty-cursor-report", +] +success_regex = ["(?m)^STARRY_GROUPED_TESTS_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", '(?m)^STARRY_GROUPED_TEST_FAILED:'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-kill-zombie-esrch/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-kill-zombie-esrch/c/CMakeLists.txt similarity index 100% rename from test-suit/starryos/normal/qemu-smp1/bugfix/bug-kill-zombie-esrch/c/CMakeLists.txt rename to test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-kill-zombie-esrch/c/CMakeLists.txt diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-kill-zombie-esrch/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-kill-zombie-esrch/c/src/main.c similarity index 60% rename from test-suit/starryos/normal/qemu-smp1/bugfix/bug-kill-zombie-esrch/c/src/main.c rename to test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-kill-zombie-esrch/c/src/main.c index 0c3fc5ca89..ab68401ee1 100644 --- a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-kill-zombie-esrch/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-kill-zombie-esrch/c/src/main.c @@ -11,32 +11,11 @@ * might be a zombie"). A separate earlier bug returned 0 after reap. * * Synchronization: - * sigwaitinfo(SIGCHLD) stalls in StarryOS because send_signal does not - * interrupt a task that is blocked in rt_sigtimedwait when the signal is - * in the wait set but currently blocked at the signal layer. - * - * Instead we use a pipe: the child writes a byte just before _exit(). - * The parent reads it to know the child has called _exit(). At that - * point the child may not yet be a zombie (close_all_fds runs before - * process.exit() in do_exit), so we spin on waitpid(WNOHANG) until the - * child is waitable — but we must NOT reap it yet. - * - * Problem: waitpid(WNOHANG) reaps atomically when it finds a zombie. - * We cannot "peek" without reaping. - * - * Solution: use a short usleep() after the pipe read. The child's - * do_exit() path is: - * close_all_fds() → pipe write end closed → parent unblocks - * process.exit() → is_zombie = true - * send SIGCHLD to parent - * The gap between pipe-close and is_zombie is a single function call. - * A 10 ms sleep is orders of magnitude longer than that gap on any - * real scheduler, making the race window negligible in practice. - * - * This is intentionally a best-effort synchronization. If the timing - * is wrong, check 1 may spuriously pass (child still alive) but will - * never spuriously fail. The test is designed to FAIL with the buggy - * kernel (is_zombie → ESRCH) and PASS with the correct kernel. + * waitpid(WNOHANG) reaps atomically when it finds a zombie, so it cannot be + * used to wait for "zombie but not reaped" state. Instead poll waitid() + * with WNOWAIT|WNOHANG: it observes that the child is waitable without + * consuming the zombie. The loop is bounded so a scheduler/process bug + * reports a test failure instead of hanging the whole QEMU case. */ #define _GNU_SOURCE @@ -64,6 +43,24 @@ static int failed; } \ } while (0) +static int wait_until_zombie(pid_t child) +{ + for (int waited_us = 0; waited_us < 5000000; waited_us += 10000) { + siginfo_t info; + memset(&info, 0, sizeof(info)); + if (waitid(P_PID, (id_t)child, &info, + WEXITED | WNOWAIT | WNOHANG) == 0) { + if (info.si_pid == child) + return 0; + } else if (errno != EINTR) { + return -1; + } + usleep(10000); + } + errno = ETIMEDOUT; + return -1; +} + int main(void) { printf("=== bug-kill-zombie-esrch ===\n"); @@ -78,13 +75,6 @@ int main(void) */ signal(SIGCHLD, SIG_DFL); - /* Pipe for synchronization: child writes before _exit(). */ - int pipefd[2]; - if (pipe(pipefd) < 0) { - perror("pipe"); - return EXIT_FAILURE; - } - pid_t child = fork(); if (child < 0) { perror("fork"); @@ -92,26 +82,15 @@ int main(void) } if (child == 0) { - /* Child: signal parent then exit. */ - close(pipefd[0]); - char byte = 1; - (void)write(pipefd[1], &byte, 1); - close(pipefd[1]); _exit(0); } - /* Parent: wait for child to write, then give it time to become zombie. */ - close(pipefd[1]); - char buf; - (void)read(pipefd[0], &buf, 1); - close(pipefd[0]); - - /* - * The child has called write() and is about to _exit(). - * do_exit() calls close_all_fds() then process.exit() (sets is_zombie). - * Sleep 10ms to let process.exit() complete. - */ - usleep(10000); + if (wait_until_zombie(child) != 0) { + perror("waitid(WNOWAIT) for zombie child"); + (void)kill(child, SIGKILL); + (void)waitpid(child, NULL, WNOHANG); + return EXIT_FAILURE; + } /* * --- check 1: kill(zombie, 0) must return 0 --- diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-kill-zombie-perm/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-kill-zombie-perm/c/CMakeLists.txt similarity index 100% rename from test-suit/starryos/normal/qemu-smp1/bugfix/bug-kill-zombie-perm/c/CMakeLists.txt rename to test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-kill-zombie-perm/c/CMakeLists.txt diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-kill-zombie-perm/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-kill-zombie-perm/c/src/main.c similarity index 82% rename from test-suit/starryos/normal/qemu-smp1/bugfix/bug-kill-zombie-perm/c/src/main.c rename to test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-kill-zombie-perm/c/src/main.c index 7a7b69acdc..a4aba4f351 100644 --- a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-kill-zombie-perm/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-kill-zombie-perm/c/src/main.c @@ -24,10 +24,9 @@ * 5. Parent reaps the zombie with waitpid(). * 6. Parent calls kill(reaped, 0) → must return -1 / ESRCH. * - * Synchronization: same pipe trick as bug-kill-zombie-esrch — child writes - * one byte just before _exit() so the parent knows close_all_fds() has run. - * A 10 ms usleep() then lets process.exit() (is_zombie = true) complete - * before we probe with kill(). + * Synchronization: poll waitid(WNOWAIT|WNOHANG) until the child is waitable + * without reaping it. The loop is bounded so a scheduler/process bug reports + * a test failure instead of hanging the whole QEMU case. * * The test is designed to FAIL on the buggy kernel (check_kill_permission * returns ESRCH for a GC'd task) and PASS once the fix stores a cred @@ -61,6 +60,24 @@ static int failed; } \ } while (0) +static int wait_until_zombie(pid_t child) +{ + for (int waited_us = 0; waited_us < 5000000; waited_us += 10000) { + siginfo_t info; + memset(&info, 0, sizeof(info)); + if (waitid(P_PID, (id_t)child, &info, + WEXITED | WNOWAIT | WNOHANG) == 0) { + if (info.si_pid == child) + return 0; + } else if (errno != EINTR) { + return -1; + } + usleep(10000); + } + errno = ETIMEDOUT; + return -1; +} + int main(void) { printf("=== bug-kill-zombie-perm ===\n"); @@ -76,13 +93,6 @@ int main(void) /* Keep SIGCHLD default so the zombie is not auto-reaped. */ signal(SIGCHLD, SIG_DFL); - /* Pipe: child writes one byte just before _exit(). */ - int pipefd[2]; - if (pipe(pipefd) < 0) { - perror("pipe"); - return EXIT_FAILURE; - } - pid_t child = fork(); if (child < 0) { perror("fork"); @@ -94,25 +104,23 @@ int main(void) * Child: also drop to the same non-root UID so the parent's * permission check (same euid) should succeed. */ - close(pipefd[0]); if (setuid(NON_ROOT_UID) < 0) { perror("child setuid"); _exit(1); } - char byte = 1; - (void)write(pipefd[1], &byte, 1); - close(pipefd[1]); _exit(0); } - /* Parent: wait for child's pipe write, then drop to non-root UID. */ - close(pipefd[1]); - char buf; - (void)read(pipefd[0], &buf, 1); - close(pipefd[0]); + if (wait_until_zombie(child) != 0) { + perror("waitid(WNOWAIT) for zombie child"); + (void)kill(child, SIGKILL); + (void)waitpid(child, NULL, WNOHANG); + return EXIT_FAILURE; + } /* - * Drop privileges AFTER the pipe read so we are still root during fork(). + * Drop privileges AFTER the child is a zombie so we are still root during + * fork and zombie-state synchronization. * Now both parent and child have uid/euid == NON_ROOT_UID. */ if (setuid(NON_ROOT_UID) < 0) { @@ -121,13 +129,6 @@ int main(void) } printf(" parent dropped to uid=%d\n", (int)getuid()); - /* - * Give the child's do_exit() time to set is_zombie = true. - * The gap between pipe-close (close_all_fds) and process.exit() is a - * single function call; 10 ms is orders of magnitude longer. - */ - usleep(10000); - /* --- check 1: kill(zombie, 0) from same-UID non-root --- */ errno = 0; int rc = kill(child, 0); diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-waitid-basic/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-waitid-basic/c/CMakeLists.txt similarity index 100% rename from test-suit/starryos/normal/qemu-smp1/bugfix/bug-waitid-basic/c/CMakeLists.txt rename to test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-waitid-basic/c/CMakeLists.txt diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-waitid-basic/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-waitid-basic/c/src/main.c similarity index 100% rename from test-suit/starryos/normal/qemu-smp1/bugfix/bug-waitid-basic/c/src/main.c rename to test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-waitid-basic/c/src/main.c diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-zombie-syscalls/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-zombie-syscalls/c/CMakeLists.txt similarity index 100% rename from test-suit/starryos/normal/qemu-smp1/bugfix/bug-zombie-syscalls/c/CMakeLists.txt rename to test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-zombie-syscalls/c/CMakeLists.txt diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-zombie-syscalls/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-zombie-syscalls/c/src/main.c similarity index 77% rename from test-suit/starryos/normal/qemu-smp1/bugfix/bug-zombie-syscalls/c/src/main.c rename to test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-zombie-syscalls/c/src/main.c index 7f3a64e42b..9978aa7f61 100644 --- a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-zombie-syscalls/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/bug-zombie-syscalls/c/src/main.c @@ -12,21 +12,21 @@ * - getpriority(PRIO_PROCESS, zombie) → 0 (nice value, not ESRCH) * - After waitpid(): all three → -1 / ESRCH * - * Synchronization (same pattern as bug-kill-zombie-esrch): - * The child writes a byte to a pipe just before _exit(). The parent reads - * it to know the child has called _exit(), then sleeps 10 ms to let - * process.exit() complete (which sets the zombie state). Only then are - * the zombie checks performed. + * Synchronization: + * waitid(WNOWAIT|WNOHANG) observes that the child is waitable without + * reaping it. The bounded loop avoids hanging the whole QEMU case if child + * exit notification regresses. */ #include +#include #include #include #include -#include #include #include #include +#include #include static int passed = 0; @@ -44,6 +44,25 @@ static int failed = 0; } \ } while (0) +static int wait_until_zombie(pid_t child) +{ + for (int waited_us = 0; waited_us < 5000000; waited_us += 10000) { + siginfo_t info; + memset(&info, 0, sizeof(info)); + if (waitid(P_PID, (id_t)child, &info, + WEXITED | WNOWAIT | WNOHANG) == 0) { + if (info.si_pid == child) + return 0; + } else if (errno != EINTR) { + return -1; + } + struct timespec ts = { .tv_sec = 0, .tv_nsec = 10000000 }; + nanosleep(&ts, NULL); + } + errno = ETIMEDOUT; + return -1; +} + int main(void) { /* Record parent's sid and pgid — zombie child must return the same. */ @@ -53,13 +72,6 @@ int main(void) printf("parent pid=%d sid=%d pgid=%d\n", (int)getpid(), (int)parent_sid, (int)parent_pgid); - /* Pipe for synchronization: child writes 1 byte before _exit(). */ - int pipefd[2]; - if (pipe(pipefd) != 0) { - perror("pipe"); - return EXIT_FAILURE; - } - pid_t child = fork(); if (child < 0) { perror("fork"); @@ -67,30 +79,15 @@ int main(void) } if (child == 0) { - /* ---- child ---- */ - close(pipefd[0]); - /* Signal parent that we are about to exit. */ - char byte = 1; - (void)write(pipefd[1], &byte, 1); - close(pipefd[1]); _exit(0); } - /* ---- parent ---- */ - close(pipefd[1]); - - /* Wait for child's write — child is about to call _exit(). */ - char byte; - (void)read(pipefd[0], &byte, 1); - close(pipefd[0]); - - /* - * Sleep 10 ms so do_exit() can finish process.exit() and register the - * zombie. The gap between pipe-close and zombie registration is tiny - * but real; the sleep makes the test reliable. - */ - struct timespec ts = { .tv_sec = 0, .tv_nsec = 10000000 }; /* 10 ms */ - nanosleep(&ts, NULL); + if (wait_until_zombie(child) != 0) { + perror("waitid(WNOWAIT) for zombie child"); + (void)kill(child, SIGKILL); + (void)waitpid(child, NULL, WNOHANG); + return EXIT_FAILURE; + } printf("\n--- checks before waitpid (zombie state) ---\n"); diff --git a/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/qemu-aarch64.toml b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/qemu-aarch64.toml new file mode 100644 index 0000000000..73b145bd86 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/qemu-aarch64.toml @@ -0,0 +1,27 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "cortex-a53", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +test_commands = [ + "/usr/bin/bug-kill-zombie-esrch", + "/usr/bin/bug-kill-zombie-perm", + "/usr/bin/bug-zombie-syscalls", + "/usr/bin/bug-waitid-basic", +] +success_regex = ["(?m)^STARRY_GROUPED_TESTS_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", '(?m)^STARRY_GROUPED_TEST_FAILED:'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/qemu-loongarch64.toml b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/qemu-loongarch64.toml new file mode 100644 index 0000000000..f56447df0f --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/qemu-loongarch64.toml @@ -0,0 +1,29 @@ +args = [ + "-machine", + "virt", + "-cpu", + "la464", + "-nographic", + "-m", + "512M", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-loongarch64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +test_commands = [ + "/usr/bin/bug-kill-zombie-esrch", + "/usr/bin/bug-kill-zombie-perm", + "/usr/bin/bug-zombie-syscalls", + "/usr/bin/bug-waitid-basic", +] +success_regex = ["(?m)^STARRY_GROUPED_TESTS_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", '(?m)^STARRY_GROUPED_TEST_FAILED:'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/qemu-riscv64.toml new file mode 100644 index 0000000000..9ee2fa6f55 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/qemu-riscv64.toml @@ -0,0 +1,27 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "rv64", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +test_commands = [ + "/usr/bin/bug-kill-zombie-esrch", + "/usr/bin/bug-kill-zombie-perm", + "/usr/bin/bug-zombie-syscalls", + "/usr/bin/bug-waitid-basic", +] +success_regex = ["(?m)^STARRY_GROUPED_TESTS_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", '(?m)^STARRY_GROUPED_TEST_FAILED:'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/qemu-x86_64.toml new file mode 100644 index 0000000000..0841f89ed2 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/zombie-bugfix/qemu-x86_64.toml @@ -0,0 +1,25 @@ +args = [ + "-nographic", + "-m", + "512M", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +test_commands = [ + "/usr/bin/bug-kill-zombie-esrch", + "/usr/bin/bug-kill-zombie-perm", + "/usr/bin/bug-zombie-syscalls", + "/usr/bin/bug-waitid-basic", +] +success_regex = ["(?m)^STARRY_GROUPED_TESTS_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", '(?m)^STARRY_GROUPED_TEST_FAILED:'] +timeout = 120