401 lines
8.9 KiB
Go
401 lines
8.9 KiB
Go
// +build linux
|
|
|
|
package fs
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/opencontainers/runc/libcontainer/cgroups"
|
|
"github.com/opencontainers/runc/libcontainer/configs"
|
|
libcontainerUtils "github.com/opencontainers/runc/libcontainer/utils"
|
|
)
|
|
|
|
var (
|
|
subsystems = subsystemSet{
|
|
&CpusetGroup{},
|
|
&DevicesGroup{},
|
|
&MemoryGroup{},
|
|
&CpuGroup{},
|
|
&CpuacctGroup{},
|
|
&PidsGroup{},
|
|
&BlkioGroup{},
|
|
&HugetlbGroup{},
|
|
&NetClsGroup{},
|
|
&NetPrioGroup{},
|
|
&PerfEventGroup{},
|
|
&FreezerGroup{},
|
|
&NameGroup{GroupName: "name=systemd", Join: true},
|
|
}
|
|
CgroupProcesses = "cgroup.procs"
|
|
HugePageSizes, _ = cgroups.GetHugePageSize()
|
|
)
|
|
|
|
var errSubsystemDoesNotExist = errors.New("cgroup: subsystem does not exist")
|
|
|
|
type subsystemSet []subsystem
|
|
|
|
func (s subsystemSet) Get(name string) (subsystem, error) {
|
|
for _, ss := range s {
|
|
if ss.Name() == name {
|
|
return ss, nil
|
|
}
|
|
}
|
|
return nil, errSubsystemDoesNotExist
|
|
}
|
|
|
|
type subsystem interface {
|
|
// Name returns the name of the subsystem.
|
|
Name() string
|
|
// Returns the stats, as 'stats', corresponding to the cgroup under 'path'.
|
|
GetStats(path string, stats *cgroups.Stats) error
|
|
// Removes the cgroup represented by 'cgroupData'.
|
|
Remove(*cgroupData) error
|
|
// Creates and joins the cgroup represented by 'cgroupData'.
|
|
Apply(*cgroupData) error
|
|
// Set the cgroup represented by cgroup.
|
|
Set(path string, cgroup *configs.Cgroup) error
|
|
}
|
|
|
|
type Manager struct {
|
|
mu sync.Mutex
|
|
Cgroups *configs.Cgroup
|
|
Paths map[string]string
|
|
}
|
|
|
|
// The absolute path to the root of the cgroup hierarchies.
|
|
var cgroupRootLock sync.Mutex
|
|
var cgroupRoot string
|
|
|
|
// Gets the cgroupRoot.
|
|
func getCgroupRoot() (string, error) {
|
|
cgroupRootLock.Lock()
|
|
defer cgroupRootLock.Unlock()
|
|
|
|
if cgroupRoot != "" {
|
|
return cgroupRoot, nil
|
|
}
|
|
|
|
root, err := cgroups.FindCgroupMountpointDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if _, err := os.Stat(root); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
cgroupRoot = root
|
|
return cgroupRoot, nil
|
|
}
|
|
|
|
type cgroupData struct {
|
|
root string
|
|
innerPath string
|
|
config *configs.Cgroup
|
|
pid int
|
|
}
|
|
|
|
func (m *Manager) Apply(pid int) (err error) {
|
|
if m.Cgroups == nil {
|
|
return nil
|
|
}
|
|
|
|
var c = m.Cgroups
|
|
|
|
d, err := getCgroupData(m.Cgroups, pid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.Paths != nil {
|
|
paths := make(map[string]string)
|
|
for name, path := range c.Paths {
|
|
_, err := d.path(name)
|
|
if err != nil {
|
|
if cgroups.IsNotFound(err) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
paths[name] = path
|
|
}
|
|
m.Paths = paths
|
|
return cgroups.EnterPid(m.Paths, pid)
|
|
}
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
paths := make(map[string]string)
|
|
for _, sys := range subsystems {
|
|
if err := sys.Apply(d); err != nil {
|
|
return err
|
|
}
|
|
// TODO: Apply should, ideally, be reentrant or be broken up into a separate
|
|
// create and join phase so that the cgroup hierarchy for a container can be
|
|
// created then join consists of writing the process pids to cgroup.procs
|
|
p, err := d.path(sys.Name())
|
|
if err != nil {
|
|
if cgroups.IsNotFound(err) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
paths[sys.Name()] = p
|
|
}
|
|
m.Paths = paths
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) Destroy() error {
|
|
if m.Cgroups.Paths != nil {
|
|
return nil
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if err := cgroups.RemovePaths(m.Paths); err != nil {
|
|
return err
|
|
}
|
|
m.Paths = make(map[string]string)
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) GetPaths() map[string]string {
|
|
m.mu.Lock()
|
|
paths := m.Paths
|
|
m.mu.Unlock()
|
|
return paths
|
|
}
|
|
|
|
func (m *Manager) GetStats() (*cgroups.Stats, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
stats := cgroups.NewStats()
|
|
for name, path := range m.Paths {
|
|
sys, err := subsystems.Get(name)
|
|
if err == errSubsystemDoesNotExist || !cgroups.PathExists(path) {
|
|
continue
|
|
}
|
|
if err := sys.GetStats(path, stats); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return stats, nil
|
|
}
|
|
|
|
func (m *Manager) Set(container *configs.Config) error {
|
|
for _, sys := range subsystems {
|
|
// Generate fake cgroup data.
|
|
d, err := getCgroupData(container.Cgroups, -1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Get the path, but don't error out if the cgroup wasn't found.
|
|
path, err := d.path(sys.Name())
|
|
if err != nil && !cgroups.IsNotFound(err) {
|
|
return err
|
|
}
|
|
|
|
if err := sys.Set(path, container.Cgroups); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if m.Paths["cpu"] != "" {
|
|
if err := CheckCpushares(m.Paths["cpu"], container.Cgroups.Resources.CpuShares); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Freeze toggles the container's freezer cgroup depending on the state
|
|
// provided
|
|
func (m *Manager) Freeze(state configs.FreezerState) error {
|
|
d, err := getCgroupData(m.Cgroups, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dir, err := d.path("freezer")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
prevState := m.Cgroups.Resources.Freezer
|
|
m.Cgroups.Resources.Freezer = state
|
|
freezer, err := subsystems.Get("freezer")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = freezer.Set(dir, m.Cgroups)
|
|
if err != nil {
|
|
m.Cgroups.Resources.Freezer = prevState
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) GetPids() ([]int, error) {
|
|
dir, err := getCgroupPath(m.Cgroups)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cgroups.GetPids(dir)
|
|
}
|
|
|
|
func (m *Manager) GetAllPids() ([]int, error) {
|
|
dir, err := getCgroupPath(m.Cgroups)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cgroups.GetAllPids(dir)
|
|
}
|
|
|
|
func getCgroupPath(c *configs.Cgroup) (string, error) {
|
|
d, err := getCgroupData(c, 0)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return d.path("devices")
|
|
}
|
|
|
|
func getCgroupData(c *configs.Cgroup, pid int) (*cgroupData, error) {
|
|
root, err := getCgroupRoot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if (c.Name != "" || c.Parent != "") && c.Path != "" {
|
|
return nil, fmt.Errorf("cgroup: either Path or Name and Parent should be used")
|
|
}
|
|
|
|
// XXX: Do not remove this code. Path safety is important! -- cyphar
|
|
cgPath := libcontainerUtils.CleanPath(c.Path)
|
|
cgParent := libcontainerUtils.CleanPath(c.Parent)
|
|
cgName := libcontainerUtils.CleanPath(c.Name)
|
|
|
|
innerPath := cgPath
|
|
if innerPath == "" {
|
|
innerPath = filepath.Join(cgParent, cgName)
|
|
}
|
|
|
|
return &cgroupData{
|
|
root: root,
|
|
innerPath: innerPath,
|
|
config: c,
|
|
pid: pid,
|
|
}, nil
|
|
}
|
|
|
|
func (raw *cgroupData) parentPath(subsystem, mountpoint, root string) (string, error) {
|
|
// Use GetThisCgroupDir instead of GetInitCgroupDir, because the creating
|
|
// process could in container and shared pid namespace with host, and
|
|
// /proc/1/cgroup could point to whole other world of cgroups.
|
|
initPath, err := cgroups.GetThisCgroupDir(subsystem)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// This is needed for nested containers, because in /proc/self/cgroup we
|
|
// see pathes from host, which don't exist in container.
|
|
relDir, err := filepath.Rel(root, initPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(mountpoint, relDir), nil
|
|
}
|
|
|
|
func (raw *cgroupData) path(subsystem string) (string, error) {
|
|
mnt, root, err := cgroups.FindCgroupMountpointAndRoot(subsystem)
|
|
// If we didn't mount the subsystem, there is no point we make the path.
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// If the cgroup name/path is absolute do not look relative to the cgroup of the init process.
|
|
if filepath.IsAbs(raw.innerPath) {
|
|
// Sometimes subsystems can be mounted togethger as 'cpu,cpuacct'.
|
|
return filepath.Join(raw.root, filepath.Base(mnt), raw.innerPath), nil
|
|
}
|
|
|
|
parentPath, err := raw.parentPath(subsystem, mnt, root)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Join(parentPath, raw.innerPath), nil
|
|
}
|
|
|
|
func (raw *cgroupData) join(subsystem string) (string, error) {
|
|
path, err := raw.path(subsystem)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := os.MkdirAll(path, 0755); err != nil {
|
|
return "", err
|
|
}
|
|
if err := writeFile(path, CgroupProcesses, strconv.Itoa(raw.pid)); err != nil {
|
|
return "", err
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func writeFile(dir, file, data string) error {
|
|
// Normally dir should not be empty, one case is that cgroup subsystem
|
|
// is not mounted, we will get empty dir, and we want it fail here.
|
|
if dir == "" {
|
|
return fmt.Errorf("no such directory for %s", file)
|
|
}
|
|
if err := ioutil.WriteFile(filepath.Join(dir, file), []byte(data), 0700); err != nil {
|
|
return fmt.Errorf("failed to write %v to %v: %v", data, file, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readFile(dir, file string) (string, error) {
|
|
data, err := ioutil.ReadFile(filepath.Join(dir, file))
|
|
return string(data), err
|
|
}
|
|
|
|
func removePath(p string, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if p != "" {
|
|
return os.RemoveAll(p)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func CheckCpushares(path string, c int64) error {
|
|
var cpuShares int64
|
|
|
|
if c == 0 {
|
|
return nil
|
|
}
|
|
|
|
fd, err := os.Open(filepath.Join(path, "cpu.shares"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fd.Close()
|
|
|
|
_, err = fmt.Fscanf(fd, "%d", &cpuShares)
|
|
if err != nil && err != io.EOF {
|
|
return err
|
|
}
|
|
|
|
if c > cpuShares {
|
|
return fmt.Errorf("The maximum allowed cpu-shares is %d", cpuShares)
|
|
} else if c < cpuShares {
|
|
return fmt.Errorf("The minimum allowed cpu-shares is %d", cpuShares)
|
|
}
|
|
|
|
return nil
|
|
}
|