1#!/bin/bash 2# 3# SSD TRIM utility for live RAID1 mirrored ext4 drives. 4# 5# By Chris Caputo. Adapted from wiper.sh (ver 2.6) by Mark Lord. 6 7VERSION=1.5 8 9# Copyright (C) 2010-2012 Chris Caputo. All rights reserved. 10# 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. 14# 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. 19# 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 23 24function usage_error(){ 25 echo "Usage:" 26 echo " ${0##*/} [--verbose] [--commit] [--reserve=#megs] [--max-ranges=#ranges] <raid_dev> <fsdir>" 27 echo "Examples:" 28 echo " ${0##*/} --verbose --commit --reserve=100 --max-ranges=512 md0 /" 29 echo " ${0##*/} --verbose --verbose md1 /boot" 30 echo 31 echo "Note: For best results, this script should be run on each ext4-based filesystem present on a RAID1 array." 32 echo 33 exit 1 34} 35 36echo "${0##*/}: TRIM utility for live RAID1 ext4 SATA SSDs, version $VERSION, by Chris Caputo, based on Mark Lord's wiper.sh." 37echo 38 39## Parameter parsing for the main script. 40## 41 42export verbose=0 43commit="" 44reservemegs=0 45max_ranges=0 46argc=$# 47raiddev="" 48fsdir="" 49while [ $argc -gt 0 ]; do 50 if [ "$1" = "--commit" ]; then 51 commit=yes 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 59 usage_error 60 else 61 if [ "$raiddev" = "" ]; then 62 raiddev=${1##*/} 63 elif [ "$fsdir" = "" ]; then 64 fsdir=$1 65 else 66 echo "$1: too many arguments, aborting." 67 exit 1 68 fi 69 fi 70 argc=$((argc - 1)) 71 shift 72done 73[ "$raiddev" = "" ] && usage_error 74[ "$fsdir" = "" ] && usage_error 75 76## Check --reserve number. 77## 78 79isdigit () # Tests whether *entire string* is numerical. 80{ # In other words, tests for integer variable. 81 [ $# -eq 1 ] || return 1 82 83 case $1 in 84 *[!0-9]*|"") return 1;; 85 *) return 0;; 86 esac 87} 88 89if ! isdigit "$reservemegs" ; then 90 echo "'$reservemegs' is not numerical" 91 exit 1 92fi 93if ! isdigit "$max_ranges" ; then 94 echo "'$max_ranges' is not numerical" 95 exit 1 96fi 97 98if [ $reservemegs -eq 0 ]; then 99 echo "Reserve defaulting to 10 megabytes." 100 reservemegs=10 101fi 102reservekilos=$((reservemegs * 1024)) 103 104## Find a required program, or else give a nicer error message than we'd otherwise see: 105## 106function find_prog(){ 107 prog="$1" 108 if [ ! -x "$prog" ]; then 109 prog="${prog##*/}" 110 p=`type -f -P "$prog" 2>/dev/null` 111 if [ "$p" = "" ]; then 112 echo "$1: needed but not found, aborting." 113 exit 1 114 fi 115 prog="$p" 116 [ $verbose -gt 0 ] && echo " --> using $prog instead of $1" 117 fi 118 echo "$prog" 119} 120 121## Ensure we have most of the necessary utilities available before trying to proceed: 122## 123hash -r ## Refresh bash's cached PATH entries 124HDPARM=`find_prog /sbin/hdparm` || exit 1 125GAWK=`find_prog /usr/bin/gawk` || exit 1 126GREP=`find_prog /bin/grep` || exit 1 127ID=`find_prog /usr/bin/id` || exit 1 128LS=`find_prog /bin/ls` || exit 1 129DF=`find_prog /bin/df` || exit 1 130RM=`find_prog /bin/rm` || exit 1 131 132[ $verbose -gt 1 ] && HDPARM="$HDPARM --verbose" 133 134## I suppose this will confuse the three SELinux users out there: 135## 136if [ `$ID -u` -ne 0 ]; then 137 echo "Only the super-user can use this (try \"sudo $0\" instead), aborting." 138 exit 1 139fi 140 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. 143## 144HDPVER=`$HDPARM -V | $GAWK '{gsub("[^0-9.]","",$2); if ($2 > 0) print ($2 * 100); else print 0; exit(0)}'` 145if [ $HDPVER -lt 925 ]; then 146 echo "$HDPARM: version >= 9.25 is required, aborting." 147 exit 1 148fi 149 150## Check that this is a RAID1 device. 151## 152if ! $GREP raid1 /sys/block/$raiddev/md/level 1>/dev/null ; then 153 echo "$raiddev is not a RAID1 array." 154 exit 1 155fi 156 157## Get list of slave devices in the RAID1 mirror. 158## 159slaves=(`$LS /sys/block/$raiddev/slaves`) 160#slaves=(sda sdb sdc) 161#slaves=(md0) 162 163## Check for DEVTYPE disk and TRIM support on each slave. 164## 165index=0 166for slave in "${slaves[@]}" 167do 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." 171 exit 1 172 fi 173 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." 177 unset slaves[index] 178 fi 179 180 let "index = $index + 1" 181done 182if [ "${slaves[0]}" = "" ]; then 183 echo "No constituent of $raiddev array supports TRIM. Aborting." 184 exit 1 185fi 186 187## Check that fsdir is on an ext4 volume. 188## 189lines=`$DF --type=ext4 $fsdir 2>/dev/null | $GREP -v ^Filesystem | wc -l` 190if [ $lines -ne 1 ]; then 191 echo "'$fsdir' does not appear to be on an ext4 filesystem. Aborting." 192 exit 1 193fi 194 195## Check that fsdir is a directory. 196## 197if [ ! -d $fsdir ]; then 198 echo "'$fsdir' is not a directory. Aborting." 199 exit 1 200fi 201 202## Check free space & calculate tmpfile size. 203## 204freesize=`$DF -P -B 1024 $fsdir | $GAWK '{r=$4}END{print r}'` 205if [ "$freesize" = "" ]; then 206 echo "'$fsdir' is unknown to '$DF'. Aborting." 207 exit 1 208fi 209if [ $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 211 exit 1 212fi 213tmpsize=$((freesize - reservekilos)) 214tmpfile="$fsdir/${0##*/}_TMPFILE.$$" 215 216## Clean up tmpfile (if any) and exit: 217## 218function do_cleanup(){ 219 if [ -e $tmpfile ]; then 220 echo "Removing temporary file '$tmpfile'..." 221 $RM -f $tmpfile 222 if [ -e $tmpfile ]; then 223 echo "Failed to remove '$tmpfile'!!!" 224 fi 225 fi 226 [ $1 -eq 0 ] && echo "Done." 227 [ $1 -eq 0 ] || echo "Aborted." >&2 228 exit $1 229} 230 231## Prepare signal handling, in case we get interrupted while $tmpfile exists: 232## 233function do_abort(){ 234 echo 235 do_cleanup 1 236} 237trap do_abort SIGTERM 238trap do_abort SIGQUIT 239trap do_abort SIGINT 240trap do_abort SIGHUP 241trap do_abort SIGPIPE 242 243## Do the fallocate. 244## This is where we finally discover whether the filesystem actually 245## supports --fallocate or not. Some folks will be disappointed here. 246## 247## Note that --fallocate does not actually write any file data to fsdev, 248## but rather simply allocates formerly-free space to the tmpfile. 249## 250echo -n "Creating temporary file (${tmpsize} KB '$tmpfile') ... " 251if ! $HDPARM --fallocate "${tmpsize}" $tmpfile ; then 252 echo "This kernel may not support 'fallocate'. Aborting." 253 exit 1 254fi 255echo 256 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. 259## 260TESTSTR=`date` 261echo "$TESTSTR" >> $tmpfile 262sync # this is critical 263SECTOR_BYTES=`$HDPARM --fibmap $tmpfile | \ 264 $GREP "byte sectors" | \ 265 $GAWK '{print $9}'` 266LAST_EXTENT_SECTOR_COUNT=`$HDPARM --fibmap $tmpfile | \ 267 tail -1 | \ 268 $GAWK '{print $4}'` 269LAST_EXTENT_LBA=`$HDPARM --fibmap $tmpfile | tail -1 | $GAWK '{print $2}'` 270 271## Verify the test string is in the extent read. 272if ! 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." 274 do_cleanup 1 275fi 276 277## Now compare the mirror and the slaves to make sure they have the same data at the same LBA. 278## 279refchksum=`dd iflag=direct status=noxfer bs=$SECTOR_BYTES count=$LAST_EXTENT_SECTOR_COUNT skip=$LAST_EXTENT_LBA if=/dev/$raiddev 2>/dev/null | sha1sum` 280index=0 281for slave in "${slaves[@]}" 282do 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` 284 285 if [ "$chksum" != "$refchksum" ]; then 286 echo "Direct I/O of last extent of tmpfile on $slave doesn't match that of $raiddev. Excluding." 287 unset slaves[index] 288 fi 289 290 let "index = $index + 1" 291done 292if [ "${slaves[0]}" = "" ]; then 293 echo "No constituent of $raiddev array has a matching checksum. Aborting." 294 do_cleanup 1 295fi 296 297echo "TRIMable constituents of $raiddev: ${slaves[@]}" 298 299## If they specified "--commit" on the command line, then prompt for confirmation first: 300## 301if [ "$commit" = "yes" ]; then 302 echo "Beginning TRIM operations..." 303else 304 echo "This will be a DRY-RUN only. Use --commit to do it for real." 305 echo "Simulating TRIM operations..." 306fi 307get_trimlist="$HDPARM --fibmap $tmpfile" 308[ $verbose -gt 0 ] && echo "get_trimlist=$get_trimlist" 309 310 311## Begin gawk program 312GAWKPROG=' 313 function append_range (lba,count ,this_count){ 314 nsectors += count; 315 while (count > 0) { 316 this_count = (count > 65535) ? 65535 : count 317 printf "%u:%u \n", lba, this_count 318 if (verbose > 1) 319 printf "%u:%u ", lba, this_count > "/dev/stderr" 320 lba += this_count 321 count -= this_count 322 nranges++; 323 } 324 } 325 { ## Output from "hdparm --fibmap", in absolute sectors: 326 if (NF == 4 && $2 ~ "^[1-9][0-9]*$") 327 append_range($2,$4) 328 next 329 } 330 END { 331 if (verbose > 1) 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" 335 exit err 336 }' 337## End gawk program 338 339## Run TRIM on each slave. Batch as requested. 340sync 341index=0 342for slave in "${slaves[@]}" 343do 344 echo "TRIM beginning on $slave..." 345 346 if [ "$commit" = "yes" ]; then 347 TRIM="$HDPARM --please-destroy-my-drive \ 348 --trim-sector-ranges-stdin /dev/$slave" 349 else 350 TRIM="$GAWK {}" 351 fi 352 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 }'` 357 case "$model" in 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 361 esac 362 else 363 slave_max_range=$max_ranges 364 fi 365 [ $verbose -gt 0 ] && echo "$slave: max-ranges = $slave_max_range" 366 367 $get_trimlist 2>/dev/null | $GAWK \ 368 -v commit="$commit" \ 369 -v verbose="$verbose" \ 370 "$GAWKPROG" | \ 371 if true; then 372 i=0 373 while read range; do 374 (( i++ )) 375 if (( i <= $slave_max_range )); then 376 ranges=$ranges" "$range 377 else 378 [ $verbose -gt 0 ] && echo -e "Trim ranges:" $ranges 379 echo $ranges | $TRIM 380 ret=$? 381 if [ $ret -ne 0 ] ; then 382 do_cleanup $ret 383 fi 384 ranges=$range 385 i=1 386 fi 387 done 388 [ $verbose -gt 0 ] && echo -e "Trim ranges:" $ranges 389 echo $ranges | $TRIM 390 ret=$? 391 if [ $ret -ne 0 ] ; then 392 do_cleanup $ret 393 fi 394 ranges="" 395 fi 396 397 ret=$? 398 if [ $ret -ne 0 ] ; then 399 echo "TRIM failed on $slave. Aborting." 400 do_cleanup $ret 401 else 402 echo "TRIM finished successfully on $slave." 403 fi 404done 405 406do_cleanup 0 407 408