百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

彻底学会使用epoll(二)——ET的读写操作实例分析

liebian365 2024-10-27 13:14 20 浏览 0 评论

相关视频推荐

面试中正经“八股文”网络原理tcp/udp,网络编程epoll/reactor

C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂

彻底学会使用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之前再将其改为非阻塞。

【文章福利】:小编整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!~点击加入(832218493需要自取)

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;
}

相关推荐

4万多吨豪华游轮遇险 竟是因为这个原因……

(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...

“菜鸟黑客”必用兵器之“渗透测试篇二”

"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...

科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白

作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...

麦子陪你做作业(二):KEGG通路数据库的正确打开姿势

作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...

知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势

智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...

每日新闻播报(September 14)_每日新闻播报英文

AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...

香港新巴城巴开放实时到站数据 供科技界研发使用

中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...

5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper

本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...

Qt动画效果展示_qt显示图片

今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...

如何从0到1设计实现一门自己的脚本语言

作者:dong...

三年级语文上册 仿写句子 需要的直接下载打印吧

描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...

C++|那些一看就很简洁、优雅、经典的小代码段

目录0等概率随机洗牌:1大小写转换2字符串复制...

二年级上册语文必考句子仿写,家长打印,孩子照着练

二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...

一年级语文上 句子专项练习(可打印)

...

亲自上阵!C++ 大佬深度“剧透”:C++26 将如何在代码生成上对抗 Rust?

...

取消回复欢迎 发表评论: