Home | History | Annotate | Download | only in module
      1 #    Copyright 2014-2015 ARM Limited
      2 #
      3 # Licensed under the Apache License, Version 2.0 (the "License");
      4 # you may not use this file except in compliance with the License.
      5 # You may obtain a copy of the License at
      6 #
      7 #     http://www.apache.org/licenses/LICENSE-2.0
      8 #
      9 # Unless required by applicable law or agreed to in writing, software
     10 # distributed under the License is distributed on an "AS IS" BASIS,
     11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 # See the License for the specific language governing permissions and
     13 # limitations under the License.
     14 #
     15 # pylint: disable=attribute-defined-outside-init
     16 import logging
     17 from collections import namedtuple
     18 
     19 from devlib.module import Module
     20 from devlib.exception import TargetError
     21 from devlib.utils.misc import list_to_ranges, isiterable
     22 from devlib.utils.types import boolean
     23 
     24 
     25 class Controller(object):
     26 
     27     def __init__(self, kind, hid, clist):
     28         """
     29         Initialize a controller given the hierarchy it belongs to.
     30 
     31         :param kind: the name of the controller
     32         :type kind: str
     33 
     34         :param hid: the Hierarchy ID this controller is mounted on
     35         :type hid: int
     36 
     37         :param clist: the list of controller mounted in the same hierarchy
     38         :type clist: list(str)
     39         """
     40         self.mount_name = 'devlib_cgh{}'.format(hid)
     41         self.kind = kind
     42         self.hid = hid
     43         self.clist = clist
     44         self.target = None
     45         self._noprefix = False
     46 
     47         self.logger = logging.getLogger('CGroup.'+self.kind)
     48         self.logger.debug('Initialized [%s, %d, %s]',
     49                           self.kind, self.hid, self.clist)
     50 
     51         self.mount_point = None
     52         self._cgroups = {}
     53 
     54     def mount(self, target, mount_root):
     55 
     56         mounted = target.list_file_systems()
     57         if self.mount_name in [e.device for e in mounted]:
     58             # Identify mount point if controller is already in use
     59             self.mount_point = [
     60                     fs.mount_point
     61                     for fs in mounted
     62                     if fs.device == self.mount_name
     63                 ][0]
     64         else:
     65             # Mount the controller if not already in use
     66             self.mount_point = target.path.join(mount_root, self.mount_name)
     67             target.execute('mkdir -p {} 2>/dev/null'\
     68                     .format(self.mount_point), as_root=True)
     69             target.execute('mount -t cgroup -o {} {} {}'\
     70                     .format(','.join(self.clist),
     71                             self.mount_name,
     72                             self.mount_point),
     73                             as_root=True)
     74 
     75         # Check if this controller uses "noprefix" option
     76         output = target.execute('mount | grep "{} "'.format(self.mount_name))
     77         if 'noprefix' in output:
     78             self._noprefix = True
     79             # self.logger.debug('Controller %s using "noprefix" option',
     80             #                   self.kind)
     81 
     82         self.logger.debug('Controller %s mounted under: %s (noprefix=%s)',
     83             self.kind, self.mount_point, self._noprefix)
     84 
     85         # Mark this contoller as available
     86         self.target = target
     87 
     88         # Create root control group
     89         self.cgroup('/')
     90 
     91     def cgroup(self, name):
     92         if not self.target:
     93             raise RuntimeError('CGroup creation failed: {} controller not mounted'\
     94                     .format(self.kind))
     95         if name not in self._cgroups:
     96             self._cgroups[name] = CGroup(self, name)
     97         return self._cgroups[name]
     98 
     99     def exists(self, name):
    100         if not self.target:
    101             raise RuntimeError('CGroup creation failed: {} controller not mounted'\
    102                     .format(self.kind))
    103         if name not in self._cgroups:
    104             self._cgroups[name] = CGroup(self, name, create=False)
    105         return self._cgroups[name].existe()
    106 
    107     def list_all(self):
    108         self.logger.debug('Listing groups for %s controller', self.kind)
    109         output = self.target.execute('{} find {} -type d'\
    110                 .format(self.target.busybox, self.mount_point),
    111                 as_root=True)
    112         cgroups = []
    113         for cg in output.splitlines():
    114             cg = cg.replace(self.mount_point + '/', '/')
    115             cg = cg.replace(self.mount_point, '/')
    116             cg = cg.strip()
    117             if cg == '':
    118                 continue
    119             self.logger.debug('Populate %s cgroup: %s', self.kind, cg)
    120             cgroups.append(cg)
    121         return cgroups
    122 
    123     def move_tasks(self, source, dest, exclude=[]):
    124         try:
    125             srcg = self._cgroups[source]
    126             dstg = self._cgroups[dest]
    127         except KeyError as e:
    128             raise ValueError('Unkown group: {}'.format(e))
    129         output = self.target._execute_util(
    130                     'cgroups_tasks_move {} {} \'{}\''.format(
    131                     srcg.directory, dstg.directory, exclude),
    132                     as_root=True)
    133 
    134     def move_all_tasks_to(self, dest, exclude=[]):
    135         """
    136         Move all the tasks to the specified CGroup
    137 
    138         Tasks are moved from all their original CGroup the the specified on.
    139         The tasks which name matches one of the string in exclude are moved
    140         instead in the root CGroup for the controller.
    141         The name of a tasks to exclude must be a substring of the task named as
    142         reported by the "ps" command. Indeed, this list will be translated into
    143         a: "ps | grep -e name1 -e name2..." in order to obtain the PID of these
    144         tasks.
    145 
    146         :param exclude: list of commands to keep in the root CGroup
    147         :type exlude: list(str)
    148         """
    149 
    150         if isinstance(exclude, str):
    151             exclude = [exclude]
    152         if not isinstance(exclude, list):
    153             raise ValueError('wrong type for "exclude" parameter, '
    154                              'it must be a str or a list')
    155 
    156         logging.debug('Moving all tasks into %s', dest)
    157 
    158         # Build list of tasks to exclude
    159         grep_filters = ''
    160         for comm in exclude:
    161             grep_filters += '-e {} '.format(comm)
    162         logging.debug('   using grep filter: %s', grep_filters)
    163         if grep_filters != '':
    164             logging.debug('   excluding tasks which name matches:')
    165             logging.debug('   %s', ', '.join(exclude))
    166 
    167         for cgroup in self._cgroups:
    168             if cgroup != dest:
    169                 self.move_tasks(cgroup, dest, grep_filters)
    170 
    171     def tasks(self, cgroup):
    172         try:
    173             cg = self._cgroups[cgroup]
    174         except KeyError as e:
    175             raise ValueError('Unkown group: {}'.format(e))
    176         output = self.target._execute_util(
    177                     'cgroups_tasks_in {}'.format(cg.directory),
    178                     as_root=True)
    179         entries = output.splitlines()
    180         tasks = {}
    181         for task in entries:
    182             tid = task.split(',')[0]
    183             try:
    184                 tname = task.split(',')[1]
    185             except: continue
    186             try:
    187                 tcmdline = task.split(',')[2]
    188             except:
    189                 tcmdline = ''
    190             tasks[int(tid)] = (tname, tcmdline)
    191         return tasks
    192 
    193     def tasks_count(self, cgroup):
    194         try:
    195             cg = self._cgroups[cgroup]
    196         except KeyError as e:
    197             raise ValueError('Unkown group: {}'.format(e))
    198         output = self.target.execute(
    199                     '{} wc -l {}/tasks'.format(
    200                     self.target.busybox, cg.directory),
    201                     as_root=True)
    202         return int(output.split()[0])
    203 
    204     def tasks_per_group(self):
    205         tasks = {}
    206         for cg in self.list_all():
    207             tasks[cg] = self.tasks_count(cg)
    208         return tasks
    209 
    210 class CGroup(object):
    211 
    212     def __init__(self, controller, name, create=True):
    213         self.logger = logging.getLogger('cgroups.' + controller.kind)
    214         self.target = controller.target
    215         self.controller = controller
    216         self.name = name
    217 
    218         # Control cgroup path
    219         self.directory = controller.mount_point
    220         if name != '/':
    221             self.directory = self.target.path.join(controller.mount_point, name[1:])
    222 
    223         # Setup path for tasks file
    224         self.tasks_file = self.target.path.join(self.directory, 'tasks')
    225         self.procs_file = self.target.path.join(self.directory, 'cgroup.procs')
    226 
    227         if not create:
    228             return
    229 
    230         self.logger.debug('Creating cgroup %s', self.directory)
    231         self.target.execute('[ -d {0} ] || mkdir -p {0}'\
    232                 .format(self.directory), as_root=True)
    233 
    234     def exists(self):
    235         try:
    236             self.target.execute('[ -d {0} ]'\
    237                 .format(self.directory), as_root=True)
    238             return True
    239         except TargetError:
    240             return False
    241 
    242     def get(self):
    243         conf = {}
    244 
    245         logging.debug('Reading %s attributes from:',
    246                 self.controller.kind)
    247         logging.debug('  %s',
    248                 self.directory)
    249         output = self.target._execute_util(
    250                     'cgroups_get_attributes {} {}'.format(
    251                     self.directory, self.controller.kind),
    252                     as_root=True)
    253         for res in output.splitlines():
    254             attr = res.split(':')[0]
    255             value = res.split(':')[1]
    256             conf[attr] = value
    257 
    258         return conf
    259 
    260     def set(self, **attrs):
    261         for idx in attrs:
    262             if isiterable(attrs[idx]):
    263                 attrs[idx] = list_to_ranges(attrs[idx])
    264             # Build attribute path
    265             if self.controller._noprefix:
    266                 attr_name = '{}'.format(idx)
    267             else:
    268                 attr_name = '{}.{}'.format(self.controller.kind, idx)
    269             path = self.target.path.join(self.directory, attr_name)
    270 
    271             self.logger.debug('Set attribute [%s] to: %s"',
    272                     path, attrs[idx])
    273 
    274             # Set the attribute value
    275             try:
    276                 self.target.write_value(path, attrs[idx])
    277             except TargetError:
    278                 # Check if the error is due to a non-existing attribute
    279                 attrs = self.get()
    280                 if idx not in attrs:
    281                     raise ValueError('Controller [{}] does not provide attribute [{}]'\
    282                                      .format(self.controller.kind, attr_name))
    283                 raise
    284 
    285     def get_tasks(self):
    286         task_ids = self.target.read_value(self.tasks_file).split()
    287         logging.debug('Tasks: %s', task_ids)
    288         return map(int, task_ids)
    289 
    290     # Used to insert fake cgroup attach events to know existing cgroup assignments
    291     def trace_cgroup_tasks(self):
    292         exec_cmd = "cgroup_trace_attach_task {} {} {}".format(self.controller.hid, self.directory, self.tasks_file)
    293         self.target._execute_util(exec_cmd)
    294 
    295     def add_task(self, tid):
    296         self.target.write_value(self.tasks_file, tid, verify=False)
    297 
    298     def add_tasks(self, tasks):
    299         for tid in tasks:
    300             self.add_task(tid)
    301 
    302     def add_proc(self, pid):
    303         self.target.write_value(self.procs_file, pid, verify=False)
    304 
    305 CgroupSubsystemEntry = namedtuple('CgroupSubsystemEntry', 'name hierarchy num_cgroups enabled')
    306 
    307 class CgroupsModule(Module):
    308 
    309     name = 'cgroups'
    310     stage = 'setup'
    311 
    312     @staticmethod
    313     def probe(target):
    314         if not target.is_rooted:
    315             return False
    316         if target.file_exists('/proc/cgroups'):
    317             return True
    318         return target.config.has('cgroups')
    319 
    320     def __init__(self, target):
    321         super(CgroupsModule, self).__init__(target)
    322 
    323         self.logger = logging.getLogger('CGroups')
    324 
    325         # Set Devlib's CGroups mount point
    326         self.cgroup_root = target.path.join(
    327             target.working_directory, 'cgroups')
    328 
    329         # Get the list of the available controllers
    330         subsys = self.list_subsystems()
    331         if len(subsys) == 0:
    332             self.logger.warning('No CGroups controller available')
    333             return
    334 
    335         # Map hierarchy IDs into a list of controllers
    336         hierarchy = {}
    337         for ss in subsys:
    338             try:
    339                 hierarchy[ss.hierarchy].append(ss.name)
    340             except KeyError:
    341                 hierarchy[ss.hierarchy] = [ss.name]
    342         self.logger.debug('Available hierarchies: %s', hierarchy)
    343 
    344         # Initialize controllers
    345         self.logger.info('Available controllers:')
    346         self.controllers = {}
    347         for ss in subsys:
    348             hid = ss.hierarchy
    349             controller = Controller(ss.name, hid, hierarchy[hid])
    350             try:
    351                 controller.mount(self.target, self.cgroup_root)
    352             except TargetError:
    353                 message = 'Failed to mount "{}" controller'
    354                 raise TargetError(message.format(controller.kind))
    355             self.logger.info('  %-12s : %s', controller.kind,
    356                              controller.mount_point)
    357             self.controllers[ss.name] = controller
    358 
    359     def list_subsystems(self):
    360         subsystems = []
    361         for line in self.target.execute('{} cat /proc/cgroups'\
    362                 .format(self.target.busybox)).splitlines()[1:]:
    363             line = line.strip()
    364             if not line or line.startswith('#'):
    365                 continue
    366             name, hierarchy, num_cgroups, enabled = line.split()
    367             subsystems.append(CgroupSubsystemEntry(name,
    368                                                    int(hierarchy),
    369                                                    int(num_cgroups),
    370                                                    boolean(enabled)))
    371         return subsystems
    372 
    373 
    374     def controller(self, kind):
    375         if kind not in self.controllers:
    376             self.logger.warning('Controller %s not available', kind)
    377             return None
    378         return self.controllers[kind]
    379 
    380     def run_into_cmd(self, cgroup, cmdline):
    381         """
    382         Get the command to run a command into a given cgroup
    383 
    384         :param cmdline: Commdand to be run into cgroup
    385         :param cgroup: Name of cgroup to run command into
    386         :returns: A command to run `cmdline` into `cgroup`
    387         """
    388         return 'CGMOUNT={} {} cgroups_run_into {} {}'\
    389                 .format(self.cgroup_root, self.target.shutils,
    390                         cgroup, cmdline)
    391 
    392     def run_into(self, cgroup, cmdline):
    393         """
    394         Run the specified command into the specified CGroup
    395 
    396         :param cmdline: Command to be run into cgroup
    397         :param cgroup: Name of cgroup to run command into
    398         :returns: Output of command.
    399         """
    400         cmd = self.run_into_cmd(cgroup, cmdline)
    401         raw_output = self.target.execute(cmd)
    402 
    403         # First line of output comes from shutils; strip it out.
    404         return raw_output.split('\n', 1)[1]
    405 
    406     def cgroups_tasks_move(self, srcg, dstg, exclude=''):
    407         """
    408         Move all the tasks from the srcg CGroup to the dstg one.
    409         A regexps of tasks names can be used to defined tasks which should not
    410         be moved.
    411         """
    412         return self.target._execute_util(
    413             'cgroups_tasks_move {} {} {}'.format(srcg, dstg, exclude),
    414             as_root=True)
    415 
    416     def isolate(self, cpus, exclude=[]):
    417         """
    418         Remove all userspace tasks from specified CPUs.
    419 
    420         A list of CPUs can be specified where we do not want userspace tasks
    421         running. This functions creates a sandbox cpuset CGroup where all
    422         user-space tasks and not-pinned kernel-space tasks are moved into.
    423         This should allows to isolate the specified CPUs which will not get
    424         tasks running unless explicitely moved into the isolated group.
    425 
    426         :param cpus: the list of CPUs to isolate
    427         :type cpus: list(int)
    428 
    429         :return: the (sandbox, isolated) tuple, where:
    430                  sandbox is the CGroup of sandboxed CPUs
    431                  isolated is the CGroup of isolated CPUs
    432         """
    433         all_cpus = set(range(self.target.number_of_cpus))
    434         sbox_cpus = list(all_cpus - set(cpus))
    435         isol_cpus = list(all_cpus - set(sbox_cpus))
    436 
    437         # Create Sandbox and Isolated cpuset CGroups
    438         cpuset = self.controller('cpuset')
    439         sbox_cg = cpuset.cgroup('/DEVLIB_SBOX')
    440         isol_cg = cpuset.cgroup('/DEVLIB_ISOL')
    441 
    442         # Set CPUs for Sandbox and Isolated CGroups
    443         sbox_cg.set(cpus=sbox_cpus, mems=0)
    444         isol_cg.set(cpus=isol_cpus, mems=0)
    445 
    446         # Move all currently running tasks to the Sandbox CGroup
    447         cpuset.move_all_tasks_to('/DEVLIB_SBOX', exclude)
    448 
    449         return sbox_cg, isol_cg
    450 
    451     def freeze(self, exclude=[], thaw=False):
    452         """
    453         Freeze all user-space tasks but the specified ones
    454 
    455         A freezer cgroup is used to stop all the tasks in the target system but
    456         the ones which name match one of the path specified by the exclude
    457         paramater. The name of a tasks to exclude must be a substring of the
    458         task named as reported by the "ps" command. Indeed, this list will be
    459         translated into a: "ps | grep -e name1 -e name2..." in order to obtain
    460         the PID of these tasks.
    461 
    462         :param exclude: list of commands paths to exclude from freezer
    463         :type exclude: list(str)
    464 
    465         :param thaw: if true thaw tasks instead
    466         :type thaw: bool
    467         """
    468 
    469         # Create Freezer CGroup
    470         freezer = self.controller('freezer')
    471         if freezer is None:
    472             raise RuntimeError('freezer cgroup controller not present')
    473         freezer_cg = freezer.cgroup('/DEVLIB_FREEZER')
    474         cmd = 'cgroups_freezer_set_state {{}} {}'.format(freezer_cg.directory)
    475 
    476         if thaw:
    477             # Restart froozen tasks
    478             freezer.target._execute_util(cmd.format('THAWED'), as_root=True)
    479             # Remove all tasks from freezer
    480             freezer.move_all_tasks_to('/')
    481             return
    482 
    483         # Move all tasks into the freezer group
    484         freezer.move_all_tasks_to('/DEVLIB_FREEZER', exclude)
    485 
    486         # Get list of not frozen tasks, which is reported as output
    487         tasks = freezer.tasks('/')
    488 
    489         # Freeze all tasks
    490         freezer.target._execute_util(cmd.format('FROZEN'), as_root=True)
    491 
    492         return tasks
    493 
    494