IO多路转接二

一、poll函数:IO多路复用的方式之一

1、接口:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

第一个参数为一个结构体指针,也可以理解为一个结构体数组,fds表示结构体的起始位置;第二个参数为描述结构体的元素个数;第三个参数为一个超时时间。

结构体里面的内容:

struct pollfd {   
int   fd;         /* file descriptor */   关注哪个文件描述符
short events;     /* requested events */   位图;关注文件描述符的哪种状态(读就绪,写就绪……)【输入参数】
short revents;    /* returned events */    位图;对应的文件描述符是读就绪还是写就绪……写到这个位置上【输出参数】
};

2、基于poll实现一个监控标准输入就绪的代码:将输入的内容打印出来,等待的过程交给poll来等待,read直接进行读写。

# include<stdio.h>
# include<poll.h>
# include<unistd.h>

int main()
{
//如果使用poll一次监控多个文件描述符,将下述fds改为一个数组的形式,下述为只监控一个文件描述符
   struct pollfd fds;
   fds.fd=0;//监控标准输入0号文件描述符
   fds.events=POLLIN;//关注当前的文件描述符是哪种就绪的状态
   while(1){
     int ret=poll(&fds,1,0);//1:表示超时天数;0:表示为阻塞状态
     if(ret<0){
        perror("poll");
        break;
     }
     //如果返回了,说明0号文件描述符就绪了
     char buf[1024]={0};
     ssize_t read_size=read(0,buf,sizeof(buf)-1);
     if(read_size<0){
        perror("read");
        return 1;
     }
     if(read_size==0){
        printf("read done!\n");
        return 0;
     }
     buf[read_size]='\0';
     printf("%s\n",buf);
    }

   return 0;
}

 

 

poll与select的对比:

a、select每次使用前都得重新设置监控描述符,poll也得设置

b、poll每次调用都要将内容拷贝到内核之中,拷贝的频繁程度相同,拷贝的数据量更大啦!poll一次拷贝一个结构体,开销反而更大了。

c、select每次调用时都得遍历文件描述符表,了解哪个文件描述符就绪……poll也得遍历,但是遍历的不是位图而是数组

d、poll唯一解决了select中的上限问题,select文件描述符上限是有限制的,对于poll,只要数组足够大,就可以放更多的文件描述符。poll把输入与输出用两个字段分开,使用时,更方便。

二、epoll(windows上没有epoll,Linux上才有)【相当于一个改进版本的poll】【高效的处理大量的文件】

1、epoll的系统调用(epoll有三个函数)

(1)创建一个epoll的句柄【文件描述符】:.

int epoll_create(int size);

参数中的size没有任何意义,可以传任何值,主要是为了保证向前兼容

(2)epoll的事件注册函数:把文件描述符添加到epoll中,或者把某个文件描述符从epoll中删除

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

第一个参数epfd:对哪个epoll对象进行操作

第二个参数int op存在三个取值

EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件; 指定关注读就绪、写就绪……
EPOLL_CTL_DEL :从epfd中删除一个fd; 

第三个参数int fd

对哪个文件描述符进行修改

第四个参数struct epoll_event *event    结构体指针

struct epoll_event
{
    uint32_t events;//当前的文件描述符关注哪一个事件(读就绪、写就绪还是异常就绪用按位或的方式放到events里面)
    epoll_data_t data;//用户自定义的数据,根据需要放任何你想要的数据
}_EPOLL_PACKED;

epoll_data_t中://根据用户的需要决定放指针;文件描述符;uint32还是uint64

typedef union epoll_data
{
   void *ptr;
   int fd;
   uint32_t u32;
   uint32_t u64;
}epoll_data_t;

events可以是以下几个宏的集合: 

EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭); 

EPOLLOUT : 表示对应的文件描述符可以写;

 EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来); 

EPOLLERR : 表示对应的文件描述符发生错误; 

EPOLLHUP : 表示对应的文件描述符被挂断;

 EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered) 来说的. EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的 话, 需要再次把这个socket加入到EPOLL队列里.

(3)epoll_wait等待(注册的动作和等待的动作分开,不同于poll和select):会一直等待直到某些文件描述符关闭

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

第一个参数:对哪个epoll对象进行等待

第二个参数:epoll_event这样的结构体指针,可以理解为一个数组,表示数组的首地址

第三个 参数:数组的元素个数

第四个参数:超时的时间,若为0则为非阻塞,立即返回;若为-1,则会永久阻塞

