Table of Contents

简介

谁这本书是为

背景

代码

libuv的基础知识

事件循环

helloworld

默认循环

错误处理

把手和要求

空转

存储方面

文件系统

读/写文件

文件系统操作

缓冲器和流

文件更改事件

网络

TCP

服务器

客户

UDP

UDP选项

查询

网络接口

线程

核心线程操作

同步基元

互斥体

其他

libuv工作队列

线程间通信

进程

产卵子进程

改变工艺参数

变更执行目录

设置环境变量

选项标志

拆卸过程

将信号发送到处理

信号

子进程I /

管道

父子

任意过程

通过管道发送文件描述符

高级事件循环

停止事件循环

实用工具


简介

https://nikhilm.github.io/uvbook/introduction.html


这本“书”是一小部分关于使用libuv作为高性能事件I / O库的教程,该库在Windows和Unix上提供相同的API。

它旨在涵盖libuv的主要领域,但不是讨论所有功能和数据结构的全面参考。有关完整的详细信息,请查阅libuv的官方文档。

本书仍在编写中,因此各节可能还不完整,但是我希望您会喜欢它的成长。

谁这本书是为

如果您正在阅读这本书,您将是:

  1. 系统程序员,创建低级程序,例如守护程序或网络服务和客户端。您已经发现事件循环方法非常适合您的应用程序,因此决定使用libuv。
  2. 一个node.js模块编写者,他想用一组暴露于JavaScript的(a)同步API包装用C或C ++编写的平台API。您将仅在node.js上下文中使用libuv。为此,您将需要一些其他资源,因为本书没有涵盖v8 / node.js的特定部分。

本书假定您熟悉C编程语言。

背景

该Node.js的项目于2009年开始为JavaScript环境从浏览器分离。使用Google的V8和Marc Lehmann的libev,node.js结合了事件化的I / O模型和非常适合编程风格的语言。由于浏览器塑造的方式。随着node.js的普及,使其在Windows上运行非常重要,但libev仅在Unix上运行。等效于Windows的内核事件通知机制(如kqueue或(e)poll)为IOCP。libuv是围绕libev或IOCP(取决于平台)的抽象,为用户提供了基于libev的API。在libuv的node-v0.9.0版本中,libev被删除。

从那时起,libuv持续成熟,并成为用于系统编程的高质量独立库。node.js之外的用户包括Mozilla的 Rust编程语言和各种语言绑定。

这本书和代码是基于libuv版本V1.3.0。

代码

本书的所有代码都包含在Github上。克隆/下载书籍,然后构建libuv:

cd libuv
./autogen.sh
./configure
make

没有必要make install。要构建示例,请在 code/目录中运行make

libuv的基础知识

https://nikhilm.github.io/uvbook/basics.html


libuv强制执行异步的事件驱动的编程风格。它的核心工作是提供事件循环以及基于回调的I / O和其他活动的通知。libuv提供了诸如计时器,无阻塞网络支持,异步文件系统访问,子进程等核心实用程序。

事件循环

在事件驱动的编程中,应用程序对某些事件表示兴趣,并在事件发生时做出响应。libuv负责从操作系统收集事件或监视其他事件源,并且用户可以注册事件发生时要调用的回调。事件循环通常永远持续运行。用伪代码:

while there are still events to process:e = get the next eventif there is a callback associated with e:call the callback

事件的一些示例是:

  • 文件已准备好写入
  • 套接字已准备好读取数据
  • 计时器已超时

uv_run()使用libuv时,此事件循环由–最终功能封装。

系统程序最常见的活动是处理输入和输出,而不是大量的数字运算。与使用常规的输入/输出函数(的问题readfprintf等等)是它们 阻塞。与处理器的速度相比,实际写入硬盘或从网络读取花费的时间不成比例。在完成任务之前,函数不会返回,因此您的程序什么也不做。对于需要高性能的程序,这是主要障碍,因为其他活动和其他I / O操作一直处于等待状态。

标准解决方案之一是使用线程。每个阻塞的I / O操作均在单独的线程(或线程池)中启动。当在线程中调用阻塞函数时,处理器可以调度另一个线程运行,这实际上需要CPU。

libuv遵循的方法使用另一种样式,即异步,非阻塞样式。大多数现代操作系统都提供事件通知子系统。例如,read套接字上的常规调用将阻塞,直到发送者实际发送了一些东西为止。而是,应用程序可以请求操作系统监视套接字并将事件通知放入队列中。应用程序可以方便地检查事件(也许在最大程度地使用处理器之前进行一些数字运算)并获取数据。之所以是异步的,是因为应用程序在某个时刻表达了兴趣,然后又在另一点(时间和空间)使用了数据。它是 非阻塞的因为应用程序进程可以自由执行其他任务。这与libuv的事件循环方法非常吻合,因为可以将操作系统事件视为另一个libuv事件。非阻塞确保其他事件可以像它们进入[1]一样快地继续处理。

注意:如何在后台运行I / O无关紧要,但是由于计算机硬件的工作方式(以线程为处理器的基本单元),libuv和OS通常会在后台运行/工作线程和/或轮询以非阻塞方式执行任务。

libuv核心开发人员之一伯特·贝尔德(Bert Belder)有一个小视频,解释了libuv的体系结构及其背景。如果您没有使用libuv或libev的经验,那么它是一种快速而有用的手表。

libuv的事件循环在文档中有更详细的说明。

helloworld

借助基础知识,让我们编写第一个libuv程序。它什么也没做,只是启动一个循环,该循环将立即退出。

helloworld / main.c

#include <stdio.h>
#include <stdlib.h>
#include <uv.h>int main() {uv_loop_t *loop = malloc(sizeof(uv_loop_t));uv_loop_init(loop);printf("Now quitting.\n");uv_run(loop, UV_RUN_DEFAULT);uv_loop_close(loop);free(loop);return 0;
}

该程序立即退出,因为它没有要处理的事件。必须告知libuv事件循环以使用各种AP​​I函数监视事件。

从libuv v1.0开始,用户应先为循环分配内存,然后再使用进行初始化。这使您可以插入自定义内存管理。请记住使用来取消初始化循环 ,然后删除存储。这些示例从不关闭循环,因为在循环结束后程序将退出,并且系统将回收内存。生产级项目,尤其是长期运行的系统程序,应注意正确发布。uv_loop_init(uv_loop_t *)uv_loop_close(uv_loop_t *)

默认循环

默认循环由libuv提供,可以使用来访问 uv_default_loop()。如果只需要一个循环,则应使用此循环。

注意:node.js使用默认循环作为其主循环。如果您正在编写绑定,则应注意这一点。

错误处理

可能失败的初始化函数或同步函数会在错误时返回负数。可能失败的异步函数会将状态参数传递给其回调。错误消息定义为UV_E* 常量。

您可以使用uv_strerror(int)uv_err_name(int)函数来分别描述错误或错误名称。const char *

I / O读取回调(例如文件和套接字)传递给参数nread。如果nread小于0,则存在错误(UV_EOF是文件结尾错误,您可能需要不同的处理方式)。

把手和要求

用户通过libuv表示对特定事件的兴趣。这通常是通过创建I / O设备,计时器或进程的句柄来完成的。句柄是不透明的结构,命名为uv_TYPE_twhere类型表示该句柄的用途。

libuv watchers

/* Handle types. */
typedef struct uv_loop_s uv_loop_t;
typedef struct uv_handle_s uv_handle_t;
typedef struct uv_stream_s uv_stream_t;
typedef struct uv_tcp_s uv_tcp_t;
typedef struct uv_udp_s uv_udp_t;
typedef struct uv_pipe_s uv_pipe_t;
typedef struct uv_tty_s uv_tty_t;
typedef struct uv_poll_s uv_poll_t;
typedef struct uv_timer_s uv_timer_t;
typedef struct uv_prepare_s uv_prepare_t;
typedef struct uv_check_s uv_check_t;
typedef struct uv_idle_s uv_idle_t;
typedef struct uv_async_s uv_async_t;
typedef struct uv_process_s uv_process_t;
typedef struct uv_fs_event_s uv_fs_event_t;
typedef struct uv_fs_poll_s uv_fs_poll_t;
typedef struct uv_signal_s uv_signal_t;/* Request types. */
typedef struct uv_req_s uv_req_t;
typedef struct uv_getaddrinfo_s uv_getaddrinfo_t;
typedef struct uv_getnameinfo_s uv_getnameinfo_t;
typedef struct uv_shutdown_s uv_shutdown_t;
typedef struct uv_write_s uv_write_t;
typedef struct uv_connect_s uv_connect_t;
typedef struct uv_udp_send_s uv_udp_send_t;
typedef struct uv_fs_s uv_fs_t;
typedef struct uv_work_s uv_work_t;/* None of the above. */
typedef struct uv_cpu_info_s uv_cpu_info_t;
typedef struct uv_interface_address_s uv_interface_address_t;
typedef struct uv_dirent_s uv_dirent_t;

手柄代表寿命长的对象。使用请求标识对此类句柄的异步操作。一个请求是短暂的(通常仅在一个回调中使用),并且通常指示一个句柄上的一个I / O操作。请求用于保留各个动作的发起和回调之间的上下文。例如,UDP套接字由表示uv_udp_t,而对套接字的单个写入使用的uv_udp_send_t 结构是在写入完成后传递给回调的。

句柄由相应的设置:

uv_TYPE_init(uv_loop_t *, uv_TYPE_t *)

功能。

回调是libuv在观察者感兴趣的事件发生时调用的函数。特定于应用程序的逻辑通常将在回调中实现。例如,IO观察者的回调将接收从文件读取的数据,计时器回调将在超时时触发,依此类推。

空转

这是使用空闲句柄的示例。在事件循环的每一回合都会调用一次回调。实用程序中讨论了空闲句柄的用例 。让我们使用一个闲置的观察者查看观察者的生命周期,并查看uv_run()由于存在观察者而现在将如何阻塞。当达到计数时,空闲观察者将停止,并uv_run()退出,因为没有活动的观察者处于活动状态。

idle-basic / main.c

#include <stdio.h>
#include <uv.h>int64_t counter = 0;void wait_for_a_while(uv_idle_t* handle) {counter++;if (counter >= 10e6)uv_idle_stop(handle);
}int main() {uv_idle_t idler;uv_idle_init(uv_default_loop(), &idler);uv_idle_start(&idler, wait_for_a_while);printf("Idling...\n");uv_run(uv_default_loop(), UV_RUN_DEFAULT);uv_loop_close(uv_default_loop());return 0;
}

存储方面

在基于回调的编程风格中,您通常需要在调用站点和回调之间传递一些“上下文”(特定于应用程序的信息)。所有句柄和请求都有一个成员,您可以将其设置为上下文并在回调中回退。这是整个C库生态系统中使用的常见模式。另外还具有类似的数据成员。void* datauv_loop_t。

[1]当然取决于硬件的容量。

文件系统

https://nikhilm.github.io/uvbook/filesystem.html


使用uv_fs_*函数和 uv_fs_t结构可以实现简单的文件系统读/写。

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

所有文件系统功能都有两种形式-同步异步

同步的形式自动被调用(和)如果回调为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)

flagsmode是标准的 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-打开文件

void on_open(uv_fs_t *req) {// The request passed to the callback is the same as the one the call setup// function was passed.assert(req == &open_req);if (req->result >= 0) {iov = uv_buf_init(buffer, sizeof(buffer));uv_fs_read(uv_default_loop(), &read_req, req->result,&iov, 1, -1, on_read);}else {fprintf(stderr, "error opening file: %s\n", uv_strerror((int)req->result));}
}

a的result字段uv_fs_t是在发生uv_fs_open回调的情况下的文件描述符 。如果文件成功打开,我们将开始读取它。

uvcat / main.c-读取回调

void on_read(uv_fs_t *req) {if (req->result < 0) {fprintf(stderr, "Read error: %s\n", uv_strerror(req->result));}else if (req->result == 0) {uv_fs_t close_req;// synchronousuv_fs_close(uv_default_loop(), &close_req, open_req.result, NULL);}else if (req->result > 0) {iov.len = req->result;uv_fs_write(uv_default_loop(), &write_req, 1, &iov, 1, -1, on_write);}
}

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

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

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

uvcat / main.c-编写回调

void on_write(uv_fs_t *req) {if (req->result < 0) {fprintf(stderr, "Write error: %s\n", uv_strerror((int)req->result));}else {uv_fs_read(uv_default_loop(), &read_req, open_req.result, &iov, 1, -1, on_read);}
}

警告:由于配置文件系统和磁盘驱动器以提高性能的方式,“成功”的写入可能尚未提交到磁盘。

我们将多米诺骨牌设置为main()

uvcat / main.c

int main(int argc, char **argv) {uv_fs_open(uv_default_loop(), &open_req, argv[1], O_RDONLY, 0, on_open);uv_run(uv_default_loop(), UV_RUN_DEFAULT);uv_fs_req_cleanup(&open_req);uv_fs_req_cleanup(&read_req);uv_fs_req_cleanup(&write_req);return 0;
}

警告:uv_fs_req_cleanup()必须始终在文件系统请求上调用该函数以释放libuv中的内部内存分配。

文件系统操作

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

文件系统操作

UV_EXTERN int uv_fs_close(uv_loop_t* loop,uv_fs_t* req,uv_file file,uv_fs_cb cb);
UV_EXTERN int uv_fs_open(uv_loop_t* loop,uv_fs_t* req,const char* path,int flags,int mode,uv_fs_cb cb);
UV_EXTERN 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);
UV_EXTERN int uv_fs_unlink(uv_loop_t* loop,uv_fs_t* req,const char* path,uv_fs_cb cb);
UV_EXTERN 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);
UV_EXTERN int uv_fs_mkdir(uv_loop_t* loop,uv_fs_t* req,const char* path,int mode,uv_fs_cb cb);
UV_EXTERN int uv_fs_mkdtemp(uv_loop_t* loop,uv_fs_t* req,const char* tpl,uv_fs_cb cb);
UV_EXTERN int uv_fs_rmdir(uv_loop_t* loop,uv_fs_t* req,const char* path,uv_fs_cb cb);
UV_EXTERN int uv_fs_scandir(uv_loop_t* loop,uv_fs_t* req,const char* path,int flags,uv_fs_cb cb);
UV_EXTERN int uv_fs_scandir_next(uv_fs_t* req,uv_dirent_t* ent);
UV_EXTERN int uv_fs_stat(uv_loop_t* loop,uv_fs_t* req,const char* path,uv_fs_cb cb);
UV_EXTERN int uv_fs_fstat(uv_loop_t* loop,uv_fs_t* req,uv_file file,uv_fs_cb cb);
UV_EXTERN int uv_fs_rename(uv_loop_t* loop,uv_fs_t* req,const char* path,const char* new_path,uv_fs_cb cb);
UV_EXTERN int uv_fs_fsync(uv_loop_t* loop,uv_fs_t* req,uv_file file,uv_fs_cb cb);
UV_EXTERN int uv_fs_fdatasync(uv_loop_t* loop,uv_fs_t* req,uv_file file,uv_fs_cb cb);
UV_EXTERN int uv_fs_ftruncate(uv_loop_t* loop,uv_fs_t* req,uv_file file,int64_t offset,uv_fs_cb cb);
UV_EXTERN 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);
UV_EXTERN int uv_fs_access(uv_loop_t* loop,uv_fs_t* req,const char* path,int mode,uv_fs_cb cb);
UV_EXTERN int uv_fs_chmod(uv_loop_t* loop,uv_fs_t* req,const char* path,int mode,uv_fs_cb cb);
UV_EXTERN int uv_fs_utime(uv_loop_t* loop,uv_fs_t* req,const char* path,double atime,double mtime,uv_fs_cb cb);
UV_EXTERN int uv_fs_futime(uv_loop_t* loop,uv_fs_t* req,uv_file file,double atime,double mtime,uv_fs_cb cb);
UV_EXTERN int uv_fs_lstat(uv_loop_t* loop,uv_fs_t* req,const char* path,uv_fs_cb cb);
UV_EXTERN int uv_fs_link(uv_loop_t* loop,uv_fs_t* req,const char* path,const char* new_path,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);

