接上一节协程源码剖析(一)
接下来讨论协程对系统调用的覆盖:hook
本文列举部分源码,协程全部源码https://github.com/wuj1nquan/bitco 对每一个函数都有详细注释
协程为了逻辑清晰,便于维护,使用同步编程方式
因此使用posix提供的系统调用recv、send等都会在io未就绪时阻塞
所以协程对这些api进行了异步改造
先来看一个简单的例子:
hook系统调用
ssize_t read(int fd, void *buf, size_t count) { // 上下文切换实现非阻塞read struct pollfd fds; fds.fd = fd; fds.events = POLLIN | POLLERR | POLLHUP; poll_inner(&fds, 1, 1); // 将当前fd交由epoll管理并让出cpu,避免read_f阻塞,等待fd就绪后由调度器返回到这里继续执行read_f int ret = read_f(fd, buf, count);// 这里是真正的系统调用read if (ret < 0) { if (errno == ECONNRESET) return -1; } return ret; }
这里对系统调用进行hook后,重新定义了一个read函数,借助poll_inner函数实现了异步read
poll_inner函数
static int poll_inner(struct pollfd *fds, nfds_t nfds, int timeout) { if (timeout == 0) { return poll(fds, nfds, timeout); } if (timeout < 0) { timeout = INT_MAX; } schedule *sched = coroutine_get_sched(); // 获取当前正在执行的协程,也就是调用poll_inner所在函数(例如read)所在的协程 if (sched == NULL) { printf("scheduler not exit!\n"); return -1; } coroutine *co = sched->curr_thread; // 获取协程对象 int i = 0; for (i = 0;i < nfds;i ++) { // 将poll监视的文件描述符和相应的事件转换为 epoll 事件 struct epoll_event ev; ev.events = pollevent_2epoll(fds[i].events); ev.data.fd = fds[i].fd; epoll_ctl(sched->poller_fd, EPOLL_CTL_ADD, fds[i].fd, &ev); co->events = fds[i].events; schedule_sched_wait(co, fds[i].fd, fds[i].events, timeout); // 让当前协程等待事件 } coroutine_yield(co); // 当前协程将控制权交给调度器,等待事件发生 for (i = 0;i < nfds;i ++) { // 处理事件完成后从 epoll 实例中删除已经监视的文件描述符,并清除相应的等待状态 struct epoll_event ev; ev.events = pollevent_2epoll(fds[i].events); ev.data.fd = fds[i].fd; epoll_ctl(sched->poller_fd, EPOLL_CTL_DEL, fds[i].fd, &ev); schedule_desched_wait(fds[i].fd); // 处理完成后将该协程从调度器的等待红黑树中删除 } return nfds; // 返回监视文件描述符数量 }
poll_inner函数将当前进行read操作的fd加入epoll管理,随后,其所在协程让出控制权给调度器,下次恢复时,fd已经就绪,实现了异步read
如此覆盖系统调用,这就是各协程之间通过系统调用借助调度器切换的底层原理
由于篇幅,只能列举部分源码进行说明,协程全部源码
协程源码 对每一个函数都有详细注释,学习首选
下一节继续讲解协程原语操作resume、yield