内存映射系统开发

news/2024/7/20 14:01:53 标签: 内存映射, 内存管理, 内存数据库, 索引, 备份

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

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

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

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

那么这里就有几个问题,首先是映射问题,起初我尝试了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索引:传送门


http://www.niftyadmin.cn/n/1072267.html

相关文章

[转载]Oracle数据库异构数据联结详解(1)

假设你有两个数据源头&#xff0c;如平面文件或表数据&#xff0c;并且要将他们兼并在一同&#xff0c;你将怎样做?假设他们有一个共同的属性&#xff0c;如客户ID&#xff0c;那么该打点方案应该是很明明&#xff1a;兼并相干的属性&#xff0c;在这个例子中&#xff0c;只需…

MySQL 管理软件

HeidiSQL web&#xff1a;http://www.heidisql.com免费的MySQL管理软件&#xff0c;而且是一个delphi的开源软件。Navicat for MySQL 9.1.6注册码SN: NAVD-ULQR-YD73-V3BRDownload: http://download.cnet.com/Navicat-MySQL-GUI/3000-10254_4-10071792.htmlnote&#xff1a;英文…

关于C#里面socket编程的一点理解

这几天倒腾socket编程&#xff0c;在博客园找到了张子阳大哥的socket编程的系列文章 看了很不错 。 网上也有许多类似的文章&#xff0c;我这里也纯属炒剩饭 。献丑了哈 首先还是用tcpListener 跟tcpClient吧 先把基础的搞懂了再说 socket编程的概念用我们以前老师的话说就是一…

mySql完全手册2011022401

【mySql部分函数】GreatestLeastBit_LengthLength 在MsSql下是Len,有一定差距&#xff1a;Length(中)2,Len(中)1CurDateCurTime/*时间函数比mssql多了很多~*/AES_EncryptAES_DecryptDecodeEncryptEncodeMd5PasswordSha/*Mssql没有此类加密函数*/Case When Else EndIf(te…

在Ubuntu 8.04里把Firefox 3Beta5升级到RC1

作者: weleoon 出自: http://www.linuxdiyf.com 现在RC1版曾经出了&#xff0c;Ubuntu8.04里的还是Beta5。1&#xff0c;sudo gedit /etc/apt/sources.list2&#xff0c;加deb http://ppa.launchpad.net/fta/ubuntu hardy main源然后生存。3&#xff0c;sudo apt-get updatesu…

自己动手写数据库(一) 从SQL语句开始

自己动手写数据库&#xff08;一&#xff09; 从SQL语句开始 关于本教程 作者&#xff1a;InsZVA&#xff0c;浙江大学 软件工程系&#xff0c;也是第一次尝试写数据库&#xff0c;如有错误还望多多指正&#xff0c;写此篇文章也望有抛砖引玉的作用。 面向读者&#xff1a;有…

MySQL 5.0新特征教程 存储历程:第一讲

滥觞&#xff1a;网海拾贝 作者&#xff1a;mysql AB;翻译:陈朋奕 Introduction 简介MySQL 5.0 新特征教程是为需要认识5.0版本新特征的MySQL老用户而写的。复杂的来说是先容了“存储历程、触发器、视图、信息架构视图”,在此谢谢译者陈朋奕的积极.停留这本书能像熟行专家那样…

自己动手写数据库(二)表的结构

自己动手写数据库&#xff08;二&#xff09;表的结构 一点点声明 我不准备贴多少代码&#xff0c;因为代码实现是开发者自由去做的&#xff0c;我这篇文章只是为了提供一个思路或者结构&#xff0c;具体的实现&#xff0c;如果做不出来&#xff0c;请多多练习基本功。 另外…