3
# SSD TRIM utility for live RAID1 mirrored ext4 drives.
5
# By Chris Caputo. Adapted from wiper.sh (ver 2.6) by Mark Lord.
9
# Copyright (C) 2010-2012 Chris Caputo. All rights reserved.
11
# This program is free software; you can redistribute it and/or
12
# modify it under the terms of the GNU General Public License Version 2,
13
# as published by the Free Software Foundation.
15
# This program is distributed in the hope that it would be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
# GNU General Public License for more details.
20
# You should have received a copy of the GNU General Public License
21
# along with this program; if not, write to the Free Software Foundation,
22
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24
function usage_error(){
26
echo " ${0##*/} [--verbose] [--commit] [--reserve=#megs] [--max-ranges=#ranges] <raid_dev> <fsdir>"
28
echo " ${0##*/} --verbose --commit --reserve=100 --max-ranges=512 md0 /"
29
echo " ${0##*/} --verbose --verbose md1 /boot"
31
echo "Note: For best results, this script should be run on each ext4-based filesystem present on a RAID1 array."
36
echo "${0##*/}: TRIM utility for live RAID1 ext4 SATA SSDs, version $VERSION, by Chris Caputo, based on Mark Lord's wiper.sh."
39
## Parameter parsing for the main script.
49
while [ $argc -gt 0 ]; do
50
if [ "$1" = "--commit" ]; then
52
elif [ "$1" = "--verbose" ]; then
53
verbose=$((verbose + 1))
54
elif [[ "$1" =~ --reserve= ]]; then
55
reservemegs=${1##--reserve=}
56
elif [[ "$1" =~ --max-ranges= ]]; then
57
max_ranges=${1##--max-ranges=}
58
elif [ "$1" = "" ]; then
61
if [ "$raiddev" = "" ]; then
63
elif [ "$fsdir" = "" ]; then
66
echo "$1: too many arguments, aborting."
73
[ "$raiddev" = "" ] && usage_error
74
[ "$fsdir" = "" ] && usage_error
76
## Check --reserve number.
79
isdigit () # Tests whether *entire string* is numerical.
80
{ # In other words, tests for integer variable.
81
[ $# -eq 1 ] || return 1
84
*[!0-9]*|"") return 1;;
89
if ! isdigit "$reservemegs" ; then
90
echo "'$reservemegs' is not numerical"
93
if ! isdigit "$max_ranges" ; then
94
echo "'$max_ranges' is not numerical"
98
if [ $reservemegs -eq 0 ]; then
99
echo "Reserve defaulting to 10 megabytes."
102
reservekilos=$((reservemegs * 1024))
104
## Find a required program, or else give a nicer error message than we'd otherwise see:
106
function find_prog(){
108
if [ ! -x "$prog" ]; then
110
p=`type -f -P "$prog" 2>/dev/null`
111
if [ "$p" = "" ]; then
112
echo "$1: needed but not found, aborting."
116
[ $verbose -gt 0 ] && echo " --> using $prog instead of $1"
121
## Ensure we have most of the necessary utilities available before trying to proceed:
123
hash -r ## Refresh bash's cached PATH entries
124
HDPARM=`find_prog /sbin/hdparm` || exit 1
125
GAWK=`find_prog /usr/bin/gawk` || exit 1
126
GREP=`find_prog /bin/grep` || exit 1
127
ID=`find_prog /usr/bin/id` || exit 1
128
LS=`find_prog /bin/ls` || exit 1
129
DF=`find_prog /bin/df` || exit 1
130
RM=`find_prog /bin/rm` || exit 1
132
[ $verbose -gt 1 ] && HDPARM="$HDPARM --verbose"
134
## I suppose this will confuse the three SELinux users out there:
136
if [ `$ID -u` -ne 0 ]; then
137
echo "Only the super-user can use this (try \"sudo $0\" instead), aborting."
141
## We need a very modern hdparm, for its --fallocate and --trim-sector-ranges-stdin flags:
142
## Version 9.25 added automatic determination of safe max-size of TRIM commands.
144
HDPVER=`$HDPARM -V | $GAWK '{gsub("[^0-9.]","",$2); if ($2 > 0) print ($2 * 100); else print 0; exit(0)}'`
145
if [ $HDPVER -lt 925 ]; then
146
echo "$HDPARM: version >= 9.25 is required, aborting."
150
## Check that this is a RAID1 device.
152
if ! $GREP raid1 /sys/block/$raiddev/md/level 1>/dev/null ; then
153
echo "$raiddev is not a RAID1 array."
157
## Get list of slave devices in the RAID1 mirror.
159
slaves=(`$LS /sys/block/$raiddev/slaves`)
160
#slaves=(sda sdb sdc)
163
## Check for DEVTYPE disk and TRIM support on each slave.
166
for slave in "${slaves[@]}"
168
# Check that slave is of DEVTYPE disk.
169
if ! $GREP "DEVTYPE=disk" /sys/block/$slave/uevent 1>/dev/null ; then
170
echo "$slave is not a whole disk. This program only works with full-disk RAID1, not RAID1 partitions."
174
# Check that slave has TRIM support. Exclude if not.
175
if ! $HDPARM -I /dev/$slave | $GREP -i '[ ][*][ ]*Data Set Management TRIM supported' &>/dev/null ; then
176
echo "$slave doesn't appear to support TRIM, per $HDPARM. Excluding."
180
let "index = $index + 1"
182
if [ "${slaves[0]}" = "" ]; then
183
echo "No constituent of $raiddev array supports TRIM. Aborting."
187
## Check that fsdir is on an ext4 volume.
189
lines=`$DF --type=ext4 $fsdir 2>/dev/null | $GREP -v ^Filesystem | wc -l`
190
if [ $lines -ne 1 ]; then
191
echo "'$fsdir' does not appear to be on an ext4 filesystem. Aborting."
195
## Check that fsdir is a directory.
197
if [ ! -d $fsdir ]; then
198
echo "'$fsdir' is not a directory. Aborting."
202
## Check free space & calculate tmpfile size.
204
freesize=`$DF -P -B 1024 $fsdir | $GAWK '{r=$4}END{print r}'`
205
if [ "$freesize" = "" ]; then
206
echo "'$fsdir' is unknown to '$DF'. Aborting."
209
if [ $freesize -lt $reservekilos ]; then
210
echo "'$fsdir' available space of $freesize KB is less than the $reservekilos KB to be reserved for the TRIM operation. Aborting." >&2
213
tmpsize=$((freesize - reservekilos))
214
tmpfile="$fsdir/${0##*/}_TMPFILE.$$"
216
## Clean up tmpfile (if any) and exit:
218
function do_cleanup(){
219
if [ -e $tmpfile ]; then
220
echo "Removing temporary file '$tmpfile'..."
222
if [ -e $tmpfile ]; then
223
echo "Failed to remove '$tmpfile'!!!"
226
[ $1 -eq 0 ] && echo "Done."
227
[ $1 -eq 0 ] || echo "Aborted." >&2
231
## Prepare signal handling, in case we get interrupted while $tmpfile exists:
237
trap do_abort SIGTERM
238
trap do_abort SIGQUIT
241
trap do_abort SIGPIPE
244
## This is where we finally discover whether the filesystem actually
245
## supports --fallocate or not. Some folks will be disappointed here.
247
## Note that --fallocate does not actually write any file data to fsdev,
248
## but rather simply allocates formerly-free space to the tmpfile.
250
echo -n "Creating temporary file (${tmpsize} KB '$tmpfile') ... "
251
if ! $HDPARM --fallocate "${tmpsize}" $tmpfile ; then
252
echo "This kernel may not support 'fallocate'. Aborting."
257
## Verify that slaves and RAID1 mirror have same base LBA. First add a test
258
## string to the tmpfile. "date" is used since it is ever changing.
261
echo "$TESTSTR" >> $tmpfile
262
sync # this is critical
263
SECTOR_BYTES=`$HDPARM --fibmap $tmpfile | \
264
$GREP "byte sectors" | \
266
LAST_EXTENT_SECTOR_COUNT=`$HDPARM --fibmap $tmpfile | \
269
LAST_EXTENT_LBA=`$HDPARM --fibmap $tmpfile | tail -1 | $GAWK '{print $2}'`
271
## Verify the test string is in the extent read.
272
if ! dd iflag=direct status=noxfer bs=$SECTOR_BYTES count=$LAST_EXTENT_SECTOR_COUNT skip=$LAST_EXTENT_LBA if=/dev/$raiddev 2>/dev/null | $GREP "$TESTSTR" &>/dev/null ; then
273
echo "Test string was not found in last extent of tmpfile, as it should have been. Aborting."
277
## Now compare the mirror and the slaves to make sure they have the same data at the same LBA.
279
refchksum=`dd iflag=direct status=noxfer bs=$SECTOR_BYTES count=$LAST_EXTENT_SECTOR_COUNT skip=$LAST_EXTENT_LBA if=/dev/$raiddev 2>/dev/null | sha1sum`
281
for slave in "${slaves[@]}"
283
chksum=`dd iflag=direct status=noxfer bs=$SECTOR_BYTES count=$LAST_EXTENT_SECTOR_COUNT skip=$LAST_EXTENT_LBA if=/dev/$slave 2>/dev/null | sha1sum`
285
if [ "$chksum" != "$refchksum" ]; then
286
echo "Direct I/O of last extent of tmpfile on $slave doesn't match that of $raiddev. Excluding."
290
let "index = $index + 1"
292
if [ "${slaves[0]}" = "" ]; then
293
echo "No constituent of $raiddev array has a matching checksum. Aborting."
297
echo "TRIMable constituents of $raiddev: ${slaves[@]}"
299
## If they specified "--commit" on the command line, then prompt for confirmation first:
301
if [ "$commit" = "yes" ]; then
302
echo "Beginning TRIM operations..."
304
echo "This will be a DRY-RUN only. Use --commit to do it for real."
305
echo "Simulating TRIM operations..."
307
get_trimlist="$HDPARM --fibmap $tmpfile"
308
[ $verbose -gt 0 ] && echo "get_trimlist=$get_trimlist"
311
## Begin gawk program
313
function append_range (lba,count ,this_count){
316
this_count = (count > 65535) ? 65535 : count
317
printf "%u:%u \n", lba, this_count
319
printf "%u:%u ", lba, this_count > "/dev/stderr"
325
{ ## Output from "hdparm --fibmap", in absolute sectors:
326
if (NF == 4 && $2 ~ "^[1-9][0-9]*$")
332
printf "\n" > "/dev/stderr"
333
if (err == 0 && commit != "yes")
334
printf "(dry-run) trimming %u sectors from %u ranges\n", nsectors, nranges > "/dev/stderr"
339
## Run TRIM on each slave. Batch as requested.
342
for slave in "${slaves[@]}"
344
echo "TRIM beginning on $slave..."
346
if [ "$commit" = "yes" ]; then
347
TRIM="$HDPARM --please-destroy-my-drive \
348
--trim-sector-ranges-stdin /dev/$slave"
353
## Different SSD's have a different maximum number of ranges they'll
354
## accept in a single TRIM command.
355
if [ $max_ranges -eq 0 ] ; then
356
model=`$HDPARM -I /dev/$slave | $GAWK '/Model Number/ { print $NF }'`
358
SSDSA[12]*) slave_max_range=512 ;; # Intel X18-M/X25-M
359
OCZ-VERTEX2) slave_max_range=64 ;; # OCZ Vertex2
360
*) slave_max_range=65535
363
slave_max_range=$max_ranges
365
[ $verbose -gt 0 ] && echo "$slave: max-ranges = $slave_max_range"
367
$get_trimlist 2>/dev/null | $GAWK \
368
-v commit="$commit" \
369
-v verbose="$verbose" \
375
if (( i <= $slave_max_range )); then
376
ranges=$ranges" "$range
378
[ $verbose -gt 0 ] && echo -e "Trim ranges:" $ranges
381
if [ $ret -ne 0 ] ; then
388
[ $verbose -gt 0 ] && echo -e "Trim ranges:" $ranges
391
if [ $ret -ne 0 ] ; then
398
if [ $ret -ne 0 ] ; then
399
echo "TRIM failed on $slave. Aborting."
402
echo "TRIM finished successfully on $slave."