Import new hdnoidle.c, a tool for WD disks with forced idle3.
authorMatthew Mondor <mmondor@pulsar-zone.net>
Wed, 2 Aug 2023 02:01:59 +0000 (02:01 +0000)
committerMatthew Mondor <mmondor@pulsar-zone.net>
Wed, 2 Aug 2023 02:01:59 +0000 (02:01 +0000)
mmsoftware/util/hdnoidle.c [new file with mode: 0644]

diff --git a/mmsoftware/util/hdnoidle.c b/mmsoftware/util/hdnoidle.c
new file mode 100644 (file)
index 0000000..be6576c
--- /dev/null
@@ -0,0 +1,340 @@
+/*
+ * 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);
+}