Rust与C++在Android Native下的跨进程通信:基于mmap的10MB图像数据共享

8 阅读5分钟

Rust 与 C++ 在 Android Native 下的实践:10MB 图像共享内存优化

背景与痛点

在近期的 Android Native 开发中,遇到了一个跨进程数据传输的性能问题。

业务场景是:主进程(Rust)需要将一帧约 几MB 的 RGB 图像数据传递给子进程(C++ 的 OCR 识别引擎)进行文字识别。运行环境为 Android Root 设备,纯 Native 环境,不涉及 Java/JNI 层。

最初的实现方案是通过标准管道(stdin/stdout)进行进程间通信:

  1. Rust 将图像的宽、高及 几MB 像素数据写入子进程的 stdin。
  2. C++ 从 stdin 读取数据,构造 cv::Mat 进行推理。
  3. C++ 将识别结果写入 stdout。
  4. Rust 从 stdout 读取结果。

在实际测试中,单帧处理的耗时数据如下:

convert: 2ms | write: 96ms | read: 213ms | total: 311ms

可以看到,write(Rust 写管道)耗时 96ms,read(C++ 读管道+推理)耗时 213ms。对于 10MB 的数据量来说,管道 I/O 带来的上下文切换和内存拷贝开销占据了较大比例。为了减少这部分开销,决定引入共享内存。

技术选型与实现

在 Android 环境下实现共享内存,通常有几种路径:

  1. shm_open (POSIX):在 Android 上受 SELinux 限制,不同 App 间通常不可用。
  2. ASharedMemory (Android NDK):需要通过 Binder 传递文件描述符(FD),纯 Native 下使用较为复杂。

由于程序拥有 Root 权限,且以 Native 二进制形式运行在 /data/local/tmp/ 下,这里选择了基于文件系统的 mmap 方案。配合 MAP_SHARED 标志,Linux 内核的 Page Cache 会将文件直接映射到物理内存中,读写速度接近直接操作内存。

架构设计

由于应用层能够严格保证时序(Rust 写完 -> 通知 C++ -> C++ 读 -> Rust 再写),不需要引入双缓冲或读写锁机制。

这里采用了 单块共享内存 + stdin 信号通知 的方案:

  • 数据通道:一块 10MB 的 mmap 内存,存放定长结构体。
  • 唤醒信号:Rust 写完数据后,向 C++ 的 stdin 写入 1 个字节。C++ 阻塞在 stdin 读取上,收到信号后直接去共享内存读取数据。这样避免了 CPU 轮询,同时保证了时序安全。

核心代码实现

1. 数据结构定义

两端定义内存布局完全一致的结构体,Rust 端使用 #[repr(C)] 保证无填充。

#[repr(C)]
pub struct SharedData {
    pub payload_len: u32,
    pub payload: [u8; 10 * 1024 * 1024], // 10MB 定长数组
}
struct SharedData {
    uint32_t payload_len;
    uint8_t payload[10 * 1024 * 1024]; // 10MB
};

2. Rust 端封装 (Writer)

使用 libc 库调用 mmap,封装为结构体并实现 Drop trait 自动释放资源。

use std::fs::{File, OpenOptions};
use std::os::unix::io::AsRawFd;
use std::{mem, ptr};
use libc;

const SHM_FILE_PATH: &str = "/data/local/tmp/rust_cpp_raw_shm.bin";

pub struct SharedMemWriter {
    file: File,
    ptr: *mut SharedData,
    size: usize,
}

impl SharedMemWriter {
    pub fn create() -> Result<Self, String> {
        let file = OpenOptions::new()
            .read(true).write(true).create(true).truncate(true)
            .open(SHM_FILE_PATH)
            .map_err(|e| format!("创建文件失败: {}", e))?;

        let size = mem::size_of::<SharedData>();
        file.set_len(size as u64)
            .map_err(|e| format!("ftruncate 失败: {}", e))?;

        let ptr = unsafe {
            libc::mmap(
                ptr::null_mut(), size,
                libc::PROT_READ | libc::PROT_WRITE,
                libc::MAP_SHARED, file.as_raw_fd(), 0,
            )
        };

        if ptr == libc::MAP_FAILED {
            return Err("mmap 失败".to_string());
        }
        Ok(Self { file, ptr: ptr as *mut SharedData, size })
    }