2、epoll在内核中的表现形式(工作原理):创建一个epoll之后,每次调用一个Add就是把epoll加到集合之中。这个集合是一颗红黑树。通过一棵红黑树描述文件描述符。创建epoll相当于创建一个结构体,该结构体(a、红黑树:管理所有文件描述符的集合,包括每个文件描述符包含的事件的状态   b、就绪队列:一旦有某些文件描述符就绪,就会通过一个回调函数,把我们对应的节点的文件描述符放到就绪队列里。)就绪队列用来放置所有已经就绪的文件描述符,有了就绪队列后,上层代码里调用epoll_wait直接返回的结果,相当于直接把就绪队列取出来、返回。通过就绪队列可以快速知道哪些文件描述符是就绪的。回调函数是指先把函数注册到系统中,在适当的时机,由系统自动调用,这个函数什么时候调用由系统或框架自动调用,不是程序员干涉得了的。epoll函数内部存在内置的回调函数机制,只要发现某个红黑树的文件描述符就绪,就会自动触发回调函数,进而把文件描述符,以及相关信息放到就绪队列。如果没有回调函数,无法得知谁就绪,就得遍历,遍历麻烦。文件描述符就绪,先把就绪信息给网卡,网卡驱动告诉内核数据进入,内核根据数据调用对应节点的回调函数,从而把内容放到红黑树里面。若是没有回调函数,反复遍历这颗树,由于树比较大,整体的效率就会比较低。

实现epoll版本的TCP服务器:1、调用epoll_create创建一个epoll句柄; 2、调用epoll_ctl, 将要监控的文件描述符进行注册; 3、调用epoll_wait, 等待文件描述符就绪;

# include<stdio.h>
# include<string.h>
# include<stdlib.h>
# include<unistd.h>
# include<sys/socket.h>
# include<sys/epoll.h>
# include<netinet/in.h>
# include<arpa/inet.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
typedef struct epoll_event epoll_event;

//初始化服务器
int serverInit(char *ip,short port)
{
   int fd=socket(AF_INET,SOCK_STREAM,0);
   if(fd<0){
     perror("socket");
     return -1;
   }
   sockaddr_in addr;
   addr.sin_family=AF_INET;
   addr.sin_addr.s_addr=inet_addr(ip);
   addr.sin_port=htons(port);
   int ret=bind(fd,(sockaddr*)&addr,sizeof(addr));
   if(ret<0){
     perror("bind");
     return -1;
   }
   ret=listen(fd,5);
   if(ret<0){
     perror("listen");
     return -1;
   }
   return fd;
}
void ProcessListensock(int epoll_fd,int listen_sock)
{
   //1、调用accept获取到new_sock
   sockaddr_in peer;
   socklen_t len=sizeof(peer);
   int new_sock=accept(listen_sock,(sockaddr*)&peer,&len);
   if(new_sock<0){
      perro("accept");
      return ;
   }
   //2、把new_sock加入到epoll之中
   epoll_event event;
   event.events=EPOLLIN;
   event.data.fd=new_sock;//把epoll字段填充成文件描述符
   //调用epoll_ctl添加
   int ret=epoll_ctl(epoll_fd,EPOLL_CTL_ADD,new_sock,&event);
   if(ret<0){
      perror("epoll_ctl");
      return ;
    }
   printf("[client %d] connect\n",new_sock);
   return;
}

ProcessNewsock(int epoll_fd,int new_sock)
{
//1、直接从new_sock中读取数据
     char buf[1024]={0};
     ssize_t read_size=read(new_sock,buf,sizeof(buf)-1);
     if(read_size<0){
        perror("read");
        return ;
     }
     if(read_size==0){
        printf("read done!");
//关闭对应的new_sock
     close(new_sock);
//把这个文件描述符从epoll删除
     epoll_ctl(epoll_fd,EPOLL_CTL_DEL,new_sock,NULL);
     printf("[client %d]disconnect\n",new_sock);
        return;
     }
     buf[read_size]='\0';
//2、对数据进行处理,并且写回到new_sock
     printf("[client %d] say %s\n",new_sock,buf);
     write(new_sock,buf,strlen(buf));
     return;
}
//使用epoll实现一个TCP版本的多连接echo_server
int main(int argc,char *argv[])
{
   if(argc!=3){
     printf("Usage./server [ip] [port]\n");
     return 1;
   }
//1、初始化服务器,创造listen_sock
   int listen_sock=serverInit(argv[1],atoi(argv[2]));
   if(listen_sock<0){
     printf("serverInit failed\n");
     return 1;
   }
   printf("serverInit OK\n");
//2、创建epoll对象
   int epoll_fd=epoll_create(10);
   if(epoll_fd<0){
     perror("epoll_create failed\n");
     return 1;
   }
//3、把listen_sock添加到epoll对象中
   epoll_event event;
   event.events=EPOLLIN;//表示关心的事件是关于文件的读就绪
//event.data这是用户自定义的数据
   event.data.fd=listen_sock;
   int ret=epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_sock,&event);//通过epoll_ctl把listen_sock加入,相当于
往红黑树中添加了一个键值对。以文件描述符作为key找到节点。
   if(ret<0){
     perror("epoll_ctl");
     return 1;
   }
