Clojure on Linuxで、CPUの使用率を求めようと、slurpで/proc/statを読み込もうとしたところ、
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でIOExceptionが発生するのはなぜ?
まず、FileInputStream.availableを/proc下のファイルに対して呼ぶとIOExceptionが発生するわけですが、これがなぜslurp関数で発生するかという点についてです。
slurpでは内部でclojure.java.io/readerでファイルをオープンしています。
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()を呼びます。そのため、今回のIOExeptionが発生してしまうのです。
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用のコードは以下のところにあります。
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になります。で、調べると
} else if ((end = ::lseek64(fd, 0L, SEEK_END)) == -1) {
ここでlseek64が-1を返しているようです。/procはprocfsで実際のファイルシステムではないので、ファイルの終了位置にseekすることができない、という事情のようです。
まとめ
slurpやclojure.java.ioの関数をつかうと、どうしてもBufferedInputStreamでラッピングされてしまうので、/procのファイルを読む場合は、自前でFileReader
およびBufferedReader
しましょう。
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"