Advertisement
Typhoon

[PYTHON] Display Memory Usage for each process

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