//4、进入循环,等待文件描述符就绪
   while(1){
       struct epoll_event output_event[100];//放置缓存数据的缓冲区
       int nfds=epoll_wait(epoll_fd,output_event,100,-1);//-1表示阻塞式的等待
       if(nfds<0){      //返回到缓冲区中放了几个元素,有几个文件描述符,放几个元素
          perror("epoll_wait");
          continue;
       }
       //数组中数据来源就是内核中的就绪队列,就是把就绪队列里的内容拷贝来
       //遍历所有的文件描述符
       int i=0;
       for(;i<nfds;++i){
//data是用户自定制的数据,这个数据前面赋值了当前的文件描述符的值,所以此处才能直接进行比较判定
          if(output_event[i].data.fd==listen_sock){
//a、就绪的文件描述符是listen_sock
        ProcessListensock(epoll_fd,listen_sock);
          }else{
//b、就绪的文件描述符是new_sock
      ProcessNewsock(epoll_fd,output_event[i].data.fd);
        }
    }
  }
  return 0;
}

进程中默认打开了3个文件描述符,默认为0,1,2;创建一个listen_sock文件描述符为3,client却显示为5。4是epoll_create创建的,epoll_create创建的句柄也是文件描述符,也回占据文件描述符表的一个位置,所以后续客户端连上之后,都是从5开始排序的。发送一个字符串,服务器端可以返回一个字符串。重新打开一个客户端,文件描述符会增加(client后跟的数字),显示建立连接,关闭客户端,显示断开连接。

epoll的优缺点(与select横向对比):

a、文件描述符数目无上限: 通过epoll_ctl()来注册一个文件描述符, 内核中使用红黑树的数据结构来管 理所有需要监控的文件描述符.select中由于文件大小的限制,最多只能有固定数目的文件描述符,epoll底层是一颗红黑树,没加一个文件描述符即加一个节点,红黑树的大小取决于内存的大小,没有明显的上限。

b、基于事件的就绪通知方式: 一旦被监听的某个文件描述符就绪, 内核会采用类似于callback的回调机制, 迅速激活这个文件描述符. 这样随着文件描述符数量的增加, 也不会影响判定就绪的性能;【回调函数和文件描述符的大小没有关系】

c、维护就绪队列: 当文件描述符就绪, 就会被放到内核中的一个就绪队列中. 这样调用epoll_wait获取就绪文件描述符的时候, 只要取队列中的元素即可, 操作的时间复杂度是O(1)【避免遍历,与上述b配合】

d、从接口使用得角度讲:不必每次都重新设置要监控得文件描述符,使接口使用更方便,也能够避免频繁的用户态和内核态之间大量拷贝文件描述符结构。【有三个函数:有客户端建立链接就添加,有客户端断开连接就减少。就不用每次调用一次epoll_wait就得重新设置一次文件描述符,避免频繁进行数据拷贝和设置。select函数由于每次调用都得设置一次文件描述符,导致代码不方便、频繁得进行用户态和内核态得拷贝。】

5、判断下面这句话说的对么:epoll中使用了内存映射机制。

内存映射机制: 内核直接将就绪队列通过mmap的方式映射到用户态. 避免了拷贝内存这样的额外性能开销 。这种说法是不正确的。a、用户态看到的结果可能定义在栈上、堆上……是一个什么样的内存是不确定的。b、拷贝过程是不可避免的,需要把内核中的数据拷贝到用户空间内存中。

6、epoll的工作方式:

(1)水平触发LT(Level Triggered)【默认】

客户端给服务器端发送数据,服务器端按照epoll的方式处理数据,假设客户端给服务器端发送10k数据,由于服务器端采用epoll的方式进行处理,epoll_wait返回,并且返回文件描述符读就绪,然后从文件描述符中读数据,此时只读取1k数据,还剩下9k数据还在缓冲区中。对于水平触发模式,如果只读取了1k数据,再次进入epoll_wait的循环,epoll_wait会立刻返回,并且返回的信息是未读完的文件描述符的就绪状态。epool,select,poll的默认情况下都是LT,水平触发。

(2)边缘触发ET(Edge Triggered)

再次进入epoll_wait的时候,此时epoll_wait就不会返回了。【一次没有把数据读完,下次就不能再读了】。ET更严格,要求程序员每次在ET返回的时候都要所有的数据都一次性处理完。按照LT的方式,代码简单。ET也有效率更高的优点,因为一次处理完毕的效率更高。(高速模式:不要分多次处理数据,一次处理完)

使用ET模式的epoll最好将文件描述符设置为非阻塞,可以在一定程度上避免出错。例如:假设服务器收到一个10k的请求,会向客户端返回一个应答数据,如果客户端收不到应答,不会发送第二个10k请求:

 

 

6、epoll的使用模式:

epoll的高性能只是相对于select的。

对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll。 只有少数的几个连接(阻塞式IO或者异步思想更合适), 这种情况下用epoll就并不合适. 具体要根据需求和场景特点来决定使用哪种IO模型.
7、epoll中的惊群问题:

 

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xuruhua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值