3.文件系统

简单的文件系统读写是通过uv_fs_*函数和uv_fs_t结构实现的。

  • 注意:libuv文件系统操作不同于套接字操作。套接字操作使用操作系统提供的非阻塞操作。文件系统操作在内部使用阻塞函数,但是在线程池中调用这些函数,并在需要应用程序交互时通知注册到事件循环中的监视者。

所有文件系统函数都有两种形式——同步和异步。

如果回调为空,同步表单会自动被调用(并阻塞)。函数的返回值是一个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 void on_open(uv_fs_t *req) {
 2     // The request passed to the callback is the same as the one the call setup
 3     // function was passed.
 4     assert(req == &open_req);
 5     if (req->result >= 0) {
 6         iov = uv_buf_init(buffer, sizeof(buffer));
 7         uv_fs_read(uv_default_loop(), &read_req, req->result,
 8                    &iov, 1, -1, on_read);
 9     }
10     else {
11         fprintf(stderr, "error opening file: %s\n", uv_strerror((int)req->result));
12     }
13 }

uv_fs_t的结果字段是uv_fs_open回调时的文件描述符。如果文件成功打开,我们就开始读文件。

uvcat/main.c - 读回调

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

在read调用的情况下,你应该传递一个初始化的缓冲区,在read回调触发之前填充数据。uv_fs_*操作几乎直接映射到某些POSIX函数,因此在这种情况下,EOF通过result为0表示。在流或管道的情况下,UV_EOF常量将作为状态传递。

在这里您可以看到编写异步程序时的一个常见模式。uv_fs_close()调用同步执行。通常一次性的任务,或者作为启动或关闭阶段的一部分完成的任务是同步执行的,因为我们感兴趣的是快速I/O,当程序正在进行它的主要任务和处理多个I/O源。对于单独任务,性能差异通常可以忽略不计,并且可能导致更简单的代码。

使用uv_fs_write()编写文件系统也很简单。您的回调将在写入完成后触发。在我们的例子中,回调只是驱动下一个读取。因此,读和写是通过回调同步进行的。

uvcat/main.c - 写回调

1 void on_write(uv_fs_t *req) {
2     if (req->result < 0) {
3         fprintf(stderr, "Write error: %s\n", uv_strerror((int)req->result));
4     }
5     else {
6         uv_fs_read(uv_default_loop(), &read_req, open_req.result, &iov, 1, -1, on_read);
7     }
8 }
  • 警告:由于文件系统和磁盘驱动器的性能配置方式,“成功”的写操作可能还没有提交到磁盘。

我们在main()中设置多米诺:

uvcat/main.c

1 int main(int argc, char **argv) {
2     uv_fs_open(uv_default_loop(), &open_req, argv[1], O_RDONLY, 0, on_open);
3     uv_run(uv_default_loop(), UV_RUN_DEFAULT);
4 
5     uv_fs_req_cleanup(&open_req);
6     uv_fs_req_cleanup(&read_req);
7     uv_fs_req_cleanup(&write_req);
8     return 0;
9 }
  • 警告:uv_fs_req_cleanup()函数必须在文件系统请求时被调用,以释放libuv中的内部内存分配。

文件系统操作

所有标准文件系统操作(如unlink、rmdir、stat)都是异步支持的,并且具有直观的参数顺序。它们遵循与读/写/打开调用相同的模式,在uv_fs_t中返回结果。结果字段。完整的列表:

文件系统操作列表

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_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_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);

缓冲区和流

libuv中的基本I/O句柄是流(uv_stream_t)。TCP套接字、UDP套接字和用于文件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。这允许流本地文件。下面是一个使用libuv的简单tee实用程序。异步执行所有操作显示了事件I/O的强大功能。这两次写操作不会互相阻塞,但我们必须小心地复制缓冲区数据,以确保在缓冲区被写完之前不会释放缓冲区。

该程序将按如下方式执行:

./uvtee <output_file>

我们开始在需要的文件上打开管道。默认情况下,指向文件的Libuv管道是双向打开的。

uvtee/main.c - 用pipes读取

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

对于使用命名管道的IPC, uv_pipe_init()的第三个参数应该设置为1。这在Processes中有介绍。uv_pipe_open()调用将管道与文件描述符关联起来,在本例中为0(标准输入)。

我们开始监控stdin。当需要新的缓冲区来保存传入数据时,调用alloc_buffer回调。Read_stdin将被这些缓冲区调用。

uvtee/main.c - 读取缓冲区

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

这里使用标准的malloc就足够了,但是您可以使用任何内存分配方案。例如,node.js使用自己的slab分配器将缓冲区与V8对象关联起来。

对于任何错误,读回调nread参数都小于0。这个错误可能是EOF,在这种情况下,我们关闭所有流,使用泛型关闭函数uv_close(),该函数根据其内部类型处理句柄。否则,nread是非负数,我们可以尝试向输出流写入那么多字节。最后记住,缓冲区的分配和回收是应用程序的责任,所以我们释放数据。

如果分配内存失败,分配回调可能返回长度为0的缓冲区。在本例中,read回调调用时出错UV_ENOBUFS。Libuv将继续尝试读取流,所以如果你想在分配失败时停止,你必须显式地调用uv_close()。

可以在nread = 0的情况下调用read回调,这表示此时没有什么可读的。大多数应用程序将忽略这一点。

uvtee/main.c - 写入pipe

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

Write_data()复制从读获得的缓冲区。这个缓冲区不会传递给写完成时触发的写回调。为了解决这个问题,我们在write_req_t中包装一个写请求和一个缓冲区,并在回调中展开它。我们创建一个副本,这样就可以从对write_data的两次调用中彼此独立地释放两个缓冲区。虽然这样的演示程序是可以接受的,但是您可能需要更智能的内存管理,比如在任何主要应用程序中引用计数缓冲区或缓冲区池。

  • 警告:如果您的程序打算与其他程序一起使用,那么它可能有意或无意地正在写入管道。这使得它很容易在接收SIGPIPE时中止。插入这句话是一个好主意:
    signal(SIGPIPE, SIG_IGN)

    在应用程序的初始化阶段。

文件变化事件

所有现代操作系统都提供了api来将监视放在单独的文件或目录上,并在文件被修改时得到通知。Libuv封装了常用的文件更改通知库1。这是libuv中比较不一致的部分之一。文件更改通知系统本身在不同的平台上差异很大,所以让所有内容在任何地方工作都很困难。为了演示,我将构建一个简单的实用程序,它在任何被监视的文件发生变化时运行一个命令:

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

使用uv_fs_event_init()启动文件更改通知:

onchange/main.c - 设置

 1 int 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 }

第三个参数是要监视的实际文件或目录。最后一个参数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 -句柄。句柄的路径字段是手表设置的文件。
  2. const char *filename -如果一个目录正在被监视,这是被更改的文件。仅在Linux和Windows上为非空。甚至在这些平台上也可能是无效的。
  3. int标志——UV_RENAME或UV_CHANGE中的一个,或者两者的位或。
  4. int status -当前为0。

在我们的示例中,我们只是打印参数并使用system()运行命令。

onchange/main.c - 文件更改通知回调

 1 void 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 }
posted @ 2021-05-11 09:47  风吹大风车  阅读(264)  评论(0)    收藏  举报