Rust 与 C++ 在 Android Native 下的实践:10MB 图像共享内存优化
背景与痛点
在近期的 Android Native 开发中,遇到了一个跨进程数据传输的性能问题。
业务场景是:主进程(Rust)需要将一帧约 几MB 的 RGB 图像数据传递给子进程(C++ 的 OCR 识别引擎)进行文字识别。运行环境为 Android Root 设备,纯 Native 环境,不涉及 Java/JNI 层。
最初的实现方案是通过标准管道(stdin/stdout)进行进程间通信:
- Rust 将图像的宽、高及 几MB 像素数据写入子进程的 stdin。
- C++ 从 stdin 读取数据,构造
cv::Mat进行推理。 - C++ 将识别结果写入 stdout。
- Rust 从 stdout 读取结果。
在实际测试中,单帧处理的耗时数据如下:
convert: 2ms | write: 96ms | read: 213ms | total: 311ms
可以看到,write(Rust 写管道)耗时 96ms,read(C++ 读管道+推理)耗时 213ms。对于 10MB 的数据量来说,管道 I/O 带来的上下文切换和内存拷贝开销占据了较大比例。为了减少这部分开销,决定引入共享内存。
技术选型与实现
在 Android 环境下实现共享内存,通常有几种路径:
shm_open(POSIX):在 Android 上受 SELinux 限制,不同 App 间通常不可用。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 封装,通过析构函数自动 munmap 和 close。
#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 等待时间大幅减少