1 # This program is free software; you can redistribute it and/or modify
2 # it under the terms of the GNU General Public License as published by
3 # the Free Software Foundation; either version 2 of the License, or
4 # (at your option) any later version.
6 # This program is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # GNU Library General Public License for more details.
11 # You should have received a copy of the GNU General Public License
12 # along with this program; if not, write to the Free Software
13 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
15 # See the COPYING file for license information.
17 # Copyright (c) 2007 Guillaume Chazarain <guichaz@gmail.com>
19 # Allow printing with same syntax in Python 2/3
20 from __future__
import print_function
33 # Try to ensure time.monotonic() is available.
34 # This normally requires Python 3.3 or later.
35 # Use PyPI monotonic if needed and available.
36 # Fall back on non-monotonic time if needed.
38 if not hasattr(time
, 'monotonic'):
39 from monotonic
import monotonic
40 time
.monotonic
= monotonic
41 except (ImportError, RuntimeError):
42 time
.monotonic
= time
.time
44 from collections
import OrderedDict
46 from iotop
.data
import find_uids
, TaskStatsNetlink
, ProcessList
, Stats
, sysctl_task_delayacct
47 from iotop
.data
import ThreadInfo
48 from iotop
.version
import VERSION
49 from iotop
import ioprio
50 from iotop
.ioprio
import IoprioSetError
53 # Utility functions for the UI
56 UNITS
= ['B', 'K', 'M', 'G', 'T', 'P', 'E']
68 expo
= int(math
.log(size
/ 2, 2) / 10)
69 return '%s%.2f %s' % (
70 sign
, (float(size
) / (1 << (10 * expo
))), UNITS
[expo
])
73 def format_size(options
, bytes
):
75 return '%.2f K' % (bytes
/ 1024.0)
76 return human_size(bytes
)
79 def format_bandwidth(options
, size
, duration
):
80 return format_size(options
, size
and float(size
) / duration
) + '/s'
83 def format_stats(options
, process
, duration
):
84 # Keep in sync with TaskStatsNetlink.members_offsets and
85 # IOTopUI.get_data(self)
86 def delay2percent(delay
): # delay in ns, duration in s
87 return '%.2f %%' % min(99.99, delay
/ (duration
* 10000000.0))
88 if options
.accumulated
:
89 stats
= process
.stats_accum
90 display_format
= lambda size
, duration
: format_size(options
, size
)
91 duration
= time
.monotonic() - process
.stats_accum_timestamp
93 stats
= process
.stats_delta
94 display_format
= lambda size
, duration
: format_bandwidth(
95 options
, size
, duration
)
96 io_delay
= delay2percent(stats
.blkio_delay_total
)
97 swapin_delay
= delay2percent(stats
.swapin_delay_total
)
98 read_bytes
= display_format(stats
.read_bytes
, duration
)
99 written_bytes
= stats
.write_bytes
- stats
.cancelled_write_bytes
100 written_bytes
= max(0, written_bytes
)
101 write_bytes
= display_format(written_bytes
, duration
)
102 return io_delay
, swapin_delay
, read_bytes
, write_bytes
105 def get_max_pid_width():
107 return len(open('/proc/sys/kernel/pid_max').read().strip())
108 except Exception as e
:
110 # Reasonable default in case something fails
113 MAX_PID_WIDTH
= get_max_pid_width()
120 class CancelInput(Exception):
124 class InvalidInt(Exception):
128 class InvalidPid(Exception):
132 class InvalidTid(Exception):
136 class InvalidIoprioData(Exception):
144 class IOTopUI(object):
147 (lambda p
, s
: p
.pid
, False),
148 (lambda p
, s
: p
.ioprio_sort_key(), False),
149 (lambda p
, s
: p
.get_user(), False),
150 (lambda p
, s
: s
.read_bytes
, True),
151 (lambda p
, s
: s
.write_bytes
- s
.cancelled_write_bytes
, True),
152 (lambda p
, s
: s
.swapin_delay_total
, True),
153 # The default sorting (by I/O % time) should show processes doing
154 # only writes, without waiting on them
155 (lambda p
, s
: s
.blkio_delay_total
or
156 int(not(not(s
.read_bytes
or s
.write_bytes
))), True),
157 (lambda p
, s
: p
.get_cmdline(), False),
160 def __init__(self
, win
, process_list
, options
):
161 self
.process_list
= process_list
162 self
.options
= options
164 self
.sorting_reverse
= IOTopUI
.sorting_keys
[self
.sorting_key
][1]
165 if not self
.options
.batch
:
169 curses
.use_default_colors()
173 # This call can fail with misconfigured terminals, for example
174 # TERM=xterm-color. This is harmless
178 self
.height
, self
.width
= self
.win
.getmaxyx()
183 if not self
.options
.batch
:
184 poll
.register(sys
.stdin
.fileno(), select
.POLLIN | select
.POLLPRI
)
185 while self
.options
.iterations
is None or \
186 iterations
< self
.options
.iterations
:
187 total
, current
= self
.process_list
.refresh_processes()
188 self
.refresh_display(iterations
== 0, total
, current
,
189 self
.process_list
.duration
)
190 if self
.options
.iterations
is not None:
192 if iterations
>= self
.options
.iterations
:
194 elif iterations
== 0:
198 events
= poll
.poll(self
.options
.delay_seconds
* 1000.0)
199 except select
.error
as e
:
200 if e
.args
and e
.args
[0] == errno
.EINTR
:
204 for (fd
, event
) in events
:
205 if event
& (select
.POLLERR | select
.POLLHUP
):
207 if not self
.options
.batch
:
210 key
= self
.win
.getch()
213 def reverse_sorting(self
):
214 self
.sorting_reverse
= not self
.sorting_reverse
216 def adjust_sorting_key(self
, delta
):
217 orig_sorting_key
= self
.sorting_key
218 self
.sorting_key
= self
.get_sorting_key(delta
)
219 if orig_sorting_key
!= self
.sorting_key
:
220 self
.sorting_reverse
= IOTopUI
.sorting_keys
[self
.sorting_key
][1]
222 def get_sorting_key(self
, delta
):
223 new_sorting_key
= self
.sorting_key
224 new_sorting_key
+= delta
225 new_sorting_key
= max(0, new_sorting_key
)
226 new_sorting_key
= min(len(IOTopUI
.sorting_keys
) - 1, new_sorting_key
)
227 if not self
.has_swapin_io
:
228 if new_sorting_key
in (5, 6):
233 return new_sorting_key
235 # I wonder if switching to urwid for the display would be better here
237 def prompt_str(self
, prompt
, default
=None, empty_is_cancel
=True):
238 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
239 self
.win
.addstr(1, 0, prompt
, curses
.A_BOLD
)
243 inp
= self
.win
.getstr(1, len(prompt
))
246 if inp
not in (None, ''):
252 def prompt_int(self
, prompt
, default
=None, empty_is_cancel
=True):
253 inp
= self
.prompt_str(prompt
, default
, empty_is_cancel
)
259 def prompt_pid(self
):
261 return self
.prompt_int('PID to ionice: ')
267 def prompt_tid(self
):
269 return self
.prompt_int('TID to ionice: ')
275 def prompt_data(self
, ioprio_data
):
277 if ioprio_data
is not None:
278 inp
= self
.prompt_int('I/O priority data (0-7, currently %s): '
279 % ioprio_data
, ioprio_data
, False)
281 inp
= self
.prompt_int('I/O priority data (0-7): ', None, False)
283 raise InvalidIoprioData()
284 if inp
< 0 or inp
> 7:
285 raise InvalidIoprioData()
288 def prompt_set(self
, prompt
, display_list
, ret_list
, selected
):
290 selected
= ret_list
.index(selected
)
293 set_len
= len(display_list
) - 1
295 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
296 self
.win
.insstr(1, 0, prompt
, curses
.A_BOLD
)
298 for i
, item
in enumerate(display_list
):
299 display
= ' %s ' % item
301 attr
= curses
.A_REVERSE
303 attr
= curses
.A_NORMAL
304 self
.win
.insstr(1, offset
, display
, attr
)
305 offset
+= len(display
)
307 key
= self
.win
.getch()
308 if key
in (curses
.KEY_LEFT
, ord('l')) and selected
> 0:
311 elif (key
in (curses
.KEY_RIGHT
, ord('r')) and
315 elif key
in (curses
.KEY_ENTER
, ord('\n'), ord('\r')):
316 return ret_list
[selected
]
317 elif key
in (27, curses
.KEY_CANCEL
, curses
.KEY_CLOSE
,
318 curses
.KEY_EXIT
, ord('q'), ord('Q')):
321 def prompt_class(self
, ioprio_class
=None):
322 prompt
= 'I/O priority class: '
323 classes_prompt
= ['Real-time', 'Best-effort', 'Idle']
324 classes_ret
= ['rt', 'be', 'idle']
325 if ioprio_class
is None:
327 inp
= self
.prompt_set(prompt
, classes_prompt
,
328 classes_ret
, ioprio_class
)
331 def prompt_error(self
, error
='Error!'):
332 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
333 self
.win
.insstr(1, 0, ' %s ' % error
, curses
.A_REVERSE
)
337 def prompt_clear(self
):
338 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
341 def handle_key(self
, key
):
342 def toggle_accumulated():
343 self
.options
.accumulated ^
= True
345 def toggle_only_io():
346 self
.options
.only ^
= True
348 def toggle_processes():
349 self
.options
.processes ^
= True
350 self
.process_list
.clear()
351 self
.process_list
.refresh_processes()
355 if self
.options
.processes
:
356 pid
= self
.prompt_pid()
357 exec_unit
= self
.process_list
.get_process(pid
)
359 tid
= self
.prompt_tid()
360 exec_unit
= ThreadInfo(tid
,
361 self
.process_list
.taskstats_connection
)
362 ioprio_value
= exec_unit
.get_ioprio()
363 (ioprio_class
, ioprio_data
) = \
364 ioprio
.to_class_and_data(ioprio_value
)
365 ioprio_class
= self
.prompt_class(ioprio_class
)
366 if ioprio_class
== 'idle':
369 ioprio_data
= self
.prompt_data(ioprio_data
)
370 exec_unit
.set_ioprio(ioprio_class
, ioprio_data
)
371 self
.process_list
.clear()
372 self
.process_list
.refresh_processes()
373 except IoprioSetError
as e
:
374 self
.prompt_error('Error setting I/O priority: %s' % e
.err
)
376 self
.prompt_error('Invalid process id!')
378 self
.prompt_error('Invalid thread id!')
379 except InvalidIoprioData
:
380 self
.prompt_error('Invalid I/O priority data!')
382 self
.prompt_error('Invalid integer!')
394 lambda: self
.reverse_sorting(),
396 lambda: self
.reverse_sorting(),
414 lambda: self
.adjust_sorting_key(-1),
416 lambda: self
.adjust_sorting_key(1),
418 lambda: self
.adjust_sorting_key(-len(IOTopUI
.sorting_keys
)),
420 lambda: self
.adjust_sorting_key(len(IOTopUI
.sorting_keys
))
423 action
= key_bindings
.get(key
, lambda: None)
428 stats
= format_stats(self
.options
, p
, self
.process_list
.duration
)
429 io_delay
, swapin_delay
, read_bytes
, write_bytes
= stats
430 format
= '%%%dd' % MAX_PID_WIDTH
433 params
+= p
.get_ioprio(),
435 params
+= p
.get_user()[:8],
436 format
+= ' %11s %11s'
437 params
+= read_bytes
, write_bytes
438 if self
.has_swapin_io
:
440 params
+= swapin_delay
, io_delay
441 elif self
.options
.batch
:
443 params
+= '?unavailable?',
445 line
= format
% (params
)
446 cmdline
= p
.get_cmdline()
447 if not self
.options
.batch
:
448 remaining_length
= self
.width
- len(line
)
449 if 2 < remaining_length
< len(cmdline
):
450 len1
= (remaining_length
- 1) // 2
451 offset2
= -(remaining_length
- len1
- 1)
452 cmdline
= cmdline
[:len1
] + '~' + cmdline
[offset2
:]
454 if not self
.options
.batch
:
455 line
= line
[:self
.width
]
458 def should_format(p
):
459 return not self
.options
.only
or \
460 p
.did_some_io(self
.options
.accumulated
)
462 processes
= list(filter(should_format
,
463 self
.process_list
.processes
.values()))
464 key
= IOTopUI
.sorting_keys
[self
.sorting_key
][0]
465 if self
.options
.accumulated
:
466 stats_lambda
= lambda p
: p
.stats_accum
468 stats_lambda
= lambda p
: p
.stats_delta
469 processes
.sort(key
=lambda p
: key(p
, stats_lambda(p
)),
470 reverse
=self
.sorting_reverse
)
471 return list(map(format
, processes
))
473 def refresh_display(self
, first_time
, total
, current
, duration
):
475 'Total DISK READ: %s | Total DISK WRITE: %s' % (
476 format_bandwidth(self
.options
, total
[0], duration
).rjust(14),
477 format_bandwidth(self
.options
, total
[1], duration
).rjust(14)),
478 'Current DISK READ: %s | Current DISK WRITE: %s' % (
479 format_bandwidth(self
.options
, current
[0], duration
).rjust(14),
480 format_bandwidth(self
.options
, current
[1], duration
).rjust(14))
483 pid
= max(0, (MAX_PID_WIDTH
- 3)) * ' '
484 if self
.options
.processes
:
488 titles
= [pid
, ' PRIO', ' USER', ' DISK READ', ' DISK WRITE',
489 ' SWAPIN', ' IO', ' COMMAND']
490 self
.has_swapin_io
= Stats
.has_blkio_delay_total
491 if self
.has_swapin_io
:
492 # Linux kernels without the sysctl return None and
493 # iotop just uses the heuristic for those versions.
494 # Linux kernels with the sysctl return True or False
495 # and iotop then uses the sysctl value instead.
496 if sysctl_task_delayacct() == False:
497 self
.has_swapin_io
= False
498 self
.adjust_sorting_key(0)
499 lines
= self
.get_data()
500 if self
.options
.time
:
501 titles
= [' TIME'] + titles
502 current_time
= time
.strftime('%H:%M:%S ')
503 lines
= [current_time
+ l
for l
in lines
]
504 summary
= [current_time
+ s
for s
in summary
]
505 if self
.options
.batch
:
506 if self
.options
.quiet
<= 2:
509 if self
.options
.quiet
<= int(first_time
):
510 print(''.join(titles
))
517 if self
.has_swapin_io
:
520 status_msg
= ('CONFIG_TASK_DELAY_ACCT '
521 'and kernel.task_delayacct sysctl '
522 'not enabled in kernel, '
523 'cannot determine SWAPIN and IO %')
527 if self
.options
.help:
528 prev
= self
.get_sorting_key(-1)
529 next
= self
.get_sorting_key(1)
535 ('o', 'all' if self
.options
.only
else 'active'),
536 ('p', 'threads' if self
.options
.processes
else 'procs'),
537 ('a', 'bandwidth' if self
.options
.accumulated
else 'accum'),
539 ('r', 'asc' if self
.sorting_reverse
else 'desc'),
540 ('left', titles
[prev
].strip()),
541 ('right', titles
[next
].strip()),
542 ('home', titles
[0].strip()),
543 ('end', titles
[-1].strip()),
546 for key
, help in help.items():
548 help_item
= [' ', key
, ': ', help]
549 help_attr
= [0, 0 if key
== 'any' else curses
.A_UNDERLINE
, 0, 0]
551 help_item
= [' ', key
, ':']
552 help_attr
= [0, 0, 0]
553 if not help_lines
or not help or len(''.join(help_lines
[help_line
]) + ''.join(help_item
)) > self
.width
:
554 help_lines
.append(help_item
)
555 help_attrs
.append(help_attr
)
558 help_lines
[help_line
] += help_item
559 help_attrs
[help_line
] += help_attr
561 len_summary
= len(summary
)
562 len_titles
= int(bool(titles
))
563 len_status_msg
= int(bool(status_msg
))
564 len_help
= len(help_lines
)
565 max_lines
= self
.height
- len_summary
- len_titles
- len_status_msg
- len_help
579 max_lines
= self
.height
- len_summary
- len_titles
- len_status_msg
- len_help
580 num_lines
= min(len(lines
), max_lines
)
582 for i
, s
in enumerate(summary
):
583 self
.win
.addstr(i
, 0, s
[:self
.width
])
585 self
.win
.hline(len_summary
, 0, ord(' ') | curses
.A_REVERSE
, self
.width
)
587 remaining_cols
= self
.width
588 for i
in range(len(titles
)):
589 if not self
.has_swapin_io
and i
in (5, 6):
591 attr
= curses
.A_REVERSE
593 if i
== self
.sorting_key
:
595 if i
== self
.sorting_key
:
596 attr |
= curses
.A_BOLD
597 title
+= self
.sorting_reverse
and '>' or '<'
598 title
= title
[:remaining_cols
]
599 remaining_cols
-= len(title
)
601 self
.win
.addstr(len_summary
, pos
, title
, attr
)
603 for i
in range(num_lines
):
605 def print_line(line
):
606 self
.win
.addstr(i
+ len_summary
+ len_titles
, 0, line
)
609 except UnicodeEncodeError:
610 # Python2: 'ascii' codec can't encode character ...
611 # https://bugs.debian.org/708252
612 print_line(lines
[i
].encode('utf-8'))
615 for ln
in range(len_help
):
616 line
= self
.height
- len_status_msg
- len_help
+ ln
617 self
.win
.hline(line
, 0, ord(' ') | curses
.A_REVERSE
, self
.width
)
619 for i
in range(len(help_lines
[ln
])):
620 self
.win
.insstr(line
, pos
, help_lines
[ln
][i
], curses
.A_REVERSE | help_attrs
[ln
][i
])
621 pos
+= len(help_lines
[ln
][i
])
623 self
.win
.insstr(self
.height
- 1, 0, status_msg
,
628 def run_iotop_window(win
, options
):
630 signal
.signal(signal
.SIGPIPE
, signal
.SIG_DFL
)
632 def clean_exit(*args
, **kwargs
):
634 signal
.signal(signal
.SIGINT
, clean_exit
)
635 signal
.signal(signal
.SIGTERM
, clean_exit
)
636 taskstats_connection
= TaskStatsNetlink(options
)
637 process_list
= ProcessList(taskstats_connection
, options
)
638 ui
= IOTopUI(win
, process_list
, options
)
642 def run_iotop(options
):
645 return run_iotop_window(None, options
)
647 return curses
.wrapper(run_iotop_window
, options
)
648 except curses
.error
as e
:
649 print('iotop interface error:', e
, file=sys
.stderr
)
652 if e
.errno
== errno
.EPERM
:
653 print(e
, file=sys
.stderr
)
655 The Linux kernel interfaces that iotop relies on now require root privileges
656 or the NET_ADMIN capability. This change occurred because a security issue
657 (CVE-2011-2494) was found that allows leakage of sensitive data across user
658 boundaries. If you require the ability to run iotop as a non-root user, please
659 configure sudo to allow you to run iotop as root.
661 Please do not file bugs on iotop about this.''', file=sys
.stderr
)
663 if e
.errno
== errno
.ENOENT
:
664 print(e
, file=sys
.stderr
)
666 The Linux kernel interfaces that iotop relies on for process I/O statistics
667 were not found. Please enable CONFIG_TASKSTATS in your Linux kernel build
668 configuration, use iotop outside a container and or share the host's
669 network namespace with the container.
671 Please do not file bugs on iotop about this.''', file=sys
.stderr
)
680 def _profile(continuation
):
681 prof_file
= 'iotop.prof'
685 print('Profiling using cProfile')
686 cProfile
.runctx('continuation()', globals(), locals(), prof_file
)
687 stats
= pstats
.Stats(prof_file
)
691 prof
= hotshot
.Profile(prof_file
, lineevents
=1)
692 print('Profiling using hotshot')
693 prof
.runcall(continuation
)
695 stats
= hotshot
.stats
.load(prof_file
)
697 stats
.sort_stats('time', 'calls')
698 stats
.print_stats(50)
699 stats
.print_callees(50)
706 USAGE
= '''%s [OPTIONS]
708 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
709 period. SWAPIN and IO are the percentages of time the thread spent respectively
710 while swapping in and waiting on I/O more generally. PRIO is the I/O priority
711 at which the thread is running (set using the ionice command).
713 Controls: left and right arrows to change the sorting column, r to invert the
714 sorting order, o to toggle the --only option, p to toggle the --processes
715 option, a to toggle the --accumulated option, i to change I/O priority, q to
716 quit, any other key to force a refresh.''' % sys
.argv
[0]
721 locale
.setlocale(locale
.LC_ALL
, '')
723 print('unable to set locale, falling back to the default locale')
724 parser
= optparse
.OptionParser(usage
=USAGE
, version
='iotop ' + VERSION
)
725 parser
.add_option('-o', '--only', action
='store_true',
726 dest
='only', default
=False,
727 help='only show processes or threads actually doing I/O')
728 parser
.add_option('-b', '--batch', action
='store_true', dest
='batch',
729 help='non-interactive mode')
730 parser
.add_option('-n', '--iter', type='int', dest
='iterations',
732 help='number of iterations before ending [infinite]')
733 parser
.add_option('-d', '--delay', type='float', dest
='delay_seconds',
734 help='delay between iterations [1 second]',
735 metavar
='SEC', default
=1)
736 parser
.add_option('-p', '--pid', type='int', dest
='pids', action
='append',
737 help='processes/threads to monitor [all]', metavar
='PID')
738 parser
.add_option('-u', '--user', type='str', dest
='users',
739 action
='append', help='users to monitor [all]',
741 parser
.add_option('-P', '--processes', action
='store_true',
742 dest
='processes', default
=False,
743 help='only show processes, not all threads')
744 parser
.add_option('-a', '--accumulated', action
='store_true',
745 dest
='accumulated', default
=False,
746 help='show accumulated I/O instead of bandwidth')
747 parser
.add_option('-k', '--kilobytes', action
='store_true',
748 dest
='kilobytes', default
=False,
749 help='use kilobytes instead of a human friendly unit')
750 parser
.add_option('-t', '--time', action
='store_true', dest
='time',
751 help='add a timestamp on each line (implies --batch)')
752 parser
.add_option('-q', '--quiet', action
='count', dest
='quiet', default
=0,
753 help='suppress some lines of header (implies --batch)')
754 parser
.add_option('--profile', action
='store_true', dest
='profile',
755 default
=False, help=optparse
.SUPPRESS_HELP
)
756 parser
.add_option('--no-help', action
='store_false', dest
='help', default
=True,
757 help='suppress listing of shortcuts')
759 options
, args
= parser
.parse_args()
761 parser
.error('Unexpected arguments: ' + ' '.join(args
))
763 options
.pids
= options
.pids
or []
764 options
.batch
= options
.batch
or options
.time
or options
.quiet
766 main_loop
= lambda: run_iotop(options
)
769 def safe_main_loop():
774 _profile(safe_main_loop
)