Script
#!/usr/bin/env bash
set -euo pipefail
# fio_test.sh
# 修复版 v2: 修复了 set -e 导致的意外退出问题 (read pipe crash)
# 增加了更稳健的错误处理和进度显示
BS_LIST=("4k" "64k" "512k" "1m")
: "${FIO_SIZE:=2G}"
: "${FIO_IODEPTH:=32}"
: "${FIO_NUMJOBS:=1}"
: "${FIO_RUNTIME:=30}"
: "${FIO_RAMP_TIME:=2}"
: "${FIO_DIRECT:=1}"
: "${FIO_IOENGINE:=libaio}"
: "${FIO_TARGET:=}"
usage() {
cat <<EOF
Usage:
sudo $0
Optional environment variables:
FIO_TARGET=/mnt/storage (skip menu and test this mountpoint directly)
FIO_SIZE=2G
FIO_IODEPTH=32
FIO_NUMJOBS=1
FIO_RUNTIME=30 (Seconds per test)
FIO_DIRECT=1
FIO_IOENGINE=libaio
ALLOW_ROOTFS=1
EOF
}
need_cmd() { command -v "$1" >/dev/null 2>&1; }
install_fio_if_missing() {
if need_cmd fio; then return 0; fi
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
echo "ERROR: fio not found. Install with: sudo apt-get update && sudo apt-get install -y fio" >&2
exit 1
fi
apt-get update -y >/dev/null
apt-get install -y fio >/dev/null
}
make_tmpdir() {
local base="$1"
local d="$base/.fio_tmp_$(date +%s)_$$"
mkdir -p "$d"
echo "$d"
}
cleanup() {
if [[ -n "${TMPDIR_PATH:-}" && -d "${TMPDIR_PATH:-}" ]]; then
rm -rf "${TMPDIR_PATH:?}"
fi
}
trap cleanup EXIT INT TERM
tty_print() {
if [[ -e /dev/tty ]]; then
printf "%s" "$*" > /dev/tty
else
printf "%s" "$*" >&2
fi
}
tty_println() { tty_print "$*"$'\n'; }
tty_readline() {
local __varname="$1"
local line=""
if [[ -r /dev/tty ]]; then
IFS= read -r line < /dev/tty || true
else
IFS= read -r line || true
fi
printf -v "$__varname" "%s" "$line"
}
is_writable_dir() {
local d="$1"
[[ -n "$d" && -d "$d" && -w "$d" ]]
}
lsblk_choices_from_f() {
if ! need_cmd lsblk; then
echo "ERROR: lsblk not found (util-linux)." >&2
exit 1
fi
local out
out="$(LC_ALL=C lsblk -fnr -o NAME,FSTYPE,FSVER,SIZE,MOUNTPOINTS 2>/dev/null || true)"
if [[ -z "$out" ]]; then
echo "ERROR: lsblk returned no output." >&2
exit 1
fi
local idx=0
while IFS= read -r line; do
[[ -z "$line" ]] && continue
local name fstype fsver size mps
name="$(awk '{print $1}' <<<"$line")"
fstype="$(awk '{print $2}' <<<"$line")"
fsver="$(awk '{print $3}' <<<"$line")"
size="$(awk '{print $4}' <<<"$line")"
mps="$(cut -d' ' -f5- <<<"$line" | sed 's/^[[:space:]]*//')"
if [[ -n "$mps" ]]; then
idx=$((idx + 1))
printf "%s\t%s\t%s\t%s\t%s\t%s\n" "$idx" "$name" "${fstype:-}" "${fsver:-}" "${size:-}" "$mps"
fi
done <<<"$out"
if (( idx == 0 )); then
echo "ERROR: No mounted filesystems found in lsblk -f output." >&2
exit 1
fi
}
select_target_mountpoint() {
mapfile -t choices < <(lsblk_choices_from_f)
tty_println "Detected mounted targets (select by number):"
tty_println ""
local line idx name fstype fsver size mps
for line in "${choices[@]}"; do
IFS=$'\t' read -r idx name fstype fsver size mps <<<"$line"
[[ -z "$fstype" ]] && fstype="-"
[[ -z "$fsver" ]] && fsver="-"
tty_println " $(printf "%2d" "$idx")) $(printf "%-8s" "$name") $(printf "%-6s" "$fstype") $(printf "%-6s" "$fsver") $(printf "%-8s" "$size") $mps"
done
tty_println ""
tty_print "Select a target by number (1-${#choices[@]}): "
local sel
tty_readline sel
if [[ ! "$sel" =~ ^[0-9]+$ ]] || (( sel < 1 || sel > ${#choices[@]} )); then
echo "ERROR: Invalid selection: ${sel:-<empty>}" >&2
exit 1
fi
IFS=$'\t' read -r idx name fstype fsver size mps <<<"${choices[$((sel - 1))]}"
local mp
mp="$(awk '{print $1}' <<<"$mps")"
if ! is_writable_dir "$mp"; then
echo "ERROR: Selected mountpoint is not writable: $mp (NAME: $name)" >&2
exit 1
fi
if [[ "$mp" == "/" && "${ALLOW_ROOTFS:-0}" != "1" ]]; then
echo "ERROR: Refusing to run on '/' by default." >&2
echo " If you are sure, set ALLOW_ROOTFS=1 and rerun." >&2
exit 1
fi
tty_println ""
tty_println "Selected target: NAME=${name}, mountpoint=${mp}, fstype=${fstype:-}, size=${size:-}"
tty_println ""
echo "$mp"
}
fio_run_terse() {
local rw="$1"
local bs="$2"
local filename="$3"
local err_file="$4"
local out=""
if [[ "${FIO_RUNTIME}" =~ ^[0-9]+$ ]] && (( FIO_RUNTIME > 0 )); then
out="$(fio \
--name=fio_test \
--filename="${filename}" \
--rw="${rw}" \
--bs="${bs}" \
--size="${FIO_SIZE}" \
--ioengine="${FIO_IOENGINE}" \
--direct="${FIO_DIRECT}" \
--iodepth="${FIO_IODEPTH}" \
--numjobs="${FIO_NUMJOBS}" \
--group_reporting \
--randrepeat=0 \
--norandommap=1 \
--invalidate=1 \
--time_based=1 \
--runtime="${FIO_RUNTIME}" \
--ramp_time="${FIO_RAMP_TIME}" \
--output-format=terse \
--terse-version=3 \
2> "${err_file}" || true)"
else
out="$(fio \
--name=fio_test \
--filename="${filename}" \
--rw="${rw}" \
--bs="${bs}" \
--size="${FIO_SIZE}" \
--ioengine="${FIO_IOENGINE}" \
--direct="${FIO_DIRECT}" \
--iodepth="${FIO_IODEPTH}" \
--numjobs="${FIO_NUMJOBS}" \
--group_reporting \
--randrepeat=0 \
--norandommap=1 \
--invalidate=1 \
--time_based=0 \
--output-format=terse \
--terse-version=3 \
2> "${err_file}" || true)"
fi
local line
line="$(printf "%s\n" "$out" | sed '/^[[:space:]]*$/d' | head -n1 || true)"
if [[ -z "$line" ]]; then
# Return empty implies error handled by caller or exit
return 1
fi
echo "$line"
}
# Python parsing script - Left aligned to avoid heredoc indentation errors
parse_terse_v3_script() {
cat <<'PY_SCRIPT'
import sys
try:
line = sys.stdin.read().strip()
if not line:
sys.exit(0) # Empty input is handled by caller
parts = line.split(';')
def fnum(i, default=0.0):
try:
return float(parts[i])
except Exception:
return float(default)
# index 6=read_bw_kib, 7=read_iops
# index 47=write_bw_kib, 48=write_iops
read_bw_kib = fnum(6, 0.0)
read_iops = fnum(7, 0.0)
write_bw_kib = fnum(47, 0.0)
write_iops = fnum(48, 0.0)
# Output: read_bytes read_iops write_bytes write_iops
print(f"{read_bw_kib * 1024.0} {read_iops} {write_bw_kib * 1024.0} {write_iops}")
except Exception:
# On python error, output zeros so bash doesn't crash on read
print("0 0 0 0")
PY_SCRIPT
}
parse_terse_v3() {
python3 -c "$(parse_terse_v3_script)"
}
format_cell() {
local bw_bytes="$1"
local iops="$2"
python3 -c "
import sys
try:
bw = float(sys.argv[1])
iops = float(sys.argv[2])
GiB = 1024**3
MiB = 1024**2
if bw >= GiB:
bw_s = f'{bw/GiB:.2f} GB/s'
else:
bw_s = f'{bw/MiB:.2f} MB/s'
if iops >= 1000:
iops_s = f'{iops/1000:.1f}k'
else:
iops_s = f'{iops:.0f}'
print(f'{bw_s} ({iops_s})')
except:
print('N/A')
" "$bw_bytes" "$iops"
}
main() {
if [[ $# -ne 0 ]]; then
usage
exit 1
fi
install_fio_if_missing
need_cmd python3 || { echo "ERROR: python3 not found." >&2; exit 1; }
local target_dir=""
if [[ -n "$FIO_TARGET" ]]; then
target_dir="$FIO_TARGET"
else
target_dir="$(select_target_mountpoint)"
fi
if ! is_writable_dir "$target_dir"; then
echo "ERROR: Target is not a writable directory: $target_dir" >&2
exit 1
fi
TMPDIR_PATH="$(make_tmpdir "$target_dir")"
local testfile="${TMPDIR_PATH}/fio_testfile.bin"
tty_println "---------------------------------------------------"
tty_println "Initializing: Creating ${FIO_SIZE} test file on ${target_dir}..."
tty_println "(This writes real data, please wait...)"
local prefill_err="${TMPDIR_PATH}/prefill.err"
if ! fio \
--name=prefill \
--filename="${testfile}" \
--rw=write \
--bs=1m \
--size="${FIO_SIZE}" \
--ioengine="${FIO_IOENGINE}" \
--direct="${FIO_DIRECT}" \
--iodepth="${FIO_IODEPTH}" \
--numjobs="${FIO_NUMJOBS}" \
--group_reporting \
--invalidate=1 \
>/dev/null 2>"${prefill_err}"; then
echo "ERROR: fio prefill failed." >&2
echo "---- fio stderr (prefill) ----" >&2
cat "${prefill_err}" >&2
echo "Hints:" >&2
echo " - Try: FIO_DIRECT=0" >&2
echo " - Try: FIO_IOENGINE=sync" >&2
exit 1
fi
# Store results per bs
declare -A RB RI WB WI
tty_println "Starting benchmarks (Runtime limit: ${FIO_RUNTIME}s per test)..."
for bs in "${BS_LIST[@]}"; do
tty_print " -> Testing Block Size: ${bs}... "
local rerr="${TMPDIR_PATH}/randread_${bs}.err"
local werr="${TMPDIR_PATH}/randwrite_${bs}.err"
# --- RANDREAD ---
local rline
rline="$(fio_run_terse "randread" "${bs}" "${testfile}" "${rerr}" || true)"
if [[ -z "$rline" ]]; then
tty_println "FAILED."
echo "ERROR: RandRead failed for ${bs}. Check ${rerr}" >&2
cat "${rerr}" >&2
exit 1
fi
# Safer read: parse to variable first, then read from string
local r_parsed
r_parsed="$(printf "%s" "$rline" | parse_terse_v3)"
local rr_bw rr_iops
read -r rr_bw rr_iops _ _ <<< "${r_parsed:-0 0 0 0}"
# --- RANDWRITE ---
local wline
wline="$(fio_run_terse "randwrite" "${bs}" "${testfile}" "${werr}" || true)"
if [[ -z "$wline" ]]; then
tty_println "FAILED."
echo "ERROR: RandWrite failed for ${bs}. Check ${werr}" >&2
cat "${werr}" >&2
exit 1
fi
local w_parsed
w_parsed="$(printf "%s" "$wline" | parse_terse_v3)"
local ww_bw ww_iops
read -r _ _ ww_bw ww_iops <<< "${w_parsed:-0 0 0 0}"
RB["$bs"]="$rr_bw"; RI["$bs"]="$rr_iops"
WB["$bs"]="$ww_bw"; WI["$bs"]="$ww_iops"
tty_println "Done."
done
# Output generation
tty_println ""
echo "---------------------------------"
echo "Block Size | 4k (IOPS) | 64k (IOPS)"
echo " ------ | --- ---- | ---- ---- "
local r4 r64 w4 w64 t4 t64
r4="$(format_cell "${RB[4k]:-0}" "${RI[4k]:-0}")"
r64="$(format_cell "${RB[64k]:-0}" "${RI[64k]:-0}")"
w4="$(format_cell "${WB[4k]:-0}" "${WI[4k]:-0}")"
w64="$(format_cell "${WB[64k]:-0}" "${WI[64k]:-0}")"
local tb4 tb64
tb4="$(python3 -c "print(float(${RB[4k]:-0}) + float(${WB[4k]:-0}))")"
tb64="$(python3 -c "print(float(${RB[64k]:-0}) + float(${WB[64k]:-0}))")"
local ti4 ti64
ti4="$(python3 -c "print(float(${RI[4k]:-0}) + float(${WI[4k]:-0}))")"
ti64="$(python3 -c "print(float(${RI[64k]:-0}) + float(${WI[64k]:-0}))")"
t4="$(format_cell "$tb4" "$ti4")"
t64="$(format_cell "$tb64" "$ti64")"
printf "Read | %-20s | %-20s\n" "$r4" "$r64"
printf "Write | %-20s | %-20s\n" "$w4" "$w64"
printf "Total | %-20s | %-20s\n" "$t4" "$t64"
echo " | | "
echo "Block Size | 512k (IOPS) | 1m (IOPS)"
echo " ------ | --- ---- | ---- ---- "
local r512 r1m w512 w1m t512 t1m
r512="$(format_cell "${RB[512k]:-0}" "${RI[512k]:-0}")"
r1m="$(format_cell "${RB[1m]:-0}" "${RI[1m]:-0}")"
w512="$(format_cell "${WB[512k]:-0}" "${WI[512k]:-0}")"
w1m="$(format_cell "${WB[1m]:-0}" "${WI[1m]:-0}")"
local tb512 tb1m ti512 ti1m
tb512="$(python3 -c "print(float(${RB[512k]:-0}) + float(${WB[512k]:-0}))")"
tb1m="$(python3 -c "print(float(${RB[1m]:-0}) + float(${WB[1m]:-0}))")"
ti512="$(python3 -c "print(float(${RI[512k]:-0}) + float(${WI[512k]:-0}))")"
ti1m="$(python3 -c "print(float(${RI[1m]:-0}) + float(${WI[1m]:-0}))")"
t512="$(format_cell "$tb512" "$ti512")"
t1m="$(format_cell "$tb1m" "$ti1m")"
printf "Read | %-20s | %-20s\n" "$r512" "$r1m"
printf "Write | %-20s | %-20s\n" "$w512" "$w1m"
printf "Total | %-20s | %-20s\n" "$t512" "$t1m"
}
Disk I/O Benchmark Script – User Manual
1. Overview
This Bash script serves as an automated wrapper for the Flexible I/O Tester (fio). It is designed to benchmark the Random Read and Random Write performance of storage devices across multiple block sizes (4k, 64k, 512k, 1m). The script simplifies the benchmarking process by handling dependency checks, temporary file creation, target selection, and result parsing, ultimately presenting the data in a concise, formatted table.
2. Prerequisites
Before executing the script, ensure the following requirements are met:
- Operating System: Linux (Bash shell required).
- Permissions: Root privileges are required (
sudo) to install dependencies and perform direct I/O operations effectively. - Dependencies:
- Python 3: Required for parsing logs and formatting the output table.
- util-linux: Specifically the
lsblkcommand (standard on most Linux distributions). - fio: The script attempts to automatically install this via
apt-getif missing. On non-Debian/Ubuntu systems, please installfiomanually.
3. Installation
- Save the script content to a file, for example,
fio_benchmark.sh. - Grant execution permissions to the file:
chmod +x fio_benchmark.sh
4. Usage
4.1. Interactive Mode (Default)
Run the script with sudo. The script will detect mounted filesystems and prompt you to select a target.
sudo ./fio_benchmark.sh
Steps:
- The script lists all detected, writable mount points.
- Enter the number corresponding to the desired target drive.
- The script creates a temporary test directory, generates a test file, and begins benchmarking.
- Upon completion, a results table is displayed, and temporary files are automatically deleted.
4.2. Automated / Non-Interactive Mode
To skip the selection menu, define the FIO_TARGET environment variable pointing to the desired directory.
sudo FIO_TARGET=/mnt/data ./fio_benchmark.sh
4.3. Testing the Root Filesystem
By default, the script prevents testing on the root directory (/) to avoid accidental system impact. To override this safety feature, set ALLOW_ROOTFS=1.
sudo ALLOW_ROOTFS=1 FIO_TARGET=/ ./fio_benchmark.sh
5. Configuration
You can customize the benchmark parameters by setting environment variables before running the script.
| Variable | Default | Description |
|---|---|---|
FIO_SIZE | 2G | The size of the test file created. |
FIO_RUNTIME | 30 | Duration (in seconds) for each block size test. |
FIO_IODEPTH | 32 | Number of I/O units to keep in flight against the file. |
FIO_NUMJOBS | 1 | Number of clones (processes/threads) of this job. |
FIO_DIRECT | 1 | If 1, use non-buffered I/O (O_DIRECT). Usually best for hardware benchmarking. |
FIO_IOENGINE | libaio | Defines how the job issues I/O to the file. |
FIO_RAMP_TIME | 2 | Warm-up time (in seconds) before statistics are logged. |
Example: Running a longer, larger test
sudo FIO_SIZE=10G FIO_RUNTIME=60 ./fio_benchmark.sh
6. Output Interpretation
The final output is a matrix displaying throughput and IOPS (Input/Output Operations Per Second).
- Block Size: The size of the data chunk read/written in a single operation. Smaller sizes (4k) test random access performance (IOPS), while larger sizes (1m) test sequential/throughput performance.
- Read / Write: The specific metrics for read or write operations.
- Total: The sum of Read and Write performance (relevant if mixed workloads were configured, though this script tests them sequentially per block size).
Example Output:
Block Size | 4k (IOPS) | 64k (IOPS)
------ | --- ---- | ---- ----
Read | 50.00 MB/s (12.8k)| 450.00 MB/s (7.2k)
Write | 48.00 MB/s (12.2k)| 420.00 MB/s (6.7k)
7. Safety and Cleanup
- Temporary Files: The script creates a hidden directory named
.fio_tmp_<timestamp>_<pid>inside the target directory. - Automatic Cleanup: A
trapfunction is registered to automatically delete this temporary directory upon script exit, interruption (Ctrl+C), or termination. - Data Safety: The script creates a new file (
fio_testfile.bin) inside the temporary directory. It does not overwrite existing user data, provided the script is pointed to a valid directory.
8. Troubleshooting
Error: “fio not found”
- Cause:
fiois not installed, and the script could not install it automatically (likely running on RHEL/CentOS/Fedora or without network). - Solution: Install manually:
sudo yum install fioorsudo pacman -S fio.
Error: “python3 not found”
- Cause: The system is missing the Python 3 interpreter.
- Solution: Install Python 3 via your package manager.
Error: “Refusing to run on ’/’ by default”
- Cause: You selected the root partition without the override flag.
- Solution: See section 4.3 above.
Script exits immediately after “Initializing”
- Cause: The prefill stage failed, often due to file system permissions or lack of free space.
- Solution: Check the error message printed to
stderr. Ensure the target drive has enough free space (default 2GB) and is writable. Try settingFIO_DIRECT=0if the filesystem does not support direct I/O (e.g., zfs/tmpfs in some configurations).