Typhoon

[PYTHON] Display Memory Usage for each process

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