Ved-vampir | 98a9917 | 2015-03-17 14:58:15 +0300 | [diff] [blame^] | 1 | #!/usr/bin/env python |
| 2 | |
| 3 | # Try to determine how much RAM is currently being used per program. |
| 4 | # Note per _program_, not per process. So for example this script |
| 5 | # will report RAM used by all httpd process together. In detail it reports: |
| 6 | # sum(private RAM for program processes) + sum(Shared RAM for program processes) |
| 7 | # The shared RAM is problematic to calculate, and this script automatically |
| 8 | # selects the most accurate method available for your kernel. |
| 9 | |
| 10 | # Licence: LGPLv2 |
| 11 | # Author: P@draigBrady.com |
| 12 | # Source: http://www.pixelbeat.org/scripts/ps_mem.py |
| 13 | |
| 14 | # V1.0 06 Jul 2005 Initial release |
| 15 | # V1.1 11 Aug 2006 root permission required for accuracy |
| 16 | # V1.2 08 Nov 2006 Add total to output |
| 17 | # Use KiB,MiB,... for units rather than K,M,... |
| 18 | # V1.3 22 Nov 2006 Ignore shared col from /proc/$pid/statm for |
| 19 | # 2.6 kernels up to and including 2.6.9. |
| 20 | # There it represented the total file backed extent |
| 21 | # V1.4 23 Nov 2006 Remove total from output as it's meaningless |
| 22 | # (the shared values overlap with other programs). |
| 23 | # Display the shared column. This extra info is |
| 24 | # useful, especially as it overlaps between programs. |
| 25 | # V1.5 26 Mar 2007 Remove redundant recursion from human() |
| 26 | # V1.6 05 Jun 2007 Also report number of processes with a given name. |
| 27 | # Patch from riccardo.murri@gmail.com |
| 28 | # V1.7 20 Sep 2007 Use PSS from /proc/$pid/smaps if available, which |
| 29 | # fixes some over-estimation and allows totalling. |
| 30 | # Enumerate the PIDs directly rather than using ps, |
| 31 | # which fixes the possible race between reading |
| 32 | # RSS with ps, and shared memory with this program. |
| 33 | # Also we can show non truncated command names. |
| 34 | # V1.8 28 Sep 2007 More accurate matching for stats in /proc/$pid/smaps |
| 35 | # as otherwise could match libraries causing a crash. |
| 36 | # Patch from patrice.bouchand.fedora@gmail.com |
| 37 | # V1.9 20 Feb 2008 Fix invalid values reported when PSS is available. |
| 38 | # Reported by Andrey Borzenkov <arvidjaar@mail.ru> |
| 39 | # V3.3 24 Jun 2014 |
| 40 | # http://github.com/pixelb/scripts/commits/master/scripts/ps_mem.py |
| 41 | |
| 42 | # Notes: |
| 43 | # |
| 44 | # All interpreted programs where the interpreter is started |
| 45 | # by the shell or with env, will be merged to the interpreter |
| 46 | # (as that's what's given to exec). For e.g. all python programs |
| 47 | # starting with "#!/usr/bin/env python" will be grouped under python. |
| 48 | # You can change this by using the full command line but that will |
| 49 | # have the undesirable affect of splitting up programs started with |
| 50 | # differing parameters (for e.g. mingetty tty[1-6]). |
| 51 | # |
| 52 | # For 2.6 kernels up to and including 2.6.13 and later 2.4 redhat kernels |
| 53 | # (rmap vm without smaps) it can not be accurately determined how many pages |
| 54 | # are shared between processes in general or within a program in our case: |
| 55 | # http://lkml.org/lkml/2005/7/6/250 |
| 56 | # A warning is printed if overestimation is possible. |
| 57 | # In addition for 2.6 kernels up to 2.6.9 inclusive, the shared |
| 58 | # value in /proc/$pid/statm is the total file-backed extent of a process. |
| 59 | # We ignore that, introducing more overestimation, again printing a warning. |
| 60 | # Since kernel 2.6.23-rc8-mm1 PSS is available in smaps, which allows |
| 61 | # us to calculate a more accurate value for the total RAM used by programs. |
| 62 | # |
| 63 | # Programs that use CLONE_VM without CLONE_THREAD are discounted by assuming |
| 64 | # they're the only programs that have the same /proc/$PID/smaps file for |
| 65 | # each instance. This will fail if there are multiple real instances of a |
| 66 | # program that then use CLONE_VM without CLONE_THREAD, or if a clone changes |
| 67 | # its memory map while we're checksumming each /proc/$PID/smaps. |
| 68 | # |
| 69 | # I don't take account of memory allocated for a program |
| 70 | # by other programs. For e.g. memory used in the X server for |
| 71 | # a program could be determined, but is not. |
| 72 | # |
| 73 | # FreeBSD is supported if linprocfs is mounted at /compat/linux/proc/ |
| 74 | # FreeBSD 8.0 supports up to a level of Linux 2.6.16 |
| 75 | |
| 76 | import getopt |
| 77 | import time |
| 78 | import errno |
| 79 | import os |
| 80 | import sys |
| 81 | |
| 82 | try: |
| 83 | # md5 module is deprecated on python 2.6 |
| 84 | # so try the newer hashlib first |
| 85 | import hashlib |
| 86 | md5_new = hashlib.md5 |
| 87 | except ImportError: |
| 88 | import md5 |
| 89 | md5_new = md5.new |
| 90 | |
| 91 | |
| 92 | # The following exits cleanly on Ctrl-C or EPIPE |
| 93 | # while treating other exceptions as before. |
| 94 | def std_exceptions(etype, value, tb): |
| 95 | sys.excepthook = sys.__excepthook__ |
| 96 | if issubclass(etype, KeyboardInterrupt): |
| 97 | pass |
| 98 | elif issubclass(etype, IOError) and value.errno == errno.EPIPE: |
| 99 | pass |
| 100 | else: |
| 101 | sys.__excepthook__(etype, value, tb) |
| 102 | sys.excepthook = std_exceptions |
| 103 | |
| 104 | # |
| 105 | # Define some global variables |
| 106 | # |
| 107 | |
| 108 | PAGESIZE = os.sysconf("SC_PAGE_SIZE") / 1024 #KiB |
| 109 | our_pid = os.getpid() |
| 110 | |
| 111 | have_pss = 0 |
| 112 | |
| 113 | class Proc: |
| 114 | def __init__(self): |
| 115 | uname = os.uname() |
| 116 | if uname[0] == "FreeBSD": |
| 117 | self.proc = '/compat/linux/proc' |
| 118 | else: |
| 119 | self.proc = '/proc' |
| 120 | |
| 121 | def path(self, *args): |
| 122 | return os.path.join(self.proc, *(str(a) for a in args)) |
| 123 | |
| 124 | def open(self, *args): |
| 125 | try: |
| 126 | return open(self.path(*args)) |
| 127 | except (IOError, OSError): |
| 128 | val = sys.exc_info()[1] |
| 129 | if (val.errno == errno.ENOENT or # kernel thread or process gone |
| 130 | val.errno == errno.EPERM): |
| 131 | raise LookupError |
| 132 | raise |
| 133 | |
| 134 | proc = Proc() |
| 135 | |
| 136 | |
| 137 | # |
| 138 | # Functions |
| 139 | # |
| 140 | |
| 141 | def parse_options(): |
| 142 | try: |
| 143 | long_options = ['split-args', 'help', 'total'] |
| 144 | opts, args = getopt.getopt(sys.argv[1:], "shtp:w:", long_options) |
| 145 | except getopt.GetoptError: |
| 146 | sys.stderr.write(help()) |
| 147 | sys.exit(3) |
| 148 | |
| 149 | if len(args): |
| 150 | sys.stderr.write("Extraneous arguments: %s\n" % args) |
| 151 | sys.exit(3) |
| 152 | |
| 153 | # ps_mem.py options |
| 154 | split_args = False |
| 155 | pids_to_show = None |
| 156 | watch = None |
| 157 | only_total = False |
| 158 | |
| 159 | for o, a in opts: |
| 160 | if o in ('-s', '--split-args'): |
| 161 | split_args = True |
| 162 | if o in ('-t', '--total'): |
| 163 | only_total = True |
| 164 | if o in ('-h', '--help'): |
| 165 | sys.stdout.write(help()) |
| 166 | sys.exit(0) |
| 167 | if o in ('-p',): |
| 168 | try: |
| 169 | pids_to_show = [int(x) for x in a.split(',')] |
| 170 | except: |
| 171 | sys.stderr.write(help()) |
| 172 | sys.exit(3) |
| 173 | if o in ('-w',): |
| 174 | try: |
| 175 | watch = int(a) |
| 176 | except: |
| 177 | sys.stderr.write(help()) |
| 178 | sys.exit(3) |
| 179 | |
| 180 | return (split_args, pids_to_show, watch, only_total) |
| 181 | |
| 182 | def help(): |
| 183 | help_msg = 'Usage: ps_mem [OPTION]...\n' \ |
| 184 | 'Show program core memory usage\n' \ |
| 185 | '\n' \ |
| 186 | ' -h, -help Show this help\n' \ |
| 187 | ' -p <pid>[,pid2,...pidN] Only show memory usage PIDs in the specified list\n' \ |
| 188 | ' -s, --split-args Show and separate by, all command line arguments\n' \ |
| 189 | ' -t, --total Show only the total value\n' \ |
| 190 | ' -w <N> Measure and show process memory every N seconds\n' |
| 191 | |
| 192 | return help_msg |
| 193 | |
| 194 | #(major,minor,release) |
| 195 | def kernel_ver(): |
| 196 | kv = proc.open('sys/kernel/osrelease').readline().split(".")[:3] |
| 197 | last = len(kv) |
| 198 | if last == 2: |
| 199 | kv.append('0') |
| 200 | last -= 1 |
| 201 | while last > 0: |
| 202 | for char in "-_": |
| 203 | kv[last] = kv[last].split(char)[0] |
| 204 | try: |
| 205 | int(kv[last]) |
| 206 | except: |
| 207 | kv[last] = 0 |
| 208 | last -= 1 |
| 209 | return (int(kv[0]), int(kv[1]), int(kv[2])) |
| 210 | |
| 211 | |
| 212 | #return Private,Shared |
| 213 | #Note shared is always a subset of rss (trs is not always) |
| 214 | def getMemStats(pid): |
| 215 | global have_pss |
| 216 | mem_id = pid #unique |
| 217 | Private_lines = [] |
| 218 | Shared_lines = [] |
| 219 | Pss_lines = [] |
| 220 | Rss = (int(proc.open(pid, 'statm').readline().split()[1]) |
| 221 | * PAGESIZE) |
| 222 | if os.path.exists(proc.path(pid, 'smaps')): #stat |
| 223 | digester = md5_new() |
| 224 | for line in proc.open(pid, 'smaps').readlines(): #open |
| 225 | # Note we checksum smaps as maps is usually but |
| 226 | # not always different for separate processes. |
| 227 | digester.update(line.encode('latin1')) |
| 228 | if line.startswith("Shared"): |
| 229 | Shared_lines.append(line) |
| 230 | elif line.startswith("Private"): |
| 231 | Private_lines.append(line) |
| 232 | elif line.startswith("Pss"): |
| 233 | have_pss = 1 |
| 234 | Pss_lines.append(line) |
| 235 | mem_id = digester.hexdigest() |
| 236 | Shared = sum([int(line.split()[1]) for line in Shared_lines]) |
| 237 | Private = sum([int(line.split()[1]) for line in Private_lines]) |
| 238 | #Note Shared + Private = Rss above |
| 239 | #The Rss in smaps includes video card mem etc. |
| 240 | if have_pss: |
| 241 | pss_adjust = 0.5 # add 0.5KiB as this avg error due to trunctation |
| 242 | Pss = sum([float(line.split()[1])+pss_adjust for line in Pss_lines]) |
| 243 | Shared = Pss - Private |
| 244 | elif (2,6,1) <= kernel_ver() <= (2,6,9): |
| 245 | Shared = 0 #lots of overestimation, but what can we do? |
| 246 | Private = Rss |
| 247 | else: |
| 248 | Shared = int(proc.open(pid, 'statm').readline().split()[2]) |
| 249 | Shared *= PAGESIZE |
| 250 | Private = Rss - Shared |
| 251 | return (Private, Shared, mem_id) |
| 252 | |
| 253 | |
| 254 | def getCmdName(pid, split_args): |
| 255 | cmdline = proc.open(pid, 'cmdline').read().split("\0") |
| 256 | if cmdline[-1] == '' and len(cmdline) > 1: |
| 257 | cmdline = cmdline[:-1] |
| 258 | |
| 259 | path = proc.path(pid, 'exe') |
| 260 | try: |
| 261 | path = os.readlink(path) |
| 262 | # Some symlink targets were seen to contain NULs on RHEL 5 at least |
| 263 | # https://github.com/pixelb/scripts/pull/10, so take string up to NUL |
| 264 | path = path.split('\0')[0] |
| 265 | except OSError: |
| 266 | val = sys.exc_info()[1] |
| 267 | if (val.errno == errno.ENOENT or # either kernel thread or process gone |
| 268 | val.errno == errno.EPERM): |
| 269 | raise LookupError |
| 270 | raise |
| 271 | |
| 272 | if split_args: |
| 273 | return " ".join(cmdline) |
| 274 | if path.endswith(" (deleted)"): |
| 275 | path = path[:-10] |
| 276 | if os.path.exists(path): |
| 277 | path += " [updated]" |
| 278 | else: |
| 279 | #The path could be have prelink stuff so try cmdline |
| 280 | #which might have the full path present. This helped for: |
| 281 | #/usr/libexec/notification-area-applet.#prelink#.fX7LCT (deleted) |
| 282 | if os.path.exists(cmdline[0]): |
| 283 | path = cmdline[0] + " [updated]" |
| 284 | else: |
| 285 | path += " [deleted]" |
| 286 | exe = os.path.basename(path) |
| 287 | cmd = proc.open(pid, 'status').readline()[6:-1] |
| 288 | if exe.startswith(cmd): |
| 289 | cmd = exe #show non truncated version |
| 290 | #Note because we show the non truncated name |
| 291 | #one can have separated programs as follows: |
| 292 | #584.0 KiB + 1.0 MiB = 1.6 MiB mozilla-thunder (exe -> bash) |
| 293 | # 56.0 MiB + 22.2 MiB = 78.2 MiB mozilla-thunderbird-bin |
| 294 | return cmd |
| 295 | |
| 296 | |
| 297 | #The following matches "du -h" output |
| 298 | #see also human.py |
| 299 | def human(num, power="Ki", units=None): |
| 300 | if units is None: |
| 301 | powers = ["Ki", "Mi", "Gi", "Ti"] |
| 302 | while num >= 1000: #4 digits |
| 303 | num /= 1024.0 |
| 304 | power = powers[powers.index(power)+1] |
| 305 | return "%.1f %sB" % (num, power) |
| 306 | else: |
| 307 | return "%.f" % ((num * 1024) / units) |
| 308 | |
| 309 | |
| 310 | def cmd_with_count(cmd, count): |
| 311 | if count > 1: |
| 312 | return "%s (%u)" % (cmd, count) |
| 313 | else: |
| 314 | return cmd |
| 315 | |
| 316 | #Warn of possible inaccuracies |
| 317 | #2 = accurate & can total |
| 318 | #1 = accurate only considering each process in isolation |
| 319 | #0 = some shared mem not reported |
| 320 | #-1= all shared mem not reported |
| 321 | def shared_val_accuracy(): |
| 322 | """http://wiki.apache.org/spamassassin/TopSharedMemoryBug""" |
| 323 | kv = kernel_ver() |
| 324 | pid = os.getpid() |
| 325 | if kv[:2] == (2,4): |
| 326 | if proc.open('meminfo').read().find("Inact_") == -1: |
| 327 | return 1 |
| 328 | return 0 |
| 329 | elif kv[:2] == (2,6): |
| 330 | if os.path.exists(proc.path(pid, 'smaps')): |
| 331 | if proc.open(pid, 'smaps').read().find("Pss:")!=-1: |
| 332 | return 2 |
| 333 | else: |
| 334 | return 1 |
| 335 | if (2,6,1) <= kv <= (2,6,9): |
| 336 | return -1 |
| 337 | return 0 |
| 338 | elif kv[0] > 2 and os.path.exists(proc.path(pid, 'smaps')): |
| 339 | return 2 |
| 340 | else: |
| 341 | return 1 |
| 342 | |
| 343 | def show_shared_val_accuracy( possible_inacc, only_total=False ): |
| 344 | level = ("Warning","Error")[only_total] |
| 345 | if possible_inacc == -1: |
| 346 | sys.stderr.write( |
| 347 | "%s: Shared memory is not reported by this system.\n" % level |
| 348 | ) |
| 349 | sys.stderr.write( |
| 350 | "Values reported will be too large, and totals are not reported\n" |
| 351 | ) |
| 352 | elif possible_inacc == 0: |
| 353 | sys.stderr.write( |
| 354 | "%s: Shared memory is not reported accurately by this system.\n" % level |
| 355 | ) |
| 356 | sys.stderr.write( |
| 357 | "Values reported could be too large, and totals are not reported\n" |
| 358 | ) |
| 359 | elif possible_inacc == 1: |
| 360 | sys.stderr.write( |
| 361 | "%s: Shared memory is slightly over-estimated by this system\n" |
| 362 | "for each program, so totals are not reported.\n" % level |
| 363 | ) |
| 364 | sys.stderr.close() |
| 365 | if only_total and possible_inacc != 2: |
| 366 | sys.exit(1) |
| 367 | |
| 368 | def get_memory_usage( pids_to_show, split_args, include_self=False, only_self=False ): |
| 369 | cmds = {} |
| 370 | shareds = {} |
| 371 | mem_ids = {} |
| 372 | count = {} |
| 373 | for pid in os.listdir(proc.path('')): |
| 374 | if not pid.isdigit(): |
| 375 | continue |
| 376 | pid = int(pid) |
| 377 | |
| 378 | # Some filters |
| 379 | if only_self and pid != our_pid: |
| 380 | continue |
| 381 | if pid == our_pid and not include_self: |
| 382 | continue |
| 383 | if pids_to_show is not None and pid not in pids_to_show: |
| 384 | continue |
| 385 | |
| 386 | try: |
| 387 | cmd = getCmdName(pid, split_args) |
| 388 | except LookupError: |
| 389 | #operation not permitted |
| 390 | #kernel threads don't have exe links or |
| 391 | #process gone |
| 392 | continue |
| 393 | |
| 394 | try: |
| 395 | private, shared, mem_id = getMemStats(pid) |
| 396 | except RuntimeError: |
| 397 | continue #process gone |
| 398 | if shareds.get(cmd): |
| 399 | if have_pss: #add shared portion of PSS together |
| 400 | shareds[cmd] += shared |
| 401 | elif shareds[cmd] < shared: #just take largest shared val |
| 402 | shareds[cmd] = shared |
| 403 | else: |
| 404 | shareds[cmd] = shared |
| 405 | cmds[cmd] = cmds.setdefault(cmd, 0) + private |
| 406 | if cmd in count: |
| 407 | count[cmd] += 1 |
| 408 | else: |
| 409 | count[cmd] = 1 |
| 410 | mem_ids.setdefault(cmd, {}).update({mem_id:None}) |
| 411 | |
| 412 | #Add shared mem for each program |
| 413 | total = 0 |
| 414 | for cmd in cmds: |
| 415 | cmd_count = count[cmd] |
| 416 | if len(mem_ids[cmd]) == 1 and cmd_count > 1: |
| 417 | # Assume this program is using CLONE_VM without CLONE_THREAD |
| 418 | # so only account for one of the processes |
| 419 | cmds[cmd] /= cmd_count |
| 420 | if have_pss: |
| 421 | shareds[cmd] /= cmd_count |
| 422 | cmds[cmd] = cmds[cmd] + shareds[cmd] |
| 423 | total += cmds[cmd] #valid if PSS available |
| 424 | |
| 425 | sorted_cmds = sorted(cmds.items(), key=lambda x:x[1]) |
| 426 | sorted_cmds = [x for x in sorted_cmds if x[1]] |
| 427 | |
| 428 | return sorted_cmds, shareds, count, total |
| 429 | |
| 430 | def print_header(): |
| 431 | sys.stdout.write(" Private + Shared = RAM used\tProgram\n\n") |
| 432 | |
| 433 | def print_memory_usage(sorted_cmds, shareds, count, total): |
| 434 | for cmd in sorted_cmds: |
| 435 | sys.stdout.write("%9s + %9s = %9s\t%s\n" % |
| 436 | (human(cmd[1]-shareds[cmd[0]]), |
| 437 | human(shareds[cmd[0]]), human(cmd[1]), |
| 438 | cmd_with_count(cmd[0], count[cmd[0]]))) |
| 439 | if have_pss: |
| 440 | sys.stdout.write("%s\n%s%9s\n%s\n" % |
| 441 | ("-" * 33, " " * 24, human(total), "=" * 33)) |
| 442 | |
| 443 | def verify_environment(): |
| 444 | if os.geteuid() != 0: |
| 445 | sys.stderr.write("Sorry, root permission required.\n") |
| 446 | if __name__ == '__main__': |
| 447 | sys.stderr.close() |
| 448 | sys.exit(1) |
| 449 | |
| 450 | try: |
| 451 | kv = kernel_ver() |
| 452 | except (IOError, OSError): |
| 453 | val = sys.exc_info()[1] |
| 454 | if val.errno == errno.ENOENT: |
| 455 | sys.stderr.write( |
| 456 | "Couldn't access " + proc.path('') + "\n" |
| 457 | "Only GNU/Linux and FreeBSD (with linprocfs) are supported\n") |
| 458 | sys.exit(2) |
| 459 | else: |
| 460 | raise |
| 461 | |
| 462 | if __name__ == '__main__': |
| 463 | split_args, pids_to_show, watch, only_total = parse_options() |
| 464 | verify_environment() |
| 465 | |
| 466 | if not only_total: |
| 467 | print_header() |
| 468 | |
| 469 | if watch is not None: |
| 470 | try: |
| 471 | sorted_cmds = True |
| 472 | while sorted_cmds: |
| 473 | sorted_cmds, shareds, count, total = get_memory_usage( pids_to_show, split_args ) |
| 474 | if only_total and have_pss: |
| 475 | sys.stdout.write(human(total, units=1)+'\n') |
| 476 | elif not only_total: |
| 477 | print_memory_usage(sorted_cmds, shareds, count, total) |
| 478 | time.sleep(watch) |
| 479 | else: |
| 480 | sys.stdout.write('Process does not exist anymore.\n') |
| 481 | except KeyboardInterrupt: |
| 482 | pass |
| 483 | else: |
| 484 | # This is the default behavior |
| 485 | sorted_cmds, shareds, count, total = get_memory_usage( pids_to_show, split_args ) |
| 486 | if only_total and have_pss: |
| 487 | sys.stdout.write(human(total, units=1)+'\n') |
| 488 | elif not only_total: |
| 489 | print_memory_usage(sorted_cmds, shareds, count, total) |
| 490 | |
| 491 | # We must close explicitly, so that any EPIPE exception |
| 492 | # is handled by our excepthook, rather than the default |
| 493 | # one which is reenabled after this script finishes. |
| 494 | sys.stdout.close() |
| 495 | |
| 496 | vm_accuracy = shared_val_accuracy() |
| 497 | show_shared_val_accuracy( vm_accuracy, only_total ) |