网络 I/O 运行原理
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
或者管道。这一部分的实现可以看这里。 - 其它线程可以通过往这个文件描述符上 write,来激活这个文件描述符上的读事件。这一部分的实现可以看这里
- 当主进程接收到这个事件后,调用回调函数。这一部分的实现可以看这里
- 其他进程/线程和主进程的通信是使用 uv_async_t 结构体实现的。Libuv 使用 loop->async_handles 记 录 所有的 uv_async_t 结 构 体 , 使 用 loop->async_io_watcher 作为所有 uv_async_t 结构体的 io 观察者。即 loop-> async_handles 队列上所有的 handle 都是共享 async_io_watcher 这个 io 观察者。第一次插入一个 uv_async_t 结构体到 async_handle 队列时, 会 初 始 化 io 观 察 者 。 如 果 再 次 注 册 一 个 async_handle , 只 会 在 loop->async_handle 队列和 handle 队列插入一个节点,而不是新增一个 io 观察者。
uv_async_t
--- 异步句柄
数据类型
-
void
(*uv_async_cb)
(uv_async_t* handle)传递给
uv_async_init()
的回调函数的类型定义。
API
-
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()
,回调函数将会再次被调用。
example
- 例:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <uv.h> 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 */
源码分析
uv_async_t 结构体
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;
}
uv_async_init
- 初始化 async handle 的一些字段。然后把 handle 插入 async_handle 队列中
- 源码
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; }
uv_async_start
- 首先来介绍一下 eventfd
eventfd
- eventfd是linux 2.6.22后系统提供的一个轻量级的进程间通信的系统调用[是一个用来通知事件的文件描述符],eventfd通过一个进程间共享的64位计数器完成进程间通信,这个计数器由在linux内核空间维护,用户可以通过调用write方法向内核空间写入一个64位的值,也可以调用read方法读取这个值
- 新建
#include <sys/eventfd.h> 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 <unistd.h> int close(int fd);
- 例:
#include <sys/eventfd.h> #include <unistd.h> #include <iostream> 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
- 关闭事件
uv_async_start
- uv__async_start 只会执行一次。主要逻辑是:
申请用于通信的文件描述符,然后把读端和回调封装到 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; }
uv__async_send ———— 触发事件的方法
- 源码
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(); }
uv_async_io ———— 事件触发后执行的回调
-
uv__async_io 会遍历 loop->async_handles 队里中所有的 uv_async_t。然后判断该 uv_async_t 是否有事件触发(通过 uv_async_t->pending 字段)。如果有的话,则执 行该 uv_async_t 对应的回调
-
源码
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); } }