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 from iotop
.data
import find_uids
, TaskStatsNetlink
, ProcessList
, Stats
34 from iotop
.data
import ThreadInfo
35 from iotop
.version
import VERSION
36 from iotop
import ioprio
37 from iotop
.ioprio
import IoprioSetError
40 # Utility functions for the UI
43 UNITS
= ['B', 'K', 'M', 'G', 'T', 'P', 'E']
54 expo
= int(math
.log(size
/ 2, 2) / 10)
55 return '%s%.2f %s' % (sign
, (float(size
) / (1 << (10 * expo
))), UNITS
[expo
])
57 def format_size(options
, bytes
):
59 return '%.2f K' % (bytes
/ 1024.0)
60 return human_size(bytes
)
62 def format_bandwidth(options
, size
, duration
):
63 return format_size(options
, size
and float(size
) / duration
) + '/s'
65 def format_stats(options
, process
, duration
):
66 # Keep in sync with TaskStatsNetlink.members_offsets and
67 # IOTopUI.get_data(self)
68 def delay2percent(delay
): # delay in ns, duration in s
69 return '%.2f %%' % min(99.99, delay
/ (duration
* 10000000.0))
70 if options
.accumulated
:
71 stats
= process
.stats_accum
72 display_format
= lambda size
, duration
: format_size(options
, size
)
73 duration
= time
.time() - process
.stats_accum_timestamp
75 stats
= process
.stats_delta
76 display_format
= lambda size
, duration
: format_bandwidth(
77 options
, size
, duration
)
78 io_delay
= delay2percent(stats
.blkio_delay_total
)
79 swapin_delay
= delay2percent(stats
.swapin_delay_total
)
80 read_bytes
= display_format(stats
.read_bytes
, duration
)
81 written_bytes
= stats
.write_bytes
- stats
.cancelled_write_bytes
82 written_bytes
= max(0, written_bytes
)
83 write_bytes
= display_format(written_bytes
, duration
)
84 return io_delay
, swapin_delay
, read_bytes
, write_bytes
86 def get_max_pid_width():
88 return len(open('/proc/sys/kernel/pid_max').read().strip())
89 except Exception as e
:
91 # Reasonable default in case something fails
94 MAX_PID_WIDTH
= get_max_pid_width()
100 class CancelInput(Exception): pass
101 class InvalidInt(Exception): pass
102 class InvalidPid(Exception): pass
103 class InvalidTid(Exception): pass
104 class InvalidIoprioData(Exception): pass
110 class IOTopUI(object):
113 (lambda p
, s
: p
.pid
, False),
114 (lambda p
, s
: p
.ioprio_sort_key(), False),
115 (lambda p
, s
: p
.get_user(), False),
116 (lambda p
, s
: s
.read_bytes
, True),
117 (lambda p
, s
: s
.write_bytes
- s
.cancelled_write_bytes
, True),
118 (lambda p
, s
: s
.swapin_delay_total
, True),
119 # The default sorting (by I/O % time) should show processes doing
120 # only writes, without waiting on them
121 (lambda p
, s
: s
.blkio_delay_total
or
122 int(not(not(s
.read_bytes
or s
.write_bytes
))), True),
123 (lambda p
, s
: p
.get_cmdline(), False),
126 def __init__(self
, win
, process_list
, options
):
127 self
.process_list
= process_list
128 self
.options
= options
130 self
.sorting_reverse
= IOTopUI
.sorting_keys
[self
.sorting_key
][1]
131 if not self
.options
.batch
:
135 curses
.use_default_colors()
139 # This call can fail with misconfigured terminals, for example
140 # TERM=xterm-color. This is harmless
144 self
.height
, self
.width
= self
.win
.getmaxyx()
149 if not self
.options
.batch
:
150 poll
.register(sys
.stdin
.fileno(), select
.POLLIN|select
.POLLPRI
)
151 while self
.options
.iterations
is None or \
152 iterations
< self
.options
.iterations
:
153 total
, actual
= self
.process_list
.refresh_processes()
154 self
.refresh_display(iterations
== 0, total
, actual
,
155 self
.process_list
.duration
)
156 if self
.options
.iterations
is not None:
158 if iterations
>= self
.options
.iterations
:
160 elif iterations
== 0:
164 events
= poll
.poll(self
.options
.delay_seconds
* 1000.0)
165 except select
.error
as e
:
166 if e
.args
and e
.args
[0] == errno
.EINTR
:
170 for (fd
, event
) in events
:
171 if event
& (select
.POLLERR | select
.POLLHUP
):
173 if not self
.options
.batch
:
176 key
= self
.win
.getch()
179 def reverse_sorting(self
):
180 self
.sorting_reverse
= not self
.sorting_reverse
182 def adjust_sorting_key(self
, delta
):
183 orig_sorting_key
= self
.sorting_key
184 self
.sorting_key
+= delta
185 self
.sorting_key
= max(0, self
.sorting_key
)
186 self
.sorting_key
= min(len(IOTopUI
.sorting_keys
) - 1, self
.sorting_key
)
187 if orig_sorting_key
!= self
.sorting_key
:
188 self
.sorting_reverse
= IOTopUI
.sorting_keys
[self
.sorting_key
][1]
190 # I wonder if switching to urwid for the display would be better here
192 def prompt_str(self
, prompt
, default
=None, empty_is_cancel
=True):
193 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
194 self
.win
.addstr(1, 0, prompt
, curses
.A_BOLD
)
198 inp
= self
.win
.getstr(1, len(prompt
))
201 if inp
not in (None, ''):
207 def prompt_int(self
, prompt
, default
= None, empty_is_cancel
= True):
208 inp
= self
.prompt_str(prompt
, default
, empty_is_cancel
)
214 def prompt_pid(self
):
216 return self
.prompt_int('PID to ionice: ')
222 def prompt_tid(self
):
224 return self
.prompt_int('TID to ionice: ')
230 def prompt_data(self
, ioprio_data
):
232 if ioprio_data
is not None:
233 inp
= self
.prompt_int('I/O priority data (0-7, currently %s): '
234 % ioprio_data
, ioprio_data
, False)
236 inp
= self
.prompt_int('I/O priority data (0-7): ', None, False)
238 raise InvalidIoprioData()
239 if inp
< 0 or inp
> 7:
240 raise InvalidIoprioData()
243 def prompt_set(self
, prompt
, display_list
, ret_list
, selected
):
245 selected
= ret_list
.index(selected
)
248 set_len
= len(display_list
) - 1
250 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
251 self
.win
.insstr(1, 0, prompt
, curses
.A_BOLD
)
253 for i
, item
in enumerate(display_list
):
254 display
= ' %s ' % item
256 attr
= curses
.A_REVERSE
258 attr
= curses
.A_NORMAL
259 self
.win
.insstr(1, offset
, display
, attr
)
260 offset
+= len(display
)
262 key
= self
.win
.getch()
263 if key
in (curses
.KEY_LEFT
, ord('l')) and selected
> 0:
266 elif key
in (curses
.KEY_RIGHT
, ord('r')) and selected
< set_len
:
269 elif key
in (curses
.KEY_ENTER
, ord('\n'), ord('\r')):
270 return ret_list
[selected
]
271 elif key
in (27, curses
.KEY_CANCEL
, curses
.KEY_CLOSE
,
272 curses
.KEY_EXIT
, ord('q'), ord('Q')):
275 def prompt_class(self
, ioprio_class
=None):
276 prompt
= 'I/O priority class: '
277 classes_prompt
= ['Real-time', 'Best-effort', 'Idle']
278 classes_ret
= ['rt', 'be', 'idle']
279 if ioprio_class
is None:
281 inp
= self
.prompt_set(prompt
, classes_prompt
, classes_ret
, ioprio_class
)
284 def prompt_error(self
, error
= 'Error!'):
285 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
286 self
.win
.insstr(1, 0, ' %s ' % error
, curses
.A_REVERSE
)
290 def prompt_clear(self
):
291 self
.win
.hline(1, 0, ord(' ') | curses
.A_NORMAL
, self
.width
)
294 def handle_key(self
, key
):
295 def toggle_accumulated():
296 self
.options
.accumulated ^
= True
297 def toggle_only_io():
298 self
.options
.only ^
= True
299 def toggle_processes():
300 self
.options
.processes ^
= True
301 self
.process_list
.clear()
302 self
.process_list
.refresh_processes()
305 if self
.options
.processes
:
306 pid
= self
.prompt_pid()
307 exec_unit
= self
.process_list
.get_process(pid
)
309 tid
= self
.prompt_tid()
310 exec_unit
= ThreadInfo(tid
,
311 self
.process_list
.taskstats_connection
)
312 ioprio_value
= exec_unit
.get_ioprio()
313 (ioprio_class
, ioprio_data
) = \
314 ioprio
.to_class_and_data(ioprio_value
)
315 ioprio_class
= self
.prompt_class(ioprio_class
)
316 if ioprio_class
== 'idle':
319 ioprio_data
= self
.prompt_data(ioprio_data
)
320 exec_unit
.set_ioprio(ioprio_class
, ioprio_data
)
321 self
.process_list
.clear()
322 self
.process_list
.refresh_processes()
323 except IoprioSetError
as e
:
324 self
.prompt_error('Error setting I/O priority: %s' % e
.err
)
326 self
.prompt_error('Invalid process id!')
328 self
.prompt_error('Invalid thread id!')
329 except InvalidIoprioData
:
330 self
.prompt_error('Invalid I/O priority data!')
332 self
.prompt_error('Invalid integer!')
344 lambda: self
.reverse_sorting(),
346 lambda: self
.reverse_sorting(),
364 lambda: self
.adjust_sorting_key(-1),
366 lambda: self
.adjust_sorting_key(1),
368 lambda: self
.adjust_sorting_key(-len(IOTopUI
.sorting_keys
)),
370 lambda: self
.adjust_sorting_key(len(IOTopUI
.sorting_keys
))
373 action
= key_bindings
.get(key
, lambda: None)
378 stats
= format_stats(self
.options
, p
, self
.process_list
.duration
)
379 io_delay
, swapin_delay
, read_bytes
, write_bytes
= stats
380 if Stats
.has_blkio_delay_total
:
381 delay_stats
= '%7s %7s ' % (swapin_delay
, io_delay
)
383 delay_stats
= ' ?unavailable? '
384 pid_format
= '%%%dd' % MAX_PID_WIDTH
385 line
= (pid_format
+ ' %4s %-8s %11s %11s %s') % (
386 p
.pid
, p
.get_ioprio(), p
.get_user()[:8], read_bytes
,
387 write_bytes
, delay_stats
)
388 cmdline
= p
.get_cmdline()
389 if not self
.options
.batch
:
390 remaining_length
= self
.width
- len(line
)
391 if 2 < remaining_length
< len(cmdline
):
392 len1
= (remaining_length
- 1) // 2
393 offset2
= -(remaining_length
- len1
- 1)
394 cmdline
= cmdline
[:len1
] + '~' + cmdline
[offset2
:]
396 if not self
.options
.batch
:
397 line
= line
[:self
.width
]
400 def should_format(p
):
401 return not self
.options
.only
or \
402 p
.did_some_io(self
.options
.accumulated
)
404 processes
= list(filter(should_format
,
405 self
.process_list
.processes
.values()))
406 key
= IOTopUI
.sorting_keys
[self
.sorting_key
][0]
407 if self
.options
.accumulated
:
408 stats_lambda
= lambda p
: p
.stats_accum
410 stats_lambda
= lambda p
: p
.stats_delta
411 processes
.sort(key
=lambda p
: key(p
, stats_lambda(p
)),
412 reverse
=self
.sorting_reverse
)
413 if not self
.options
.batch
:
414 del processes
[self
.height
- 2:]
415 return list(map(format
, processes
))
417 def refresh_display(self
, first_time
, total
, actual
, duration
):
419 'Total DISK READ : %s | Total DISK WRITE : %s' % (
420 format_bandwidth(self
.options
, total
[0], duration
).rjust(14),
421 format_bandwidth(self
.options
, total
[1], duration
).rjust(14)),
422 'Actual DISK READ: %s | Actual DISK WRITE: %s' % (
423 format_bandwidth(self
.options
, actual
[0], duration
).rjust(14),
424 format_bandwidth(self
.options
, actual
[1], duration
).rjust(14))
427 pid
= max(0, (MAX_PID_WIDTH
- 3)) * ' '
428 if self
.options
.processes
:
432 titles
= [pid
, ' PRIO', ' USER', ' DISK READ', ' DISK WRITE',
433 ' SWAPIN', ' IO', ' COMMAND']
434 lines
= self
.get_data()
435 if self
.options
.time
:
436 titles
= [' TIME'] + titles
437 current_time
= time
.strftime('%H:%M:%S ')
438 lines
= [current_time
+ l
for l
in lines
]
439 summary
= [current_time
+ s
for s
in summary
]
440 if self
.options
.batch
:
441 if self
.options
.quiet
<= 2:
444 if self
.options
.quiet
<= int(first_time
):
445 print(''.join(titles
))
451 for i
, s
in enumerate(summary
):
452 self
.win
.addstr(i
, 0, s
[:self
.width
])
453 self
.win
.hline(len(summary
), 0, ord(' ') | curses
.A_REVERSE
,
455 remaining_cols
= self
.width
456 for i
in range(len(titles
)):
457 attr
= curses
.A_REVERSE
459 if i
== self
.sorting_key
:
461 if i
== self
.sorting_key
:
462 attr |
= curses
.A_BOLD
463 title
+= self
.sorting_reverse
and '>' or '<'
464 title
= title
[:remaining_cols
]
465 remaining_cols
-= len(title
)
466 self
.win
.addstr(title
, attr
)
467 if Stats
.has_blkio_delay_total
:
470 status_msg
= ('CONFIG_TASK_DELAY_ACCT not enabled in kernel, '
471 'cannot determine SWAPIN and IO %')
472 num_lines
= min(len(lines
), self
.height
- 2 - int(bool(status_msg
)))
473 for i
in range(num_lines
):
475 def print_line(line
):
476 self
.win
.addstr(i
+ len(summary
) + 1, 0, line
)
479 except UnicodeEncodeError:
480 # Python2: 'ascii' codec can't encode character ...
481 # http://bugs.debian.org/708252
482 print_line(lines
[i
].encode('utf-8'))
486 self
.win
.insstr(self
.height
- len(summary
), 0, status_msg
,
490 def run_iotop_window(win
, options
):
492 signal
.signal(signal
.SIGPIPE
, signal
.SIG_DFL
)
494 def clean_exit(*args
, **kwargs
):
496 signal
.signal(signal
.SIGINT
, clean_exit
)
497 signal
.signal(signal
.SIGTERM
, clean_exit
)
498 taskstats_connection
= TaskStatsNetlink(options
)
499 process_list
= ProcessList(taskstats_connection
, options
)
500 ui
= IOTopUI(win
, process_list
, options
)
503 def run_iotop(options
):
506 return run_iotop_window(None, options
)
508 return curses
.wrapper(run_iotop_window
, options
)
510 if e
.errno
== errno
.EPERM
:
511 print(e
, file=sys
.stderr
)
513 The Linux kernel interfaces that iotop relies on now require root priviliges
514 or the NET_ADMIN capability. This change occured because a security issue
515 (CVE-2011-2494) was found that allows leakage of sensitive data across user
516 boundaries. If you require the ability to run iotop as a non-root user, please
517 configure sudo to allow you to run iotop as root.
519 Please do not file bugs on iotop about this.''', file=sys
.stderr
)
528 def _profile(continuation
):
529 prof_file
= 'iotop.prof'
533 print('Profiling using cProfile')
534 cProfile
.runctx('continuation()', globals(), locals(), prof_file
)
535 stats
= pstats
.Stats(prof_file
)
539 prof
= hotshot
.Profile(prof_file
, lineevents
=1)
540 print('Profiling using hotshot')
541 prof
.runcall(continuation
)
543 stats
= hotshot
.stats
.load(prof_file
)
545 stats
.sort_stats('time', 'calls')
546 stats
.print_stats(50)
547 stats
.print_callees(50)
554 USAGE
= '''%s [OPTIONS]
556 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
557 period. SWAPIN and IO are the percentages of time the thread spent respectively
558 while swapping in and waiting on I/O more generally. PRIO is the I/O priority at
559 which the thread is running (set using the ionice command).
561 Controls: left and right arrows to change the sorting column, r to invert the
562 sorting order, o to toggle the --only option, p to toggle the --processes
563 option, a to toggle the --accumulated option, i to change I/O priority, q to
564 quit, any other key to force a refresh.''' % sys
.argv
[0]
568 locale
.setlocale(locale
.LC_ALL
, '')
570 print('unable to set locale, falling back to the default locale')
571 parser
= optparse
.OptionParser(usage
=USAGE
, version
='iotop ' + VERSION
)
572 parser
.add_option('-o', '--only', action
='store_true',
573 dest
='only', default
=False,
574 help='only show processes or threads actually doing I/O')
575 parser
.add_option('-b', '--batch', action
='store_true', dest
='batch',
576 help='non-interactive mode')
577 parser
.add_option('-n', '--iter', type='int', dest
='iterations',
579 help='number of iterations before ending [infinite]')
580 parser
.add_option('-d', '--delay', type='float', dest
='delay_seconds',
581 help='delay between iterations [1 second]',
582 metavar
='SEC', default
=1)
583 parser
.add_option('-p', '--pid', type='int', dest
='pids', action
='append',
584 help='processes/threads to monitor [all]', metavar
='PID')
585 parser
.add_option('-u', '--user', type='str', dest
='users', action
='append',
586 help='users to monitor [all]', metavar
='USER')
587 parser
.add_option('-P', '--processes', action
='store_true',
588 dest
='processes', default
=False,
589 help='only show processes, not all threads')
590 parser
.add_option('-a', '--accumulated', action
='store_true',
591 dest
='accumulated', default
=False,
592 help='show accumulated I/O instead of bandwidth')
593 parser
.add_option('-k', '--kilobytes', action
='store_true',
594 dest
='kilobytes', default
=False,
595 help='use kilobytes instead of a human friendly unit')
596 parser
.add_option('-t', '--time', action
='store_true', dest
='time',
597 help='add a timestamp on each line (implies --batch)')
598 parser
.add_option('-q', '--quiet', action
='count', dest
='quiet', default
=0,
599 help='suppress some lines of header (implies --batch)')
600 parser
.add_option('--profile', action
='store_true', dest
='profile',
601 default
=False, help=optparse
.SUPPRESS_HELP
)
603 options
, args
= parser
.parse_args()
605 parser
.error('Unexpected arguments: ' + ' '.join(args
))
607 options
.pids
= options
.pids
or []
608 options
.batch
= options
.batch
or options
.time
or options
.quiet
610 main_loop
= lambda: run_iotop(options
)
613 def safe_main_loop():
618 _profile(safe_main_loop
)