ファイルシステム

ファイルシステムの単純な読み書きには、uv_fs_*関数とuv_fs_t構造体を使います。

注記

libuvのファイルシステム操作は、ソケット操作とは異なります。ソケット操作では、オペレーティングシステムが提供するノンブロッキング操作を使います。ファイルシステム操作では内部でブロッキング関数を使いますが、これらの関数をスレッドプールで呼び出し、アプリケーションとのやり取りが必要になると、イベントループに登録された監視対象に通知します。

すべてのファイルシステム関数には、同期と非同期の2つの形式があります。

コールバックがnullなら、同期形式が自動的に呼び出され、処理はブロックします。関数の戻り値は、libuvエラーコードです。通常、これは同期呼び出しでのみ有用です。非同期形式はコールバックを渡したときに呼び出され、戻り値は0です。

ファイルの読み書き

ファイル記述子は、次の関数で取得します。

int uv_fs_open(uv_loop_t* loop, uv_fs_t* req, const char* path, int flags, int mode, uv_fs_cb cb)

flagsとmodeは、標準のUnixフラグです。libuvが適切なWindowsのフラグへの変換を処理します。

ファイル記述子は、次の関数で閉じます。

int uv_fs_close(uv_loop_t* loop, uv_fs_t* req, uv_file file, uv_fs_cb cb)

ファイルシステム操作のコールバックは、次のシグネチャを持ちます。

void callback(uv_fs_t* req);

単純なcatの実装を見てみましょう。まず、ファイルを開いたときのコールバックを登録します。

uvcat/main.c - ファイルを開く

 1    // The request passed to the callback is the same as the one the call setup
 2    // function was passed.
 3    assert(req == &open_req);
 4    if (req->result >= 0) {
 5        iov = uv_buf_init(buffer, sizeof(buffer));
 6        uv_fs_read(uv_default_loop(), &read_req, req->result,
 7                   &iov, 1, -1, on_read);
 8    }
 9    else {
10        fprintf(stderr, "error opening file: %s\n", uv_strerror((int)req->result));
11    }
12}
13

resultフィールドは、uv_fs_tでは、uv_fs_openコールバック時にファイル記述子です。ファイルを正常に開けたら、読み取りを開始します。

uvcat/main.c - 読み取りコールバック

 1    if (req->result < 0) {
 2        fprintf(stderr, "Read error: %s\n", uv_strerror(req->result));
 3    }
 4    else if (req->result == 0) {
 5        uv_fs_t close_req;
 6        // synchronous
 7        uv_fs_close(uv_default_loop(), &close_req, open_req.result, NULL);
 8    }
 9    else if (req->result > 0) {
10        iov.len = req->result;
11        uv_fs_write(uv_default_loop(), &write_req, 1, &iov, 1, -1, on_write);
12    }
13}
14

読み取り呼び出しでは、初期化済みのバッファを渡してください。読み取りコールバックが呼び出される前に、このバッファにデータが格納されます。uv_fs_*操作は特定のPOSIX関数にほぼ直接対応するため、この場合のEOFは、resultが0であることで示されます。ストリームやパイプの場合は、代わりにUV_EOF定数がステータスとして渡されます。

ここには、非同期プログラムでよく使われるパターンがあります。uv_fs_close()の呼び出しは同期的に実行します。通常、一度限りのタスクや起動・終了段階の一部として行うタスクは、同期的に実行します。高速なI/Oが必要なのは、プログラムが主要な仕事を行い、複数のI/Oソースを扱っているときだからです。単独のタスクでは、通常、性能差は無視できるほど小さく、同期処理の方がコードを単純にできることがあります。

ファイルシステムへの書き込みも、uv_fs_write()を使えば同様に簡単です。コールバックは、書き込みが完了した後に呼び出されます。この例では、コールバックは単に次の読み取りを開始します。このように、読み取りと書き込みはコールバックを通じて交互に進みます。

uvcat/main.c - 書き込みコールバック

1    if (req->result < 0) {
2        fprintf(stderr, "Write error: %s\n", uv_strerror((int)req->result));
3    }
4    else {
5        uv_fs_read(uv_default_loop(), &read_req, open_req.result, &iov, 1, -1, on_read);
6    }
7}
8

