ram.sh 21 KB

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