[Linux][C言語] ハードディスクのS.M.A.R.T.情報を自作プログラムで取得する

常時稼働しているLinuxマシンでハードディスクの状態を定期的にチェックして、異常な状態を早めに把握できるようにするため、libatasmartを使って、ハードディスクのS.M.A.R.T.情報をチェックするプログラムを作成しましたので、その概要を紹介します

# 用意するもの

* C言語の開発環境
* S.M.A.R.T.のライブラリ libatasmart (http://0pointer.de/blog/projects/being-smart.html)

libatasmartは各ディストリビューションからパッケージがあれば、yumやemerge、apt-getなどのコマンドでインストールしてください。

もし用意されていなければ、以下からソースコードを取得します

http://0pointer.de/public/libatasmart-0.19.tar.xz

libatasmartのコンパイルは prefix と libdir(64bit環境の場合) くらいを入れておけば良いと思います

<pre>
$ ./configure --prefix=/usr --libdir=/usr/lib64
$ make
$ sudo make install
</pre>

# サンプルコード

ライブラリのインストールができたら、以下のコードをエディタに入れてコンパイルしてみましょう

```c:check_hd_health.c
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <atasmart.h>  /* libatasmart */

void usage(const char *command) {
  printf("Usage: %s DEVICE\n", command);
}

int get_hd_health(const char *disk) {
  int ret = -1;
  SkDisk *skdisk;
  SkBool smart_available = 0;
  SkBool disk_awake = 0;
  SkBool disk_status = 0;
  uint64_t disk_temp = 0;
  uint64_t disk_ontime = 0;

  if (sk_disk_open(disk, &skdisk) < 0) {
    fprintf(stderr, "Failed to open disk %s: %s(%d)\n", disk, strerror(errno), errno);
    return -1;
  }

  if (sk_disk_smart_is_available(skdisk, &smart_available) < 0) {
    fprintf(stderr, "Failed to query whether SMART is available %s: %s(%d)\n", disk, strerror(errno), errno);
    goto done;
  }
  if (!smart_available) {
    fprintf(stderr, "%s is not support SMART\n", disk);
    goto done;
  }

  if (sk_disk_smart_read_data(skdisk) < 0) {
    fprintf(stderr, "Failed to read SMART data %s: %s(%d)\n", disk, strerror(errno), errno);
    goto done;
  }

  if (sk_disk_check_sleep_mode(skdisk, &disk_awake) < 0) {
    fprintf(stderr, "Failed to get sleep mode\n");
  } else {
    printf("drive is %s\n", disk_awake ? "active" : "sleep");
  }

  ret = 0;
  if (sk_disk_smart_get_temperature(skdisk, &disk_temp) == 0) {
    uint64_t celsius = (disk_temp - 273150) / 1000;  /* convert milli kelvin to celsius */
    printf("drive temperture: %zu C\n", celsius);
  }

  if (sk_disk_smart_get_power_on(skdisk, &disk_ontime) == 0) {
    unsigned long long onhour = disk_ontime / 1000 / 3600;
    printf("drive power on hour: %llu h\n", onhour);
  }

  if (sk_disk_smart_status(skdisk, &disk_status) == 0) {
    printf("drive status: %s\n", disk_status ? "GOOD" : "BAD");
  }

done:
  sk_disk_free(skdisk);

  return ret;
}

int main(int argc, char *argv[]) {
  int ret;

  if (argc <= 1) {
    usage(argv[0]);
    return 1;
  }

  ret = get_hd_health(argv[1]);

  return ret;
}
```

## コンパイル

<pre>
$ gcc -o check_hd_health check_hd_health.c -latasmart -Wall
</pre>

何もエラーが出なければ、実行バイナリが出来上がるはずです。

## 使い方

引数にハードディスクのデバイス(/dev/sdaなど)を指定してください

<pre>
./check_hd_health %%HDDデバイス%%
</pre>

以下の内容が出力されます

1. ディスクのsleep状態 (active / sleep)
2. ハードディスクの温度
3. 通電時間
4. ハードディスクの健康状態 (GOOD / BAD)

実行例

<pre>
./check_hd_health /dev/sdc
drive is active
drive temperture: 51 C
drive power on hour: 4418 h
drive status: GOOD
</pre>

# APIの説明

サンプルで上げた以外に使えそうなAPIも含めて、libatasmartに用意されているものを示します

| API | 説明 |
|:----|:-----|
| int sk_disk_open(const char *name, SkDisk **d) | SkDiskインスタンスを生成します。nameにハードディスクのデバイスを指定してください|
| int sk_disk_smart_read_data(SkDisk *d) | S.M.A.R.T.の情報を取得する時に実行します |
| void sk_disk_free(SkDisk *d) | SkDiskインスタンスを解放します |
| int sk_disk_get_size(SkDisk *d, uint64_t *bytes) | ハードディスクの容量を取得します |
| int sk_disk_check_sleep_mode(SkDisk *d, SkBool *awake) | ハードディスクの(awake/sleep)状態を取得します |
| int sk_disk_identify_is_available(SkDisk *d, SkBool *available) | _不明_ |
| int sk_disk_identify_parse(SkDisk *d, const SkIdentifyParsedData **data) | _不明_ |
| int sk_disk_smart_is_available(SkDisk *d, SkBool *available) | S.M.A.R.T.が有効であることを確認します |
| int sk_disk_smart_status(SkDisk *d, SkBool *good) | ディスクの健康状態(GOOD/BAD)を取得します |
| int sk_disk_get_blob(SkDisk *d, const void **blob, size_t *size) | _不明_ |
| int sk_disk_set_blob(SkDisk *d, const void *blob, size_t size) | _不明_ |
| int sk_disk_smart_parse(SkDisk *d, const SkSmartParsedData **data) | _不明_ |
| int sk_disk_smart_parse_attributes(SkDisk *d, SkSmartAttributeParseCallback cb, void* userdata) | 任意の項目のS.M.A.R.T.情報を取得する時に使います|
| int sk_disk_smart_self_test(SkDisk *d, SkSmartSelfTest test) | 自己診断を実行します |
| int sk_disk_smart_get_power_on(SkDisk *d, uint64_t *mseconds) | ハードディスクの通電時間を取得します |
| int sk_disk_smart_get_power_cycle(SkDisk *d, uint64_t *count) | 電源ONにされた回数を取得します |
| int sk_disk_smart_get_bad(SkDisk *d, uint64_t *sectors) | 不良セクタの数を取得します |
| int sk_disk_smart_get_temperature(SkDisk *d, uint64_t *mkelvin) | ハードディスクの温度を取得します。単位がミリケルビンなので、℃表記にするには273150を引いて1000で割ってください |
| int sk_disk_smart_get_overall(SkDisk *d, SkSmartOverall *overall) | _不明_ |
| int sk_disk_dump(SkDisk *d) | S.M.A.R.T.の情報を全て画面に表示します |

※ 公式のドキュメントがリンク切れになってましたので、ソースコードを読んで推測しています。不明な物もありますし、間違いがあるかもしれませんがご了承ください

# 任意のS.M.A.R.T.の情報を得るには

APIから直接取得できる値もいくつかありますが、それ以外の値を取得するには、sk_disk_smart_parse_attributes を使います

<pre>
int sk_disk_smart_parse_attributes(SkDisk d, SkSmartAttributeParseCallback cb, void userdata)
</pre>

第2引数で指定した関数がS.M.A.R.T.項目毎に呼ばれますので、関数の中で目的のidが来た時に値を取得するようにします

※ IDについては[wikipedia](https://ja.wikipedia.org/wiki/Self-Monitoring,_Analysis_and_Reporting_Technology "wikipedia - Self-Monitoring, Analysis and Reporting Technology")などでご確認ください

第3引数で、第2引数の中で取得した値を入れる変数のポインタを指定します

渡された、SkSmartAttributeParsedData構造体の中のpretty_valueから値がuint64_t型で取得できます

## 例) udma-crc-error-count を取得する

```c:get_errorcount.c
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <atasmart.h>  /* libatasmart */

void usage(const char *command) {
  printf("Usage: %s DEVICE\n", command);
}

static void get_errorcount_cb(SkDisk *d, const SkSmartAttributeParsedData *a, uint64_t *value) {
    if (a->id != 199)  /* udma-crc-error-count */
            return;

    *value = a->pretty_value;
}

int get_errorcount(const char *disk) {
  int ret = -1;
  uint64_t value;
  SkDisk *skdisk;

  if (sk_disk_open(disk, &skdisk) < 0) {
    fprintf(stderr, "Failed to open disk %s: %s(%d)\n", disk, strerror(errno), errno);
    return -1;
  }

  if (sk_disk_smart_read_data(skdisk) < 0) {
    fprintf(stderr, "Failed to read SMART data %s: %s(%d)\n", disk, strerror(errno), errno);
    goto done;
  }

  if (sk_disk_smart_parse_attributes(skdisk, (SkSmartAttributeParseCallback) get_errorcount_cb, &value) < 0) {
    fprintf(stderr, "Failed to get attribute: %s(%d)\n", strerror(errno), errno);
    goto done;
  }

  printf("udma-crc-error-count: %zu\n", value);
  ret = 0;

done:
  sk_disk_free(skdisk);

  return ret;
}

int main(int argc, char *argv[]) {
  int ret;

  if (argc <= 1) {
    usage(argv[0]);
    return 1;
  }

  ret = get_errorcount(argv[1]);

  return ret;
}
```

※ 注意
get_errorcount_cb() は SkSmartAttributeParseCallback型で何かの値を返して、動作を変える事も出来るのかもしれませんが、今回は値が取れれば十分ですので、戻り値をstatic voidにしています

# 補足

ハードディスクのデバイスにはroot権限が必要な場合があるため、一般ユーザーで実行するとpermission deniedになってしまうことがあります。

<pre>
$ ./check_hd_health /dev/sda
Failed to open disk /dev/sda: Permission denied(13)
</pre>

そんな時には check_hd_health のオーナーをrootにしてSUID (Set User ID)を付与すれば、一般ユーザーでも実行可能になります

<pre>
# chown root check_hd_health
# chmod 4755 check_hd_health
</pre>

ls で見た時に、ユーザーのパーミッションが rws (3つ目が"s")となっていればSUIDがついている状態です

<pre>
# ls -l check_hd_health
-rwsr-xr-x 1 root users 17048 11月  1 20:16 check_hd_health
</pre>