I/O复用之select、poll、epoll函数

本文介绍了Linux下I/O复用技术的三种实现方式:select、poll和epoll。详细讲解了这三个系统调用的工作原理、参数及使用方法,包括超时设置、事件检测和文件描述符管理。epoll作为Linux特有的高效I/O复用机制,通过事件表实现了对文件描述符的高效管理。
摘要由CSDN通过智能技术生成

为了提高程序处理效率和机制,经常需要一个程序可以达到监听甚至处理多个文件描述符的性能,为了带到这种机制我们需要借用I/O复用来实现。I/O复用虽然可以同时处理多个文件,但是它本身是阻塞的。就是当文件有多个就绪的时候程序检测到了才会继续往下执行,而且在执行的时候如果没有家外界措施他就会按照就绪的顺序执行,如果要实现并行的处理,可以通过进程或者线程来实现,在Linux下面如果要实现I/O复用,主要靠select、poll、epoll三个函数来实现,在这里我们业主要针对这三个函数进行描述。这三个系统调用较为复杂,概念性较强需要大家仔细阅读他们的说明,下面我们来看看那这三个函数的具体实现:

1、select系统调用:

这个函数的作用是在一段时间内,监听用户就绪的文件描述符上的可读、可写、异常这些事件。函数定义如下:

#include<sys/select.h>
int select(int  nfds,fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout );

接下来我们先从前到后的对select函数的参数进行讲解,首先nfds是整形类型的文件描述符,就是客户需要被坚挺的文件描述符,可以使文件描述符集合,当时通常在使用的时候我们要给文件描述符的总数nfds加1,因为在这里文件描述符集合的下表是从0开始的;其次是readfds、writefds、excesfds三个参数分别是表示可读、可写、异常事件,他们三个是fd_set结构体指针类型。fd_set结构体定义如下:

#include<typesizes.h>
#define __FD_SETSIZE 1024
 #include<sys/select.h>
 #define FD_SETSIZE __FD_SETSIZE
 typedef long int __fd_mask;
 #undef __NFDBITS
 #define __NFDBITS
 typedef struct
 {
     #ifdef __USE_XOPEN
        __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
    #define __FD_BITS(set) ((set)->fds_bits)
    #else
        __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
    #define __FD_BITS(set) ((set)->__fds_bits)
    #endif
 }fd_set;

fd_set是一个仅包含一个整形数组的结构体,每个元素的每一位(bit)就可以标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,有限制。
fd_set结构体定义的数组中可以由四个函数来控制,通过这四个函数可以实现对结构体数组的置位、清位、将所有位置零、测试某一位是否被设置:

#include<sys/select.h>
FD_ZERO(fd_set *fdset);//清除fdset的所有位
FD_SET(int fd, fd_set *fdset);//设置fdset的位fd
FD_CLR(int fd, fd_set* fdset);//清除fdset的位fd
int FD_ISSET(int fd, fd_set* fdset);//检测fdset的位fd是否被设置

最后一个参数timeout是用来设置select函数的超时时间,就是设置在文件描述符没有准备好等待的时间,因为timeout是结构体变量,它的成员有秒和微秒,所以设定的时间较为精确,设定的时间如果时间到了文件描述符还没有事件就绪,它就会不再等待直接返回,如果设置为0那么将不会阻塞且会立即返回,设置为NULL或者-1将会一直阻塞,知道文件描述符时间就绪才会返回,这也是我们经常用到的地方。下面我们来看看这个文件时间结构体的定义:

struct timeval
{
    long tv_sec;//秒
    long tv_usec;//微秒
};

select成功调用后会返回就绪的文件描述符的个数,没有准备好的文件描述符如果在设置的时间或者立即返回的情况下会返回0,调用失败就会返回-1并设置errno的值,如果在select函数阻塞的期间接收到信号,也会立即返回-1设置errno的值为EINTR。
下面我来看看代码的实现:

#include<iostream>                                                                                                                      
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/socket.h>
#include<string.h>
using namespace std;

#define MAX_CLIENT_SIZE 5
#define MAX_BUFFER_SIZE 1024

