目录
一 概述
github地址
官方文档
官方文档中文
libuv是跨平台、轻量级的异步I/O库,由Node.js团队发起和维护。它提供了事件循环、定时器、异步文件和网络操作等功能,使开发者可以方便地处理各种I/O任务。
libuv提供了一套强大而易用的异步I/O编程接口,在网络编程、文件系统操作、定时器等方面具有广泛的应用场景。由于其开源、跨平台、高效、稳定等优点,被越来越多的开发者采用并集成在自己的项目中。
无论是在Node.js还是其他项目中,libuv都负责处理底层的事件循环和I/O操作,使开发者能够编写高效且非阻塞的代码。它在不同的操作系统上使用不同的后端实现,如epoll
、kqueue
、IOCP
等,以便充分利用各个平台的特性和性能。
“uv” 是指"Unicorn Velociraptor"的缩写。"Unicorn Velociraptor"是libuv的创始人 Bert Belder 的幽默取名,没有特殊的技术含义。
libuv的主要特点包括:
跨平台:libuv可以在多种操作系统上运行,包括Windows、Linux、macOS等,使得开发者无需考虑操作系统的差异性。
异步模型:libuv基于事件驱动模型实现异步I/O,允许应用程序在处理资源紧张、高并发的客户端请求时,不阻塞主线程,提高可伸缩性和响应速度。
网络编程支持:libuv提供了对TCP/UDP以及TLS/SSL等协议的支持,可以轻松实现网络通信功能。
文件系统支持:libuv支持异步文件操作,包括读取、写入、修改、删除等操作,避免文件操作导致的线程阻塞或死锁问题。
定时器支持:libuv提供定时器功能,允许应用程序在一定时间后执行指定的回调函数。
多线程支持:libuv可以创建多个事件循环对象,每个事件循环对象都有自己的I/O线程池,应用程序可以分配不同的任务给不同的事件循环处理。
单以 Linux 平台来看,libuv 主要工作可以简单划为两部分:
- 围绕 epoll,处理那些被 epoll 支持的 IO 操作;
- 线程池(Thread pool),处理那些不被 epoll 支持的 IO 操作;
二 Windows下libuv编译和调用
1 编译
2 调用
添加include文件和链接库:
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
int main()
{
//初始化
uv_loop_t *loop = (uv_loop_t *)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);
getchar();
return 0;
}
三 TCP
1 server
libuv 对于 tcp 消息的处理,同样是基于 stream 的,步骤如下:
uv_tcp_init()
建立 tcp 句柄;uv_tcp_bind()
方法绑定ip;uv_listen()
方法监听,有新连接时,调用回调函数;uv_accept()
方法获取客户端套接字;uv_read_start()
方法读取客户端数据;uv_write()
方法向客户端发送数据;uv_close()
关闭套接字;
void uv_close(uv_handle_t* handle, uv_close_cb close_cb)
typedef void (*uv_close_cb)(uv_handle_t* handle);
void on_close(uv_handle_t *handle) {
if (handle != NULL)
free(handle);
}
uv_close((uv_handle_t *) client, on_close);
2 client
uv_tcp_init()
建立 tcp 句柄;uv_tcp_bind()
方法绑定ip;uv_tcp_connect
连接服务器;uv_write()
方法向服务器发送数据;
int uv_write(uv_write_t* req, uv_stream_t* handle, const uv_buf_t bufs[],
unsigned int nbufs, uv_write_cb cb);
//req:是需要传递给回调函数的数据,发送需要申请资源,并在回调函数中释放
//handle:是接受的客户端
//bufs[]:是一个 uv_buf_t 数组,可以一次添加多组数据,最终按照顺序发送
//nbufs:表示需要发送的数组元素个数,一般小于等于 bufs 的大小
uv_close()
关闭套接字;
3 示例
client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <uv.h>
#include <string>
#include <iostream>
#define DEFAULT_PORT 7000
#define DEFAULT_BACKLOG -1
uv_loop_t* loop;
struct sockaddr_in addr;
uv_tcp_t client;
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);
}
void alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf)
{
buf->base = (char*)malloc(suggested_size);
buf->len = suggested_size;
}
void echo_write(uv_write_t* req, int status)
{
if (status)
{
fprintf(stderr, "Write error %s\n", uv_strerror(status));
}
free_write_req(req);
}
void read_client_cb(uv_stream_t* client, ssize_t nread, const uv_buf_t* buf)
{
if (nread > 0)
{
printf("get message from server: %.*s\n", nread, buf->base);
return;
}
if (nread < 0)
{
if (nread != UV_EOF)
fprintf(stderr, "Read error %s\n", uv_err_name(nread));
uv_close((uv_handle_t*)client, NULL);
}
free(buf->base);
}
void on_connect(uv_connect_t* req, int status)
{
if (status < 0)
{
fprintf(stderr, "Connection error %s\n", uv_strerror(status));
return;
}
uv_read_start((uv_stream_t*)req->handle, alloc_buffer, read_client_cb);
uv_write_t* uvreq = (uv_write_t*)malloc(sizeof(uv_write_t));
char str[] = "hello server, this is client!";
uv_buf_t uvBuf = uv_buf_init(str, strlen(str));
uv_write(uvreq, req->handle, &uvBuf, 1, echo_write);
/*uv_tcp_t* client = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, client);*/
fprintf(stdout, "Connect ok\n");
while (1)
{
std::string s;
std::cin >> str;
uvBuf = uv_buf_init(str, strlen(str));
uv_write(uvreq, req->handle, &uvBuf, 1, echo_write);
}
}
int main()
{
loop = uv_default_loop();
uv_tcp_init(loop, &client);
uv_connect_t* connect = (uv_connect_t*)malloc(sizeof(uv_connect_t));
uv_ip4_addr("127.0.0.1", DEFAULT_PORT, &addr);
int r = uv_tcp_connect(connect, &client, (const struct sockaddr*)&addr, on_connect);
if (r)
{
fprintf(stderr, "connect error %s\n", uv_strerror(r));
return 1;
}
uv_run(loop, UV_RUN_DEFAULT);
return 0;
}
server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <uv.h>
#include <string>
#define DEFAULT_PORT 7000
#define DEFAULT_BACKLOG -1
uv_loop_t* loop;
struct sockaddr_in addr;
void free_write_req(uv_write_t* req)
{
free(req);
}
void alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf)
{
buf->base = (char*)malloc(suggested_size);
buf->len = suggested_size;
}
void write_client_cb(uv_write_t* req, int status)
{
if (status)
{
fprintf(stderr, "Write error %s\n", uv_strerror(status));
}
free_write_req(req);
}
void read_client_cb(uv_stream_t* client, ssize_t nread, const uv_buf_t* buf)
{
if (nread > 0)
{
printf("get message from client: %.*s\n", nread, buf->base);
char str[] = "hello client, this is server!";
printf("write message to client: %.*s\n", nread, str);
uv_write_t* uvreq = (uv_write_t*)malloc(sizeof(uv_write_t));
uv_buf_t uvBuf = uv_buf_init(str, strlen(str));
uv_write(uvreq, client, &uvBuf, 1, write_client_cb);
return;
}
if (nread < 0)
{
if (nread != UV_EOF)
fprintf(stderr, "Read error %s\n", uv_err_name(nread));
uv_close((uv_handle_t*)client, NULL);
}
free(buf->base);
}
void on_new_connection(uv_stream_t* server, int status)
{
if (status < 0)
{
fprintf(stderr, "New connection error %s\n", uv_strerror(status));
return;
}
uv_tcp_t* client = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, client);
//4.uv_accept接收链接。
//5.使用stream处理来和客户端通信。
if (uv_accept(server, (uv_stream_t*)client) == 0)
{
uv_read_start((uv_stream_t*)client, alloc_buffer, read_client_cb);
}
else
{
uv_close((uv_handle_t*)client, NULL);
}
}
int main()
{
//1.uv_tcp_init建立tcp句柄
loop = uv_default_loop();
uv_tcp_t server;
uv_tcp_init(loop, &server);
//2.uv_tcp_bind绑定。
uv_ip4_addr("127.0.0.1", DEFAULT_PORT, &addr);
uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
//3.uv_listen建立监听,当有新的连接到来时,激活调用回调函数。
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;
}
uv_run(loop, UV_RUN_DEFAULT);
return 0;
}
四 UDP
- udp 服务器的例子
/*
libuv udp server
*/
#include <uv.h>
#include <stdio.h>
#include <stdlib.h>
void on_alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf)
{
// Allocate a buffer for receiving data
buf->base = (char*)malloc(suggested_size);
buf->len = suggested_size;
}
void on_recv_data(uv_udp_t* handle, ssize_t nread, const uv_buf_t* buf, const struct sockaddr* addr, unsigned flags)
{
if (nread < 0)
{
// Error occurred while receiving data
fprintf(stderr, "Error: %s\n", uv_strerror(nread));
free(buf->base);
return;
}
if (nread > 0)
{
// Process the received data
printf("Received data: %s\n", buf->base);
}
free(buf->base);
}
int main()
{
uv_loop_t* loop = uv_default_loop();
uv_udp_t server;
uv_udp_init(loop, &server);
struct sockaddr_in addr;
uv_ip4_addr("0.0.0.0", 12345, &addr);
uv_udp_bind(&server, (const struct sockaddr*)&addr, UV_UDP_REUSEADDR);
uv_udp_recv_start(&server, on_alloc_buffer, on_recv_data);
return uv_run(loop, UV_RUN_DEFAULT);
}
- udp 客户端的例子
/*
libuv udp client
*/
#include <uv.h>
#include <stdio.h>
#include <stdlib.h>
#include <cstring>
int main()
{
uv_loop_t* loop = uv_default_loop();
uv_udp_t socket;
uv_udp_init(loop, &socket);
uv_udp_send_t send_req;
uv_buf_t buffer;
const char* message = "Hello, server!";
buffer = uv_buf_init(const_cast<char*>(message), strlen(message));
struct sockaddr_in server_addr;
uv_ip4_addr("127.0.0.1", 12345, &server_addr);
uv_udp_send(&send_req, &socket, &buffer, 1, (const struct sockaddr*)&server_addr, nullptr);
uv_run(loop, UV_RUN_DEFAULT);
uv_loop_close(loop);
return 0;
}
五 线程
1 概述
libuv 的线程 API 与 Linux 的 pthread 的 API 在使用方法和语义上很接近,因为要跨平台,所以 libuv
支持的线程API个数很有限。libuv中只有一个主线程,主线程上只有一个 event loop
。如下为创建线程的一个简单示例:
#include <stdio.h>
#include <uv.h>
void thread_fun(void* arg)//线程方法
{
fprintf(stdout, "thread start\n");
int param = *((int*)arg);
}
int main()
{
int param = 100;
uv_thread_t thread_id;
uv_thread_create(&thread_id, thread_fun, ¶m); //创建线程并开始
uv_thread_join(&thread_id); //等待线程结束
return 0;
}
2 线程同步
①、互斥量
UV_EXTERN int uv_mutex_init(uv_mutex_t* handle); //执行成功返回0,错误返回错误码
UV_EXTERN void uv_mutex_lock(uv_mutex_t* handle); //调试模式下出错会引发abort()中断,而不是返回错误码
UV_EXTERN int uv_mutex_trylock(uv_mutex_t* handle); //libuv的API中并未实现递归上锁
UV_EXTERN void uv_mutex_unlock(uv_mutex_t* handle);
UV_EXTERN void uv_mutex_destroy(uv_mutex_t* handle);
②、读写锁
int uv_rwlock_init(uv_rwlock_t* rwlock);
void uv_rwlock_rdlock(uv_rwlock_t* rwlock); //读
int uv_rwlock_tryrdlock(uv_rwlock_t* rwlock);
void uv_rwlock_rdunlock(uv_rwlock_t* rwlock);
void uv_rwlock_wrlock(uv_rwlock_t* rwlock); //写
int uv_rwlock_trywrlock(uv_rwlock_t* rwlock);
void uv_rwlock_wrunlock(uv_rwlock_t* rwlock);
void uv_rwlock_destroy(uv_rwlock_t* rwlock);
③、屏障
libuv中屏障相关方法分别对应linux中pthread_xxx()相关方法的功能,“屏障”就相当于是线程的栏杆。可以将多个线程挡在同一栏杆前,直到所有线程到齐,然后撤下栏杆同时放行。
int uv_barrier_init(uv_barrier_t* barrier, unsigned int count); //初始化屏障并指定要等待的线程个数
int uv_barrier_wait(uv_barrier_t* barrier); //在线程中调用该方法,告诉屏障我已经到达栏杆处了,然后屏障会检查是否所有线程都已经到达,否的话已经到达栏杆的线程会阻塞,是的话放行所有线程
void uv_barrier_destroy(uv_barrier_t* barrier); //释放屏障
④、其它
libuv同样支持信号量,条件变量,而且API的使用方法和 pthread
中的用法很类似,具体可以参考libuv文档。
libuv中还提供一个 uv_once()
方法,在多个线程中通过 uv_once
来调用指定方法的话,该方法只会被一个线程所调用,如下所示两个线程执行完毕后 g_i
的值是1:
uv_once_t once_only = UV_ONCE_INIT;
int g_i = 0;
void increment() {
i++;
}
void thread1() {
/* ... work */
uv_once(&once_only, increment);
}
void thread2() {
/* ... work */
uv_once(&once_only, increment);
}
3TLS(线程局部存储)
TLS就是只能被线程内的各个方法访问的变量,该变量不能被其它线程访问。如下为libuv中TLS相关方法,具体可以参考libuv文档。
int uv_key_create(uv_key_t* key) //对应pthread中的pthread_key_create()
void uv_key_delete(uv_key_t* key)
void* uv_key_get(uv_key_t* key)
void uv_key_set(uv_key_t* key, void* value)
4 任务队列
libuv
提供了一个线程池,可用于运行用户代码,libuv
中的工作队列中的任务会在线程池中执行;libuv
中的线程池在内部用于运行所有文件系统操作以及 getaddrinfo()
和 getnameinfo()
请求;libuv 中的线程池的默认数量为4,可以在启动时修改环境变量 UV_THREADPOOL_SIZE
来修改,最大值为 1024(1.30.0版本之前是128);libuv
中的线程池是全局的,并在所有事件循环之间共享,当特定的函数利用 uv_queue_work()
方法使用工作队列时,libuv
会预分配线程池,以较小的内存开销(128个线程为1MB),来提高线程性能。
以下三种类型的操作会在全局线程池中进行:
文件系统操作;
DNS函数(getaddrinfo
和 getnameinfo
);
使用 uv_queue_work()
调度的用户代码;
需要注意的是,即使使用了线程池,libuv 的方法也不是线程安全的。
uv_queue_work()
会使用线程池执行指定的任务(线程池默认大小为4,可以通过环境变量UV_THREADPOOL_SIZE
来设置),而且当任务完成后会在主线程中运行指定的回调。uv_queue_work()
的第二个参数需要一个 uv_work_t
(任务方法和完成通知回调方法的参数),可以通过 uv_work_t::data
域来传递上下文数据,如下所示将 uv_work_t
放到上下文数据结构中的话,可以一次 malloc
把 uv_work_t
和数据都申请了出来。
uv_cancel()
可以取消工作队列中的任务,其参数为 uv_work_t
。只有还未开始的任务可以被取消,此时回调通知方法中的 status
参数值为 UV_ECANCELED
,如果任务已经开始执行或者执行完毕,uv_cancel()
返回0。uv_cancel()
同样可以用在 uv_fs_t
和 uv_getaddrinfo_t
请求上。
#include <stdio.h>
#include <uv.h>
struct SData {
uv_work_t req;
char* str;
int n = 0;
};
void fun(uv_work_t* req)
{
SData* pBaton = (SData*)req->data;
}
void after_fun(uv_work_t* req, int status) {
if(status == UV_ECANCELED) { //任务被取消
}
else {
}
SData* pBaton = (SData*)req->data;
free(pBaton->str);
free(pBaton);
}
int main()
{
uv_loop_t* loop = uv_default_loop();
SData* pBaton = (SData*)malloc(sizeof(SData));
pBaton->req.data = (void*)pBaton;
pBaton->str = _strdup("str");
uv_queue_work(loop, &pBaton->req, fun, after_fun);
return uv_run(loop, UV_RUN_DEFAULT);
}
int uv_queue_work(uv_loop_t* loop, uv_work_t* req, uv_work_cb work_cb, uv_after_work_cb after_work_cb);
,添加一个任务到工作队列中,在主线程中调用。
loop
: 事件循环
req
: 传入到任务的数据,一般使用 req.data
参数传递
work_cb
: 执行方法
after_work_cb
: 执行方法完成后执行
work_cb
方法会在函数中执行,after_work_cb
方法在创建线程中执行
void (*uv_work_cb)(uv_work_t* req);
,void (*uv_after_work_cb)(uv_work_t* req, int status);
,如果调用 uv_cancel
方法取消了队列,则 uv_after_work_cb
的 status
为 UV_ECANCELED
。
int uv_cancel(uv_req_t* req);
,取消未执行的队列中的任务,在任务中调用;
req 为任务的参数;
如果调用此方法取消了任务,则 after_work_cb
回调函数的 status
的值为 UV_ECANCELED
;
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <uv.h>
void print(uv_work_t *req)
{
sleep(1);
long num = (long)req->data;
printf("thread id is: %ld, num is: %d\n", uv_thread_self(), num);
}
void after_print(uv_work_t *req, int status)
{
printf("after print, req data is %d, status is %d\n", req->data, status);
}
int main()
{
uv_loop_t *loop = uv_default_loop();
uv_work_t req[5];
for (int index = 0; index < 5; index++)
{
req[index].data = (void *)(long)index;
uv_queue_work(loop, &req[index], print, after_print);
sleep(1);
}
return uv_run(loop, UV_RUN_DEFAULT);
}
5 异步调用
使用 uv_async_init()
和 uv_async_send()
可以异步的执行一个方法,该方法会在 event-loop
主线程中执行。uv_async_send()
相当于是向主线程发了一个消息,主线程收到消息后会执行 uv_async_init()
设置的方法。有可能多次调用uv_async_send
后只运行了一次回调函数:比如你调用了两次 uv_async_send()
,而 libuv很忙,暂时还没有机会运行回调函数。
async handle
可译为异步句柄,它主要是用于提供异步唤醒的功能,比如在用户线程中唤醒主事件循环线程,并且触发对应的回调函数。
从事件循环线程的处理过程可知,它在io循环时会进入阻塞状态,而阻塞的具体时间则通过计算得到,那么在某些情况下,我们想要唤醒事件循环线程,就可以通过ansyc去操作,比如当线程池的线程处理完事件后,执行的结果是需要交给事件循环线程的,这时就需要用到唤醒事件循环线程,当然方法也是很简单,调用一下 uv_async_send()
函数通知事件循环线程即可。libuv线程池中的线程就是利用这个机制和主事件循环线程通讯。
其实深入看 uv__async_start()
源码你就会发现,它实际上也是通过pipe管道进行唤醒的,因为主线程的io循环其实是在观察是否有可读写的io,libuv将管道等都抽象为io观察者了,在io循环中观察管道的读取端,当有数据到来则唤醒,越是深入了解libuv,你就会发现其实就是各个线程、进程间的通信,一种异步的通信,只不过libuv处理的很好,抽象了很多数据结构,并且衍生出了很多子类。
#include <stdio.h>
#include <uv.h>
uv_async_t async; //异步对象
uv_work_t req;
void thread_fun(uv_work_t* req)
{
async.data = (void*)_strdup("data");
uv_async_send(&async); //执行异步对象
}
void after_thread_fun(uv_work_t* req, int status)
{
uv_close((uv_handle_t*)&async, NULL); //释放异步对象,任务完成的回调after_thread_fun()会在异步方法async_fun()之后执行
}
void async_fun(uv_async_t* handle)
{
char* pData = (char*)handle->data;
free(pData);
}
int main()
{
uv_loop_t* loop = uv_default_loop();
uv_async_init(loop, &async, async_fun); //初始化异步对象
uv_queue_work(loop, &req, thread_fun, after_thread_fun);
return uv_run(loop, UV_RUN_DEFAULT);
}
6 API
int uv_async_init(uv_loop_t* loop, uv_async_t* async, uv_async_cb async_cb);
初始化句柄(uv_async_t 类型),回调函数 async_cb
可以为NULL,返回0表示成功,<0 表示错误码;
uv_async_init()
初始化函数不同于其他handle的初始化函数,因为它会立即将 async handle
设置为活跃状态,所以 async handle
没有 start
相关的函数。
int uv_async_send(uv_async_t* async);
唤醒时间循环,执行 async
的回调函数(uv_async_init
初始化指定的回调),async
将被传递给回调函数,返回0表示成功,<0 表示错误码,在任何线程中调用此方法都是安全的,回调函数将会在 uv_async_init
指定的 loop
线程中执行;
void uv_close(uv_handle_t* handle, uv_close_cb close_cb);
和 uv_async_init
对应,调用之后执行回调 close_cb
,handle
会被立即释放,但是 close_cb 会在事件循环到来之时执行,用于释放句柄相关的其他资源
#include <iostream>
#include <uv.h>
#include <stdio.h>
#include <unistd.h>
uv_loop_t *loop;
uv_async_t async;
double percentage;
void print(uv_async_t *handle)
{
printf("thread id: %ld, value is %ld\n", uv_thread_self(), (long)handle->data);
}
void run(uv_work_t *req)
{
long count = (long)req->data;
for (int index = 0; index < count; index++)
{
printf("run thread id: %ld, index: %d\n", uv_thread_self(), index);
async.data = (void *)(long)index;
uv_async_send(&async);
sleep(1);
}
}
void after(uv_work_t *req, int status)
{
printf("done, thread id: %ld\n", uv_thread_self());
uv_close((uv_handle_t *)&async, NULL);
}
int main()
{
printf("main thread id: %ld\n", uv_thread_self());
loop = uv_default_loop();
uv_work_t req;
int size = 5;
req.data = (void *)(long)size;
uv_async_init(loop, &async, print);
uv_queue_work(loop, &req, run, after);
return uv_run(loop, UV_RUN_DEFAULT);
}
7 async的处理过程
前面所介绍的都是初始化与通知的方式,那么在事件循环中怎么去处理 async handle
呢?
我们注意到 uv_async_init()
函数可能多次被调用,初始化多个 async handle
,但是 loop->async_io_watcher
只有一个,那么问题来了,那么多个 async handle
都共用一个io观察者(假设loop是一个),那么在 loop->async_io_watcher
上有 I/O 事件时,并不知道是哪个 async handle
发送的,因此我们要知道 async handle
是如何处理这些的。
我们也知道从 uv__io_init()
函数中已经注册了一个 uv__async_io()
函数用于处理 loop->async_io_watcher
的 I/O 事件,那么我们就看 uv__async_io()
函数的处理过程即可:
static void uv__async_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
char buf[1024];
ssize_t r;
QUEUE queue;
QUEUE* q;
uv_async_t* h;
assert(w == &loop->async_io_watcher);
for (;;) {
/* 不断的读取 w->fd 上的数据到 buf 中直到为空,buf 中的数据无实际用途 */
r = read(w->fd, buf, sizeof(buf));
if (r == sizeof(buf))
continue;
/* 读取到数据跳出循环 */
if (r != -1)
break;
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
if (errno == EINTR)
continue;
abort();
}
QUEUE_MOVE(&loop->async_handles, &queue);
/* 遍历队列 */
while (!QUEUE_EMPTY(&queue)) {
q = QUEUE_HEAD(&queue);
/* 获取async handle */
h = QUEUE_DATA(q, uv_async_t, queue);
QUEUE_REMOVE(q);
/* 重新插入队列 */
QUEUE_INSERT_TAIL(&loop->async_handles, q);
if (0 == uv__async_spin(h))
continue; /* Not pending. */
if (h->async_cb == NULL)
continue;
/* 调用async 的回调函数 */
h->async_cb(h);
}
}
六 定时器
1 概述
在定时器启动后一段特定的时间后,定时器会调用回调函数。Libuv的定时器还可以设定为按照时间间隔定时启动,而不仅仅是启动一次。
简单地使用一个定时器,超过时间 timeout
作为参数初始化定时器(意味第一次启动之后多久响应回调事件),参数 repeat
表示回调函数每次调用的间隔(前提条件是 uv_run
启动了循环事件)。定时器可以在任意时刻被终止。
uv_timer_t timer_req;
int uv_timer_init(uv_loop_t* loop, uv_timer_t* handle);
int uv_timer_start(uv_timer_t* handle, uv_timer_cb cb, uint64_t timeout, uint64_t repeat);
int uv_timer_stop(uv_timer_t* handle);
uv_timer_init
没有什么特殊的地方,只是初始化一下 handle
的状态,并将其添加到 loop->handle_queue
中;
uv_timer_start
内部做了这些工作:
int uv_timer_start(uv_timer_t* handle,
uv_timer_cb cb,
uint64_t timeout,
uint64_t repeat) {
uint64_t clamped_timeout;
// loop->time 表示 loop 当前的时间。loop 每次迭代开始时,会用当次时间更新该值
// clamped_timeout 就是该 timer 未来超时的时间点,这里直接计算好,这样未来就不需要
// 计算了,直接从 timers 中取符合条件的即可
if (clamped_timeout < timeout)
clamped_timeout = (uint64_t) -1;
handle->timer_cb = cb;
handle->timeout = clamped_timeout;
handle->repeat = repeat;
// 除了预先计算好的 clamped_timeout 以外,未来当 clamped_timeout 相同时,使用这里的
// 自增 start_id 作为比较条件来觉得 handle 的执行先后顺序
handle->start_id = handle->loop->timer_counter++;
// 将 handle 插入到 timer_heap 中,这里的 heap 是 binary min heap,所以根节点就是
// clamped_timeout 值(或者 start_id)最小的 handle
heap_insert(timer_heap(handle->loop),
(struct heap_node*) &handle->heap_node,
timer_less_than);
// 设置 handle 的开始状态
uv__handle_start(handle);
return 0;
}
uv_timer_stop
内部做了这些工作:
int uv_timer_stop(uv_timer_t* handle) {
if (!uv__is_active(handle))
return 0;
// 将 handle 移出 timer_heap,和 heap_insert 操作一样,除了移出之外
// 还会维护 timer_heap 以保障其始终是 binary min heap
heap_remove(timer_heap(handle->loop),
(struct heap_node*) &handle->heap_node,
timer_less_than);
// 设置 handle 的状态为停止
uv__handle_stop(handle);
return 0;
}
到目前为止,我们已经知道所谓的 start 和 stop 其实可以粗略地概括为,往属性 loop->timer_heap
中插入或者移出 handle
,并且这个属性使用一个名为 binary min heap
的数据结构;
然后我们再回顾上文的 uv_run
:
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
// ...
while (r != 0 && loop->stop_flag == 0) {
// ...
uv__update_time(loop);
uv__run_timers(loop);
// ...
}
// ...
}
uv__update_time
我们已经见过了,作用就是在循环开头阶段、使用当前时间设置属性 loop->time
,我们只需要最后看一下 uv__run_timers
的内容,就可以串联整个流程:
void uv__run_timers(uv_loop_t* loop) {
struct heap_node* heap_node;
uv_timer_t* handle;
for (;;) {
// 取根节点,该值保证始终是所有待执行的 handle
// 中,最先超时的那一个
heap_node = heap_min(timer_heap(loop));
if (heap_node == NULL)
break;
handle = container_of(heap_node, uv_timer_t, heap_node);
if (handle->timeout > loop->time)
break;
// 停止、移出 handle、顺便维护 timer_heap
uv_timer_stop(handle);
// 如果是需要 repeat 的 handle,则重新加入到 timer_heap 中
// 会在下一次事件循环中、由本方法继续执行
uv_timer_again(handle);
// 执行超时 handle 其对应的回调
handle->timer_cb(handle);
}
}
以上,就是 timer 在 Libuv 中的大致实现方式。
循环的间隔也可以被随时定义,使用:
uv_timer_set_repeat(uv_timer_t* timer,int64_t repeat);
它会在可能的情况下发挥作用。如果上述函数是在定时器回调函数中被调用,这意味着:
如果定时器未设置循环,此时定时器已经停止。需要先用uv_timer_start重新启动。
如果定时器被设置为循环,那么下一次超时的时间已经被规划好,所以在切换新的循环间隔之前,旧的循环间隔仍然还会发挥一次作用。
工具函数:
int uv_timer_again(uv_timer_t *)
只适用于循环定时器,相当于停止定时器,然后把原先的timeout和repeat值都设置为之前的repeat值,并启动定时器。如果调用该函数的时候,定时器尚未启动,那么则调用失败(错误码UV_EINVAL)并返回-1。
2 示例
启动一个循环定时器(repeating timer),它会调用 uv_timer_start
后,5秒(timeout)启动回调函数,然后每2秒(repeat)循环启动回调函数。
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
static uv_timer_t testTimer;
uv_loop_t* g_loop;
void Test(uv_timer_t* handle)
{
fprintf(stdout, "load Test1 %llu\n", uv_now(g_loop));
static int count = 0;
count++;
fprintf(stdout, "update shopActive %d\n", count);
if (count == 5)
{
fprintf(stdout, "save shopData %d\n", count);
count = 0;
}
}
int main(int argc, char* argv[])
{
g_loop = uv_default_loop();
uv_timer_init(g_loop, &testTimer);
uv_timer_start(&testTimer, Test, 5000, 2000);
uv_run(g_loop, UV_RUN_DEFAULT);
printf("main loop stop\n");
return 0;
}
七 管道
1 uv_pipe_t
struct uv_pipe_s {
UV_HANDLE_FIELDS
UV_STREAM_FIELDS
int ipc; /* non-zero if this pipe is used for passing handles */
UV_PIPE_PRIVATE_FIELDS
};
typedef struct uv_pipe_s uv_pipe_t;
这个结构体代表了一个管道(pipe)。管道是一种在进程间进行通信的方法,可以用于在不同的进程之间传递数据。
Pipe handles provide an abstraction over local domain sockets on Unix and named pipes on Windows.
libuv中的uv_pipe起到的是unix like系统中unix域socket以及windows中命名管道的抽象封装,也就意味着我们可以使用这一工具简单的实现进程间通讯(IPC)。
2 uv_write_t
struct uv_write_s {
UV_REQ_FIELDS
uv_write_cb cb;
uv_stream_t* send_handle; /* TODO: make private and unix-only in v2.x. */
uv_stream_t* handle;
UV_WRITE_PRIVATE_FIELDS
};
typedef struct uv_write_s uv_write_t;
这个结构体用于表示写入操作的请求。用于发起向流(例如管道、套接字等)写入数据的异步请求。
3 uv_pipe_init
int uv_pipe_init(uv_loop_t* loop, uv_pipe_t* handle, int ipc);
这个函数用于初始化管道(pipe),初始化一个pipe handle,ipc指示了该管道是否会用来进行进程间handle传输(这里应该指的是进程间文件描述符传递吧?Unix域socket之间的)。形参解析:
loop
:一个指向 libuv 事件循环的指针,表示要将管道初始化到哪个事件循环上。
handle
:一个指向 uv_pipe_t
结构体实例的指针,表示要初始化的管道。
ipc
:一个整数,表示是否启用进程间通信(Inter-Process Communication,IPC)功能。如果传递非零值,则表示启用 IPC 功能;如果传递 0,则表示不启用 IPC 功能。
4 uv_pipe_open
int uv_pipe_open(uv_pipe_t* handle, uv_file file);
这个函数用于将一个已有的文件描述符(例如由其他进程创建的管道)绑定到一个 uv_pipe_t
结构体实例上。打开现存的文件描述符,会被设置成非阻塞模式。形参解析:
handle
:一个指向已初始化的 uv_pipe_t
结构体实例的指针,表示要打开的管道。
file
:一个文件描述符,表示要绑定到管道的已有的文件描述符。
5 uv_pipe_bind
int uv_pipe_bind(uv_pipe_t* handle, const char* name)
绑定到路径(名字)
6 uv_pipe_connect
void uv_pipe_connect(uv_connect_t* req, uv_pipe_t* handle, const char* name, uv_connect_cb cb)
连接(connect)到指定pipe,并调用回调cb
7 uv_pipe_getsockname
int uv_pipe_getsockname(const uv_pipe_t* handle, char* buffer, size_t* size)
获得指定pipe绑定的sockname
8 uv_pipe_getpeername
int uv_pipe_getpeername(const uv_pipe_t* handle, char* buffer, size_t* size)
获得pipe连接的sockname(服务器端)
9 uv_read_start
int uv_read_start(uv_stream_t* stream, uv_alloc_cb alloc_cb, uv_read_cb read_cb);
这个函数用于启动从流(stream)中读取数据。用于设置 libuv 在流上开始异步读取操作的条件和回调函数。形参解析:
stream
:一个指向已初始化的 uv_stream_t
结构体实例的指针,表示要开始读取数据的流。
alloc_cb
:一个回调函数,用于分配内存以存储接收到的数据。它的原型为 void (alloc_cb)(uv_handle_t handle, size_t suggested_size, uv_buf_t* buf)
。
read_cb
:一个回调函数,用于处理接收到的数据。它的原型为 void (read_cb)(uv_stream_t stream, ssize_t nread, const uv_buf_t* buf)
。
10 uv_write
int uv_write(uv_write_t* req, uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs, uv_write_cb cb);
这个函数用于向流(stream)中写入数据。用于发起一个异步写入操作,将数据写入到指定的流中。形参解析:
req
:一个指向 uv_write_t
结构体实例的指针,表示要发起的写入请求。
handle
:一个指向已初始化的 uv_stream_t
结构体实例的指针,表示要写入数据的流。
bufs
:一个 uv_buf_t
数组,表示要写入的数据缓冲区。
nbufs
:表示要写入的数据缓冲区的数量。
cb
:一个回调函数,表示写入操作完成后要执行的回调函数。它的原型为 void (cb)(uv_write_t req, int status)
。
八 常用接口
int uv_is_readable(const uv_stream_t* handle)
如果流可读,则返回1,否则返回0。
int uv_is_writable(const uv_stream_t * handle)
如果流是可写的,则返回1,否则返回0。
int uv_stream_set_blocking(uv_stream_t * handle,int)
启用或禁用流的阻止模式,启用阻止模式后,所有写入将同步完成。否则接口将保持不变,例如,操作的完成或失败仍将通过异步进行的回调报告。
参考
九 回到 libuv
回到 libuv
,我们将以 event-loop
为主要脉络,结合上文提到的 epoll
,以及下面将会介绍到的线程池,继续 libuv
在 Linux 上的实现细节一探究竟。
1 event-loop
I/O(或 事件)循环是 libuv 的核心组件。 它为全部I/O操作建立内容, 并且它必须关联到单个线程。 可以运行多个事件循环 只要每个运行在不同的线程。 libuv 事件循环(或任何其他涉及循环或句柄的API,就此而言) 不是线程安全的 除非另行说明。
事件循环遵循很常见的单线程异步I/O方法:全部(网络) I/O 在非阻塞的套接字上执行,在给定平台上使用最好的可用的机制来轮询: epoll在Linux上、kqueue在OSX和其他BSD上,event ports 在SunOS上 和IOCP在Windows上。 作为循环迭代的一部分,循环将阻塞等待套接字上已被添加到轮询器的I/O活动, 并且回调函数将被触发以指示套接字状态 (可读、可写挂起),这样句柄能够读、写或执行所需要的I/O操作。
为了更好地理解事件循环怎么运作,下面的图表显示了一次循环迭代的所有阶段:
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);
// 处理 timer 队列
uv__run_timers(loop);
// 处理 pending 队列
ran_pending = uv__run_pending(loop);
// 处理 idle 队列
uv__run_idle(loop);
// 处理 prepare 队列
uv__run_prepare(loop);
// 执行 io_poll
uv__io_poll(loop, timeout);
uv__metrics_update_idle_time(loop);
// 执行 check 队列
uv__run_check(loop);
// 执行 closing 队列
uv__run_closing_handles(loop);
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break;
}
return r;
}
之所以各种形式的回调(比如 setTimeout)在优先级上会有差别,就在于它们使用的是不同的队列,而不同的队列在每次事件循环的迭代中的执行顺序不同。
- 循环概念 ‘now’ 被更新。 在开始事件循环计的时候事件循环缓存当前的时间以减少时间相关的系统调用的数目。
- 如果循环处于 活动 状态的话一次迭代开始,否则的话循环立刻终止。 那么, 何时一个循环确定是 活动 的?如果一个循环有活动的和被引用的句柄、 活动的请求或正在关闭的句柄,它被确定为 活动 的。
- 运行适当的计时器。 所有在循环概念 now 之前到期的活动的计时器的回调函数被调用。
- 待处理的回调函数被调用。 大多数情况下,在I/O轮询之后所有的I/O回调函数会被调用。 然而有些情况下,这些回调推延到下一次迭代中。 如果前一次的迭代推延了任何的I/O回调函数的调用,回调函数将在此刻运行。
- 空转句柄的回调函数被调用。 虽有不恰当的名字,当其活动时空转句柄在每次循环迭代时都会运行。
- 准备句柄的回调函数被调用。 在循环将为I/O阻塞前,准备句柄的回调函数被调用。
- 计算轮询时限。 在为I/O阻塞前,循环计算出它应该阻塞多长时间。 这些是计算时限的规则:
- 如果循环使用 UV_RUN_NOWAIT 标志运行,时限是0。
- 如果循环即将被终止(uv_stop() 被调用),时限是0。
- 如果没有活动的句柄或请求,时限是0。
- 如果没有任何活动的空转句柄,时限是0。
- 如果有任何待处理的句柄,时限是0。
- 如果以上均不符合,采用最近的计时器的时限,如果没有活动计时器的话,为无穷大。
- 循环为I/O阻塞。 此刻循环将按上一步计算的时限为I/O阻塞。 对于所有监视给定文件描述符读写操作的I/O相关的句柄, 在此刻它们的回调函数被调用。
- 检查句柄的回调函数被调用。 在循环为I/O阻塞之后,检查句柄的回调函数被调用。 检查句柄基本上与准备句柄相辅相成。
- 关闭 回调函数被调用。 如果一个句柄通过调用 uv_close() 被关闭, 它的回调函数将被调用。
- 当循环以
UV_RUN_ONCE
的特别情况下,它意味着前移。 在I/O阻塞之后也许没有触发I/O回调函数,但是已经过去了一些时间, 所以可能有到期的计时器,这些计时器的回调函数被调用。 - 迭代结束。 如果循环以
UV_RUN_NOWAIT
或UV_RUN_ONCE
模式运行,则迭代结束且uv_run()
将返回。 如果循环以UV_RUN_DEFAULT
运行,它将继续从头开始,如果它仍是活动的,否则的话它也会结束。
重要:libuv 使用了一个线程池来使得异步文件I/O操作可实现, 但是网络I/O 总是在单线程中执行,即每个循环的线程。
2 Event loop reference count
event-loop在没有了活跃的handle之后,便会终止。整套系统的工作方式是:在handle增加时,event-loop的引用计数加1,在handle停止时,引用计数减少1。当然,libuv也允许手动地更改引用计数,通过使用:
void uv_ref(uv_handle_t*);
void uv_unref(uv_handle_t*);
这样,就可以达到允许loop即使在有正在活动的定时器时,仍然能够推出。或者是使用自定义的uv_handle_t对象来使得loop保持工作。
第二个函数可以和间隔循环定时器结合使用。你会有一个每隔x秒执行一次的垃圾回收器,或者是你的网络服务器会每隔一段时间向其他人发送一次心跳信号,但是你不想只有在所有垃圾回收完或者出现错误时才能停止他们。如果你想要在你其他的监视器都退出后,终止程序。这时你就可以立即unref定时器,即便定时器这时是loop上唯一还在运行的监视器,你依旧可以停止uv_run()。
它们同样会出现在node.js中,如js的API中封装的libuv方法。每一个js的对象产生一个uv_handle_t(所有监视器的超类),同样可以被uv_ref和uv_unref。
3 句柄和请求
按照官网的描述,它们是对 event-loop 中执行的操作的抽象,前者表示需要长期存在的操作,后者表示短暂的操作。单看文字描述可能不太好理解,我们看一下它们的使用方式有何不同
对于 Handle 表示的长期存在的操作来说,它们的 API 具有类似下面的形式:
// IO 操作
int uv_poll_init_socket(uv_loop_t* loop, uv_poll_t* handle, uv_os_sock_t socket);
int uv_poll_start(uv_poll_t* handle, int events, uv_poll_cb cb);
int uv_poll_stop(uv_poll_t* poll);
// timer
int uv_timer_init(uv_loop_t* loop, uv_timer_t* handle);
int uv_timer_start(uv_timer_t* handle, uv_timer_cb cb, uint64_t timeout, uint64_t repeat);
int uv_timer_stop(uv_timer_t* handle);
大致都有这三个步骤(并不是全部):初始化 -> 开始 -> 停止。很好理解吧,因为是长期存在的操作,它开始了就会持续被处理,所以需要安排一个「停止」的 API
而对于 Request 表示的短暂操作来说,比如域名解析操作:
int uv_getaddrinfo(uv_loop_t* loop, uv_getaddrinfo_t* req, uv_getaddrinfo_cb getaddrinfo_cb, /* ... */);
域名解析操作的交互形式是,我们提交需要解析的地址,方法会返回解析的结果(这样的感觉似乎有点 HTTP 1.0 请求的样子),所以按「请求 - Request」来命名这样的操作的原因就变得有画面感了
不过 Handle 和 Request 两者不是互斥的概念,Handle 内部实现可能也用到了 Request。因为一些宏观来看的长期操作,在每个时间切片内是可以看成是 Request 的,比如我们处理一个请求,可以看成是一个 Handle,而在当次的请求中,我们很可能会做一些读取和写入的操作,这些操作就可以看成是 Request
min heap
后面我们会看到,除了 timer 之外的 handle 都存放在名为 queue 的数据结构中,而存放 timer handle 的数据结构则为 min heap。那么我们就来看看这样的差别选择有何深意?
所谓 min heap 其实是(如需更全面的介绍,可以参考 Binary Tree):complete binary tree
,根节点为整个 tree 中最小的节点。
先看 binary tree(二元树的定义是):
所有节点都只有最多两个子节点
进一步看 complete binary tree 的定义则是:
除了最后一层以外,其余层中的每个节点都有两个子节点
最后一层的摆布逻辑是,从左往右依次摆放(尽量填满左边)
下面是几个例子:
complete binary tree 的例子:
18
/ \
15 30
/ \ / \
40 50 100 40
/ \ /
8 7 9
下面不是 complete binary tree,因为最后一层没有优先放满左边
18
/ \
40 30
/ \
100 40
min heap 的例子,根节点是最小值、父节点始终小于其子节点:
18
/ \
40 30
/ \
100 40
在 libuv 中对 timer handle 所需的操作是:
添加和移除 timer handle
快速拿到 clamped_timeout 最小的 timer handle
而 min heap 兼顾了上面的需求:
相对数组而言,具有更高的插入和移除的效率
相对链表而言,具有更高的效率来维护极值(这里是最小值)
heap 的实现在文件是 heap-inl.h,我加入了一些注释,有兴趣的同学可以继续一探究竟
pending
上面,我们已经了解了每次事件循环迭代中、处于第一顺位的 timer 的处理,接下来我们来看处在第二顺位的 pending 队列的处理:
static int uv__run_pending(uv_loop_t* loop) {
QUEUE* q;
QUEUE pq;
uv__io_t* w;
if (QUEUE_EMPTY(&loop->pending_queue))
return 0;
QUEUE_MOVE(&loop->pending_queue, &pq);
// 不断从队列中弹出元素进行操作
while (!QUEUE_EMPTY(&pq)) {
q = QUEUE_HEAD(&pq);
QUEUE_REMOVE(q);
QUEUE_INIT(q);
w = QUEUE_DATA(q, uv__io_t, pending_queue);
w->cb(loop, w, POLLOUT);
}
return 1;
}
从源码来看,仅仅是从队列 loop->pending_queue 中不断弹出元素然后执行,并且弹出的元素是 uv__io_t 结构体的属性,从名字来看大致应该是 IO 相关的操作
另外,对 loop->pending_queue 进行插入操作的只有函数 uv__io_feed,该函数的被调用点基本是执行一些 IO 相关的收尾工作
queue
和上文出现的 min heap 一样,queue 也是主要用到的数据结构,所以我们在第一次见到它的时候、顺便介绍一下
min heap 的实现相对更深一些,所以提供了基于源码的注释 heap-inl.h 让感兴趣的读者深入了解一下,而 queue 则相对就简单一些,加上源码中随处会出现操作 queue 的宏,了解这些宏到底做了什么、会让阅读源码时更加安心
接下来我们就一起看看 queue 和一些常用的操作它的宏,首先是起始状态:
queue 在 libuv 中被设计成一个环形结构,所以起始状态就是 next 和 prev 都指向自身
接下来我们来看一下往 queue 插入一个新的元素是怎样的形式:
上图分两部分,上半部分是已有的 queue、h 表示其当前的 head,q 是待插入的元素。下半部分是插入后的结果,图中的红色表示 prev 的通路,紫色表示 next 的通路,顺着通路我们可以发现它们始终是一个环形结构
上图演示的 QUEUE_INSERT_TAIL 顾名思义是插入到队尾,而因为是环形结构,我们需要修改头、尾、待插入元素三者的引用关系
再看一下移除某个元素的形式:
移除某个元素就比较简单了,就是将该元素的 prev 和 next 连接起来即可,这样连接后,就跳过了该元素,使得该元素呈现出被移除的状态(无法在通路中访问到)
继续看下连接两个队列的操作:
看上去貌似很复杂,其实就是把两个环先解开,然后首尾相连成为一个新的环即可。这里通过意识流的作图方式,使用 1 和 2 标注了代码和连接动作的对应关系
最后看一下将队列一分为二的操作:
上图同样通过意识流的作图方式,使用 1 和 2 标注了代码和连接动作的对应关系;将原本以 h 开头的 queue,在 q 处剪开,h 和 q 之前的元素相连接成为一个新的 queue;n 作为另一个 queue 的开头,连接 q 和断开前的队列的末尾,构成另一个 queue
上面演示了一些具有有代表性的 queue 操作,感兴趣的同学可以继续查看 queue.h 来一探究竟
idle,check,prepare
大家或许会奇怪,为什么没有按照它们在事件循环中的顺序进行介绍,而且还把它们三个放在了一起
如果大家在源码中搜索 uv__run_idle
或者 uv__run_check
会更加奇怪,因为我们只能找到它们的声明,甚至找不到它们的定义
其实它们都是在 loop-watcher.c
中通过宏生成的,因为它们的操作都是一样的 - 从各自的队列中取出 handle 然后执行即可
需要说明的是,大家不要被 idle
的名字迷惑了,它并不是事件循环闲置的时候才会执行的队列,而是在每次时间循环迭代中,都会执行的,完全没有 idle
之意
不过要说完全没有 idle 之意似乎也不是特别合适,比如 idle
和 prepare
队列在内部实现上,无非是先后执行的队列而已:
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
// ...
while (r != 0 && loop->stop_flag == 0) {
// ...
uv__run_idle(loop);
uv__run_prepare(loop);
uv__io_poll(loop, timeout);
// ...
}
// ...
}
那么现在有一个 handle
,我们希望它在 uv__io_poll
之前执行,是添加到 idle
还是 prepare
队列中呢?
我觉得 prepare
是取「为了下面的 uv__io_poll
做准备」之意,所以如果是为了 io_poll
做准备的 handle
,那么可以添加到 prepare
队列中,其余则可以添加到 idle
之中。同样的设定我觉得也适用于 check
,它运行在 io_poll
之后,可以让用户做一些检验 IO 执行结果的工作,让任务队列更加语义化。
空转的回调函数会在每一次的event-loop循环激发一次。空转的回调函数可以用来执行一些优先级较低的活动。比如,你可以向开发者发送应用程序的每日性能表现情况,以便于分析,或者是使用用户应用cpu时间来做SETI运算:)。空转程序还可以用于GUI应用。比如你在使用event-loop来下载文件,如果tcp连接未中断而且当前并没有其他的事件,则你的event-loop会阻塞,这也就意味着你的下载进度条会停滞,用户会面对一个无响应的程序。面对这种情况,空转监视器可以保持UI可操作。
uv_loop_t *loop;
uv_fs_t stdin_watcher;
uv_idle_t idler;
char buffer[1024];
int main() {
loop = uv_default_loop();
uv_idle_init(loop, &idler);
uv_buf_t buf = uv_buf_init(buffer, 1024);
uv_fs_read(loop, &stdin_watcher, 0, &buf, 1, -1, on_type);
uv_idle_start(&idler, crunch_away);
return uv_run(loop, UV_RUN_DEFAULT);
}
上述程序中,我们将空转监视器和我们真正关心的事件排在一起。crunch_away会被循环地调用,直到输入字符并回车。然后程序会被中断很短的时间,用来处理数据读取,然后在接着调用空转的回调函数。
void crunch_away(uv_idle_t* handle) {
// Compute extra-terrestrial life
// fold proteins
// computer another digit of PI
// or similar
fprintf(stderr, "Computing PI...\n");
// just to avoid overwhelming your terminal emulator
uv_idle_stop(handle);
io poll
对于 io_poll 我们还是从事件循环开始分析
从事件循环开始
下面是上文已经介绍过的事件循环的片段:
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
// ...
while (r != 0 && loop->stop_flag == 0) {
// ...
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
uv__io_poll(loop, timeout);
// ...
}
// ...
}
上面的代码计算了一个 timeout 用于调用 uv__io_poll(loop, timeout)
的确是 epoll
uv__io_poll 定义在 linux-core.c 中,虽然这是一个包含注释在内接近 300 行的函数,但想必大家也发现了,其中的核心逻辑就是开头演示的 epoll 的用法:
void uv__io_poll(uv_loop_t* loop, int timeout) {
while (!QUEUE_EMPTY(&loop->watcher_queue)) {
// ...
// `loop->backend_fd` 是使用 `epoll_create` 创建的 epoll 实例
epoll_ctl(loop->backend_fd, op, w->fd, &e)
// ...
}
// ...
for (;;) {
// ...
if (/* ... */) {
// ...
} else {
// ...
// `epoll_wait` 和 `epoll_pwait` 只有细微的差别,所以这里只考虑前者
nfds = epoll_wait(loop->backend_fd,
events,
ARRAY_SIZE(events),
timeout);
// ...
}
}
// ...
for (i = 0; i < nfds; i++) {
// ...
w = loop->watchers[fd];
// ...
w->cb(loop, w, pe->events);
}
}
timeout
epoll_wait 的 timeout 参数的含义是:
- 如果是 -1 表示一直等到有事件产生
- 如果是 0 则立即返回,包含调用时产生的事件
- 如果是其余整数,则以 milliseconds 为单位,规约到未来某个系统时间片内
结合上面这些,我们看下 uv_backend_timeout 是如何计算 timeout 的:
int uv_backend_timeout(const uv_loop_t* loop) {
// 时间循环被外部停止了,所以让 `uv__io_poll` 理解返回
// 以便尽快结束事件循环
if (loop->stop_flag != 0)
return 0;
// 没有待处理的 handle 和 request,则也不需要等待了,同样让 `uv__io_poll`
// 尽快返回
if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
return 0;
// idle 队列不为空,也要求 `uv__io_poll` 尽快返回,这样尽快进入下一个时间循环
// 否则会导致 idle 产生过高的延迟
if (!QUEUE_EMPTY(&loop->idle_handles))
return 0;
// 和上一步目的一样,不过这里是换成了 pending 队列
if (!QUEUE_EMPTY(&loop->pending_queue))
return 0;
// 和上一步目的一样,不过这里换成,待关闭的 handles,都是为了避免目标队列产生
// 过高的延迟
if (loop->closing_handles)
return 0;
return uv__next_timeout(loop);
}
int uv__next_timeout(const uv_loop_t* loop) {
const struct heap_node* heap_node;
const uv_timer_t* handle;
uint64_t diff;
heap_node = heap_min(timer_heap(loop));
// 如果没有 timer 待处理,则可以放心的 block 住,等待事件到达
if (heap_node == NULL)
return -1; /* block indefinitely */
handle = container_of(heap_node, uv_timer_t, heap_node);
// 有 timer,且 timer 已经到了要被执行的时间内,则需让 `uv__io_poll`
// 尽快返回,以在下一个事件循环迭代内处理超时的 timer
if (handle->timeout <= loop->time)
return 0;
// 没有 timer 超时,用最小超时间减去、当前的循环时间的差值,作为超时时间
// 因为在为了这个差值时间内是没有 timer 超时的,所以可以放心 block 以等待
// epoll 事件
diff = handle->timeout - loop->time;
if (diff > INT_MAX)
diff = INT_MAX;
return (int) diff;
}
上面的 uv__next_timeout 实现主要分为三部分:
- 只有在没有 timer 待处理的时候,才会是 -1,结合本节开头对 epoll_wait 的 timeout 参数的解释,-1 会让后续的 uv__io_poll 进入 block 状态、完全等待事件的到达
- 当有 timer,且有超时的 timer handle->timeout <= loop->time,则返回 0,这样 uv__io_poll 不会 block 住事件循环,目的是为了快速进入下一次事件循环、以执行超时的 timer
- 当有 timer,不过都没有超时,则计算最小超时时间 diff 来作为 uv__io_poll 的阻塞时间
不知道大家发现没有,timeout 的计算,其核心指导思想就是要尽可能的让 CPU 时间能够在事件循环的多次迭代的、多个不同任务队列的执行、中尽可能的分配均匀,避免某个类型的任务产生很高的延迟
小栗子
了解了 io_poll 队列是如何执行之后,我们通过一个 echo server 的小栗子,来对 io_poll 有个整体的认识:
uv_loop_t *loop;
void echo_write(uv_write_t *req, int status) {
// ...
// 一些无所谓有,但有所谓无的收尾工作
}
void echo_read(uv_stream_t *client, ssize_t nread, uv_buf_t buf) {
// ...
// 创建一个写入请求(上文已经介绍过 Request 和 Handle 的区别),
// 将读取的客户端内容写回给客户端,写入完成后进入回调 `echo_write`
uv_write_t *write_req = (uv_write_t*)malloc(sizeof(uv_write_t));
uv_write(write_req, client, &buf, 1, echo_write);
}
void on_new_connection(uv_stream_t *server, int status) {
// ...
// 创建 client 实例并关联到事件循环
uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, client);
// 与建立客户端连接,并读取客户端输入,读取完成后进入 `echo_read` 回调
if (uv_accept(server, (uv_stream_t*) client) == 0) {
uv_read_start((uv_stream_t*) client, alloc_buffer, echo_read);
}
// ...
}
int main() {
// 创建事件循环
loop = uv_default_loop();
// 创建 server 实例并关联事件循环
uv_tcp_t server;
uv_tcp_init(loop, &server);
// ...
// 绑定 server 到某个端口,并接受请求
uv_tcp_bind(&server, uv_ip4_addr("0.0.0.0", 7000));
// 新的客户端请求到达后,会进去到 `on_new_connection` 回调
uv_listen((uv_stream_t*) &server, 128, on_new_connection);
// ...
// 启动事件循环
return uv_run(loop, UV_RUN_DEFAULT);
}
十 Thead pool
到目前为止,我们已经确认过 io_poll 内部实现确实是使用的 epoll。在本文的开头,我们也提到 epoll 目前并不能处理所有的 IO 操作,对于那些 epoll 不支持的 IO 操作,libuv 统一使用其内部的线程池来模拟出异步 IO。接下来我们看看线程池的大致工作形式
创建
因为我们已经知道读写文件的操作是无法使用 epoll 的,那么就顺着这个线索,通过 uv_fs_read 的内部实现,找到 uv__work_submit 方法,发现是在其中初始化的线程池:
void uv__work_submit(uv_loop_t* loop,
struct uv__work* w,
enum uv__work_kind kind,
void (*work)(struct uv__work* w),
void (*done)(struct uv__work* w, int status)) {
uv_once(&once, init_once);
// ...
post(&w->wq, kind);
}
所以线程池的创建、是一个延迟创建的单例。init_once 内部会调用 init_threads 来完成线程池初始化工作:
static uv_thread_t default_threads[4];
static void init_threads(void) {
// ...
nthreads = ARRAY_SIZE(default_threads);
val = getenv("UV_THREADPOOL_SIZE");
// ...
for (i = 0; i < nthreads; i++)
if (uv_thread_create(threads + i, worker, &sem))
abort();
// ...
}
通过上面的实现,我们知道默认的线程池中线程的数量是 4,并且可以通过 UV_THREADPOOL_SIZE 环境变量重新指定该数值
除了对线程池进行单例延迟创建,uv__work_submit 当然还是会提交任务的,这部分工作是由 post(&w->wq, kind) 完成的,我们来看下 post 方法的实现细节:
static void post(QUEUE* q, enum uv__work_kind kind) {
uv_mutex_lock(&mutex);
// ...
// 将任务插入到 `wq` 这个线程共享的队列中
QUEUE_INSERT_TAIL(&wq, q);
// 如果有空闲线程,则通知它们开始工作
if (idle_threads > 0)
uv_cond_signal(&cond);
uv_mutex_unlock(&mutex);
}
可以发现对于提交任务,其实就是将任务插入到线程共享队列 wq,并且有空闲线程时才会通知它们工作。那么,如果此时没有空闲线程的话,是不是任务就被忽略了呢?答案是否,因为工作线程会在完成当前工作后,主动检查 wq 队列是否还有待完成的工作,有的话会继续完成,没有的话,则进入睡眠,等待下次被唤醒(后面会继续介绍这部分细节)
任务如何调度
上面在创建线程的时候 uv_thread_create(threads + i, worker, &sem) 中的 worker 就是线程执行的内容,我们来看下 worker 的大致内容:
// 线程池的 wq,提交的任务都先链到其中
static QUEUE wq;
static void worker(void* arg) {
// ...
// `idle_threads` 和 `run_slow_work_message` 这些是线程共享的,所以要加个锁
uv_mutex_lock(&mutex);
for (;;) {
// 这里的条件判断,可以大致看成是「没有任务」为 true
while (QUEUE_EMPTY(&wq) ||
(QUEUE_HEAD(&wq) == &run_slow_work_message &&
QUEUE_NEXT(&run_slow_work_message) == &wq &&
slow_io_work_running >= slow_work_thread_threshold())) {
// 轮转到当前进程时因为没有任务,则无事可做
// 空闲线程数 +1
idle_threads += 1;
// `uv_cond_wait` 内部是使用 `pthread_cond_wait` 调用后会:
// - 让线程进入等待状态,等待条件变量 `cond` 发生变更
// - 对 `mutex` 解锁
//
// 此后,其他线程中均可使用 `uv_cond_signal` 内部是 `pthread_cond_signal`
// 来广播一个条件变量 `cond` 变更的事件,操作系统内部会随机唤醒一个等待 `cond`
// 变更的线程,并在被唤醒线程的 uv_cond_wait 调用返回之前,对之前传入的 `mutex`
// 参数上锁
//
// 因此循环跳出(有任务)后,`mutex` 一定是上锁的
uv_cond_wait(&cond, &mutex);
idle_threads -= 1;
}
// ...
// 因为上锁了,所以放心进行队列的弹出操作
q = QUEUE_HEAD(&wq);
QUEUE_REMOVE(q);
// ...
// 因为已经完成了弹出,可以解锁,让其他线程可以继续操作队列
uv_mutex_unlock(&mutex);
// 利用 c 结构体的小特性,做字段偏移,拿到 `q` 所属的 `uv__work` 实例
w = QUEUE_DATA(q, struct uv__work, wq);
w->work(w);
// 下面要操作 `w->loop->wq` 所以要上锁
uv_mutex_lock(&w->loop->wq_mutex);
w->work = NULL;
// 需要看仔细,和开头部分线程池中的 wq 区别开
QUEUE_INSERT_TAIL(&w->loop->wq, &w->wq);
// 唤醒主线程的事件循环
uv_async_send(&w->loop->wq_async);
uv_mutex_unlock(&w->loop->wq_mutex);
// 这一步上锁是必须的,因为下次迭代的开头又需要
// 操作共享内存,不过不必担心死锁,因为它和下一次迭代
// 中的 `uv_cond_wait` 解锁操作是对应的
uv_mutex_lock(&mutex);
// ...
}
}
上面我们保留了相对重要的内容,并加以注释。可以大致地概括为:
- 对于线程池中的线程,会通过 uv_cond_wait 来等待被唤醒
- 线程被唤醒后就从 wq 中主动找一个任务做,完成任务就唤醒主线程,因为回调需要在主线程被执行
- 随后就进入下一次迭代,如果有任务,就继续完成,直至没有任务时,通过 uv_cond_wait 再次进入睡眠状态
- 唤醒是通过在另外的线程中使用 uv_cond_signal 来通知操作系统做调度
- 线程池是一个可伸缩的设计,当一个任务都没有时,线程会都进入睡眠状态,当任务逐渐增多时,会由活动的线程尝试唤醒睡眠中的线程
唤醒主线程
当线程池完成任务后,需要通知主线程执行对应的回调。通知的方式很有意思,我们先来看下事件循环初始化操作 uv_loop_init:
int uv_loop_init(uv_loop_t* loop) {
// ...
// 初始化 min heap 和各种队列,用于存放各式的 handles
heap_init((struct heap*) &loop->timer_heap);
QUEUE_INIT(&loop->wq);
QUEUE_INIT(&loop->idle_handles);
QUEUE_INIT(&loop->async_handles);
QUEUE_INIT(&loop->check_handles);
QUEUE_INIT(&loop->prepare_handles);
QUEUE_INIT(&loop->handle_queue);
// ...
// 调用 `epoll_create` 创建 epoll 实例
err = uv__platform_loop_init(loop);
if (err)
goto fail_platform_init;
// ...
// 用于线程池通知的初始化
err = uv_async_init(loop, &loop->wq_async, uv__work_done);
// ...
}
上面的代码中 uv_async_init 是用于初始化线程池通知相关的工作,下面是它的函数签名:
int uv_async_init(uv_loop_t* loop, uv_async_t* handle, uv_async_cb async_cb);
所以第三个实参 uv__work_done 其实是一个回调函数,我们可以看下它的内容:
void uv__work_done(uv_async_t* handle) {
struct uv__work* w;
uv_loop_t* loop;
QUEUE* q;
QUEUE wq;
int err;
loop = container_of(handle, uv_loop_t, wq_async);
uv_mutex_lock(&loop->wq_mutex);
// 将目前的 `loop->wq` 全部移动到局部变量 `wq` 中,
//
// `loop->wq` 中的内容是在上文 worker 中任务完成后使用
// `QUEUE_INSERT_TAIL(&w->loop->wq, &w->wq)` 添加的
//
// 这样尽快释放锁,让其他任务可尽快接入
QUEUE_MOVE(&loop->wq, &wq);
uv_mutex_unlock(&loop->wq_mutex);
// 遍历 `wq` 执行其中每个任务的完成回调
while (!QUEUE_EMPTY(&wq)) {
q = QUEUE_HEAD(&wq);
QUEUE_REMOVE(q);
w = container_of(q, struct uv__work, wq);
err = (w->work == uv__cancelled) ? UV_ECANCELED : 0;
w->done(w, err);
}
}
知道了 uv__work_done 就是负责执行任务完成回调的工作后,继续看一下 uv_async_init 的内容,看看其内部是如何使用 uv__work_done 的:
int uv_async_init(uv_loop_t* loop, uv_async_t* handle, uv_async_cb async_cb) {
// ...
// 待调查
err = uv__async_start(loop);
// ...
// 创建了一个 async handle
uv__handle_init(loop, (uv_handle_t*)handle, UV_ASYNC);
// 在目前的脉络中 `async_cb` 就是 `uv__work_done` 了
handle->async_cb = async_cb;
handle->pending = 0;
// 把 async handle 加入到队列 `loop->async_handles` 中
QUEUE_INSERT_TAIL(&loop->async_handles, &handle->queue);
// ...
}
我们继续看一下之前待调查的 uv__async_start 的内容:
static int uv__async_start(uv_loop_t* loop) {
// ...
// `eventfd` 可以创建一个 epoll 内部维护的 fd,该 fd 可以和其他真实的 fd(比如 socket fd)一样
// 添加到 epoll 实例中,可以监听它的可读事件,也可以对其进行写入操作,因此就用户代码就可以借助这个
// 看似虚拟的 fd 来实现的事件订阅了
err = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (err < 0)
return UV__ERR(errno);
pipefd[0] = err;
pipefd[1] = -1;
// ...
uv__io_init(&loop->async_io_watcher, uv__async_io, pipefd[0]);
uv__io_start(loop, &loop->async_io_watcher, POLLIN);
loop->async_wfd = pipefd[1];
return 0;
}
我们知道 epoll
是支持 socket fd
的,对于支持的 fd
,epoll
的事件调度将非常的高效。而对于不支持的 IO 操作,libuv 则使用 eventfd
创建一个虚拟的 fd
,继续利用 fd
的事件调度功能
我们继续看下上面出现的 uv__io_start
的细节,来确认一下事件订阅的步骤:
void uv__io_start(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
// ...
// 大家可以翻到上面 `uv__io_poll` 的部分,会发现其中有遍历 `loop->watcher_queue`
// 将其中的 fd 都加入到 epoll 实例中,以订阅它们的事件的动作
if (QUEUE_EMPTY(&w->watcher_queue))
QUEUE_INSERT_TAIL(&loop->watcher_queue, &w->watcher_queue);
// 将 fd 和对应的任务关联的操作,同样可以翻看上面的 `uv__io_poll`,当接收到事件
// 通知后,会有从 `loop->watchers` 中根据 fd 取出任务并执行其完成回调的动作
// 另外,根据 fd 确保 watcher 不会被重复添加
if (loop->watchers[w->fd] == NULL) {
loop->watchers[w->fd] = w;
loop->nfds++;
}
}
确认了事件订阅步骤以后,我们来看下事件回调的内容。上面的形参 w
在我们目前的脉络中,对应的实参是 loop->async_io_watcher
,而它是通过 uv__io_init(&loop->async_io_watcher, uv__async_io, pipefd[0])
初始化的,我们看一下 uv__io_init
的函数签名:
void uv__io_init(uv__io_t* w, uv__io_cb cb, int fd);
所以 uv__async_io
是接收到虚拟 fd
事件的回调函数,继续看下它的内容:
static void uv__async_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
// ...
// 确保 `w` 必定是 `loop->async_io_watcher`
assert(w == &loop->async_io_watcher);
for (;;) {
// 从中读一些内容,`w->fd` 就是上面使用 `eventfd` 创建的虚拟 fd
// 不出意外的话,通知那端的方式、一定是往这个 fd 里面写入一些内容,我们可以后面继续确认
// 从中读取一些内容的目的是避免缓冲区被通知所用的不含实际意义的字节占满
r = read(w->fd, buf, sizeof(buf));
// ...
}
// 执行 `loop->async_handles` 队列,任务实际的回调
QUEUE_MOVE(&loop->async_handles, &queue);
while (!QUEUE_EMPTY(&queue)) {
q = QUEUE_HEAD(&queue);
h = QUEUE_DATA(q, uv_async_t, queue);
QUEUE_REMOVE(q);
QUEUE_INSERT_TAIL(&loop->async_handles, q);
// ...
h->async_cb(h);
}
}
我们已经知道了事件的订阅,以及事件响应的方式
接着继续确认一下事件通知是如何在线程池中触发的。uv_async_send
是唤醒主线程的开放 API,它其实是调用的内部 API uv__async_send
:
static void uv__async_send(uv_loop_t* loop) {
const void* buf;
ssize_t len;
int fd;
// ...
fd = loop->async_io_watcher.fd;
do
// 果然事件通知这一端就是往 `eventfd` 创建的虚拟 fd 写入数据
// 剩下的就是交给 epoll 高效的事件调度机制唤醒事件订阅方就可以了
r = write(fd, buf, len);
while (r == -1 && errno == EINTR);
// ...
}
我们最后通过一副意识流的图,对上面的线程池的流程进行小结:
上图中我们的任务是在 uv__run_idle(loop)
; 执行的回调中通过 uv__work_submit
完成的,但是实际上,对于使用事件循环的应用而言,整个应用的时间片都划分在了各个不同的队列回调中,所以实际上、从其余的队列中提交任务也是可能的
closing
我们开头已经介绍过,只有 Handle
才配备了关闭的 API,因为 Request
是一个短暂任务。Handle 的关闭需要使用 uv_close
:
void uv_close(uv_handle_t* handle, uv_close_cb close_cb) {
assert(!uv__is_closing(handle));
handle->flags |= UV_HANDLE_CLOSING;
handle->close_cb = close_cb;
switch (handle->type) {
// 根据不同的 handle 类型,执行各自的资源回收工作
case UV_NAMED_PIPE:
uv__pipe_close((uv_pipe_t*)handle);
break;
case UV_TTY:
uv__stream_close((uv_stream_t*)handle);
break;
case UV_TCP:
uv__tcp_close((uv_tcp_t*)handle);
break;
// ...
default:
assert(0);
}
// 添加到 `loop->closing_handles`
uv__make_close_pending(handle);
}
void uv__make_close_pending(uv_handle_t* handle) {
assert(handle->flags & UV_HANDLE_CLOSING);
assert(!(handle->flags & UV_HANDLE_CLOSED));
handle->next_closing = handle->loop->closing_handles;
handle->loop->closing_handles = handle;
}
调用 uv_close
关闭 Handle
后,libuv 会先释放 Handle
占用的资源(比如关闭 fd),随后通过调用 uv__make_close_pending
把 handle
连接到 closing_handles
队列中,该队列会在事件循环中被 uv__run_closing_handles(loop)
调用所执行
使用了事件循环后,业务代码的执行时机都在回调中,由于 closing_handles
是最后一个被执行的队列,所以在其余队列的回调中、那些执行 uv_close
时传递的回调,都会在当次迭代中被执行。
Passing data to worker thread
在使用uv_queue_work的时候,你通常需要给工作线程传递复杂的数据。解决方案是自定义struct,然后使用uv_work_t.data指向它。一个稍微的不同是必须让uv_work_t作为这个自定义struct的成员之一(把这叫做接力棒)。这么做就可以使得,同时回收数据和uv_wortk_t。
struct ftp_baton {
uv_work_t req;
char *host;
int port;
char *username;
char *password;
}
ftp_baton *baton = (ftp_baton*) malloc(sizeof(ftp_baton));
baton->req.data = (void*) baton;
baton->host = strdup("my.webhost.com");
baton->port = 21;
// ...
uv_queue_work(loop, &baton->req, ftp_session, ftp_cleanup);
现在我们创建完了接力棒,并把它排入了队列中。
现在就可以随性所欲地获取自己想要的数据啦。
void ftp_session(uv_work_t *req) {
ftp_baton *baton = (ftp_baton*) req->data;
fprintf(stderr, "Connecting to %s\n", baton->host);
}
void ftp_cleanup(uv_work_t *req) {
ftp_baton *baton = (ftp_baton*) req->data;
free(baton->host);
// ...
free(baton);
}
我们既回收了接力棒,同时也回收了监视器。
十一 External I/O with polling
通常在使用第三方库的时候,需要应对他们自己的IO,还有保持监视他们的socket和内部文件。在此情形下,不可能使用标准的IO流操作,但第三方库仍然能整合进event-loop中。所有这些需要的就是,第三方库就必须允许你访问它的底层文件描述符,并且提供可以处理有用户定义的细微任务的函数。但是一些第三库并不允许你这么做,他们只提供了一个标准的阻塞IO函数,此函数会完成所有的工作并返回。在event-loop的线程直接使用它们是不明智的,而是应该使用libuv的工作线程。当然,这也意味着失去了对第三方库的颗粒化控制。
libuv的uv_poll简单地监视了使用了操作系统的监控机制的文件描述符。从某方面说,libuv实现的所有的IO操作,的背后均有uv_poll的支持。无论操作系统何时监视到文件描述符的改变,libuv都会调用响应的回调函数。
现在我们简单地实现一个下载管理程序,它会通过libcurl来下载文件。我们不会直接控制libcurl,而是使用libuv的event-loop,通过非阻塞的异步的多重接口来处理下载,与此同时,libuv会监控IO的就绪状态。
// uvwget/main.c - The setup
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
#include <curl/curl.h>
uv_loop_t *loop;
CURLM *curl_handle;
uv_timer_t timeout;
}
int main(int argc, char **argv) {
loop = uv_default_loop();
if (argc <= 1)
return 0;
if (curl_global_init(CURL_GLOBAL_ALL)) {
fprintf(stderr, "Could not init cURL\n");
return 1;
}
uv_timer_init(loop, &timeout);
curl_handle = curl_multi_init();
curl_multi_setopt(curl_handle, CURLMOPT_SOCKETFUNCTION, handle_socket);
curl_multi_setopt(curl_handle, CURLMOPT_TIMERFUNCTION, start_timeout);
while (argc-- > 1) {
add_download(argv[argc], argc);
}
uv_run(loop, UV_RUN_DEFAULT);
curl_multi_cleanup(curl_handle);
return 0;
}
每种库整合进libuv的方式都是不同的。以libcurl的例子来说,我们注册了两个回调函数。socket回调函数handle_socket会在socket状态改变的时候被触发,因此我们不得不开始轮询它。start_timeout是libcurl用来告知我们下一次的超时间隔的,之后我们就应该不管当前IO状态,驱动libcurl向前。这些也就是libcurl能处理错误或驱动下载进度向前的原因。
可以这么调用下载器:
$ ./uvwget [url1] [url2] …
1
我们可以把url当成参数传入程序。
//uvwget/main.c - Adding urls
void add_download(const char *url, int num) {
char filename[50];
sprintf(filename, "%d.download", num);
FILE *file;
file = fopen(filename, "w");
if (file == NULL) {
fprintf(stderr, "Error opening %s\n", filename);
return;
}
CURL *handle = curl_easy_init();
curl_easy_setopt(handle, CURLOPT_WRITEDATA, file);
curl_easy_setopt(handle, CURLOPT_URL, url);
curl_multi_add_handle(curl_handle, handle);
fprintf(stderr, "Added download %s -> %s\n", url, filename);
}
我们允许libcurl直接向文件写入数据。
start_timeout会被libcurl立即调用。它会启动一个libuv的定时器,使用CURL_SOCKET_TIMEOUT驱动curl_multi_socket_action,当其超时时,调用它。curl_multi_socket_action会驱动libcurl,也会在socket状态改变的时候被调用。但在我们深入讲解它之前,我们需要轮询监听socket,等待handle_socket被调用。
//uvwget/main.c - Setting up polling
void start_timeout(CURLM *multi, long timeout_ms, void *userp) {
if (timeout_ms <= 0)
timeout_ms = 1; /* 0 means directly call socket_action, but we'll do it in a bit */
uv_timer_start(&timeout, on_timeout, timeout_ms, 0);
}
int handle_socket(CURL *easy, curl_socket_t s, int action, void *userp, void *socketp) {
curl_context_t *curl_context;
if (action == CURL_POLL_IN || action == CURL_POLL_OUT) {
if (socketp) {
curl_context = (curl_context_t*) socketp;
}
else {
curl_context = create_curl_context(s);
curl_multi_assign(curl_handle, s, (void *) curl_context);
}
}
switch (action) {
case CURL_POLL_IN:
uv_poll_start(&curl_context->poll_handle, UV_READABLE, curl_perform);
break;
case CURL_POLL_OUT:
uv_poll_start(&curl_context->poll_handle, UV_WRITABLE, curl_perform);
break;
case CURL_POLL_REMOVE:
if (socketp) {
uv_poll_stop(&((curl_context_t*)socketp)->poll_handle);
destroy_curl_context((curl_context_t*) socketp);
curl_multi_assign(curl_handle, s, NULL);
}
break;
default:
abort();
}
return 0;
}
我们关心的是socket的文件描述符s,还有action。对应每一个socket,我们都创造了uv_poll_t,并用curl_multi_assign把它们关联起来。每当回调函数被调用时,socketp都会指向它。
在下载完成或失败后,libcurl需要移除poll。所以我们停止并回收了poll的handle。
我们使用UV_READABLE或UV_WRITABLE开始轮询,基于libcurl想要监视的事件。当socket已经准备好读或写后,libuv会调用轮询的回调函数。在相同的handle上调用多次uv_poll_start是被允许的,这么做可以更新事件的参数。curl_perform是整个程序的关键。
//uvwget/main.c - Driving libcurl.
void curl_perform(uv_poll_t *req, int status, int events) {
uv_timer_stop(&timeout);
int running_handles;
int flags = 0;
if (status < 0) flags = CURL_CSELECT_ERR;
if (!status && events & UV_READABLE) flags |= CURL_CSELECT_IN;
if (!status && events & UV_WRITABLE) flags |= CURL_CSELECT_OUT;
curl_context_t *context;
context = (curl_context_t*)req;
curl_multi_socket_action(curl_handle, context->sockfd, flags, &running_handles);
check_multi_info();
}
首先我们要做的是停止定时器,因为内部还有其他要做的事。接下来我们我们依据触发回调函数的事件,来设置flag。然后,我们使用上述socket和flag作为参数,来调用curl_multi_socket_action。在此刻libcurl会在内部完成所有的工作,然后尽快地返回事件驱动程序在主线程中急需的数据。libcurl会在自己的队列中将传输进度的消息排队。对于我们来说,我们只关心是否传输完成,这类消息。所以我们将这类消息提取出来,并将传输完成的handle回收。
uvwget/main.c - Reading transfer status.
void check_multi_info(void) {
char *done_url;
CURLMsg *message;
int pending;
while ((message = curl_multi_info_read(curl_handle, &pending))) {
switch (message->msg) {
case CURLMSG_DONE:
curl_easy_getinfo(message->easy_handle, CURLINFO_EFFECTIVE_URL,
&done_url);
printf("%s DONE\n", done_url);
curl_multi_remove_handle(curl_handle, message->easy_handle);
curl_easy_cleanup(message->easy_handle);
break;
default:
fprintf(stderr, "CURLMSG default\n");
abort();
}
}
}
Loading libraries
libuv提供了一个跨平台的API来加载共享库shared libraries。这就可以用来实现你自己的插件/扩展/模块系统,它们可以被nodejs通过require()调用。只要你的库输出的是正确的符号,用起来还是很简单的。在载入第三方库的时候,要注意错误和安全检查,否则你的程序就会表现出不可预测的行为。下面这个例子实现了一个简单的插件,它只是打印出了自己的名字。
首先看下提供给插件作者的接口。
plugin/plugin.h
#ifndef UVBOOK_PLUGIN_SYSTEM
#define UVBOOK_PLUGIN_SYSTEM
// Plugin authors should use this to register their plugins with mfp.
void mfp_register(const char *name);
#endif
你可以在你的程序中给插件添加更多有用的功能(mfp is My Fancy Plugin)。使用了这个api的插件的例子:
plugin/hello.c
#include "plugin.h"
void initialize() {
mfp_register("Hello World!");
}
我们的接口定义了,所有的插件都应该有一个能被程序调用的initialize函数。这个插件被编译成了共享库,因此可以被我们的程序在运行的时候载入。
$ ./plugin libhello.dylib
Loading libhello.dylib
Registered plugin "Hello World!"
Note
共享库的后缀名在不同平台上是不一样的。在Linux上是libhello.so。
使用uv_dlopen首先载入了共享库libhello.dylib。再使用uv_dlsym获取了该插件的initialize函数,最后在调用它。
//plugin/main.c
#include "plugin.h"
typedef void (*init_plugin_function)();
void mfp_register(const char *name) {
fprintf(stderr, "Registered plugin \"%s\"\n", name);
}
int main(int argc, char **argv) {
if (argc == 1) {
fprintf(stderr, "Usage: %s [plugin1] [plugin2] ...\n", argv[0]);
return 0;
}
uv_lib_t *lib = (uv_lib_t*) malloc(sizeof(uv_lib_t));
while (--argc) {
fprintf(stderr, "Loading %s\n", argv[argc]);
if (uv_dlopen(argv[argc], lib)) {
fprintf(stderr, "Error: %s\n", uv_dlerror(lib));
continue;
}
init_plugin_function init_plugin;
if (uv_dlsym(lib, "initialize", (void **) &init_plugin)) {
fprintf(stderr, "dlsym error: %s\n", uv_dlerror(lib));
continue;
}
init_plugin();
}
return 0;
}
函数uv_dlopen需要传入一个共享库的路径作为参数。当它成功时返回0,出错时返回-1。使用uv_dlerror可以获取出错的消息。
uv_dlsym的第三个参数保存了一个指向第二个参数所保存的函数的指针。init_plugin_function是一个函数的指针,它指向了我们所需要的程序插件的函数。
十二 TTY
文字终端长期支持非常标准化的控制序列。它经常被用来增强终端输出的可读性。例如grep --colour。libuv提供了跨平台的,uv_tty_t抽象(stream)和相关的处理ANSI escape codes 的函数。这也就是说,libuv同样在Windows上实现了对等的ANSI codes,并且提供了获取终端信息的函数。
首先要做的是,使用读/写文件描述符来初始化uv_tty_t。如下:
int uv_tty_init(uv_loop_t*, uv_tty_t*, uv_file fd, int readable)
1
设置readable为true,意味着你打算使用uv_read_start从stream从中读取数据。
最好还要使用uv_tty_set_mode来设置其为正常模式。也就是运行大多数的TTY格式,流控制和其他的设置。其他的模式还有这些。
记得当你的程序退出后,要使用uv_tty_reset_mode恢复终端的状态。这才是礼貌的做法。另外要注意礼貌的地方是关心重定向。如果使用者将你的命令的输出重定向到文件,控制序列不应该被重写,因为这会阻碍可读性和grep。为了保证文件描述符确实是TTY,可以使用uv_guess_handle函数,比较返回值是否为UV_TTY。
下面是一个把白字打印到红色背景上的例子。
tty/main.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <uv.h>
uv_loop_t *loop;
uv_tty_t tty;
int main() {
loop = uv_default_loop();
uv_tty_init(loop, &tty, 1, 0);
uv_tty_set_mode(&tty, UV_TTY_MODE_NORMAL);
if (uv_guess_handle(1) == UV_TTY) {
uv_write_t req;
uv_buf_t buf;
buf.base = "\033[41;37m";
buf.len = strlen(buf.base);
uv_write(&req, (uv_stream_t*) &tty, &buf, 1, NULL);
}
uv_write_t req;
uv_buf_t buf;
buf.base = "Hello TTY\n";
buf.len = strlen(buf.base);
uv_write(&req, (uv_stream_t*) &tty, &buf, 1, NULL);
uv_tty_reset_mode();
return uv_run(loop, UV_RUN_DEFAULT);
}
最后要说的是uv_tty_get_winsize(),它能获取到终端的宽和长,当成功获取后返回0。下面这个小程序实现了一个动画的效果。
// tty-gravity/main.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <uv.h>
uv_loop_t *loop;
uv_tty_t tty;
uv_timer_t tick;
uv_write_t write_req;
int width, height;
int pos = 0;
char *message = " Hello TTY ";
void update(uv_timer_t *req) {
char data[500];
uv_buf_t buf;
buf.base = data;
buf.len = sprintf(data, "\033[2J\033[H\033[%dB\033[%luC\033[42;37m%s",
pos,
(unsigned long) (width-strlen(message))/2,
message);
uv_write(&write_req, (uv_stream_t*) &tty, &buf, 1, NULL);
pos++;
if (pos > height) {
uv_tty_reset_mode();
uv_timer_stop(&tick);
}
}
int main() {
loop = uv_default_loop();
uv_tty_init(loop, &tty, 1, 0);
uv_tty_set_mode(&tty, 0);
if (uv_tty_get_winsize(&tty, &width, &height)) {
fprintf(stderr, "Could not get TTY information\n");
uv_tty_reset_mode();
return 1;
}
fprintf(stderr, "Width %d, height %d\n", width, height);
uv_timer_init(loop, &tick);
uv_timer_start(&tick, update, 200, 200);
return uv_run(loop, UV_RUN_DEFAULT);
}
escape codes的对应表如下:
代码 | 意义 |
---|---|
2 J Clear part of the screen, 2 is entire screen | |
H Moves cursor to certain position, default top-left | |
n B Moves cursor down by n lines | |
n C Moves cursor right by n columns | |
m Obeys string of display settings, in this case green background (40+2), white text (30+7) | |
正如你所见,它能输出酷炫的效果,你甚至可以发挥想象,用它来制作电子游戏。更有趣的输出,可以使用http://www.gnu.org/software/ncurses/ncurses.html。 |
十三 参考
Libuv 之 - 只看这篇是不够的
我应该跟libuv说声对不起,我错怪了libuv
libuv线程池和主线程通信原理
libuv中文API手册,中文教程
libuv 源码分析1: loop和poll
libuv
【libuv高效编程】libuv学习超详细教程8——libuv signal 信号句柄解读
libuv库学习笔记-Timers-Event loop reference count等