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
if not self.options.batch:
173
key = self.win.getch()
176
def reverse_sorting(self):
177
self.sorting_reverse = not self.sorting_reverse
179
def adjust_sorting_key(self, delta):
180
orig_sorting_key = self.sorting_key
181
self.sorting_key += delta
182
self.sorting_key = max(0, self.sorting_key)
183
self.sorting_key = min(len(IOTopUI.sorting_keys) - 1, self.sorting_key)
184
if orig_sorting_key != self.sorting_key:
185
self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
187
# I wonder if switching to urwid for the display would be better here
189
def prompt_str(self, prompt, default=None, empty_is_cancel=True):
190
self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
191
self.win.addstr(1, 0, prompt, curses.A_BOLD)
195
inp = self.win.getstr(1, len(prompt))
198
if inp not in (None, ''):
204
def prompt_int(self, prompt, default = None, empty_is_cancel = True):
205
inp = self.prompt_str(prompt, default, empty_is_cancel)
211
def prompt_pid(self):
213
return self.prompt_int('PID to ionice: ')
219
def prompt_tid(self):
221
return self.prompt_int('TID to ionice: ')
227
def prompt_data(self, ioprio_data):
229
if ioprio_data is not None:
230
inp = self.prompt_int('I/O priority data (0-7, currently %s): '
231
% ioprio_data, ioprio_data, False)
233
inp = self.prompt_int('I/O priority data (0-7): ', None, False)
235
raise InvalidIoprioData()
236
if inp < 0 or inp > 7:
237
raise InvalidIoprioData()
240
def prompt_set(self, prompt, display_list, ret_list, selected):
242
selected = ret_list.index(selected)
245
set_len = len(display_list) - 1
247
self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
248
self.win.insstr(1, 0, prompt, curses.A_BOLD)
250
for i, item in enumerate(display_list):
251
display = ' %s ' % item
253
attr = curses.A_REVERSE
255
attr = curses.A_NORMAL
256
self.win.insstr(1, offset, display, attr)
257
offset += len(display)
259
key = self.win.getch()
260
if key in (curses.KEY_LEFT, ord('l')) and selected > 0:
263
elif key in (curses.KEY_RIGHT, ord('r')) and selected < set_len:
266
elif key in (curses.KEY_ENTER, ord('\n'), ord('\r')):
267
return ret_list[selected]
268
elif key in (27, curses.KEY_CANCEL, curses.KEY_CLOSE,
269
curses.KEY_EXIT, ord('q'), ord('Q')):
272
def prompt_class(self, ioprio_class=None):
273
prompt = 'I/O priority class: '
274
classes_prompt = ['Real-time', 'Best-effort', 'Idle']
275
classes_ret = ['rt', 'be', 'idle']
276
if ioprio_class is None:
278
inp = self.prompt_set(prompt, classes_prompt, classes_ret, ioprio_class)
281
def prompt_error(self, error = 'Error!'):
282
self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
283
self.win.insstr(1, 0, ' %s ' % error, curses.A_REVERSE)
287
def prompt_clear(self):
288
self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
291
def handle_key(self, key):
292
def toggle_accumulated():
293
self.options.accumulated ^= True
294
def toggle_only_io():
295
self.options.only ^= True
296
def toggle_processes():
297
self.options.processes ^= True
298
self.process_list.clear()
299
self.process_list.refresh_processes()
302
if self.options.processes:
303
pid = self.prompt_pid()
304
exec_unit = self.process_list.get_process(pid)
306
tid = self.prompt_tid()
307
exec_unit = ThreadInfo(tid,
308
self.process_list.taskstats_connection)
309
ioprio_value = exec_unit.get_ioprio()
310
(ioprio_class, ioprio_data) = \
311
ioprio.to_class_and_data(ioprio_value)
312
ioprio_class = self.prompt_class(ioprio_class)
313
if ioprio_class == 'idle':
316
ioprio_data = self.prompt_data(ioprio_data)
317
exec_unit.set_ioprio(ioprio_class, ioprio_data)
318
self.process_list.clear()
319
self.process_list.refresh_processes()
320
except IoprioSetError as e:
321
self.prompt_error('Error setting I/O priority: %s' % e.err)
323
self.prompt_error('Invalid process id!')
325
self.prompt_error('Invalid thread id!')
326
except InvalidIoprioData:
327
self.prompt_error('Invalid I/O priority data!')
329
self.prompt_error('Invalid integer!')
341
lambda: self.reverse_sorting(),
343
lambda: self.reverse_sorting(),
361
lambda: self.adjust_sorting_key(-1),
363
lambda: self.adjust_sorting_key(1),
365
lambda: self.adjust_sorting_key(-len(IOTopUI.sorting_keys)),
367
lambda: self.adjust_sorting_key(len(IOTopUI.sorting_keys))
370
action = key_bindings.get(key, lambda: None)
375
stats = format_stats(self.options, p, self.process_list.duration)
376
io_delay, swapin_delay, read_bytes, write_bytes = stats
377
if Stats.has_blkio_delay_total:
378
delay_stats = '%7s %7s ' % (swapin_delay, io_delay)
380
delay_stats = ' ?unavailable? '
381
pid_format = '%%%dd' % MAX_PID_WIDTH
382
line = (pid_format + ' %4s %-8s %11s %11s %s') % (
383
p.pid, p.get_ioprio(), p.get_user()[:8], read_bytes,
384
write_bytes, delay_stats)
385
cmdline = p.get_cmdline()
386
if not self.options.batch:
387
remaining_length = self.width - len(line)
388
if 2 < remaining_length < len(cmdline):
389
len1 = (remaining_length - 1) // 2
390
offset2 = -(remaining_length - len1 - 1)
391
cmdline = cmdline[:len1] + '~' + cmdline[offset2:]
393
if not self.options.batch:
394
line = line[:self.width]
397
def should_format(p):
398
return not self.options.only or \
399
p.did_some_io(self.options.accumulated)
401
processes = list(filter(should_format,
402
self.process_list.processes.values()))
403
key = IOTopUI.sorting_keys[self.sorting_key][0]
404
if self.options.accumulated:
405
stats_lambda = lambda p: p.stats_accum
407
stats_lambda = lambda p: p.stats_delta
408
processes.sort(key=lambda p: key(p, stats_lambda(p)),
409
reverse=self.sorting_reverse)
410
if not self.options.batch:
411
del processes[self.height - 2:]
412
return list(map(format, processes))
414
def refresh_display(self, first_time, total, actual, duration):
416
'Total DISK READ : %s | Total DISK WRITE : %s' % (
417
format_bandwidth(self.options, total[0], duration).rjust(14),
418
format_bandwidth(self.options, total[1], duration).rjust(14)),
419
'Actual DISK READ: %s | Actual DISK WRITE: %s' % (
420
format_bandwidth(self.options, actual[0], duration).rjust(14),
421
format_bandwidth(self.options, actual[1], duration).rjust(14))
424
pid = max(0, (MAX_PID_WIDTH - 3)) * ' '
425
if self.options.processes:
429
titles = [pid, ' PRIO', ' USER', ' DISK READ', ' DISK WRITE',
430
' SWAPIN', ' IO', ' COMMAND']
431
lines = self.get_data()
432
if self.options.time:
433
titles = [' TIME'] + titles
434
current_time = time.strftime('%H:%M:%S ')
435
lines = [current_time + l for l in lines]
436
summary = [current_time + s for s in summary]
437
if self.options.batch:
438
if self.options.quiet <= 2:
441
if self.options.quiet <= int(first_time):
442
print(''.join(titles))
448
for i, s in enumerate(summary):
449
self.win.addstr(i, 0, s[:self.width])
450
self.win.hline(len(summary), 0, ord(' ') | curses.A_REVERSE,
452
remaining_cols = self.width
453
for i in range(len(titles)):
454
attr = curses.A_REVERSE
456
if i == self.sorting_key:
458
if i == self.sorting_key:
459
attr |= curses.A_BOLD
460
title += self.sorting_reverse and '>' or '<'
461
title = title[:remaining_cols]
462
remaining_cols -= len(title)
463
self.win.addstr(title, attr)
464
if Stats.has_blkio_delay_total:
467
status_msg = ('CONFIG_TASK_DELAY_ACCT not enabled in kernel, '
468
'cannot determine SWAPIN and IO %')
469
num_lines = min(len(lines), self.height - 2 - int(bool(status_msg)))
470
for i in range(num_lines):
472
self.win.addstr(i + len(summary) + 1, 0, lines[i])
476
self.win.insstr(self.height - len(summary), 0, status_msg,
480
def run_iotop_window(win, options):
482
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
483
taskstats_connection = TaskStatsNetlink(options)
484
process_list = ProcessList(taskstats_connection, options)
485
ui = IOTopUI(win, process_list, options)
488
def run_iotop(options):
491
return run_iotop_window(None, options)
493
return curses.wrapper(run_iotop_window, options)
495
if e.errno == errno.EPERM:
496
print(e, file=sys.stderr)
498
The Linux kernel interfaces that iotop relies on now require root priviliges
499
or the NET_ADMIN capability. This change occured because a security issue
500
(CVE-2011-2494) was found that allows leakage of sensitive data across user
501
boundaries. If you require the ability to run iotop as a non-root user, please
502
configure sudo to allow you to run iotop as root.
504
Please do not file bugs on iotop about this.''', file=sys.stderr)
513
def _profile(continuation):
514
prof_file = 'iotop.prof'
518
print('Profiling using cProfile')
519
cProfile.runctx('continuation()', globals(), locals(), prof_file)
520
stats = pstats.Stats(prof_file)
524
prof = hotshot.Profile(prof_file, lineevents=1)
525
print('Profiling using hotshot')
526
prof.runcall(continuation)
528
stats = hotshot.stats.load(prof_file)
530
stats.sort_stats('time', 'calls')
531
stats.print_stats(50)
532
stats.print_callees(50)
539
USAGE = '''%s [OPTIONS]
541
DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
542
period. SWAPIN and IO are the percentages of time the thread spent respectively
543
while swapping in and waiting on I/O more generally. PRIO is the I/O priority at
544
which the thread is running (set using the ionice command).
546
Controls: left and right arrows to change the sorting column, r to invert the
547
sorting order, o to toggle the --only option, p to toggle the --processes
548
option, a to toggle the --accumulated option, i to change I/O priority, q to
549
quit, any other key to force a refresh.''' % sys.argv[0]
553
locale.setlocale(locale.LC_ALL, '')
555
print('unable to set locale, falling back to the default locale')
556
parser = optparse.OptionParser(usage=USAGE, version='iotop ' + VERSION)
557
parser.add_option('-o', '--only', action='store_true',
558
dest='only', default=False,
559
help='only show processes or threads actually doing I/O')
560
parser.add_option('-b', '--batch', action='store_true', dest='batch',
561
help='non-interactive mode')
562
parser.add_option('-n', '--iter', type='int', dest='iterations',
564
help='number of iterations before ending [infinite]')
565
parser.add_option('-d', '--delay', type='float', dest='delay_seconds',
566
help='delay between iterations [1 second]',
567
metavar='SEC', default=1)
568
parser.add_option('-p', '--pid', type='int', dest='pids', action='append',
569
help='processes/threads to monitor [all]', metavar='PID')
570
parser.add_option('-u', '--user', type='str', dest='users', action='append',
571
help='users to monitor [all]', metavar='USER')
572
parser.add_option('-P', '--processes', action='store_true',
573
dest='processes', default=False,
574
help='only show processes, not all threads')
575
parser.add_option('-a', '--accumulated', action='store_true',
576
dest='accumulated', default=False,
577
help='show accumulated I/O instead of bandwidth')
578
parser.add_option('-k', '--kilobytes', action='store_true',
579
dest='kilobytes', default=False,
580
help='use kilobytes instead of a human friendly unit')
581
parser.add_option('-t', '--time', action='store_true', dest='time',
582
help='add a timestamp on each line (implies --batch)')
583
parser.add_option('-q', '--quiet', action='count', dest='quiet', default=0,
584
help='suppress some lines of header (implies --batch)')
585
parser.add_option('--profile', action='store_true', dest='profile',
586
default=False, help=optparse.SUPPRESS_HELP)
588
options, args = parser.parse_args()
590
parser.error('Unexpected arguments: ' + ' '.join(args))
592
options.pids = options.pids or []
593
options.batch = options.batch or options.time or options.quiet
595
main_loop = lambda: run_iotop(options)
598
def safe_main_loop():
603
_profile(safe_main_loop)