Created
May 20, 2020 08:56
-
-
Save z4yx/0cf2db35d811fe19e518c12a6cfef4ca to your computer and use it in GitHub Desktop.
ketanarlulkar/hdparm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# | |
# SATA SSD free-space TRIM utility, by Mark Lord <mlord@pobox.com> | |
VERSION=3.6 | |
# 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 "Allocating 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 $? |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment