1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """A class for managing the Linux cgroup subsystem."""
7 from __future__ import print_function
15 from chromite.lib import cros_build_lib
16 from chromite.lib import locking
17 from chromite.lib import osutils
18 from chromite.lib import signals
19 from chromite.lib import sudo
22 # Rough hierarchy sketch:
23 # - all cgroup aware cros code should nest here.
24 # - No cros code should modify this namespace- this is user/system configurable
25 # - only. A release_agent can be specified, although we won't use it.
28 # - cbuildbot instances land here only when they're cleaning their task pool.
29 # - this root namespace is *not* auto-removed; it's left so that user/system
30 # - configuration is persistant.
31 # cros/%(process-name)s/
34 # - a cbuildbot job pool, owned by pid. These are autocleaned.
35 # cros/cbuildbot/%(pid)i/
37 # - a job pool using process that was invoked by cbuildbot.
38 # - for example, cros/cbuildbot/42/cros_sdk:34
39 # - this pattern continues arbitrarily deep, and is autocleaned.
40 # cros/cbuildbot/%(pid1)i/%(basename_of_pid2)s:%(pid2)i/
42 # An example for cros_sdk (pid 552) would be:
44 # and it's children would be accessible in 552/tasks, or
45 # would create their own namespace w/in and assign themselves to it.
48 class _GroupWasRemoved(Exception):
49 """Exception representing when a group was unexpectedly removed.
51 Via design, this should only be possible when instantiating a new
52 pool, but the parent pool has been removed- this means effectively that
53 we're supposed to shutdown (either we've been sigterm'd and ignored it,
58 def _FileContains(filename, strings):
59 """Greps a group of expressions, returns whether all were found."""
60 contents = osutils.ReadFile(filename)
61 return all(s in contents for s in strings)
64 def EnsureInitialized(functor):
65 """Decorator for Cgroup methods to ensure the method is ran only if inited"""
67 def f(self, *args, **kwargs):
68 # pylint: disable=W0212
70 return functor(self, *args, **kwargs)
72 # Dummy up our wrapper to make it look like what we're wrapping,
73 # and expose the underlying docstrings.
74 f.__name__ = functor.__name__
75 f.__doc__ = functor.__doc__
76 f.__module__ = functor.__module__
82 """Class representing a group in cgroups hierarchy.
84 Note the instance may not exist on disk; it will be created as necessary.
85 Additionally, because cgroups is kernel maintained (and mutated on the fly
86 by processes using it), chunks of this class are /explicitly/ designed to
87 always go back to disk and recalculate values.
90 path: Absolute on disk pathway to the cgroup directory.
91 tasks: Pids contained in this immediate cgroup, and the owning pids of
92 any first level groups nested w/in us.
93 all_tasks: All Pids, and owners of nested groups w/in this point in
95 nested_groups: The immediate cgroups nested w/in this one. If this
96 cgroup is 'cbuildbot/buildbot', 'cbuildbot' would have a nested_groups
97 of [Cgroup('cbuildbot/buildbot')] for example.
98 all_nested_groups: All cgroups nested w/in this one, regardless of depth.
99 pid_owner: Which pid owns this cgroup, if the cgroup is following cros
100 conventions for group naming.
103 NEEDED_SUBSYSTEMS = ('cpuset',)
104 PROC_PATH = '/proc/cgroups'
105 _MOUNT_ROOT_POTENTIALS = ('/sys/fs/cgroup',)
106 _MOUNT_ROOT_FALLBACK = '/dev/cgroup'
109 # Whether or not the cgroup implementation does auto inheritance via
110 # cgroup.clone_children
111 _SUPPORTS_AUTOINHERIT = False
114 @cros_build_lib.MemoizedSingleCall
116 """If cgroups are supported, initialize the system state"""
117 if not cls.IsSupported():
120 def _EnsureMounted(mnt, args):
121 if _FileContains('/proc/mounts', [mnt]):
124 # Grab a lock so in the off chance we have multiple programs (like two
125 # cros_sdk launched in parallel) running this init logic, we don't end
126 # up mounting multiple times.
127 lock_path = '/tmp/.chromite.cgroups.lock'
128 with locking.FileLock(lock_path, 'cgroup lock') as lock:
130 if _FileContains('/proc/mounts', [mnt]):
133 # Not all distros mount cgroup_root to sysfs.
134 osutils.SafeMakedirs(mnt, sudo=True)
135 cros_build_lib.SudoRunCommand(['mount'] + args + [mnt], print_cmd=False)
139 mount_root_args = ['-t', 'tmpfs', 'cgroup_root']
141 opts = ','.join(cls.NEEDED_SUBSYSTEMS)
142 cgroup_root_args = ['-t', 'cgroup', '-o', opts, 'cros']
144 return _EnsureMounted(cls.MOUNT_ROOT, mount_root_args) and \
145 _EnsureMounted(cls.CGROUP_ROOT, cgroup_root_args)
148 @cros_build_lib.MemoizedSingleCall
150 """Function to sanity check if everything is setup to use cgroups"""
151 if not cls.InitSystem():
153 cls._SUPPORTS_AUTOINHERIT = os.path.exists(
154 os.path.join(cls.CGROUP_ROOT, 'cgroup.clone_children'))
158 @cros_build_lib.MemoizedSingleCall
159 def IsSupported(cls):
160 """Sanity check as to whether or not cgroups are supported."""
161 # Is the cgroup subsystem even enabled?
163 if not os.path.exists(cls.PROC_PATH):
166 # Does it support the subsystems we want?
167 if not _FileContains(cls.PROC_PATH, cls.NEEDED_SUBSYSTEMS):
170 for potential in cls._MOUNT_ROOT_POTENTIALS:
171 if os.path.exists(potential):
172 cls.MOUNT_ROOT = potential
175 cls.MOUNT_ROOT = cls._MOUNT_ROOT_FALLBACK
177 cls.CGROUP_ROOT = os.path.join(cls.MOUNT_ROOT, 'cros')
180 def __init__(self, namespace, autoclean=True, lazy_init=False, parent=None,
181 _is_root=False, _overwrite=True):
182 """Initalize a cgroup instance.
185 namespace: What cgroup namespace is this in? cbuildbot/1823 for example.
186 autoclean: Should this cgroup be removed once unused?
187 lazy_init: Should we create the cgroup immediately, or when needed?
188 parent: A Cgroup instance; if the namespace is cbuildbot/1823, then the
189 parent *must* be the cgroup instance for namespace cbuildbot.
190 _is_root: Internal option, shouldn't be used by consuming code.
191 _overwrite: Internal option, shouldn't be used by consuming code.
194 self._overwrite = bool(_overwrite)
199 namespace = os.path.normpath(namespace)
201 raise ValueError("Either _is_root must be set to True, or parent must "
203 if namespace in ('.', ''):
204 raise ValueError("Invalid namespace %r was given" % (namespace,))
206 self.namespace = namespace
207 self.autoclean = autoclean
213 def _LimitName(self, name, for_path=False, multilevel=False):
214 """Translation function doing sanity checks on derivative namespaces
216 If you're extending this class, you should be using this for any namespace
217 operations that pass through a nested group.
219 # We use a fake pathway here, and this code must do so. To calculate the
220 # real pathway requires knowing CGROUP_ROOT, which requires sudo
221 # potentially. Since this code may be invoked just by loading the module,
222 # no execution/sudo should occur. However, if for_path is set, we *do*
223 # require CGROUP_ROOT- which is fine, since we sort that on the way out.
224 fake_path = os.path.normpath(os.path.join('/fake-path', self.namespace))
225 path = os.path.normpath(os.path.join(fake_path, name))
227 # Ensure that the requested pathway isn't trying to sidestep what we
228 # expect, and in the process it does internal validation checks.
229 if not path.startswith(fake_path + '/'):
230 raise ValueError("Name %s tried descending through this namespace into"
231 " another; this isn't allowed." % (name,))
232 elif path == self.namespace:
233 raise ValueError("Empty name %s" % (name,))
234 elif os.path.dirname(path) != fake_path and not multilevel:
235 raise ValueError("Name %s is multilevel, but disallowed." % (name,))
237 # Get the validated/normalized name.
238 name = path[len(fake_path):].strip('/')
240 return os.path.join(self.path, name)
245 return os.path.abspath(os.path.join(self.CGROUP_ROOT, self.namespace))
249 s = set(x.strip() for x in self.GetValue('tasks', '').splitlines())
250 s.update(x.pid_owner for x in self.nested_groups)
257 for group in self.all_nested_groups:
258 s.update(group.tasks)
262 def nested_groups(self):
266 targets = [x for x in os.listdir(path)
267 if os.path.isdir(os.path.join(path, x))]
268 except EnvironmentError as e:
269 if e.errno != errno.ENOENT:
272 targets = [self.AddGroup(x, lazy_init=True, _overwrite=False)
275 # Suppress initialization checks- if it exists on disk, we know it
276 # is already initialized.
282 def all_nested_groups(self):
283 # Do a depth first traversal.
286 for subgroup in walk(group.nested_groups):
289 return list(walk(self.nested_groups))
292 @cros_build_lib.MemoizedSingleCall
294 # Ensure it's in cros namespace- if it is outside of the cros namespace,
295 # we shouldn't make assumptions about the naming convention used.
296 if not self.GroupIsAParent(_cros_node):
298 # See documentation at the top of the file for the naming scheme.
299 # It's basically "%(program_name)s:%(owning_pid)i" if the group
301 return os.path.basename(self.namespace).rsplit(':', 1)[-1]
303 def GroupIsAParent(self, node):
304 """Is the given node a parent of us?"""
305 parent_path = node.path + '/'
306 return self.path.startswith(parent_path)
308 def GetValue(self, key, default=None):
309 """Query a cgroup configuration key from disk.
311 If the file doesn't exist, return the given default.
314 return osutils.ReadFile(os.path.join(self.path, key))
315 except EnvironmentError as e:
316 if e.errno != errno.ENOENT:
320 def _AddSingleGroup(self, name, **kwargs):
321 """Method for creating a node nested within this one.
323 Derivative classes should override this method rather than AddGroup;
324 see __init__ for the supported keywords.
326 return self.__class__(os.path.join(self.namespace, name), **kwargs)
328 def AddGroup(self, name, **kwargs):
329 """Add and return a cgroup nested in this one.
331 See __init__ for the supported keywords. If this isn't a direct child
332 (for example this instance is cbuildbot, and the name is 1823/x), it'll
333 create the intermediate groups as lazy_init=True, setting autoclean to
334 via the logic described for autoclean_parents below.
337 name: Name of group to add.
338 autoclean_parents: Optional keyword argument; if unspecified, it takes
339 the value of autoclean (or True if autoclean isn't specified). This
340 controls whether any intermediate nodes that must be created for
341 multilevel groups are autocleaned.
343 name = self._LimitName(name, multilevel=True)
345 autoclean = kwargs.pop('autoclean', True)
346 autoclean_parents = kwargs.pop('autoclean_parents', autoclean)
347 chunks = name.split('/', 1)
349 # pylint: disable=W0212
350 for chunk in chunks[:-1]:
351 node = node._AddSingleGroup(chunk, parent=node,
352 autoclean=autoclean_parents, **kwargs)
353 return node._AddSingleGroup(chunks[-1], parent=node,
354 autoclean=autoclean, **kwargs)
356 @cros_build_lib.MemoizedSingleCall
357 def Instantiate(self):
358 """Ensure this group exists on disk in the cgroup hierarchy"""
360 if self.namespace == '.':
361 # If it's the root of the hierarchy, leave it alone.
364 if self.parent is not None:
365 self.parent.Instantiate()
366 osutils.SafeMakedirs(self.path, sudo=True)
368 force_inheritance = True
369 if self.parent.GetValue('cgroup.clone_children', '').strip() == '1':
370 force_inheritance = False
372 if force_inheritance:
373 if self._SUPPORTS_AUTOINHERIT:
374 # If the cgroup version supports it, flip the auto-inheritance setting
375 # on so that cgroups nested here don't have to manually transfer
377 self._SudoSet('cgroup.clone_children', '1')
380 # TODO(ferringb): sort out an appropriate filter/list for using:
381 # for name in os.listdir(parent):
382 # rather than just transfering these two values.
383 for name in ('cpuset.cpus', 'cpuset.mems'):
384 if not self._overwrite:
385 # Top level nodes like cros/cbuildbot we don't want to overwrite-
386 # users/system may've leveled configuration. If it's empty,
387 # overwrite it in those cases.
388 val = self.GetValue(name, '').strip()
391 self._SudoSet(name, self.parent.GetValue(name, ''))
392 except (EnvironmentError, cros_build_lib.RunCommandError):
393 # Do not leave half created cgroups hanging around-
394 # it makes compatibility a pain since we have to rewrite
395 # the cgroup each time. If instantiation fails, we know
396 # the group is screwed up, or the instantiaton code is-
397 # either way, no reason to leave it alive.
398 self.RemoveThisGroup()
403 # Since some of this code needs to check/reset this function to be ran,
404 # we use a more developer friendly variable name.
405 Instantiate._cache_key = '_inited'
407 def _SudoSet(self, key, value):
408 """Set a cgroup file in this namespace to a specific value"""
409 name = self._LimitName(key, True)
411 return sudo.SetFileContents(name, value, cwd=os.path.dirname(name))
412 except cros_build_lib.RunCommandError as e:
413 if e.exception is not None:
414 # Command failed before the exec itself; convert ENOENT
417 if isinstance(exc, EnvironmentError) and exc.errno == errno.ENOENT:
418 raise _GroupWasRemoved(self.namespace, e)
421 def RemoveThisGroup(self, strict=False):
422 """Remove this specific cgroup
424 If strict is True, then we must be removed.
426 if self._RemoveGroupOnDisk(self.path, strict=strict):
431 def RemoveGroup(self, name, strict=False):
432 """Removes a nested cgroup of ours
435 name: the namespace to remove.
436 strict: if False, remove it if possible. If True, its an error if it
439 return self._RemoveGroupOnDisk(self._LimitName(name, for_path=True),
443 def _RemoveGroupOnDisk(cls, path, strict, sudo_strict=True):
444 """Perform the actual group removal.
447 path: The cgroup's location on disk.
448 strict: Boolean; if true, then it's an error if the group can't be
449 removed. This can occur if there are still processes in it, or in
451 sudo_strict: See SudoRunCommand's strict option.
453 # Depth first recursively remove our children cgroups, then ourselves.
454 # Allow this to fail since currently it's possible for the cleanup code
455 # to not fully kill the hierarchy. Note that we must do just rmdirs,
456 # rm -rf cannot be used- it tries to remove files which are unlinkable
457 # in cgroup (only namespaces can be removed via rmdir).
458 # See Documentation/cgroups/ for further details.
459 path = os.path.normpath(path) + '/'
460 # Do a sanity check to ensure that we're not touching anything we
462 if not path.startswith(cls.CGROUP_ROOT):
463 raise RuntimeError("cgroups.py: Was asked to wipe path %s, refusing. "
464 "strict was %r, sudo_strict was %r"
465 % (path, strict, sudo_strict))
467 result = cros_build_lib.SudoRunCommand(
468 ['find', path, '-depth', '-type', 'd', '-exec', 'rmdir', '{}', '+'],
469 redirect_stderr=True, error_code_ok=not strict,
470 print_cmd=False, strict=sudo_strict)
471 if result.returncode == 0:
473 elif not os.path.isdir(path):
474 # We were invoked against a nonexistant path.
478 def TransferCurrentProcess(self, threads=True):
479 """Move the current process into this cgroup.
481 If threads is True, we move our threads into the group in addition.
482 Note this must be called in a threadsafe manner; it primarily exists
483 as a helpful default since python stdlib generates some background
484 threads (even when the code is operated synchronously). While we
485 try to handle that scenario, it's implicitly racy since python
486 gives no clean/sane way to control/stop thread creation; thus it's
487 on the invokers head to ensure no new threads are being generated
491 return self.TransferPid(os.getpid())
496 threads = set(self._GetCurrentProcessThreads())
498 # Track any failures; a failure means the thread died under
499 # feet, implying we shouldn't trust the current state.
500 force_run |= not self.TransferPid(tid, True)
501 if not force_run and threads == seen:
502 # We got two runs of this code seeing the same threads; assume
503 # we got them all since the first run moved those threads into
504 # our cgroup, and the second didn't see any new threads. While
505 # there may have been new threads between run1/run2, we do run2
506 # purely to snag threads we missed in run1; anything split by
507 # a thread from run1 would auto inherit our cgroup.
511 def _GetCurrentProcessThreads(self):
512 """Lookup the given tasks (pids fundamentally) for our process."""
513 # Note that while we could try doing tricks like threading.enumerate,
514 # that's not guranteed to pick up background c/ffi threads; generally
515 # that's ultra rare, but the potential exists thus we ask the kernel
516 # instead. What sucks however is that python releases the GIL; thus
517 # consuming code has to know of this, and protect against it.
518 return map(int, os.listdir('/proc/self/task'))
521 def TransferPid(self, pid, allow_missing=False):
522 """Assigns a given process to this cgroup."""
523 # Assign this root process to the new cgroup.
525 self._SudoSet('tasks', '%d' % int(pid))
527 except cros_build_lib.RunCommandError:
528 if not allow_missing:
532 # TODO(ferringb): convert to snakeoil.weakref.WeakRefFinalizer
534 if self.autoclean and self._inited and self.CGROUP_ROOT:
535 # Suppress any sudo_strict behaviour, since we may be invoked
536 # during interpreter shutdown.
537 self._RemoveGroupOnDisk(self.path, False, sudo_strict=False)
539 def TemporarilySwitchToNewGroup(self, namespace, **kwargs):
540 """Context manager to create a new cgroup & temporarily switch into it."""
541 node = self.AddGroup(namespace, **kwargs)
542 return self.TemporarilySwitchToGroup(node)
544 @contextlib.contextmanager
545 def TemporarilySwitchToGroup(self, group):
546 """Temporarily move this process into the given group, moving back after.
548 Used in a context manager fashion (aka, the with statement).
550 group.TransferCurrentProcess()
554 self.TransferCurrentProcess()
556 def KillProcesses(self, poll_interval=0.05, remove=False, sigterm_timeout=10):
557 """Kill all processes in this namespace."""
559 my_pids = set(map(str, self._GetCurrentProcessThreads()))
561 def _SignalPids(pids, signum):
562 cros_build_lib.SudoRunCommand(
563 ['kill', '-%i' % signum] + sorted(pids),
564 print_cmd=False, error_code_ok=True, redirect_stdout=True,
565 combine_stdout_stderr=True)
567 # First sigterm what we can, exiting after 2 runs w/out seeing pids.
568 # Let this phase run for a max of 10 seconds; afterwards, switch to
570 time_end = time.time() + sigterm_timeout
571 saw_pids, pids = True, set()
572 while time.time() < time_end:
576 self_kill = my_pids.intersection(pids)
578 raise Exception("Bad API usage: asked to kill cgroup %s, but "
579 "current pid %s is in that group. Effectively "
580 "asked to kill ourselves."
581 % (self.namespace, self_kill))
589 new_pids = pids.difference(previous_pids)
591 _SignalPids(new_pids, signal.SIGTERM)
592 # As long as new pids keep popping up, skip sleeping and just keep
593 # stomping them as quickly as possible (whack-a-mole is a good visual
594 # analogy of this). We do this to ensure that fast moving spawns
595 # are dealt with as quickly as possible. When considering this code,
596 # it's best to think about forkbomb scenarios- shouldn't occur, but
597 # synthetic fork-bombs can occur, thus this code being aggressive.
600 time.sleep(poll_interval)
602 # Next do a sigkill scan. Again, exit only after no pids have been seen
603 # for two scans, and all groups are removed.
604 groups_existed = True
606 pids = self.all_tasks
609 self_kill = my_pids.intersection(pids)
611 raise Exception("Bad API usage: asked to kill cgroup %s, but "
612 "current pid %i is in that group. Effectively "
613 "asked to kill ourselves."
614 % (self.namespace, self_kill))
616 _SignalPids(pids, signal.SIGKILL)
618 elif not (saw_pids or groups_existed):
623 time.sleep(poll_interval)
625 # Note this is done after the sleep; try to give the kernel time to
626 # shutdown the processes. They may still be transitioning to defunct
627 # kernel side by when we hit this scan, but that's fine- the next will
629 # This needs to be nonstrict; it's possible the kernel is currently
630 # killing the pids we've just sigkill'd, thus the group isn't removable
631 # yet. Additionally, it's possible a child got forked we didn't see.
632 # Ultimately via our killing/removal attempts, it will be removed,
633 # just not necessarily on the first run.
635 if self.RemoveThisGroup(strict=False):
636 # If we successfully removed this group, then there can be no pids,
637 # sub groups, etc, within it. No need to scan further.
639 groups_existed = True
641 groups_existed = [group.RemoveThisGroup(strict=False)
642 for group in self.nested_groups]
643 groups_existed = not all(groups_existed)
648 def _FindCurrentCrosGroup(cls, pid=None):
649 """Find and return the cros namespace a pid is currently in.
651 If no pid is given, os.getpid() is substituted.
655 elif not isinstance(pid, (long, int)):
656 raise ValueError("pid must be None, or an integer/long. Got %r" % (pid,))
660 # See the kernels Documentation/filesystems/proc.txt if you're unfamiliar
661 # w/ procfs, and keep in mind that we have to work across multiple kernel
663 cpuset = osutils.ReadFile('/proc/%s/cpuset' % (pid,)).rstrip('\n')
664 except EnvironmentError as e:
665 if e.errno != errno.ENOENT:
667 with open('/proc/%s/cgroup' % pid) as f:
669 # First digit is the hierachy index, 2nd is subsytem, 3rd is space.
671 # 2:cpuset:/cros/cbuildbot/1234
673 line = line.rstrip('\n')
676 line = line.split(':', 2)
677 if line[1] == 'cpuset':
681 if not cpuset or not cpuset.startswith("/cros/"):
683 return cpuset[len("/cros/"):].strip("/")
686 def FindStartingGroup(cls, process_name, nesting=True):
687 """Create and return the starting cgroup for ourselves nesting if allowed.
689 Note that the node returned is either a generic process pool (e.g.
690 cros/cbuildbot), or the parent pool we're nested within; processes
691 generated in this group are the responsibility of this process to
692 deal with- nor should this process ever try triggering a kill w/in this
693 portion of the tree since they don't truly own it.
696 process_name: See the hierarchy comments at the start of this module.
697 This should basically be the process name- cros_sdk for example,
699 nesting: If we're invoked by another cros cgroup aware process,
700 should we nest ourselves in their hierarchy? Generally speaking,
701 client code should never have a reason to disable nesting.
703 if not cls.IsUsable():
708 target = cls._FindCurrentCrosGroup()
710 target = process_name
712 return _cros_node.AddGroup(target, autoclean=False)
715 class ContainChildren(cros_build_lib.MasterPidContextManager):
716 """Context manager for containing children processes.
718 This manager creates a job pool derived from the specified Cgroup |node|
719 and transfers the current process into it upon __enter__.
721 Any children processes created at that point will inherit our cgroup;
722 they can only escape the group if they're running as root and move
723 themselves out of this hierarchy.
725 Upon __exit__, transfer the current process back to this group, then
726 SIGTERM (progressing to SIGKILL) any immediate children in the pool,
727 finally removing the pool if possible. After sending SIGTERM, we wait
728 |sigterm_timeout| seconds before sending SIGKILL.
730 If |pool_name| is given, that name is used rather than os.getpid() for
731 the job pool created.
733 Finally, note that during cleanup this will suppress all signals
734 to ensure that it cleanses any children before returning.
737 def __init__(self, node, pool_name=None, sigterm_timeout=10):
738 super(ContainChildren, self).__init__()
742 self.pool_name = pool_name
743 self.sigterm_timeout = sigterm_timeout
744 self.run_kill = False
747 self.pid = os.getpid()
749 # Note: We use lazy init here so that we cannot trigger a
750 # _GroupWasRemoved -- we want that to be contained.
751 pool_name = str(self.pid) if self.pool_name is None else self.pool_name
752 self.child = self.node.AddGroup(pool_name, autoclean=True, lazy_init=True)
754 self.child.TransferCurrentProcess()
755 except _GroupWasRemoved:
757 "Group %s was removed under our feet; pool shutdown is underway"
758 % self.child.namespace)
761 def _exit(self, *_args, **_kwargs):
762 with signals.DeferSignals():
763 self.node.TransferCurrentProcess()
765 self.child.KillProcesses(remove=True,
766 sigterm_timeout=self.sigterm_timeout)
768 # Non-strict since the group may have failed to be created.
769 self.child.RemoveThisGroup(strict=False)
772 def SimpleContainChildren(process_name, nesting=True, pid=None, **kwargs):
773 """Convenience context manager to create a cgroup for children containment
775 See Cgroup.FindStartingGroup and Cgroup.ContainChildren for specifics.
776 If Cgroups aren't supported on this system, this is a noop context manager.
778 node = Cgroup.FindStartingGroup(process_name, nesting=nesting)
780 return cros_build_lib.NoOpContextManager()
783 name = '%s:%i' % (process_name, pid)
784 return ContainChildren(node, name, **kwargs)
786 # This is a generic group, not associated with any specific process id, so
787 # we shouldn't autoclean it on exit; doing so would delete the group from
788 # under the feet of any other processes interested in using the group.
789 _root_node = Cgroup(None, _is_root=True, autoclean=False, lazy_init=True)
790 _cros_node = _root_node.AddGroup('cros', autoclean=False, lazy_init=True,