#! /bin/bash # # smart_spindown rev. 1 # # Copyright (C) 2003 by Bart Samwel # # You may do with this file (and parts thereof) whatever you want, as long # as my copyright notice is retained. # # Extended by joerk at gentoo-wiki.com # modified for a mostly idle backup disk by B Persson # # # A command line parameter --conf= is required. The specified file # will be sourced as shell code. It must set the variable DISK to the pathname # of the disk to be monitored. It is recommended to use one of the links under # /dev/disk. # # How it works: This program monitors the activity on a disk. If there # is no activity for a while, the disk is spun down. The time without # activity that is required is dynamic, using a backoff factor. When # the recent spun-down periods are relatively short, this means that the # machine might be busy with something, so the script tries to wait for # longer periods without activity before spinning down again. When spun-down # periods are long, the backoff factor is decreased, and the disk is spun # down after shorter periods without activity. # # Configuration # # Output levels. Level 2 is verbose, level 1 is normal output. # Enable all levels you would like to see. OUTLEVEL1=true OUTLEVEL2=false # Decide which output to use. Useful if run in daemon mode # echo or logger # output1 is logged to normal, output2 to debug syslog # Is PID of this shell logged? #OUTPUT1=echo #OUTPUT2=echo OUTPUTTAG="$(basename -- $0)[$$]" OUTPUT1="logger -t $OUTPUTTAG -p user.notice --" OUTPUT2="logger -t $OUTPUTTAG -p user.debug --" # Multiplication factor for the backoff after a spinup, in percentages. # Default is 300 = factor 3. BACKOFF_INCREASE_PCT=300 # Multiplication factor for the backoff at every poll that shows that # the disk is spun down. This determines how fast the backoff value # decreases. BACKOFF_DECREASE_PCT=96 # The base inactivity wait time (in seconds). This is multiplied by # the backoff factor to determine the real inactivity wait time. WAITTIME=150 # The maximum inactivity wait time (in seconds). # This also limits the backoff factor: the backoff factor cannot increase # above a value that makes the inactivity wait time larger than MAXWAIT. # Something, probably in Linux, seems to read a few disk sectors every ten # minutes while the disk is spinning, which prevents this program from ever # spinning down the disk once the inactivity wait time grows longer than ten # minutes. To avoid this, MAXWAIT is set to nine minutes. MAXWAIT=540 # Time (in seconds) between polls to see if the disk is active again. # Default is 10 seconds. POLLTIME=10 # Enable this if you don't use laptop_mode. This will make the script # sync before spinning down the disk. NO_LAPTOP_MODE=true # # Parse the command line: # if [[ $# -ne 1 ]] ; then echo 'Exactly one parameter is required, namely --conf=.' >&2 exit 1 fi case "$1" in --conf=*) CONF="${1#*=}" ;; *) echo 'unrecognized parameter' >&2 exit 1 ;; esac # # Read the configuration: # source "$CONF" if [[ $? -ne 0 ]] ; then echo 'Reading the configuration file failed.' >&2 exit 1 fi # Get the device name of the disk to monitor: DEVNAME=`realpath "$DISK"` if [[ $? -ne 0 ]] ; then echo 'bad disk pathname' >&2 exit 1 fi if [[ ! -b "$DEVNAME" ]] ; then echo 'The disk pathname is not a block device.' >&2 exit 1 fi # Stats file: the file used to monitor the disk's activity. STATSFILE=/sys/block/`basename "$DEVNAME"`/stat # # Let's go! # # Number of poll times that the disk was found to be spun down. POLLSSPUNDOWN=0 # Number of spindowns performed SPINDOWNS=0 # Number of times (*100) the WAITTIME of inactivity required before spindown BACKOFF_FACTOR=100 # Stats: Total time the disk has been up. UPTIME=0 # Total duration of last spun-down period. LASTDOWNTIME=0 # Total duration of the last spun-up period. LASTUPTIME=0 # Duration of the last poll. Always equal to POLLTIME except the first # time around. LASTPOLLTIME=0 # Make sure the stuff we use is in the cache. I've seen it happen # that the script spun the disk down, and then "sleep" wasn't in # the cache and the disk spun right up again. :) true false sleep 1 # Terminate on request. trap 'exit 0' HUP INT TERM # Log the end of script execution. trap "$OUTPUT1 'Exiting.'" EXIT $OUTLEVEL1 && ${OUTPUT1} "Monitoring spindown opportunities for $DEVNAME." if ($OUTLEVEL1) ; then hdparm -C $DEVNAME |grep active >/dev/null if [ $? -eq 0 ] ; then ${OUTPUT1} "$DEVNAME is currently spun up." ; else ${OUTPUT1} "$DEVNAME is currently spun down." ; fi ; fi while [[ true ]]; do hdparm -C $DEVNAME |grep active >/dev/null if [ $? -eq 0 ] ; then THISWAIT=$(($WAITTIME*$BACKOFF_FACTOR/100)) ; if [[ $THISWAIT -gt $MAXWAIT ]] ; then THISWAIT=$MAXWAIT ; fi ; # Increase the backoff irrespective of whether we failed # or not. The backoff should drop again by the lack of # spinups afterwards. BACKOFF_FACTOR=$(($BACKOFF_FACTOR*$BACKOFF_INCREASE_PCT/100)) ; if [[ $(($BACKOFF_FACTOR*$WAITTIME/100)) -gt $MAXWAIT ]] ; then BACKOFF_FACTOR=$(($MAXWAIT*100/$WAITTIME)) ; fi ; if ( $OUTLEVEL2 ) ; then # UPTIME is used only for OUTLEVEL2 messages. UPTIME=$(($UPTIME+$LASTPOLLTIME)) ; fi ; LASTUPTIME=$(($LASTUPTIME+$LASTPOLLTIME)) ; if [ $LASTPOLLTIME -ne 0 ] ; then $OUTLEVEL1 && ${OUTPUT1} "$DEVNAME spun up after $LASTDOWNTIME seconds." ; fi NUM_EQUALS=0 ; $OUTLEVEL2 && ${OUTPUT2} "Waiting for $THISWAIT seconds of inactivity..." ; PREVIOUS_STATS=`cat $STATSFILE` ; while [[ $(($NUM_EQUALS*5)) -lt $THISWAIT ]]; do sleep 5 ; if ( $OUTLEVEL2 ) ; then # UPTIME is used only for OUTLEVEL2 messages. UPTIME=$(($UPTIME+5)) ; fi ; LASTUPTIME=$(($LASTUPTIME+5)) ; NEXT_STATS=`cat $STATSFILE` ; if [ "$PREVIOUS_STATS" != "$NEXT_STATS" ] ; then NUM_EQUALS=0 ; PREVIOUS_STATS="$NEXT_STATS" $OUTLEVEL2 && ${OUTPUT2} "Restarting..." ; else NUM_EQUALS=$(($NUM_EQUALS+1)) ; $OUTLEVEL2 && ${OUTPUT2} "Seconds of quiet: $(($NUM_EQUALS*5))" ; fi done # We've just had $THISWAIT seconds of inactivity. The inactivity indicates # that we're ready to go to sleep. Laptop mode will have synced all # writes for us after the last read, so we don't have to explicitly # sync. if ( $NO_LAPTOP_MODE ) ; then sync ; fi ; hdparm -q -y $DEVNAME ; if ( $OUTLEVEL2 ) ; then # SPINDOWNS is used only for OUTLEVEL2 messages. SPINDOWNS=$(($SPINDOWNS+1)) ; fi ; $OUTLEVEL1 && ${OUTPUT1} "$DEVNAME spun down after $LASTUPTIME seconds (with $THISWAIT seconds of inactivity)." ; LASTUPTIME=0 ; LASTDOWNTIME=0 ; else if ( $OUTLEVEL2 ) ; then # POLLSSPUNDOWN and SPINDOWNS are used only for OUTLEVEL2 messages. POLLSSPUNDOWN=$(($POLLSSPUNDOWN+1)) ; if [[ $SPINDOWNS -eq 0 ]] ; then SPINDOWNS=1 ; fi fi ; LASTDOWNTIME=$(($LASTDOWNTIME+$LASTPOLLTIME)) ; BACKOFF_FACTOR=$(($BACKOFF_FACTOR*$BACKOFF_DECREASE_PCT/100)) ; if [ $BACKOFF_FACTOR -lt 100 ] ; then BACKOFF_FACTOR=100 ; fi fi ; if ( $OUTLEVEL2 ) ; then ${OUTPUT2} "spindowns: $SPINDOWNS, time up/down: $UPTIME/$(($POLLSSPUNDOWN*$POLLTIME)), backoff $BACKOFF_FACTOR, down for $LASTDOWNTIME (avg $(($POLLSSPUNDOWN*$POLLTIME/$SPINDOWNS)))." ; fi ; sleep $POLLTIME ; LASTPOLLTIME=$POLLTIME ; done