ps_mem.py 21 KB

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