From: Matthew Mondor Date: Wed, 2 Aug 2023 02:01:59 +0000 (+0000) Subject: Import new hdnoidle.c, a tool for WD disks with forced idle3. X-Git-Url: http://git.pulsar-zone.net/?a=commitdiff_plain;h=a9412d9ee0c05036bf7fb0c0473092ee3ee29e5c;p=mmondor.git Import new hdnoidle.c, a tool for WD disks with forced idle3. --- diff --git a/mmsoftware/util/hdnoidle.c b/mmsoftware/util/hdnoidle.c new file mode 100644 index 0000000..be6576c --- /dev/null +++ b/mmsoftware/util/hdnoidle.c @@ -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 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/rwdd or /dev/sd) + * 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#ifdef __linux__ +//#include /* O_DIRECT */ +#include /* BLKGETSIZE64 */ +#include /* BLKPBSZGET */ +#include /* open(2) */ +#include /* open(2) */ +#endif +#if defined (__NetBSD__) || defined (__FreeBSD__) +#include /* 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); +}