警告

ファイルシステムやディスクドライブは性能を重視して構成されているため、「成功」した書き込みでも、まだディスクに確定していない場合があります。

処理の連鎖を開始するのは、main()です。

uvcat/main.c

1    uv_fs_open(uv_default_loop(), &open_req, argv[1], O_RDONLY, 0, on_open);
2    uv_run(uv_default_loop(), UV_RUN_DEFAULT);
3
4    uv_fs_req_cleanup(&open_req);
5    uv_fs_req_cleanup(&read_req);
6    uv_fs_req_cleanup(&write_req);
7    return 0;
8}

警告

uv_fs_req_cleanup()関数は、libuv内部で割り当てられたメモリを解放するため、ファイルシステム要求に対して必ず呼び出す必要があります。

ファイルシステム操作

unlink、rmdir、statなど、標準的なファイルシステム操作はすべて非同期でサポートされており、引数の順序も直感的です。読み取り・書き込み・オープン呼び出しと同じパターンに従い、結果をuv_fs_t.resultフィールドに返します。全一覧は次のとおりです。

ファイルシステム操作

int uv_fs_close(uv_loop_t* loop, uv_fs_t* req, uv_file file, uv_fs_cb cb);
int uv_fs_open(uv_loop_t* loop, uv_fs_t* req, const char* path, int flags, int mode, uv_fs_cb cb);
int uv_fs_read(uv_loop_t* loop, uv_fs_t* req, uv_file file, const uv_buf_t bufs[], unsigned int nbufs, int64_t offset, uv_fs_cb cb);
int uv_fs_unlink(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_fs_cb cb);
int uv_fs_write(uv_loop_t* loop, uv_fs_t* req, uv_file file, const uv_buf_t bufs[], unsigned int nbufs, int64_t offset, uv_fs_cb cb);
int uv_fs_copyfile(uv_loop_t* loop, uv_fs_t* req, const char* path, const char* new_path, int flags, uv_fs_cb cb);
int uv_fs_mkdir(uv_loop_t* loop, uv_fs_t* req, const char* path, int mode, uv_fs_cb cb);
int uv_fs_mkdtemp(uv_loop_t* loop, uv_fs_t* req, const char* tpl, uv_fs_cb cb);
int uv_fs_mkstemp(uv_loop_t* loop, uv_fs_t* req, const char* tpl, uv_fs_cb cb);
int uv_fs_rmdir(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_fs_cb cb);
int uv_fs_scandir(uv_loop_t* loop, uv_fs_t* req, const char* path, int flags, uv_fs_cb cb);
int uv_fs_scandir_next(uv_fs_t* req, uv_dirent_t* ent);
int uv_fs_opendir(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_fs_cb cb);
int uv_fs_readdir(uv_loop_t* loop, uv_fs_t* req, uv_dir_t* dir, uv_fs_cb cb);
int uv_fs_closedir(uv_loop_t* loop, uv_fs_t* req, uv_dir_t* dir, uv_fs_cb cb);
int uv_fs_stat(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_fs_cb cb);
int uv_fs_fstat(uv_loop_t* loop, uv_fs_t* req, uv_file file, uv_fs_cb cb);
int uv_fs_rename(uv_loop_t* loop, uv_fs_t* req, const char* path, const char* new_path, uv_fs_cb cb);
int uv_fs_fsync(uv_loop_t* loop, uv_fs_t* req, uv_file file, uv_fs_cb cb);
int uv_fs_fdatasync(uv_loop_t* loop, uv_fs_t* req, uv_file file, uv_fs_cb cb);
int uv_fs_ftruncate(uv_loop_t* loop, uv_fs_t* req, uv_file file, int64_t offset, uv_fs_cb cb);
int uv_fs_sendfile(uv_loop_t* loop, uv_fs_t* req, uv_file out_fd, uv_file in_fd, int64_t in_offset, size_t length, uv_fs_cb cb);
int uv_fs_access(uv_loop_t* loop, uv_fs_t* req, const char* path, int mode, uv_fs_cb cb);
int uv_fs_chmod(uv_loop_t* loop, uv_fs_t* req, const char* path, int mode, uv_fs_cb cb);
int uv_fs_utime(uv_loop_t* loop, uv_fs_t* req, const char* path, double atime, double mtime, uv_fs_cb cb);
int uv_fs_futime(uv_loop_t* loop, uv_fs_t* req, uv_file file, double atime, double mtime, uv_fs_cb cb);
int uv_fs_lutime(uv_loop_t* loop, uv_fs_t* req, const char* path, double atime, double mtime, uv_fs_cb cb);
int uv_fs_lstat(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_fs_cb cb);
int uv_fs_link(uv_loop_t* loop, uv_fs_t* req, const char* path, const char* new_path, uv_fs_cb cb);
int uv_fs_symlink(uv_loop_t* loop, uv_fs_t* req, const char* path, const char* new_path, int flags, uv_fs_cb cb);
int uv_fs_readlink(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_fs_cb cb);
int uv_fs_realpath(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_fs_cb cb);
int uv_fs_fchmod(uv_loop_t* loop, uv_fs_t* req, uv_file file, int mode, uv_fs_cb cb);
int uv_fs_chown(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_uid_t uid, uv_gid_t gid, uv_fs_cb cb);
int uv_fs_fchown(uv_loop_t* loop, uv_fs_t* req, uv_file file, uv_uid_t uid, uv_gid_t gid, uv_fs_cb cb);
int uv_fs_lchown(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_uid_t uid, uv_gid_t gid, uv_fs_cb cb);
int uv_fs_statfs(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_fs_cb cb);