基于流的函数比文件系统函数更易于使用,并且libuv会在uv_read_start()被调用一次(直到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的功能。两次写入不会相互阻塞,但是我们必须小心复制缓冲区数据,以确保在写入缓冲区之前不释放缓冲区。

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

./uvtee <output_file>

我们开始在所需文件上打开管道。默认情况下,通往文件的libuv管道是双向打开的。

uvtee / main.c-在管道上阅读

int main(int argc, char **argv) {loop = uv_default_loop();uv_pipe_init(loop, &stdin_pipe, 0);uv_pipe_open(&stdin_pipe, 0);uv_pipe_init(loop, &stdout_pipe, 0);uv_pipe_open(&stdout_pipe, 1);uv_fs_t file_req;int fd = uv_fs_open(loop, &file_req, argv[1], O_CREAT | O_RDWR, 0644, NULL);uv_pipe_init(loop, &file_pipe, 0);uv_pipe_open(&file_pipe, fd);uv_read_start((uv_stream_t*)&stdin_pipe, alloc_buffer, read_stdin);uv_run(loop, UV_RUN_DEFAULT);return 0;
}

uv_pipe_init()对于使用命名管道的IPC ,应将第三个参数设置为1。这在“过程”中进行了介绍。该uv_pipe_open()呼叫的管与文件描述符相关联,在这种情况下0(标准输入)。

我们开始监视stdin。在alloc_buffer新的缓冲区,须持有传入的数据调用回调函数。read_stdin将使用这些缓冲区进行调用。

uvtee / main.c-读取缓冲区

void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {*buf = uv_buf_init((char*) malloc(suggested_size), suggested_size);
}void read_stdin(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {if (nread < 0){if (nread == UV_EOF){// end of fileuv_close((uv_handle_t *)&stdin_pipe, NULL);uv_close((uv_handle_t *)&stdout_pipe, NULL);uv_close((uv_handle_t *)&file_pipe, NULL);}} else if (nread > 0) {write_data((uv_stream_t *)&stdout_pipe, nread, *buf, on_stdout_write);write_data((uv_stream_t *)&file_pipe, nread, *buf, on_file_write);}// OK to free buffer as write_data copies it.if (buf->base)free(buf->base);
}

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

发生nread任何错误时,read回调参数均小于0。此错误可能是EOF,在这种情况下,我们将使用uv_close()基于其内部类型处理该句柄的通用关闭函数来关闭所有流。否则nread为非负数,我们可以尝试将那么多字节写入输出流。最后,请记住缓冲区分配和释放是应用程序的责任,因此我们释放了数据。

如果分配回调未能分配内存,则它可能返回长度为零的缓冲区。在这种情况下,将使用错误UV_ENOBUFS调用读取回调。libuv将继续尝试读取流,因此,uv_close()如果要在分配失败时停止,则必须显式调用。

可以使用调用read回调,指示此时没有要读取的内容。大多数应用程序只会忽略这一点。nread = 0

uvtee / main.c-写入管道

typedef struct {uv_write_t req;uv_buf_t buf;
} write_req_t;void free_write_req(uv_write_t *req) {write_req_t *wr = (write_req_t*) req;free(wr->buf.base);free(wr);
}void on_stdout_write(uv_write_t *req, int status) {free_write_req(req);
}void on_file_write(uv_write_t *req, int status) {free_write_req(req);
}void write_data(uv_stream_t *dest, size_t size, uv_buf_t buf, uv_write_cb cb) {write_req_t *req = (write_req_t*) malloc(sizeof(write_req_t));req->buf = uv_buf_init((char*) malloc(size), size);memcpy(req->buf.base, buf.base, size);uv_write((uv_write_t*) req, (uv_stream_t*)dest, &req->buf, 1, cb);
}

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-设置

int main(int argc, char **argv) {if (argc <= 2) {fprintf(stderr, "Usage: %s <command> <file1> [file2 ...]\n", argv[0]);return 1;}loop = uv_default_loop();command = argv[1];while (argc-- > 2) {fprintf(stderr, "Adding watch on %s\n", argv[argc]);uv_fs_event_t *fs_event_req = malloc(sizeof(uv_fs_event_t));uv_fs_event_init(loop, fs_event_req);// The recursive flag watches subdirectories too.uv_fs_event_start(fs_event_req, run_command, argv[argc], UV_FS_EVENT_RECURSIVE);}return uv_run(loop, UV_RUN_DEFAULT);
}

第三个参数是要监视的实际文件或目录。最后一个参数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-如果正在监视目录,则该文件已更改。仅适用null于Linux和Windows。可能null 即使在这些平台上。

  3. int flags-的UV_RENAMEUV_CHANGE,或的按位或

    都。

  4. int status -目前为0。

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

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

void run_command(uv_fs_event_t *handle, const char *filename, int events, int status) {char path[1024];size_t size = 1023;// Does not handle error if path is longer than 1023.uv_fs_event_getpath(handle, path, &size);path[size] = '\0';fprintf(stderr, "Change detected in %s: ", path);if (events & UV_RENAME)fprintf(stderr, "renamed");if (events & UV_CHANGE)fprintf(stderr, "changed");fprintf(stderr, " %s\n", filename ? filename : "");system(command);
}

[1]inotify on Linux, FSEvents on Darwin, kqueue on BSDs, ReadDirectoryChangesW on Windows, event ports on Solaris, unsupported on Cygwin

[2]see Pipes

网络

https://nikhilm.github.io/uvbook/networking.html


libuv中的网络与直接使用BSD套接字接口没有太大区别,有些事情更容易实现,所有都是无阻塞的,但是概念保持不变。此外,libuv还提供实用程序功能来抽象烦人,重复和低级的任务,例如使用BSD套接字结构设置套接字,DNS查找以及调整各种套接字参数。

uv_tcp_tuv_udp_t结构用于网络I / O。

注意:本章中的代码示例用于显示某些libuv API。它们不是高质量代码的示例。它们会泄漏内存,并不一定总是正确关闭连接。

TCP

TCP是面向连接的流协议,因此基于libuv流基础结构。

服务器

服务器套接字通过以下方式进行:

  1. uv_tcp_init TCP句柄。
  2. uv_tcp_bind 它。
  3. 通话uv_listen手柄上有每当一个新的连接是由客户端建立了一个调用的回调。
  4. 使用uv_accept接受连接。
  5. 使用流操作与客户端进行通信。

这是一个简单的回显服务器

tcp-echo-server / main.c-监听套接字

int main() {loop = uv_default_loop();uv_tcp_t server;uv_tcp_init(loop, &server);uv_ip4_addr("0.0.0.0", DEFAULT_PORT, &addr);uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);int r = uv_listen((uv_stream_t*) &server, DEFAULT_BACKLOG, on_new_connection);if (r) {fprintf(stderr, "Listen error %s\n", uv_strerror(r));return 1;}return uv_run(loop, UV_RUN_DEFAULT);
}

您可以看到该实用程序功能uv_ip4_addr正在用于从人类可读的IP地址,端口对转换为BSD套接字API所需的sockaddr_in结构。可以使用来获得相反的结果uv_ip4_name

注意:uv_ip6_*ip4功能有类似物。

大多数设置功能都是同步的,因为它们受CPU限制。 uv_listen是我们返回libuv的回调样式的地方。第二个参数是积压队列–排队连接的最大长度。

当客户端启动连接时,需要回调才能为客户端套接字设置句柄并使用关联该句柄uv_accept。在这种情况下,我们也对从此流中读取感兴趣。

tcp-echo-server / main.c-接受客户端

void on_new_connection(uv_stream_t *server, int status) {if (status < 0) {fprintf(stderr, "New connection error %s\n", uv_strerror(status));// error!return;}uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));uv_tcp_init(loop, client);if (uv_accept(server, (uv_stream_t*) client) == 0) {uv_read_start((uv_stream_t*) client, alloc_buffer, echo_read);}else {uv_close((uv_handle_t*) client, NULL);}
}

其余功能集与流示例非常相似,可以在代码中找到。只要记得在uv_close不需要套接字时调用即可。uv_listen如果您对接受连接不感兴趣,甚至可以在回调中完成此操作。

客户

在服务器上绑定/监听/接受的地方,在客户端只需调用即可uv_tcp_connect。所使用的uv_connect_cb样式回调相同。尝试:uv_listenuv_tcp_connect

uv_tcp_t* socket = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, socket);uv_connect_t* connect = (uv_connect_t*)malloc(sizeof(uv_connect_t));struct sockaddr_in dest;
uv_ip4_addr("127.0.0.1", 80, &dest);uv_tcp_connect(connect, socket, (const struct sockaddr*)&dest, on_connect);

on_connect建立连接后将在哪里调用。回调接收该uv_connect_t结构,该结构具有.handle 指向套接字的成员。

UDP

该用户数据报协议提供无连接的,不可靠的网络通信。因此,libuv不提供流。相反,libuv通过uv_udp_t句柄(用于接收)和 uv_udp_send_t请求(用于发送)以及相关功能提供非阻塞UDP支持。也就是说,用于读取/写入的实际API与正常的流读取非常相似。为了了解如何使用UDP,该示例显示了从DHCP服务器获取IP地址的第一阶段-DHCP Discover。

注意:您将必须以root用户身份运行udp-dhcp,因为它使用低于1024的知名端口号。

udp-dhcp / main.c-设置和发送UDP数据包

uv_loop_t *loop;
uv_udp_t send_socket;
uv_udp_t recv_socket;int main() {loop = uv_default_loop();uv_udp_init(loop, &recv_socket);struct sockaddr_in recv_addr;uv_ip4_addr("0.0.0.0", 68, &recv_addr);uv_udp_bind(&recv_socket, (const struct sockaddr *)&recv_addr, UV_UDP_REUSEADDR);uv_udp_recv_start(&recv_socket, alloc_buffer, on_read);uv_udp_init(loop, &send_socket);struct sockaddr_in broadcast_addr;uv_ip4_addr("0.0.0.0", 0, &broadcast_addr);uv_udp_bind(&send_socket, (const struct sockaddr *)&broadcast_addr, 0);uv_udp_set_broadcast(&send_socket, 1);uv_udp_send_t send_req;uv_buf_t discover_msg = make_discover_msg();struct sockaddr_in send_addr;uv_ip4_addr("255.255.255.255", 67, &send_addr);uv_udp_send(&send_req, &send_socket, &discover_msg, 1, (const struct sockaddr *)&send_addr, on_send);return uv_run(loop, UV_RUN_DEFAULT);
}

注意:IP地址0.0.0.0用于绑定到所有接口。IP地址255.255.255.255是一个广播地址,这意味着数据包将被发送到子网上的所有接口。port0表示操作系统随机分配一个端口。

首先,我们将接收套接字设置为绑定在端口68(DHCP客户端)上的所有接口上,并开始对其进行读取。这将从任何答复的DHCP服务器读回响应。我们使用UV_UDP_REUSEADDR标志与在此计算机上同一端口上运行的任何其他系统DHCP客户端配合使用。然后,我们设置一个类似的发送套接字,并用于在端口67(DHCP服务器)上uv_udp_send发送广播消息

这是必要的设置广播标志,否则你将得到一个 EACCES错误[1] 。发送的确切信息与本书无关,如果您有兴趣,可以研究代码。与往常一样,如果发生错误,则读和写回调将收到状态代码<0。

由于UDP套接字未连接到特定对等方,因此读取回调将收到有关数据包发送方的额外参数。

nread如果没有更多数据可读取,则可能为零。如果addr为NULL,则表示没有要读取的内容(回调不应该做任何事情);如果不为NULL,则表示在处从主机接收到空数据报 addr。该flags参数可能是UV_UDP_PARTIAL分配器提供的缓冲区是否不足以容纳数据。在这种情况下,操作系统将丢弃不适合的数据(适合您的是UDP!)。

udp-dhcp / main.c-读取数据包

void on_read(uv_udp_t *req, ssize_t nread, const uv_buf_t *buf, const struct sockaddr *addr, unsigned flags) {if (nread < 0) {fprintf(stderr, "Read error %s\n", uv_err_name(nread));uv_close((uv_handle_t*) req, NULL);free(buf->base);return;}char sender[17] = { 0 };uv_ip4_name((const struct sockaddr_in*) addr, sender, 16);fprintf(stderr, "Recv from %s\n", sender);// ... DHCP specific codeunsigned int *as_integer = (unsigned int*)buf->base;unsigned int ipbin = ntohl(as_integer[4]);unsigned char ip[4] = {0};int i;for (i = 0; i < 4; i++)ip[i] = (ipbin >> i*8) & 0xff;fprintf(stderr, "Offered IP %d.%d.%d.%d\n", ip[3], ip[2], ip[1], ip[0]);free(buf->base);uv_udp_recv_stop(req);
}

UDP选项

时间到现场

可以使用更改套接字上发送的数据包的TTL uv_udp_set_ttl

仅IPv6堆栈

IPv6套接字可用于IPv4和IPv6通信。如果只想将套接字限制为IPv6,则将UV_UDP_IPV6ONLY标志传递给 uv_udp_bind [2]。

组播¶

套接字可以使用以下方法(取消)订阅多播组:

UV_EXTERN int uv_udp_set_membership(uv_udp_t* handle,const char* multicast_addr,const char* interface_addr,uv_membership membership);

这里membershipUV_JOIN_GROUPUV_LEAVE_GROUP

在本指南中很好地解释了多播的概念。

多播数据包的本地环回默认启用[3],用于 uv_udp_set_multicast_loop将其关闭。

组播数据包的生存时间可以使用进行更改 uv_udp_set_multicast_ttl

查询

libuv提供异步DNS解析。为此,它提供了自己的 getaddrinfo替代品[4]。在回调中,您可以对检索到的地址执行常规的套接字操作。让我们连接到Freenode来查看DNS解析的示例。

dns / main.c

int main() {loop = uv_default_loop();struct addrinfo hints;hints.ai_family = PF_INET;hints.ai_socktype = SOCK_STREAM;hints.ai_protocol = IPPROTO_TCP;hints.ai_flags = 0;uv_getaddrinfo_t resolver;fprintf(stderr, "irc.freenode.net is... ");int r = uv_getaddrinfo(loop, &resolver, on_resolved, "irc.freenode.net", "6667", &hints);if (r) {fprintf(stderr, "getaddrinfo call error %s\n", uv_err_name(r));return 1;}return uv_run(loop, UV_RUN_DEFAULT);
}

如果uv_getaddrinfo返回非零值,则说明设置出现问题,并且根本不会调用您的回调。uv_getaddrinfo返回后可以立即释放所有参数。该主机名,servname和提示 结构都记录在了的getaddrinfo手册页。NULL在这种情况下,回调函数可以同步运行。

在解析器回调中,您可以从的链接列表中选择任何IP 。这也说明。有必要在回调中进行调用。struct addrinfo(s)uv_tcp_connectuv_freeaddrinfo

dns / main.c

void on_resolved(uv_getaddrinfo_t *resolver, int status, struct addrinfo *res) {if (status < 0) {fprintf(stderr, "getaddrinfo callback error %s\n", uv_err_name(status));return;}char addr[17] = {'\0'};uv_ip4_name((struct sockaddr_in*) res->ai_addr, addr, 16);fprintf(stderr, "%s\n", addr);uv_connect_t *connect_req = (uv_connect_t*) malloc(sizeof(uv_connect_t));uv_tcp_t *socket = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));uv_tcp_init(loop, socket);uv_tcp_connect(connect_req, socket, (const struct sockaddr*) res->ai_addr, on_connect);uv_freeaddrinfo(res);
}

libuv还提供了反向uv_getnameinfo。

网络接口

可以使用libuv通过libuv获得有关系统网络接口的信息uv_interface_addresses。这个简单的程序仅打印出所有界面详细信息,因此您可以了解可用的字段。这有助于服务启动时绑定到IP地址。

interfaces / main.c

#include <stdio.h>
#include <uv.h>int main() {char buf[512];uv_interface_address_t *info;int count, i;uv_interface_addresses(&info, &count);i = count;printf("Number of interfaces: %d\n", count);while (i--) {uv_interface_address_t interface = info[i];printf("Name: %s\n", interface.name);printf("Internal? %s\n", interface.is_internal ? "Yes" : "No");if (interface.address.address4.sin_family == AF_INET) {uv_ip4_name(&interface.address.address4, buf, sizeof(buf));printf("IPv4 address: %s\n", buf);}else if (interface.address.address4.sin_family == AF_INET6) {uv_ip6_name(&interface.address.address6, buf, sizeof(buf));printf("IPv6 address: %s\n", buf);}printf("\n");}uv_free_interface_addresses(info, count);return 0;
}

is_internal对于环回接口,则为true。请注意,如果物理接口具有多个IPv4 / IPv6地址,则名称将被多次报告,每个地址仅被报告一次。

[1]http://beej.us/guide/bgnet/output/html/multipage/advanced.html#broadcast

[2]在Windows上仅在Windows Vista及更高版本上受支持。

[3]http://www.tldp.org/HOWTO/Multicast-HOWTO-6.html#ss6.1

[4]libuvgetaddrinfo在libuv线程池中使用系统。libuv v0.8.0和更早版本还包含c-ares作为替代,但是在v0.9.0中已将其删除。

线程

https://nikhilm.github.io/uvbook/threads.html


等一下?我们为什么要加入线程?没有事件循环应该的方式网络编程的规模?好吧 线程仍然是处理器执行任务的媒介。因此,即使您可能需要遍历各种同步原语,线程有时还是很有用的。

内部使用线程来伪造所有系统调用的异步特性。libuv还使用线程允许应用程序异步执行实际上正在阻塞的任务,方法是生成线程并在完成时收集结果。

今天,有两个主要的线程库:Windows线程实现和POSIX的pthreads。libuv的线程API与pthreads API类似,并且通常具有相似的语义。

libuv线程功能的一个显着方面是它是libuv中的一个独立部分。尽管其他功能密切依赖于事件循环和回调原理,但线程是完全不可知的,它们根据需要进行阻塞,直接通过返回值发出错误信号,并且如第一个示例所示, 甚至不需要运行事件循环。

libuv的线程API也非常有限,因为线程的语义和语法在所有平台上都不同,具有不同的完整性级别。

本章进行以下假设:只有一个事件循环在一个线程(主线程)中运行。没有其他线程与事件循环进行交互(使用除外uv_async_send)。

核心线程操作

这里没有太多,您只需使用启动一个线程,uv_thread_create()然后等待使用关闭它uv_thread_join()

创建/main.c

int main() {int tracklen = 10;uv_thread_t hare_id;uv_thread_t tortoise_id;uv_thread_create(&hare_id, hare, &tracklen);uv_thread_create(&tortoise_id, tortoise, &tracklen);uv_thread_join(&hare_id);uv_thread_join(&tortoise_id);return 0;
}

TIPS:uv_thread_tpthread_t在Unix上只是一个别名,但这是一个实现细节,请避免依赖它始终为真。

第二个参数是将用作线程入口点的函数,最后一个参数是可用于将自定义参数传递给线程的参数。该函数现在将在单独的线程中运行,该线程由操作系统抢先调度:void *hare

创建/main.c

void hare(void *arg) {int tracklen = *((int *) arg);while (tracklen) {tracklen--;sleep(1);fprintf(stderr, "Hare ran another step\n");}fprintf(stderr, "Hare done running!\n");
}

与之不同的是pthread_join(),它不允许目标线程使用第二个参数将值传递回调用线程uv_thread_join()。要发送值,请使用线程间通信。

同步基元

本节特意是斯巴达式的。本书不是关于线程的,因此我只在这里列出libuv API中的任何惊喜。对于其余的内容,您可以查看pthreads手册页。

互斥体

互斥锁函数直接映射到pthread等效项。

libuv互斥函数

UV_EXTERN int uv_mutex_init(uv_mutex_t* handle);
UV_EXTERN void uv_mutex_destroy(uv_mutex_t* handle);
UV_EXTERN void uv_mutex_lock(uv_mutex_t* handle);
UV_EXTERN int uv_mutex_trylock(uv_mutex_t* handle);
UV_EXTERN void uv_mutex_unlock(uv_mutex_t* handle);

uv_mutex_init()uv_mutex_trylock()功能将成功返回0,否则错误代码。

如果libuv已编制与调试启用uv_mutex_destroy(), uv_mutex_lock()uv_mutex_unlock()abort()在错误。uv_mutex_trylock()如果错误不是 EAGAIN或,则类似地将中止EBUSY

某些平台支持递归互斥,但是您不应该依赖它们。如果锁定互斥锁的线程尝试再次锁定它,则BSD互斥锁实现将引发错误。例如,类似的构造:

uv_mutex_lock(a_mutex);
uv_thread_create(thread_id, entry, (void *)a_mutex);
uv_mutex_lock(a_mutex);
// more things here

可用于等待另一个线程初始化一些东西然后解锁,a_mutex但是如果处于调试模式,则将导致程序崩溃,或者在的第二次调用中返回错误uv_mutex_lock()

注意:linux上的互斥体支持递归互斥体的属性,但是该API不会通过libuv公开。

读写锁是一种更精细的访问机制。两个读取器可以同时访问共享内存。当读者持有该锁时,作家可能不会获得该锁。持有者持有读者时,读者或作家可能不会获得该锁。数据库中经常使用读写锁。这是一个玩具示例。

锁/ main.c-简单的rwlocks

#include <stdio.h>
#include <uv.h>uv_barrier_t blocker;
uv_rwlock_t numlock;
int shared_num;void reader(void *n)
{int num = *(int *)n;int i;for (i = 0; i < 20; i++) {uv_rwlock_rdlock(&numlock);printf("Reader %d: acquired lock\n", num);printf("Reader %d: shared num = %d\n", num, shared_num);uv_rwlock_rdunlock(&numlock);printf("Reader %d: released lock\n", num);}uv_barrier_wait(&blocker);
}void writer(void *n)
{int num = *(int *)n;int i;for (i = 0; i < 20; i++) {uv_rwlock_wrlock(&numlock);printf("Writer %d: acquired lock\n", num);shared_num++;printf("Writer %d: incremented shared num = %d\n", num, shared_num);uv_rwlock_wrunlock(&numlock);printf("Writer %d: released lock\n", num);}uv_barrier_wait(&blocker);
}int main()
{uv_barrier_init(&blocker, 4);shared_num = 0;uv_rwlock_init(&numlock);uv_thread_t threads[3];int thread_nums[] = {1, 2, 1};uv_thread_create(&threads[0], reader, &thread_nums[0]);uv_thread_create(&threads[1], reader, &thread_nums[1]);uv_thread_create(&threads[2], writer, &thread_nums[2]);uv_barrier_wait(&blocker);uv_barrier_destroy(&blocker);uv_rwlock_destroy(&numlock);return 0;
}

运行此程序,观察读者有时会如何重叠。在有多个作者的情况下,调度程序通常会给他们更高的优先级,因此,如果您添加两个作者,您会发现两个作者倾向于先完成,然后再让读者有机会。

在上面的示例中,我们还使用了屏障,以便主线程可以等待所有读者和作家表明他们已经结束。

其他

libuv还通过与pthread类似的API支持信号量,条件变量和屏障。

另外,libuv提供了便利功能uv_once()。多个线程可以尝试uv_once()使用给定的守卫和函数指针进行调用,只有第一个将获胜,该函数将被调用一次,并且只能被调用一次

/* Initialize guard */
static uv_once_t once_only = UV_ONCE_INIT;int i = 0;void increment() {i++;
}void thread1() {/* ... work */uv_once(once_only, increment);
}void thread2() {/* ... work */uv_once(once_only, increment);
}int main() {/* ... spawn threads */
}

完成所有线程后,。i == 1

libuv v0.11.11及更高版本还添加了用于线程本地存储的uv_key_t结构和api。

libuv工作队列

uv_queue_work()是一项便利功能,它允许应用程序在单独的线程中运行任务,并具有在任务完成时触发的回调。一个看似简单的功能,uv_queue_work() 吸引人的是,它允许潜在地将任何第三方库与事件循环范例一起使用。使用事件循环时,必须确保在执行I / O时不会在循环线程中定期运行的函数阻塞或严重占用CPU的时间,因为这意味着循环速度变慢并且不会在以下位置处理事件名额已满。

但是,如果需要响应性(经典的“每个客户端一个线程”服务器模型),则许多现有代码都具有阻塞功能(例如,在后台执行I / O的例程)与线程一起使用,并且它们与事件循环库一起使用通常涉及在单独的线程中滚动运行任务的自己的系统。libuv为此提供了一个方便的抽象。

这是一个受node.js启发的简单示例,即癌症。我们将计算斐波那契数,在此过程中要稍作睡眠,但要在单独的线程中运行它,以便阻塞和CPU绑定任务不会阻止事件循环执行其他活动。

队列工作/main.c-懒惰的斐波那契

void fib(uv_work_t *req) {int n = *(int *) req->data;if (random() % 2)sleep(1);elsesleep(3);long fib = fib_(n);fprintf(stderr, "%dth fibonacci is %lu\n", n, fib);
}void after_fib(uv_work_t *req, int status) {fprintf(stderr, "Done calculating %dth fibonacci\n", *(int *) req->data);
}

实际的任务功能很简单,没有任何迹象表明它将在单独的线程中运行。该uv_work_t结构的线索。您可以使用字段通过它传递任意数据,并使用它与线程进行通信。但是,如果在两个线程都在运行时进行更改,请确保使用正确的锁。void* data

触发是uv_queue_work

队列工作/main.c

int main() {loop = uv_default_loop();int data[FIB_UNTIL];uv_work_t req[FIB_UNTIL];int i;for (i = 0; i < FIB_UNTIL; i++) {data[i] = i;req[i].data = (void *) &data[i];uv_queue_work(loop, &req[i], fib, after_fib);}return uv_run(loop, UV_RUN_DEFAULT);
}

线程函数将在单独的线程中启动,传递 uv_work_t结构,一旦函数返回,将运行事件循环的线程上调用after函数。它将传递相同的结构。

为了将包装器写入阻塞库,一种常见的模式 是使用警棍交换数据。

从libuv 0.9.4版本开始uv_cancel(),可以使用附加功能。这使您可以取消libuv工作队列上的任务。仅可以取消尚未开始的任务。如果任务已经开始执行或已经完成执行uv_cancel() 则将失败

uv_cancel()如果用户请求终止,则对清理挂起的任务很有用。例如,音乐播放器可以将多个目录排队,以扫描音频文件。如果用户终止程序,则应迅速退出程序,而不要等到所有未决请求都运行后再退出。

让我们修改fibonacci示例以进行演示uv_cancel()。我们首先设置一个信号处理程序以终止。

取消队列/main.c

int main() {loop = uv_default_loop();int data[FIB_UNTIL];int i;for (i = 0; i < FIB_UNTIL; i++) {data[i] = i;fib_reqs[i].data = (void *) &data[i];uv_queue_work(loop, &fib_reqs[i], fib, after_fib);}uv_signal_t sig;uv_signal_init(loop, &sig);uv_signal_start(&sig, signal_handler, SIGINT);return uv_run(loop, UV_RUN_DEFAULT);
}

当用户通过按下触发信号时,Ctrl+C我们将发送 uv_cancel()给所有工作人员。uv_cancel()将为0已经执行或完成的返回。

取消队列/main.c

void signal_handler(uv_signal_t *req, int signum)
{printf("Signal received!\n");int i;for (i = 0; i < FIB_UNTIL; i++) {uv_cancel((uv_req_t*) &fib_reqs[i]);}uv_signal_stop(req);
}

对于确实成功取消的任务,将调用after函数并将其status设置为UV_ECANCELED

取消队列/main.c

void after_fib(uv_work_t *req, int status) {if (status == UV_ECANCELED)fprintf(stderr, "Calculation of %d cancelled.\n", *(int *) req->data);
}

uv_cancel()也可以与uv_fs_tuv_getaddrinfo_t 请求一起使用。对于文件系统功能系列,uv_fs_t.errorno将设置为UV_ECANCELED

TIPS:一个设计良好的程序将可以终止已经开始执行的长期运行的工作程序。这样的工作人员可以定期检查仅主要过程设置为终止信号的变量。

线程间通信

有时你想不同的线程实际上相互发送消息,而 他们正在运行。例如,您可能在一个单独的线程中运行了一些长时间任务(也许使用uv_queue_work),但想通知主线程的进度。这是让下载管理器通知用户正在运行的下载状态的简单示例。

进度/main.c

uv_loop_t *loop;
uv_async_t async;
}int main() {loop = uv_default_loop();uv_work_t req;int size = 10240;req.data = (void*) &size;uv_async_init(loop, &async, print_progress);uv_queue_work(loop, &req, fake_download, after);return uv_run(loop, UV_RUN_DEFAULT);
}

