Howto: Spin down external (USB/Firewire) hard drives on idle
Linux - HardwareThis forum is for Hardware issues.
Having trouble installing a piece of hardware? Want to know if that peripheral is compatible with Linux?
Notices
Welcome to LinuxQuestions.org, a friendly and active Linux Community.
You are currently viewing LQ as a guest. By joining our community you will have the ability to post topics, receive our newsletter, use the advanced search, subscribe to threads and access many other special features. Registration is quick, simple and absolutely free. Join our community today!
Note that registered members see fewer ads, and ContentLink is completely disabled once you log in.
If you have any problems with the registration process or your account login, please contact us. If you need to reset your password, click here.
Having a problem logging in? Please visit this page to clear all LQ-related cookies.
Get a virtual cloud desktop with the Linux distro that you want in less than five minutes with Shells! With over 10 pre-installed distros to choose from, the worry-free installation life is here! Whether you are a digital nomad or just looking for flexibility, Shells can put your Linux machine on the device that you want to use.
Exclusive for LQ members, get up to 45% off per month. Click here for more info.
Howto: Spin down external (USB/Firewire) hard drives on idle
Most external hard drives will respond to an sdparm command to stop the drive. However, it's inconvenient to run the command(s) manually every time, especially if the drives are used off-hours by cron jobs. I created the attached script, idleDrive, which will let you spin down external drives automatically after they go idle. To use the script, add a cron job (usually on root) to run idleDrive, and specify the drive to be idled by either label, vendor or UUID. For example, to spin down a drive with the label "Videos" after five minutes of idle, you would add the cron job (assuming idleDrive is in /usr/local/bin/):
The script itself follows. I use this on five different drives across three system. I've read that some drives will not respond to the spin-down commands. Some USB/IEEE1394 combo drives will only respond on one interface. You will need the sdparm command installed; it should be available from your distributions repositories. Hopefully, this will work for you:
Code:
#!/bin/bash
#-------------------------------------------------------------------------------
#
# idleDrive
#
# Author: Mace Moneta
# Version: 1.2
# Created: 10/12/2007
# Modified: 10/18/2007
#
# Description: Spin down the drive when it goes idle
#
# Options:
#
# vendor=vendor-name Example: vendor=Maxtor
# uuid=uuid-string Example: uuid=104355ff-685e-4fa9-8869-bfa82d2fb9b6
# label=volume-label Example: label=ExternalDisk
#
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
# ************************** Variables **************************
#-------------------------------------------------------------------------------
PARAM=$1
OPTION=${PARAM%%=*}
VALUE=${PARAM##*=}
LOCK="/dev/shm/idleDrive-${VALUE}.lock"
STATE="/dev/shm/idleDrive-${VALUE}.state"
DOWN="/dev/shm/idleDrive-${VALUE}.down"
#-------------------------------------------------------------------------------
# ************************* Subroutines *************************
#-------------------------------------------------------------------------------
#--- FUNCTION ----------------------------------------------------------------
# Name: log
# Description: Log a message to the syslog and terminal
# Parameters: The message to log
# Returns: Nothing
#-------------------------------------------------------------------------------
function log () {
PREFIX="idleDrive.info"
PREFIXLEN=${#PREFIX}
PREFIXLEN=$((10#$PREFIXLEN+2))
DASHES="---------------------------------------------------------------------------------"
MSG=$1
MSGLEN=${#MSG}
MSGLEN=$((10#$MSGLEN+$PREFIXLEN))
DASH=${DASHES:0:$MSGLEN}
echo
echo "$DASH"
/usr/bin/logger -s -t $PREFIX "$MSG"
echo "$DASH"
}
#--- FUNCTION ----------------------------------------------------------------
# Name: CLEANUP
# Description: Clear the lock and exit
# Parameters: None
# Returns: Nothing
#-------------------------------------------------------------------------------
function CLEANUP () {
/bin/rm -f $LOCK
log "Exiting on interrupt"
exit
}
trap CLEANUP INT
trap CLEANUP HUP
trap CLEANUP QUIT
trap CLEANUP USR1
trap CLEANUP TERM
#-------------------------------------------------------------------------------
# ************************* Mainline ****************************
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
# Validate the parameters and
# Identify the device that the drive is known to the system as.
#-------------------------------------------------------------------------------
case "$OPTION" in
"vendor" )
for drive in `/bin/ls /sys/block/sd?/device/vendor`
do
if [ "`/bin/cat $drive`" == "$VALUE" ]
then
drive=${drive##/sys/block/}
drive="/dev/${drive%%/device/vendor}"
disk=${drive##/dev/}
break
else
exit 0
fi
done ;;
"uuid" )
drive=`/bin/ls -l /dev/disk/by-uuid/ | \
/bin/grep "$VALUE" | \
/bin/awk '{print $11}'`
drive=${drive##*/}
disk=${drive:0:3}
drive="/dev/${disk:0:3}"
if [ "$disk" == "" ]
then
exit 0
fi ;;
"label" )
drive=`/bin/ls -l /dev/disk/by-label/ | \
/bin/grep "$VALUE" | \
/bin/awk '{print $11}'`
drive=${drive##*/}
disk=${drive:0:3}
drive="/dev/${disk:0:3}"
if [ "$disk" == "" ]
then
exit 0
fi ;;
* )
log "Invalid option: $OPTION"
log "Specify vendor, uuid or label"
exit 1 ;;
esac
#-------------------------------------------------------------------------------
# Serialize execution
#-------------------------------------------------------------------------------
if [ -f $LOCK ]
then
log "Already in progress for $drive ($VALUE)"
exit 1
fi
/bin/touch $LOCK
#-------------------------------------------------------------------------------
# Check I/O stats to see if the drive has been idle
#-------------------------------------------------------------------------------
state=`/bin/grep " $disk " /proc/diskstats`
if [ -f $STATE ]
then
previousState=`/bin/cat $STATE`
else
log "Initializing state for $drive ($VALUE)"
echo "$state" > $STATE
/bin/rm -f $LOCK
exit 0
fi
#-------------------------------------------------------------------------------
# If the I/O stats haven't changed, the drive is idle
#-------------------------------------------------------------------------------
if [ "$state" == "$previousState" ] && [ ! -f $DOWN ]
then
log "Spin down drive $drive ($VALUE)"
/bin/sync
/usr/bin/sdparm --flexible --command=sync $drive &>/dev/null
/usr/bin/sdparm --flexible --command=stop $drive &>/dev/null
state=`/bin/grep " $disk " /proc/diskstats`
echo "$state" > $STATE
echo "down" > $DOWN
else
if [ "$state" != "$previousState" ]
then
/bin/rm -f $DOWN
echo "$state" > $STATE
fi
fi
#-------------------------------------------------------------------------------
# Done
#-------------------------------------------------------------------------------
/bin/rm -f $LOCK
exit 0
I'm running [k]ubuntu 7.10, and had to make a few small changes to get this to work on my system. First, awk is located in /usr/bin as opposed to /bin -- you can either create a symlink from /bin/awk to /usr/bin/awk or change the script to point to the right place for ubuntu. Second, for the uuid case, awk should check for the 10th field instead of the 11th. That is,
Code:
/bin/awk '{print $10}'`
Finally, the vendor matching doesn't seem to work for me. The else which results in exit should probably be moved outside of the for loop, after testing on $disk. As it is now, if the first drive doesn't equal the vendor, it'll exit. That is, it seems it should be:
Code:
"vendor" )
for drive in `/bin/ls /sys/block/sd?/device/vendor`
do
if [ "`/bin/cat $drive`" == "$VALUE" ]
then
drive=${drive##/sys/block/}
drive="/dev/${drive%%/device/vendor}"
disk=${drive##/dev/}
break
fi
done
if [ "$disk" == "" ]
then
log "found no drive for vendor $VALUE"
exit 0
fi ;;
Though I think even that will only match the last drive where the vendor is $VALUE. Even with that change, I can't get vendor matching to work for me. I am far from a shell scripting guru, so there probably are errors in my code.
I did not test the matching by label.
Other than that, the script works great for me. Thanks!
(Note, my whole modified version is below. This code is provided without warranty, including implied warranties. I abandon any copyright claim I may have in any of the modifications I have made into the public domain. The original author Mace Moneta, of course, still holds his copyright on the code.)
Code:
#!/bin/bash
#-------------------------------------------------------------------------------
#
# idleDrive
#
# Author: Mace Moneta
# Version: 1.2
# Created: 10/12/2007
# Modified: 10/18/2007
#
# Description: Spin down the drive when it goes idle
#
# Options:
#
# vendor=vendor-name Example: vendor=Maxtor
# uuid=uuid-string Example: uuid=104355ff-685e-4fa9-8869-bfa82d2fb9b6
# label=volume-label Example: label=ExternalDisk
#
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
# ************************** Variables **************************
#-------------------------------------------------------------------------------
PARAM=$1
OPTION=${PARAM%%=*}
VALUE=${PARAM##*=}
LOCK="/dev/shm/idleDrive-${VALUE}.lock"
STATE="/dev/shm/idleDrive-${VALUE}.state"
DOWN="/dev/shm/idleDrive-${VALUE}.down"
MYAWK="/usr/bin/awk"
#-------------------------------------------------------------------------------
# ************************* Subroutines *************************
#-------------------------------------------------------------------------------
#--- FUNCTION ----------------------------------------------------------------
# Name: log
# Description: Log a message to the syslog and terminal
# Parameters: The message to log
# Returns: Nothing
#-------------------------------------------------------------------------------
function log () {
PREFIX="idleDrive.info"
PREFIXLEN=${#PREFIX}
PREFIXLEN=$((10#$PREFIXLEN+2))
DASHES="---------------------------------------------------------------------------------"
MSG=$1
MSGLEN=${#MSG}
MSGLEN=$((10#$MSGLEN+$PREFIXLEN))
DASH=${DASHES:0:$MSGLEN}
echo
echo "$DASH"
/usr/bin/logger -s -t $PREFIX "$MSG"
echo "$DASH"
}
#--- FUNCTION ----------------------------------------------------------------
# Name: CLEANUP
# Description: Clear the lock and exit
# Parameters: None
# Returns: Nothing
#-------------------------------------------------------------------------------
function CLEANUP () {
/bin/rm -f $LOCK
log "Exiting on interrupt"
exit
}
trap CLEANUP INT
trap CLEANUP HUP
trap CLEANUP QUIT
trap CLEANUP USR1
trap CLEANUP TERM
#-------------------------------------------------------------------------------
# ************************* Mainline ****************************
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
# Validate the parameters and
# Identify the device that the drive is known to the system as.
#-------------------------------------------------------------------------------
case "$OPTION" in
"vendor" )
for drive in `/bin/ls /sys/block/sd?/device/vendor`
do
if [ "`/bin/cat $drive`" == "$VALUE" ]
then
drive=${drive##/sys/block/}
drive="/dev/${drive%%/device/vendor}"
disk=${drive##/dev/}
break
fi
done
if [ "$disk" == "" ]
then
log "found no drive for vendor $VALUE"
exit 0
fi ;;
"uuid" )
drive=`/bin/ls -l /dev/disk/by-uuid/ | \
/bin/grep "$VALUE" | \
$MYAWK '{print $10}'`
drive=${drive##*/}
disk=${drive:0:3}
drive="/dev/${disk:0:3}"
if [ "$disk" == "" ]
then
log "found no drive for uuid $VALUE"
exit 0
fi ;;
"label" )
drive=`/bin/ls -l /dev/disk/by-label/ | \
/bin/grep "$VALUE" | \
$MYAWK '{print $11}'`
drive=${drive##*/}
disk=${drive:0:3}
drive="/dev/${disk:0:3}"
if [ "$disk" == "" ]
then
log "found no drive for label $VALUE"
exit 0
fi ;;
* )
log "Invalid option: $OPTION"
log "Specify vendor, uuid or label"
exit 1 ;;
esac
#-------------------------------------------------------------------------------
# Serialize execution
#-------------------------------------------------------------------------------
if [ -f $LOCK ]
then
log "Already in progress for $drive ($VALUE)"
exit 1
fi
/bin/touch $LOCK
#-------------------------------------------------------------------------------
# Check I/O stats to see if the drive has been idle
#-------------------------------------------------------------------------------
state=`/bin/grep " $disk " /proc/diskstats`
if [ -f $STATE ]
then
previousState=`/bin/cat $STATE`
else
log "Initializing state for $drive ($VALUE)"
echo "$state" > $STATE
/bin/rm -f $LOCK
exit 0
fi
#-------------------------------------------------------------------------------
# If the I/O stats haven't changed, the drive is idle
#-------------------------------------------------------------------------------
if [ "$state" == "$previousState" ] && [ ! -f $DOWN ]
then
log "Spin down drive $drive ($VALUE)"
/bin/sync
/usr/bin/sdparm --flexible --command=sync $drive &>/dev/null
/usr/bin/sdparm --flexible --command=stop $drive &>/dev/null
state=`/bin/grep " $disk " /proc/diskstats`
echo "$state" > $STATE
echo "down" > $DOWN
else
if [ "$state" != "$previousState" ]
then
/bin/rm -f $DOWN
echo "$state" > $STATE
fi
fi
#-------------------------------------------------------------------------------
# Done
#-------------------------------------------------------------------------------
/bin/rm -f $LOCK
exit 0
Thank you macemoneta for the script and radar2k for the corrections. I'm using Ubuntu too and I had to modify the script a little.
Firstly, I had to call sdparm --command=stop twice the first time it (sdparm) is run as a workarround to a bug related with that utility (bug url: External disk won't spindown).
Another super tiny fix I've made is to use ls -l --full-time instead of ls -l because it appears that LC_TIME is not set correctly for processes spawned by cron. This must be the explanation as to why radar2k had to have awk check for the 10th field instead of the 11th.
Finally I removed the by vendor and by label disk suspension.
Here is my modified version:
Code:
#!/bin/bash
# Author: Mace Moneta
# Description: Spin down the drive when it goes idle
# Url: http://www.linuxquestions.org/questions/linux-hardware-18/howto-spin-down-external-usb-firewire-hard-drives-on-idle-593192/
# Modified By: 666f6f
PARAM=$1
OPTION=${PARAM%%=*}
VALUE=${PARAM##*=}
LOCK_FILE="/dev/shm/idle-drive-${VALUE}.lock"
STATE_FILE="/dev/shm/idle-drive-${VALUE}.state"
DOWN_FILE="/dev/shm/idle-drive-${VALUE}.down"
FIX="/dev/shm/idle-drive.fixed"
idle_drive_log() {
local PREFIX="idle-drive.info"
local PREFIXLEN=${#PREFIX}
local PREFIXLEN=$((10#$PREFIXLEN+2))
local MSG=$1
local MSGLEN=${#MSG}
local MSGLEN=$((10#$MSGLEN+$PREFIXLEN))
logger -s -t $PREFIX "$MSG"
}
idle_drive_cleanup() {
rm -f $LOCK_FILE
idle_drive_log "Exiting on interrupt."
exit
}
trap idle_drive_cleanup INT
trap idle_drive_cleanup HUP
trap idle_drive_cleanup QUIT
trap idle_drive_cleanup USR1
trap idle_drive_cleanup TERM
DRIVE=`ls -l --full-time /dev/disk/by-uuid/ | grep "$VALUE" | awk '{print $11}'`
DRIVE=${DRIVE##*/}
DISK=${DRIVE:0:3}
DRIVE="/dev/${DISK:0:3}"
echo $DRIVE
echo $DISK
if [ "$DISK" == "" ]; then
idle_drive_log 'Disk not found.'
exit 1
fi
if [ -f $LOCK_FILE ]; then
idle_drive_log "Already in progress for $DRIVE ($VALUE)."
exit 1
fi
touch $LOCK_FILE
# Check I/O stats to see if the drive has been idle
STATE=`grep " $DISK " /proc/diskstats`
if [ -f $STATE_FILE ]; then
STATE_PREVIOUS=`/bin/cat $STATE_FILE`
else
idle_drive_log "Initializing state for $DRIVE ($VALUE)."
echo "$STATE" > $STATE_FILE
rm -f $LOCK_FILE
exit 0
fi
# If the I/O stats haven't changed, the drive is idle
if [ "$STATE" == "$STATE_PREVIOUS" ] && [ ! -f $DOWN_FILE ]; then
idle_drive_log "Disk state unchanged and disk spinning. Spin down drive $DRIVE ($VALUE)"
sync
sdparm --flexible --command=sync $DRIVE &>/dev/null
sdparm --flexible --command=stop $DRIVE &>/dev/null
[ ! -f $FIX ] && sleep 5 && touch $FIX && sdparm --flexible --command=stop $DRIVE &>/dev/null
STATE=`grep " $DISK " /proc/diskstats`
echo "$STATE" > $STATE_FILE
touch $DOWN_FILE
else
if [ "$STATE" != "$STATE_PREVIOUS" ]; then
idle_drive_log "Disk state changed. Deleting $DOWN_FILE and refreshing state."
rm -f $DOWN_FILE
echo "$STATE" > $STATE_FILE
fi
fi
# Done
rm -f $LOCK_FILE
exit 0
I've been trying to spindown my external USB hard drive (solely used for backup purposes) without much success until I stumbled across this and other similar threads. What in the end worked for me, were the following set of commands. I should also add that I'm using Slackware 13.1 (64-bit) and external Buffalo HD enclosure (HD-HC320IU2 - 320GB replaced with a 2TB drive).
Identify the correct SCSI generic (sg) device id for your drive. I only have two, so it was easy to "eyeball" the one I was looking for. If you only have two drives, the second drive will most likely be /dev/sg1 (/dev/sg0 being the primary hdd):
Code:
ls -la /dev/sg*
Use sdparm to spin the drive down (substitute your sg device for /dev/sgx):
It works on Slackware64 13.1 with an Hitachi "Portable DRIVE" a.k.a "SimpleDRIVE Mini" (500 GB USB HDD), model 0S00462 -- something I've been wanting to do since Feb 2010.
One surprising phenomenon: the USB HDD did not get much cooler than it is when spinning and idle. Presumably the electronics consume significant power.
Next project: how to determine the /dev/sg* name programatically starting with partition major and minor numbers from udev, preferably without having to install sg_utils.
The information in post 6 is outdated. Under Slackware64 14.0 (kernel 3.2.29, udev 182, sdparm 1.07) the SCSI pass-through device files are /dev/bsg/*, not /dev/sg* as earlier.
The general technique of using the SCSI pass-through device file as the sdparm DEVICE argument when sending commands is still valid.
Here's a function to implement it (logging and error trap code removed for clarity)
Code:
#--------------------------
# Name: spin_down_drive
# Purpose: spins down the drive
# Global variable used: $disk_dev_file
#--------------------------
function spin_down_drive {
local dev_bsg_X
# Get the /dev/bsg/* file name from the /dev/sd* file name
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# /dev/bsg/* is the "pass-through" interface to the SCSI driver. It must
# be used as the DEVICE argument when sdparm is used to send commands to
# the device.
dev_bsg_X=/dev/bsg/$( lsscsi --generic | grep $disk_dev_file | sed -e 's/\[//' -e 's/].*$//' )
# Spin down
# ~~~~~~~~~
sdparm --command=stop --quiet $dev_bsg_X
} # end of function spin_down_drive
LinuxQuestions.org is looking for people interested in writing
Editorials, Articles, Reviews, and more. If you'd like to contribute
content, let us know.