バッファとストリーム

libuvの基本的なI/Oハンドルはストリーム(uv_stream_t)です。TCPソケット、TTY、ファイルI/OやIPCのためのパイプは、すべてストリームのサブクラスとして扱われます。

ストリームは、サブクラスごとの専用関数で初期化し、その後、次の関数で操作します。

int uv_read_start(uv_stream_t*, uv_alloc_cb alloc_cb, uv_read_cb read_cb);
int uv_read_stop(uv_stream_t*);
int uv_write(uv_write_t* req, uv_stream_t* handle,
             const uv_buf_t bufs[], unsigned int nbufs, uv_write_cb cb);

ストリーム用の関数は、ファイルシステム用の関数よりも簡単に使えます。uv_read_start()を一度呼び出すと、libuvは、uv_read_stop()を呼び出すまで自動的にストリームから読み取り続けます。

データの個々の単位はバッファ、すなわちuv_buf_tです。これは単に、バイト列へのポインタ(uv_buf_t.base)と長さ(uv_buf_t.len)をまとめたものです。uv_buf_tは軽量で、値渡しされます。管理が必要なのは実際のバイト列であり、アプリケーションが割り当てと解放を行わなければなりません。

エラー

このプログラムは常に動作するわけではありません。より良いものが必要です

ストリームの実演には、uv_pipe_tを使う必要があります。これにより、ローカルファイルをストリームとして扱えます[2]。ここではlibuvを使った単純なteeユーティリティを示します。すべての操作を非同期で行うことで、イベント駆動I/Oの力がわかります。2つの書き込みは互いをブロックしませんが、書き込みが完了するまでバッファを解放しないよう、バッファのデータを慎重にコピーする必要があります。

プログラムは、次のように実行します。

./uvtee <output_file>

まず、必要なファイルに対してパイプを開きます。libuvでは、ファイルへのパイプはデフォルトで双方向として開かれます。

uvtee/main.c - パイプからの読み取り

 1    loop = uv_default_loop();
 2
 3    uv_pipe_init(loop, &stdin_pipe, 0);
 4    uv_pipe_open(&stdin_pipe, 0);
 5
 6    uv_pipe_init(loop, &stdout_pipe, 0);
 7    uv_pipe_open(&stdout_pipe, 1);
 8    
 9    uv_fs_t file_req;
