[置顶] select poll epoll使用示例

    select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
    这三个方法都要调用驱动中的struct file_operations中的.poll方法。poll是一个函数指针,申明如下
unsigned int (*poll) (struct file *, struct poll_table_struct *);
在驱动中poll的实现方法如下:
.....
static unsigned int Test_poll(struct file *filp, poll_table *wait)
{
unsigned int mask = 0;
......... 
poll_wait(filp, &(int_queue), wait);
if(read ready)

mask |=POLLIN | POLLRDNORM;
}
        if(write ready)

mask |=return POLLOUT | POLLWRNORM;
}
............
        return  mask ;
}
static const struct file_operations Test_fops = {
.owner = THIS_MODULE,
.......
.poll = Test_poll ,
        .......
};
注意:.poll本身是不会阻塞的,不论读写条件是否满足,都会立即返回。阻塞是在调用 .poll的时候实现的,poll返回0的时候,将阻塞,等待int_queue。
int_queue是一个 类型为wait_queue_head_t结构的等待队列。
在驱动初始化的时候调用init_waitqueue_head(&(int_queue));初始化等待队列
当读写条件满足的时候,调用wake_up_interruptible(&( int_queue));唤醒等待队列


poll方法返回的数据如下:
常量             说明
POLLIN           普通或优先级数据可读
POLLRDNORM       普通数据可读
POLLRDBAND       优先级数据可读
POLLPRI          高优先级数据可读
POLLOUT          普通数据可写
POLLWRNORM       普通数据可写
POLLWRBAND       优先级数据可写
POLLERR          发生错误
POLLHUP          发生挂起
POLLNVAL         描述字不是一个打开的文件


1 select的实现示例
  select相关函数原型如下:
  
  int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout);
  maxfd是需要监视的最大的文件描述符值+1,例如如果我们分别要监视3个文件描述符fd1 fd2 fd3 ,数值分别为100,500,1000,那么maxfd就必须写1001。
  rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合。
  timeout 为设置的超时时间,如果为NULL,则一直等待,直到监视的资源可用。
  函数返回值:小于0 函数执行出错 ,超时将返回0 。大于0 表示有资源可用的数目,例如我们分别监视了fd1 fd2 fd3 那么如果fd1 发的同时可以写了则返回2。


  对fd_set的操作可以通过下面的宏来操作:
  FD_ZERO(fd_set*); 用来清空fd_set集合,即让fd_set集合不再包含任何文件句柄。
  FD_SET(int ,fd_set *); 用来将一个给定的文件描述符加入fd_set集合之中
  FD_CLR(int ,fd_set*); 用来将一个给定的文件描述符从集合中删除
  FD_ISSET(int ,fd_set*);检测fd在fdset集合中是否存在,存在返回真,否则,返回假(也可以认为集合中指定的文件描述符是否可以读写)。


  这里需要注意的是select执行的时候,rdset、wrset、exset、timeout、会被改变,所以每次执行select前一定要重新设置下。例如rdset分别监视fd1 fd2 fd3,当fd3可以读都,select返回,那么这个时候rdset就只设置了fd3,如果我们没有重新设置,那fd1 fd2就监视不到了。同样timeout也是一样,当超时返回后,timeout变为0,如果这个时候没有重新设置,那么select函数就会立即返回了,无论是否资源可用。


  下面是示例代码:
void * Test_select( void*  arg )
{
fd_set fdsi;
fd_set fdso;
int maxfdp ;
int ret;
struct timeval timeout;
        int fd ,fd2;
fd = open(DEV_FILENAME, O_RDWR  );
if(fd<0) 
{
printf("open device error\r\n");

return NULL ;
}
fd2 = open(DEV_FILENAME2, O_RDWR  );
if(fd<0) 
{
printf("open device error\r\n");

return NULL ;
}

    maxfdp = fd>fd2?fd+1:fd2+1 ;
    
    while(exitFlag==0)
    {
    timeout.tv_sec=1;//1秒超时
        timeout.tv_usec = 0;
        FD_ZERO(&fdsi); 
    FD_SET(fd,&fdsi);
    FD_SET(fd2,&fdsi);
         
    
    FD_ZERO(&fdso); 
    FD_SET(fd,&fdso);
    FD_SET(fd2,&fdso); 
   
    ret = select(maxfdp,&fdsi,&fdso,NULL,&timeout);
  //如果不需要超时可以改为  ret = select(maxfdp,&fdsi,&fdso,NULL,NULL);
    if(ret==0)
    {
    printf("select timeout\r\n");
    continue;
    }
    else if(ret<0)
    {
    printf("select error\r\n");
    exitFlag= 1;
    }
    else
    {
     
    if(FD_ISSET(fd,&fdsi)) 
    {
       //read data;
    }
    else if(FD_ISSET(fd,&fdso)) 
    {
       //write data;
    }
   
    if(FD_ISSET(fd2,&fdsi)) 
    {
       //read data;
    }
    else if(FD_ISSET(fd2,&fdso)) 
    {
       //write data;
    }
    }
    }
close(fd );
close(fd2 );
return NULL;
}
说明:在系统内核中select是采用轮询来处理的,如果需要监视的文件描述符越多,就需要消耗更多的资源。所以如果要监视很多文件描述符,最好使用epoll(epoll在后面会介绍)。
select监视的最大数量有限制,通常是1024。


2 poll的实现示例


  poll实现比select简单,只有一个函数就可以了,函数原型如下
  int poll (struct pollfd *fds, nfds_t nfds, int timeout);
  参数fds :pollfd 结构体的指针,这个结构描述了需要监视的文件描述符和我们关心的事件。这个函数也可以监视多个文件描述符。
            struct pollfd结构描述如下
  struct pollfd
  {
    int fd;                     /* poll 的文件描述符.  */
    short int events;           /* fd 上感兴趣的事件(需要等待的事件).  */
    short int revents;          /* fd 上实际发生的事件. 也就是poll返回时 的状态*/
  }; 
  参数 nfds:fds的个数,也就是要监视的文件描述符的个数。
  参数 timeout : 超时的毫秒数,如果为-1则表示永远等待。
  函数返回值 返回0表示超时 返回值小于0函数出错,大于0 实际可用的资源数




  下面是示例代码:  
