内存映射系统开发

来源:互联网 发布:淘宝发货炫富图片大全 编辑:程序博客网 时间:2024/06/06 23:18

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

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

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

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

那么这里就有几个问题,首先是映射问题,起初我尝试了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 quicklyvar count = 0   // Windows only support 100ns levelvar DataBlockList list.List// CreateImage creates a image file and returns the addressfunc 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 datafunc 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索引:传送门

0 0
原创粉丝点击