10    int fd = uv_fs_open(loop, &file_req, argv[1], O_CREAT | O_RDWR, 0644, NULL);
11    uv_pipe_init(loop, &file_pipe, 0);
12    uv_pipe_open(&file_pipe, fd);
13
14    uv_read_start((uv_stream_t*)&stdin_pipe, alloc_buffer, read_stdin);
15
16    uv_run(loop, UV_RUN_DEFAULT);
17    return 0;
18}

uv_pipe_init()の第3引数は、名前付きパイプによるIPCでは1に設定する必要があります。これについては、プロセスで説明します。uv_pipe_open()の呼び出しは、パイプをファイル記述子、この例では0(標準入力)に関連付けます。

stdinの監視を開始します。alloc_bufferコールバックは、入力データを保持する新しいバッファが必要になると呼び出されます。read_stdinは、これらのバッファを渡して呼び出されます。

uvtee/main.c - バッファの読み取り

 1    *buf = uv_buf_init((char*) malloc(suggested_size), suggested_size);
 2}
 3
 4void free_write_req(uv_write_t *req) {
 5    if (nread < 0){
 6        if (nread == UV_EOF){
 7            // end of file
 8            uv_close((uv_handle_t *)&stdin_pipe, NULL);
 9            uv_close((uv_handle_t *)&stdout_pipe, NULL);
10            uv_close((uv_handle_t *)&file_pipe, NULL);
11        }
12    } else if (nread > 0) {
13        write_data((uv_stream_t *)&stdout_pipe, nread, *buf, on_stdout_write);
14        write_data((uv_stream_t *)&file_pipe, nread, *buf, on_file_write);
15    }
16
17    // OK to free buffer as write_data copies it.
18    if (buf->base)
19        free(buf->base);
20}
21

ここでは標準のmallocで十分ですが、任意のメモリ割り当て方式を使えます。たとえば、node.jsは、バッファをV8オブジェクトに関連付ける独自のスラブアロケータを使っています。

読み取りコールバックのnreadパラメータは、エラー時には0未満になります。このエラーはEOFの場合もあり、その場合は汎用のクローズ関数、uv_close()を使ってすべてのストリームを閉じます。この関数は、ハンドルの内部型に応じて処理します。それ以外の場合、nreadは非負の数であり、そのバイト数を出力ストリームに書き込むことができます。最後に、バッファの割り当てと解放はアプリケーションの責任なので、データを解放します。

割り当てコールバックは、メモリの割り当てに失敗すると、長さ0のバッファを返すことがあります。この場合、読み取りコールバックはUV_ENOBUFSエラーで呼び出されます。ただし、libuvは引き続きストリームからの読み取りを試みるため、割り当て失敗時に停止したいなら、明示的にuv_close()を呼び出す必要があります。

読み取りコールバックは、nread = 0で呼び出されることもあります。これは、その時点で読み取るものがないことを示します。ほとんどのアプリケーションでは、単に無視します。

uvtee/main.c - パイプへの書き込み

 1    uv_write_t req;
 2    uv_buf_t buf;
 3} write_req_t;
 4
 5uv_loop_t *loop;
 6    write_req_t *wr = (write_req_t*) req;
 7    free(wr->buf.base);
 8    free(wr);
 9}
10
11void on_stdout_write(uv_write_t *req, int status) {
12    free_write_req(req);
13}
14
15void on_file_write(uv_write_t *req, int status) {
16    free_write_req(req);
17}
18
19void write_data(uv_stream_t *dest, size_t size, uv_buf_t buf, uv_write_cb cb) {
20    write_req_t *req = (write_req_t*) malloc(sizeof(write_req_t));
21    req->buf = uv_buf_init((char*) malloc(size), size);
22    memcpy(req->buf.base, buf.base, size);
23    uv_write((uv_write_t*) req, (uv_stream_t*)dest, &req->buf, 1, cb);
24}
25

write_data()は、読み取りで得られたバッファをコピーします。このバッファは、書き込み完了時に呼び出される書き込みコールバックには引き渡されません。これを解決するため、書き込み要求とバッファをwrite_req_tでまとめ、コールバックで取り出します。コピーを作成することで、2回のwrite_dataの呼び出しによる2つのバッファを、互いに独立して解放できます。このような実演プログラムでは許容できますが、大きなアプリケーションでは、参照カウント付きバッファやバッファプールなど、より賢いメモリ管理を使いたいでしょう。

