Linux select,poll,epol

参考文献:
select : https://blog.csdn.net/youyou1543724847/article/details/83445226

原文地址:
https

1. select

函数原型:

int select (int nfds,  fd_set *readfds,   fd_set *writefds, fd_set *exceptfds,    struct timeval *timeout) 

返回值分为三种情况:

  • 做好准备的文件描述符的个数
  • 返回0:超时;
  • 返回-1:错误;

当没有错误、没有超时,返回时,不能直接直到那个文件可以操作,而需要遍历fd_set,对每个描述符进行测试,检查该文件是否ready(通过宏 FD_ISSET进行测试,FD_ISSET(fd, &fds) )。

另外,关于select支持的监控文件数量限制:
select监控的文件描述符都要在fd_set 中设置。但是linux对fd_set 的大小做了限制:#define __FD_SETSIZE 1024

参见:https://blog.csdn.net/youyou1543724847/article/details/83445226

一般使用步骤
初始化化好需要监听的文件,绑定到 对应的fd_set中,然后调用select等待相关的文件可读、可写或是异常发生。然后对所有监听的文件列表进行一一测试,判定是否有事件发生,如有就处理。直到处理完所有的文件列表

2. poll

函数原型:

int poll(struct pollfd fd[],  nfds_t nfds,   int timeout);

poll函数与select函数差不多
不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。

struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};

pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

3. epoll

linux2.6 内核正式引入。

3.1 函数原型

3.1.1 创建epoll文件句柄

int epoll_create(int size);

函数参数:

  • size: 用来告诉内核要监听的数目一共有多少个

返回值:

  • 成功时,返回一个非负整数的文件描述符,作为创建好的epoll句柄
  • 调用失败时,返回-1,错误信息可以通过errno获得。

3.1.2 在特定的epoll文件句柄上,注册、修改、删除 要监听文件与事件

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

函数参数:

  • 参数epfd:epoll_create()函数返回的epoll句柄。
  • 参数op:操作选项。指定本次调用是新注册、修改、还是删除已经注册的fd
  • 参数fd:要进行操作的目标文件描述符。
  • 参数event:struct epoll_event结构指针,将fd和要进行的操作关联起来。

返回值:

  • 成功时,返回0,作为创建好的epoll句柄;
  • 调用失败时,返回-1,错误信息可以通过errno获得。

说明:epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

参数op的可选值有以下3个:

  • EPOLL_CTL_ADD:注册新的fd到epfd中;
  • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
  • EPOLL_CTL_DEL:从epfd中删除一个fd;

struct epoll_event结构

typedef union epoll_data {  
   void *ptr;  
   int fd;  
   __uint32_t u32;  
   __uint64_t u64;  
} epoll_data_t;  
  
struct epoll_event {  
   __uint32_t events;    /* Epoll events */  
   epoll_data_t data;   /* User data variable */  
};  

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

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

关于epoll events中的 水平触发与边缘触发:
Edge Triggered (ET) :边缘触发(events 设置为EPOLLET),只有数据到来,才触发,不管缓存区中是否还有数据。
Level Triggered (LT): (默认的工作方式)水平触发,只要有数据都会触发(如上一次调用时,数据没有读完,再次调用时,还是再次触发事件)

3.1.2 开始监听,等待事件的产生

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

函数参数:

  • 参数epfd:epoll_create()函数返回的epoll句柄。
  • 参数events:struct epoll_event结构指针,用来从内核得到事件的集合。
  • 参数 maxevents:告诉内核这个events有多大
  • 参数 timeout: 等待时的超时时间,以毫秒为单位。

返回值:

  • 成功时,返回需要处理的事件数目。具体那些fd可用,通过epoll_event *events获取,有意义的数据长度就是这里的返回值;
  • 调用失败时,返回0,表示等待超时。

3.2使用示例

#include<stdio.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<unistd.h>
#include<ctype.h>

int main(int argc,char *argv[])
{
 

   int  listenfd = socket(AF_INET,SOCK_STREAM,0);
  .........//省略对该socket的初始化共组,包括绑定端口,开始监听client请求  

    // 创建一个epoll fd
    int  efd = epoll_create(1024);
    struct epoll_event  tep.events = EPOLLIN;   tep.data.fd = listenfd;
    // 把监听socket 先添加到efd中
    ret = epoll_ctl(efd,EPOLL_CTL_ADD,listenfd,&tep);
 

   // 循环等待
    struct epoll_event   ep[MAX_OPEN_FD]; //使用ep保存需要处理的返回的epoll_event
    for (;;)
    {
        // 返回已就绪的epoll_event,-1表示阻塞,没有就绪的epoll_event,将一直等待
        size_t   nready = epoll_wait(efd,  ep   ,MAX_OPEN_FD   ,-1   );
        for (int i = 0; i < nready; ++i) //处理每个ready的fd
        {
            if (ep[i].data.fd == listenfd ) //server socket可读,即有新client在建立连接
            {
                connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);
                tep.events = EPOLLIN;
                tep.data.fd = connfd;
                ret = epoll_ctl(efd,EPOLL_CTL_ADD,connfd,&tep);  //和新的client建立连接,将新的confd注册到epoll上。
            }
            // 否则,读取数据
            else //之前的client发送来了数据,需要处理
            {
                connfd = ep[i].data.fd;
                int bytes = read(connfd,buf,MAXLEN);
                // 客户端关闭连接
                if (bytes == 0){
                    ret =epoll_ctl(efd,EPOLL_CTL_DEL,connfd,NULL); //处理完这个连接了,在epoll中取消注册
                    close(connfd); //关闭连接
                    printf("client[%d] closed\n", i);
                }
                else
                {
                    for (int j = 0; j < bytes; ++j)
                    {   buf[j] = toupper(buf[j]);    }
                    // 向客户端发送数据
                    write(connfd,buf,bytes);
                }
            }
        }
    }
    return 0;
}

3.3 epoll说明

Linux 内核:epoll系统向内核注册了一个文件系统,用于存储上述的被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。

epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket(通过epoll_ctl注册),这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。

通过epoll_ctl函数都会被解释成对相关红黑树上的节点的操作。重复添加是没有用的。

当把事件添加进来的时候时候会完成关键的一步,那就是该事件都会与相应的设备(网卡)驱动程序建立回调关系,当相应的事件发生后,就会调用这个回调函数,该回调函数在内核中被称为:ep_poll_callback,这个回调函数其实就所把这个事件添加到rdllist这个双向链表中。一旦有事件发生,epoll就会将该事件添加到双向链表中。

那么当我们调用epoll_wait时,epoll_wait只需要检查rdlist双向链表中是否有存在注册的事件,效率非常可观。这里也需要将发生了的事件复制到用户态内存中即可。

猜你喜欢

转载自blog.csdn.net/youyou1543724847/article/details/85055685
今日推荐