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 return new_sorting_key
229 # I wonder if switching to urwid for the display would be better here
231 def prompt_str(self
, prompt
, default
=None, empty_is_cancel
=True):
232 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
233 self
.win
.addstr(1, 0, prompt
, curses
.A_BOLD
)
237 inp
= self
.win
.getstr(1, len(prompt
))
240 if inp
not in (None, ''):
246 def prompt_int(self
, prompt
, default
=None, empty_is_cancel
=True):
247 inp
= self
.prompt_str(prompt
, default
, empty_is_cancel
)
253 def prompt_pid(self
):
255 return self
.prompt_int('PID to ionice: ')
261 def prompt_tid(self
):
263 return self
.prompt_int('TID to ionice: ')
269 def prompt_data(self
, ioprio_data
):
271 if ioprio_data
is not None:
272 inp
= self
.prompt_int('I/O priority data (0-7, currently %s): '
273 % ioprio_data
, ioprio_data
, False)
275 inp
= self
.prompt_int('I/O priority data (0-7): ', None, False)
277 raise InvalidIoprioData()
278 if inp
< 0 or inp
> 7:
279 raise InvalidIoprioData()
282 def prompt_set(self
, prompt
, display_list
, ret_list
, selected
):
284 selected
= ret_list
.index(selected
)
287 set_len
= len(display_list
) - 1
289 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
290 self
.win
.insstr(1, 0, prompt
, curses
.A_BOLD
)
292 for i
, item
in enumerate(display_list
):
293 display
= ' %s ' % item
295 attr
= curses
.A_REVERSE
297 attr
= curses
.A_NORMAL
298 self
.win
.insstr(1, offset
, display
, attr
)
299 offset
+= len(display
)
301 key
= self
.win
.getch()
302 if key
in (curses
.KEY_LEFT
, ord('l')) and selected
> 0:
305 elif (key
in (curses
.KEY_RIGHT
, ord('r')) and
309 elif key
in (curses
.KEY_ENTER
, ord('\n'), ord('\r')):
310 return ret_list
[selected
]
311 elif key
in (27, curses
.KEY_CANCEL
, curses
.KEY_CLOSE
,
312 curses
.KEY_EXIT
, ord('q'), ord('Q')):
315 def prompt_class(self
, ioprio_class
=None):
316 prompt
= 'I/O priority class: '
317 classes_prompt
= ['Real-time', 'Best-effort', 'Idle']
318 classes_ret
= ['rt', 'be', 'idle']
319 if ioprio_class
is None:
321 inp
= self
.prompt_set(prompt
, classes_prompt
,
322 classes_ret
, ioprio_class
)
325 def prompt_error(self
, error
='Error!'):
326 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
327 self
.win
.insstr(1, 0, ' %s ' % error
, curses
.A_REVERSE
)
331 def prompt_clear(self
):
332 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
335 def handle_key(self
, key
):
336 def toggle_accumulated():
337 self
.options
.accumulated ^
= True
339 def toggle_only_io():
340 self
.options
.only ^
= True
342 def toggle_processes():
343 self
.options
.processes ^
= True
344 self
.process_list
.clear()
345 self
.process_list
.refresh_processes()
349 if self
.options
.processes
:
350 pid
= self
.prompt_pid()
351 exec_unit
= self
.process_list
.get_process(pid
)
353 tid
= self
.prompt_tid()
354 exec_unit
= ThreadInfo(tid
,
355 self
.process_list
.taskstats_connection
)
356 ioprio_value
= exec_unit
.get_ioprio()
357 (ioprio_class
, ioprio_data
) = \
358 ioprio
.to_class_and_data(ioprio_value
)
359 ioprio_class
= self
.prompt_class(ioprio_class
)
360 if ioprio_class
== 'idle':
363 ioprio_data
= self
.prompt_data(ioprio_data
)
364 exec_unit
.set_ioprio(ioprio_class
, ioprio_data
)
365 self
.process_list
.clear()
366 self
.process_list
.refresh_processes()
367 except IoprioSetError
as e
:
368 self
.prompt_error('Error setting I/O priority: %s' % e
.err
)
370 self
.prompt_error('Invalid process id!')
372 self
.prompt_error('Invalid thread id!')
373 except InvalidIoprioData
:
374 self
.prompt_error('Invalid I/O priority data!')
376 self
.prompt_error('Invalid integer!')
388 lambda: self
.reverse_sorting(),
390 lambda: self
.reverse_sorting(),
408 lambda: self
.adjust_sorting_key(-1),
410 lambda: self
.adjust_sorting_key(1),
412 lambda: self
.adjust_sorting_key(-len(IOTopUI
.sorting_keys
)),
414 lambda: self
.adjust_sorting_key(len(IOTopUI
.sorting_keys
))
417 action
= key_bindings
.get(key
, lambda: None)
422 stats
= format_stats(self
.options
, p
, self
.process_list
.duration
)
423 io_delay
, swapin_delay
, read_bytes
, write_bytes
= stats
424 if self
.has_swapin_io
:
425 delay_stats
= '%7s %7s ' % (swapin_delay
, io_delay
)
427 delay_stats
= ' ?unavailable? '
428 pid_format
= '%%%dd' % MAX_PID_WIDTH
429 line
= (pid_format
+ ' %4s %-8s %11s %11s %s') % (
430 p
.pid
, p
.get_ioprio(), p
.get_user()[:8], read_bytes
,
431 write_bytes
, delay_stats
)
432 cmdline
= p
.get_cmdline()
433 if not self
.options
.batch
:
434 remaining_length
= self
.width
- len(line
)
435 if 2 < remaining_length
< len(cmdline
):
436 len1
= (remaining_length
- 1) // 2
437 offset2
= -(remaining_length
- len1
- 1)
438 cmdline
= cmdline
[:len1
] + '~' + cmdline
[offset2
:]
440 if not self
.options
.batch
:
441 line
= line
[:self
.width
]
444 def should_format(p
):
445 return not self
.options
.only
or \
446 p
.did_some_io(self
.options
.accumulated
)
448 processes
= list(filter(should_format
,
449 self
.process_list
.processes
.values()))
450 key
= IOTopUI
.sorting_keys
[self
.sorting_key
][0]
451 if self
.options
.accumulated
:
452 stats_lambda
= lambda p
: p
.stats_accum
454 stats_lambda
= lambda p
: p
.stats_delta
455 processes
.sort(key
=lambda p
: key(p
, stats_lambda(p
)),
456 reverse
=self
.sorting_reverse
)
457 return list(map(format
, processes
))
459 def refresh_display(self
, first_time
, total
, current
, duration
):
461 'Total DISK READ: %s | Total DISK WRITE: %s' % (
462 format_bandwidth(self
.options
, total
[0], duration
).rjust(14),
463 format_bandwidth(self
.options
, total
[1], duration
).rjust(14)),
464 'Current DISK READ: %s | Current DISK WRITE: %s' % (
465 format_bandwidth(self
.options
, current
[0], duration
).rjust(14),
466 format_bandwidth(self
.options
, current
[1], duration
).rjust(14))
469 pid
= max(0, (MAX_PID_WIDTH
- 3)) * ' '
470 if self
.options
.processes
:
474 titles
= [pid
, ' PRIO', ' USER', ' DISK READ', ' DISK WRITE',
475 ' SWAPIN', ' IO', ' COMMAND']
476 self
.has_swapin_io
= Stats
.has_blkio_delay_total
477 if self
.has_swapin_io
:
478 # Linux kernels without the sysctl return None and
479 # iotop just uses the heuristic for those versions.
480 # Linux kernels with the sysctl return True or False
481 # and iotop then uses the sysctl value instead.
482 if sysctl_task_delayacct() == False:
483 self
.has_swapin_io
= False
484 lines
= self
.get_data()
485 if self
.options
.time
:
486 titles
= [' TIME'] + titles
487 current_time
= time
.strftime('%H:%M:%S ')
488 lines
= [current_time
+ l
for l
in lines
]
489 summary
= [current_time
+ s
for s
in summary
]
490 if self
.options
.batch
:
491 if self
.options
.quiet
<= 2:
494 if self
.options
.quiet
<= int(first_time
):
495 print(''.join(titles
))
502 if self
.has_swapin_io
:
505 status_msg
= ('CONFIG_TASK_DELAY_ACCT '
506 'and kernel.task_delayacct sysctl '
507 'not enabled in kernel, '
508 'cannot determine SWAPIN and IO %')
512 if self
.options
.help:
513 prev
= self
.get_sorting_key(-1)
514 next
= self
.get_sorting_key(1)
520 ('o', 'all' if self
.options
.only
else 'active'),
521 ('p', 'threads' if self
.options
.processes
else 'procs'),
522 ('a', 'bandwidth' if self
.options
.accumulated
else 'accum'),
524 ('r', 'asc' if self
.sorting_reverse
else 'desc'),
525 ('left', titles
[prev
].strip()),
526 ('right', titles
[next
].strip()),
527 ('home', titles
[0].strip()),
528 ('end', titles
[-1].strip()),
531 for key
, help in help.items():
533 help_item
= [' ', key
, ': ', help]
534 help_attr
= [0, 0 if key
== 'any' else curses
.A_UNDERLINE
, 0, 0]
536 help_item
= [' ', key
, ':']
537 help_attr
= [0, 0, 0]
538 if not help_lines
or not help or len(''.join(help_lines
[help_line
]) + ''.join(help_item
)) > self
.width
:
539 help_lines
.append(help_item
)
540 help_attrs
.append(help_attr
)
543 help_lines
[help_line
] += help_item
544 help_attrs
[help_line
] += help_attr
546 len_summary
= len(summary
)
547 len_titles
= int(bool(titles
))
548 len_status_msg
= int(bool(status_msg
))
549 len_help
= len(help_lines
)
550 max_lines
= self
.height
- len_summary
- len_titles
- len_status_msg
- len_help
564 max_lines
= self
.height
- len_summary
- len_titles
- len_status_msg
- len_help
565 num_lines
= min(len(lines
), max_lines
)
567 for i
, s
in enumerate(summary
):
568 self
.win
.addstr(i
, 0, s
[:self
.width
])
570 self
.win
.hline(len_summary
, 0, ord(' ') | curses
.A_REVERSE
, self
.width
)
572 remaining_cols
= self
.width
573 for i
in range(len(titles
)):
574 attr
= curses
.A_REVERSE
576 if i
== self
.sorting_key
:
578 if i
== self
.sorting_key
:
579 attr |
= curses
.A_BOLD
580 title
+= self
.sorting_reverse
and '>' or '<'
581 title
= title
[:remaining_cols
]
582 remaining_cols
-= len(title
)
584 self
.win
.addstr(len_summary
, pos
, title
, attr
)
586 for i
in range(num_lines
):
588 def print_line(line
):
589 self
.win
.addstr(i
+ len_summary
+ len_titles
, 0, line
)
592 except UnicodeEncodeError:
593 # Python2: 'ascii' codec can't encode character ...
594 # https://bugs.debian.org/708252
595 print_line(lines
[i
].encode('utf-8'))
598 for ln
in range(len_help
):
599 line
= self
.height
- len_status_msg
- len_help
+ ln
600 self
.win
.hline(line
, 0, ord(' ') | curses
.A_REVERSE
, self
.width
)
602 for i
in range(len(help_lines
[ln
])):
603 self
.win
.insstr(line
, pos
, help_lines
[ln
][i
], curses
.A_REVERSE | help_attrs
[ln
][i
])
604 pos
+= len(help_lines
[ln
][i
])
606 self
.win
.insstr(self
.height
- 1, 0, status_msg
,
611 def run_iotop_window(win
, options
):
613 signal
.signal(signal
.SIGPIPE
, signal
.SIG_DFL
)
615 def clean_exit(*args
, **kwargs
):
617 signal
.signal(signal
.SIGINT
, clean_exit
)
618 signal
.signal(signal
.SIGTERM
, clean_exit
)
619 taskstats_connection
= TaskStatsNetlink(options
)
620 process_list
= ProcessList(taskstats_connection
, options
)
621 ui
= IOTopUI(win
, process_list
, options
)
625 def run_iotop(options
):
628 return run_iotop_window(None, options
)
630 return curses
.wrapper(run_iotop_window
, options
)
631 except curses
.error
as e
:
632 print('iotop interface error:', e
, file=sys
.stderr
)
635 if e
.errno
== errno
.EPERM
:
636 print(e
, file=sys
.stderr
)
638 The Linux kernel interfaces that iotop relies on now require root privileges
639 or the NET_ADMIN capability. This change occurred because a security issue
640 (CVE-2011-2494) was found that allows leakage of sensitive data across user
641 boundaries. If you require the ability to run iotop as a non-root user, please
642 configure sudo to allow you to run iotop as root.
644 Please do not file bugs on iotop about this.''', file=sys
.stderr
)
646 if e
.errno
== errno
.ENOENT
:
647 print(e
, file=sys
.stderr
)
649 The Linux kernel interfaces that iotop relies on for process I/O statistics
650 were not found. Please enable CONFIG_TASKSTATS in your Linux kernel build
651 configuration, use iotop outside a container and or share the host's
652 network namespace with the container.
654 Please do not file bugs on iotop about this.''', file=sys
.stderr
)
663 def _profile(continuation
):
664 prof_file
= 'iotop.prof'
668 print('Profiling using cProfile')
669 cProfile
.runctx('continuation()', globals(), locals(), prof_file
)
670 stats
= pstats
.Stats(prof_file
)
674 prof
= hotshot
.Profile(prof_file
, lineevents
=1)
675 print('Profiling using hotshot')
676 prof
.runcall(continuation
)
678 stats
= hotshot
.stats
.load(prof_file
)
680 stats
.sort_stats('time', 'calls')
681 stats
.print_stats(50)
682 stats
.print_callees(50)
689 USAGE
= '''%s [OPTIONS]
691 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
692 period. SWAPIN and IO are the percentages of time the thread spent respectively
693 while swapping in and waiting on I/O more generally. PRIO is the I/O priority
694 at which the thread is running (set using the ionice command).
696 Controls: left and right arrows to change the sorting column, r to invert the
697 sorting order, o to toggle the --only option, p to toggle the --processes
698 option, a to toggle the --accumulated option, i to change I/O priority, q to
699 quit, any other key to force a refresh.''' % sys
.argv
[0]
704 locale
.setlocale(locale
.LC_ALL
, '')
706 print('unable to set locale, falling back to the default locale')
707 parser
= optparse
.OptionParser(usage
=USAGE
, version
='iotop ' + VERSION
)
708 parser
.add_option('-o', '--only', action
='store_true',
709 dest
='only', default
=False,
710 help='only show processes or threads actually doing I/O')
711 parser
.add_option('-b', '--batch', action
='store_true', dest
='batch',
712 help='non-interactive mode')
713 parser
.add_option('-n', '--iter', type='int', dest
='iterations',
715 help='number of iterations before ending [infinite]')
716 parser
.add_option('-d', '--delay', type='float', dest
='delay_seconds',
717 help='delay between iterations [1 second]',
718 metavar
='SEC', default
=1)
719 parser
.add_option('-p', '--pid', type='int', dest
='pids', action
='append',
720 help='processes/threads to monitor [all]', metavar
='PID')
721 parser
.add_option('-u', '--user', type='str', dest
='users',
722 action
='append', help='users to monitor [all]',
724 parser
.add_option('-P', '--processes', action
='store_true',
725 dest
='processes', default
=False,
726 help='only show processes, not all threads')
727 parser
.add_option('-a', '--accumulated', action
='store_true',
728 dest
='accumulated', default
=False,
729 help='show accumulated I/O instead of bandwidth')
730 parser
.add_option('-k', '--kilobytes', action
='store_true',
731 dest
='kilobytes', default
=False,
732 help='use kilobytes instead of a human friendly unit')
733 parser
.add_option('-t', '--time', action
='store_true', dest
='time',
734 help='add a timestamp on each line (implies --batch)')
735 parser
.add_option('-q', '--quiet', action
='count', dest
='quiet', default
=0,
736 help='suppress some lines of header (implies --batch)')
737 parser
.add_option('--profile', action
='store_true', dest
='profile',
738 default
=False, help=optparse
.SUPPRESS_HELP
)
739 parser
.add_option('--no-help', action
='store_false', dest
='help', default
=True,
740 help='suppress listing of shortcuts')
742 options
, args
= parser
.parse_args()
744 parser
.error('Unexpected arguments: ' + ' '.join(args
))
746 options
.pids
= options
.pids
or []
747 options
.batch
= options
.batch
or options
.time
or options
.quiet
749 main_loop
= lambda: run_iotop(options
)
752 def safe_main_loop():
757 _profile(safe_main_loop
)