#!/bin/bash # # SSD TRIM utility for live RAID1 mirrored ext4 drives. # # By Chris Caputo. Adapted from wiper.sh (ver 2.6) by Mark Lord. VERSION=1.5 # Copyright (C) 2010-2012 Chris Caputo. All rights reserved. # # 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 function usage_error(){ echo "Usage:" echo " ${0##*/} [--verbose] [--commit] [--reserve=#megs] [--max-ranges=#ranges] " echo "Examples:" echo " ${0##*/} --verbose --commit --reserve=100 --max-ranges=512 md0 /" echo " ${0##*/} --verbose --verbose md1 /boot" echo echo "Note: For best results, this script should be run on each ext4-based filesystem present on a RAID1 array." echo exit 1 } echo "${0##*/}: TRIM utility for live RAID1 ext4 SATA SSDs, version $VERSION, by Chris Caputo, based on Mark Lord's wiper.sh." echo ## Parameter parsing for the main script. ## export verbose=0 commit="" reservemegs=0 max_ranges=0 argc=$# raiddev="" fsdir="" while [ $argc -gt 0 ]; do if [ "$1" = "--commit" ]; then commit=yes elif [ "$1" = "--verbose" ]; then verbose=$((verbose + 1)) elif [[ "$1" =~ --reserve= ]]; then reservemegs=${1##--reserve=} elif [[ "$1" =~ --max-ranges= ]]; then max_ranges=${1##--max-ranges=} elif [ "$1" = "" ]; then usage_error else if [ "$raiddev" = "" ]; then raiddev=${1##*/} elif [ "$fsdir" = "" ]; then fsdir=$1 else echo "$1: too many arguments, aborting." exit 1 fi fi argc=$((argc - 1)) shift done [ "$raiddev" = "" ] && usage_error [ "$fsdir" = "" ] && usage_error ## Check --reserve number. ## isdigit () # Tests whether *entire string* is numerical. { # In other words, tests for integer variable. [ $# -eq 1 ] || return 1 case $1 in *[!0-9]*|"") return 1;; *) return 0;; esac } if ! isdigit "$reservemegs" ; then echo "'$reservemegs' is not numerical" exit 1 fi if ! isdigit "$max_ranges" ; then echo "'$max_ranges' is not numerical" exit 1 fi if [ $reservemegs -eq 0 ]; then echo "Reserve defaulting to 10 megabytes." reservemegs=10 fi reservekilos=$((reservemegs * 1024)) ## 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 echo "$1: needed but not found, aborting." exit 1 fi prog="$p" [ $verbose -gt 0 ] && echo " --> using $prog instead of $1" 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 GAWK=`find_prog /usr/bin/gawk` || 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 [ $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." 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." exit 1 fi ## Check that this is a RAID1 device. ## if ! $GREP raid1 /sys/block/$raiddev/md/level 1>/dev/null ; then echo "$raiddev is not a RAID1 array." exit 1 fi ## Get list of slave devices in the RAID1 mirror. ## slaves=(`$LS /sys/block/$raiddev/slaves`) #slaves=(sda sdb sdc) #slaves=(md0) ## Check for DEVTYPE disk and TRIM support on each slave. ## index=0 for slave in "${slaves[@]}" do # Check that slave is of DEVTYPE disk. if ! $GREP "DEVTYPE=disk" /sys/block/$slave/uevent 1>/dev/null ; then echo "$slave is not a whole disk. This program only works with full-disk RAID1, not RAID1 partitions." exit 1 fi # Check that slave has TRIM support. Exclude if not. if ! $HDPARM -I /dev/$slave | $GREP -i '[ ][*][ ]*Data Set Management TRIM supported' &>/dev/null ; then echo "$slave doesn't appear to support TRIM, per $HDPARM. Excluding." unset slaves[index] fi let "index = $index + 1" done if [ "${slaves[0]}" = "" ]; then echo "No constituent of $raiddev array supports TRIM. Aborting." exit 1 fi ## Check that fsdir is on an ext4 volume. ## lines=`$DF --type=ext4 $fsdir 2>/dev/null | $GREP -v ^Filesystem | wc -l` if [ $lines -ne 1 ]; then echo "'$fsdir' does not appear to be on an ext4 filesystem. Aborting." exit 1 fi ## Check that fsdir is a directory. ## if [ ! -d $fsdir ]; then echo "'$fsdir' is not a directory. Aborting." exit 1 fi ## Check free space & calculate tmpfile size. ## freesize=`$DF -P -B 1024 $fsdir | $GAWK '{r=$4}END{print r}'` if [ "$freesize" = "" ]; then echo "'$fsdir' is unknown to '$DF'. Aborting." exit 1 fi if [ $freesize -lt $reservekilos ]; then echo "'$fsdir' available space of $freesize KB is less than the $reservekilos KB to be reserved for the TRIM operation. Aborting." >&2 exit 1 fi tmpsize=$((freesize - reservekilos)) tmpfile="$fsdir/${0##*/}_TMPFILE.$$" ## Clean up tmpfile (if any) and exit: ## function do_cleanup(){ if [ -e $tmpfile ]; then echo "Removing temporary file '$tmpfile'..." $RM -f $tmpfile if [ -e $tmpfile ]; then echo "Failed to remove '$tmpfile'!!!" fi 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 ## Do the fallocate. ## 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. ## echo -n "Creating temporary file (${tmpsize} KB '$tmpfile') ... " if ! $HDPARM --fallocate "${tmpsize}" $tmpfile ; then echo "This kernel may not support 'fallocate'. Aborting." exit 1 fi echo ## Verify that slaves and RAID1 mirror have same base LBA. First add a test ## string to the tmpfile. "date" is used since it is ever changing. ## TESTSTR=`date` echo "$TESTSTR" >> $tmpfile sync # this is critical SECTOR_BYTES=`$HDPARM --fibmap $tmpfile | \ $GREP "byte sectors" | \ $GAWK '{print $9}'` LAST_EXTENT_SECTOR_COUNT=`$HDPARM --fibmap $tmpfile | \ tail -1 | \ $GAWK '{print $4}'` LAST_EXTENT_LBA=`$HDPARM --fibmap $tmpfile | tail -1 | $GAWK '{print $2}'` ## Verify the test string is in the extent read. 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 echo "Test string was not found in last extent of tmpfile, as it should have been. Aborting." do_cleanup 1 fi ## Now compare the mirror and the slaves to make sure they have the same data at the same LBA. ## 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` index=0 for slave in "${slaves[@]}" do 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` if [ "$chksum" != "$refchksum" ]; then echo "Direct I/O of last extent of tmpfile on $slave doesn't match that of $raiddev. Excluding." unset slaves[index] fi let "index = $index + 1" done if [ "${slaves[0]}" = "" ]; then echo "No constituent of $raiddev array has a matching checksum. Aborting." do_cleanup 1 fi echo "TRIMable constituents of $raiddev: ${slaves[@]}" ## If they specified "--commit" on the command line, then prompt for confirmation first: ## if [ "$commit" = "yes" ]; then echo "Beginning TRIM operations..." else echo "This will be a DRY-RUN only. Use --commit to do it for real." echo "Simulating TRIM operations..." fi get_trimlist="$HDPARM --fibmap $tmpfile" [ $verbose -gt 0 ] && echo "get_trimlist=$get_trimlist" ## Begin gawk program GAWKPROG=' function append_range (lba,count ,this_count){ nsectors += count; while (count > 0) { this_count = (count > 65535) ? 65535 : count printf "%u:%u \n", lba, this_count if (verbose > 1) printf "%u:%u ", lba, this_count > "/dev/stderr" lba += this_count count -= this_count nranges++; } } { ## Output from "hdparm --fibmap", in absolute sectors: if (NF == 4 && $2 ~ "^[1-9][0-9]*$") append_range($2,$4) next } END { if (verbose > 1) printf "\n" > "/dev/stderr" if (err == 0 && commit != "yes") printf "(dry-run) trimming %u sectors from %u ranges\n", nsectors, nranges > "/dev/stderr" exit err }' ## End gawk program ## Run TRIM on each slave. Batch as requested. sync index=0 for slave in "${slaves[@]}" do echo "TRIM beginning on $slave..." if [ "$commit" = "yes" ]; then TRIM="$HDPARM --please-destroy-my-drive \ --trim-sector-ranges-stdin /dev/$slave" else TRIM="$GAWK {}" fi ## Different SSD's have a different maximum number of ranges they'll ## accept in a single TRIM command. if [ $max_ranges -eq 0 ] ; then model=`$HDPARM -I /dev/$slave | $GAWK '/Model Number/ { print $NF }'` case "$model" in SSDSA[12]*) slave_max_range=512 ;; # Intel X18-M/X25-M OCZ-VERTEX2) slave_max_range=64 ;; # OCZ Vertex2 *) slave_max_range=65535 esac else slave_max_range=$max_ranges fi [ $verbose -gt 0 ] && echo "$slave: max-ranges = $slave_max_range" $get_trimlist 2>/dev/null | $GAWK \ -v commit="$commit" \ -v verbose="$verbose" \ "$GAWKPROG" | \ if true; then i=0 while read range; do (( i++ )) if (( i <= $slave_max_range )); then ranges=$ranges" "$range else [ $verbose -gt 0 ] && echo -e "Trim ranges:" $ranges echo $ranges | $TRIM ret=$? if [ $ret -ne 0 ] ; then do_cleanup $ret fi ranges=$range i=1 fi done [ $verbose -gt 0 ] && echo -e "Trim ranges:" $ranges echo $ranges | $TRIM ret=$? if [ $ret -ne 0 ] ; then do_cleanup $ret fi ranges="" fi ret=$? if [ $ret -ne 0 ] ; then echo "TRIM failed on $slave. Aborting." do_cleanup $ret else echo "TRIM finished successfully on $slave." fi done do_cleanup 0