Intensive disk activity on Linux often explains sluggish shells, slow applications, or stalled services even when CPU graphs look normal. Observing disk Input/Output (I/O) in detail highlights which workloads saturate storage bandwidth or create long queues, making performance investigations more accurate than relying on CPU metrics alone.

The kernel maintains counters for block devices and processes, and tools such as iostat, iotop, and ioping convert those counters into readable statistics. iostat focuses on per-disk throughput and utilization, iotop attributes disk I/O to individual processes or threads in real time, and ioping probes latency by repeatedly issuing small read operations against a device or directory.

Choosing sensible sampling intervals and scopes keeps monitoring overhead low while still surfacing patterns such as sustained high iowait or sudden spikes in response times. Detailed per-process and per-device statistics typically require a user with sudo privileges, and running aggressive latency tests on production storage can add extra load if intervals are too short.

Steps to monitor disk activity on Linux:

  1. Open a terminal on the Linux system using a user account that can run sudo.
  2. Install the sysstat package that provides the iostat command on Ubuntu and other Debian-based systems.
    $ sudo apt update
    Hit:1 http://ports.ubuntu.com/ubuntu-ports noble InRelease
    Hit:2 http://ports.ubuntu.com/ubuntu-ports noble-updates InRelease
    Hit:3 http://ports.ubuntu.com/ubuntu-ports noble-backports InRelease
    Hit:4 http://ports.ubuntu.com/ubuntu-ports noble-security InRelease
    Reading package lists...
    Building dependency tree...
    Reading state information...
    5 packages can be upgraded. Run 'apt list --upgradable' to see them.
    $ sudo apt install --assume-yes sysstat
    Reading package lists...
    Building dependency tree...
    Reading state information...
    The following additional packages will be installed:
      libsensors-config libsensors5 xz-utils
    Suggested packages:
      lm-sensors isag
    The following NEW packages will be installed:
      libsensors-config libsensors5 sysstat xz-utils
    0 upgraded, 4 newly installed, 0 to remove and 5 not upgraded.
    Need to get 781 kB of archives.
    After this operation, 2597 kB of additional disk space will be used.
    Get:1 http://ports.ubuntu.com/ubuntu-ports noble/main arm64 libsensors-config all 1:3.6.0-9build1 [5546 B]
    Get:2 http://ports.ubuntu.com/ubuntu-ports noble/main arm64 libsensors5 arm64 1:3.6.0-9build1 [27.0 kB]
    Get:3 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 xz-utils arm64 5.6.1+really5.4.5-1ubuntu0.2 [268 kB]
    Get:4 http://ports.ubuntu.com/ubuntu-ports noble/main arm64 sysstat arm64 12.6.1-2 [480 kB]
    Fetched 781 kB in 3s (309 kB/s)
    Selecting previously unselected package libsensors-config.
    (Reading database ... (Reading database ... 5%(Reading database ... 10%(Reading database ... 15%(Reading database ... 20%(Reading database ... 25%(Reading database ... 30%(Reading database ... 35%(Reading database ... 40%(Reading database ... 45%(Reading database ... 50%(Reading database ... 55%(Reading database ... 60%(Reading database ... 65%(Reading database ... 70%(Reading database ... 75%(Reading database ... 80%(Reading database ... 85%(Reading database ... 90%(Reading database ... 95%(Reading database ... 100%(Reading database ... 17940 files and directories currently installed.)
    Preparing to unpack .../libsensors-config_1%3a3.6.0-9build1_all.deb ...
    Unpacking libsensors-config (1:3.6.0-9build1) ...
    Selecting previously unselected package libsensors5:arm64.
    Preparing to unpack .../libsensors5_1%3a3.6.0-9build1_arm64.deb ...
    Unpacking libsensors5:arm64 (1:3.6.0-9build1) ...
    Selecting previously unselected package xz-utils.
    Preparing to unpack .../xz-utils_5.6.1+really5.4.5-1ubuntu0.2_arm64.deb ...
    Unpacking xz-utils (5.6.1+really5.4.5-1ubuntu0.2) ...
    Selecting previously unselected package sysstat.
    Preparing to unpack .../sysstat_12.6.1-2_arm64.deb ...
    Unpacking sysstat (12.6.1-2) ...
    Setting up libsensors-config (1:3.6.0-9build1) ...
    Setting up xz-utils (5.6.1+really5.4.5-1ubuntu0.2) ...
    update-alternatives: using /usr/bin/xz to provide /usr/bin/lzma (lzma) in auto mode
    update-alternatives: warning: skip creation of /usr/share/man/man1/lzma.1.gz because associated file /usr/share/man/man1/xz.1.gz (of link group lzma) doesn't exist
    update-alternatives: warning: skip creation of /usr/share/man/man1/unlzma.1.gz because associated file /usr/share/man/man1/unxz.1.gz (of link group lzma) doesn't exist
    update-alternatives: warning: skip creation of /usr/share/man/man1/lzcat.1.gz because associated file /usr/share/man/man1/xzcat.1.gz (of link group lzma) doesn't exist
    update-alternatives: warning: skip creation of /usr/share/man/man1/lzmore.1.gz because associated file /usr/share/man/man1/xzmore.1.gz (of link group lzma) doesn't exist
    update-alternatives: warning: skip creation of /usr/share/man/man1/lzless.1.gz because associated file /usr/share/man/man1/xzless.1.gz (of link group lzma) doesn't exist
    update-alternatives: warning: skip creation of /usr/share/man/man1/lzdiff.1.gz because associated file /usr/share/man/man1/xzdiff.1.gz (of link group lzma) doesn't exist
    update-alternatives: warning: skip creation of /usr/share/man/man1/lzcmp.1.gz because associated file /usr/share/man/man1/xzcmp.1.gz (of link group lzma) doesn't exist
    update-alternatives: warning: skip creation of /usr/share/man/man1/lzgrep.1.gz because associated file /usr/share/man/man1/xzgrep.1.gz (of link group lzma) doesn't exist
    update-alternatives: warning: skip creation of /usr/share/man/man1/lzegrep.1.gz because associated file /usr/share/man/man1/xzegrep.1.gz (of link group lzma) doesn't exist
    update-alternatives: warning: skip creation of /usr/share/man/man1/lzfgrep.1.gz because associated file /usr/share/man/man1/xzfgrep.1.gz (of link group lzma) doesn't exist
    Setting up libsensors5:arm64 (1:3.6.0-9build1) ...
    Setting up sysstat (12.6.1-2) ...
    debconf: unable to initialize frontend: Dialog
    debconf: (Dialog frontend will not work on a dumb terminal, an emacs shell buffer, or without a controlling terminal.)
    debconf: falling back to frontend: Readline
    debconf: unable to initialize frontend: Readline
    debconf: (Can't locate Term/ReadLine.pm in @INC (you may need to install the Term::ReadLine module) (@INC entries checked: /etc/perl /usr/local/lib/aarch64-linux-gnu/perl/5.38.2 /usr/local/share/perl/5.38.2 /usr/lib/aarch64-linux-gnu/perl5/5.38 /usr/share/perl5 /usr/lib/aarch64-linux-gnu/perl-base /usr/lib/aarch64-linux-gnu/perl/5.38 /usr/share/perl/5.38 /usr/local/lib/site_perl) at /usr/share/perl5/Debconf/FrontEnd/Readline.pm line 8.)
    debconf: falling back to frontend: Teletype
    
    Creating config file /etc/default/sysstat with new version
    update-alternatives: using /usr/bin/sar.sysstat to provide /usr/bin/sar (sar) in auto mode
    update-alternatives: warning: skip creation of /usr/share/man/man1/sar.1.gz because associated file /usr/share/man/man1/sar.sysstat.1.gz (of link group sar) doesn't exist
    Created symlink /etc/systemd/system/sysstat.service.wants/sysstat-collect.timer -> /usr/lib/systemd/system/sysstat-collect.timer.
    Created symlink /etc/systemd/system/sysstat.service.wants/sysstat-summary.timer -> /usr/lib/systemd/system/sysstat-summary.timer.
    Created symlink /etc/systemd/system/multi-user.target.wants/sysstat.service -> /usr/lib/systemd/system/sysstat.service.
    Processing triggers for libc-bin (2.39-0ubuntu8.6) ...

    On RHEL, CentOS, and Fedora, install using sudo dnf install --assumeyes sysstat.

  3. Run iostat without options to display CPU usage and block device statistics since boot.
    $ iostat
    Linux 6.12.54-linuxkit (host.example.net) 	01/13/26 	_aarch64_	(10 CPU)
    
    avg-cpu:  %user   %nice %system %iowait  %steal   %idle
               0.51    0.00    0.24    0.00    0.00   99.24
    
    Device             tps    kB_read/s    kB_wrtn/s    kB_dscd/s    kB_read    kB_wrtn    kB_dscd
    loop0             0.00         0.05         1.23         4.71       7875     205988     786444
    vda               1.58         3.08       254.27     12062.34     515061   42462540 2014358104
    vdb               0.43       418.13         0.00         0.00   69825840          0          0
    
    

    The avg-cpu section summarizes CPU time breakdown, and the Device table lists per-device throughput and request rates.

  4. Focus on a specific disk by passing the device path to iostat together with an interval in seconds for continuous monitoring.
    $ iostat /dev/vdb -y 1 3
    Linux 6.12.54-linuxkit (host.example.net) 	01/13/26 	_aarch64_	(10 CPU)
    
    
    avg-cpu:  %user   %nice %system %iowait  %steal   %idle
               0.00    0.00    0.00    0.00    0.00  100.00
    
    Device             tps    kB_read/s    kB_wrtn/s    kB_dscd/s    kB_read    kB_wrtn    kB_dscd
    vdb               0.00         0.00         0.00         0.00          0          0          0
    
    
    avg-cpu:  %user   %nice %system %iowait  %steal   %idle
               0.00    0.00    0.10    0.00    0.00   99.90
    
    Device             tps    kB_read/s    kB_wrtn/s    kB_dscd/s    kB_read    kB_wrtn    kB_dscd
    vdb               0.00         0.00         0.00         0.00          0          0          0
    
    
    avg-cpu:  %user   %nice %system %iowait  %steal   %idle
               0.00    0.00    0.00    0.00    0.00  100.00
    
    Device             tps    kB_read/s    kB_wrtn/s    kB_dscd/s    kB_read    kB_wrtn    kB_dscd
    vdb               0.00         0.00         0.00         0.00          0          0          0

    Higher tps and write rates on a device indicate more active workloads using that disk.

  5. Install iotop to monitor per-process disk I/O usage on Ubuntu and other Debian-based systems.
    $ sudo apt update
    Hit:1 http://ports.ubuntu.com/ubuntu-ports noble InRelease
    Hit:2 http://ports.ubuntu.com/ubuntu-ports noble-updates InRelease
    Hit:3 http://ports.ubuntu.com/ubuntu-ports noble-backports InRelease
    Hit:4 http://ports.ubuntu.com/ubuntu-ports noble-security InRelease
    Reading package lists...
    Building dependency tree...
    Reading state information...
    5 packages can be upgraded. Run 'apt list --upgradable' to see them.
    $ sudo apt install --assume-yes iotop
    Reading package lists...
    Building dependency tree...
    Reading state information...
    The following NEW packages will be installed:
      iotop
    0 upgraded, 1 newly installed, 0 to remove and 5 not upgraded.
    Need to get 24.4 kB of archives.
    After this operation, 163 kB of additional disk space will be used.
    Get:1 http://ports.ubuntu.com/ubuntu-ports noble/main arm64 iotop arm64 0.6-42-ga14256a-0.2build1 [24.4 kB]
    Fetched 24.4 kB in 1s (26.6 kB/s)
    Selecting previously unselected package iotop.
    (Reading database ... (Reading database ... 5%(Reading database ... 10%(Reading database ... 15%(Reading database ... 20%(Reading database ... 25%(Reading database ... 30%(Reading database ... 35%(Reading database ... 40%(Reading database ... 45%(Reading database ... 50%(Reading database ... 55%(Reading database ... 60%(Reading database ... 65%(Reading database ... 70%(Reading database ... 75%(Reading database ... 80%(Reading database ... 85%(Reading database ... 90%(Reading database ... 95%(Reading database ... 100%(Reading database ... 18160 files and directories currently installed.)
    Preparing to unpack .../iotop_0.6-42-ga14256a-0.2build1_arm64.deb ...
    Unpacking iotop (0.6-42-ga14256a-0.2build1) ...
    Setting up iotop (0.6-42-ga14256a-0.2build1) ...
    update-alternatives: using /usr/sbin/iotop-py to provide /usr/sbin/iotop (iotop) in auto mode
    update-alternatives: warning: skip creation of /usr/share/man/man8/iotop.8.gz because associated file /usr/share/man/man8/iotop-py.8.gz (of link group iotop) doesn't exist

    On RHEL, CentOS, and Fedora, install using sudo dnf install --assumeyes iotop.

  6. Run iotop with sudo to see real-time disk read and write activity for all running processes.
    $ sudo iotop -b -n 1
    Total DISK READ:         0.00 B/s | Total DISK WRITE:         0.00 B/s
    Current DISK READ:       0.00 B/s | Current DISK WRITE:       0.00 B/s
      TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN      IO    COMMAND
        1 be/4 root        0.00 B/s    0.00 B/s ?unavailable?  systemd
       23 be/3 root        0.00 B/s    0.00 B/s ?unavailable?  systemd-journald
      171 be/4 messageb    0.00 B/s    0.00 B/s ?unavailable?  @dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only
      174 be/4 root        0.00 B/s    0.00 B/s ?unavailable?  systemd-logind
     1426 be/4 systemd-    0.00 B/s    0.00 B/s ?unavailable?  systemd-timesyncd
     1427 be/4 systemd-    0.00 B/s    0.00 B/s ?unavailable?  systemd-timesyncd [sd-resolve]
     3365 be/4 syslog      0.00 B/s    0.00 B/s ?unavailable?  rsyslogd -n -iNONE
     3367 be/4 syslog      0.00 B/s    0.00 B/s ?unavailable?  rsyslogd -n -iNONE [in:imuxsock]
     3368 be/4 syslog      0.00 B/s    0.00 B/s ?unavailable?  rsyslogd -n -iNONE [in:imklog]
     3369 be/4 syslog      0.00 B/s    0.00 B/s ?unavailable?  rsyslogd -n -iNONE [rs:main Q:Reg]
     3846 be/4 root        0.00 B/s    0.00 B/s ?unavailable?  sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
     7319 be/4 root        0.00 B/s    0.00 B/s ?unavailable?  cron -f -P
    10215 be/4 root        0.00 B/s    0.00 B/s ?unavailable?  sudo iotop -b -n 1
    10221 be/4 root        0.00 B/s    0.00 B/s ?unavailable?  python3 /usr/sbin/iotop -b -n 1

    The DISK READ and DISK WRITE columns show per-process throughput, and the IO column reflects time spent waiting on disk operations when the kernel exposes task statistics.

  7. Filter the view to only processes currently performing disk I/O by using the --only option with iotop.
    $ sudo iotop -b -n 1 --only
    Total DISK READ:         0.00 B/s | Total DISK WRITE:         0.00 B/s
    Current DISK READ:       0.00 B/s | Current DISK WRITE:       0.00 B/s
      TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN      IO    COMMAND

    Omit --only to display idle processes as well when the filtered view is empty.

  8. Install ioping to measure disk latency and responsiveness on Ubuntu and other Debian-based systems.
    $ sudo apt update
    Hit:1 http://ports.ubuntu.com/ubuntu-ports noble InRelease
    Hit:2 http://ports.ubuntu.com/ubuntu-ports noble-updates InRelease
    Hit:3 http://ports.ubuntu.com/ubuntu-ports noble-backports InRelease
    Hit:4 http://ports.ubuntu.com/ubuntu-ports noble-security InRelease
    Reading package lists...
    Building dependency tree...
    Reading state information...
    5 packages can be upgraded. Run 'apt list --upgradable' to see them.
    $ sudo apt install --assume-yes ioping
    Reading package lists...
    Building dependency tree...
    Reading state information...
    The following NEW packages will be installed:
      ioping
    0 upgraded, 1 newly installed, 0 to remove and 5 not upgraded.
    Need to get 17.8 kB of archives.
    After this operation, 86.0 kB of additional disk space will be used.
    Get:1 http://ports.ubuntu.com/ubuntu-ports noble/universe arm64 ioping arm64 1.3-1 [17.8 kB]
    Fetched 17.8 kB in 1s (20.5 kB/s)
    Selecting previously unselected package ioping.
    (Reading database ... (Reading database ... 5%(Reading database ... 10%(Reading database ... 15%(Reading database ... 20%(Reading database ... 25%(Reading database ... 30%(Reading database ... 35%(Reading database ... 40%(Reading database ... 45%(Reading database ... 50%(Reading database ... 55%(Reading database ... 60%(Reading database ... 65%(Reading database ... 70%(Reading database ... 75%(Reading database ... 80%(Reading database ... 85%(Reading database ... 90%(Reading database ... 95%(Reading database ... 100%(Reading database ... 18181 files and directories currently installed.)
    Preparing to unpack .../ioping_1.3-1_arm64.deb ...
    Unpacking ioping (1.3-1) ...
    Setting up ioping (1.3-1) ...

    On RHEL, CentOS, and Fedora, install using sudo dnf install --assumeyes ioping.

  9. Measure latency for a block device or directory by running ioping against the desired target.
    $ sudo ioping -c 6 /dev/vdb
    4 KiB <<< /dev/vdb (block device 606.5 MiB): request=1 time=119.4 us (warmup)
    4 KiB <<< /dev/vdb (block device 606.5 MiB): request=2 time=251.9 us
    4 KiB <<< /dev/vdb (block device 606.5 MiB): request=3 time=639.8 us
    4 KiB <<< /dev/vdb (block device 606.5 MiB): request=4 time=142.5 us
    4 KiB <<< /dev/vdb (block device 606.5 MiB): request=5 time=596.0 us
    4 KiB <<< /dev/vdb (block device 606.5 MiB): request=6 time=305.9 us
    
    --- /dev/vdb (block device 606.5 MiB) ioping statistics ---
    5 requests completed in 1.94 ms, 20 KiB read, 2.58 k iops, 10.1 MiB/s
    generated 6 requests in 5.01 s, 24 KiB, 1 iops, 4.79 KiB/s
    min/avg/max/mdev = 142.5 us / 387.2 us / 639.8 us / 196.1 us

    High-frequency latency tests on busy or fragile storage can increase load and may stress underlying disks if run for long periods.

  10. Verify monitoring by performing a disk-heavy operation, such as copying a large file, and confirming that throughput and latency values increase in iostat, iotop, and ioping while the operation is in progress.

    Use simple commands such as cp largefile.img /tmp/ in another terminal to generate test disk activity when validating the monitoring setup.