相干视频举荐
面试中正经“八股文”网络原理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;
}