文接上篇用Rust实现MMKV-开篇

libc::mmap封装 Link to heading

首先需要对libcunsafe调用提供一个基本的封装。其中最核心的几个调用为:

  • 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)
    }
}

下一篇将介绍数据校验和加密的设计。