异步线程通信在循环上起作用因此尽管任何线程都可以成为消息发送者,但是只有具有libuv循环的线程才可以成为接收者(或者说循环就是接收者)。每当libuvprint_progress收到消息时,它将使用异步观察程序调用回调()。

警告:重要的是要意识到,由于消息发送是异步的,因此回调可以uv_async_send在另一个线程中调用之后立即调用,也可以在一段时间后调用。libuv可能还会合并多个调用,uv_async_send并且仅调用一次回调。该libuv做的唯一保证是-回调函数被调用至少一次通话后uv_async_send。如果您没有对的待处理调用uv_async_send,则不会调用该回调。如果您进行了两个或多个调用,而libuv还没有运行回调的机会,则对于的多次调用 ,它 可能调用一次回调uv_async_send。对于一个事件,您的回调将永远不会被调用两次。

进度/main.c

double percentage;void fake_download(uv_work_t *req) {int size = *((int*) req->data);int downloaded = 0;while (downloaded < size) {percentage = downloaded*100.0/size;async.data = (void*) &percentage;uv_async_send(&async);sleep(1);downloaded += (200+random())%1000; // can only download max 1000bytes/sec,// but at least a 200;}

在下载功能中,我们修改进度指示器,并使用来排队传递消息uv_async_send。请记住:uv_async_send也是非阻塞的,将立即返回。

进度/main.c

void print_progress(uv_async_t *handle) {double percentage = *((double*) handle->data);fprintf(stderr, "Downloaded %.2f%%\n", percentage);

回调是一种标准的libuv模式,它从观察程序中提取数据。

最后,重要的是要记住清理观察者。

进度/main.c

void after(uv_work_t *req, int status) {fprintf(stderr, "Download complete\n");uv_close((uv_handle_t*) &async, NULL);

在此示例显示了data字段的滥用之后,bnoordhuis 指出使用该data字段不是线程安全的, uv_async_send()实际上仅是为了唤醒事件循环。使用互斥或​​rwlock可以确保以正确的顺序执行访问。

注意:互斥体和rwlocks不要一个信号处理器内工作,而 uv_async_send做。

uv_async_send所需的一种用例是与需要线程亲和性的库进行互操作时。例如,在node.js中,一个v8引擎实例,上下文及其对象绑定到该v8实例启动所在的线程。与来自另一个线程的v8数据结构进行交互可能导致不确定的结果。现在考虑绑定第三方库的一些node.js模块。它可能是这样的:

  1. 在node中,第三方库设置为带有JavaScript回调,以调用以获取更多信息:

var lib = require('lib');
lib.on_progress(function() {console.log("Progress");
});lib.do();// do other stuff
  1. lib.do应该是非阻塞的,但是第三方lib正在阻塞,因此绑定使用uv_queue_work

  2. 在单独的线程中完成的实际工作想调用进度回调,但是不能直接调用v8与JavaScript进行交互。因此它使用uv_async_send

  3. 在主循环线程(即v8线程)中调用异步回调,然后与v8交互以调用JavaScript回调。

进程

https://nikhilm.github.io/uvbook/processes.html


libuv提供了可观的子进程管理,可以抽象化平台差异,并允许使用流或命名管道与子进程进行通信。

在Unix中,一个常见的习惯用法是让每个进程都做一件事情并做好。在这种情况下,一个进程通常使用多个子进程来完成任务(类似于在shell中使用管道)。与带有线程和共享内存的模型相比,带有消息的多进程模型也可能更易于推理。

对基于事件的程序的普遍克制是,它们无法利用现代计算机中的多个内核。在多线程程序中,内核可以执行调度并将不同的线程分配给不同的内核,从而提高性能。但是一个事件循环只有一个线程。解决方法可以是启动多个进程,每个进程运行一个事件循环,并将每个进程分配给一个单独的CPU内核。

产卵子进程

最简单的情况是,您只想启动一个进程并知道它何时退出。这是通过使用实现的uv_spawn

产生/ main.c

uv_process_t child_req;
uv_process_options_t options;
int main() {loop = uv_default_loop();char* args[3];args[0] = "mkdir";args[1] = "test-dir";args[2] = NULL;options.exit_cb = on_exit;options.file = "mkdir";options.args = args;int r;if ((r = uv_spawn(loop, &child_req, &options))) {fprintf(stderr, "%s\n", uv_strerror(r));return 1;} else {fprintf(stderr, "Launched process with ID %d\n", child_req.pid);}return uv_run(loop, UV_RUN_DEFAULT);
}

注意:options因为它是全局变量,所以用零隐式初始化。如果更改options为局部变量,请记住对其进行初始化以使所有未使用的字段无效:uv_process_options_t options = {0};

uv_process_t结构仅充当句柄,所有选项均通过设置 uv_process_options_t。要简单地启动一个进程,您只需要设置 fileandargs字段。file是要执行的程序。由于 内部uv_spawn使用execvp,因此无需提供完整路径。最后,根据基本约定,arguments数组必须比参数的数量大一个,最后一个元素为NULL

调用之后uv_spawnuv_process_t.pid将包含子进程的进程ID。

将使用退出状态和 导致退出的信号类型来调用退出回调。

产生/ main.c

void on_exit(uv_process_t *req, int64_t exit_status, int term_signal) {fprintf(stderr, "Process exited with status %" PRId64 ", signal %d\n", exit_status, term_signal);uv_close((uv_handle_t*) req, NULL);

这是必需的进程退出后关闭过程守望。

改变工艺参数

在启动子进程之前,您可以使用中的字段控制执行环境uv_process_options_t

变更执行目录

设置uv_process_options_t.cwd到相应的目录。

设置环境变量

uv_process_options_t.env是一个以空值结尾的字符串数组,每种形式都VAR=VALUE用于为进程设置环境变量。设置为NULL从父(this)进程继承环境。

选项标志

设置uv_process_options_t.flags以下标志的按位或,将修改子进程的行为:

  • UV_PROCESS_SETUID-将孩子的执行用户ID设置为uv_process_options_t.uid
  • UV_PROCESS_SETGID-将孩子的执行组ID设置为uv_process_options_t.gid

仅在Unixuv_spawn上支持更改UID / GID,在Windows上使用会失败UV_ENOTSUP

  • UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS-uv_process_options_t.args在Windows上不执行引号或转义 。在Unix上被忽略。
  • UV_PROCESS_DETACHED-在新会话中启动子进程,该进程将在父进程退出后继续运行。请参见下面的示例。

拆卸过程

传递标志UV_PROCESS_DETACHED可用于启动守护程序或独立于父进程的子进程,以便父进程退出不会对其产生影响。

分离/ main.c

int main() {loop = uv_default_loop();char* args[3];args[0] = "sleep";args[1] = "100";args[2] = NULL;options.exit_cb = NULL;options.file = "sleep";options.args = args;options.flags = UV_PROCESS_DETACHED;int r;if ((r = uv_spawn(loop, &child_req, &options))) {fprintf(stderr, "%s\n", uv_strerror(r));return 1;}fprintf(stderr, "Launched sleep with PID %d\n", child_req.pid);uv_unref((uv_handle_t*) &child_req);return uv_run(loop, UV_RUN_DEFAULT);

只需记住,该句柄仍在监视孩子,因此您的程序将不会退出。uv_unref()如果您想更加一劳永逸,请使用。

将信号发送到处理

libuv包装标准kill(2)的Unix和实现一个与Windows类似语义的系统调用,有一点需要注意:所有的SIGTERM, SIGINTSIGKILL,导致过程的终止。的签名uv_kill是:

uv_err_t uv_kill(int pid, int signum);

对于使用libuv开始的进程,您可以使用uv_process_kill代替,它接受uv_process_t观察者作为第一个参数,而不是pid。在这种情况下,请记住呼叫 uv_close观察者。

信号

libuv还提供了一些Windows支持的Unix信号包装器。

使用uv_signal_init()初始化处理,它与一个循环关联。要在该处理程序上侦听特定信号,请uv_signal_start()与处理程序功能一起使用。每个处理程序只能与一个信号编号关联,随后的调用将 uv_signal_start()覆盖较早的关联。使用uv_signal_stop()驻足观看。这是一个演示各种可能性的小示例:

信号/main.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <uv.h>uv_loop_t* create_loop()
{uv_loop_t *loop = malloc(sizeof(uv_loop_t));if (loop) {uv_loop_init(loop);}return loop;
}void signal_handler(uv_signal_t *handle, int signum)
{printf("Signal received: %d\n", signum);uv_signal_stop(handle);
}// two signal handlers in one loop
void thread1_worker(void *userp)
{uv_loop_t *loop1 = create_loop();uv_signal_t sig1a, sig1b;uv_signal_init(loop1, &sig1a);uv_signal_start(&sig1a, signal_handler, SIGUSR1);uv_signal_init(loop1, &sig1b);uv_signal_start(&sig1b, signal_handler, SIGUSR1);uv_run(loop1, UV_RUN_DEFAULT);
}// two signal handlers, each in its own loop
void thread2_worker(void *userp)
{uv_loop_t *loop2 = create_loop();uv_loop_t *loop3 = create_loop();uv_signal_t sig2;uv_signal_init(loop2, &sig2);uv_signal_start(&sig2, signal_handler, SIGUSR1);uv_signal_t sig3;uv_signal_init(loop3, &sig3);uv_signal_start(&sig3, signal_handler, SIGUSR1);while (uv_run(loop2, UV_RUN_NOWAIT) || uv_run(loop3, UV_RUN_NOWAIT)) {}
}int main()
{printf("PID %d\n", getpid());uv_thread_t thread1, thread2;uv_thread_create(&thread1, thread1_worker, 0);uv_thread_create(&thread2, thread2_worker, 0);uv_thread_join(&thread1);uv_thread_join(&thread2);return 0;
}

注意:uv_run(loop, UV_RUN_NOWAIT)类似于 只处理一个事件。如果没有任何未决事件,UV_RUN_ONCE将阻止,而UV_RUN_NOWAIT将立即返回。我们使用NOWAIT,这样一个循环就不会饿死了,因为另一个循环没有待处理的活动。uv_run(loop, UV_RUN_ONCE)

发送SIGUSR1到流程,您将发现处理程序被调用4次,每次调用一次uv_signal_t。处理程序仅停止每个句柄,以便程序退出。对所有处理程序的这种分派非常有用。使用多个事件循环的服务器可以确保在终止之前安全地保存所有数据,只需在每个循环中添加的观察者即可 SIGINT

子进程I /

一个正常的,新生成的过程有其自身的一组文件描述符,用0,1和2的存在stdinstdoutstderr分别。有时您可能想与孩子共享文件描述符。例如,也许您的应用程序启动了一个子命令,并且您希望将任何错误记录在日志文件中,但请忽略stdout。为此,您希望stderr孩子与父母的stderr相同。在这种情况下,libuv支持 继承文件描述符。在此示例中,我们调用测试程序,该程序是:

proc-streams / test.c

#include <stdio.h>int main()
{fprintf(stderr, "This is stderr\n");printf("This is stdout\n");return 0;
}

实际程序proc-streams仅在共享时运行此程序stderr。使用中的stdio字段 设置子进程的文件描述符uv_process_options_t。首先将stdio_count字段设置为要设置的文件描述符的数量。uv_process_options_t.stdio是的数组 uv_stdio_container_t,它是:

typedef struct uv_stdio_container_s {uv_stdio_flags flags;union {uv_stream_t* stream;int fd;} data;
} uv_stdio_container_t;

标志可以具有多个值。使用UV_IGNORE如果不打算使用。如果将前三个stdio字段标记为,UV_IGNORE它们将重定向到/dev/null

由于我们要传递现有的描述符,因此我们将使用UV_INHERIT_FD。然后将设置fdstderr

proc-streams / main.c

int main() {loop = uv_default_loop();/* ... */options.stdio_count = 3;uv_stdio_container_t child_stdio[3];child_stdio[0].flags = UV_IGNORE;child_stdio[1].flags = UV_IGNORE;child_stdio[2].flags = UV_INHERIT_FD;child_stdio[2].data.fd = 2;options.stdio = child_stdio;options.exit_cb = on_exit;options.file = args[0];options.args = args;int r;if ((r = uv_spawn(loop, &child_req, &options))) {fprintf(stderr, "%s\n", uv_strerror(r));return 1;}return uv_run(loop, UV_RUN_DEFAULT);
}

如果您运行proc-stream,将看到仅显示“ This is stderr”行。尝试将其标记stdout为继承并查看输出。

将此重定向应用于流非常简单。通过设置flags 于UV_INHERIT_STREAM和设置data.stream在父进程流,子进程可以把这个流作为标准I / O。这可以用来实现CGI之类的东西。

一个示例CGI脚本/可执行文件是:

cgi / tick.c

#include <stdio.h>
#include <unistd.h>int main() {int i;for (i = 0; i < 10; i++) {printf("tick\n");fflush(stdout);sleep(1);}printf("BOOM!\n");return 0;
}

CGI服务器将本章和“网络”中的概念结合在一起,以便向每个客户端发送十次滴答声,然后关闭该连接。

cgi / main.c

void on_new_connection(uv_stream_t *server, int status) {if (status == -1) {// error!return;}uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));uv_tcp_init(loop, client);if (uv_accept(server, (uv_stream_t*) client) == 0) {invoke_cgi_script(client);}else {uv_close((uv_handle_t*) client, NULL);}

在这里,我们只接受TCP连接并将套接字()传递给 invoke_cgi_script

cgi / main.c

    args[1] = NULL;/* ... finding the executable path and setting up arguments ... */options.stdio_count = 3;uv_stdio_container_t child_stdio[3];child_stdio[0].flags = UV_IGNORE;child_stdio[1].flags = UV_INHERIT_STREAM;child_stdio[1].data.stream = (uv_stream_t*) client;child_stdio[2].flags = UV_IGNORE;options.stdio = child_stdio;options.exit_cb = cleanup_handles;options.file = args[0];options.args = args;// Set this so we can close the socket after the child process exits.child_req.data = (void*) client;int r;if ((r = uv_spawn(loop, &child_req, &options))) {fprintf(stderr, "%s\n", uv_strerror(r));

stdoutCGI脚本的设置为套接字,以便将壁虱脚本打印的内容发送到客户端。通过使用进程,我们可以将读/写缓冲卸载到操作系统上,因此在便利性方面,这很棒。请注意,创建流程是一项昂贵的任务。

管道

libuv的uv_pipe_t结构对Unix程序员有些混乱,因为它会立即变幻|并使用pipe(7)。但是uv_pipe_t与匿名管道无关,而是IPC机制。uv_pipe_t 可以由Unix域套接字或Windows命名管道支持,以允许多个进程进行通信。这将在下面讨论。

父子

父母和孩子可以通过设置uv_stdio_container_t.flagsUV_CREATE_PIPEUV_READABLE_PIPE或按位组合 创建的管道进行单向或双向通信UV_WRITABLE_PIPE。读/写标志是从子进程的角度来看的。

任意过程

由于域套接字[1]在文件系统中可以具有众所周知的名称和位置,因此它们可以用于无关进程之间的IPC。 开源桌面环境使用的D-BUS系统使用域套接字进行事件通知。然后,当联系人联机或检测到新硬件时,各种应用程序会做出反应。MySQL服务器还运行一个域套接字,客户端可以在该套接字上进行交互。

使用域套接字时,通常遵循客户机/服务器模式,套接字的创建者/所有者充当服务器。初始设置后,消息传递与TCP并无不同,因此我们将重用echo服务器示例。

管道回显服务器/ main.c

int main() {loop = uv_default_loop();uv_pipe_t server;uv_pipe_init(loop, &server, 0);signal(SIGINT, remove_sock);int r;if ((r = uv_pipe_bind(&server, "echo.sock"))) {fprintf(stderr, "Bind error %s\n", uv_err_name(r));return 1;}if ((r = uv_listen((uv_stream_t*) &server, 128, on_new_connection))) {fprintf(stderr, "Listen error %s\n", uv_err_name(r));return 2;}return uv_run(loop, UV_RUN_DEFAULT);
}

我们给套接字命名,echo.sock这意味着它将在本地目录中创建。就流API而言,此套接字现在的行为与TCP套接字没有什么不同。您可以使用socat测试此服务器:

$ socat - /path/to/socket

想要连接到域套接字的客户端将使用:

void uv_pipe_connect(uv_connect_t *req, uv_pipe_t *handle, const char *name, uv_connect_cb cb);

这里name将是echo.sock或类似。

通过管道发送文件描述符

关于域套接字的很酷的事情是,可以通过在域套接字上发送文件描述符来在进程之间交换文件描述符。这允许进程将其I / O移交给其他进程。应用程序包括负载平衡服务器,工作进程和其他方法,以最佳利用CPU。libuv目前仅支持通过管道发送TCP套接字或其他管道

为了演示,我们将研究回显服务器的实现,该实现以循环的方式将客户端传递给工作进程。该程序有点涉及,虽然本书仅包含摘要,但建议阅读完整的代码以真正理解它。

工作进程非常简单,因为文件描述符由主服务器移交给它。

多回显服务器/worker.c

uv_loop_t *loop;
uv_pipe_t queue;
int main() {loop = uv_default_loop();uv_pipe_init(loop, &queue, 1 /* ipc */);uv_pipe_open(&queue, 0);uv_read_start((uv_stream_t*)&queue, alloc_buffer, on_new_connection);return uv_run(loop, UV_RUN_DEFAULT);
}

queue是在另一端连接到主进程的管道,沿着该管道发送新的文件描述符。重要的是将ipc 参数设置uv_pipe_init为1,以指示此管道将用于进程间通信!由于主服务器会将文件句柄写入工作者的标准输入,因此我们将管道连接到stdinusing uv_pipe_open

多回显服务器/worker.c

void on_new_connection(uv_stream_t *q, ssize_t nread, const uv_buf_t *buf) {if (nread < 0) {if (nread != UV_EOF)fprintf(stderr, "Read error %s\n", uv_err_name(nread));uv_close((uv_handle_t*) q, NULL);return;}uv_pipe_t *pipe = (uv_pipe_t*) q;if (!uv_pipe_pending_count(pipe)) {fprintf(stderr, "No pending count\n");return;}uv_handle_type pending = uv_pipe_pending_type(pipe);assert(pending == UV_TCP);uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));uv_tcp_init(loop, client);if (uv_accept(q, (uv_stream_t*) client) == 0) {uv_os_fd_t fd;uv_fileno((const uv_handle_t*) client, &fd);fprintf(stderr, "Worker %d: Accepted fd %d\n", getpid(), fd);uv_read_start((uv_stream_t*) client, alloc_buffer, echo_read);}else {uv_close((uv_handle_t*) client, NULL);}
}

首先,我们调用uv_pipe_pending_count()以确保可以读取句柄。如果您的程序可以处理不同类型的句柄, uv_pipe_pending_type()则可以用来确定类型。尽管accept在此代码中看起来很奇怪,但实际上是有道理的。什么 accept传统上做的就是从另一个文件描述符(监听套接字)文件描述符(客户端)。这正是我们在这里所做的。client从中获取文件描述符()queue。从这一点来看,工作人员将执行标准的回显服务器内容。

现在转到主服务器,让我们看一下如何启动工作程序以实现负载平衡。

多回显服务器/main.c

struct child_worker {uv_process_t req;uv_process_options_t options;uv_pipe_t pipe;
} *workers;

child_worker结构包装了流程,以及主流程和单个流程之间的管道。

多回显服务器/main.c

void setup_workers() {round_robin_counter = 0;// ...// launch same number of workers as number of CPUsuv_cpu_info_t *info;int cpu_count;uv_cpu_info(&info, &cpu_count);uv_free_cpu_info(info, cpu_count);child_worker_count = cpu_count;workers = calloc(sizeof(struct child_worker), cpu_count);while (cpu_count--) {struct child_worker *worker = &workers[cpu_count];uv_pipe_init(loop, &worker->pipe, 1);uv_stdio_container_t child_stdio[3];child_stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE;child_stdio[0].data.stream = (uv_stream_t*) &worker->pipe;child_stdio[1].flags = UV_IGNORE;child_stdio[2].flags = UV_INHERIT_FD;child_stdio[2].data.fd = 2;worker->options.stdio = child_stdio;worker->options.stdio_count = 3;worker->options.exit_cb = close_process_handle;worker->options.file = args[0];worker->options.args = args;uv_spawn(loop, &worker->req, &worker->options); fprintf(stderr, "Started worker %d\n", worker->req.pid);}
}

在设置工作程序时,我们使用了漂亮的libuv函数uv_cpu_info来获取CPU数量,以便我们可以启动相等数量的工作程序。再次重要的是,使用第三个参数将管道初始化为充当IPC通道的管道。然后,我们指示子进程stdin将成为可读管道(从子进程的角度来看)。到这里为止一切都很简单。启动工作程序,并等待将文件描述符写入其标准输入。

在其中on_new_connection(TCP基础结构在中初始化 main()),我们接受客户端套接字并将其传递给循环中的下一个工作进程。

多回显服务器/main.c

void on_new_connection(uv_stream_t *server, int status) {if (status == -1) {// error!return;}uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));uv_tcp_init(loop, client);if (uv_accept(server, (uv_stream_t*) client) == 0) {uv_write_t *write_req = (uv_write_t*) malloc(sizeof(uv_write_t));dummy_buf = uv_buf_init("a", 1);struct child_worker *worker = &workers[round_robin_counter];uv_write2(write_req, (uv_stream_t*) &worker->pipe, &dummy_buf, 1, (uv_stream_t*) client, NULL);round_robin_counter = (round_robin_counter + 1) % child_worker_count;}else {uv_close((uv_handle_t*) client, NULL);}
}

uv_write2呼叫处理所有的抽象,它仅仅是在句柄传递(的问题client)作为右边的参数。这样,我们的多进程回显服务器就可以运行了。

感谢Kyle指出uv_write2()即使在发送句柄时也需要一个非空缓冲区。


[1]在本节中,域套接字也代表Windows上的命名管道。

高级事件循环

https://nikhilm.github.io/uvbook/eventloops.html


libuv提供了对事件循环的大量用户控制,并且您可以通过处理多个循环来获得有趣的结果。您还可以将libuv的事件循环嵌入到另一个基于事件循环的库中-想象一个基于Qt的UI,而Qt的事件循环则驱动一个libuv后端,该后端执行密集的系统级任务。

停止事件循环

uv_stop()可用于停止事件循环。循环最早将停止运行的时间是在下一次迭代中,可能是以后。这意味着准备在循环此迭代中处理的事件仍将被处理,因此uv_stop()不能用作终止开关。当uv_stop() 调用时,该循环不会在此迭代中阻塞I / O。这些东西的语义可能有点难以理解,因此让我们看一下 uv_run()所有控制流的发生位置。

src / unix / core.c-uv_run

int uv_run(uv_loop_t* loop, uv_run_mode mode) {int timeout;int r;int ran_pending;r = uv__loop_alive(loop);if (!r)uv__update_time(loop);while (r != 0 && loop->stop_flag == 0) {uv__update_time(loop);uv__run_timers(loop);ran_pending = uv__run_pending(loop);uv__run_idle(loop);uv__run_prepare(loop);timeout = 0;if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)timeout = uv_backend_timeout(loop);uv__io_poll(loop, timeout);

stop_flag由设置uv_stop()。现在,所有libuv回调都在事件循环中被调用,这就是为什么uv_stop()在其中调用它们仍将导致循环发生此迭代的原因。首先,libuv更新计时器,然后运行挂起的计时器,空闲并准备回调,并调用所有挂起的I / O回调。如果您要拨打uv_stop()任何电话,stop_flag 将被设置。这导致uv_backend_timeout()返回0,这就是为什么循环不会在I / O上阻塞的原因。另一方面,如果您调用 uv_stop()了一个检查处理程序,则I / O已经完成并且不受影响。

uv_stop() 在计算结果或出现错误时关闭循环很有用,而不必确保所有处理程序都一步一步停止。

这是一个简单的示例,该示例停止循环并演示循环的当前迭代如何进行。

uvstop / main.c

#include <stdio.h>
#include <uv.h>int64_t counter = 0;void idle_cb(uv_idle_t *handle) {printf("Idle callback\n");counter++;if (counter >= 5) {uv_stop(uv_default_loop());printf("uv_stop() called\n");}
}void prep_cb(uv_prepare_t *handle) {printf("Prep callback\n");
}int main() {uv_idle_t idler;uv_prepare_t prep;uv_idle_init(uv_default_loop(), &idler);uv_idle_start(&idler, idle_cb);uv_prepare_init(uv_default_loop(), &prep);uv_prepare_start(&prep, prep_cb);uv_run(uv_default_loop(), UV_RUN_DEFAULT);return 0;
}

实用工具

https://nikhilm.github.io/uvbook/utilities.html


略。

原文总目录

Table of Contents

  • Introduction

    • Who this book is for
    • Background
    • Code
  • Basics of libuv
    • Event loops
    • Hello World
    • Error handling
    • Handles and Requests
  • Filesystem
    • Reading/Writing files
    • Filesystem operations
    • Buffers and Streams
    • File change events
  • Networking
    • TCP
    • UDP
    • Querying DNS
    • Network interfaces
  • Threads
    • Core thread operations
    • Synchronization Primitives
    • libuv work queue
    • Inter-thread communication
  • Processes
    • Spawning child processes
    • Changing process parameters
    • Detaching processes
    • Sending signals to processes
    • Signals
    • Child Process I/O
    • Pipes
  • Advanced event loops
    • Stopping an event loop
  • Utilities
    • Timers
    • Event loop reference count
    • Idler pattern
    • Passing data to worker thread
    • External I/O with polling
    • Check & Prepare watchers
    • Loading libraries
    • TTY
  • About
    • Licensing

跨平台异步IO库 libuv 源代码接口详解相关推荐

  1. Python|线程和进程|阻塞|非阻塞|同步|异步|生成器和协程|资源竞争|进程间通信|aiohttp库|daemon属性值详解|语言基础50课:学习(11)

    文章目录 系列目录 原项目地址 第34课:Python中的并发编程-1 线程和进程 多线程编程 使用 Thread 类创建线程对象 继承 Thread 类自定义线程 使用线程池 守护线程 资源竞争 G ...

  2. IO流输入输出流入门详解

    IO流输入输出流入门详解 1.Java中以流的形式处理数据 2.流是一种有序的数据序列,分为输入输出流 3.Java是面向对象的程序设计语言,每个数据都是一个对象 4.Java中的输入输出功能来自ja ...

  3. Python的C语言接口 - 详解官方文档

    Python的C语言接口 - 详解官方文档 索引 Python的C语言接口 - 详解官方文档 介绍 / Introduce 代码标准 / Coding Standards 包含文件 / Include ...

  4. mysql relay bin 主库_MySQL主库binlog(master-log)与从库relay-log关系代码详解

    主库binlog: # at 2420 #170809 17:16:20 server id 1882073306 end_log_pos 2451 CRC32 0x58f2db87 Xid = 32 ...

  5. 【java8新特性】——lambda表达式与函数式接口详解(一)

    一.简介 java8于2014年发布,相比于java7,java8新增了非常多的特性,如lambda表达式.函数式接口.方法引用.默认方法.新工具(编译工具).Stream API.Date Time ...

  6. linux 更新软件命令 失败,对linux下软件(库)的更新命令详解

    在ubuntu服务器下安装包的时候,经常会用到sudo apt-get install 包名 或 sudo pip install 包名,那么两者有什么区别呢? 1.区别 pip用来安装来自PyPI( ...

  7. mysql 从库relay_MySQL主库binlog(master-log)与从库relay-log关系代码详解

    主库binlog: # at 2420 #170809 17:16:20 server id 1882073306 end_log_pos 2451 CRC32 0x58f2db87 Xid = 32 ...

  8. 【JavaWeb】Servlet系列——HttpServletRequest接口详解

    文章目录 23 HttpServletRequest接口详解 23.1 关于HttpServletRequest接口 23.2 HttpServletRequest接口的实现类谁写的? HttpSer ...

  9. Callable接口详解

    Callable接口详解 Callable: 返回结果并且可能抛出异常的任务. 优点: 可以获得任务执行返回值: 通过与Future的结合,可以实现利用Future来跟踪异步计算的结果. Runnab ...

最新文章

  1. c语言程序做四则运算还要余数,大整数四则运算 高质量C语言程序.doc
  2. 【Storm】一张图搞定Storm的运行架构
  3. [导入]XML for Analysis(XMLA)开发详解-(3)各版本的SQL Server及Windows下配置XMLA over HTTP访问SASS(OLAP)的文档合集...
  4. SOCKS代理工具EarthWorm、sSoks
  5. Git客户端图文详解如何安装配置GitHub操作流程攻略
  6. 警惕!不要让页面响应时间成为应用性能指标上的杠精
  7. Python破解百度翻js代码
  8. 在职场上被逼「造反」的文科生,半年后25k,还强硬的说我拿低了...
  9. Java TCP协议传输
  10. idea 设置内存_IntelliJ IDEA修改内存使得变得流畅
  11. 软件测试微信钱包,面试官:请你说说微信发红包,有哪些测试点
  12. Java生成无限制带参小程序码
  13. 数学基础——数理逻辑
  14. 企业上云规划与云原生环境设计
  15. python微控制器编程pdf_Python高性能编程 中文高清pdf完整版[17MB]
  16. cms概述 。比较shopex和ecshop区别 。smarty模板引擎的入门
  17. 拍案叫绝的创意如何诞生?
  18. 帝企鹅日记(史上第二受欢迎记录片 老少皆宜的冰雪童话)
  19. 江南大学数字媒体技术大三上作业整理——顾清宇
  20. 用dnsmasq 实现DNS劫持

热门文章

  1. 记:使用IScroll.js 开发picker日历组件遇到的问题及经验总结
  2. windows安装各版本python解释器和anaconda
  3. JSON有关的一道题
  4. python-day02
  5. 【Java并发编程】16、ReentrantReadWriteLock源码分析
  6. 织梦个人空间中调用ip,会员类型,邮箱,金币,会员积分
  7. IntelliJ IDEA打jar时,MANIFEST.MF内容出错
  8. Android4.0升级新特性
  9. C++ 线程同步 (学习笔记)
  10. 计算机水平考试模拟,计算机水平考试模拟试题(doc 23页)