--- /dev/null
+/*
+ * Copyright (c) 2023, Matthew Mondor.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY MATTHEW MONDOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL MATTHEW MONDOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * This is a utility program for use with certain hard disks implementing
+ * their own aggressive inactivity standby/spindown disregarding usual
+ * settings configurable via the BIOS and OS facilities. On long running
+ * systems, some of these, like WD "green" disks, will constantly spindown
+ * after as few as 8 seconds of inactivity. They may then spinup again
+ * quickly at next activity or even because of automatic SMART checks. This
+ * adds a lot of stress and can cause the disk to prematurely fail.
+ * hdparm -J <device> can be used to obtain the idle3 delay value.
+ *
+ * This program attempts to read some uncached data frequently enough to
+ * prevent the disk from spinning down unnecessarily, hopefully mitigating
+ * the issue.
+ *
+ * SMART statistics typically show a high Load_Cycle_Count to Power_On_Hours
+ * ratio. Some tools apparently allow to reconfigure the firmware of those
+ * disks to behave better, but open source ones (hdparm, idle3-tools are
+ * considered dangerous and even the official WD tool wdidle3.exe requires
+ * DOS and warns of possible data loss, while recommending to not use it on
+ * unlisted models (with an aging list missing various newer models) and to
+ * make sure to have backups.
+ *
+ * Should run as the superuser. You may want to use permissions 600 for
+ * /usr/local/etc/hdnoidle.conf as well.
+ *
+ * Usage: hdnoidle &
+ * When started, will output information about drives to work on. Any error
+ * will be logged to stderr, that you may want to redirect to a file.
+ * Reads /usr/local/etc/hdnoidle.conf expected to contain one line per device
+ * to keep busy, consisting of columns for the following parameters.
+ * Column:
+ * 0: raw device file of disk (i.e.: /dev/rwd<n>d or /dev/sd<a>)
+ * 1: delay in seconds between random seek+read activity for this disk.
+ * 3 is recommended for disks configured to idle every 8 seconds.
+ *
+ * To build:
+ * $ cc -Wall -o hdnoidle hdnoidle.c
+ */
+
+
+#define CONFIG "/usr/local/etc/hdnoidle.conf"
+#define CONFIG_LZ 256
+
+
+/* Linux may use 32-bit off_t, other unix systems long use 64-bit. */
+#ifdef __linux__
+#define _FILE_OFFSET_BITS 64
+#define _GNU_SOURCE
+#define __USE_GNU
+#endif
+
+
+#include <err.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/ioctl.h>
+#ifdef __linux__
+//#include <bits/fcntl-linux.h> /* O_DIRECT */
+#include <sys/mount.h> /* BLKGETSIZE64 */
+#include <linux/fs.h> /* BLKPBSZGET */
+#include <sys/types.h> /* open(2) */
+#include <sys/stat.h> /* open(2) */
+#endif
+#if defined (__NetBSD__) || defined (__FreeBSD__)
+#include <sys/disk.h> /* DIOCGMEDIASIZE, DIOCGSECTORSIZE */
+#endif
+
+
+/* Control structure for each disk */
+typedef struct noidledisk {
+ const char *dev; /* Device file */
+ int fd; /* Raw or unbuffered device file handle */
+ unsigned int delay, t; /* Delay in seconds between reads, current */
+ size_t bsize; /* Device block size */
+ off_t size, offset; /* Device size in bytes, current offset */
+ uint8_t *sbuf; /* Buffer to use when reading a block */
+} noidledisk_t;
+
+
+int main(void);
+void conf_read(noidledisk_t **, int *);
+void conf_setup(noidledisk_t *, int);
+int disk_getsize(int, off_t *);
+ssize_t disk_getblocksize(int);
+
+
+/*
+ * Splits columns of a string delimited by spaces and/or tabs, and fills
+ * char **argv with pointers to each column. Returns the number of columns
+ * that could be filled in. The supplied string IS modified.
+ */
+int
+straspl(char **argv, char *str, int maxcols)
+{
+ char *ptr, *ptr2;
+ int col;
+
+ for (ptr = str, col = 0; *ptr != '\0' && col < maxcols; ) {
+ for (; *ptr == ' ' || *ptr == '\t'; ptr++) ;
+ if (*ptr != '\0') {
+ for (ptr2 = ptr;
+ *ptr != '\0' && *ptr != ' ' && *ptr != '\t';
+ ptr++) ;
+ if (ptr != ptr2) {
+ if (*ptr != '\0')
+ *ptr++ = '\0';
+ argv[col++] = ptr2;
+ } else
+ break;
+ } else
+ break;
+ }
+
+ return col;
+}
+
+void
+conf_read(noidledisk_t **disks, int *dn)
+{
+ FILE *fh;
+ noidledisk_t *d = NULL;
+ int n = 0, i;
+ char *cols[3] = { NULL, NULL, NULL };
+ char line[CONFIG_LZ + 1];
+
+ if ((fh = fopen(CONFIG, "r")) == NULL)
+ err(EXIT_FAILURE, "fopen(%s)", CONFIG);
+ /* Count non-comment lines matching required number of columns */
+ while (fgets(line, CONFIG_LZ, fh) != NULL) {
+ line[CONFIG_LZ] = '\0';
+ if (*line != '#') {
+ if (straspl(cols, line, 2) == 2)
+ n++;
+ }
+ }
+ (void)fclose(fh);
+
+ /* Allocate structs */
+ if ((d = malloc(sizeof(noidledisk_t) * n)) == NULL)
+ err(EXIT_FAILURE, "malloc()");
+
+ /* Reopen and fill structs */
+ if ((fh = fopen(CONFIG, "r")) == NULL)
+ err(EXIT_FAILURE, "fopen(%s)", CONFIG);
+ i = 0;
+ while (fgets(line, CONFIG_LZ, fh) != NULL) {
+ line[CONFIG_LZ] = '\0';
+ if (*line != '#') {
+ if (straspl(cols, line, 2) == 2) {
+ if ((d[i].dev = strdup(cols[0])) == NULL)
+ err(EXIT_FAILURE, "strdup()");
+ if ((d[i].delay = atoi(cols[1])) < 1)
+ err(EXIT_FAILURE, "atoi(delay) < 1");
+ i++;
+ }
+ }
+ }
+ (void)fclose(fh);
+
+ if (i != n)
+ err(EXIT_FAILURE, "conf_read() - i != n");
+
+ *disks = d;
+ *dn = n;
+}
+
+void
+conf_setup(noidledisk_t *d, int nd)
+{
+ int i;
+
+ for (i = 0; i < nd; i++) {
+ uint8_t *membuf;
+ int pad;
+
+ /*
+ * O_DIRECT has no effect when already using a raw device but
+ * Linux now lacks real raw devices and apparently relies on
+ * this to hopefully offer similar behavior.
+ */
+ if ((d[i].fd = open(d[i].dev, O_RDONLY | O_DIRECT)) == -1)
+ err(EXIT_FAILURE, "open(%s)", d[i].dev);
+ if (disk_getsize(d[i].fd, &(d[i].size)) == -1)
+ err(EXIT_FAILURE, "disk_getsize(%s)", d[i].dev);
+ if ((d[i].bsize = disk_getblocksize(d[i].fd)) == -1)
+ err(EXIT_FAILURE, "disk_getblocksize(%s)", d[i].dev);
+
+ d[i].t = d[i].delay;
+ d[i].offset = 0;
+
+ /*
+ * We need memory to also be block-aligned. Since malloc(3)
+ * will only guarantee 32-bit or 64-bit aligned memory, let's
+ * allocate a larger buffer and use an aligned sub-buffer.
+ */
+ if ((membuf = malloc(65536)) == NULL)
+ err(EXIT_FAILURE, "malloc(65536)");
+ d[i].sbuf = membuf;
+
+ if ((d[i].sbuf = malloc(d[i].bsize)) == NULL)
+ err(EXIT_FAILURE, "malloc(%lu)",
+ (unsigned long)d[i].bsize);
+ if ((pad = (uintptr_t)d[i].sbuf % d->bsize) > 0)
+ pad = d->bsize - pad;
+ d[i].sbuf += pad;
+ if (d[i].sbuf + d->bsize > &membuf[65536])
+ err(EXIT_FAILURE,
+ "Device block size for %s larger than expected!",
+ d[i].dev);
+
+ (void)printf("hdnoidle: device=%s, size=%" PRIu64 ", "
+ "blocksize=%lu, delay=%u\n",
+ d[i].dev, d[i].size, (unsigned long)d[i].bsize,
+ d[i].delay);
+ }
+}
+
+int
+disk_getsize(int fd, off_t *size)
+{
+#if defined (__linux__)
+ return ioctl(fd, BLKGETSIZE64, size);
+#elif defined (__NetBSD__) || defined (__FreeBSD__)
+ return ioctl(fd, DIOCGMEDIASIZE, size);
+#else
+#error "disk_getsize() unported."
+#endif
+}
+
+ssize_t
+disk_getblocksize(int fd)
+{
+
+#if defined (__linux__)
+ int size1, size2;
+
+ /* XXX Apparently BLKSSZGET too */
+ if (ioctl(fd, BLKPBSZGET, &size1) == -1)
+ return -1;
+ if (ioctl(fd, BLKBSZGET, &size2) == -1)
+ return -1;
+ return (ssize_t)(size1 > size2 ? size1 : size2);
+
+#elif defined (__NetBSD__) || defined (__FreeBSD__)
+ unsigned int size;
+
+ if (ioctl(fd, DIOCGSECTORSIZE, &size) == -1)
+ return -1;
+ return (ssize_t)size;
+
+#else
+#error "disk_getblocksize() unported."
+#endif
+}
+
+int
+main(void)
+{
+ noidledisk_t *disks = NULL;
+ int nd = 0, i;
+
+ conf_read(&disks, &nd);
+ conf_setup(disks, nd);
+
+ /*
+ * Loop through configured disks, reading data and updating the offset
+ * when their timer expires.
+ */
+ for (;;) {
+ (void)sleep(1);
+ for (i = 0; i < nd; i++) {
+ noidledisk_t *d = &disks[i];
+
+ if (--d->t < 1) {
+ off_t pad;
+
+ d->t = d->delay;
+ if (lseek(d->fd, d->offset, SEEK_SET) == -1) {
+ warn("seek(%s, %" PRIu64 ")", d->dev,
+ d->offset);
+ continue;
+ }
+ if (read(d->fd, d->sbuf, d->bsize) == -1)
+ warn("read(dev=%s (fd=%d), buf=%p, "
+ "sz=%lu) off=%" PRIu64,
+ d->dev, d->fd, d->sbuf,
+ (unsigned long)d->bsize,
+ d->offset);
+ /*
+ * Skip forward somewhat arbitrarily hoping to
+ * hit a non-cached block and to defeat
+ * readahead.
+ */
+ d->offset += (d->bsize * 1000) +
+ (random() % (d->bsize * 100000));
+ /* Align to block size */
+ if ((pad = d->offset % d->bsize) > 0)
+ pad = d->bsize - pad;
+ d->offset += pad;
+ /* Wrap if overflow */
+ if (d->offset + d->bsize >= d->size)
+ d->offset = 0;
+ }
+ }
+ }
+
+ exit(EXIT_SUCCESS);
+}