Skip to content

Commit

Permalink
add logic to stack lcow layers on a single VPMEM device
Browse files Browse the repository at this point in the history
Signed-off-by: Maksim An <maksiman@microsoft.com>
  • Loading branch information
anmaxvl committed Feb 2, 2021
1 parent 251b969 commit 9c8e6fe
Show file tree
Hide file tree
Showing 9 changed files with 735 additions and 113 deletions.
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de h1:dlfGmNcE3jDAec
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd h1:JNn81o/xG+8NEo3bC/vx9pbi/g2WI8mtP2/nXzu297Y=
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
github.com/coreos/go-systemd/v22 v22.0.0 h1:XJIw/+VlJ+87J+doOxznsAWIdmWuViOVhkQamW5YV28=
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
Expand All @@ -32,6 +33,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
Expand Down
8 changes: 8 additions & 0 deletions internal/guestrequest/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,18 @@ type LCOWMappedDirectory struct {
ReadOnly bool `json:"ReadOnly,omitempty"`
}

// One of potentially multiple read-only layers mapped on a VPMem device
type LCOWMappedLayer struct {
DeviceOffsetInBytes uint64 `json:"DeviceOffsetInBytes,omitempty"`
DeviceSizeInBytes uint64 `json:"DeviceSizeInBytes,omitempty"`
}

// Read-only layers over VPMem
type LCOWMappedVPMemDevice struct {
DeviceNumber uint32 `json:"DeviceNumber,omitempty"`
MountPath string `json:"MountPath,omitempty"`
// Mapping is ignored when MountPath is not empty
Mapping LCOWMappedLayer `json:"Mapping,omitempty"`
}

type LCOWMappedVPCIDevice struct {
Expand Down
12 changes: 6 additions & 6 deletions internal/schema2/virtual_p_mem_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
*
* No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen)
*
* API version: 2.1
* API version: 2.4
* Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
*/

package hcsschema

type VirtualPMemDevice struct {
HostPath string `json:"HostPath,omitempty"`

ReadOnly bool `json:"ReadOnly,omitempty"`

ImageFormat string `json:"ImageFormat,omitempty"`
HostPath string `json:"HostPath,omitempty"`
ReadOnly bool `json:"ReadOnly,omitempty"`
ImageFormat string `json:"ImageFormat,omitempty"`
SizeBytes int32 `json:"SizeBytes,omitempty"`
Mappings map[string]VirtualPMemMapping `json:"Mappings,omitempty"`
}
15 changes: 15 additions & 0 deletions internal/schema2/virtual_p_mem_mapping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* HCS API
*
* No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen)
*
* API version: 2.4
* Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
*/

package hcsschema

type VirtualPMemMapping struct {
HostPath string `json:"HostPath,omitempty"`
ImageFormat string `json:"ImageFormat,omitempty"`
}
13 changes: 9 additions & 4 deletions internal/uvm/create_lcow.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,16 @@ func CreateLCOW(ctx context.Context, opts *OptionsLCOW) (_ *UtilityVM, err error
},
}
// Add to our internal structure
uvm.vpmemDevices[0] = &vpmemInfo{
hostPath: opts.RootFSFile,
uvmPath: "/",
refCount: 1,
pMemDev := newVPMemDevice()
mapping := &vpmemMapping{
hostPath: opts.RootFSFile,
uvmPath: "/",
refCount: 1,
deviceOffset: 0,
deviceSize: DefaultVPMemSizeBytes,
}
pMemDev.mapDevice(ctx, mapping)
uvm.vpmemDevices[0] = pMemDev
}

vmDebugging := false
Expand Down
134 changes: 134 additions & 0 deletions internal/uvm/mem_allocator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package uvm

import (
"github.com/pkg/errors"
)

var (
ErrInvalidMemoryClass = errors.New("invalid memory class")
)

type memSlot struct {
memCls uint32
offset uint64
size uint64
next *memSlot
}

type memoryAllocator struct {
slots [MemClsNum]*memSlot
busy map[uint64]*memSlot
}

// newDefaultMemoryAllocator returns an allocator with a single 0-offset 4GB memory slot
func newDefaultMemoryAllocator() memoryAllocator {
mem := memoryAllocator{
busy: make(map[uint64]*memSlot),
}
mem.slots[MemClsNum-1] = &memSlot{
offset: 0,
size: 4 * 1024 * MegaByte,
}
return mem
}

func (mc *memoryAllocator) findFreeOffset(memCls uint32) (uint64, error) {
memClsCount := uint32(len(mc.slots))
if memCls >= MemClsNum {
return 0, ErrInvalidMemoryClass
}

for i := memCls; i < memClsCount; i++ {
if ms := mc.slots[i]; ms != nil {
return ms.offset, nil
}
}
return 0, ErrNotEnoughSpace
}

// allocate returns first available memory slot for a given `memCls` size class. If none left,
// expands the existing memCls
func (mc *memoryAllocator) allocate(memCls uint32) (*memSlot, error) {
if memCls >= MemClsNum {
return nil, ErrInvalidMemoryClass
}

alloc := func() *memSlot {
s := mc.slots[memCls]
if s != nil {
mc.slots[memCls] = s.next
s.next = nil
mc.busy[s.offset] = s
return s
}
return nil
}

if slot := alloc(); slot != nil {
return slot, nil
}

if err := mc.expand(memCls); err != nil {
return nil, err
}

slot := alloc()
if slot != nil {
return slot, nil
}

return nil, ErrNotEnoughSpace
}

