SUP3R-US3R

mem.py

Oct 30th, 2016
138
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 17.62 KB | None | 0 0
  1. #!/usr/bin/env python
  2.  
  3. import getopt
  4. import time
  5. import errno
  6. import os
  7. import sys
  8.  
  9. # The following exits cleanly on Ctrl-C or EPIPE
  10. # while treating other exceptions as before.
  11. def std_exceptions(etype, value, tb):
  12.     sys.excepthook = sys.__excepthook__
  13.     if issubclass(etype, KeyboardInterrupt):
  14.         pass
  15.     elif issubclass(etype, IOError) and value.errno == errno.EPIPE:
  16.         pass
  17.     else:
  18.         sys.__excepthook__(etype, value, tb)
  19. sys.excepthook = std_exceptions
  20.  
  21. #
  22. #   Define some global variables
  23. #
  24.  
  25. PAGESIZE = os.sysconf("SC_PAGE_SIZE") / 1024 #KiB
  26. our_pid = os.getpid()
  27.  
  28. have_pss = 0
  29. have_swap_pss = 0
  30.  
  31. class Proc:
  32.     def __init__(self):
  33.         uname = os.uname()
  34.         if uname[0] == "FreeBSD":
  35.             self.proc = '/compat/linux/proc'
  36.         else:
  37.             self.proc = '/proc'
  38.  
  39.     def path(self, *args):
  40.         return os.path.join(self.proc, *(str(a) for a in args))
  41.  
  42.     def open(self, *args):
  43.         try:
  44.             if sys.version_info < (3,):
  45.                 return open(self.path(*args))
  46.             else:
  47.                 return open(self.path(*args), errors='ignore')
  48.         except (IOError, OSError):
  49.             val = sys.exc_info()[1]
  50.             if (val.errno == errno.ENOENT or # kernel thread or process gone
  51.                 val.errno == errno.EPERM):
  52.                 raise LookupError
  53.             raise
  54.  
  55. proc = Proc()
  56.  
  57.  
  58. #
  59. #   Functions
  60. #
  61.  
  62. def parse_options():
  63.     try:
  64.         long_options = [
  65.             'split-args',
  66.             'help',
  67.             'total',
  68.             'discriminate-by-pid',
  69.             'swap'
  70.         ]
  71.         opts, args = getopt.getopt(sys.argv[1:], "shtdSp:w:", long_options)
  72.     except getopt.GetoptError:
  73.         sys.stderr.write(help())
  74.         sys.exit(3)
  75.  
  76.     if len(args):
  77.         sys.stderr.write("Extraneous arguments: %s\n" % args)
  78.         sys.exit(3)
  79.  
  80.     # ps_mem.py options
  81.     split_args = False
  82.     pids_to_show = None
  83.     discriminate_by_pid = False
  84.     show_swap = False
  85.     watch = None
  86.     only_total = False
  87.  
  88.     for o, a in opts:
  89.         if o in ('-s', '--split-args'):
  90.             split_args = True
  91.         if o in ('-t', '--total'):
  92.             only_total = True
  93.         if o in ('-d', '--discriminate-by-pid'):
  94.             discriminate_by_pid = True
  95.         if o in ('-S', '--swap'):
  96.             show_swap = True
  97.         if o in ('-h', '--help'):
  98.             sys.stdout.write(help())
  99.             sys.exit(0)
  100.         if o in ('-p',):
  101.             try:
  102.                 pids_to_show = [int(x) for x in a.split(',')]
  103.             except:
  104.                 sys.stderr.write(help())
  105.                 sys.exit(3)
  106.         if o in ('-w',):
  107.             try:
  108.                 watch = int(a)
  109.             except:
  110.                 sys.stderr.write(help())
  111.                 sys.exit(3)
  112.  
  113.     return (
  114.         split_args,
  115.         pids_to_show,
  116.         watch,
  117.         only_total,
  118.         discriminate_by_pid,
  119.         show_swap
  120.     )
  121.  
  122.  
  123. def help():
  124.     help_msg = 'Usage: ps_mem [OPTION]...\n' \
  125.         'Show program core memory usage\n' \
  126.         '\n' \
  127.         '  -h, -help                   Show this help\n' \
  128.         '  -p <pid>[,pid2,...pidN]     Only show memory usage PIDs in the '\
  129.         'specified list\n' \
  130.         '  -s, --split-args            Show and separate by, all command line'\
  131.         ' arguments\n' \
  132.         '  -t, --total                 Show only the total value\n' \
  133.         '  -d, --discriminate-by-pid   Show by process rather than by program\n' \
  134.         '  -S, --swap                  Show swap information\n' \
  135.         '  -w <N>                      Measure and show process memory every'\
  136.         ' N seconds\n'
  137.  
  138.     return help_msg
  139.  
  140.  
  141. # (major,minor,release)
  142. def kernel_ver():
  143.     kv = proc.open('sys/kernel/osrelease').readline().split(".")[:3]
  144.     last = len(kv)
  145.     if last == 2:
  146.         kv.append('0')
  147.     last -= 1
  148.     while last > 0:
  149.         for char in "-_":
  150.             kv[last] = kv[last].split(char)[0]
  151.         try:
  152.             int(kv[last])
  153.         except:
  154.             kv[last] = 0
  155.         last -= 1
  156.     return (int(kv[0]), int(kv[1]), int(kv[2]))
  157.  
  158.  
  159. #return Private,Shared
  160. #Note shared is always a subset of rss (trs is not always)
  161. def getMemStats(pid):
  162.     global have_pss
  163.     global have_swap_pss
  164.     mem_id = pid #unique
  165.     Private_lines = []
  166.     Shared_lines = []
  167.     Pss_lines = []
  168.     Rss = (int(proc.open(pid, 'statm').readline().split()[1])
  169.            * PAGESIZE)
  170.     Swap_lines = []
  171.     Swap_pss_lines = []
  172.  
  173.     Swap = 0
  174.     Swap_pss = 0
  175.  
  176.     if os.path.exists(proc.path(pid, 'smaps')):  # stat
  177.         lines = proc.open(pid, 'smaps').readlines()  # open
  178.         # Note we checksum smaps as maps is usually but
  179.         # not always different for separate processes.
  180.         mem_id = hash(''.join(lines))
  181.         for line in lines:
  182.             if line.startswith("Shared"):
  183.                 Shared_lines.append(line)
  184.             elif line.startswith("Private"):
  185.                 Private_lines.append(line)
  186.             elif line.startswith("Pss"):
  187.                 have_pss = 1
  188.                 Pss_lines.append(line)
  189.             elif line.startswith("Swap:"):
  190.                 Swap_lines.append(line)
  191.             elif line.startswith("SwapPss:"):
  192.                 have_swap_pss = 1
  193.                 Swap_pss_lines.append(line)
  194.         Shared = sum([int(line.split()[1]) for line in Shared_lines])
  195.         Private = sum([int(line.split()[1]) for line in Private_lines])
  196.         #Note Shared + Private = Rss above
  197.         #The Rss in smaps includes video card mem etc.
  198.         if have_pss:
  199.             pss_adjust = 0.5 # add 0.5KiB as this avg error due to truncation
  200.             Pss = sum([float(line.split()[1])+pss_adjust for line in Pss_lines])
  201.             Shared = Pss - Private
  202.         # Note that Swap = Private swap + Shared swap.
  203.         Swap = sum([int(line.split()[1]) for line in Swap_lines])
  204.         if have_swap_pss:
  205.             # The kernel supports SwapPss, that shows proportional swap share.
  206.             # Note that Swap - SwapPss is not Private Swap.
  207.             Swap_pss = sum([int(line.split()[1]) for line in Swap_pss_lines])
  208.     elif (2,6,1) <= kernel_ver() <= (2,6,9):
  209.         Shared = 0 #lots of overestimation, but what can we do?
  210.         Private = Rss
  211.     else:
  212.         Shared = int(proc.open(pid, 'statm').readline().split()[2])
  213.         Shared *= PAGESIZE
  214.         Private = Rss - Shared
  215.     return (Private, Shared, mem_id, Swap, Swap_pss)
  216.  
  217.  
  218. def getCmdName(pid, split_args, discriminate_by_pid):
  219.     cmdline = proc.open(pid, 'cmdline').read().split("\0")
  220.     if cmdline[-1] == '' and len(cmdline) > 1:
  221.         cmdline = cmdline[:-1]
  222.  
  223.     path = proc.path(pid, 'exe')
  224.     try:
  225.         path = os.readlink(path)
  226.         # Some symlink targets were seen to contain NULs on RHEL 5 at least
  227.         # https://github.com/pixelb/scripts/pull/10, so take string up to NUL
  228.         path = path.split('\0')[0]
  229.     except OSError:
  230.         val = sys.exc_info()[1]
  231.         if (val.errno == errno.ENOENT or # either kernel thread or process gone
  232.             val.errno == errno.EPERM):
  233.             raise LookupError
  234.         raise
  235.  
  236.     if split_args:
  237.         return " ".join(cmdline)
  238.     if path.endswith(" (deleted)"):
  239.         path = path[:-10]
  240.         if os.path.exists(path):
  241.             path += " [updated]"
  242.         else:
  243.             #The path could be have prelink stuff so try cmdline
  244.             #which might have the full path present. This helped for:
  245.             #/usr/libexec/notification-area-applet.#prelink#.fX7LCT (deleted)
  246.             if os.path.exists(cmdline[0]):
  247.                 path = cmdline[0] + " [updated]"
  248.             else:
  249.                 path += " [deleted]"
  250.     exe = os.path.basename(path)
  251.     cmd = proc.open(pid, 'status').readline()[6:-1]
  252.     if exe.startswith(cmd):
  253.         cmd = exe #show non truncated version
  254.         #Note because we show the non truncated name
  255.         #one can have separated programs as follows:
  256.         #584.0 KiB +   1.0 MiB =   1.6 MiB    mozilla-thunder (exe -> bash)
  257.         # 56.0 MiB +  22.2 MiB =  78.2 MiB    mozilla-thunderbird-bin
  258.     if sys.version_info >= (3,):
  259.         cmd = cmd.encode(errors='replace').decode()
  260.     if discriminate_by_pid:
  261.         cmd = '%s [%d]' % (cmd, pid)
  262.     return cmd
  263.  
  264.  
  265. #The following matches "du -h" output
  266. #see also human.py
  267. def human(num, power="Ki", units=None):
  268.     if units is None:
  269.         powers = ["Ki", "Mi", "Gi", "Ti"]
  270.         while num >= 1000: #4 digits
  271.             num /= 1024.0
  272.             power = powers[powers.index(power)+1]
  273.         return "%.1f %sB" % (num, power)
  274.     else:
  275.         return "%.f" % ((num * 1024) / units)
  276.  
  277.  
  278. def cmd_with_count(cmd, count):
  279.     if count > 1:
  280.         return "%s (%u)" % (cmd, count)
  281.     else:
  282.         return cmd
  283.  
  284. #Warn of possible inaccuracies
  285. #2 = accurate & can total
  286. #1 = accurate only considering each process in isolation
  287. #0 = some shared mem not reported
  288. #-1= all shared mem not reported
  289. def shared_val_accuracy():
  290.     """http://wiki.apache.org/spamassassin/TopSharedMemoryBug"""
  291.     kv = kernel_ver()
  292.     pid = os.getpid()
  293.     if kv[:2] == (2,4):
  294.         if proc.open('meminfo').read().find("Inact_") == -1:
  295.             return 1
  296.         return 0
  297.     elif kv[:2] == (2,6):
  298.         if os.path.exists(proc.path(pid, 'smaps')):
  299.             if proc.open(pid, 'smaps').read().find("Pss:")!=-1:
  300.                 return 2
  301.             else:
  302.                 return 1
  303.         if (2,6,1) <= kv <= (2,6,9):
  304.             return -1
  305.         return 0
  306.     elif kv[0] > 2 and os.path.exists(proc.path(pid, 'smaps')):
  307.         return 2
  308.     else:
  309.         return 1
  310.  
  311. def show_shared_val_accuracy( possible_inacc, only_total=False ):
  312.     level = ("Warning","Error")[only_total]
  313.     if possible_inacc == -1:
  314.         sys.stderr.write(
  315.          "%s: Shared memory is not reported by this system.\n" % level
  316.         )
  317.         sys.stderr.write(
  318.          "Values reported will be too large, and totals are not reported\n"
  319.         )
  320.     elif possible_inacc == 0:
  321.         sys.stderr.write(
  322.          "%s: Shared memory is not reported accurately by this system.\n" % level
  323.         )
  324.         sys.stderr.write(
  325.          "Values reported could be too large, and totals are not reported\n"
  326.         )
  327.     elif possible_inacc == 1:
  328.         sys.stderr.write(
  329.          "%s: Shared memory is slightly over-estimated by this system\n"
  330.          "for each program, so totals are not reported.\n" % level
  331.         )
  332.     sys.stderr.close()
  333.     if only_total and possible_inacc != 2:
  334.         sys.exit(1)
  335.  
  336.  
  337. def get_memory_usage(pids_to_show, split_args, discriminate_by_pid,
  338.                      include_self=False, only_self=False):
  339.     cmds = {}
  340.     shareds = {}
  341.     mem_ids = {}
  342.     count = {}
  343.     swaps = {}
  344.     shared_swaps = {}
  345.     for pid in os.listdir(proc.path('')):
  346.         if not pid.isdigit():
  347.             continue
  348.         pid = int(pid)
  349.  
  350.         # Some filters
  351.         if only_self and pid != our_pid:
  352.             continue
  353.         if pid == our_pid and not include_self:
  354.             continue
  355.         if pids_to_show is not None and pid not in pids_to_show:
  356.             continue
  357.  
  358.         try:
  359.             cmd = getCmdName(pid, split_args, discriminate_by_pid)
  360.         except LookupError:
  361.             #operation not permitted
  362.             #kernel threads don't have exe links or
  363.             #process gone
  364.             continue
  365.  
  366.         try:
  367.             private, shared, mem_id, swap, swap_pss = getMemStats(pid)
  368.         except RuntimeError:
  369.             continue #process gone
  370.         if shareds.get(cmd):
  371.             if have_pss: #add shared portion of PSS together
  372.                 shareds[cmd] += shared
  373.             elif shareds[cmd] < shared: #just take largest shared val
  374.                 shareds[cmd] = shared
  375.         else:
  376.             shareds[cmd] = shared
  377.         cmds[cmd] = cmds.setdefault(cmd, 0) + private
  378.         if cmd in count:
  379.             count[cmd] += 1
  380.         else:
  381.             count[cmd] = 1
  382.         mem_ids.setdefault(cmd, {}).update({mem_id: None})
  383.  
  384.         # Swap (overcounting for now...)
  385.         swaps[cmd] = swaps.setdefault(cmd, 0) + swap
  386.         if have_swap_pss:
  387.             shared_swaps[cmd] = shared_swaps.setdefault(cmd, 0) + swap_pss
  388.         else:
  389.             shared_swaps[cmd] = 0
  390.  
  391.     # Total swaped mem for each program
  392.     total_swap = 0
  393.  
  394.     # Total swaped shared mem for each program
  395.     total_shared_swap = 0
  396.  
  397.     # Add shared mem for each program
  398.     total = 0
  399.  
  400.     for cmd in cmds:
  401.         cmd_count = count[cmd]
  402.         if len(mem_ids[cmd]) == 1 and cmd_count > 1:
  403.             # Assume this program is using CLONE_VM without CLONE_THREAD
  404.             # so only account for one of the processes
  405.             cmds[cmd] /= cmd_count
  406.             if have_pss:
  407.                 shareds[cmd] /= cmd_count
  408.         cmds[cmd] = cmds[cmd] + shareds[cmd]
  409.         total += cmds[cmd]  # valid if PSS available
  410.         total_swap += swaps[cmd]
  411.         if have_swap_pss:
  412.             total_shared_swap += shared_swaps[cmd]
  413.  
  414.     sorted_cmds = sorted(cmds.items(), key=lambda x:x[1])
  415.     sorted_cmds = [x for x in sorted_cmds if x[1]]
  416.  
  417.     return sorted_cmds, shareds, count, total, swaps, shared_swaps, \
  418.         total_swap, total_shared_swap
  419.  
  420.  
  421. def print_header(show_swap, discriminate_by_pid):
  422.     output_string = " Private  +   Shared  =  RAM used"
  423.     if show_swap:
  424.         if have_swap_pss:
  425.             output_string += " " * 5 + "Shared Swap"
  426.         output_string += "   Swap used"
  427.     output_string += "\tProgram"
  428.     if discriminate_by_pid:
  429.         output_string += "[pid]"
  430.     output_string += "\n\n"
  431.     sys.stdout.write(output_string)
  432.  
  433.  
  434. def print_memory_usage(sorted_cmds, shareds, count, total, swaps, total_swap,
  435.                        shared_swaps, total_shared_swap, show_swap):
  436.     for cmd in sorted_cmds:
  437.  
  438.         output_string = "%9s + %9s = %9s"
  439.         output_data = (human(cmd[1]-shareds[cmd[0]]),
  440.                        human(shareds[cmd[0]]), human(cmd[1]))
  441.         if show_swap:
  442.             if have_swap_pss:
  443.                 output_string += "\t%9s"
  444.                 output_data += (human(shared_swaps[cmd[0]]),)
  445.             output_string += "   %9s"
  446.             output_data += (human(swaps[cmd[0]]),)
  447.         output_string += "\t%s\n"
  448.         output_data += (cmd_with_count(cmd[0], count[cmd[0]]),)
  449.  
  450.         sys.stdout.write(output_string % output_data)
  451.  
  452.     if have_pss:
  453.         if show_swap:
  454.             if have_swap_pss:
  455.                 sys.stdout.write("%s\n%s%9s%s%9s%s%9s\n%s\n" %
  456.                                  ("-" * 61, " " * 24, human(total), " " * 7,
  457.                                   human(total_shared_swap), " " * 3,
  458.                                   human(total_swap), "=" * 61))
  459.             else:
  460.                 sys.stdout.write("%s\n%s%9s%s%9s\n%s\n" %
  461.                                  ("-" * 45, " " * 24, human(total), " " * 3,
  462.                                   human(total_swap), "=" * 45))
  463.         else:
  464.             sys.stdout.write("%s\n%s%9s\n%s\n" %
  465.                              ("-" * 33, " " * 24, human(total), "=" * 33))
  466.  
  467.  
  468. def verify_environment():
  469.     if os.geteuid() != 0:
  470.         sys.stderr.write("Sorry, root permission required.\n")
  471.         sys.stderr.close()
  472.         sys.exit(1)
  473.  
  474.     try:
  475.         kernel_ver()
  476.     except (IOError, OSError):
  477.         val = sys.exc_info()[1]
  478.         if val.errno == errno.ENOENT:
  479.             sys.stderr.write(
  480.               "Couldn't access " + proc.path('') + "\n"
  481.               "Only GNU/Linux and FreeBSD (with linprocfs) are supported\n")
  482.             sys.exit(2)
  483.         else:
  484.             raise
  485.  
  486. def main():
  487.     split_args, pids_to_show, watch, only_total, discriminate_by_pid, \
  488.     show_swap = parse_options()
  489.  
  490.     verify_environment()
  491.  
  492.     if not only_total:
  493.         print_header(show_swap, discriminate_by_pid)
  494.  
  495.     if watch is not None:
  496.         try:
  497.             sorted_cmds = True
  498.             while sorted_cmds:
  499.                 sorted_cmds, shareds, count, total, swaps, shared_swaps, \
  500.                     total_swap, total_shared_swap = \
  501.                     get_memory_usage(pids_to_show, split_args,
  502.                                      discriminate_by_pid)
  503.                 if only_total and have_pss:
  504.                     sys.stdout.write(human(total, units=1)+'\n')
  505.                 elif not only_total:
  506.                     print_memory_usage(sorted_cmds, shareds, count, total,
  507.                                        swaps, total_swap, shared_swaps,
  508.                                        total_shared_swap, show_swap)
  509.  
  510.                 sys.stdout.flush()
  511.                 time.sleep(watch)
  512.             else:
  513.                 sys.stdout.write('Process does not exist anymore.\n')
  514.         except KeyboardInterrupt:
  515.             pass
  516.     else:
  517.         # This is the default behavior
  518.         sorted_cmds, shareds, count, total, swaps, shared_swaps, total_swap, \
  519.             total_shared_swap = get_memory_usage(pids_to_show, split_args,
  520.                                                  discriminate_by_pid)
  521.         if only_total and have_pss:
  522.             sys.stdout.write(human(total, units=1)+'\n')
  523.         elif not only_total:
  524.             print_memory_usage(sorted_cmds, shareds, count, total, swaps,
  525.                                total_swap, shared_swaps, total_shared_swap,
  526.                                show_swap)
  527.  
  528.     # We must close explicitly, so that any EPIPE exception
  529.     # is handled by our excepthook, rather than the default
  530.     # one which is reenabled after this script finishes.
  531.     sys.stdout.close()
  532.  
  533.     vm_accuracy = shared_val_accuracy()
  534.     show_shared_val_accuracy( vm_accuracy, only_total )
  535.  
  536. if __name__ == '__main__': main()
Advertisement
Add Comment
Please, Sign In to add comment