• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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