内存映射系统开发

为了使用内存作为数据库的主要存储方式,开发内存数据库,我们需要对内存中的数据进行保证。即可以备份与还原,那么为了将内存中的数据备份到外存中,我们可以采取以下策略:

选取一个外存文件,将其映射到某个内存地址;

当更新内存时,适时地更新外存文件;

系统重启时,从外存中重新读取内存内容。

那么这里就有几个问题,首先是映射问题,起初我尝试了win32api:

    createFileMapping, _ = syscall.GetProcAddress(kernel32, "CreateFileMappingW")
    mapViewOfFile, _ = syscall.GetProcAddress(kernel32, "MapViewOfFile")
    createFile, _ = syscall.GetProcAddress(kernel32, "CreateFileW")
    closeHandle, _ = syscall.GetProcAddress(kernel32, "CloseHandle")
    flushViewOfFile, _ = syscall.GetProcAddress(kernel32, "FlushViewOfFile")
    unmapViewOfFile, _ = syscall.GetProcAddress(kernel32, "UnmapViewOfFile")

但是实际使用中,发现win32系统为了性能等方面的考虑,映射文件后,不一定就真正给你开辟了内存空间来访问,这时访问会出现异常,windows捕获异常后才会再次加载这些文件,导致测试时时好时坏。由此,我决定自己写一个文件映射的库。

当创建一个文件映射时,我们使用malloc申请一块内存,然后创建一个对应大小的文件,并将地址与文件路径的对应关系存入map中:

var ImageTable = make(map[uintptr]string)
var commonBuffer = make([]byte, 1024 * 1024)    // To clear the file quickly
var count = 0   // Windows only support 100ns level
var DataBlockList list.List
// CreateImage creates a image file and returns the address
func CreateImage(size int) (ip *DataBlock, err error) {
    defer signalBackup()
    filename := common.COMMON_DIR + "\\image\\" + strconv.Itoa(count)
    count++
    ip = &DataBlock {
        RawPtr:     uintptr(C.malloc(C.size_t(size))),
        Size:       size,
    }
    file, err := os.Create(filename)
    defer file.Close()
    for i := size;i > 0;i -= 1024 * 1024 {
        if i < 1024 * 1024 {
            file.Write(commonBuffer[:i])
        } else {
            file.Write(commonBuffer)
        }
    }
    ImageTable[ip.RawPtr] = filename
    DataBlockList.PushBack(ip)
    return
}

commonBuffer数组是一个比较大的0数组,为了快速刷到文件中,而不用每次创建文件都创建一个buffer。count变量的使用,是由于windows系统最多支持到100ns级的时间记录,为了让文件名序列化不受干扰,设置count变量,每次创建镜像都会自增。同时,该变量和映射表都会被备份在文件中,便于以后的恢复(后文会提及)。DataBlockList是一个DataBlock的链表,为了备份时方便遍历而设置,DataBlock是为了封装Read,Write函数而实现的数据类型。Read,Write函数用于读写内存,为什么不直接让使用者读写呢,因为数据库经常是多个会话同时操作,并行访问需要对资源加锁。以下便是DataBlock的实现:

type DataBlock struct {
    RawPtr      uintptr
    Size        int
    RWMutex     sync.RWMutex
}
func (b *DataBlock) read(offset, size int) ([]byte, error) {
    if offset + size > b.Size {
        return nil, OUT_OF_SIZE
    }
    var header reflect.SliceHeader
    header.Data = uintptr(b.RawPtr + uintptr(offset))
    header.Len = size
    header.Cap = size
    return *(*[]byte)(unsafe.Pointer(&header)), nil
}

func (b *DataBlock) Read(offset, size int) ([]byte, error) {
    b.RWMutex.RLock()
    defer b.RWMutex.RUnlock()
    return b.read(offset, size)
}

func (b *DataBlock) write(offset int, data []byte) (int, error) {
    var header reflect.SliceHeader
    size := len(data)
    header.Data = uintptr(b.RawPtr + uintptr(offset))
    header.Len = size
    header.Cap = size
    d := *(*[]byte)(unsafe.Pointer(&header))
    var n int
    if offset + size > b.Size {
        n = b.Size - offset
    } else {
        n = size
    }
    copy(d, data[:n])
    return n, nil
}

