【网络编程】二、poll, epoll, selector 的区别
参考:https://www.cnblogs.com/aspirant/p/9166944.html
一、网络接收数据的示意图
我们先从只监听一个socket开始讲起:
首先我们有一个程序A,他运行这下面这样一段代码:
//创建socket int s = socket(AF_INET, SOCK_STREAM, 0); //绑定端口 bind(s, ...) //监听 listen(s, ...) //接受客户端连接 int c = accept(s, ...) //接收客户端数据,没有数据就先阻塞在这里 recv(c, ...); //将数据打印出来 printf(...)
当程序A运行到recv()的时候它阻塞了,
之后就挂起在等待队列中,等待被唤醒之后继续执行,
而在Linux中,万事万物皆为文件,我们的socket也占用了一个fd。
我们的A就挂在socket中的等待队列中。
接下来我们来看一个简单的流程:
- 首先第一步是通过网线发送数据到网卡
- 网卡将数据存到内存中
- 网卡对cpu发出中断信号,提醒cpu过来处理网卡的任务
- cpu接到信号后暂时中断自己的任务,过来运行网卡准备的中断程序
- 中断程序的内容是:将网卡写到内存中的网络数据写入socket的输入缓冲区
- 然后中断程序再唤醒处于阻塞状态的A唤醒,并挂到工作队列中让cpu运行它,而cpu就会运行刚刚代码的最后一段:打印
//将数据打印出来 printf(...)
二、epoll的三个函数
2.1、epoll_create
命令:创建1个多路复用器
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
注意:size参数只是告诉内核这个 epoll对象会处理的事件大致数目,而不是能够处理的事件的最大个数。在 Linux最新的一些内核版本的实现中,这个 size参数没有任何意义。
2.2、epoll_ctl
epoll_ctl命令:向多路复用器,添加各种文件描述符(套接字socket)的网络事件监听(serverSocketChannel的连接就绪,socketChannel的链接完成就绪,读就绪,写就绪事件)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); epoll的事件注册函数,epoll_ctl向 epoll对象中添加、修改或者删除感兴趣的事件,返回0表示成功,否则返回–1,此时需要根据errno错误码判断错误类型。 它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。 epoll_wait方法返回的事件必然是通过 epoll_ctl添加到 epoll中的。 第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示: EPOLL_CTL_ADD:注册新的fd到epfd中; EPOLL_CTL_MOD:修改已经注册的fd的监听事件; EPOLL_CTL_DEL:从epfd中删除一个fd; 第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事件 events可以是以下几个宏的集合: EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLOUT:表示对应的文件描述符可以写; EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); EPOLLERR:表示对应的文件描述符发生错误; EPOLLHUP:表示对应的文件描述符被挂断; EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。 EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
2.3、epoll_wait
epoll_wait命令:当有网络事件就绪,操作系统中断程序会中断进程,让多路服务器返回网络就绪的文件描述符(套接字socket)
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。如果返回–1,则表示出现错误,需要检查 errno错误码判断错误类型。
第1个参数 epfd是 epoll的描述符。
第2个参数 events则是分配好的 epoll_event结构体数组,epoll将会把发生的事件复制到 events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)。
第3个参数 maxevents表示本次可以返回的最大事件数目,通常 maxevents参数与预分配的events数组的大小是相等的。
第4个参数 timeout表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果 timeout为0,则表示 epoll_wait在 rdllist链表中为空,立刻返回,不会等待。
2.4、关于Epoll的两种工作模式:ET、LT
epoll有两种工作模式:LT(水平触发)模式和ET(边缘触发)模式。
默认情况下,epoll采用 LT模式工作,这时可以处理阻塞和非阻塞套接字,而上表中的 EPOLLET表示可以将一个事件改为 ET模式。ET模式的效率要比 LT模式高,它只支持非阻塞套接字。
ET模式与LT模式的区别在于:
当一个新的事件到来时,ET模式下当然可以从 epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字没有新的事件再次到来时,在 ET模式下是无法再次从 epoll_wait调用中获取这个事件的;而 LT模式则相反,只要一个事件对应的套接字缓冲区还有数据,就总能从 epoll_wait中获取这个事件。因此,在 LT模式下开发基于 epoll的应用要简单一些,不太容易出错,而在 ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。默认情况下,Nginx是通过 ET模式使用 epoll的。
结论:
ET模式仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用ET模式,需要一直read/write直到出错为止,很多人反映为什么采用ET模式只接收了一部分数据就再也得不到通知了,大多因为这样;而LT模式是只要有数据没有处理就会一直通知下去的.
三、select,poll , epoll的区别
I/O多路复用主要是指liunx操作系统的命令函数,上述的实现方式也是Liunx的不同的实现版本。
四、Epoll的工作原理和流程图
4.1、创建 Epoll 对象
如下图所示,当某个进程调用 epoll_create 方法时,内核会创建一个 eventpoll 对象(也就是程序中 Epfd 所代表的对象)。
内核创建 eventpoll 对象
eventpoll 对象也是文件系统中的一员,和 Socket 一样,它也会有等待队列。
创建一个代表该 Epoll 的 eventpoll 对象是必须的,因为内核要维护“就绪列表”等数据,“就绪列表”可以作为 eventpoll 的成员。
4.2、维护监视列表
创建 Epoll 对象后,可以用 epoll_ctl 添加或删除所要监听的 Socket。以添加 Socket 为例。
添加所要监听的 Socket
如上图,如果通过 epoll_ctl 添加 Sock1、Sock2 和 Sock3 的监视,内核会将 eventpoll 添加到这三个 Socket 的等待队列中。
当 Socket 收到数据后,中断程序会操作 eventpoll 对象,而不是直接操作进程。
4.3、接收数据
当 Socket 收到数据后,中断程序会给 eventpoll 的“就绪列表”添加 Socket 引用。
给就绪列表添加引用
如上图展示的是 Sock2 和 Sock3 收到数据后,中断程序让 Rdlist 引用这两个 Socket。
eventpoll 对象相当于 Socket 和进程之间的中介,Socket 的数据接收并不直接影响进程,而是通过改变 eventpoll 的就绪列表来改变进程状态。
当程序执行到 epoll_wait 时,如果 Rdlist 已经引用了 Socket,那么 epoll_wait 直接返回,如果 Rdlist 为空,阻塞进程。
4.4、阻塞和唤醒进程
假设计算机中正在运行进程 A 和进程 B,在某时刻进程 A 运行到了 epoll_wait 语句。
epoll_wait 阻塞进程
如上图所示,内核会将进程 A 放入 eventpoll 的等待队列中,阻塞进程。
当 Socket 接收到数据,中断程序一方面修改 Rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入工作队列。也因为 Rdlist 的存在,进程 A 可以知道哪些 Socket 发生了变化。