Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 866 lines (814 sloc) 27.3 KB
#!/bin/bash
#
# SATA SSD free-space TRIM utility, by Mark Lord <mlord@pobox.com>
VERSION=3.5
# Copyright (C) 2009-2010 Mark Lord. All rights reserved.
#
# Contains hfsplus and ntfs code contributed by Heiko Wegeler <heiko.wegeler@googlemail.com>.
# Package sleuthkit version >=3.1.1 is required for HFS+. Package ntfs-3g and ntfsprogs is required for NTFS.
#
# Requires gawk, a really-recent hdparm, and various other programs.
# This needs to be redone entirely in C, for 64-bit math, someday.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License Version 2,
# as published by the Free Software Foundation.
#
# This program is distributed in the hope that it would be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Note for OCZ Vertex-LE users: the drive firmware will error when
# attempting to trim the final sector of the drive. To avoid this,
# partition the drive such that the final sector is not used.
export LANG=C
## The usual terse usage information:
##
function usage_error(){
echo >&2
echo "Linux tune-up (TRIM) utility for SATA SSDs"
echo "Usage: $0 [--verbose] [--commit] <mount_point|block_device>" >&2
echo " Eg: $0 /dev/sda1" >&2
echo >&2
exit 1
}
## Parameter parsing for the main script.
## Yeah, we could use getopt here instead, but what fun would that be?
##
echo
echo "${0##*/}: Linux SATA SSD TRIM utility, version $VERSION, by Mark Lord."
export verbose=0
commit=""
destroy_me=""
argc=$#
arg=""
while [ $argc -gt 0 ]; do
if [ "$1" = "--commit" ]; then
commit=yes
elif [ "$1" = "--please-prematurely-wear-out-my-ssd" ]; then
destroy_me=yes
elif [ "$1" = "--verbose" ]; then
verbose=$((verbose + 1))
elif [ "$1" = "" ]; then
usage_error
else
if [ "$arg" != "" ]; then
echo "$1: too many arguments, aborting." >&2
exit 1
fi
arg="$1"
fi
argc=$((argc - 1))
shift
done
[ "$arg" = "" ] && usage_error
## Find a required program, or else give a nicer error message than we'd otherwise see:
##
function find_prog(){
prog="$1"
if [ ! -x "$prog" ]; then
prog="${prog##*/}"
p=`type -f -P "$prog" 2>/dev/null`
if [ "$p" = "" ]; then
[ "$2" != "quiet" ] && echo "$1: needed but not found, aborting." >&2
exit 1
fi
prog="$p"
[ $verbose -gt 0 ] && echo " --> using $prog instead of $1" >&2
fi
echo "$prog"
}
## Ensure we have most of the necessary utilities available before trying to proceed:
##
hash -r ## Refresh bash's cached PATH entries
HDPARM=`find_prog /sbin/hdparm` || exit 1
FIND=`find_prog /usr/bin/find` || exit 1
STAT=`find_prog /usr/bin/stat` || exit 1
GAWK=`find_prog /usr/bin/gawk` || exit 1
BLKID=`find_prog /sbin/blkid` || exit 1
GREP=`find_prog /bin/grep` || exit 1
ID=`find_prog /usr/bin/id` || exit 1
LS=`find_prog /bin/ls` || exit 1
DF=`find_prog /bin/df` || exit 1
RM=`find_prog /bin/rm` || exit 1
STAT=`find_prog /usr/bin/stat` || exit 1
[ $verbose -gt 1 ] && HDPARM="$HDPARM --verbose"
## I suppose this will confuse the three SELinux users out there:
##
if [ `$ID -u` -ne 0 ]; then
echo "Only the super-user can use this (try \"sudo $0\" instead), aborting." >&2
exit 1
fi
## We need a very modern hdparm, for its --fallocate and --trim-sector-ranges-stdin flags:
## Version 9.25 added automatic determination of safe max-size of TRIM commands.
##
HDPVER=`$HDPARM -V | $GAWK '{gsub("[^0-9.]","",$2); if ($2 > 0) print ($2 * 100); else print 0; exit(0)}'`
if [ $HDPVER -lt 925 ]; then
echo "$HDPARM: version >= 9.25 is required, aborting." >&2
exit 1
fi
## Convert relative path "$1" into an absolute pathname, resolving all symlinks:
##
function get_realpath(){
iter=0
p="$1"
while [ -e "$p" -a $iter -lt 100 ]; do
## Strip trailing slashes:
while [ "$p" != "/" -a "$p" != "${p%%/}" ]; do
p="${p%%/}"
done
## Split into directory:leaf portions:
d="${p%/*}"
t="${p##*/}"
## If the split worked, then cd into the directory portion:
if [ "$d" != "" -a "$d" != "$p" ]; then
cd -P "$d" || exit
p="$t"
fi
## If what we have left is a directory, then cd to it and print realpath:
if [ -d "$p" ]; then
cd -P "$p" || exit
pwd -P
exit
## Otherwise if it is a symlink, read the link and loop again:
elif [ -h "$p" ]; then
p="`$LS -ld "$p" | awk '{sub("^[^>]*-[>] *",""); print}'`"
## Otherwise, prefix $p with the cwd path and print it:
elif [ -e "$p" ]; then
[ "${p:0:1}" = "/" ] || p="`pwd -P`/$p"
echo "$p"
exit
fi
iter=$((iter + 1))
done
}
function get_devpath(){
dir="$1"
kdev=`$STAT --format="%04D" "$dir" 2>/dev/null`
[ "$kdev" = "" ] && exit 1
major=$((0x${kdev:0:2}))
minor=$((0x${kdev:2:2}))
$FIND /dev -xdev -type b -exec $LS -ln {} \; | $GAWK -v major="$major," -v minor="$minor" \
'($5 == major && $6 == minor){r=$NF}END{print r}'
}
## Convert "$arg" into an absolute pathname target, with no symlinks or embedded blanks:
target="`get_realpath "$arg"`"
if [ "$target" = "" ]; then
[ "$arg" = "/dev/root" ] && target="`get_devpath /`"
if [ "$target" = "" ]; then
echo "$arg: unable to determine full pathname, aborting." >&2
exit 1
fi
fi
if [ "$target" != "${target##* }" ]; then
echo "\"$target\": pathname has embedded blanks, aborting." >&2
exit 1
fi
## Take a first cut at online/offline determination, based on the target:
##
if [ -d "$target" ]; then
method=online
elif [ -b "$target" ]; then
method=offline
else
echo "$target: not a block device or mount point, aborting." >&2
exit 1
fi
## Find the active mount-point (fsdir) associated with a device ($1: fsdev).
## This is complicated, and probably still buggy, because a single
## device can show up under *multiple* mount points in /proc/mounts.
##
function get_fsdir(){
rw=""
r=""
while read -a m ; do
pdev="${m[0]}"
[ "$pdev" = "$1" ] || pdev="`get_realpath "$pdev"`"
if [ "$pdev" = "$1" ]; then
if [ "$rw" != "rw" ]; then
rw="${m[3]:0:2}"
r="${m[1]}"
fi
fi
#echo "$pdev ${m[1]} ${m[2]} ${m[3]}"
done
echo -n "$r"
}
## Find the device (fsdev) associated with a mount point ($1: fsdir).
## Since mounts can be stacked on top of each other, we return the
## one from the last occurance in the list from /proc/mounts.
##
function get_fsdev(){ ## from fsdir
get_realpath "`$GAWK -v p="$1" '{if ($2 == p) r=$1} END{print r}' < /proc/mounts`"
}
## Find the r/w or r/o status (fsmode) of a filesystem mount point ($1: fsdir)
## We get it from the last occurance of the mount point in the list from /proc/mounts,
## and convert it to a longer human-readable string.
##
function get_fsmode(){ ## from fsdir
mode="`$GAWK -v p="$1" '{if ($2 == p) r=substr($4,1,2)} END{print r}' < /proc/mounts`"
if [ "$mode" = "ro" ]; then
echo "read-only"
elif [ "$mode" = "rw" ]; then
echo "read-write"
else
echo "$fsdir: unable to determine mount status, aborting." >&2
exit 1
fi
}
## Try and determine the device name associated with the root filesystem.
## This is nearly impossible to do in any perfect fashion.
##
## Redhat/Fedora no longer have an rdev command. Silly them.
## So we now implement it internally, below.
##
## match_rootdev *should* work, but on some distros it may find only "/dev/root",
## and "/dev/root" is not usually a real device. We leave it like that for now,
## because that's the pattern such systems also use in /proc/mounts.
## Later, at time of use, we'll try harder to find the real rootdev.
##
## FIXME: apparently this doesn't work on SuSE Linux, though.
## So for there, we'll likely need to read /etc/mtab,
## or be a lot more clever and get it somehow from statfs or something.
## FIXME: or use target from /dev/root symlink for Gentoo as well.
##
function match_rootdev() {
rdev=""
rdevno="$1"
while read bdev ; do
if [ "$rdev" = "" -o "$bdev" != "/dev/root" ]; then
devno=$($STAT -c "0x%t%02T" "$bdev" 2>/dev/null)
[ "$devno" = "$rdevno" ] && rdev="$bdev"
fi
done
echo -n "$rdev"
}
rootdev=$($FIND /dev/ -type b 2>/dev/null | match_rootdev $($STAT -c "0x%D" '/'))
[ $verbose -gt 0 ] && echo "rootdev=$rootdev"
## The user gave us a directory (mount point) to TRIM,
## which implies that we will be doing an online TRIM
## using --fallocate and --fibmap to find the free extents.
## Do some preliminary correctness/feasibility checks on fsdir:
##
if [ "$method" = "online" ]; then
## Ensure fsdir exists and is accessible to us:
fsdir="$target"
cd "$fsdir" || exit 1
if [ "$fsdir" = "/" ]; then
fsdev="$rootdev"
else
## Figure out what device holds the filesystem.
fsdev="`get_fsdev $fsdir`"
if [ "$fsdev" = "" ]; then
echo "$fsdir: not found in /proc/mounts, aborting." >&2
exit 1
fi
fi
## The root filesystem may show up as the phoney "/dev/root" device
## in /proc/mounts (ugh). So if we see that, then substitute the rootdev
## that $DF gave us earlier. But $DF may have the same problem (double ugh).
##
[ ! -e "$fsdev" -a "$fsdev" = "/dev/root" ] && fsdev="$rootdev"
## Ensure that fsdev exists and is a block device:
if [ ! -e "$fsdev" ]; then
if [ "$fsdev" != "/dev/root" ]; then
echo "$fsdev: not found" >&2
exit 1
fi
if [ "$rootdev" = "" ]; then
echo "$fsdev: not found" >&2
exit 1
fi
fsdev="$rootdev"
fi
if [ ! -b "$fsdev" ]; then
echo "$fsdev: not a block device" >&2
exit 1
fi
## If it is mounted read-only, we must switch to doing an "offline" trim of fsdev:
fsmode="`get_fsmode $fsdir`" || exit 1
[ $verbose -gt 0 ] && echo "fsmode1: fsmode=$fsmode"
[ "$fsmode" = "read-only" ] && method=offline
fi
## This is not an "else" clause from the above, because "method" may have changed.
## For offline TRIM, we need the block device, and it cannot be mounted read-write:
##
if [ "$method" = "offline" ]; then
## We might already have fsdev/fsdir from above; if not, we need to find them.
if [ "$fsdev" = "" -o "$fsdir" = "" ]; then
fsdev="$target"
fsdir="`get_fsdir "$fsdev" < /proc/mounts`"
## More weirdness for /dev/root in /proc/mounts:
if [ "$fsdir" = "" -a "$fsdev" = "$rootdev" ]; then
fsdir="`get_fsdir /dev/root < /proc/mounts`"
if [ "$fsdir" = "" ]; then
rdev="`get_devpath /`"
[ "$rdev" != "" ] && fsdir="`get_fsdir "$rdev" < /proc/mounts`"
fi
fi
fi
## If the filesystem is truly not-mounted, then fsdir will still be empty here.
## It could be mounted, though. Read-only is fine, but read-write means we need
## to switch gears and do an "online" TRIM instead of an "offline" TRIM.
##
if [ "$fsdir" != "" ]; then
fsmode="`get_fsmode $fsdir`" || exit 1
[ $verbose -gt 0 ] && echo "fsmode2: fsmode=$fsmode"
if [ "$fsmode" = "read-write" ]; then
method=online
cd "$fsdir" || exit 1
fi
fi
fi
## Use $LS to find the major number of a block device:
##
function get_major(){
$LS -ln "$1" | $GAWK '{print gensub(",","",1,$5)}'
}
## At this point, we have finalized our selection of online vs. offline,
## and we definitely know the fsdev, as well as the fsdir (fsdir="" if not-mounted).
##
## Now guess at the underlying rawdev name, which could be exactly the same as fsdev.
## Then determine whether or not rawdev claims support for TRIM commands.
## Note that some devices lie about support, and later reject the TRIM commands.
##
rawdev=`echo $fsdev | $GAWK '{print gensub("[0-9]*$","","g")}'`
rawdev="`get_realpath "$rawdev"`"
if [ ! -e "$rawdev" ]; then
rawdev=""
elif [ ! -b "$rawdev" ]; then
rawdev=""
elif [ "`get_major $fsdev`" -ne "`get_major $rawdev`" ]; then ## sanity check
rawdev=""
else
## "SCSI" drives only; no LVM confusion for now:
maj="$(get_major $fsdev)"
maj_ok=0
for scsi_major in 8 65 66 67 68 69 70 71 ; do
[ "$maj" = "$scsi_major" ] && maj_ok=1
done
if [ $maj_ok -eq 0 ]; then
echo "$rawdev: does not appear to be a SCSI/SATA SSD, aborting." >&2
exit 1
fi
if ! $HDPARM -I $rawdev | $GREP -i '[ ][*][ ]*Data Set Management TRIM supported' &>/dev/null ; then
if [ "$commit" = "yes" ]; then
echo "$rawdev: DSM/TRIM command not supported, aborting." >&2
exit 1
fi
echo "$rawdev: DSM/TRIM command not supported (continuing with dry-run)." >&2
fi
fi
if [ "$rawdev" = "" ]; then
echo "$fsdev: unable to reliably determine the underlying physical device name, aborting" >&2
exit 1
fi
## We also need to know the offset of fsdev from the beginning of rawdev,
## because TRIM requires absolute sector numbers within rawdev:
##
fsoffset=`$HDPARM -g "$fsdev" | $GAWK 'END {print $NF}'`
## Next step is to determine what type of filesystem we are dealing with (fstype):
##
if [ "$fsdir" = "" ]; then
## Not mounted: use $BLKID to determine the fstype of fsdev:
fstype=`$BLKID -w /dev/null -c /dev/null $fsdev 2>/dev/null | \
$GAWK '/ TYPE=".*"/{sub("^.* TYPE=\"",""); sub("[\" ][\" ]*.*$",""); print}'`
[ $verbose -gt 0 ] && echo "$fsdev: fstype=$fstype"
else
## Mounted: we could just use $BLKID here, too, but it's safer to use /proc/mounts directly:
fstype="`$GAWK -v p="$fsdir" '{if ($2 == p) r=$3} END{print r}' < /proc/mounts`"
[ $verbose -gt 0 ] && echo "$fsdir: fstype=$fstype"
fi
if [ "$fstype" = "" ]; then
echo "$fsdev: unable to determine filesystem type, aborting." >&2
exit 1
fi
## Some helper funcs and vars for use with the xfs filesystem tools:
##
function xfs_abort(){
echo "$fsdev: unable to determine xfs filesystem ${1-parameters}, aborting." >&2
exit 1
}
function xfs_trimlist(){
$XFS_DB -r -c "freesp -d" "$fsdev" ## couldn't get this to work inline
}
xfs_agoffsets=""
xfs_blksects=0
## We used to allow single-drive btrfs here, but it stopped working in linux-2.6.31,
## and Chris Mason says "unsafe at any speed" really. So it's been dropped now.
##
if [ "$fstype" = "btrfs" ]; then ## hdparm --fibmap fails, due to fake 0:xx device nodes
echo "$target: btrfs filesystem type not supported (cannot determine physical devices), aborting." >&2
exit 1
fi
## Now figure out whether we can actually do TRIM on this type of filesystem:
##
if [ "$method" = "online" ]; then
## Print sensible error messages for some common situations,
## rather than failing with more confusing messages later on..
##
if [ "$fstype" = "ext2" -o "$fstype" = "ext3" ]; then ## No --fallocate support
echo "$target: cannot TRIM $fstype filesystem when mounted read-write, aborting." >&2
exit 1
fi
## Figure out if we have enough free space to even attempt TRIM:
##
freesize=`$DF -P -B 1024 . | $GAWK '{r=$4}END{print r}'`
if [ "$freesize" = "" ]; then
echo "$fsdev: unknown to '$DF'"
exit 1
fi
if [ $freesize -lt 15000 ]; then
echo "$target: filesystem too full for TRIM, aborting." >&2
exit 1
fi
## Figure out how much space to --fallocate (later), keeping in mind
## that this is a live filesystem, and we need to leave some space for
## other concurrent activities, as well as for filesystem overhead (metadata).
## So, reserve at least 1% or 7500 KB, whichever is larger:
##
reserved=$((freesize / 100))
[ $reserved -lt 7500 ] && reserved=7500
[ $verbose -gt 0 ] && echo "freesize = ${freesize} KB, reserved = ${reserved} KB"
tmpsize=$((freesize - reserved))
tmpfile="WIPER_TMPFILE.$$"
get_trimlist="$HDPARM --fibmap $tmpfile"
else
## We can only do offline TRIM on filesystems that we "know" about here.
## Currently, this includes the ext2/3/4 family, xfs, and reiserfs.
## The first step for any of these is to ensure that the filesystem is "clean",
## and immediately abort if it is not.
##
get_trimlist=""
if [ "$fstype" = "ext2" -o "$fstype" = "ext3" -o "$fstype" = "ext4" ]; then
DUMPE2FS=`find_prog /sbin/dumpe2fs` || exit 1
fstate="`$DUMPE2FS $fsdev 2>/dev/null | $GAWK '/^[Ff]ilesystem state:/{print $NF}' 2>/dev/null`"
if [ "$fstate" != "clean" ]; then
echo "$target: filesystem not clean, please run \"e2fsck $fsdev\" first, aborting." >&2
exit 1
fi
get_trimlist="$DUMPE2FS $fsdev"
elif [ "$fstype" = "xfs" ]; then
XFS_DB=`find_prog /sbin/xfs_db` || exit 1
XFS_REPAIR=`find_prog /sbin/xfs_repair` || exit 1
if ! $XFS_REPAIR -n "$fsdev" &>/dev/null ; then
echo "$fsdev: filesystem not clean, please run \"xfs_repair $fsdev\" first, aborting." >&2
exit 1
fi
## For xfs, life is more complex than with ext2/3/4 above.
## The $XFS_DB tool does not return absolute block numbers for freespace,
## but rather gives them as relative to it's allocation groups (ag's).
## So, we'll need to interogate it for the offset of each ag within the filesystem.
## The agoffsets are extracted from $XFS_DB as sector offsets within the fsdev.
##
agcount=`$XFS_DB -r -c "sb" -c "print agcount" "$fsdev" | $GAWK '{print 0 + $NF}'`
[ "$agcount" = "" -o "$agcount" = "0" ] && xfs_abort "agcount"
xfs_agoffsets=
i=0
while [ $i -lt $agcount ]; do
agoffset=`$XFS_DB -r -c "sb" -c "convert agno $i daddr" "$fsdev" \
| $GAWK '{print 0 + gensub("[( )]","","g",$2)}'`
[ "$agoffset" = "" ] && xfs_abort "agoffset-$i"
[ $i -gt 0 ] && [ $agoffset -le ${xfs_agoffsets##* } ] && xfs_abort "agoffset[$i]"
xfs_agoffsets="$xfs_agoffsets $agoffset"
i=$((i + 1))
done
xfs_agoffsets="${xfs_agoffsets:1}" ## strip leading space
## We also need xfs_blksects for later, because freespace gets listed as block numbers.
##
blksize=`$XFS_DB -r -c "sb" -c "print blocksize" "$fsdev" | $GAWK '{print 0 + $NF}'`
[ "$blksize" = "" -o "$blksize" = "0" ] && xfs_abort "block size"
xfs_blksects=$((blksize/512))
get_trimlist="xfs_trimlist"
elif [ "$fstype" = "reiserfs" ]; then
DEBUGREISERFS=`find_prog /sbin/debugreiserfs` || exit 1
( $DEBUGREISERFS $fsdev | $GREP '^Filesystem state:.consistent' ) &> /dev/null
if [ $? -ne 0 ]; then
echo "Please run fsck.reiserfs first, aborting." >&2
exit 1
fi
get_trimlist="$DEBUGREISERFS -m $fsdev"
elif [ "$fstype" = "hfsplus" ]; then
OD=`find_prog /usr/bin/od` || exit 1
TR=`find_prog /usr/bin/tr` || exit 1
#check sleuthkit
FSSTAT=`find_prog /usr/local/bin/fsstat`
if [ "$?" = "1" ]; then
echo "fsstat and icat from package sleuthkit >= 3.1.1 is required for hfsplus."
exit 1
fi
ICAT=`find_prog /usr/local/bin/icat`
if [ "`$ICAT -f list 2>/dev/stdout|$GREP HFS+`" = "" ]; then
echo "Wrong icat, version from package sleuthkit >= 3.1.1 is required for hfsplus."
exit 1
fi
#check for unmounted properly
if [ "`$FSSTAT -f hfs $fsdev | $GREP "Volume Unmounted Properly"`" = "" ]; then
echo "Hfsplus volume unmounted improperly!"
exit 1
fi
#check $AllocationFile inode
FFIND=`find_prog /usr/local/bin/ffind`
if [ "`$FFIND -f hfs $fsdev 6`" != "/\$AllocationFile" ]; then
echo "Hfsplus bitmap \$AllocationFile is not inode 6!"
exit 1
fi
#get offset for hfsplus with a wrapper
hfsoffset=`$FSSTAT -f hfs $fsdev | $GREP "File system is embedded in an HFS wrapper at offset "|$TR -d "\t"`
if [ -n "$hfsoffset" ]; then
hfsoffset=${hfsoffset:52}
((fsoffset=fsoffset+hfsoffset))
echo "File system is embedded in an HFS wrapper at offset $hfsoffset"
fi
blksize=`$FSSTAT -f hfs $fsdev | $GREP "Allocation Block Size: "|$TR -d "\t"`
blksize=${blksize:23}
blksects=$((blksize / 512))
#get count of used bytes in $AllocationFile
blkcount=`$FSSTAT -f hfs $fsdev | $GREP "Block Range: 0 - "`
blkcount=${blkcount:17}
bytecount=$((blkcount/blksects))
method="bitmap_offline"
get_trimlist="echo $blksects hfsplus `$ICAT -f hfs $fsdev 6 | $OD -N $bytecount -An -vtu1 -j0 -w1`"
elif [ "$fstype" = "ntfs" ]; then
NTFSINFO=`find_prog /usr/bin/ntfsinfo` || exit 1
NTFSCAT=`find_prog /usr/bin/ntfscat` || exit 1
NTFSPROBE=`find_prog /usr/bin/ntfs-3g.probe` || exit 1
OD=`find_prog /usr/bin/od` || exit 1
TR=`find_prog /usr/bin/tr` || exit 1
#check for unmounted properly
$NTFSPROBE -w $fsdev 2>/dev/null
if [ $? -ne 0 ]; then
echo "$fsdev contains an unclean file system!"
exit 1
fi
#check for volume version
if [ "`$NTFSINFO -m -f $fsdev | $GREP "Volume Version: 3.1"`" = "" ]; then
echo "NTFS volume version must be 3.1!"
exit 1
fi
blksize=`$NTFSINFO -m -f $fsdev | $GREP "Cluster Size: " | $TR -d "\t"`
blksize=${blksize:14}
blksects=$((blksize / 512))
#get count of used bytes in $Bitmap
blkcount=`$NTFSINFO -m -f $fsdev | $GREP "Volume Size in Clusters: " | $TR -d "\t"`
blkcount=${blkcount:25}
bytecount=$((blkcount/blksects))
method="bitmap_offline"
get_trimlist="echo $blksects ntfs `$NTFSCAT $fsdev \\\$Bitmap | $OD -N $bytecount -An -vtu1 -j0 -w1`"
fi
if [ "$get_trimlist" = "" ]; then
echo "$target: offline TRIM not supported for $fstype filesystems, aborting." >&2
exit 1
fi
fi
## All ready. Now let the user know exactly what we intend to do:
##
mountstatus="$fstype non-mounted"
[ "$fsdir" = "" ] || mountstatus="$fstype mounted $fsmode at $fsdir"
echo "Preparing for $method TRIM of free space on $fsdev ($mountstatus)."
## If they specified "--commit" on the command line, then prompt for confirmation first:
##
if [ "$commit" = "yes" ]; then
if [ "$destroy_me" = "" ]; then
echo >/dev/tty
echo -n "This operation could silently destroy your data. Are you sure (y/N)? " >/dev/tty
read yn < /dev/tty
if [ "$yn" != "y" -a "$yn" != "Y" ]; then
echo "Aborting." >&2
exit 1
fi
fi
TRIM="$HDPARM --please-destroy-my-drive --trim-sector-ranges-stdin $rawdev"
else
echo "This will be a DRY-RUN only. Use --commit to do it for real."
TRIM="$GAWK {}"
fi
## Useful in a few places later on:
##
function sync_disks(){
echo -n "Syncing disks.. "
sync
echo
}
## Clean up tmpfile (if any) and exit:
##
function do_cleanup(){
if [ "$method" = "online" ]; then
if [ -e $tmpfile ]; then
echo "Removing temporary file.."
$RM -f $tmpfile
fi
sync_disks
fi
[ $1 -eq 0 ] && echo "Done."
[ $1 -eq 0 ] || echo "Aborted." >&2
exit $1
}
## Prepare signal handling, in case we get interrupted while $tmpfile exists:
##
function do_abort(){
echo
do_cleanup 1
}
trap do_abort SIGTERM
trap do_abort SIGQUIT
trap do_abort SIGINT
trap do_abort SIGHUP
trap do_abort SIGPIPE
## For online TRIM, go ahead and create the huge temporary file.
## This is where we finally discover whether the filesystem actually
## supports --fallocate or not. Some folks will be disappointed here.
##
## Note that --fallocate does not actually write any file data to fsdev,
## but rather simply allocates formerly-free space to the tmpfile.
##
if [ "$method" = "online" ]; then
if [ -e "$tmpfile" ]; then
if ! $RM -f "$tmpfile" ; then
echo "$tmpfile: already exists and could not be removed, aborting." >&2
exit 1
fi
fi
echo -n "Creating temporary file (${tmpsize} KB).. "
if ! $HDPARM --fallocate "${tmpsize}" $tmpfile ; then
echo "$target: this kernel may not support 'fallocate' on a $fstype filesystem, aborting." >&2
exit 1
fi
echo
fi
## Finally, we are now ready to TRIM something!
##
## Feed the "get_trimlist" output into a gawk program which will
## extract the trimable lba-ranges (extents) and batch them together
## into huge --trim-sector-ranges calls.
##
## We are limited by at least one thing when doing this:
## 1. Some device drivers may not support more than 255 sectors
## full of lba:count range data per TRIM command.
## The latest hdparm versions now take care of that automatically.
##
sync_disks
if [ "$commit" = "yes" ]; then
echo "Beginning TRIM operations.."
else
echo "Simulating TRIM operations.."
fi
[ $verbose -gt 0 ] && echo "get_trimlist=$get_trimlist"
## Begin gawk program
GAWKPROG='
BEGIN {
if (xfs_agoffsets != "") {
method = "xfs_offline"
agcount = split(xfs_agoffsets,agoffset," ");
}
}
function append_range (lba,count ,this_count){
nsectors += count;
while (count > 0) {
this_count = (count > 65535) ? 65535 : count
printf "%u:%u ", lba, this_count
if (verbose > 1)
printf "%u:%u ", lba, this_count > "/dev/stderr"
lba += this_count
count -= this_count
nranges++;
}
}
(method == "online") { ## Output from "hdparm --fibmap", in absolute sectors:
if (NF == 4 && $2 ~ "^[1-9][0-9]*$")
append_range($2,$4)
next
}
(method == "xfs_offline") { ## Output from xfs_db:
if (NF == 3 && gensub("[0-9 ]","","g",$0) == "" && $1 < agcount) {
lba = agoffset[1 + $1] + ($2 * xfs_blksects) + fsoffset
count = $3 * xfs_blksects
append_range(lba,count)
}
next
}
(method == "bitmap_offline") {
n = split($0,f)
blksects = f[1]
fstype = f[2]
bitmap_start = 3
range_first = -1 #clusters
range_last = -1
for (i = bitmap_start; i <= n-1; i++) {
if (f[i] == 0) {
if (range_first == -1)
range_first = (i-bitmap_start) * 8
range_last = (i-bitmap_start) * 8 + 7
} else if (f[i] == 255 && range_first > -1){
#printf range_first "-" range_last "\n" > "/dev/stderr"
lba = (range_first * blksects) + fsoffset
count = (range_last - range_first + 1) * blksects
append_range(lba,count)
range_first = -1
range_last = -1
} else {
for (b = 0; b < 8; b++) {
if (fstype == "ntfs")
bit = and(f[i], lshift(1, b)) ? 1 : 0
else #hfsplus
bit = and(f[i], lshift(1, 7-b)) ? 1 : 0
if (bit == 0) {
if (range_first == -1) {
range_first = (i-bitmap_start) * 8 + b
range_last = (i-bitmap_start) * 8 + b
} else
range_last += 1
} else if (range_first > -1) {
#printf range_first "-" range_last " " > "/dev/stderr"
lba = (range_first * blksects) + fsoffset
count = (range_last - range_first + 1) * blksects
if (fstype == "ntfs")
append_range(lba,count)
else if (count > (2 * blksects)) #faster for hfsplus
append_range(lba,count)
range_first = -1
range_last = -1
}
}
}
}
if (range_first > -1){
#printf range_first "-" range_last " " > "/dev/stderr"
lba = (range_first * blksects) + fsoffset
count = (range_last - range_first + 1) * blksects
append_range(lba,count)
}
next
}
/^Block size: *[1-9]/ { ## First stage output from dumpe2fs:
blksects = $NF / 512
next
}
/^Group [0-9][0-9]*:/ { ## Second stage output from dumpe2fs:
in_groups = 1
next
}
/^ *Free blocks: [0-9]/ { ## Bulk of output from dumpe2fs:
if (blksects && in_groups) {
n = split(substr($0,16),f,",* *")
for (i = 1; i <= n; ++i) {
if (f[i] ~ "^[1-9][0-9]*-[1-9][0-9]*$") {
split(f[i],b,"-")
lba = (b[1] * blksects) + fsoffset
count = (b[2] - b[1] + 1) * blksects
append_range(lba,count)
} else if (f[i] ~ "^[1-9][0-9]*$") {
lba = (f[i] * blksects) + fsoffset
count = blksects
append_range(lba,count)
}
}
next
}
}
/^Reiserfs super block/ {
method = "reiserfs"
next
}
/^Blocksize: / {
if (method == "reiserfs") {
blksects = $2 / 512
next
}
}
/^#[0-9][0-9]*:.*Free[(]/ { ## debugreiserfs
if (method == "reiserfs" && blksects > 0) {
n = split($0,f)
for (i = 4; i <= n; ++i) {
if (f[i] ~ "^ *Free[(]") {
if (2 == split(gensub("[^-0-9]","","g",f[i]),b,"-")) {
lba = (b[1] * blksects) + fsoffset
count = (b[2] - b[1] + 1) * blksects
append_range(lba, count)
}
}
}
next
}
}
END {
if (err == 0 && commit != "yes")
printf "(dry-run) trimming %u sectors from %u ranges\n", nsectors, nranges > "/dev/stderr"
exit err
}'
## End gawk program
$get_trimlist 2>/dev/null | $GAWK \
-v commit="$commit" \
-v method="$method" \
-v rawdev="$rawdev" \
-v fsoffset="$fsoffset" \
-v verbose="$verbose" \
-v xfs_blksects="$xfs_blksects" \
-v xfs_agoffsets="$xfs_agoffsets" \
"$GAWKPROG" | $TRIM
do_cleanup $?