slurpで/proc/statを読み込めない理由

Clojure on Linuxで、CPUの使用率を求めようと、slurpで/proc/statを読み込もうとしたところ、

```clj
user=> (slurp "/proc/stat")
IOException Invalid argument  java.io.FileInputStream.available (FileInputStream.java:-2)
```

のExceptionが発生します。同様の現象がMLにありましたが、ノーレスです。
https://groups.google.com/forum/#!topic/clojure/3465V9GtkGE

ので、調べてみました。

## slurpでIOEｘceptionが発生するのはなぜ?

まず、FileInputStream.availableを/proc下のファイルに対して呼ぶとIOExceptionが発生するわけですが、これがなぜslurp関数で発生するかという点についてです。

slurpでは内部でclojure.java.io/readerでファイルをオープンしています。

```clj
user=> (source slurp)
(defn slurp
  "Opens a reader on f and reads all its contents, returning a string.
  See clojure.java.io/reader for a complete list of supported arguments."
  {:added "1.0"}
  ([f & opts]
     (let [opts (normalize-slurp-opts opts)
           sb (StringBuilder.)]
       (with-open [#^java.io.Reader r (apply jio/reader f opts)]
         (loop [c (.read r)]
           (if (neg? c)
             (str sb)
             (do
               (.append sb (char c))
               (recur (.read r)))))))))
```

このreaderでBufferedReaderを作るのですが、
  FileInputStream → BufferedInputStream → InputStreamReader → BufferedReader
の順でデコレイトされます。BufferedInputStreamが余分に感じますね！

そして、このBufferedInputStreamがこの場合悪さをします。BufferedInputStreamは、Socketからの読み込みのような断続的にデータが読み込み可能となるようなストリームを扱うので、readメソッドの中でavailable()を呼びます。そのため、今回のIOEｘeptionが発生してしまうのです。

```java:BufferedInputStream.java
    public synchronized int read(byte b[], int off, int len)
        throws IOException
    {
        getBufIfOpen(); // Check for closed stream
        if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }

        int n = 0;
        for (;;) {
            int nread = read1(b, off + n, len - n);
            if (nread <= 0)
                return (n == 0) ? nread : n;
            n += nread;
            if (n >= len)
                return n;
            // if not closed but no bytes available, return
            InputStream input = in;
            if (input != null && input.available() <= 0)
                return n;
        }
    }
```

## /proc/statでFileInputStream.availableを呼ぶとIOExceptionが発生するのはなぜ?

FileInputStream.availableのソースを見てみます。が、このメソッドはnativeなので、実際はOSごとに処理がことなります。Linux用のコードは以下のところにあります。

```cpp:hotspot/src/os/linux/vm/os_linux.cpp
int os::available(int fd, jlong *bytes) {
  jlong cur, end;
  int mode;
  struct stat64 buf64;

  if (::fstat64(fd, &buf64) >= 0) {
    mode = buf64.st_mode;
    if (S_ISCHR(mode) || S_ISFIFO(mode) || S_ISSOCK(mode)) {
      /*
      * XXX: is the following call interruptible? If so, this might
      * need to go through the INTERRUPT_IO() wrapper as for other
      * blocking, interruptible calls in this file.
      */
      int n;
      if (::ioctl(fd, FIONREAD, &n) >= 0) {
        *bytes = n;
        return 1;
      }
    }
  }
  if ((cur = ::lseek64(fd, 0L, SEEK_CUR)) == -1) {
    return 0;
  } else if ((end = ::lseek64(fd, 0L, SEEK_END)) == -1) {
    return 0;
  } else if (::lseek64(fd, cur, SEEK_SET) == -1) {
    return 0;
  }
  *bytes = end - cur;
  return 1;
}
```

ここで0が返ると、IOExceptionになります。で、調べると

```cpp
  } else if ((end = ::lseek64(fd, 0L, SEEK_END)) == -1) {
```

ここでlseek64が-1を返しているようです。/procは[procfs](http://ja.wikipedia.org/wiki/Procfs)で実際のファイルシステムではないので、ファイルの終了位置にseekすることができない、という事情のようです。

## まとめ

slurpやclojure.java.ioの関数をつかうと、どうしてもBufferedInputStreamでラッピングされてしまうので、/procのファイルを読む場合は、自前で`FileReader`および`BufferedReader`しましょう。

```clj
user=> (with-open [rdr (-> (java.io.FileReader. "/proc/stat") java.io.BufferedReader.)] (.readLine rdr))
"cpu  1496745 1567 428721 5810283 13961 68 6011 0 0 0"
```