    /// 分块写入:将 width, height, padding, rgb 直接写入共享内存
    pub fn write_parts(&mut self, width: u32, height: u32, header_size: usize, rgb: &[u8]) {
        let shared = unsafe { &mut *self.ptr };
        let total_len = header_size + rgb.len();
        if total_len > shared.payload.len() {
            panic!("数据超出共享内存 10MB 限制!");
        }

        let payload = &mut shared.payload[..total_len];
        payload[..4].copy_from_slice(&width.to_le_bytes());
        payload[4..8].copy_from_slice(&height.to_le_bytes());
        if header_size > 8 {
            payload[8..header_size].fill(0); // padding 清零
        }
        payload[header_size..].copy_from_slice(rgb);
        
        shared.payload_len = total_len as u32;
        self.flush();
    }

    fn flush(&self) {
        unsafe { libc::msync(self.ptr as *mut libc::c_void, self.size, libc::MS_SYNC); }
    }
}

impl Drop for SharedMemWriter {
    fn drop(&mut self) {
        unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.size); }
    }
}

Rust 业务层调用逻辑: 根据配置决定走共享内存还是走原管道。如果走共享内存,写完数据后往 stdin 发送 1 字节信号。

if let Some(writer) = &mut self.share_mem_writer {
    // 1. 数据直接写入共享内存
    writer.write_parts(width, height, self.header_size.as_usize(), &rgb);
    
    // 2. 发送 1 字节信号通知 C++ 读取
    let mut stdin = self.stdin.lock().await;
    stdin.write_all(&[1u8]).await?;
    stdin.flush().await?;
} else {
    // 原有 stdin 管道写入逻辑...
}

3. C++ 端封装 (Reader)

C++ 端利用 RAII 封装,通过析构函数自动 munmapclose

#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

const char* SHM_FILE_PATH = "/data/local/tmp/rust_cpp_raw_shm.bin";

struct SharedData {
    uint32_t payload_len;
    uint8_t payload[10 * 1024 * 1024];
};

class SharedMemReader {
private:
    int fd = -1;
    void* mapped_ptr = nullptr;
    SharedData* data_ptr = nullptr;
    size_t file_size = 0;
public:
    bool open(const char* path) {
        fd = ::open(path, O_RDONLY);
        if (fd == -1) return false;
        struct stat st;
        if (fstat(fd, &st) == -1) { ::close(fd); return false; }
        file_size = st.st_size;
        mapped_ptr = mmap(nullptr, file_size, PROT_READ, MAP_SHARED, fd, 0);
        if (mapped_ptr == MAP_FAILED) {
            mapped_ptr = nullptr; ::close(fd); return false;
        }
        data_ptr = static_cast<SharedData*>(mapped_ptr);
        return true;
    }
    const SharedData* get_data() const { return data_ptr; }
    ~SharedMemReader() {
        if (mapped_ptr) munmap(mapped_ptr, file_size);
        if (fd != -1) ::close(fd);
    }
};

C++ 业务层调用逻辑: 启动时增加 --enable-share 参数解析。循环中,阻塞等待 stdin 信号,收到后直接通过指针偏移解析图像数据。

// 开启共享内存分支
if (enableShare) {
    // 1. 阻塞等待 1 字节信号
    char signal;
    std::cin.read(&signal, 1);
    if (std::cin.gcount() < 1) break;

    // 2. 从共享内存指针解析数据
    const SharedData* shared = shmReader.get_data();
    std::memcpy(&width, shared->payload, sizeof(width));
    std::memcpy(&height, shared->payload + sizeof(width), sizeof(height));
    
    // 3. 计算像素指针起始位置
    pixelsPtr = shared->payload + headerSize;
} 
// 未开启共享内存分支 (原逻辑)
else {
    // ... std::cin.read(header) ...
    pixels.resize(pixelSize);
    std::cin.read(pixels.data(), pixelSize);
    pixelsPtr = reinterpret_cast<const uint8_t*>(pixels.data());
}

// 构造 Mat 并推理
cv::Mat imgRgb = cv::Mat(height, width, CV_8UC3, const_cast<uint8_t*>(pixelsPtr)).clone();

性能对比

替换为共享内存方案后,单帧处理耗时数据对比:

优化前convert: 2ms | write: 96ms | read: 213ms | total: 311ms 优化后convert: 4ms | write: 6ms | read: 202ms | total: 213ms

  • write (96ms -> 6ms):减少了管道写入的内核态拷贝和系统调用开销,写入耗时显著降低。
  • read (213ms -> 202ms):C++ 端减少了流解析过程,剩余耗时主要为模型推理。
  • total (311ms -> 213ms):整体耗时减少约 31%。

总结

在 Android Root / Native 环境下,基于文件系统的 mmap 结合 1 字节 stdin 信号通知,是跨进程传输大数据的一种有效方案。它绕开了 Android 特有的 IPC 限制,实现了低延迟的内存共享。通过这次调整,进程间的 I/O 等待时间大幅减少