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
32 from collections
import OrderedDict
34 from iotop
.data
import find_uids
, TaskStatsNetlink
, ProcessList
, Stats
35 from iotop
.data
import ThreadInfo
36 from iotop
.version
import VERSION
37 from iotop
import ioprio
38 from iotop
.ioprio
import IoprioSetError
41 # Utility functions for the UI
44 UNITS
= ['B', 'K', 'M', 'G', 'T', 'P', 'E']
56 expo
= int(math
.log(size
/ 2, 2) / 10)
57 return '%s%.2f %s' % (
58 sign
, (float(size
) / (1 << (10 * expo
))), UNITS
[expo
])
61 def format_size(options
, bytes
):
63 return '%.2f K' % (bytes
/ 1024.0)
64 return human_size(bytes
)
67 def format_bandwidth(options
, size
, duration
):
68 return format_size(options
, size
and float(size
) / duration
) + '/s'
71 def format_stats(options
, process
, duration
):
72 # Keep in sync with TaskStatsNetlink.members_offsets and
73 # IOTopUI.get_data(self)
74 def delay2percent(delay
): # delay in ns, duration in s
75 return '%.2f %%' % min(99.99, delay
/ (duration
* 10000000.0))
76 if options
.accumulated
:
77 stats
= process
.stats_accum
78 display_format
= lambda size
, duration
: format_size(options
, size
)
79 duration
= time
.monotonic() - process
.stats_accum_timestamp
81 stats
= process
.stats_delta
82 display_format
= lambda size
, duration
: format_bandwidth(
83 options
, size
, duration
)
84 io_delay
= delay2percent(stats
.blkio_delay_total
)
85 swapin_delay
= delay2percent(stats
.swapin_delay_total
)
86 read_bytes
= display_format(stats
.read_bytes
, duration
)
87 written_bytes
= stats
.write_bytes
- stats
.cancelled_write_bytes
88 written_bytes
= max(0, written_bytes
)
89 write_bytes
= display_format(written_bytes
, duration
)
90 return io_delay
, swapin_delay
, read_bytes
, write_bytes
93 def get_max_pid_width():
95 return len(open('/proc/sys/kernel/pid_max').read().strip())
96 except Exception as e
:
98 # Reasonable default in case something fails
101 MAX_PID_WIDTH
= get_max_pid_width()
108 class CancelInput(Exception):
112 class InvalidInt(Exception):
116 class InvalidPid(Exception):
120 class InvalidTid(Exception):
124 class InvalidIoprioData(Exception):
132 class IOTopUI(object):
135 (lambda p
, s
: p
.pid
, False),
136 (lambda p
, s
: p
.ioprio_sort_key(), False),
137 (lambda p
, s
: p
.get_user(), False),
138 (lambda p
, s
: s
.read_bytes
, True),
139 (lambda p
, s
: s
.write_bytes
- s
.cancelled_write_bytes
, True),
140 (lambda p
, s
: s
.swapin_delay_total
, True),
141 # The default sorting (by I/O % time) should show processes doing
142 # only writes, without waiting on them
143 (lambda p
, s
: s
.blkio_delay_total
or
144 int(not(not(s
.read_bytes
or s
.write_bytes
))), True),
145 (lambda p
, s
: p
.get_cmdline(), False),
148 def __init__(self
, win
, process_list
, options
):
149 self
.process_list
= process_list
150 self
.options
= options
152 self
.sorting_reverse
= IOTopUI
.sorting_keys
[self
.sorting_key
][1]
153 if not self
.options
.batch
:
157 curses
.use_default_colors()
161 # This call can fail with misconfigured terminals, for example
162 # TERM=xterm-color. This is harmless
166 self
.height
, self
.width
= self
.win
.getmaxyx()
171 if not self
.options
.batch
:
172 poll
.register(sys
.stdin
.fileno(), select
.POLLIN | select
.POLLPRI
)
173 while self
.options
.iterations
is None or \
174 iterations
< self
.options
.iterations
:
175 total
, current
= self
.process_list
.refresh_processes()
176 self
.refresh_display(iterations
== 0, total
, current
,
177 self
.process_list
.duration
)
178 if self
.options
.iterations
is not None:
180 if iterations
>= self
.options
.iterations
:
182 elif iterations
== 0:
186 events
= poll
.poll(self
.options
.delay_seconds
* 1000.0)
187 except select
.error
as e
:
188 if e
.args
and e
.args
[0] == errno
.EINTR
:
192 for (fd
, event
) in events
:
193 if event
& (select
.POLLERR | select
.POLLHUP
):
195 if not self
.options
.batch
:
198 key
= self
.win
.getch()
201 def reverse_sorting(self
):
202 self
.sorting_reverse
= not self
.sorting_reverse
204 def adjust_sorting_key(self
, delta
):
205 orig_sorting_key
= self
.sorting_key
206 self
.sorting_key
= self
.get_sorting_key(delta
)
207 if orig_sorting_key
!= self
.sorting_key
:
208 self
.sorting_reverse
= IOTopUI
.sorting_keys
[self
.sorting_key
][1]
210 def get_sorting_key(self
, delta
):
211 new_sorting_key
= self
.sorting_key
212 new_sorting_key
+= delta
213 new_sorting_key
= max(0, new_sorting_key
)
214 new_sorting_key
= min(len(IOTopUI
.sorting_keys
) - 1, new_sorting_key
)
215 return new_sorting_key
217 # I wonder if switching to urwid for the display would be better here
219 def prompt_str(self
, prompt
, default
=None, empty_is_cancel
=True):
220 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
221 self
.win
.addstr(1, 0, prompt
, curses
.A_BOLD
)
225 inp
= self
.win
.getstr(1, len(prompt
))
228 if inp
not in (None, ''):
234 def prompt_int(self
, prompt
, default
=None, empty_is_cancel
=True):
235 inp
= self
.prompt_str(prompt
, default
, empty_is_cancel
)
241 def prompt_pid(self
):
243 return self
.prompt_int('PID to ionice: ')
249 def prompt_tid(self
):
251 return self
.prompt_int('TID to ionice: ')
257 def prompt_data(self
, ioprio_data
):
259 if ioprio_data
is not None:
260 inp
= self
.prompt_int('I/O priority data (0-7, currently %s): '
261 % ioprio_data
, ioprio_data
, False)
263 inp
= self
.prompt_int('I/O priority data (0-7): ', None, False)
265 raise InvalidIoprioData()
266 if inp
< 0 or inp
> 7:
267 raise InvalidIoprioData()
270 def prompt_set(self
, prompt
, display_list
, ret_list
, selected
):
272 selected
= ret_list
.index(selected
)
275 set_len
= len(display_list
) - 1
277 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
278 self
.win
.insstr(1, 0, prompt
, curses
.A_BOLD
)
280 for i
, item
in enumerate(display_list
):
281 display
= ' %s ' % item
283 attr
= curses
.A_REVERSE
285 attr
= curses
.A_NORMAL
286 self
.win
.insstr(1, offset
, display
, attr
)
287 offset
+= len(display
)
289 key
= self
.win
.getch()
290 if key
in (curses
.KEY_LEFT
, ord('l')) and selected
> 0:
293 elif (key
in (curses
.KEY_RIGHT
, ord('r')) and
297 elif key
in (curses
.KEY_ENTER
, ord('\n'), ord('\r')):
298 return ret_list
[selected
]
299 elif key
in (27, curses
.KEY_CANCEL
, curses
.KEY_CLOSE
,
300 curses
.KEY_EXIT
, ord('q'), ord('Q')):
303 def prompt_class(self
, ioprio_class
=None):
304 prompt
= 'I/O priority class: '
305 classes_prompt
= ['Real-time', 'Best-effort', 'Idle']
306 classes_ret
= ['rt', 'be', 'idle']
307 if ioprio_class
is None:
309 inp
= self
.prompt_set(prompt
, classes_prompt
,
310 classes_ret
, ioprio_class
)
313 def prompt_error(self
, error
='Error!'):
314 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
315 self
.win
.insstr(1, 0, ' %s ' % error
, curses
.A_REVERSE
)
319 def prompt_clear(self
):
320 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
323 def handle_key(self
, key
):
324 def toggle_accumulated():
325 self
.options
.accumulated ^
= True
327 def toggle_only_io():
328 self
.options
.only ^
= True
330 def toggle_processes():
331 self
.options
.processes ^
= True
332 self
.process_list
.clear()
333 self
.process_list
.refresh_processes()
337 if self
.options
.processes
:
338 pid
= self
.prompt_pid()
339 exec_unit
= self
.process_list
.get_process(pid
)
341 tid
= self
.prompt_tid()
342 exec_unit
= ThreadInfo(tid
,
343 self
.process_list
.taskstats_connection
)
344 ioprio_value
= exec_unit
.get_ioprio()
345 (ioprio_class
, ioprio_data
) = \
346 ioprio
.to_class_and_data(ioprio_value
)
347 ioprio_class
= self
.prompt_class(ioprio_class
)
348 if ioprio_class
== 'idle':
351 ioprio_data
= self
.prompt_data(ioprio_data
)
352 exec_unit
.set_ioprio(ioprio_class
, ioprio_data
)
353 self
.process_list
.clear()
354 self
.process_list
.refresh_processes()
355 except IoprioSetError
as e
:
356 self
.prompt_error('Error setting I/O priority: %s' % e
.err
)
358 self
.prompt_error('Invalid process id!')
360 self
.prompt_error('Invalid thread id!')
361 except InvalidIoprioData
:
362 self
.prompt_error('Invalid I/O priority data!')
364 self
.prompt_error('Invalid integer!')
376 lambda: self
.reverse_sorting(),
378 lambda: self
.reverse_sorting(),
396 lambda: self
.adjust_sorting_key(-1),
398 lambda: self
.adjust_sorting_key(1),
400 lambda: self
.adjust_sorting_key(-len(IOTopUI
.sorting_keys
)),
402 lambda: self
.adjust_sorting_key(len(IOTopUI
.sorting_keys
))
405 action
= key_bindings
.get(key
, lambda: None)
410 stats
= format_stats(self
.options
, p
, self
.process_list
.duration
)
411 io_delay
, swapin_delay
, read_bytes
, write_bytes
= stats
412 if Stats
.has_blkio_delay_total
:
413 delay_stats
= '%7s %7s ' % (swapin_delay
, io_delay
)
415 delay_stats
= ' ?unavailable? '
416 pid_format
= '%%%dd' % MAX_PID_WIDTH
417 line
= (pid_format
+ ' %4s %-8s %11s %11s %s') % (
418 p
.pid
, p
.get_ioprio(), p
.get_user()[:8], read_bytes
,
419 write_bytes
, delay_stats
)
420 cmdline
= p
.get_cmdline()
421 if not self
.options
.batch
:
422 remaining_length
= self
.width
- len(line
)
423 if 2 < remaining_length
< len(cmdline
):
424 len1
= (remaining_length
- 1) // 2
425 offset2
= -(remaining_length
- len1
- 1)
426 cmdline
= cmdline
[:len1
] + '~' + cmdline
[offset2
:]
428 if not self
.options
.batch
:
429 line
= line
[:self
.width
]
432 def should_format(p
):
433 return not self
.options
.only
or \
434 p
.did_some_io(self
.options
.accumulated
)
436 processes
= list(filter(should_format
,
437 self
.process_list
.processes
.values()))
438 key
= IOTopUI
.sorting_keys
[self
.sorting_key
][0]
439 if self
.options
.accumulated
:
440 stats_lambda
= lambda p
: p
.stats_accum
442 stats_lambda
= lambda p
: p
.stats_delta
443 processes
.sort(key
=lambda p
: key(p
, stats_lambda(p
)),
444 reverse
=self
.sorting_reverse
)
445 return list(map(format
, processes
))
447 def refresh_display(self
, first_time
, total
, current
, duration
):
449 'Total DISK READ: %s | Total DISK WRITE: %s' % (
450 format_bandwidth(self
.options
, total
[0], duration
).rjust(14),
451 format_bandwidth(self
.options
, total
[1], duration
).rjust(14)),
452 'Current DISK READ: %s | Current DISK WRITE: %s' % (
453 format_bandwidth(self
.options
, current
[0], duration
).rjust(14),
454 format_bandwidth(self
.options
, current
[1], duration
).rjust(14))
457 pid
= max(0, (MAX_PID_WIDTH
- 3)) * ' '
458 if self
.options
.processes
:
462 titles
= [pid
, ' PRIO', ' USER', ' DISK READ', ' DISK WRITE',
463 ' SWAPIN', ' IO', ' COMMAND']
464 lines
= self
.get_data()
465 if self
.options
.time
:
466 titles
= [' TIME'] + titles
467 current_time
= time
.strftime('%H:%M:%S ')
468 lines
= [current_time
+ l
for l
in lines
]
469 summary
= [current_time
+ s
for s
in summary
]
470 if self
.options
.batch
:
471 if self
.options
.quiet
<= 2:
474 if self
.options
.quiet
<= int(first_time
):
475 print(''.join(titles
))
482 if Stats
.has_blkio_delay_total
:
485 status_msg
= ('CONFIG_TASK_DELAY_ACCT not enabled in kernel, '
486 'cannot determine SWAPIN and IO %')
490 if self
.options
.help:
491 prev
= self
.get_sorting_key(-1)
492 next
= self
.get_sorting_key(1)
498 ('o', 'all' if self
.options
.only
else 'active'),
499 ('p', 'threads' if self
.options
.processes
else 'procs'),
500 ('a', 'bandwidth' if self
.options
.accumulated
else 'accum'),
502 ('r', 'asc' if self
.sorting_reverse
else 'desc'),
503 ('left', titles
[prev
].strip()),
504 ('right', titles
[next
].strip()),
505 ('home', titles
[0].strip()),
506 ('end', titles
[-1].strip()),
509 for key
, help in help.items():
511 help_item
= [' ', key
, ': ', help]
512 help_attr
= [0, 0 if key
== 'any' else curses
.A_UNDERLINE
, 0, 0]
514 help_item
= [' ', key
, ':']
515 help_attr
= [0, 0, 0]
516 if not help_lines
or not help or len(''.join(help_lines
[help_line
]) + ''.join(help_item
)) > self
.width
:
517 help_lines
.append(help_item
)
518 help_attrs
.append(help_attr
)
521 help_lines
[help_line
] += help_item
522 help_attrs
[help_line
] += help_attr
524 len_summary
= len(summary
)
525 len_titles
= int(bool(titles
))
526 len_status_msg
= int(bool(status_msg
))
527 len_help
= len(help_lines
)
528 max_lines
= self
.height
- len_summary
- len_titles
- len_status_msg
- len_help
542 max_lines
= self
.height
- len_summary
- len_titles
- len_status_msg
- len_help
543 num_lines
= min(len(lines
), max_lines
)
545 for i
, s
in enumerate(summary
):
546 self
.win
.addstr(i
, 0, s
[:self
.width
])
548 self
.win
.hline(len_summary
, 0, ord(' ') | curses
.A_REVERSE
, self
.width
)
550 remaining_cols
= self
.width
551 for i
in range(len(titles
)):
552 attr
= curses
.A_REVERSE
554 if i
== self
.sorting_key
:
556 if i
== self
.sorting_key
:
557 attr |
= curses
.A_BOLD
558 title
+= self
.sorting_reverse
and '>' or '<'
559 title
= title
[:remaining_cols
]
560 remaining_cols
-= len(title
)
562 self
.win
.addstr(len_summary
, pos
, title
, attr
)
564 for i
in range(num_lines
):
566 def print_line(line
):
567 self
.win
.addstr(i
+ len_summary
+ len_titles
, 0, line
)
570 except UnicodeEncodeError:
571 # Python2: 'ascii' codec can't encode character ...
572 # https://bugs.debian.org/708252
573 print_line(lines
[i
].encode('utf-8'))
576 for ln
in range(len_help
):
577 line
= self
.height
- len_status_msg
- len_help
+ ln
578 self
.win
.hline(line
, 0, ord(' ') | curses
.A_REVERSE
, self
.width
)
580 for i
in range(len(help_lines
[ln
])):
581 self
.win
.insstr(line
, pos
, help_lines
[ln
][i
], curses
.A_REVERSE | help_attrs
[ln
][i
])
582 pos
+= len(help_lines
[ln
][i
])
584 self
.win
.insstr(self
.height
- 1, 0, status_msg
,
589 def run_iotop_window(win
, options
):
591 signal
.signal(signal
.SIGPIPE
, signal
.SIG_DFL
)
593 def clean_exit(*args
, **kwargs
):
595 signal
.signal(signal
.SIGINT
, clean_exit
)
596 signal
.signal(signal
.SIGTERM
, clean_exit
)
597 taskstats_connection
= TaskStatsNetlink(options
)
598 process_list
= ProcessList(taskstats_connection
, options
)
599 ui
= IOTopUI(win
, process_list
, options
)
603 def run_iotop(options
):
606 return run_iotop_window(None, options
)
608 return curses
.wrapper(run_iotop_window
, options
)
610 if e
.errno
== errno
.EPERM
:
611 print(e
, file=sys
.stderr
)
613 The Linux kernel interfaces that iotop relies on now require root privileges
614 or the NET_ADMIN capability. This change occurred because a security issue
615 (CVE-2011-2494) was found that allows leakage of sensitive data across user
616 boundaries. If you require the ability to run iotop as a non-root user, please
617 configure sudo to allow you to run iotop as root.
619 Please do not file bugs on iotop about this.''', file=sys
.stderr
)
629 def _profile(continuation
):
630 prof_file
= 'iotop.prof'
634 print('Profiling using cProfile')
635 cProfile
.runctx('continuation()', globals(), locals(), prof_file
)
636 stats
= pstats
.Stats(prof_file
)
640 prof
= hotshot
.Profile(prof_file
, lineevents
=1)
641 print('Profiling using hotshot')
642 prof
.runcall(continuation
)
644 stats
= hotshot
.stats
.load(prof_file
)
646 stats
.sort_stats('time', 'calls')
647 stats
.print_stats(50)
648 stats
.print_callees(50)
655 USAGE
= '''%s [OPTIONS]
657 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
658 period. SWAPIN and IO are the percentages of time the thread spent respectively
659 while swapping in and waiting on I/O more generally. PRIO is the I/O priority
660 at which the thread is running (set using the ionice command).
662 Controls: left and right arrows to change the sorting column, r to invert the
663 sorting order, o to toggle the --only option, p to toggle the --processes
664 option, a to toggle the --accumulated option, i to change I/O priority, q to
665 quit, any other key to force a refresh.''' % sys
.argv
[0]
670 locale
.setlocale(locale
.LC_ALL
, '')
672 print('unable to set locale, falling back to the default locale')
673 parser
= optparse
.OptionParser(usage
=USAGE
, version
='iotop ' + VERSION
)
674 parser
.add_option('-o', '--only', action
='store_true',
675 dest
='only', default
=False,
676 help='only show processes or threads actually doing I/O')
677 parser
.add_option('-b', '--batch', action
='store_true', dest
='batch',
678 help='non-interactive mode')
679 parser
.add_option('-n', '--iter', type='int', dest
='iterations',
681 help='number of iterations before ending [infinite]')
682 parser
.add_option('-d', '--delay', type='float', dest
='delay_seconds',
683 help='delay between iterations [1 second]',
684 metavar
='SEC', default
=1)
685 parser
.add_option('-p', '--pid', type='int', dest
='pids', action
='append',
686 help='processes/threads to monitor [all]', metavar
='PID')
687 parser
.add_option('-u', '--user', type='str', dest
='users',
688 action
='append', help='users to monitor [all]',
690 parser
.add_option('-P', '--processes', action
='store_true',
691 dest
='processes', default
=False,
692 help='only show processes, not all threads')
693 parser
.add_option('-a', '--accumulated', action
='store_true',
694 dest
='accumulated', default
=False,
695 help='show accumulated I/O instead of bandwidth')
696 parser
.add_option('-k', '--kilobytes', action
='store_true',
697 dest
='kilobytes', default
=False,
698 help='use kilobytes instead of a human friendly unit')
699 parser
.add_option('-t', '--time', action
='store_true', dest
='time',
700 help='add a timestamp on each line (implies --batch)')
701 parser
.add_option('-q', '--quiet', action
='count', dest
='quiet', default
=0,
702 help='suppress some lines of header (implies --batch)')
703 parser
.add_option('--profile', action
='store_true', dest
='profile',
704 default
=False, help=optparse
.SUPPRESS_HELP
)
705 parser
.add_option('--no-help', action
='store_false', dest
='help', default
=True,
706 help='suppress listing of shortcuts')
708 options
, args
= parser
.parse_args()
710 parser
.error('Unexpected arguments: ' + ' '.join(args
))
712 options
.pids
= options
.pids
or []
713 options
.batch
= options
.batch
or options
.time
or options
.quiet
715 main_loop
= lambda: run_iotop(options
)
718 def safe_main_loop():
723 _profile(safe_main_loop
)