libcontainer: cgroups: add pids controller support

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>
This commit is contained in:
Aleksa Sarai 2015-12-15 00:33:56 +11:00
parent 5c46b9d438
commit db3159c9d9
8 changed files with 238 additions and 1 deletions

View File

@ -23,6 +23,7 @@ var (
&MemoryGroup{},
&CpuGroup{},
&CpuacctGroup{},
&PidsGroup{},
&BlkioGroup{},
&HugetlbGroup{},
&NetClsGroup{},

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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"`
@ -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"`

View File

@ -55,6 +55,7 @@ var subsystems = subsystemSet{
&fs.MemoryGroup{},
&fs.CpuGroup{},
&fs.CpuacctGroup{},
&fs.PidsGroup{},
&fs.BlkioGroup{},
&fs.HugetlbGroup{},
&fs.PerfEventGroup{},
@ -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
@ -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
}
@ -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 {

View File

@ -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"`

View File

@ -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)
}

View File

@ -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 {