unix(like)
世界里,一切皆文件,而文件是什么呢?文件就是一串二进制流而已,不管socket
,还是FIFO
、管道、终端,一切都是文件,一切都是流。在信息 交换的过程中,我们对这些流进行数据的收发操作,简称为I/O
操作(input and output)
,从数据流中读取数据,系统会调用read
(读取数据);写入数据,系统调用write
(写入数据)。不过话说回来了 ,计算机里有这么多的流,我怎么知道要操作哪个流呢?对,就是文件描述符,即通常所说的fd
,一个fd
就是一个整数,所以,对这个整数的操作,就是对这个文件(流)的操作。我们创建一个socket
,通过系统调用会,返回一个文件描述符,那么剩下对socket
的操作就会转化为对这个描述符的操作。
在 Libuv 中,通过 循环 来不断取出 watcher 队列中的事件,这个事件结构体上保存了文件描述符,这个文件描述符上的事件和事件触发后的回调,所以可以通过这个事件结构体来初始化 epoll_event,之后利用 epoll_wait 来等待文件描述符上 I/O 事件的发生,事件发生之后,调用相应的回调函数。我们可以通过调用 IO观察者 相关API来将事件推入 watcher 队列中。
下面就让咱们通过这种机制来了解进程/线程间是如何通信的吧!
eventfd
或者管道。这一部分的实现可以看这里。uv_async_t
— 异步句柄uv_async_t
异步句柄类型。
void (*uv_async_cb)
(uv_async_t* handle)
传递给 uv_async_init()
的回调函数的类型定义。
int uv_async_init
(uv_loop_t* loop, uv_async_t* async, uv_async_cb async_cb)
初始化句柄。 允许回调函数为NULL。
返回: | 0 当成功时,或者一个 < 0 的错误代码当失败时。 |
---|
不同于其他句柄初始化函数,句柄立刻开始。
int uv_async_send
(uv_async_t* async)
唤醒事件循环并且调用异步句柄的回调函数。
返回: | 0 当成功时,或者一个 < 0 的错误代码当失败时。 |
---|
从任何线程调用这个函数都是安全的。 回调函数将从循环的线程上被调用。
libuv将会合并对 uv_async_send()
的调用,那就是说,不是对它的每个调用会 yield 回调函数的执行。 例如:如果在回调函数被调用前一连调用 uv_async_send()
5 次,回调函数将只会调用一次。 如果在回调函数被调用后再次调用 uv_async_send()
,回调函数将会再次被调用。
#include
#include
#include
#include
uv_loop_t *loop;
uv_async_t async;
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 上
async.data = (void*) &percentage;
// uv_async_send同样是非阻塞的,调用后会立即返回
// 给 loop 进程发送消息
uv_async_send(&async);
sleep(1);
downloaded += (200+random())%1000;
}
}
void after(uv_work_t *req, int status) {
fprintf(stderr, "Download complete\n");
uv_close((uv_handle_t*) &async, NULL);
}
// 函数print_progress是标准的libuv模式,从监视器中抽取数据
void print_progress(uv_async_t *handle) {
// 获取线程池中的线程发来的消息
double percentage = *((double*) handle->data);
fprintf(stderr, "Downloaded %.2f%%\n", percentage);
}
int main() {
loop = uv_default_loop();
uv_work_t req;
int size = 10240;
req.data = (void*) &size;
// 初始化在 loop 上线程通信句柄 async 的回调函数 print_progress
uv_async_init(loop, &async, print_progress);
// 在线程池选取一个线程执行 fake_download 函数
uv_queue_work(loop, &req, fake_download, after);
return uv_run(loop, UV_RUN_DEFAULT);
}
// 执行结果
/*
Downloaded 0.00%
Downloaded 5.69%
Downloaded 6.53%
Downloaded 16.07%
Downloaded 17.20%
Downloaded 26.89%
Downloaded 32.12%
Downloaded 37.84%
Downloaded 44.60%
Downloaded 52.89%
Downloaded 58.96%
Downloaded 64.44%
Downloaded 66.66%
Downloaded 75.35%
Downloaded 77.88%
Downloaded 87.29%
Downloaded 88.52%
Downloaded 95.74%
Download complete
*/
struct uv_async_t {
// 句柄[handle]相关参数
// uv_handle_t
void* data;
uv_loop_t* loop;
uv_handle_type type;
uv_close_cb close_cb;
void* handle_queue[2];
union {
int fd;
void* reserved[4];
} u;
uv_handle_t* next_closing;
unsigned int flags;
// 异步句柄相关参数
uv_async_cb async_cb;
void* queue[2];
int pending;
}
int uv_async_init(uv_loop_t* loop, uv_async_t* handle, uv_async_cb async_cb) {
int err;
/* 其实这个函数的内部主要做了以下操作:创建一个管道,并且将管道注册到loop->async_io_watcher,并且start */
err = uv__async_start(loop);
if (err)
return err;
/* 设置UV_HANDLE_REF标记,并且将async handle 插入loop->handle_queue */
uv__handle_init(loop, (uv_handle_t*)handle, UV_ASYNC);
handle->async_cb = async_cb;
// 标记是否有任务完成了
handle->pending = 0;
/* queue 作为队列节点插入 loop->async_handles */
QUEUE_INSERT_TAIL(&loop->async_handles, &handle->queue);
// 激活 handle 为 active 状态
uv__handle_start(handle);
return 0;
}
#include
int eventfd(unsigned int initval, int flags);
initval 为64位计数器初始值
flags 可以是以下三个标志位的OR结果:
EFD_CLOEXEC
: fork子进程时不继承,对于多线程的程序设上这个值不会有错的。EFD_NONBLOCK
: 文件会被设置成O_NONBLOCK,读操作不阻塞。若不设置,一直阻塞直到计数器中的值大于0。EFD_SEMAPHORE
: 支持 semophore 语义的read,每次读操作,计数器的值自减1。返回 eventfd 类型的文件描述符
读取计数器中的值。
typedef uint64_t eventfd_t;
int eventfd_read(int fd, eventfd_t *value);
- 如果计数器中的值大于0:
- 设置了
EFD_SEMAPHORE
标志位,则返回1,且计数器中的值也减去1。- 没有设置
EFD_SEMAPHORE
标志位,则返回计数器中的值,且计数器置0。
- 如果计数器中的值为0:
- 设置了
EFD_NONBLOCK
标志位就直接返回-1。- 没有设置
EFD_NONBLOCK
标志位就会一直阻塞直到计数器中的值大于0。
向计数器中写入值。
int eventfd_write(int fd, eventfd_t value);
- 如果写入值的和小于0xFFFFFFFFFFFFFFFE,则写入成功
- 如果写入值的和大于0xFFFFFFFFFFFFFFFE
- 设置了
EFD_NONBLOCK
标志位就直接返回-1。- 如果没有设置
EFD_NONBLOCK
标志位,则会一直阻塞直到read操作执行
#include
int close(int fd);
#include
#include
#include
int main() {
int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
eventfd_write(efd, 2);
eventfd_t count;
eventfd_read(efd, &count);
std::cout << count << std::endl;
close(efd);
}
上述程序主要做了如下事情:
- 创建事件,初始计数器为0;
- 写入计数2;
- 读出计数2
- 关闭事件
申请用于通信的文件描述符,然后把读端和回调封装到 loop->async_io_watcher, 写端保存在 loop->async_wfd
// 初始化异步通信的 io 观察者
static int uv__async_start(uv_loop_t* loop) {
int pipefd[2];
int err;
/*
因为 libuv 在初始化的时候会主动注册一个
用于主线程和子线程通信的 async handle。
从而初始化了 async_io_watcher。所以如果后续
再注册 async handle,则不需要处理了。
父子线程通信时,libuv 是优先使用 eventfd,如果不支持会回退到匿名管道。
如果是匿名管道
fd 是管道的读端,loop->async_wfd 是管道的写端
如果是 eventfd
fd 是读端也是写端。async_wfd 是-1
所以这里判断 loop->async_io_watcher.fd 而不是 async_wfd 的值
*/
if (loop->async_io_watcher.fd != -1)
return 0;
#ifdef __linux__
// 获取一个用于进程间通信的 fd
err = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (err < 0)
return UV__ERR(errno);
// 成功则保存起来,不支持则使用管道通信作为进程间通信
pipefd[0] = err;
pipefd[1] = -1;
#else
err = uv__make_pipe(pipefd, UV_NONBLOCK_PIPE);
if (err < 0)
return err;
#endif
// 初始化 io 观察者 async_io_watcher
uv__io_init(&loop->async_io_watcher, uv__async_io, pipefd[0]);
// 注册 io 观察者到 loop 里,并注册需要监听的事件 POLLIN,读
uv__io_start(loop, &loop->async_io_watcher, POLLIN);
// 用于主线程和子线程通信的 fd,管道的写端,子线程使用
loop->async_wfd = pipefd[1];
return 0;
}
int uv_async_send(uv_async_t* handle) {
/* Do a cheap read first. */
if (ACCESS_ONCE(int, handle->pending) != 0)
return 0;
/*
设置 async handle 的 pending 标记
如果 pending 是 0,则设置为 1,返回 0,如果是 1 则返回 1,
所以同一个 handle 如果多次调用该函数是会被合并的
*/
if (cmpxchgi(&handle->pending, 0, 1) != 0)
return 0;
/* Wake up the other thread's event loop. */
// 真正的唤醒操作是uv__async_send()函数
uv__async_send(handle->loop);
/* Tell the other thread we're done. */
if (cmpxchgi(&handle->pending, 1, 2) != 1)
abort();
return 0;
}
static void uv__async_send(uv_loop_t* loop) {
const void* buf;
ssize_t len;
int fd;
int r;
buf = "";
len = 1;
// 用于异步通信的管道的写端
fd = loop->async_wfd;
#if defined(__linux__)
// 说明用的是 eventfd 而不是管道
if (fd == -1) {
static const uint64_t val = 1;
buf = &val;
len = sizeof(val);
fd = loop->async_io_watcher.fd; /* eventfd */
}
#endif
// 通知读端
do
r = write(fd, buf, len);
while (r == -1 && errno == EINTR);
if (r == len)
return;
if (r == -1)
if (errno == EAGAIN || errno == EWOULDBLOCK)
return;
abort();
}
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));
// 如果数据大于 buf 的长度,接着读,清空这一轮写入的数据
if (r == sizeof(buf))
continue;
// 不等于-1,说明读成功,失败的时候返回-1,errno 是错误码
if (r != -1)
break;
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
// 被信号中断,继续读
if (errno == EINTR)
continue;
// 出错,发送 abort 信号
abort();
}
// 把 async_handles 队列里的所有节点都移到 queue 变量中
QUEUE_MOVE(&loop->async_handles, &queue);
/* 遍历队列判断是谁发的消息,并执行相应的回调函数 */
// 因为 uv_async_init() 函数可能多次被调用,始化多个 async handle,但是 loop->async_io_watcher
// 只有一个,所以要遍历 async_handles,来看看是哪些 async 被触发了
while (!QUEUE_EMPTY(&queue)) {
// 逐个取出节点
q = QUEUE_HEAD(&queue);
// 根据结构体字段获取结构体首地址
h = QUEUE_DATA(q, uv_async_t, queue);
// 从队列中移除该节点
QUEUE_REMOVE(q);
// 重新插入 async_handles 队列,等待下次事件
QUEUE_INSERT_TAIL(&loop->async_handles, q);
/*
判断哪些 async 被触发了。pending 在 uv_async_send
里设置成 1,如果 pending 等于 1,则清 0,返回 1.如果
pending 等于 0,则返回 0
*/
if (0 == uv__async_spin(h))
continue; /* Not pending. */
if (h->async_cb == NULL)
continue;
/* 调用async 的回调函数 */
h->async_cb(h);
}
}