// release returns resources back, also making sure that the `offset` and `memCls` are valid
func (mc *memoryAllocator) release(memCls uint32, offset uint64) error {
ms, ok := mc.busy[offset]
if !ok {
return errors.Errorf("no memory allocated at offset: %d", offset)
}

if ms == nil {
return errors.Errorf("expected memory slot at offset: %d, found nil instead", offset)
}

if memCls != ms.memCls {
return errors.Errorf("invalid release request. memory slot types mismatch: actual=%d, requested=%d", ms.memCls, memCls)
}

delete(mc.busy, offset)
ms.next = mc.slots[ms.memCls]
mc.slots[ms.memCls] = ms
return nil
}

// expand breaks down the next memory class into smaller memCls type segments
func (mc *memoryAllocator) expand(memCls uint32) error {
nextMemCls := memCls + 1
if nextMemCls >= MemClsNum {
return ErrNotEnoughSpace
}

if mc.slots[nextMemCls] == nil {
err := mc.expand(nextMemCls)
if err != nil {
return err
}
}

nextSlot := mc.slots[nextMemCls]
mc.slots[nextMemCls] = nextSlot.next
nextSlot.next = nil

offset := nextSlot.offset
memClsSize := getMemoryClassSize(memCls)
for i := uint64(0); i < 4; i++ {
n := &memSlot{
size: memClsSize,
offset: offset + (3-i)*memClsSize,
memCls: memCls,
}
n.next = mc.slots[memCls]
mc.slots[memCls] = n
}
return nil
}
147 changes: 147 additions & 0 deletions internal/uvm/mem_allocator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package uvm

import "testing"

func testAllocate(t *testing.T, ma *memoryAllocator, memCls uint32, expectedOffset uint64) {
s, err := ma.allocate(memCls)
if err != nil {
t.Errorf("unexpected error: %s", err)
}
if s.offset != expectedOffset {
t.Errorf("wrong offset, expected offset=%d, actual offset=%d", expectedOffset, s.offset)
}
if len(ma.busy) != 1 {
t.Error("memory slot wasn't marked as busy")
}
}

func Test_MemAlloc_findFreeOffset(t *testing.T) {
ma := newDefaultMemoryAllocator()
offset, err := ma.findFreeOffset(0)
if err != nil {
t.Errorf("unexpected error: %s", err)
}

if offset != 0 {
t.Errorf("expected offset=%d, got %d", 0, offset)
}
}

func Test_MemAlloc_allocate_without_expand(t *testing.T) {
ma := &memoryAllocator{
busy: make(map[uint64]*memSlot),
}
ma.slots[0] = &memSlot{
offset: 0,
size: MegaByte,
memCls: 0,
}

testAllocate(t, ma, 0, 0)
}

func Test_MemAlloc_allocate_not_enough_space(t *testing.T) {
ma := memoryAllocator{
busy: make(map[uint64]*memSlot),
}

_, err := ma.allocate(0)
if err == nil {
t.Error("expected error, got nil")
}
if err != ErrNotEnoughSpace {
t.Errorf("expected error=%s, got error=%s", ErrNotEnoughSpace, err)
}
}

func Test_MemAlloc_expand(t *testing.T) {
ma := &memoryAllocator{
busy: make(map[uint64]*memSlot),
}
ma.slots[1] = &memSlot{
offset: 0,
size: 4 * MegaByte,
memCls: 1,
}

err := ma.expand(0)
if err != nil {
t.Errorf("unexpected error: %s", err)
}

if ma.slots[1] != nil {
t.Error("slot 1 was not broken into smaller pieces")
}

s := ma.slots[0]
for i := 0; i < 4; i++ {
expected := uint64(i) * MegaByte
if s.offset != expected {
t.Errorf("unexpected expand. expected offset=%d, got offset=%d", expected, s.offset)
}
s = s.next
}

if s != nil {
t.Errorf("extra memory slots: %v", s)
}
}

func Test_MemAlloc_allocate_automatically_expands(t *testing.T) {
ma := memoryAllocator{
busy: make(map[uint64]*memSlot),
}
ma.slots[2] = &memSlot{
offset: MegaByte,
size: 16 * MegaByte,
memCls: 2,
}

testAllocate(t, &ma, 0, MegaByte)

if ma.slots[1] == nil {
t.Error("memory not extended for the memCls 1")
}
if ma.slots[1].offset != 5*MegaByte {
t.Error("wrong offset for the memCls 1")
}
if ma.slots[2] != nil {
t.Error("expected to find nil")
}
}

func Test_MemAlloc_alloc_and_release(t *testing.T) {
ma := &memoryAllocator{
busy: make(map[uint64]*memSlot),
}
ma.slots[0] = &memSlot{
offset: 0,
size: MegaByte,
memCls: 0,
}

testAllocate(t, ma, 0, 0)

err := ma.release(0, 0)
if err != nil {
t.Errorf("error releasing resources: %s", err)
}
if len(ma.busy) != 0 {
t.Errorf("resources not marked as free: %v", ma.busy)
}
if ma.slots[0] == nil || ma.slots[0].offset != 0 {
t.Error("resource not assigned back to the free list")
}
}

func Test_MemAlloc_alloc_invalid_class(t *testing.T) {
ma := &memoryAllocator{}

_, err := ma.allocate(MemClsNum)
if err == nil {
t.Error("no error returned")
}
if err != ErrInvalidMemoryClass {
t.Errorf("expected error=%s, got error=%s", ErrInvalidMemoryClass, err)
}
}
Loading

0 comments on commit 9c8e6fe

Please sign in to comment.