內存映射系統開發

爲了使用內存作爲數據庫的主要存儲方式,開發內存數據庫,我們需要對內存中的數據進行保證。即可以備份與還原,那麼爲了將內存中的數據備份到外存中,我們可以採取以下策略:

選取一個外存文件,將其映射到某個內存地址;

當更新內存時,適時地更新外存文件;

系統重啓時,從外存中重新讀取內存內容。

那麼這裏就有幾個問題,首先是映射問題,起初我嘗試了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索引:傳送門

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