├── cgput.py ├── cgfreeze.py ├── cgrc.py ├── cgls.py ├── cgconf.yaml.example ├── cgwait.py ├── README.rst ├── cgtime.py └── cgconf.py /cgput.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | 5 | import argparse 6 | parser = argparse.ArgumentParser( 7 | description='Put given tgids into a control group.') 8 | parser.add_argument('cgroup', help='Cgroup name.') 9 | parser.add_argument('tgids', nargs='+', 10 | help='Thread group (process) ids or thread ids.') 11 | optz = parser.parse_args() 12 | 13 | import itertools as it, operator as op, functools as ft 14 | from glob import iglob 15 | import os, sys 16 | 17 | dst_set = set(it.imap( os.path.realpath, 18 | iglob('/sys/fs/cgroup/*/{}/cgroup.procs'.format(optz.cgroup)) )) 19 | if not dst_set: 20 | parser.error('Cgroup not found in any hierarchy: {}'.format(optz.cgroup)) 21 | 22 | for dst in dst_set: 23 | for tgid in optz.tgids: 24 | open(dst, 'wb').write('{}\n'.format(tgid)) 25 | -------------------------------------------------------------------------------- /cgfreeze.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals, print_function 4 | 5 | import argparse 6 | parser = argparse.ArgumentParser( 7 | description='Put/ specified cgroup(s) in/out-of the freezer.') 8 | parser.add_argument('-u', '--unfreeze', 9 | action='store_true', help='Unfreeze the cgroup(s).') 10 | parser.add_argument('-c', '--check', 11 | action='store_true', help='Just get the state of a specified cgroup(s).') 12 | parser.add_argument('cgroups', nargs='+', help='Cgroup(s) to operate on.') 13 | argz = parser.parse_args() 14 | 15 | import os, sys 16 | 17 | for cg in argz.cgroups: 18 | cg_state = '/sys/fs/cgroup/freezer/tagged/{}/freezer.state'.format(cg) 19 | if not os.path.exists(cg_state): 20 | print('{}: inaccessible'.format(cg), file=sys.stderr) 21 | continue 22 | if argz.check: 23 | print('{}: {}'.format(cg, open(cg_state).read().strip())) 24 | else: 25 | state = b'FROZEN\n' if not argz.unfreeze else b'THAWED\n' 26 | open(cg_state, 'wb').write(state) 27 | -------------------------------------------------------------------------------- /cgrc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | 5 | # No argparser here - it's just not worth it. 6 | # Usage: 7 | # cgrc -g group < cmd... 8 | # cgrc -g group cmd... 9 | # cgrc -q group cmd... 10 | # cgrc.group cmd... 11 | # If "group" starts with /, it won't be prefixed with "tagged/" 12 | 13 | cgname_tpl = 'tagged/{}' # template for cgroup path 14 | tasks = '/sys/fs/cgroup/*/{}/cgroup.procs' # path to resource controllers' pseudo-fs'es 15 | 16 | import os, sys 17 | 18 | cmd_base = sys.argv[0].rsplit('.py', 1)[0] 19 | if '.' in cmd_base: 20 | cgname, cmd = cmd_base.split('.', 1)[-1], sys.argv[1:] 21 | else: 22 | cmd_base, cmd = (list(), sys.argv[1:])\ 23 | if not sys.argv[1].startswith('-s ') else\ 24 | ( ''.join(open(sys.argv[2]).readlines()[1:]).strip().split(), 25 | sys.argv[1].split()[1:] + sys.argv[3:] ) 26 | if cmd[0] in ['-g', '-q']: 27 | opt, cgname, cmd = cmd[0], cmd[1], cmd[2:] 28 | if opt == '-q': 29 | import subprocess 30 | subprocess.check_call(['cgwait', '-e', cgname], close_fds=True) 31 | else: cgname, cmd = cmd[0], cmd[1:] 32 | cmd = cmd_base + cmd 33 | 34 | cgname = cgname_tpl.format(cgname)\ 35 | if not cgname.startswith('/') else cgname.lstrip('/') 36 | 37 | from glob import glob 38 | 39 | cg_pid = '{}\n'.format(os.getpid()) 40 | for tasks in glob(tasks.format(cgname))\ 41 | + glob(tasks.format(cgname.replace('/', '.'))): 42 | with open(tasks, 'wb') as dst: dst.write(cg_pid) 43 | 44 | os.execvp(cmd[0], cmd) 45 | -------------------------------------------------------------------------------- /cgls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | 5 | import argparse 6 | parser = argparse.ArgumentParser( 7 | description='List pids (tgids), belonging to a specified cgroups.') 8 | parser.add_argument('cgroups', nargs='+', help='Cgroup(s) to operate on.') 9 | parser.add_argument('--no-checks', 10 | action='store_true', help='Do not perform sanity checks on pid sets.') 11 | argz = parser.parse_args() 12 | 13 | cg_root = '/sys/fs/cgroup' 14 | 15 | import itertools as it, operator as op, functools as ft 16 | from os.path import join, isdir 17 | from glob import glob 18 | import os, sys 19 | 20 | listdirs = lambda path: it.ifilter( isdir, 21 | it.imap(ft.partial(join, path), os.listdir(path)) ) 22 | collect_pids = lambda path, pids_set:\ 23 | pids_set.update(int(line.strip()) for line in open(join(path, 'cgroup.procs'))) 24 | 25 | def collect_pids_recurse(path, pids_set): 26 | collect_pids(path, pids_set) 27 | for sp in listdirs(path): collect_pids_recurse(sp, pids_set) 28 | 29 | pids = dict() 30 | 31 | for rc in os.listdir(cg_root): 32 | pids[rc] = pids_set = set() 33 | for cg_name in argz.cgroups: 34 | cg = 'tagged/{}'.format(cg_name) 35 | cg_path = glob(join(cg_root, rc, cg))\ 36 | or glob(join(cg_root, rc, cg.replace('/', '.')))\ 37 | or glob('{}.*'.format(join(cg_root, rc, cg.replace('/', '.')))) 38 | if not cg_path: 39 | del pids[rc] 40 | continue 41 | for path in cg_path: collect_pids_recurse(path, pids_set) 42 | 43 | if not argz.no_checks: 44 | for (ak, a), (bk, b) in it.combinations(pids.items(), 2): 45 | pids_diff = a.symmetric_difference(b) 46 | if not pids_diff: continue 47 | print( 'Difference between rc pid sets {}/{}: {}'\ 48 | .format(ak, bk, ' '.join(map(bytes, pids_diff))), file=sys.stderr ) 49 | 50 | pids_all = reduce(op.or_, pids.viewvalues(), set()) 51 | sys.stdout.write(''.join(it.imap('{}\n'.format, pids_all))) 52 | -------------------------------------------------------------------------------- /cgconf.yaml.example: -------------------------------------------------------------------------------- 1 | path: /sys/fs/cgroup 2 | 3 | defaults: 4 | _tasks: root:wheel:664 5 | _admin: root:wheel:644 6 | # _path: root:root:755 # won't be chown/chmod'ed, if unset 7 | freezer: 8 | 9 | groups: 10 | 11 | # Must be applied to root cgroup 12 | memory.use_hierarchy: 1 13 | 14 | # base: 15 | # _default: true # to put all pids here initially 16 | # cpu.shares: 1000 17 | # blkio.weight: 1000 18 | 19 | user.slice: 20 | cpu: 21 | cfs_quota_us: 1_500_000 22 | cfs_period_us: 1_000_000 23 | 24 | tagged: 25 | 26 | cave: 27 | _tasks: root:paludisbuild 28 | _admin: root:paludisbuild 29 | cpu: 30 | shares: 100 31 | cfs_quota_us: 100_000 32 | cfs_period_us: 250_000 33 | blkio.weight: 100 34 | memory.soft_limit_in_bytes: 2G 35 | 36 | desktop: 37 | 38 | roam: 39 | _tasks: root:users 40 | cpu.shares: 300 41 | blkio.weight: 300 42 | memory.soft_limit_in_bytes: 2G 43 | 44 | de_misc: 45 | memory.soft_limit_in_bytes: 700M 46 | memory.limit_in_bytes: 1500M 47 | 48 | vm: 49 | quasi: 50 | misc: 51 | cpu.shares: 200 52 | blkio.weight: 100 53 | memory.soft_limit_in_bytes: 1200M 54 | memory.limit_in_bytes: 1500M 55 | 56 | bench: 57 | # Subdir for adhoc cgroups created by user 58 | tmp: 59 | # Corresponding pw_gid will be used, if "user:" is specified 60 | # Specs like "user", ":group:770" or "::775" are all valid. 61 | _tasks: 'fraggod:' 62 | _admin: 'fraggod:' 63 | _path: 'fraggod:' 64 | # These will be initialized as dirs with proper uid/gid, but no stuff applied there 65 | cpuacct: 66 | memory: 67 | blkio: 68 | # Limits that groups in tmp/ can't transcend 69 | cpu.shares: 500 70 | blkio.weight: 500 71 | memory.soft_limit_in_bytes: 300M 72 | memory.limit_in_bytes: 500M 73 | -------------------------------------------------------------------------------- /cgwait.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals, print_function 4 | 5 | #################### 6 | 7 | poll_interval = 5 8 | timeout = 0 9 | 10 | #################### 11 | 12 | import argparse 13 | parser = argparse.ArgumentParser(description='Wait for certain cgroup events.') 14 | parser.add_argument('-i', '--poll-interval', type=float, default=poll_interval, 15 | help='task-files polling interval (default: %(default)ss).') 16 | parser.add_argument('-t', '--timeout', type=float, default=timeout, 17 | help='Timeout for operation (default: %(default)ss).' 18 | ' Results in non-zero exit code, 0 or negative to disable.') 19 | parser.add_argument('-e', '--empty', action='store_true', 20 | help='Wait for cgroup(s) to become empty (default).') 21 | parser.add_argument('--debug', action='store_true', help='Verbose operation mode.') 22 | parser.add_argument('cgroups', nargs='+', help='Cgroup(s) to operate on.') 23 | argz = parser.parse_args() 24 | 25 | if not argz.empty: argz.empty = True 26 | if argz.debug: 27 | import logging 28 | logging.basicConfig(level=logging.DEBUG if argz.debug else logging.INFO) 29 | log = logging.getLogger() 30 | 31 | import itertools as it, operator as op, functools as ft 32 | from glob import glob 33 | from time import time, sleep 34 | 35 | tasks = list( 36 | glob('/sys/fs/cgroup/*/{}/tasks'.format(cg)) 37 | + glob('/sys/fs/cgroup/*/{}/tasks'.format(cg.replace('/', '.'))) 38 | for cg in it.imap('tagged/{}'.format, argz.cgroups) ) 39 | for task_file in tasks: # sanity check 40 | if not task_file: parser.error('No task-files found for cgroup: {}') 41 | tasks = set(it.chain.from_iterable(tasks)) 42 | 43 | if argz.debug: 44 | log.debug('Watching task-files: {}, timeout: {}'.format(' '.join(tasks), timeout)) 45 | 46 | done = False 47 | deadline = time() + argz.timeout if argz.timeout > 0 else 0 48 | while True: 49 | if argz.empty: 50 | for task_file in tasks: 51 | if open(task_file).read().strip(): 52 | if argz.debug: log.debug('task-file isnt empty: {}'.format(task_file)) 53 | break 54 | else: done = True 55 | if done: break 56 | if deadline and time() > deadline: exit(2) 57 | sleep(argz.poll_interval) 58 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | cgroup-tools 2 | ------------ 3 | 4 | **DEPRECATION NOTICE**: 5 | 6 | | These scripts are useless on a modern linux with unified cgroup-v2 hierarchy. 7 | | On a systemd-enabled machine, use "systemd-run --scope" instead. 8 | | See also: `mk-fg/fgtk#cgrc tool `_ 9 | 10 | A set of tools to work with cgroup tree and process classification/QoS 11 | according to it. 12 | 13 | More (of a bit outdated) info can be found `in a blog post here 14 | `_. 15 | 16 | Main script there - cgconf - allows to use YAML like this to configure initial 17 | cgroup hierarcy like this:: 18 | 19 | path: /sys/fs/cgroup 20 | 21 | defaults: 22 | _tasks: root:wheel:664 23 | _admin: root:wheel:644 24 | # _path: root:root:755 # won't be chown/chmod'ed, if unset 25 | freezer: 26 | 27 | groups: 28 | 29 | # Must be applied to root cgroup 30 | memory.use_hierarchy: 1 31 | 32 | # base: 33 | # _default: true # to put all pids here initially 34 | # cpu.shares: 1000 35 | # blkio.weight: 1000 36 | 37 | user.slice: 38 | cpu: 39 | cfs_quota_us: 1_500_000 40 | cfs_period_us: 1_000_000 41 | 42 | tagged: 43 | 44 | cave: 45 | _tasks: root:paludisbuild 46 | _admin: root:paludisbuild 47 | cpu: 48 | shares: 100 49 | cfs_quota_us: 100_000 50 | cfs_period_us: 250_000 51 | blkio.weight: 100 52 | memory.soft_limit_in_bytes: 2G 53 | 54 | desktop: 55 | 56 | roam: 57 | _tasks: root:users 58 | cpu.shares: 300 59 | blkio.weight: 300 60 | memory.soft_limit_in_bytes: 2G 61 | 62 | de_misc: 63 | memory.soft_limit_in_bytes: 700M 64 | memory.limit_in_bytes: 1500M 65 | 66 | vm: 67 | quasi: 68 | misc: 69 | cpu.shares: 200 70 | blkio.weight: 100 71 | memory.soft_limit_in_bytes: 1200M 72 | memory.limit_in_bytes: 1500M 73 | 74 | bench: 75 | # Subdir for adhoc cgroups created by user 76 | tmp: 77 | # Corresponding pw_gid will be used, if "user:" is specified 78 | # Specs like "user", ":group:770" or "::775" are all valid. 79 | _tasks: 'fraggod:' 80 | _admin: 'fraggod:' 81 | _path: 'fraggod:' 82 | # These will be initialized as dirs with proper uid/gid, but no stuff applied there 83 | cpuacct: 84 | memory: 85 | blkio: 86 | # Limits that groups in tmp/ can't transcend 87 | cpu.shares: 500 88 | blkio.weight: 500 89 | memory.soft_limit_in_bytes: 300M 90 | memory.limit_in_bytes: 500M 91 | 92 | And then something like ``cgrc `` to run anything 93 | inside these (can also be used in shebang with -s, with cmd from stdin, etc). 94 | 95 | Other tools allow waiting for threads within some cgroup to finish before 96 | proceeding (cgwait), put stuff running there on hold easily (cgfreeze) and run 97 | stuff in temp-cgroup, reporting accounting data for it afterwards (cgtime). 98 | 99 | cgconf and cgrc turn out to be surprisingly useful still, despite systemd adding 100 | knobs to control cgroup resource limits (but not all of them, and spread over 101 | lot of small files, which are pain if you need a big picture of e.g. weights) 102 | and systemd-run, which hides i/o of whatever it runs in systemd slices. 103 | -------------------------------------------------------------------------------- /cgtime.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | 5 | import os, sys, struct 6 | 7 | len_fmt = '!I' 8 | len_fmt_bytes = struct.calcsize(len_fmt) 9 | cg_root = '/sys/fs/cgroup' 10 | 11 | 12 | ### Fork child pid that will be cgroup-confined asap 13 | 14 | (cmd_r, cmd_w), (cmd_start_r, cmd_start_w) = os.pipe(), os.pipe() 15 | cmd_pid = os.fork() 16 | 17 | if not cmd_pid: 18 | os.close(cmd_w), os.close(cmd_start_r) 19 | cmd_r, cmd_start_w = os.fdopen(cmd_r, 'rb', 0), os.fdopen(cmd_start_w, 'wb', 0) 20 | 21 | def cmd_main(): 22 | # Read list of cgroups to use 23 | data_len = cmd_r.read(len_fmt_bytes) 24 | if len(data_len) != len_fmt_bytes: sys.exit(1) 25 | data_len, = struct.unpack(len_fmt, data_len) 26 | cmd = cmd_r.read(data_len).splitlines() 27 | cmd_r.close() 28 | if not cmd: sys.exit(0) # parent pid failed 29 | cg_paths, cmd = cmd[:-1], cmd[-1] 30 | if cg_paths[-1] == '-': # "quiet" mark 31 | cg_paths = cg_paths[:-1] 32 | devnull = open(os.devnull, 'wb') 33 | os.dup2(devnull.fileno(), sys.stdout.fileno()) 34 | os.dup2(devnull.fileno(), sys.stderr.fileno()) 35 | cmd = map(lambda arg: arg.decode('hex'), cmd.split('\0')) 36 | assert cg_paths, cg_paths 37 | 38 | # cgroups are applied here so that there won't 39 | # be any delay or other stuff between that and exec 40 | cg_pid = '{}\n'.format(os.getpid()) 41 | for tasks in cg_paths: 42 | with open(tasks, 'wb') as dst: dst.write(cg_pid) 43 | cmd_start_w.write('.') 44 | cmd_start_w.flush() 45 | cmd_start_w.close() 46 | os.execvp(cmd[0], cmd) 47 | 48 | cmd_main() 49 | os.abort() # should never get here 50 | 51 | os.close(cmd_r), os.close(cmd_start_w) 52 | cmd_w, cmd_start_r = os.fdopen(cmd_w, 'wb', 0), os.fdopen(cmd_start_r, 'rb', 0) 53 | 54 | 55 | ### Parent pid 56 | 57 | import itertools as it, operator as op, functools as ft 58 | from os.path import join, exists, dirname, isdir 59 | from collections import OrderedDict 60 | from glob import iglob 61 | from time import time 62 | 63 | page_size = os.sysconf('SC_PAGE_SIZE') 64 | page_size_kb = page_size // 1024 65 | user_hz = os.sysconf('SC_CLK_TCK') 66 | sector_bytes = 512 67 | 68 | def num_format(n, decimals=3): 69 | n, ndec = list(bytes(n)), '' 70 | if '.' in n: 71 | ndec = n.index('.') 72 | n, ndec = n[:ndec], '.' + ''.join(n[ndec+1:][:decimals]) 73 | res = list() 74 | while n: 75 | for i in xrange(3): 76 | res.append(n.pop()) 77 | if not n: break 78 | if not n: break 79 | res.append('_') 80 | return ''.join(reversed(res)) + ndec 81 | 82 | def dev_resolve( major, minor, 83 | log_fails=True, _cache = dict(), _cache_time=600 ): 84 | ts_now, dev_cached = time(), False 85 | while True: 86 | if not _cache: ts = 0 87 | else: 88 | dev = major, minor 89 | dev_cached, ts = (None, _cache[None])\ 90 | if dev not in _cache else _cache[dev] 91 | # Update cache, if necessary 92 | if ts_now > ts + _cache_time or dev_cached is False: 93 | _cache.clear() 94 | for link in it.chain(iglob('/dev/mapper/*'), iglob('/dev/sd*'), iglob('/dev/xvd*')): 95 | link_name = os.path.basename(link) 96 | try: link_dev = os.stat(link).st_rdev 97 | except OSError: continue # EPERM, EINVAL 98 | _cache[(os.major(link_dev), os.minor(link_dev))] = link_name, ts_now 99 | _cache[None] = ts_now 100 | continue # ...and try again 101 | if dev_cached: dev_cached = dev_cached.replace('.', '_') 102 | elif log_fails: 103 | log.warn( 'Unable to resolve device' 104 | ' from major/minor numbers: %s:%s', major, minor ) 105 | return dev_cached or None 106 | 107 | def main(args=None): 108 | import argparse 109 | parser = argparse.ArgumentParser( 110 | description='Tool to measure resources consumed' 111 | ' by a group of processes, no matter how hard they fork.' 112 | ' Does that by creating a temp cgroup and running passed command there.') 113 | parser.add_argument('cmdline', nargs='+', 114 | help='Command to run and any arguments for it.') 115 | parser.add_argument('-g', '--cgroup', 116 | default='bench/tmp', metavar='{ /path | tagged-path }', 117 | help='Hierarchy path to create temp-cgroup under' 118 | ' ("/" means root cgroup, default: %(default)s).' 119 | ' Any missing path components will be created.' 120 | ' If relative name is specified, it will be interpreted from /tagged path.') 121 | parser.add_argument('-c', '--rcs', 122 | default='cpuacct, blkio, memory', metavar='rc1[,rc2,...]', 123 | help='Comma-separated list of rc hierarchies to get metrics from (default: %(default)s).' 124 | ' Should have corresponding path mounted under {}.'.format(cg_root)) 125 | parser.add_argument('-q', '--quiet', action='store_true', 126 | help='Redirect stderr/stdout for started pid to /dev/null.') 127 | parser.add_argument('-d', '--debug', action='store_true', help='Verbose operation mode.') 128 | opts = parser.parse_args(sys.argv[1:] if args is None else args) 129 | 130 | global log 131 | import logging 132 | logging.basicConfig(level=logging.DEBUG if opts.debug else logging.INFO) 133 | log = logging.getLogger() 134 | 135 | # Check all rc tasks-file paths 136 | cg_subpath = 'tmp.{}'.format(cmd_pid) 137 | cg_tasks, cg_path = OrderedDict(), join('tagged', opts.cgroup).lstrip('/') 138 | for rc in map(bytes.strip, opts.rcs.split(',')): 139 | tasks = join(cg_root, rc, cg_path, cg_subpath, 'tasks') 140 | assert '\n' not in tasks, repr(tasks) 141 | os.makedirs(dirname(tasks)) 142 | assert exists(tasks), tasks 143 | cg_tasks[rc] = tasks 144 | 145 | # Append cmdline, send data to child 146 | data = cg_tasks.values() 147 | if opts.quiet: data.append('-') 148 | data = '\n'.join(it.chain( data, 149 | ['\0'.join(map(lambda arg: arg.encode('hex'), opts.cmdline))] )) 150 | cmd_w.write(struct.pack(len_fmt, len(data)) + data) 151 | cmd_w.flush() 152 | 153 | # Wait for signal to start counting 154 | mark = cmd_start_r.read(1) 155 | ts0 = time() 156 | assert mark == '.', repr(mark) 157 | cmd_start_r.close() 158 | 159 | pid, status = os.waitpid(cmd_pid, 0) 160 | ts1 = time() 161 | 162 | err = status >> 8 163 | if status & 0xff: 164 | print('Unclean exit of child pid due to signal: {}'.format((status & 0xff) >> 1)) 165 | err = err or 1 166 | 167 | # Make sure everything finished running there 168 | leftovers = set() 169 | for tasks in cg_tasks.values(): 170 | with open(tasks) as src: 171 | leftovers.update(map(int, src.read().splitlines())) 172 | if leftovers: 173 | print( 'Main pid has finished, but cgroups have leftover threads' 174 | ' still running: {}'.format(', '.join(map(bytes, leftovers))), file=sys.stderr ) 175 | err = err or 1 176 | 177 | # Collect/print accounting data 178 | acct = OrderedDict() 179 | acct['cmd'] = ' '.join(opts.cmdline) 180 | acct['wall_clock'] = '{:.3f}'.format(ts1 - ts0) 181 | acct['exit_status'] = '{} {}'.format(status >> 8, status & 0xff >> 1) 182 | 183 | acct_srcs = OrderedDict() 184 | for cg_path in map(dirname, cg_tasks.viewvalues()): 185 | for p in os.listdir(cg_path): acct_srcs[p] = join(cg_path, p) 186 | 187 | acct_nums = OrderedDict([ 188 | ('cpuacct', ['usage', 'usage_percpu']), 189 | ('memory', [ 190 | 'max_usage_in_bytes', 191 | 'memsw.max_usage_in_bytes', 192 | 'kmem.max_usage_in_bytes', 193 | 'kmem.tcp.max_usage_in_bytes']) ]) 194 | for rc, metrics in acct_nums.viewitems(): 195 | for p in metrics: 196 | p = '{}.{}'.format(rc, p) 197 | if p not in acct_srcs: continue 198 | with open(acct_srcs[p]) as src: 199 | numbers = map(int, src.read().strip().split()) 200 | acct[p] = ' '.join(map(num_format, numbers)) 201 | 202 | for p in 'time sectors io_merged io_serviced io_wait_time'.split(): 203 | p = 'blkio.{}'.format(p) 204 | try: src = acct_srcs[p] 205 | except KeyError: pass 206 | else: 207 | with open(src) as src: src = src.read().splitlines() 208 | for line in src: 209 | line = line.split() 210 | if not line or line[0] == 'Total': continue 211 | t = None 212 | try: dev, t, v = line 213 | except ValueError: dev, v = line 214 | dev = dev_resolve(*map(int, dev.split(':'))) 215 | if not dev: continue 216 | label = '{}[{}]'.format(p, dev) 217 | if t: label += '[{}]'.format(t) 218 | acct[label] = num_format(int(v)) 219 | 220 | for k, v in acct.viewitems(): 221 | print('{}: {}'.format(k, v), file=sys.stderr) 222 | 223 | # Cleanup tmp dirs 224 | leftovers = set() 225 | for tasks in cg_tasks.values(): 226 | tasks_dir = dirname(tasks) 227 | try: os.rmdir(tasks_dir) 228 | except (OSError, IOError): leftovers.add(tasks_dir) 229 | if leftovers: 230 | print( 'Leftover cgroup dirs remaining:{}\n'\ 231 | .format('\n '.join([''] + sorted(leftovers))), file=sys.stderr ) 232 | err = err or 1 233 | 234 | return err 235 | 236 | if __name__ == '__main__': sys.exit(main()) 237 | -------------------------------------------------------------------------------- /cgconf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | 5 | #################### 6 | 7 | import os, sys 8 | conf = '{}.yaml'.format(os.path.realpath(__file__).rsplit('.', 1)[0]) 9 | 10 | valid_rcs = set( line.split()[0] 11 | for line in open('/proc/cgroups') 12 | if line and not line.startswith('#') ) 13 | 14 | #################### 15 | 16 | 17 | import itertools as it, operator as op, functools as ft 18 | from subprocess import Popen, PIPE, STDOUT 19 | from os.path import join, isdir, isfile 20 | import yaml 21 | 22 | 23 | _default_perms = _default_rcs = _default_cg = _mounts = None 24 | 25 | 26 | def init_rc(rc, rc_path): 27 | log.debug('Initializing path for rc (%s): %r', rc, rc_path) 28 | 29 | mkdir_chk = not isdir(rc_path) 30 | if mkdir_chk: 31 | log.debug('Creating rc path: %r', rc_path) 32 | if not optz.dry_run: os.mkdir(rc_path) 33 | 34 | global _mounts 35 | if _mounts is None: 36 | _mounts = set(it.imap(op.itemgetter(4), it.ifilter( 37 | lambda line: (line[7] == 'cgroup')\ 38 | or (line[7] == '-' and line[8] == 'cgroup'),\ 39 | it.imap(op.methodcaller('split'), open('/proc/self/mountinfo')) ))) 40 | log.debug('Found mounts: %s', _mounts) 41 | 42 | if mkdir_chk or rc_path not in _mounts: 43 | if os.path.islink(rc_path): 44 | log.debug( 'Symlink in place of rc-path (rc: %s),' 45 | ' skipping (assuming hack or joint mount): %r', rc, rc_path ) 46 | else: 47 | mount_cmd = 'mount', '-t', 'cgroup', '-o', rc, rc, rc_path 48 | log.debug('Mounting rc path: %r (%s)', rc_path, ' '.join(mount_cmd)) 49 | if not optz.dry_run: 50 | if Popen(mount_cmd).wait(): 51 | raise RuntimeError( 'Failed to mount' 52 | ' rc path: {!r}, command: {}'.format(rc_path, mount_cmd) ) 53 | _mounts.add(rc_path) 54 | 55 | 56 | def parse_perms(spec): 57 | uid = gid = mode = None 58 | if not spec: return uid, gid, mode 59 | try: uid, spec = spec.split(':', 1) 60 | except ValueError: uid, spec = spec, None 61 | if uid: 62 | try: uid = int(uid) 63 | except ValueError: 64 | import pwd 65 | uid = pwd.getpwnam(uid).pw_uid 66 | else: uid = None 67 | if spec is None: return uid, gid, mode 68 | try: gid, spec = spec.split(':', 1) 69 | except ValueError: gid, spec = spec, None 70 | if not gid: 71 | if uid is not None: # "user:" spec 72 | import pwd 73 | gid = pwd.getpwuid(uid).pw_gid 74 | else: gid = None 75 | else: 76 | try: gid = int(gid) 77 | except ValueError: 78 | import grp 79 | gid = grp.getgrnam(gid).gr_gid 80 | if spec: mode = int(spec, 8) 81 | return uid, gid, mode 82 | 83 | def merge_perms(set1, set2): 84 | return tuple( 85 | tuple( 86 | (val1 if val1 is not None else val2) 87 | for val1, val2 in it.izip_longest(sset1, sset2, fillvalue=None) ) 88 | for sset1, sset2 in it.izip_longest(set1, set2, fillvalue=list()) ) 89 | 90 | def format_perms(*pset): 91 | pstrs = list() 92 | for t in pset: 93 | t = list(t) 94 | if isinstance(t[2], int): t[2] = '{:o}'.format(t[2]) 95 | pstrs.append(':'.join(map(bytes, t))) 96 | return ', '.join(pstrs) 97 | 98 | _units = dict( Ki=2**10, Mi=2**20, 99 | Gi=2**30, K=1e3, M=1e6, G=1e9 ) 100 | def interpret_val(val): 101 | try: 102 | num, units = val.split(' ') 103 | num = int(num) * _units[units] 104 | except (AttributeError, IndexError, ValueError, KeyError): return val 105 | else: return num 106 | 107 | 108 | def configure(path, settings, perms): 109 | global _default_perms 110 | if _default_perms is None: 111 | _default_perms = list() 112 | for k in '_tasks', '_admin', '_path': 113 | try: val = conf['defaults'][k] 114 | except KeyError: val = None 115 | _default_perms.append(parse_perms(val)) 116 | _default_perms = tuple(_default_perms) 117 | log.debug('Default permissions: %s', _default_perms) 118 | 119 | perms = merge_perms(map(parse_perms, perms), _default_perms) 120 | log.debug('Setting permissions for %r: %s', path, format_perms(perms)) 121 | if not optz.dry_run: 122 | if any(map(lambda n: n is not None, perms[2][:2])): 123 | os.chown(path, *perms[2][:2]) 124 | if perms[2][2] is not None: os.chmod(path, perms[2][2]) 125 | for node in it.ifilter(isfile, it.imap( 126 | ft.partial(join, path), os.listdir(path) )): 127 | os.chown(node, *perms[1][:2]) 128 | os.chmod(node, perms[1][2]) 129 | for fn in 'tasks', 'cgroup.procs': 130 | os.chown(join(path, fn), *perms[0][:2]) 131 | os.chmod(join(path, fn), perms[0][2]) 132 | 133 | log.debug('Configuring %r: %s', path, settings) 134 | if not optz.dry_run: 135 | for node, val in settings.viewitems(): 136 | val = interpret_val(val) 137 | ctl_path = join(path, node) 138 | ctl = open(ctl_path, 'wb') 139 | ctl.write(b'{}\n'.format(val)) 140 | try: ctl.close() 141 | except (IOError, OSError) as err: 142 | log.error('Failed to apply parameter (%r = %r): %s', ctl_path, val, err) 143 | 144 | 145 | def classify(cg_path, tasks): 146 | if not optz.dry_run: 147 | for task in tasks: 148 | try: 149 | if not open('/proc/{}/cmdline'.format(task)).read(): continue # kernel thread 150 | with open(join(cg_path, 'cgroup.procs'), 'wb') as ctl: ctl.write(b'{}\n'.format(task)) 151 | except (OSError, IOError): pass # most likely dead pid 152 | 153 | 154 | def is_rc_setting(key): 155 | 'Returns True if key is a setting for some cgroup variable, False for subpath.' 156 | if key in valid_rcs: return True 157 | if key and key.split('.', 1)[0] in valid_rcs: return True 158 | return False 159 | 160 | def settings_inline(rc_dict): 161 | rc_inline = dict() 162 | for rc,settings in rc_dict: 163 | if isinstance(settings, dict): 164 | for k,v in settings.viewitems(): 165 | rc_inline['{}.{}'.format(rc, k)] = v 166 | if not settings: rc_inline[rc] = dict() 167 | elif settings is None: rc_inline[rc] = dict() 168 | else: rc_inline[rc] = settings 169 | return rc_inline 170 | 171 | def settings_dict(rc_inline): 172 | rc_dict = dict(rc_inline) 173 | for rc_spec,val in rc_dict.items(): 174 | if '.' in rc_spec: 175 | rc, param = rc_spec.split('.', 1) 176 | rc_dict[rc] = rc_dict.get(rc, dict()) 177 | rc_dict[rc][param] = val 178 | del rc_dict[rc_spec] 179 | elif val is None: rc_dict[rc_spec] = dict() 180 | return rc_dict 181 | 182 | def settings_for_rc(rc, settings): 183 | return dict( ('{}.{}'.format(rc, k), v) 184 | for k,v in settings.viewitems() ) 185 | 186 | def path_for_rc(rc, name): 187 | return name # blkio is pseudo-hierarhical these days 188 | # return name if rc != 'blkio' else name.replace('/', '.') 189 | 190 | def parse_cg(name='', contents=dict()): 191 | if name and name.rsplit('/', 1)[-1].startswith('_'): 192 | log.debug('Skipping special (prefixed) section: %s', name) 193 | return 194 | 195 | global _default_rcs 196 | if _default_rcs is None: 197 | _default_rcs = settings_inline(it.ifilter( 198 | lambda v: not v[0].startswith('_'), 199 | conf.get('defaults', dict()).viewitems() )) 200 | log.debug( 'Default settings:\n%s', 201 | '\n'.join(' {!r} = {!r}'.format(k,v) for k,v in 202 | sorted(_default_rcs.viewitems(), key=op.itemgetter(0))) ) 203 | if contents is None: contents = dict() 204 | contents_rc = dict((k,v) for k,v in contents.viewitems() if is_rc_setting(k)) 205 | 206 | log.debug(' -- Processing group %r', name or '(root)') 207 | 208 | if name.endswith('_') or contents_rc\ 209 | or not contents or filter(lambda k: k.startswith('_'), contents): 210 | name = name.rstrip('_') 211 | for k in contents_rc: del contents[k] # don't process these as subgroups 212 | contents_rc = settings_inline(contents_rc.viewitems()) 213 | 214 | if contents_rc: 215 | log.debug('Detected rc settings for group, applying: %s', contents_rc) 216 | 217 | settings = _default_rcs.copy() 218 | settings.update( 219 | settings_inline(it.ifilter( 220 | lambda v: not v[0].startswith('_'), 221 | contents_rc.viewitems() )) ) 222 | settings = settings_dict(settings.viewitems()) 223 | 224 | for rc,settings in settings.viewitems(): 225 | log.debug('Configuring %r: %s = %s', name, rc, settings) 226 | rc_path, rc_name = join(conf['path'], rc), path_for_rc(rc, name) 227 | init_rc(rc, rc_path) 228 | cg_path = join(rc_path, rc_name) 229 | if not isdir(cg_path): 230 | log.debug('Creating cgroup path: %r', cg_path) 231 | if not optz.dry_run: 232 | cg_base = rc_path 233 | for slug in rc_name.split('/'): 234 | cg_base = join(cg_base, slug) 235 | if not isdir(cg_base): os.mkdir(cg_base) 236 | configure( cg_path, settings_for_rc(rc, settings), 237 | (contents.get('_tasks', ''), contents.get('_admin', ''), contents.get('_path', '')) ) 238 | if contents.get('_default'): 239 | global _default_cg 240 | if _default_cg is not None and _default_cg != name: 241 | raise ValueError('There can be only one default cgroup') 242 | log.debug('Populating default cgroup: %r', cg_path) 243 | if not optz.dry_run: 244 | read_pids = lambda path: (int(line.strip()) for line in open(join(path, 'cgroup.procs'))) 245 | pids = set( read_pids(rc_path) 246 | if not optz.reset else it.chain.from_iterable( 247 | read_pids(root) for root,dirs,files in os.walk(rc_path) 248 | if 'cgroup.procs' in files and root != cg_path ) ) 249 | classify(cg_path, pids) 250 | _default_cg = name 251 | 252 | if contents: # can be leftovers after diff with contents_rc 253 | for subname, contents in contents.viewitems(): 254 | parse_cg(join(name, subname), contents) 255 | 256 | 257 | 258 | def main(args=None): 259 | global optz, conf, log 260 | 261 | import argparse 262 | parser = argparse.ArgumentParser( 263 | description='Tool to create cgroup hierarchy and perform a basic tasks classification.') 264 | parser.add_argument('-c', '--conf', default=conf, help='Configuration file (default: %(default)s).') 265 | parser.add_argument('-r', '--reset', action='store_true', 266 | help='Put all processes into a default cgroup, not just non-classified ones.') 267 | parser.add_argument('-p', '--dry-run', action='store_true', help='Just show what has to be done.') 268 | parser.add_argument('--debug', action='store_true', help='Verbose operation mode.') 269 | optz = parser.parse_args(sys.argv[1:] if args is None else args) 270 | 271 | import logging 272 | logging.basicConfig(level=logging.DEBUG if optz.debug else logging.INFO) 273 | log = logging.getLogger() 274 | 275 | conf = yaml.safe_load(open(optz.conf).read().replace('\t', ' ')) 276 | parse_cg(contents=conf['groups']) 277 | 278 | if __name__ == '__main__': sys.exit(main()) 279 | --------------------------------------------------------------------------------