int main(int argc, char* argv[])
 {
     if(argc <= 2)
     {
         cout<<"usage: %s ip_address port_number"<<basename( argv[0] )<<endl;
         return 1;
     }
     char* ip = argv[1];
     int port = atoi( argv[2] ); 
     int sockser = socket( AF_INET, SOCK_STREAM, 0);
     assert( sockser >= 0);

     struct sockaddr_in address,client_addr;
     address.sin_family = AF_INET;
     address.sin_port = htons(port);
     inet_pton( AF_INET, ip, &address.sin_addr);
     //address.sin_addr.s_addr = inet_addr(ip ); 
     int ret = 0;
     fd_set readfds;
     int conn_num = 0;
     int max_sock = sockser;
    int conn_clientfd[MAX_CLIENT_SIZE] = {0};
     cout<<"max_sock = "<<max_sock<<endl;

     ret = bind(sockser, (struct sockaddr*)&address, sizeof(address));
     assert( ret != -1);

     ret = listen(sockser, 5);
     assert( ret != -1);
     char buf[MAX_BUFFER_SIZE];
     socklen_t client_addrlength = sizeof( client_addr );
     while(1)
     {
             FD_ZERO(&readfds);
             FD_SET(sockser, &readfds);
             for(int i=0; i<conn_num; ++i )//轮询查看事件就绪文件描述符
             {
                 if( conn_clientfd[i]!= 0)
                     FD_SET(conn_clientfd[i], &readfds);
            }
             ret = select( max_sock+2, &readfds, NULL, NULL, NULL);
             assert( ret > 0 );
            if( conn_num < MAX_CLIENT_SIZE )
            {
                  conn_clientfd[conn_num++] = conn;
                  if(sockser > max_sock)
                      max_sock = sockser;
            }
            else
            {
                  char *str = "server over load";
                  cout<<str<<endl;
                  send(conn , str, strlen(str)+1, 0);
            }
               continue;
         }
         for(int i=0; i< conn_num; ++i)
         {
                if( FD_ISSET(conn_clientfd[i], &readfds))
                {
                    ret = recv(conn_clientfd[i], buf, MAX_BUFFER_SIZE, 0);
                    printf("from %d client maseg,maseg size is  %d Byte", i, ret);
                     send(conn_clientfd[i], buf, strlen(buf)+1, 0);
                }
                else
                 conn_num --;

             }
         //continue;

        }
         close(sockser);
         return 0;
     }                                                                                                                                       

2、poll系统调用:

poll函数也是系统调用和select类似,需要轮询一定数量的文件描述符,以测试其中是否有事件就绪,函数定义如下:

#include<poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);

1)fds 是struct pollfd类型的数组,它可以检测设定的文件描述符上发生的可读、可写、异常事件等。pollfd结构体的定义如下:

struct pollfd
{
    int fd;//文件描述符
    short events;//注册的事件
    short revents;//文件描述符实际发生的事件,由内核填充
};

poll和select函数一样,如果有事件发生,需要轮询产看revents来查看具体是哪个描述符就绪,调用成功返回文件符事件就绪的总个数,如果调用失败就返回-1,并设置error。poll支持的事件有:
这里写图片描述
这里写图片描述
上图这些事件都是events成员可以检测的事件,同时events可以告诉poll函数检测的是fd上面的那些事件,它是一些列时间的按位或,事件的发生会有内核修改成员revents,已通知程序fd上发生了那些事。
其中POLLRDNORM、POLLRDBAND、POLLWRNORM、POLLWRBAND由XOPEN规范定义。但他们实际上是将POLLIN和POLLOUT事件分的更精细,以区别对普通数据和有线数据。但Linux并不完全支持通它们。
通常我们写程序是通过recv调用返回值区分socket上接受的是有效数据还是对方关闭连接的请求,并做出处理,但是现在Linux内核2.6.17开始,GNU为poll系统调用增加了一个专门检测对方关闭连接的请求好出发的事件,就是POLLRDHUP事件。
2)nfds是nfds_t定义的参数,它的功能主要是指定被监听书剑几个fd的大小,nfds_t的定义如下:
typedef unsigned long int nfds_t;
3)timeout 是指定函数poll的超时值,单位是毫秒。如果timeout设定-1,poll永远等待、阻塞,知道事件发生;设置为0就会立即返回。下面我们看看具体poll函数在程序中的使用,代码实现:

#include<iostream>
#include<unistd.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<assert.h>
#include<poll.h>
using namespace std;
#define MAX_CLIENT_SIZE 5
int main()
{
     int sockser = socket(AF_INET, SOCK_STREAM, 0);
     assert(sockser >= 0);     
     struct sockaddr_in address,client_addr;
     address.sin_family = AF_INET;
     address.sin_port = htons( 9090 );
     inet_pton( AF_INET, "127.0.0.1", &address.sin_addr );

     int ret = 0;
     ret = bind( sockser, (struct sockaddr*)&address, sizeof(address));
      assert(ret != -1);
     ret = listen( sockser, 5);
     assert( ret != -1); 
      struct pollfd client_fds[MAX_CLIENT_SIZE+1];
      for(int i=1; i<MAX_CLIENT_SIZE; ++i)                                                                                                                                   
      {
          client_fds[i].fd = -1;
          client_fds[i].events = 0;
      }
      nfds_t nfds = 0;
      client_fds[0].fd = sockser;
      client_fds[0].events = POLLIN;
      socklen_t client_addrlength = sizeof(client_addr);
      char buf[256];
      while(1)
      {
          ret = poll(client_fds, nfds+1, -1);
          if(ret < 0)
          {
              cout<<"poll fail"<<endl;
              return 1;
          }
          if(client_fds[0].revents & POLLIN)
          {
              int sockconn = accept( client_fds[0].fd, (struct sockaddr*)&client_addr, &client_addrlength);
              if(sockconn < 0)
              {
                  cout<<"accept fail"<<endl;
                  continue;
              }
              cout<<"a nwe client come"<<endl;
              for(int i=1; i< MAX_CLIENT_SIZE+1; ++i)
              {
                  if(i > MAX_CLIENT_SIZE)
                  {
                      char *str = "server overload";
                      cout<<str<<endl;
                      send(sockconn, str, strlen(str)+1, 0);
                      return 1;
                  }
                  else  if(client_fds[i].fd == -1)
                  {
                      client_fds[i].fd = sockconn;
                      client_fds[i].events = POLLIN;
                      nfds ++;
                      break;
                  }
              }
              continue;
          }                                                                                                                                                                  
          for(int i=1; i<MAX_CLIENT_SIZE; ++i)
          {
              if(client_fds[i].revents & POLLIN)
              {
                 ret =  recv(client_fds[i].fd, buf, 256, 0);
                  cout<<"from "<<i<<"client maseg,the maseg size is"<<ret<<"Bite:>"<<buf<<endl;
                  send(client_fds[i].fd, buf, strlen(buf), 0);
              }
          }

      }
      close(sockser);
      return 0;
 }                                                                                                                                                                           



3、epoll系统调用:

它是Linux特有的I/O服用函数。它在实现和使用上与select、poll有很大差异。第一epoll使用一组函数来完成任务,而不是单个函数。其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表里,不想两个系统调用每次调用都需要重新传入文件描述符或事件集。最后epoll系统调用有一个借口函数,就是用来监测事件表中的文件描述符集中文件描述符是否有事件发生,返回值和前两个系统调用的意义一样。下面我们来看看这三个函数的原型和使用方法。
1)epoll需要一个额外的文件描述符来标示这个事件表,事件表的创建:

#include<sys/epoll.h>
int epoll_create(int size);

参数size只是告诉内核这个事件表需要多大的空间而已,由该函数返回的文件描述符是epoll系统调用的第一个参数。
2)我们来看看epoll内核事件表的操作函数,epoll_ctl:

#include<sys/epoll.h>
int epol_ctl(int epollfd, int op, int fd, struct epoll_event *event);

这个函数的作用是对用户关心的文件描述符和设定好该文件描述符的被监测事件一并添加进事件表中,为下面epoll系统调用的接口epoll_wait函数的检测做铺垫,这里的第一个参数epollfd就是在程序前面设定的事件表描述符;参数op是制定对事件表的操作类型,操作类型有三种:
EPOLL_CAT_ADD,往事件表中注册fd上的事件。
EPOLL_CTL_MOD,修改fd上注册事件。
EPOLL——CTL_DEL,删除fd文件描述符的注册事件。
fd参数就是要注测进事件表中的文件描述符。events是参数为了指定文件描述符fd上的事件设定的,它是一个结构体epoll_event类型的,该类型有两个成员,我们来详细看一下epoll_event结构体:

struct epoll_event
{
    __unit32_t events;//
    epoll_data_t data;//
};