void * Test_poll( void*  arg )
{
    struct pollfd Events[2] ;
    int ret;
    int fd ,fd2;
    fd = open(DEV_FILENAME, O_RDWR  );
    if(fd<0) 
    {
        printf("open device error\r\n");
return NULL ;
    }
    fd2 = open(DEV_FILENAME2, O_RDWR  );
    if(fd<0) 
    {
printf("open device error\r\n");
return NULL ;
    }
    Events[0].fd = fd;
    Events[0].events = POLLIN | POLLERR;     /*关心读取和出错事件*/
 
    Events[1].fd = fd2;
    Events[1].events = POLLIN | POLLERR;     /*关心读取和出错事件*/
    while(exitFlag==0)
    {
     
    ret = poll ((struct pollfd *)&Events[0], 2, -1);//如果要设置超时可以把-1改为要设置的超时的毫秒数
    if(ret==0)
    {
    printf("poll timeout\r\n");
    continue;
    }
    else if(ret<0)
    {
    printf("poll error\r\n");
    exitFlag= 1;
    }
    else
    {
     
    if (Events[0].revents & POLLERR) 
    {
printf ("device error!\n");              
                }
if (Events[1].revents & POLLERR) 
    {
printf ("device error!\n");              
                }
                         
            if (Events[0].revents & POLLIN) {
                  //read data;              
           
            
            if (Events[1].revents & POLLIN) {
                  //read data;              
           
    }
    }
    close(fd );
    close(fd2 );
    return NULL;
}
说明:poll函数的实现方法和select类似,所以等待的文件描述符越多,越消耗资源。


3 epoll实现说明
  epoll实现很简单,就三个函数就解决了,下面是函数申明:


  int epoll_create(int size);
  这个函数的功能创建一个epoll的句柄,当创建好epoll句柄后,它就是会占用一个fd值所以使用完epoll后一定要close掉,避免占用资源。
  参数size:用来告诉内核这个监听的数目一共有多少。 自从Linux 2.6.8开始,size参数被忽略,但是依然要大于0。
  返回值:成功返回epoll的句柄,失败返回-1。


  int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  参数 epfd: epoll的句柄,是epoll_create()的返回值
  参数 op:操作码。用三个宏来表示的
           EPOLL_CTL_ADD:注册新的fd到epfd中;
           EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
           EPOLL_CTL_DEL:从epfd中删除一个fd;
  参数 fd:要监视的文件描述符。
  参数event :要监视的事件的描述。
  struct epoll_event的结构说明如下:
  struct epoll_event {
__uint32_t events; /* 需要监视的事件,如EPOLLIN、EPOLLOUT等等 */
epoll_data_t data; /* 用户数据*/
  }; 
  typedef union epoll_data
  {
  void        *ptr;
  int          fd;
  __uint32_t   u32;
  __uint64_t   u64;
  } epoll_data_t;
  返回值:0成功,小于0 失败


  int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  该函数等待事件产生。
  参数epfd: epoll的句柄
  参数events:从内核得到的事件集合
  参数maxevents:events的大小。events的大小取决于可能同时产生的事件的数量。例如我们虽然同时监视了fd1 fd2 fd3 三个文件描述符,但是这3个之间是不可能同时可读或者可写的,那么将maxevents设置为1也没有问题,但是如果三个事件可能会同时产生,那么最好就设置为3。如果设置为1,3个事件同时产生,epoll_wait第一次返回的时候就会只返回第一事件了。(不过在此调用这个函数的时候,会马上返回。例如当fd1 fd2 fd3同时可读,但是maxevents为1时,那么第一次调用epoll_wait返回值为1,报告fd1的时间,第二次调用,马上返回1,调用fd2的事件,第三次调用马上返回1,报告fd3的事件.而如果maxevents为3的话,当fd1 fd2 fd3同时可读,epoll_wait返回值为3,同时报告fd1 fd2 fd3的事件)。
  参数timeout:超时的毫秒数,如果为-1表示一直等待直到事件产生。


  返回值:超时返回0 ,返回小于0 函数执行失败,大于0,返回值为实际的事件数目。


下面是示例代码:


static int epoll_register( int  epoll_fd, int  fd )
{
    struct epoll_event  ev;
    int                 ret, flags;
    /*****************************************/ 
    flags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
    //资料上说需要设置为非阻塞的方式,我试了下,似乎不设置为非阻塞的方式也不会出错
    /**********************************************/
    ev.events  = EPOLLIN;
    ev.data.fd = fd; 
    ret = epoll_ctl( epoll_fd, EPOLL_CTL_ADD, fd, &ev );  
    return ret;
}
void * Test_epoll( void*  arg )
{
int   epoll_fd  ,ret ;
int count =0;
struct epoll_event   events[2];
int                  ne, nevents; 
int fd ,fd2;
fd = open(DEV_FILENAME, O_RDWR  );
if(fd<0) 
{
printf("open device error\r\n");

return NULL ;
}
fd2 = open(DEV_FILENAME2, O_RDWR  );
if(fd<0) 
{
printf("open device error\r\n");

return NULL ;

 
epoll_fd = epoll_create(1);
epoll_register( epoll_fd, fd );
epoll_register( epoll_fd, fd2 );
while(exitFlag==0)
{

        nevents = epoll_wait( epoll_fd, events, 2, -1 );
        if (nevents < 0) 
        {
            exitFlag =1;
                printf("epoll_wait() unexpected error: %s", strerror(errno));
            }
           
       
        
        for (ne = 0; ne < nevents; ne++) 
        {
            if ((events[ne].events & (EPOLLERR|EPOLLHUP)) != 0) 
            {
                 
            }
            if ((events[ne].events & EPOLLIN) != 0) 
            {
            if( fd == events[ne].data.fd)
            {
            //read data;
            }
            else if( fd2 == events[ne].data.fd)
            {
            //read data;
            }
             
 
           
            }
        }
}
epoll_ctl( epoll_fd, EPOLL_CTL_DEL, fd, NULL ); 
epoll_ctl( epoll_fd, EPOLL_CTL_DEL, fd2, NULL ); 
ret=close(epoll_fd);
 
return NULL;
}


最后总结:


(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。


(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
所以最好尽量使用epoll。他的速度更快,系统开销更小。

你可能感兴趣的:([置顶] select poll epoll使用示例)