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 16import logging 17from collections import namedtuple 18 19from devlib.module import Module 20from devlib.exception import TargetError 21from devlib.utils.misc import list_to_ranges, isiterable 22from devlib.utils.types import boolean 23 24 25class 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 210class 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 305CgroupSubsystemEntry = namedtuple('CgroupSubsystemEntry', 'name hierarchy num_cgroups enabled') 306 307class 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 thawed_cg = freezer.cgroup('/') 475 476 if thaw: 477 # Restart froozen tasks 478 freezer_cg.set(state='THAWED') 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_cg.set(state='FROZEN') 491 492 return tasks 493 494