func (b *DataBlock) Write(offset int, data []byte) (int, error) {
    b.RWMutex.Lock()
    defer b.RWMutex.Unlock()
    var copies *DataBlock
    copies, ok := CopyTable[b]
    if !ok {
        return b.write(offset, data)
    }
    copies.Write(offset, data)
    return b.Write(offset, data)
}
DataBlock结构有一个读写锁做并发控制,允许多个线程同时读,但不允许写和任何其他写或者读操作同时进行,保证线程安全。
同时这段代码用到了CopyTable,CopyTable是一个记录正在复制的表,因为我们申请的空间是固定的,一旦需要扩容,就需要复制操作,而复制是一个很耗时的操作,在此过程中,其他线程可能操作/改变正在复制的数据,所以在write函数中加入复制表的判断,如果该块正在被复制,那么对该块的操作要同时写入两个副本。对CopyTable表的操作见copy函数:

func Copy(dst, src *DataBlock) (int, error) {
    CopyTable[src] = dst
    data, err := src.Read(0, src.Size)
    if err != nil {
        return 0, err
    }
    delete(CopyTable, src)
    return dst.Write(0, data)
}
有了Copy函数,重新分配内存的ReallocImage函数也是水到渠成,重新创建一个文件,大小为新的大小,申请一块对应的空间,然后建立映射表,删除原来的映射表:

// ReallocImage creates a new bigger image file and returns the new address with copying data
func ReallocImage(ip *DataBlock, size int) (*DataBlock, error) {
    defer signalBackup()
    filename := common.COMMON_DIR + "\\image\\" + strconv.Itoa(count)
    count++
    os.Remove(ImageTable[ip.RawPtr])
    ipNew := &DataBlock {
        RawPtr:     uintptr(C.malloc(C.size_t(size))),
        Size:       size,
    }
    file, err := os.Create(filename)
    defer file.Close()
    if err != nil {
        return nil, err
    }
    for i := size;i > 0;i -= 1024 * 1024 {
        if i < 1024 * 1024 {
            file.Write(commonBuffer[:i])
        } else {
            file.Write(commonBuffer)
        }
    }
    Copy(ipNew, ip)
    delete(ImageTable, ip.RawPtr)
    C.free(unsafe.Pointer(ip.RawPtr))
    ImageTable[ipNew.RawPtr] = filename
    RemoveBlock(ip)
    DataBlockList.PushBack(ipNew)
    return ipNew, nil
}
至于ReleaseImage函数,只需要释放资源,对应修改映射关系即可,接下来说明备份与恢复系统,备份系统由一个单独的线程控制,该线程在收到备份命令前阻塞,收到信号后开始备份,将映射表和count写入文件,然后分别写入每个镜像:

func signalBackup() {
    startBackup <- true
}

func BackupRoutine() {
    for {
        <- startBackup
        SaveImageTable()
        SyncAllImageToFile()
        close(startBackup)
        startBackup = make(chan bool, MAX_BAK_CHAN_SIZE)
    }
}
signalBackup函数负责发送一个备份信号,该函数会在用户进行文件镜像相关操作时触发,详情见上文内存镜像相关函数。


func (b *DataBlock) SyncToFile() error {
    data, err := b.Read(0, b.Size)
    if err != nil {
        return err
    }
    filename, ok := ImageTable[b.RawPtr]
    if !ok {
        return NOT_FOUND_ADDRESS
    }
    log.WriteLog("sys", "Sync " + strconv.Itoa(int(b.RawPtr)) + " to file.")
    return ioutil.WriteFile(filename, data, 0666)
}
func SaveImageTable() {
    tempTable := make(map[string]string)
    for k, v := range ImageTable {
        tempTable[strconv.Itoa(int(k))] = v
    }
    data, _ := json.Marshal(tempTable)
    ioutil.WriteFile(common.COMMON_DIR + "\\image\\imageTable.json", data, 0666)
    ioutil.WriteFile(common.COMMON_DIR + "\\image\\count", []byte(strconv.Itoa(count)), 0666)
    log.WriteLog("sys", "Save image table to file.")
}


DataBlock的SyncToFile将数据写入文件以持久化,SaveImageTable将映射关系写入文件。恢复系统基于之前的备份,将文件映射表读取出来,并对每个文件镜像重新装载,将新的指针和旧的指针做一一对应关系,存入RecoveryTable中,完成内存镜像的重装。具体代码详见:MonkeyDB2@GitHub MonkeyDB2目标为做支持sql与nosql的高性能内存数据库。

本系统可以配合内存池优化技术:传送门 来实现以下论文讲述的DCBST索引:传送门

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章