相干视频举荐
面试中正经“八股文”网络原理 tcp/udp,网络编程 epoll/reactor
epoll 原理分析 以及 reactor 模型利用
epoll 原理分析以及三握四挥的解决
LinuxC++ 后盾服务器开发架构师收费学习地址
彻底学会应用 epoll(一)——ET 模式实现剖析
接上一篇
首先,看程序四的例子。
l 程序四
#include <unistd.h>
#include <iostream>
#include <sys/epoll.h>
using namespace std;
int main(void)
{
int epfd,nfds;
struct epoll_event ev,events[5];//ev 用于注册事件,数组用于返回要解决的事件
epfd=epoll_create(1);// 只须要监听一个描述符——规范输入
ev.data.fd=STDOUT_FILENO;
ev.events=EPOLLOUT|EPOLLET;// 监听读状态同时设置 ET 模式
epoll_ctl(epfd,EPOLL_CTL_ADD,STDOUT_FILENO,&ev);// 注册 epoll 事件
for(;;)
{nfds=epoll_wait(epfd,events,5,-1);
for(int i=0;i<nfds;i++)
{if(events[i].data.fd==STDOUT_FILENO)
cout<<"hello world!"<<endl;
}
}
};
这个程序的性能是只有规范输入写就绪,就输入“hello world!”。
运行后果:
咱们发现这将是一个死循环。上面具体分析一下这个程序的执行过程:
(1) 首先初始 buffer 为空,buffer 中有空间可写,这时无论是 ET 还是 LT 都会将对应的 epitem 退出 rdlist(对应第一节图中的红线),导致 epoll_wait 就返回写就绪。
(2) 程序想规范输入输入”hello world!”和换行符,因为规范输入为控制台的时候缓冲是“行缓冲”, 所以换行符导致 buffer 中的内容清空,这就对应第二节中 ET 模式下写就绪的第二种状况——当有旧数据被发送走时,即 buffer 中待写的内容变少得时候会触发 fd 状态的扭转。所以下次 epoll_wait 会返回写就绪。之后反复这个过程始终循环上来。
咱们再看程序五。
程序五
绝对程序四这里仅仅去掉了输入的换行操作。即:
cout<<“hello world!”;
运行后果如下:
咱们看到程序成挂起状态。因为第一次 epoll_wait 返回写就绪后,程序向规范输入的 buffer 中写入“hello world!”,然而因为没有输入换行,所以 buffer 中的内容始终存在,下次 epoll_wait 的时候,尽管有写空间然而 ET 模式下不再返回写就绪。回顾第一节对于 ET 的实现,这种状况起因就是第一次 buffer 为空,导致 epitem 退出 rdlist,返回一次就绪后移除此 epitem,之后尽管 buffer 依然可写,然而因为对应 epitem 曾经不再 rdlist 中,就不会对其就绪 fd 的 events 的在检测了。
程序六
int main(void)
{
int epfd,nfds;
struct epoll_event ev,events[5];//ev 用于注册事件,数组用于返回要解决的事件
epfd=epoll_create(1);// 只须要监听一个描述符——规范输入
ev.data.fd=STDOUT_FILENO;
ev.events=EPOLLOUT;// 应用默认 LT 模式
epoll_ctl(epfd,EPOLL_CTL_ADD,STDOUT_FILENO,&ev);// 注册 epoll 事件
for(;;)
{
nfds=epoll_wait(epfd,events,5,-1);
for(int i=0;i<nfds;i++)
{if(events[i].data.fd==STDOUT_FILENO)
cout<<"hello world!";
}
}
};
程序六绝对程序五仅仅是批改 ET 模式为默认的 LT 模式,咱们发现程序再次死循环。这时候起因曾经很分明了,因为当向 buffer 写入”hello world!”后,尽管 buffer 没有输入清空,然而 LT 模式下只有 buffer 有写空间就返回写就绪,所以会始终输入”hello world!”, 当 buffer 满的时候,buffer 会主动刷清输入,同样会造成 epoll_wait 返回写就绪。
程序七
int main(void)
{
int epfd,nfds;
struct epoll_event ev,events[5];//ev 用于注册事件,数组用于返回要解决的事件
epfd=epoll_create(1);// 只须要监听一个描述符——规范输入
ev.data.fd=STDOUT_FILENO;
ev.events=EPOLLOUT|EPOLLET;// 监听读状态同时设置 ET 模式
epoll_ctl(epfd,EPOLL_CTL_ADD,STDOUT_FILENO,&ev);// 注册 epoll 事件
for(;;)
{
nfds=epoll_wait(epfd,events,5,-1);
for(int i=0;i<nfds;i++)
{if(events[i].data.fd==STDOUT_FILENO)
cout<<"hello world!";
ev.data.fd=STDOUT_FILENO;
ev.events=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,STDOUT_FILENO,&ev); // 从新 MOD 事件(ADD 有效)
}
}
};
程序七绝对于程序五在每次向规范输入的 buffer 输入”hello world!”后,从新 MOD OUT 事件。所以相当于每次从新进行第一节中红线形容的路径返回就绪,导致程序循环输入。
ET 模式下的读写
通过后面几节剖析,咱们能够晓得,当 epoll 工作在 ET 模式下时,对于读操作,如果 read 一次没有读尽 buffer 中的数据,那么下次将得不到读就绪的告诉,造成 buffer 中已有的数据无机会读出,除非有新的数据再次达到。对于写操作,次要是因为 ET 模式下 fd 通常为非阻塞造成的一个问题——如何保障将用户要求写的数据写完。
要解决上述两个 ET 模式下的读写问题,咱们必须实现:
a. 对于读,只有 buffer 中还有数据就始终读;
b. 对于写,只有 buffer 还有空间且用户申请写的数据还未写完,就始终写。
要实现上述 a、b 两个成果,咱们有两种办法解决。
办法一
(1) 每次读入操作后(read,recv),用户被动 epoll_mod IN 事件,此时只有该 fd 的缓冲还有数据能够读,则 epoll_wait 会返回读就绪。
(2) 每次输入操作后(write,send),用户被动 epoll_mod OUT 事件,此时只有该该 fd 的缓冲能够发送数据(发送 buffer 不满),则 epoll_wait 就会返回写就绪(有时候采纳该机制告诉 epoll_wai 醒过来)。
这个办法的原理咱们在之前探讨过:当 buffer 中有数据可读(即 buffer 不空)且用户对相应 fd 进行 epoll_mod IN 事件时 ET 模式返回读就绪,当 buffer 中有可写空间(即 buffer 不满)且用户对相应 fd 进行 epoll_mod OUT 事件时返回写就绪。
所以失去如下解决形式:
if(events[i].events&EPOLLIN)// 如果收到数据,那么进行读入
{
cout << "EPOLLIN" << endl;
sockfd = events[i].data.fd;
if ((n = read(sockfd, line, MAXLINE))>0)
{
line[n] = ‘/0’;
cout << "read" << line << endl;
if(n==MAXLINE)
{
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); // 数据还没读完,从新 MOD IN 事件
}
else
{
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //buffer 中的数据曾经读取结束 MOD OUT 事件
}
}
else if (n == 0)
{
close(sockfd);
}
}
else if(events[i].events&EPOLLOUT) // 如果有数据发送
{
sockfd = events[i].data.fd;
write(sockfd, line, n);
ev.data.fd=sockfd; // 设置用于读操作的文件描述符
ev.events=EPOLLIN|EPOLLET; // 设置用于注测的读操作事件
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); // 批改 sockfd 上要解决的事件为 EPOLIN
}
注:对于 write 操作,因为 sockfd 是工作在阻塞模式下的,所以没有必要进行非凡解决,和 LT 应用一样。
剖析:这种办法存在几个问题:
(1) 对于 read 操作后的判断——if(n==MAXLINE),不能阐明这种状况 buffer 就肯定还有没有读完的数据,试想万一 buffer 中一共就有 MAXLINE 字节数据呢?这样持续 MOD IN 就不再失去告诉,而也就没有机会对相应 sockfd MOD OUT。
(2) 那么如果服务端用其余形式可能在适当机会对相应的 sockfd MOD OUT, 是否这种办法就可取呢?咱们首先思考一下为什么要用 ET 模式,因为 ET 模式可能缩小 epoll_wait 等零碎调用,而咱们在这里每次 read 后都要 MOD IN, 之后又要 epoll_wait,势必造成效率升高,这不是事与愿违吗?
综上,此形式不应该应用。
l 办法二
读: 只有可读, 就始终读, 直到返回 0, 或者 errno = EAGAIN
写: 只有可写, 就始终写, 直到数据发送完, 或者 errno = EAGAIN
if (events[i].events & EPOLLIN)
{
n = 0;
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0)
{
n += nread;
}
if (nread == -1 && errno != EAGAIN)
{
perror(“read error”);
}
ev.data.fd = fd;
ev.events = events[i].events | EPOLLOUT;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
if (events[i].events & EPOLLOUT)
{
int nwrite, data_size = strlen(buf);
n = data_size;
while (n > 0)
{
nwrite = write(fd, buf + data_size – n, n);
if (nwrite < n)
{
if (nwrite == -1 && errno != EAGAIN)
{
perror(“write error”);
}
break;
}
n -= nwrite;
}
ev.data.fd=fd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev); // 批改 sockfd 上要解决的事件为 EPOLIN
}
注:应用这种形式肯定要使每个连贯的套接字工作于非阻塞模式,因为读写须要始终读或写直到出错(对于读,当读到的理论字节数小于申请字节数时就能够进行),而如果你的文件描述符如果不是非阻塞的,那这个始终读或始终写势必会在最初一次阻塞。这样就不能在阻塞在 epoll_wait 上了,造成其余文件描述符的工作饿死。
综上:办法一不适宜应用,咱们只能应用办法二,所以也就常说“ET 须要工作在非阻塞模式”, 当然这并不能阐明 ET 不能工作在阻塞模式,而是工作在阻塞模式可能在运行中会呈现一些问题。
l 办法三
仔细分析办法二的写操作,咱们发现这种形式并不很完满, 因为写操作返回 EAGAIN 就终止写,然而返回 EAGAIN 只能说名以后 buffer 已满不可写,并不能保障用户(或服务端)要求写的数据曾经写完。那么如何保障对非阻塞的套接字写够申请的字节数才返回呢(阻塞的套接字直到将申请写的字节数写完才返回)?
咱们须要封装 socket_write()的函数用来解决这种状况, 该函数会尽量将数据写完再返回,返回 - 1 示意出错。在 socket_write()外部, 当写缓冲已满 (send() 返回 -1, 且 errno 为 EAGAIN), 那么会期待后再重试.
ssize_t socket_write(int sockfd, const char* buffer, size_t buflen)
{
ssize_t tmp;
size_t total = buflen;
const char* p = buffer;
while(1)
{
tmp = write(sockfd, p, total);
if(tmp < 0)
{
// 当 send 收到信号时, 能够持续写, 但这里返回 -1.
if(errno == EINTR)
return -1;
// 当 socket 是非阻塞时, 如返回此谬误, 示意写缓冲队列已满,
// 在这里做延时后再重试.
if(errno == EAGAIN)
{usleep(1000);
continue;
}
return -1;
}
if((size_t)tmp == total)
return buflen;
total -= tmp;
p += tmp;
}
return tmp;// 返回已写字节数
}
剖析:这种形式也存在问题,因为在实践上可能会长工夫的阻塞在 socket_write()外部(buffer 中的数据得不到发送,始终返回 EAGAIN), 但暂没有更好的方法。
不过看到这种形式时,我在想在 socket_write 中将 sockfd 改为阻塞模式应该一样可行,等再次 epoll_wait 之前再将其改为非阻塞。
【文章福利】:小编整顿了一些集体感觉比拟好的学习书籍、视频材料共享在群文件外面,有须要的能够自行添加哦!~ 点击退出(须要自取)
5.2 ET 模式下的 accept
思考这种状况:多个连贯同时达到,服务器的 TCP 就绪队列霎时积攒多个就绪
连贯,因为是边缘触发模式,epoll 只会告诉一次,accept 只解决一个连贯,导致 TCP 就绪队列中剩下的连贯都得不到解决。
解决办法是用 while 循环抱住 accept 调用,解决完 TCP 就绪队列中的所有连贯后再退出循环。如何晓得是否解决完就绪队列中的所有连贯呢?accept 返回 -1 并且 errno 设置为 EAGAIN 就示意所有连贯都解决完。
的正确应用形式为:
while ((conn_sock = accept(listenfd,(struct sockaddr ) &remote, (size_t )&addrlen)) > 0) {
handle_client(conn_sock);
}
if (conn_sock == -1) {
if (errno != EAGAIN && errno != ECONNABORTED
&& errno != EPROTO && errno != EINTR)
perror("accept");
}
起因:如果 accept 工作在阻塞模式,思考这种状况:TCP 连贯被客户端夭折,即在服务器调用 accept 之前(此时 select 等曾经返回连贯达到读就绪),客户端被动发送 RST 终止连贯,导致刚刚建设的连贯从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会始终阻塞在 accept 调用上,直到其余某个客户建设一个新的连贯为止。然而在此期间,服务器单纯地阻塞在 accept 调用上(理论应该阻塞在 select 上),就绪队列中的其余描述符都得不到解决。
解决办法是把监听套接口设置为非阻塞,当客户在服务器调用 accept 之前停止
某个连贯时,accept 调用能够立刻返回 -1,这时源自 Berkeley 的实现会在内核中解决该事件,并不会将该事件告诉给 epoll,而其余实现把 errno 设置为 ECONNABORTED 或者 EPROTO 谬误,咱们应该疏忽这两个谬误。(具体可参看 UNP v1 p363)
6.1 ET 模式为什么要设置在非阻塞模式下工作
因为 ET 模式下的读写须要始终读或写直到出错(对于读,当读到的理论字节数小于申请字节数时就能够进行),而如果你的文件描述符如果不是非阻塞的,那这个始终读或始终写势必会在最初一次阻塞。这样就不能在阻塞在 epoll_wait 上了,造成其余文件描述符的工作饿死。
6.2 应用 ET 和 LT 的区别
LT:程度触发,效率会低于 ET 触发,尤其在大并发,大流量的状况下。然而 LT 对代码编写要求比拟低,不容易呈现问题。LT 模式服务编写上的体现是:只有有数据没有被获取,内核就一直告诉你,因而不必放心事件失落的状况。
ET:边缘触发,效率十分高,在并发,大流量的状况下,会比 LT 少很多 epoll 的零碎调用,因而效率高。然而对编程要求高,须要粗疏的解决每个申请,否则容易产生失落事件的状况。
上面举一个列子来阐明 LT 和 ET 的区别(都是非阻塞模式,阻塞就不说了,效率太低):
采纳 LT 模式下,如果 accept 调用有返回就能够马上建设以后这个连贯了,再 epoll_wait 期待下次告诉,和 select 一样。
然而对于 ET 而言,如果 accpet 调用有返回,除了建设以后这个连贯外,不能马上就 epoll_wait 还须要持续循环 accpet,直到返回 -1,且 errno==EAGAIN,
从实质上讲:与 LT 相比,ET 模型是通过缩小零碎调用来达到进步并行效率的。
6.3 一道腾讯后盾开发的面试题
应用 Linux epoll 模型,程度(LT)触发模式,当 socket 可写时,会不停的触发 socket 可写的事件,如何解决?
第一种最广泛的形式:
须要向 socket 写数据的时候才把 socket 退出 epoll,期待可写事件。承受到可写事件后,调用 write 或者 send 发送数据。当所有数据都写完后,把 socket 移出 epoll。
这种形式的毛病是,即便发送很少的数据,也要把 socket 退出 epoll,写完后在移出 epoll,有肯定操作代价。
一种改良的形式:
开始不把 socket 退出 epoll,须要向 socket 写数据的时候,间接调用 write 或者 send 发送数据。如果返回 EAGAIN,把 socket 退出 epoll,在 epoll 的驱动下写数据,全副数据发送结束后,再移出 epoll。
这种形式的长处是:数据不多的时候能够防止 epoll 的事件处理,提高效率。
6.4 什么状况下用 ET
很简略,当你想进步程序效率的时候。
最初附一个 epoll 实例:
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/epoll.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <errno.h>
#define MAX_EVENTS 10
#define PORT 8080
// 设置 socket 连贯为非阻塞模式
void setnonblocking(int sockfd) {
int opts;
opts = fcntl(sockfd, F_GETFL);
if(opts < 0) {perror("fcntl(F_GETFL)\n");
exit(1);
}
opts = (opts | O_NONBLOCK);
if(fcntl(sockfd, F_SETFL, opts) < 0) {perror("fcntl(F_SETFL)\n");
exit(1);
}
}
int main(){struct epoll_event ev, events[MAX_EVENTS]; //ev 负责增加事件,events 接管返回事件
int addrlen, listenfd, conn_sock, nfds, epfd, fd, i, nread, n;
struct sockaddr_in local, remote;
char buf[BUFSIZ];
// 创立 listen socket
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("sockfd\n");
exit(1);
}
setnonblocking(listenfd);//listenfd 设置为非阻塞[1]
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);;
local.sin_port = htons(PORT);
if(bind(listenfd, (struct sockaddr *) &local, sizeof(local)) < 0) {perror("bind\n");
exit(1);
}
listen(listenfd, 20);
epfd = epoll_create(MAX_EVENTS);
if (epfd == -1) {perror("epoll_create");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listenfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) == -1) {// 监听 listenfd
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;) {nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nfds == -1) {perror("epoll_pwait");
exit(EXIT_FAILURE);
}
for (i = 0; i < nfds; ++i) {fd = events[i].data.fd;
if (fd == listenfd) {while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote,
(size_t *)&addrlen)) > 0) {setnonblocking(conn_sock);// 上面设置 ET 模式,所以要设置非阻塞
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) {// 读监听
perror("epoll_ctl: add"); // 连贯套接字
exit(EXIT_FAILURE);
}
}
if (conn_sock == -1) {
if (errno != EAGAIN && errno != ECONNABORTED
&& errno != EPROTO && errno != EINTR)
perror("accept");
}
continue;
}
if (events[i].events & EPOLLIN) {
n = 0;
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) {//ET 下能够读就始终读
n += nread;
}
if (nread == -1 && errno != EAGAIN) {perror("read error");
}
ev.data.fd = fd;
ev.events = events[i].events | EPOLLOUT; //MOD OUT
if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1) {perror("epoll_ctl: mod");
}
}
if (events[i].events & EPOLLOUT) {sprintf(buf, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\nHello World", 11);
int nwrite, data_size = strlen(buf);
n = data_size;
while (n > 0) {nwrite = write(fd, buf + data_size - n, n);//ET 下始终将要写数据写完
if (nwrite < n) {if (nwrite == -1 && errno != EAGAIN) {perror("write error");
}
break;
}
n -= nwrite;
}
close(fd);
}
}
}
return 0;
}