警告

他のプログラムと組み合わせて使うことを想定したプログラムは、意図的に、あるいは知らずに、パイプに書き込んでいる場合があります。このため、SIGPIPEを受信して異常終了する可能性があります。次の処理を、

signal(SIGPIPE, SIG_IGN)

アプリケーションの初期化段階に入れるとよいでしょう。

ファイル変更イベント

現代のオペレーティングシステムはすべて、個々のファイルやディレクトリを監視し、ファイルが変更されたときに通知を受けるためのAPIを提供しています。libuvは一般的なファイル変更通知ライブラリをラップしています[1]。これはlibuvの中でも一貫性に欠ける部分の1つです。ファイル変更通知の仕組み自体がプラットフォームごとに大きく異なるため、すべての環境で動作させるのは困難です。実演として、監視対象のいずれかのファイルが変更されるたびにコマンドを実行する、単純なユーティリティを作ります。

./onchange <command> <file1> [file2] ...

注記

現在、この例はOSXとWindowsでのみ動作します。uv_fs_event_startの注記を参照してください。

ファイル変更通知は、uv_fs_event_init()で開始します。

onchange/main.c - セットアップ

 1int main(int argc, char **argv) {
 2    if (argc <= 2) {
 3        fprintf(stderr, "Usage: %s <command> <file1> [file2 ...]\n", argv[0]);
 4        return 1;
 5    }
 6
 7    loop = uv_default_loop();
 8    command = argv[1];
 9
10    while (argc-- > 2) {
11        fprintf(stderr, "Adding watch on %s\n", argv[argc]);
12        uv_fs_event_t *fs_event_req = malloc(sizeof(uv_fs_event_t));
13        uv_fs_event_init(loop, fs_event_req);
14        // The recursive flag watches subdirectories too.
15        uv_fs_event_start(fs_event_req, run_command, argv[argc], UV_FS_EVENT_RECURSIVE);
16    }
17
18    return uv_run(loop, UV_RUN_DEFAULT);
19}

第3引数は、実際に監視するファイルまたはディレクトリです。最後の引数、flagsは、次のいずれかです。

/*
 * Flags to be passed to uv_fs_event_start().
 */
enum uv_fs_event_flags {
    UV_FS_EVENT_WATCH_ENTRY = 1,
    UV_FS_EVENT_STAT = 2,
    UV_FS_EVENT_RECURSIVE = 4
};

UV_FS_EVENT_WATCH_ENTRYとUV_FS_EVENT_STATは、(まだ)何もしません。UV_FS_EVENT_RECURSIVEは、対応するプラットフォームでサブディレクトリの監視も開始します。

コールバックは、次の引数を受け取ります。

  1. uv_fs_event_t *handle - ハンドルです。ハンドルのpathフィールドは、監視を設定したファイルです。

  2. const char *filename - ディレクトリを監視している場合は、変更されたファイルです。LinuxとWindowsでのみ非nullになります。ただし、これらのプラットフォームでもnullになる場合があります。

  3. int events - UV_RENAMEまたはUV_CHANGEのいずれか、あるいは両者のビット単位ORです。

  4. int status - status < 0なら、libuvエラーがあります。

この例では、単に引数を表示し、system()を使ってコマンドを実行します。

onchange/main.c - ファイル変更通知コールバック

 1void run_command(uv_fs_event_t *handle, const char *filename, int events, int status) {
 2    char path[1024];
 3    size_t size = 1023;
 4    // Does not handle error if path is longer than 1023.
 5    uv_fs_event_getpath(handle, path, &size);
 6    path[size] = '\0';
 7
 8    fprintf(stderr, "Change detected in %s: ", path);
 9    if (events & UV_RENAME)
10        fprintf(stderr, "renamed");
11    if (events & UV_CHANGE)
12        fprintf(stderr, "changed");
13
14    fprintf(stderr, " %s\n", filename ? filename : "");
15    system(command);
16}