Rombobjörn

summaryrefslogtreecommitdiff
path: root/smart_spindown
blob: 20ba55668baf0709fe383c8b1fd8a8a7b6987339 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
#! /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=<pathname> 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=<pathname>.' >&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