2 # iotop: Display I/O usage of processes in a top like UI
3 # Copyright (c) 2007 Guillaume Chazarain <guichaz@yahoo.fr>, GPLv2
4 # See ./iotop.py --help for some help
6 # 20070723: Added support for taskstats version > 4
7 # 20070813: Handle short replies, and fix bandwidth calculation when delay != 1s
8 # 20070819: Fix "-P -p NOT_A_TGID", optimize -p, handle empty process list
9 # 20070825: More accurate cutting of the command line, handle terminal resizing
10 # 20070826: Document taskstats bug: http://lkml.org/lkml/2007/8/2/185
12 # 20071219: Tolerate misconfigured terminals
26 # Check for requirements:
27 # o Python >= 2.5 for AF_NETLINK sockets
28 # o Linux >= 2.6.20 with I/O accounting
33 except AttributeError:
36 ioaccounting
= os
.path
.exists('/proc/self/io')
38 if not python25
or not ioaccounting
:
39 def boolean2string(boolean
):
40 return boolean
and 'Found' or 'Not found'
41 print 'Could not run iotop as some of the requirements are not met:'
42 print '- Python >= 2.5 for AF_NETLINK support:', boolean2string(python25
)
43 print '- Linux >= 2.6.20 with I/O accounting support:', \
44 boolean2string(ioaccounting
)
49 # Based on code from pynl80211: Netlink message generation/parsing
50 # http://git.sipsolutions.net/?p=pynl80211.git
51 # Copyright 2007 Johannes Berg <johannes@sipsolutions.net>
62 def __init__(self
, type, str, *kw
):
65 self
.data
= struct
.pack(str, *kw
)
70 hdr
= struct
.pack('HH', len(self
.data
)+4, self
.type)
71 length
= len(self
.data
)
72 pad
= ((length
+ 4 - 1) & ~
3 ) - length
73 return hdr
+ self
.data
+ '\0' * pad
76 return struct
.unpack('H', self
.data
)[0]
78 class NulStrAttr(Attr
):
79 def __init__(self
, type, str):
80 Attr
.__init
__(self
, type, '%dsB'%len(str), str, 0)
83 def __init__(self
, type, val
):
84 Attr
.__init
__(self
, type, 'L', val
)
89 def __init__(self
, tp
, flags
= 0, seq
= -1, payload
= []):
94 if type(payload
) == list:
97 contents
.append(attr
._dump
())
98 self
.payload
= ''.join(contents
)
100 self
.payload
= payload
102 def send(self
, conn
):
104 self
.seq
= conn
.seq()
107 length
= len(self
.payload
)
109 hdr
= struct
.pack('IHHII', length
+ 4*4, self
.type, self
.flags
,
111 conn
.send(hdr
+ self
.payload
)
114 def __init__(self
, nltype
, groups
=0, unexpected_msg_handler
= None):
115 self
.fd
= socket
.socket(socket
.AF_NETLINK
, socket
.SOCK_RAW
, nltype
)
116 self
.fd
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_SNDBUF
, 65536)
117 self
.fd
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_RCVBUF
, 65536)
118 self
.fd
.bind((0, groups
))
119 self
.pid
, self
.groups
= self
.fd
.getsockname()
121 self
.unexpected
= unexpected_msg_handler
127 cntnts
= self
.fd
.recv(65536)
128 # should check msgflags for TRUNC!
129 len, type, flags
, seq
, pid
= struct
.unpack('IHHII', cntnts
[:16])
130 m
= Message(type, flags
, seq
, cntnts
[16:])
132 if m
.type == NLMSG_ERROR
:
133 errno
= -struct
.unpack('i', m
.payload
[:4])[0]
135 e
= OSError('Netlink error: %s (%d)' % \
136 (os
.strerror(errno
), errno
))
144 def parse_attributes(str):
147 l
, tp
= struct
.unpack('HH', str[:4])
148 attrs
[tp
] = Attr(tp
, str[4:l
])
149 l
= ((l
+ 4 - 1) & ~
3 )
153 CTRL_CMD_GETFAMILY
= 3
155 CTRL_ATTR_FAMILY_ID
= 1
156 CTRL_ATTR_FAMILY_NAME
= 2
159 def __init__(self
, cmd
, version
= 0):
161 self
.version
= version
164 return struct
.pack('BBxx', self
.cmd
, self
.version
)
166 def _genl_hdr_parse(data
):
167 return GenlHdr(*struct
.unpack('BBxx', data
))
169 GENL_ID_CTRL
= NLMSG_MIN_TYPE
171 class GeNlMessage(Message
):
172 def __init__(self
, family
, cmd
, attrs
=[], flags
=0):
176 Message
.__init
__(self
, family
, flags
=flags
,
177 payload
=[GenlHdr(self
.cmd
)] + attrs
)
180 def __init__(self
, conn
):
183 def get_family_id(self
, family
):
184 a
= NulStrAttr(CTRL_ATTR_FAMILY_NAME
, family
)
185 m
= GeNlMessage(GENL_ID_CTRL
, CTRL_CMD_GETFAMILY
,
186 flags
=NLM_F_REQUEST
, attrs
=[a
])
189 gh
= _genl_hdr_parse(m
.payload
[:4])
190 attrs
= parse_attributes(m
.payload
[4:])
191 return attrs
[CTRL_ATTR_FAMILY_ID
].u16()
194 # Netlink usage for taskstats
197 TASKSTATS_CMD_GET
= 1
198 TASKSTATS_CMD_ATTR_PID
= 1
199 TASKSTATS_CMD_ATTR_TGID
= 2
201 class TaskStatsNetlink(object):
202 # Keep in sync with human_stats(stats, duration)
204 ('blkio_delay_total', 40),
205 ('swapin_delay_total', 56),
208 ('write_bytes', 256),
209 ('cancelled_write_bytes', 264)
212 def __init__(self
, options
):
213 self
.options
= options
214 self
.connection
= Connection(NETLINK_GENERIC
)
215 controller
= Controller(self
.connection
)
216 self
.family_id
= controller
.get_family_id('TASKSTATS')
218 def get_task_stats(self
, pid
):
219 if self
.options
.processes
:
220 attr
= TASKSTATS_CMD_ATTR_TGID
222 attr
= TASKSTATS_CMD_ATTR_PID
223 request
= GeNlMessage(self
.family_id
, cmd
=TASKSTATS_CMD_GET
,
224 attrs
=[U32Attr(attr
, pid
)],
226 request
.send(self
.connection
)
228 reply
= self
.connection
.recv()
230 if e
.errno
== errno
.ESRCH
:
231 # OSError: Netlink error: No such process (3)
234 if len(reply
.payload
) < 292:
237 reply_data
= reply
.payload
[20:]
239 reply_length
, reply_type
= struct
.unpack('HH', reply
.payload
[4:8])
240 reply_version
= struct
.unpack('H', reply
.payload
[20:22])[0]
241 assert reply_length
>= 288
242 assert reply_type
== attr
+ 3
243 assert reply_version
>= 4
246 for name
, offset
in TaskStatsNetlink
.members_offsets
:
247 data
= reply_data
[offset
: offset
+ 8]
248 res
[name
] = struct
.unpack('Q', data
)[0]
256 def find_uids(options
):
259 for u
in options
.users
or []:
264 passwd
= pwd
.getpwnam(u
)
266 print >> sys
.stderr
, 'Unknown user:', u
271 options
.uids
.append(uid
)
276 def __init__(self
, pid
, options
):
280 for name
, offset
in TaskStatsNetlink
.members_offsets
:
281 self
.stats
[name
] = (0, 0) # Total, Delta
282 self
.parse_status('/proc/%d/status' % pid
, options
)
284 def check_if_valid(self
, uid
, options
):
285 self
.valid
= options
.pids
or not options
.uids
or uid
in options
.uids
287 def parse_status(self
, path
, options
):
288 for line
in open(path
):
289 if line
.startswith('Name:'):
290 # Name kernel threads
291 self
.name
= '[' + line
.split()[1].strip() + ']'
292 elif line
.startswith('Uid:'):
293 uid
= int(line
.split()[1])
294 # We check monitored PIDs only here
295 self
.check_if_valid(uid
, options
)
297 self
.user
= pwd
.getpwuid(uid
).pw_name
302 def add_stats(self
, stats
):
303 self
.stats_timestamp
= time
.time()
304 for name
, value
in stats
.iteritems():
305 prev_value
= self
.stats
[name
][0]
306 self
.stats
[name
] = (value
, value
- prev_value
)
308 def get_cmdline(self
):
309 # A process may exec, so we must always reread its cmdline
311 proc_cmdline
= open('/proc/%d/cmdline' % self
.pid
)
313 return '{no such process}'
314 cmdline
= proc_cmdline
.read(4096)
315 parts
= cmdline
.split('\0')
316 first_command_char
= parts
[0].rfind('/') + 1
317 parts
[0] = parts
[0][first_command_char
:]
318 cmdline
= ' '.join(parts
).strip()
319 return cmdline
.encode('string_escape') or self
.name
321 class ProcessList(object):
322 def __init__(self
, taskstats_connection
, options
):
325 self
.taskstats_connection
= taskstats_connection
326 self
.options
= options
328 # A first time as we are interested in the delta
329 self
.update_process_counts()
331 def get_process(self
, pid
):
332 process
= self
.processes
.get(pid
, None)
335 process
= pinfo(pid
, self
.options
)
337 # IOError: [Errno 2] No such file or directory: '/proc/...'
339 if not process
.valid
:
341 self
.processes
[pid
] = process
344 def list_pids(self
, tgid
):
345 if self
.options
.processes
or self
.options
.pids
:
348 return map(int, os
.listdir('/proc/%d/task' % tgid
))
352 def update_process_counts(self
):
353 total_read
= total_write
= duration
= 0
354 tgids
= self
.options
.pids
or [int(tgid
) for tgid
in os
.listdir('/proc')
355 if '0' <= tgid
[0] and tgid
[0] <= '9']
357 for pid
in self
.list_pids(tgid
):
358 process
= self
.get_process(pid
)
360 stats
= self
.taskstats_connection
.get_task_stats(pid
)
363 process
.add_stats(stats
)
364 total_read
+= process
.stats
['read_bytes'][1]
365 total_write
+= process
.stats
['write_bytes'][1]
367 duration
= process
.stats
['ac_etime'][1] / 1000000.0
368 return total_read
, total_write
, duration
370 def refresh_processes(self
):
371 for process
in self
.processes
.values():
373 total_read_and_write_and_duration
= self
.update_process_counts()
375 for pid
, process
in self
.processes
.iteritems():
377 to_delete
.append(pid
)
378 for pid
in to_delete
:
379 del self
.processes
[pid
]
380 return total_read_and_write_and_duration
383 # Utility functions for the UI
386 UNITS
= ['B', 'K', 'M', 'G', 'T', 'P', 'E']
388 def human_bandwidth(size
, duration
):
389 bw
= size
and float(size
) / duration
390 for i
in xrange(len(UNITS
) - 1, 0, -1):
393 res
= '%.2f %s' % ((float(bw
) / base
), UNITS
[i
])
396 res
= str(bw
) + ' ' + UNITS
[0]
399 def human_stats(stats
):
400 # Keep in sync with TaskStatsNetlink.members_offsets and
401 # IOTopUI.get_data(self)
402 duration
= stats
['ac_etime'][1] / 1000000.0
403 def delay2percent(name
): # delay in ns, duration in s
406 return '%.2f %%' % min(99.99, stats
[name
][1] / (duration
* 10000000.0))
407 io_delay
= delay2percent('blkio_delay_total')
408 swapin_delay
= delay2percent('swapin_delay_total')
409 read_bytes
= human_bandwidth(stats
['read_bytes'][1], duration
)
410 written_bytes
= stats
['write_bytes'][1] - stats
['cancelled_write_bytes'][1]
411 written_bytes
= max(0, written_bytes
)
412 write_bytes
= human_bandwidth(written_bytes
, duration
)
413 return io_delay
, swapin_delay
, read_bytes
, write_bytes
419 class IOTopUI(object):
422 (lambda p
: p
.pid
, False),
423 (lambda p
: p
.user
, False),
424 (lambda p
: p
.stats
['read_bytes'][1], True),
425 (lambda p
: p
.stats
['write_bytes'][1] -
426 p
.stats
['cancelled_write_bytes'][1], True),
427 (lambda p
: p
.stats
['swapin_delay_total'][1], True),
428 # The default sorting (by I/O % time) should show processes doing
429 # only writes, without waiting on them
430 (lambda p
: p
.stats
['blkio_delay_total'][1] or
431 int(not(not(p
.stats
['read_bytes'][1] or
432 p
.stats
['write_bytes'][1]))), True),
433 (lambda p
: p
.get_cmdline(), False),
436 def __init__(self
, win
, process_list
, options
):
437 self
.process_list
= process_list
438 self
.options
= options
440 self
.sorting_reverse
= IOTopUI
.sorting_keys
[5][1]
441 if not self
.options
.batch
:
444 curses
.use_default_colors()
449 # This call can fail with misconfigured terminals, for example
450 # TERM=xterm-color. This is harmless
454 self
.height
, self
.width
= self
.win
.getmaxyx()
459 if not self
.options
.batch
:
460 poll
.register(sys
.stdin
.fileno(), select
.POLLIN|select
.POLLPRI
)
461 while self
.options
.iterations
is None or \
462 iterations
< self
.options
.iterations
:
463 total
= self
.process_list
.refresh_processes()
464 total_read
, total_write
, duration
= total
465 self
.refresh_display(total_read
, total_write
, duration
)
466 if self
.options
.iterations
is not None:
468 if iterations
>= self
.options
.iterations
:
472 events
= poll
.poll(self
.options
.delay_seconds
* 1000.0)
473 except select
.error
, e
:
474 if e
.args
and e
.args
[0] == errno
.EINTR
:
478 if not self
.options
.batch
:
481 key
= self
.win
.getch()
484 def reverse_sorting(self
):
485 self
.sorting_reverse
= not self
.sorting_reverse
487 def adjust_sorting_key(self
, delta
):
488 orig_sorting_key
= self
.sorting_key
489 self
.sorting_key
+= delta
490 self
.sorting_key
= max(0, self
.sorting_key
)
491 self
.sorting_key
= min(len(IOTopUI
.sorting_keys
) - 1, self
.sorting_key
)
492 if orig_sorting_key
!= self
.sorting_key
:
493 self
.sorting_reverse
= IOTopUI
.sorting_keys
[self
.sorting_key
][1]
495 def handle_key(self
, key
):
502 lambda: self
.reverse_sorting(),
504 lambda: self
.reverse_sorting(),
506 lambda: self
.adjust_sorting_key(-1),
508 lambda: self
.adjust_sorting_key(1),
510 lambda: self
.adjust_sorting_key(-len(IOTopUI
.sorting_keys
)),
512 lambda: self
.adjust_sorting_key(len(IOTopUI
.sorting_keys
))
515 action
= key_bindings
.get(key
, lambda: None)
520 stats
= human_stats(p
.stats
)
521 io_delay
, swapin_delay
, read_bytes
, write_bytes
= stats
522 line
= '%5d %-8s %11s %11s %7s %7s ' % (p
.pid
, p
.user
[:8],
523 read_bytes
, write_bytes
, swapin_delay
, io_delay
)
524 if self
.options
.batch
:
525 max_cmdline_length
= 4096
527 max_cmdline_length
= self
.width
- len(line
)
528 line
+= p
.get_cmdline()[:max_cmdline_length
]
531 processes
= self
.process_list
.processes
.values()
532 key
= IOTopUI
.sorting_keys
[self
.sorting_key
][0]
533 processes
.sort(key
=key
, reverse
=self
.sorting_reverse
)
534 if not self
.options
.batch
:
535 del processes
[self
.height
- 2:]
536 return map(format
, processes
)
538 def refresh_display(self
, total_read
, total_write
, duration
):
539 summary
= 'Total DISK READ: %s | Total DISK WRITE: %s' % (
540 human_bandwidth(total_read
, duration
),
541 human_bandwidth(total_write
, duration
))
542 titles
= [' PID', ' USER', ' DISK READ', ' DISK WRITE',
543 ' SWAPIN', ' IO', ' COMMAND']
544 lines
= self
.get_data()
545 if self
.options
.batch
:
547 print ''.join(titles
)
552 self
.win
.addstr(summary
)
553 self
.win
.hline(1, 0, ord(' ') | curses
.A_REVERSE
, self
.width
)
554 for i
in xrange(len(titles
)):
555 attr
= curses
.A_REVERSE
557 if i
== self
.sorting_key
:
558 attr |
= curses
.A_BOLD
559 title
+= self
.sorting_reverse
and '>' or '<'
560 self
.win
.addstr(title
, attr
)
561 for i
in xrange(len(lines
)):
562 self
.win
.insstr(i
+ 2, 0, lines
[i
])
565 def run_iotop(win
, options
):
566 taskstats_connection
= TaskStatsNetlink(options
)
567 process_list
= ProcessList(taskstats_connection
, options
)
568 ui
= IOTopUI(win
, process_list
, options
)
577 USAGE
= 'Usage: %s [OPTIONS]' % sys
.argv
[0] + '''
579 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
580 period. SWAPIN and IO are the percentages of time the thread spent respectively
581 while swapping in and waiting on I/O more generally.
582 Controls: left and right arrows to show the sorting column, r to invert the
583 sorting order, q to quit, any other key to force a refresh'''
586 parser
= optparse
.OptionParser(usage
=USAGE
, version
='iotop ' + VERSION
)
587 parser
.add_option('-d', '--delay', type='float', dest
='delay_seconds',
588 help='delay between iterations [1 second]',
589 metavar
='SEC', default
=1)
590 parser
.add_option('-p', '--pid', type='int', dest
='pids', action
='append',
591 help='processes to monitor [all]', metavar
='PID')
592 parser
.add_option('-u', '--user', type='str', dest
='users', action
='append',
593 help='users to monitor [all]', metavar
='USER')
594 parser
.add_option('-b', '--batch', action
='store_true', dest
='batch',
595 help='non-interactive mode')
596 parser
.add_option('-P', '--processes', action
='store_true',
598 help='show only processes, not all threads')
599 parser
.add_option('-n', '--iter', type='int', dest
='iterations',
601 help='number of iterations before ending [infinite]')
602 options
, args
= parser
.parse_args()
604 parser
.error('Unexpected arguments: ' + ' '.join(args
))
606 options
.pids
= options
.pids
or []
608 run_iotop(None, options
)
610 curses
.wrapper(run_iotop
, options
)
612 if __name__
== '__main__':
615 except KeyboardInterrupt: