epoll的网络模型,从redis、memcached到nginx,一起搞定
linux服务器性能优化之异步的原理与实现
手写一个用户态网络协议栈,瞬间提升你网络功底
强迫症不能忍受这种极其绕的概念而不给个说法。同步(synchronous)/异步(asynchronous),阻塞(blocking)/非阻塞(non-blocking),阻塞IO/非阻塞IO/同步IO/异步IO/IO复用(IO Multiplexing),select/poll/epoll这些概念困扰我许久,下面给出这一阶段我个人的理解。
线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。用线程执行程序流的过程去理解同步异步,阻塞非阻塞。同步异步关注的是流执行过程需不需要等待外部调用的结果,而阻塞非阻塞关注的是外部调用对流本身产生的影响。
线程的执行过程中,产生一个外部调用,如果需要等待该调用返回才能继续线程流则叫做同步,不需要等待结果返回线程流可以继续往下执行的情况叫做异步。
区分:线程流向下执行需不需要等待系统调用的结果。
线程执行过程中,产生一个外部调用后,会不会把该线程流“堵”住,会“堵”对应的是阻塞,反之为非阻塞。
上一节中对同步/异步,阻塞/非阻塞的描述只能说能够恰好区分它们,如果不是在计算机领域而是生活中,道理也类似。然而计算机中的某些专业术语又需要放在专门的情景中去看,例如下面将要提到的Linux IO模型,建议理解模型本身,而不是抠同步/异步与阻塞非阻塞的字眼,因为会发现就算是非阻塞模型也有阻塞的部分,同步IO与异步IO的区别是IO操作的时候会不会让process阻塞。
下面对模型描述的最终来源为:
Richard Stevens的“UNIX®️ Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2节“I/O Models ”
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
用IO操作中有阻塞来判断,5种IO模型中4种属于同步IO,分别是阻塞IO模型,非阻塞IO模型,IO复用模型,信号驱动IO模型。
阻塞IO是socket的默认设置,其模型如下图所示,程序调用recvfrom产生一个系统调用,kernel收到该调用请求后有两个步骤,第一是等待数据准备好,第二是将数据从内核空间拷贝到用户空间然后返回OK,用户空间收到系统调用返回后才会继续程序流的执行。
socket使用非阻塞IO模型需要对socket进行另行设置,非阻塞IO模型如下所示。内核收到系统调用后,若数据未准备好立即返回error,用户进程收到error会继续产生系统调用,直到数据准备好了并被拷贝到用户空间。
【文章福利】需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)
select/poll/epoll对应的是IO复用模型,优势是能够监听多个socket,模型图如下所示。用户进程调用select产生系统调用,kernel会监听所有select负责的socket,一旦有一个socket数据准备好了,kernel即返回,用户再去recvfrom产生系统调用将数据从内核空间读到用户空间。
用户程序注册一个信号handler,然后继续做后续的事情,当内核数据准备好了会发送一个信号,程序调用recvfrom进行系统调用将数据从内核空间拷贝到用户空间。
用IO操作中无阻塞来判断,5种IO模型中只有异步IO。
异步IO模型如下,aio_read产生系统调用,kernel在数据准备好后将数据从内核空间拷贝到用户空间后返回一个信号告知read数据成功,整个过程程序调用aio_read后就继续执行其他部分直到收到信号,调用handler处理。
Kernel有两个过程,等待数据准备好和拷贝数据到用户空间,用户程序的阻塞一般有两种情况,select的阻塞和socket IO的阻塞,5中IO模型的对比如下。
Select/poll/epoll能够同时监听多个文件描述符fd,当有fd的读写操作完成时会返回这些fd,可以对应于IO复用模型中的系统调用查询fd是否准备好数据的那一部分。
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
typedef struct {
unsigned long *in, *out, *ex;
unsigned long *res_in, *res_out, *res_ex;
} fd_set_bits;
select在用户层使用某个结构标识被监听的fd以及监听的状态,每一个fd用1bit表示,为1表示这个文件是被监听的,0表示不监听。in, out, ex指向的bit数组表示对应的读,写,异常文件的描述符。res_in, res_out,res_ex指向的bit数组表示对应的读,写,异常文件的描述符的检测结果。
1、这个结构被拷贝到内核层,
2、对所有的fd注册回调函数__pollwait
3、调用fd的poll方法遍历整个FD_SESIZET所有的fd,检查是不是自己需要监听的,如果监听的fd发生了感兴趣的事(文件读写操作完成或者异常,参考用户态预先的设置),则poll方法返回一个描述读写操作是否就绪的mask掩码,根据mask掩码给fd_set赋值。poll->poll_wait->__pollwait会把当前进程挂到对应文件的inode中的fd的等待队列。
4、如果一轮遍历无果则挂起,直到超时或者有设备驱动发生自身资源可读写后将其从等待队列唤醒,则执行新一轮的遍历。
5、把fd_set从内核空间拷贝到用户空间并将进程从各个等待队列中删除。
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd;
short events;
short revents;
};
poll的实现机制与select类似,不一样的是poll的使用中用户态直接提供了需要监听的fd的信息,pollfd结构记录被监听的fd和它的状态。
struct poll_list {
struct poll_list *next;
int len;
struct pollfd entries[0];
};
另外poll使用poll_list结构来记录监听的fd,每一个poll_list节点都包含一个pollfd数组,参数被拷贝到内核后,poll_list被遍历,换言之pollfd数组被遍历,与select一样,所有的fd都被遍历。
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll将过程细化为一组系统调用,包括1个epoll_create,多个epoll_ctrl,1个epoll_wait。内核针对epoll操作添加了一个文件系统”eventpollfs”,每一个或者多个要监视的fd都有一个对应的eventpollfs文件系统的inode节点,主要信息保存在eventpoll结构体中,而被监视的文件的重要信息则保存在epitem结构体中。在执行epoll_create和epoll_ctrl时把用户态的信息保存到内核态了,之后即使反复地调用epoll_wait,也不会像select/poll那样重复地拷贝参数,扫描fd,反复地把当前进程放入/放出等待队列。
epoll_create()的功能是创建一个eventpollfs文件系统的inode节点,主要信息保存在eventpoll结构体中,eventpoll记录了eventpollfs文件系统的inde节点的重要信息,其中成员rbr保存该epoll文件节点监视的所有描述符,组织的方式是一棵红黑树,被监视的文件的重要信息则保存在epitem结构体中。
Epoll_ctl实现一系列操作,它调用ep_find()从eventpoll中的红黑树获得epitem结构体,根据op参数的不同而选择不同的操作。当op为EPOLL_CTL_ADD,一般情况下epitem无法在eventpoll的红黑树中找到,所以调用ep_insert创建一个epitem结构体并插入到对应的红黑树中,
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
…
revents = tfile->f_op->poll(tfile, &epq.pt);
…
然后ep_insert调用init_poll_funcptr注册一个回调函数ep_ptable_queue_proc,该函数会在调用f_op->poll时被执行。Ep_ptable_queue_proc函数分配一个epoll等待队列结点epoll_entry,一方面把它挂到文件操作的等待队列中,另一方面把它挂到epitem的队列中。此外它还注册了一个等待队列的回调函数ep_poll_callback,ep_poll_callback在完成操作完成,唤醒当前等待进程之前被调用,会把epitem放到eventpoll的完成队列,然后唤醒等待进程。
epoll_wait的工作是等待文件操作完成并返回。它的主体是ep_poll(),该函数检查eventpoll中有没有已经完成的事件,有的话就把结果返回。没有的话调用schedule_timeout()进入休眠,直到进程被再度唤醒或者超时。
select/poll的弱点在于需要轮询遍历fd,当监听fd多时开销大;而epoll依赖于回调函数,当活跃fd过多时开销就大。