events成员是epoll事件类型,epoll支持的事件和poll系统调用大部分相同,只是在poll对应事件前面加’E’,epoll比poll多了两个额外的事件类型:EPOLLET和EPOLLONESHOT,对于epoll的高校运作非常关键;data成员适用于存放用户数据的,它是一个联合体类行的变量,我们也一样来看看epoll_data_t这个类型的原型:

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

在这四个成员中用的最多的是fd,它是指定事件所有目标的文件描述符。ptr成员成员可用来指定与fd相关的用户数据。由于是联合体,我们不能同时使用其ptr成员和fd成员,因此,如果将文件描述符和用户数据关联起来,以实现快速的数据访问,只能使用其他手段,比如放弃使用epoll_data_t的fd成员,而在ptr指定的用户数据中包含fd。
—-epoll_ctl这个函数调用成功返回0,失败则返回-1,并设置errno。
3)epoll_wait函数:
它是epoll系统调用最后一步,也是比较关键的一步,这里开始对事件表中文件描述符集设定的事件进行检测工作,其原型如下:

#include<sys/epoll.h>
int epoll_wait(int epollfd, struct epoll_event* events, int maxevents, int timeout);

第一个参数就是我们申请的时间表空间标识符,第二个是我们申请用来存放事件发生文件描述符的数组,这也就是epoll系统调用和select、poll不同的地方,它是把检测到事件发生的文间描述符单独的放在这个数组中,不用像前两个系统调用那样,虽然能检测到文件描述符集中事件发生,但是没法确定是那个或者那几个文件描述符事件发生了,需要轮询查看,epoll系统调用只需对这里的events集合里左右成员操作即可。下面我们来看看具体代码实现:

 #include<iostream>
 #include<stdio.h>
 #include<unistd.h>
 #include<stdlib.h>
 #include<string.h>
 #include<sys/socket.h>
 #include<arpa/inet.h>
 #include<netinet/in.h>
 #include<assert.h>
 #include<sys/epoll.h>
 using namespace std;                                                                                                                                                                             
 #define MAX_BUFFER_SIZE 256
 #define MAX_EVENT_SIZE 1024

 void add_fd(int epollfd, int fd, int stat)
 {
     struct epoll_event ev;
     ev.events = stat;//设置该文件描述符的被监测事件
     ev.data.fd = fd;
     epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);
 }
 int main()
 {
     int sockser = socket(AF_INET, SOCK_STREAM, 0);
     struct sockaddr_in address, clientaddr;
     address.sin_family = AF_INET;
     address.sin_port = htons(9090);
     inet_pton( PF_INET, "127.0.0.1", &address.sin_addr);
     int ret = 0;
     ret = bind(sockser, (struct sockaddr*)&address, sizeof(address));
     assert(ret != -1);
     ret = listen(sockser, 5);
     assert( ret !=  -1 );
     int epoll_fd = epoll_create(5);
     add_fd(epoll_fd, sockser, EPOLLIN);//将服务器放在事件表中,并设定关注事件
     struct epoll_event events[MAX_EVENT_SIZE];//单独存放发生时间的集合
     char buf[MAX_BUFFER_SIZE];
     socklen_t client_addrlength = sizeof( clientaddr );
     while(1)
     {
         int ret = epoll_wait(epoll_fd, events, MAX_EVENT_SIZE, -1);
         if(ret < 0)
         {
             perror("epoll:> ");
             close( sockser );
             return 1;
         }
         else
         {
             for(int i=0; i< ret; ++i)
             {
                 int fd = events[i].data.fd;
                 if( (sockser == fd)&& (events[i].events & EPOLLIN))
                 {
                     int sockconn = accept(sockser, (struct sockaddr*)&clientaddr, &client_addrlength);
                     assert(sockconn != -1);
                     add_fd(epoll_fd, sockconn, EPOLLIN);

                 }
                 else if(events[i].events & EPOLLIN)
                 {
                     int res = recv(events[i].data.fd, buf, MAX_BUFFER_SIZE, 0);
                     printf("from %d number client masege,the size of maseg is %d byte %s:>",i,res,buf);
                     send(events[i].data.fd, buf, strlen(buf)+1, 0);
                 }
             }
         }

     }
     printf("sever over!\n");
     close(sockser);
     return 0;
 }                                                                                                                                                                                                                                                                                            

上面所有说明是本人再学习I/O复用时候对三个系统调用的理解,欢迎大家留言讨论select、poll、epoll三个函数的各种属性。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值