Skip to content

Commit

Permalink
libcontainer: cgroups: add pids controller support
Browse files Browse the repository at this point in the history
Add support for the pids cgroup controller to libcontainer, a recent
feature that is available in Linux 4.3+.

Unfortunately, due to the init process being written in Go, it can spawn
an an unknown number of threads due to blocked syscalls. This results in
the init process being unable to run properly, and thus small pids.max
configs won't work properly.

Signed-off-by: Aleksa Sarai <asarai@suse.com>
  • Loading branch information
cyphar committed Jan 11, 2016
1 parent 5c46b9d commit db3159c
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 1 deletion.
1 change: 1 addition & 0 deletions libcontainer/cgroups/fs/apply_raw.go
Expand Up @@ -23,6 +23,7 @@ var (
&MemoryGroup{},
&CpuGroup{},
&CpuacctGroup{},
&PidsGroup{},
&BlkioGroup{},
&HugetlbGroup{},
&NetClsGroup{},
Expand Down
62 changes: 62 additions & 0 deletions libcontainer/cgroups/fs/pids.go
@@ -0,0 +1,62 @@
// +build linux

package fs

import (
"fmt"
"strconv"

"github.com/opencontainers/runc/libcontainer/cgroups"
"github.com/opencontainers/runc/libcontainer/configs"
)

type PidsGroup struct {
}

func (s *PidsGroup) Name() string {
return "pids"
}

func (s *PidsGroup) Apply(d *cgroupData) error {
dir, err := d.join("pids")
if err != nil && !cgroups.IsNotFound(err) {
return err
}

if err := s.Set(dir, d.config); err != nil {
return err
}

return nil
}

func (s *PidsGroup) Set(path string, cgroup *configs.Cgroup) error {
if cgroup.Resources.PidsLimit != 0 {
// "max" is the fallback value.
limit := "max"

if cgroup.Resources.PidsLimit > 0 {
limit = strconv.FormatInt(cgroup.Resources.PidsLimit, 10)
}

if err := writeFile(path, "pids.max", limit); err != nil {
return err
}
}

return nil
}

func (s *PidsGroup) Remove(d *cgroupData) error {
return removePath(d.path("pids"))
}

func (s *PidsGroup) GetStats(path string, stats *cgroups.Stats) error {
value, err := getCgroupParamUint(path, "pids.current")
if err != nil {
return fmt.Errorf("failed to parse pids.current - %s", err)
}

stats.PidsStats.Current = value
return nil
}
83 changes: 83 additions & 0 deletions libcontainer/cgroups/fs/pids_test.go
@@ -0,0 +1,83 @@
// +build linux

package fs

import (
"strconv"
"testing"

"github.com/opencontainers/runc/libcontainer/cgroups"
)

const (
maxUnlimited = -1
maxLimited = 1024
)

func TestPidsSetMax(t *testing.T) {
helper := NewCgroupTestUtil("pids", t)
defer helper.cleanup()

helper.writeFileContents(map[string]string{
"pids.max": "max",
})

helper.CgroupData.config.Resources.PidsLimit = maxLimited
pids := &PidsGroup{}
if err := pids.Set(helper.CgroupPath, helper.CgroupData.config); err != nil {
t.Fatal(err)
}

value, err := getCgroupParamUint(helper.CgroupPath, "pids.max")
if err != nil {
t.Fatalf("Failed to parse pids.max - %s", err)
}

if value != maxLimited {
t.Fatalf("Expected %d, got %d for setting pids.max - limited", maxLimited, value)
}
}

func TestPidsSetUnlimited(t *testing.T) {
helper := NewCgroupTestUtil("pids", t)
defer helper.cleanup()

helper.writeFileContents(map[string]string{
"pids.max": strconv.Itoa(maxLimited),
})

helper.CgroupData.config.Resources.PidsLimit = maxUnlimited
pids := &PidsGroup{}
if err := pids.Set(helper.CgroupPath, helper.CgroupData.config); err != nil {
t.Fatal(err)
}

value, err := getCgroupParamString(helper.CgroupPath, "pids.max")
if err != nil {
t.Fatalf("Failed to parse pids.max - %s", err)
}

if value != "max" {
t.Fatalf("Expected %s, got %s for setting pids.max - unlimited", "max", value)
}
}

func TestPidsStats(t *testing.T) {
helper := NewCgroupTestUtil("pids", t)
defer helper.cleanup()

helper.writeFileContents(map[string]string{
"pids.current": strconv.Itoa(1337),
"pids.max": strconv.Itoa(maxLimited),
})

pids := &PidsGroup{}
stats := *cgroups.NewStats()
if err := pids.GetStats(helper.CgroupPath, &stats); err != nil {
t.Fatal(err)
}

if stats.PidsStats.Current != 1337 {
t.Fatalf("Expected %d, got %d for pids.current", 1337, stats.PidsStats.Current)
}
}
6 changes: 6 additions & 0 deletions libcontainer/cgroups/stats.go
Expand Up @@ -49,6 +49,11 @@ type MemoryStats struct {
Stats map[string]uint64 `json:"stats,omitempty"`
}

type PidsStats struct {
// number of pids in the cgroup
Current uint64 `json:"current,omitempty"`
}

type BlkioStatEntry struct {
Major uint64 `json:"major,omitempty"`
Minor uint64 `json:"minor,omitempty"`
Expand Down Expand Up @@ -80,6 +85,7 @@ type HugetlbStats struct {
type Stats struct {
CpuStats CpuStats `json:"cpu_stats,omitempty"`
MemoryStats MemoryStats `json:"memory_stats,omitempty"`
PidsStats PidsStats `json:"pids_stats,omitempty"`
BlkioStats BlkioStats `json:"blkio_stats,omitempty"`
// the map is in the format "size of hugepage: stats of the hugepage"
HugetlbStats map[string]HugetlbStats `json:"hugetlb_stats,omitempty"`
Expand Down
19 changes: 18 additions & 1 deletion libcontainer/cgroups/systemd/apply_systemd.go
Expand Up @@ -55,6 +55,7 @@ var subsystems = subsystemSet{
&fs.MemoryGroup{},
&fs.CpuGroup{},
&fs.CpuacctGroup{},
&fs.PidsGroup{},
&fs.BlkioGroup{},
&fs.HugetlbGroup{},
&fs.PerfEventGroup{},
Expand Down Expand Up @@ -233,7 +234,7 @@ func (m *Manager) Apply(pid int) error {
return err
}

// we need to manually join the freezer, net_cls, net_prio and cpuset cgroup in systemd
// we need to manually join the freezer, net_cls, net_prio, pids and cpuset cgroup in systemd
// because it does not currently support it via the dbus api.
if err := joinFreezer(c, pid); err != nil {
return err
Expand All @@ -246,6 +247,10 @@ func (m *Manager) Apply(pid int) error {
return err
}

if err := joinPids(c, pid); err != nil {
return err
}

if err := joinCpuset(c, pid); err != nil {
return err
}
Expand Down Expand Up @@ -394,6 +399,18 @@ func joinNetCls(c *configs.Cgroup, pid int) error {
return netcls.Set(path, c)
}

func joinPids(c *configs.Cgroup, pid int) error {
path, err := join(c, "pids", pid)
if err != nil && !cgroups.IsNotFound(err) {
return err
}
pids, err := subsystems.Get("pids")
if err != nil {
return err
}
return pids.Set(path, c)
}

func getSubsystemPath(c *configs.Cgroup, subsystem string) (string, error) {
mountpoint, err := cgroups.FindCgroupMountpoint(subsystem)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions libcontainer/configs/cgroup_unix.go
Expand Up @@ -64,6 +64,9 @@ type Resources struct {
// MEM to use
CpusetMems string `json:"cpuset_mems"`

// Process limit; set <= `0' to disable limit.
PidsLimit int64 `json:"pids_limit"`

// Specifies per cgroup weight, range is from 10 to 1000.
BlkioWeight uint16 `json:"blkio_weight"`

Expand Down
64 changes: 64 additions & 0 deletions libcontainer/integration/exec_test.go
Expand Up @@ -525,6 +525,70 @@ func testCpuShares(t *testing.T, systemd bool) {
}
}

func TestPids(t *testing.T) {
testPids(t, false)
}

func TestPidsSystemd(t *testing.T) {
if !systemd.UseSystemd() {
t.Skip("Systemd is unsupported")
}
testPids(t, true)
}

func testPids(t *testing.T, systemd bool) {
if testing.Short() {
return
}

rootfs, err := newRootfs()
ok(t, err)
defer remove(rootfs)

config := newTemplateConfig(rootfs)
if systemd {
config.Cgroups.Parent = "system.slice"
}
config.Cgroups.Resources.PidsLimit = -1

// Running multiple processes.
_, ret, err := runContainer(config, "", "/bin/sh", "-c", "/bin/true | /bin/true | /bin/true | /bin/true")
if err != nil && strings.Contains(err.Error(), "no such directory for pids.max") {
t.Skip("PIDs cgroup is unsupported")
}
ok(t, err)

if ret != 0 {
t.Fatalf("expected fork() to succeed with no pids limit")
}

// Enforce a permissive limit (shell + 6 * true + 3).
config.Cgroups.Resources.PidsLimit = 10
_, ret, err = runContainer(config, "", "/bin/sh", "-c", "/bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true")
if err != nil && strings.Contains(err.Error(), "no such directory for pids.max") {
t.Skip("PIDs cgroup is unsupported")
}
ok(t, err)

if ret != 0 {
t.Fatalf("expected fork() to succeed with permissive pids limit")
}

// Enforce a restrictive limit (shell + 6 * true + 3).
config.Cgroups.Resources.PidsLimit = 10
out, ret, err := runContainer(config, "", "/bin/sh", "-c", "/bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true")
if err != nil && strings.Contains(err.Error(), "no such directory for pids.max") {
t.Skip("PIDs cgroup is unsupported")
}
if err != nil && !strings.Contains(out.String(), "sh: can't fork") {
ok(t, err)
}

if err == nil {
t.Fatalf("expected fork() to fail with restrictive pids limit")
}
}

func TestRunWithKernelMemory(t *testing.T) {
testRunWithKernelMemory(t, false)
}
Expand Down
1 change: 1 addition & 0 deletions spec.go
Expand Up @@ -456,6 +456,7 @@ func createCgroupConfig(name string, spec *specs.LinuxRuntimeSpec, devices []*co
c.Resources.CpuRtPeriod = r.CPU.RealtimePeriod
c.Resources.CpusetCpus = r.CPU.Cpus
c.Resources.CpusetMems = r.CPU.Mems
c.Resources.PidsLimit = r.Pids.Limit
c.Resources.BlkioWeight = r.BlockIO.Weight
c.Resources.BlkioLeafWeight = r.BlockIO.LeafWeight
for _, wd := range r.BlockIO.WeightDevice {
Expand Down

0 comments on commit db3159c

Please sign in to comment.