文接上篇用Rust实现MMKV-开篇。
libc::mmap封装 Link to heading
首先需要对libc
的unsafe
调用提供一个基本的封装。其中最核心的几个调用为:
libc::mmap
// 接收一个文件描述符,map成功后返回指向相应内存的指针libc::msync
// 将内存中的数据同步到文件libc::munmap
// 解除mmap释放内存
代码不多,如下:
// MAP_POPULATE是mmap调用需要的flag,具体作用可以查阅mmap文档:
// https://man7.org/linux/man-pages/man2/mmap.2.html
#[cfg(any(target_os = "linux", target_os = "android"))]
const MAP_POPULATE: libc::c_int = libc::MAP_POPULATE;
#[cfg(not(any(target_os = "linux", target_os = "android")))]
const MAP_POPULATE: libc::c_int = 0;
struct RawMmap {
// 数据指针,这里预期mmap成功指针非空,所以使用NonNull包装
ptr: NonNull<libc::c_void>,
// mmap的长度
len: usize,
}
impl RawMmap {
// 使用RawFd构造RawMmap
fn new(fd: RawFd, len: usize) -> io::Result<RawMmap> {
unsafe {
let ptr = libc::mmap(
ptr::null_mut(),
len as libc::size_t,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED | MAP_POPULATE,
fd,
0,
);
if ptr == libc::MAP_FAILED {
Err(io::Error::last_os_error())
} else {
libc::madvise(ptr, len, libc::MADV_WILLNEED);
Ok(RawMmap {
ptr: NonNull::new(ptr).unwrap(),
len,
})
}
}
}
// 将内存数据同步到文件
fn flush(&self, len: usize) -> io::Result<()> {
let result = unsafe { libc::msync(self.ptr.as_ptr(), len as libc::size_t, libc::MS_SYNC) };
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
}
// 为RawMmap实现Drop,结构体析构时将自动释放mmap
impl Drop for RawMmap {
fn drop(&mut self) {
unsafe {
libc::munmap(self.ptr.as_ptr(), self.len as libc::size_t);
}
}
}
// 将RawMmap解引用为&[u8],这样可以直接将其当作byte数组操作
impl Deref for RawMmap {
type Target = [u8];
#[inline]
fn deref(&self) -> &[u8] {
unsafe { slice::from_raw_parts(self.ptr.as_ptr() as *const u8, self.len) }
}
}
impl DerefMut for RawMmap {
#[inline]
fn deref_mut(&mut self) -> &mut [u8] {
unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr() as *mut u8, self.len) }
}
}
// 实现Send后可以在线程间传递RawMmap的所有权
unsafe impl Send for RawMmap {}
核心需求实现 Link to heading
接着我们从需求出发,这里需要一个组件能提供如下的能力:
- 顺序写入字节序列,考虑到需要去重key,去重的时候实际是丢弃旧的存储数据,把内存中的数据重新写入,所以也需要能够从头部写入;
- 顺序读取字节序列;
- 需要知道当前内容占用的空间,以便判断是否需要去重或者扩容。
考虑到mmap的特性:当map到一个文件的时候,其大小是固定的(默认为文件大小),如果我们初始新建文件,给文件设置大小后,内容会被填充为0。也就是说,不能以文件的长度来代表当前有效内容的长度(因为可能没有写满初始设置的文件大小)。而我们重新初始化时去读取文件内容时,需要知道有效的内容终止在何处,所以我们要将有效内容的长度也持久化,初始化时,先读取到内容长度,然后再使用buffer依次解析数据到末尾为止。这里想到buffer的设计:头4个字节存储buffer的长度,后续存储buffer的字节序列;可以用同样的方式设计整个文件格式:头部特定字节存储有效内容长度,后续存储内容。
以下是memory_map这个mod的关键代码:
// 文件头预留8字节用来存储有效内容的长度
const LEN_OFFSET: usize = 8;
// 使用单元素元组包装RawMmap
pub struct MemoryMap(RawMmap);
impl MemoryMap {
pub fn new(file: &File, len: usize) -> Self {
let raw_mmap = RawMmap::new(file.as_raw_fd(), len).unwrap();
MemoryMap(raw_mmap)
}
// 当前写指针的偏移量,为存储在头8个字节里面有效内容长度加上头部偏移量8
pub fn offset(&self) -> usize {
usize::from_be_bytes(self.0[0..LEN_OFFSET].try_into().unwrap()) + LEN_OFFSET
}
// 往末尾写数据
pub fn append(&mut self, value: Vec<u8>) -> std::io::Result<()> {
let data_len = value.len();
let start = self.offset();
let content_len = start - LEN_OFFSET;
let end = data_len + start;
let new_content_len = data_len + content_len;
// 这里直接将RawMmap当数组使用,这就是上面为其实现Deref的用处所在
// 更新有效内容长度
self.0[0..LEN_OFFSET].copy_from_slice(new_content_len.to_be_bytes().as_slice());
// 写入实际内容
self.0[start..end].copy_from_slice(value.as_slice());
// 将mmap的内容sync到文件
self.0.flush(end)
}
/// 将内容长度置0,以便重头开始写
pub fn reset(&mut self) -> io::Result<()> {
let len = 0usize;
self.0[0..LEN_OFFSET].copy_from_slice(len.to_be_bytes().as_slice());
self.0.flush(LEN_OFFSET)
}
// 从mmap中读取slice
pub fn read(&self, range: Range<usize>) -> &[u8] {
self.0[range].as_ref()
}
}
而当写入一些数据之后,下次启动重新初始化时,需要从文件中顺序解码buffer,这里可以直接为MemoryMap
实现Iterator
:
pub struct Iter<'a> {
mm: &'a MemoryMap,
// 保存当前的起始位置
start: usize,
end: usize,
}
impl MemoryMap {
pub fn iter(&self) -> Iter {
// 由于头8个字节存储的是长度,所以buffer的数据起始要偏移8
let start = LEN_OFFSET;
// 有效数据长度,迭代器将从sart位置一直读取到end位置结束
let end = self.offset();
Iter {
mm: self,
start,
end
}
}
}
impl <'a> Iterator for Iter<'a> {
// 关联类型为Buffer,即可以从MemoryMap按照Iterator的方式读取出Buffer
type Item = Buffer;
fn next(&mut self) -> Option<Self::Item> {
if self.start >= self.end {
return None
}
// 解码Buffer,并得到已经解码的数据的长度
let (buffer, len) = Buffer::from_encoded_bytes(
self.mm.read(self.start..self.end).as_ref()
);
// 起始位置后移到已经读取完的位置
self.start += len as usize;
Some(buffer)
}
}
下一篇将介绍数据校验和加密的设计。