Skip to content

Commit e5dca5d

Browse files
committed
Add memory policy support
Implement support for Linux memory policy in OCI spec PR: opencontainers/runtime-spec#1282 Signed-off-by: Antti Kervinen <[email protected]>
1 parent 9902a3d commit e5dca5d

File tree

15 files changed

+469
-10
lines changed

15 files changed

+469
-10
lines changed

features.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ var featuresCommand = cli.Command{
5858
IntelRdt: &features.IntelRdt{
5959
Enabled: &t,
6060
},
61+
MemoryPolicy: &features.MemoryPolicy{
62+
Modes: specconv.KnownMemoryPolicyModes(),
63+
Flags: specconv.KnownMemoryPolicyFlags(),
64+
},
6165
MountExtensions: &features.MountExtensions{
6266
IDMap: &features.IDMap{
6367
Enabled: &t,

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ require (
1515
github.com/moby/sys/userns v0.1.0
1616
github.com/mrunalp/fileutils v0.5.1
1717
github.com/opencontainers/cgroups v0.0.4
18-
github.com/opencontainers/runtime-spec v1.2.2-0.20250401095657-e935f995dd67
18+
github.com/opencontainers/runtime-spec v1.2.2-0.20250804081626-bfdffd548aa6
1919
github.com/opencontainers/selinux v1.12.0
2020
github.com/seccomp/libseccomp-golang v0.11.0
2121
github.com/sirupsen/logrus v1.9.3

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ github.com/mrunalp/fileutils v0.5.1 h1:F+S7ZlNKnrwHfSwdlgNSkKo67ReVf8o9fel6C3dkm
4747
github.com/mrunalp/fileutils v0.5.1/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
4848
github.com/opencontainers/cgroups v0.0.4 h1:XVj8P/IHVms/j+7eh8ggdkTLAxjz84ZzuFyGoE28DR4=
4949
github.com/opencontainers/cgroups v0.0.4/go.mod h1:s8lktyhlGUqM7OSRL5P7eAW6Wb+kWPNvt4qvVfzA5vs=
50-
github.com/opencontainers/runtime-spec v1.2.2-0.20250401095657-e935f995dd67 h1:Q+KewUGTMamIe6Q39xCD/T1NC1POmaTlWnhjikCrZHA=
51-
github.com/opencontainers/runtime-spec v1.2.2-0.20250401095657-e935f995dd67/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
50+
github.com/opencontainers/runtime-spec v1.2.2-0.20250804081626-bfdffd548aa6 h1:6S6r1L8VO9b1UfgIQi+nteqlElma9KDlzZw/nM3ctI0=
51+
github.com/opencontainers/runtime-spec v1.2.2-0.20250804081626-bfdffd548aa6/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
5252
github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
5353
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
5454
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

libcontainer/configs/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ type Config struct {
214214
// to limit the resources (e.g., L3 cache, memory bandwidth) the container has available
215215
IntelRdt *IntelRdt `json:"intel_rdt,omitempty"`
216216

217+
// MemoryPolicy specifies NUMA memory policy for the container.
218+
MemoryPolicy *LinuxMemoryPolicy `json:"memoryPolicy,omitempty"`
219+
217220
// RootlessEUID is set when the runc was launched with non-zero EUID.
218221
// Note that RootlessEUID is set to false when launched with EUID=0 in userns.
219222
// When RootlessEUID is set, runc creates a new userns for the container.

libcontainer/configs/memorypolicy.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package configs
2+
3+
// LinuxMemoryPolicy contains memory policy configuration.
4+
type LinuxMemoryPolicy struct {
5+
// Mode combines memory poliy mode and mode flags. Refer to
6+
// set_mempolicy() documentation for details.
7+
Mode uint
8+
// Contains NUMA nodes to which the mode applies.
9+
Nodes []int
10+
}

libcontainer/init_linux.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,10 @@ func setupIOPriority(config *initConfig) error {
659659
return nil
660660
}
661661

662+
func setupMemoryPolicy(config *configs.Config) error {
663+
return system.SetMempolicy(config.MemoryPolicy.Mode, config.MemoryPolicy.Nodes)
664+
}
665+
662666
func setupPersonality(config *configs.Config) error {
663667
return system.SetLinuxPersonality(config.Personality.Domain)
664668
}

libcontainer/setns_init_linux.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ func (l *linuxSetnsInit) Init() error {
110110
if err := apparmor.ApplyProfile(l.config.AppArmorProfile); err != nil {
111111
return err
112112
}
113+
if l.config.Config.MemoryPolicy != nil {
114+
if err := setupMemoryPolicy(l.config.Config); err != nil {
115+
return err
116+
}
117+
}
113118
if l.config.Config.Personality != nil {
114119
if err := setupPersonality(l.config.Config); err != nil {
115120
return err

libcontainer/specconv/spec_linux.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os"
1010
"path/filepath"
1111
"sort"
12+
"strconv"
1213
"strings"
1314
"sync"
1415
"time"
@@ -41,6 +42,12 @@ var (
4142
flag int
4243
}
4344
complexFlags map[string]func(*configs.Mount)
45+
mpolModeMap map[specs.MemoryPolicyModeType]uint
46+
mpolModeFMap map[specs.MemoryPolicyFlagType]uint
47+
)
48+
49+
const (
50+
maxNumaNode = 1023
4451
)
4552

4653
func initMaps() {
@@ -148,6 +155,22 @@ func initMaps() {
148155
m.IDMapping.Recursive = true
149156
},
150157
}
158+
159+
mpolModeMap = map[specs.MemoryPolicyModeType]uint{
160+
specs.MpolDefault: 0,
161+
specs.MpolPreferred: 1,
162+
specs.MpolBind: 2,
163+
specs.MpolInterleave: 3,
164+
specs.MpolLocal: 4,
165+
specs.MpolPreferredMany: 5,
166+
specs.MpolWeightedInterleave: 6,
167+
}
168+
169+
mpolModeFMap = map[specs.MemoryPolicyFlagType]uint{
170+
specs.MpolFStaticNodes: 1 << 15,
171+
specs.MpolFRelativeNodes: 1 << 14,
172+
specs.MpolFNumaBalancing: 1 << 13,
173+
}
151174
})
152175
}
153176

@@ -184,6 +207,30 @@ func KnownMountOptions() []string {
184207
return res
185208
}
186209

210+
// KnownMemoryPolicyModes returns the list of the known memory policy modes.
211+
// Used by `runc features`.
212+
func KnownMemoryPolicyModes() []string {
213+
initMaps()
214+
var res []string
215+
for k := range mpolModeMap {
216+
res = append(res, string(k))
217+
}
218+
sort.Strings(res)
219+
return res
220+
}
221+
222+
// KnownMemoryPolicyFlags returns the list of the known memory policy mode flags.
223+
// Used by `runc features`.
224+
func KnownMemoryPolicyFlags() []string {
225+
initMaps()
226+
var res []string
227+
for k := range mpolModeFMap {
228+
res = append(res, string(k))
229+
}
230+
sort.Strings(res)
231+
return res
232+
}
233+
187234
// AllowedDevices is the set of devices which are automatically included for
188235
// all containers.
189236
//
@@ -467,6 +514,28 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) {
467514
MemBwSchema: spec.Linux.IntelRdt.MemBwSchema,
468515
}
469516
}
517+
if spec.Linux.MemoryPolicy != nil &&
518+
(spec.Linux.MemoryPolicy.Mode != "" ||
519+
spec.Linux.MemoryPolicy.Nodes != "" ||
520+
len(spec.Linux.MemoryPolicy.Flags) > 0) {
521+
var ok bool
522+
specMp := spec.Linux.MemoryPolicy
523+
confMp := &configs.LinuxMemoryPolicy{}
524+
if confMp.Mode, ok = mpolModeMap[specMp.Mode]; !ok {
525+
return nil, fmt.Errorf("invalid memory policy mode %q", specMp.Mode)
526+
}
527+
if confMp.Nodes, err = parseListSet(specMp.Nodes, 0, maxNumaNode); err != nil {
528+
return nil, fmt.Errorf("invalid memory policy nodes %q: %w", specMp.Nodes, err)
529+
}
530+
for _, specFlag := range specMp.Flags {
531+
confModeFlag, ok := mpolModeFMap[specFlag]
532+
if !ok {
533+
return nil, fmt.Errorf("invalid memory policy flag %q", specFlag)
534+
}
535+
confMp.Mode |= confModeFlag
536+
}
537+
config.MemoryPolicy = confMp
538+
}
470539
if spec.Linux.Personality != nil {
471540
if len(spec.Linux.Personality.Flags) > 0 {
472541
logrus.Warnf("ignoring unsupported personality flags: %+v because personality flag has not supported at this time", spec.Linux.Personality.Flags)
@@ -1135,6 +1204,50 @@ func parseMountOptions(options []string) *configs.Mount {
11351204
return &m
11361205
}
11371206

1207+
// parseListSet parses "list set" syntax ("0,61-63,2") into a list ([0, 61, 62, 63, 2]).
1208+
func parseListSet(listSet string, minValue, maxValue int) ([]int, error) {
1209+
var result []int
1210+
parts := strings.Split(listSet, ",")
1211+
for _, part := range parts {
1212+
switch {
1213+
case part == "":
1214+
continue
1215+
case strings.Contains(part, "-"):
1216+
rangeParts := strings.Split(part, "-")
1217+
if len(rangeParts) != 2 {
1218+
return nil, fmt.Errorf("invalid range: %s", part)
1219+
}
1220+
start, err := strconv.Atoi(rangeParts[0])
1221+
if err != nil {
1222+
return nil, err
1223+
}
1224+
end, err := strconv.Atoi(rangeParts[1])
1225+
if err != nil {
1226+
return nil, err
1227+
}
1228+
if start > end {
1229+
return nil, fmt.Errorf("invalid range %s: start > end", part)
1230+
}
1231+
if start < minValue || end > maxValue {
1232+
return nil, fmt.Errorf("invalid range %s: out of range %d-%d", part, minValue, maxValue)
1233+
}
1234+
for i := start; i <= end; i++ {
1235+
result = append(result, i)
1236+
}
1237+
default:
1238+
num, err := strconv.Atoi(part)
1239+
if err != nil {
1240+
return nil, err
1241+
}
1242+
if num < minValue || num > maxValue {
1243+
return nil, fmt.Errorf("invalid value %d: out of range %d-%d", num, minValue, maxValue)
1244+
}
1245+
result = append(result, num)
1246+
}
1247+
}
1248+
return result, nil
1249+
}
1250+
11381251
func SetupSeccomp(config *specs.LinuxSeccomp) (*configs.Seccomp, error) {
11391252
if config == nil {
11401253
return nil, nil

libcontainer/specconv/spec_linux_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,111 @@ func TestSetupSeccomp(t *testing.T) {
338338
}
339339
}
340340

341+
func TestParseListSet(t *testing.T) {
342+
testCases := []struct {
343+
name string
344+
listSet string
345+
minValue, maxValue int
346+
expectedInts []int
347+
expectedErr string
348+
}{
349+
{
350+
name: "empty string",
351+
listSet: "",
352+
},
353+
{
354+
name: "single value at max",
355+
listSet: "42",
356+
expectedInts: []int{42},
357+
maxValue: 42,
358+
},
359+
{
360+
name: "full range from min to max",
361+
listSet: "0-7",
362+
expectedInts: []int{0, 1, 2, 3, 4, 5, 6, 7},
363+
maxValue: 7,
364+
},
365+
{
366+
name: "comma separated values",
367+
listSet: "1,3",
368+
expectedInts: []int{1, 3},
369+
maxValue: 5,
370+
},
371+
{
372+
name: "comma separated values and overlapping ranges",
373+
listSet: "3-5,1,4-6",
374+
expectedInts: []int{3, 4, 5, 1, 4, 5, 6},
375+
maxValue: 10,
376+
},
377+
{
378+
name: "empty ranges, single number ranges",
379+
listSet: ",2,,4-4,",
380+
expectedInts: []int{2, 4},
381+
maxValue: 4,
382+
},
383+
{
384+
name: "value out of range",
385+
listSet: "2-4,0",
386+
minValue: 1,
387+
maxValue: 4,
388+
expectedErr: "invalid value",
389+
},
390+
{
391+
name: "end of range out of range",
392+
listSet: "2-4,0",
393+
maxValue: 3,
394+
expectedErr: "invalid range",
395+
},
396+
{
397+
name: "start is greater than end",
398+
listSet: "4-3,0",
399+
maxValue: 1024,
400+
expectedErr: "invalid range",
401+
},
402+
{
403+
name: "syntax error in value",
404+
listSet: "a",
405+
maxValue: 1024,
406+
expectedErr: "invalid syntax",
407+
},
408+
{
409+
name: "syntax error at range start",
410+
listSet: "0,?-4",
411+
maxValue: 1024,
412+
expectedErr: "invalid syntax",
413+
},
414+
{
415+
name: "syntax error at range end",
416+
listSet: "0,4-,2",
417+
maxValue: 1024,
418+
expectedErr: "invalid syntax",
419+
},
420+
}
421+
for _, tc := range testCases {
422+
t.Run(tc.name, func(t *testing.T) {
423+
obsInts, obsErr := parseListSet(tc.listSet, tc.minValue, tc.maxValue)
424+
if len(obsInts) != len(tc.expectedInts) {
425+
t.Errorf("Expected %d ints, got %d (%v)", len(tc.expectedInts), len(obsInts), obsInts)
426+
}
427+
for i, v := range obsInts {
428+
if len(tc.expectedInts) > i && v != tc.expectedInts[i] {
429+
t.Errorf("Expected at index %d value %d, got %d", i, tc.expectedInts[i], v)
430+
}
431+
}
432+
switch {
433+
case tc.expectedErr == "" && obsErr != nil:
434+
t.Errorf("Expected no error, got %v", obsErr)
435+
case tc.expectedErr != "" && obsErr == nil:
436+
t.Errorf("Expected error %v, got nil", tc.expectedErr)
437+
case tc.expectedErr != "" && obsErr != nil:
438+
if !strings.Contains(obsErr.Error(), tc.expectedErr) {
439+
t.Errorf("Expected error containing %q, got %v", tc.expectedErr, obsErr)
440+
}
441+
}
442+
})
443+
}
444+
}
445+
341446
func TestLinuxCgroupWithMemoryResource(t *testing.T) {
342447
cgroupsPath := "/user/cgroups/path/id"
343448

libcontainer/standard_init_linux.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,13 @@ func (l *linuxStandardInit) Init() error {
238238
}
239239
}
240240

241+
// Set memory policy if specified.
242+
if l.config.Config.MemoryPolicy != nil {
243+
if err := setupMemoryPolicy(l.config.Config); err != nil {
244+
return err
245+
}
246+
}
247+
241248
// Set personality if specified.
242249
if l.config.Config.Personality != nil {
243250
if err := setupPersonality(l.config.Config); err != nil {

0 commit comments

Comments
 (0)