欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

select和poll和epoll

程序员文章站 2022-06-13 12:30:51
...

什么是I/O复用:使应用程序同时管理多个文件描述符,以提高程序的性能。

select

在一段指定时间内,监听用户感兴趣的文件描述符上的事件(可读、可写、异常)。

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

<1>nfds:指定被监听的文件描述符的总数,通常为select监听的所有文件描述符+1,因为文件描述符是从0开始计数的。
<2>readfds、writefds、exceptfds:分别指向可读、可写和异常事件对应的文件描述符的集合,调用select函数时,通过这三个参数传入 自己感兴趣的文件描述符。select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就位。

#include <typesizes.h>
#define __FD_SETSIZE 1024 //fd_set能容纳的文件描述符的数量由它决定
#include <sys/select.h>
#define FD_SETSIZE __FD_SETSIZE
typedef long int __fd_mask
#undef __NFDBITS
#define __NFDBITS (8 * (int)sizeof( __fd_mask ) )
typedef struct
{
    #ifdef__USE_XOPEN
        __fd_mask fds_bits[__FD_SETSIZE/__NFDBITS];//数组
    #define __FDS_BITS(set) ((set)->fds_bits)//设置
    #else
        __fd_mask __fds_bits[__FD_SETSIZE/__NFDBITS];//数组
    #define __FDS_BITS(set) ((set)->__fds_bits)//设置
    #endif
}fd_set;//fd_set结构体仅包含一个整形数组,该数组的每一位标记一个文件描述符

//使用宏来访问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是否被设置

<3>timeout:设置select函数的超时时间。它是一个timeval结构体类型的指针,采用指针可以使内核修改它以告诉应用程序select等待了多久。函数调用失败时,timeout的值是不确定的。

struct timeval
{
    long tv_sec;//秒数
    long tv_usec;//微妙数
};//为0立即返回;为NULL,select将一直阻塞直到某个文件描述符就绪。

<4>select调用成功,返回就绪的文件描述符总数。失败时返回-1并设置errno。如果在select等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR。

文件描述符就绪条件

可读
<1>socket内核接收缓冲区中的字节大于或者等于其低水位标记SO_RCVLOWAT。
<2>socket通信的对方关闭连接。
<3>监听socket上有新的连接请求。
<4>socket上有未处理的错误。使用getsckopt来读取和清除该错误。
可写
<1>socket内核发送缓冲区中的可用字节大于或等于其低水位标记SO_SNDLOWAT。
<2>socket的写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号。
<3>socket使用非阻塞connect连接成功或者失败之后。
<4>socket上有未处理的错误。使用getsckopt来读取和清除该错误。
异常:socket上收到带外数据。
带外数据:当socket上接收到普通数据和带外数据都将使select返回,但socket处于不同的就绪状态,前者处于可读状态,后者处于异常状态。

//对于可读事件,采用普通的recv函数读取数据
if(FD_ISSET(connfd, &read_fds))
{
    ret = recv(connfd, buffer, sizeof(buffer)-1, 0);
}
//对于异常事件,采用带MSG_OOB标志的recv函数处理
if(FD_ISSET(connfd, &exception_fds))
{
    ret = recv(connfd,buffer, sizeof(buffer)-1, MSG_OOB);
}

poll

在指定的时间内轮询一定数量的文件描述符,以测试其是否就绪。

//pool API:
#include <poll.h>
int poll(struct poolfd *fds, nfds_t nfds, int timeout);

<1>fds:是一个pollfd结构体类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写、和异常事件。

struct poolfd
{
    int fd;//文件描述符
    short events;//注册的事件,即poll监听fd上的哪些事件,它是一系列事件的按位或
    short revents;//实际发生的事件,由内核填充,通知应用程序fd上实际发生了哪些事件
};

select和poll和epoll
<2>nfds:指定被监听事件集合fds的大小。

typedef unsigned long int nfds_t;

<3>timeout:指定poll的超时时间,单位是毫秒。为-1时,poll永远阻塞,直到某个时间发生。为0时,立即返回。

epoll

epoll是linux特有的I/O复用函数。epoll使用一组函数来完成任务。epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无需像select和poll那样每次调用都要重复传入文件描述符集或事件集。但是epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表(Linux下一切皆文件)。

//创建
#include <sys/epoll.h>
int epoll_create(int size);
//size只是给内核一个提示,告诉它事件表需要多大
//返回的文件描述符将作为其他所有epoll系统调用的第一个参数,指定访问内核事件表

//操作epoll的内核事件表
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//成功返回0
<1>epfd是需要操作的文件描述符。
<2>op指定操作类型。
    //EPOLL_CTL_ADD,往事件表中注册事件
    //EPOLL_CTL_MOD,修改fd上注册的事件
    //EPOLL_CTL_DEL,删除fd上的注册事件
<3>event指定事件。
struct epoll_event
{
    _unit32_t events;//epoll事件,描述事件类型,和poll的宏相同只需添加E  POLLIN-->EPOLLIN
    //epoll的额外事件类型:EPOLLET和EPOLLONESHOT
    epoll_data_t data;//存储用户数据
    typedef union epoll_data
    {
        void *ptr;//指定和fd相关的用户数据
        int fd;//文件描述符
        unit32_t u32;
        unit64_t u64;
    }epoll_data_t;
};

//在一段超时时间内等待一组文件描述符上的事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
<1>epoll_create()返回的值。
<2>epoll_wait如果检测到就绪事件,就将所有的就绪事件从内核事件表复制到events指定的数组中。这个数组只用于输出epoll_wait检测到的就绪事件,而不像select和poll那样既用于传入用户注册的事件,又用于输出epoll_wait检测到的就绪事件。这就极大提高了应用程序检索就绪文件描述符的效率。

//索引poll返回的就绪文件描述符
int ret = poll(fds, MAX_EVENT_NUMBER, -1);
for(int i = 0; i < MAX_EVENT_NUMBER; ++i)//MAX_EVENT_NUMBER
{
    if(fds[i].revents & POLLIN)
    {
        int sockfd = fds[i].fd;
        //处理sockfd
    }
}

//索引epoll返回的就绪文件描述符
int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
for(int i = 0; i < ret; ++i)//ret
{
    int sockfd = events[i].data.fd;
    //处理sockfd
}

<3>maxevents指定最多监听多少个事件,必须大于0。
<4>timeout和poll的相同。
<5>成功时返回就绪的文件描述符的个数。

LT和ET模式

LT模式:默认工作模式,这种模式下相当于一个高效的poll。
采用LT模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件,这样,当应用程序下一次调用 epoll_wait时,epoll_wait还会再次向应用程序告知此事件,直到该事件被处理。

ET模式:当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符,ET模式是epoll的高效工作模式。
采用ET模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。ET模式在很大程度上降低了同一个epoll事件被重复触发的次数。因此效率较高。

EPOLLONESHOT事件
即使我们使用ET模式,一个socket上的某个事件还是可能会被触发多次,这在并发的程序中就会引起一个问题。如:一个线程在读取完某个socket上的数据后开始处理这些数据,而在数据的处理过程中该socket上又有新数据可读(EPOLLIN再次被触发),此时另外一个线程被唤醒来读取这些新的数据。于是就出现了两个线程同时操作一个socket的局面。我们期望一个socket连接在任一时刻都只能被一个线程处理。可用epoll的EPOLLONESHOT事件实现。对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上的一个可读、可写或者异常事件。

select、poll和epoll的比较

这三组I/O复用系统调用都能同时监听多个文件描述符。它们将等待由timeout参数指定的超时时间,直到一个或多个文件描述符上有事件发生返回。
select和poll和epoll

<1>select的 fd_set 结构体没有将文件描述符和事件绑定,仅仅是一个文件描述符的集合,因此需要三个参数来分别传入可读、可写和异常事件,这使得select不能处理更多类型的事件。由于内核对fd_set的在线修改,应用程序下次调用select前,必须重置这个3个fd_set集合。

<2>poll的pollfd把文件描述符和事件定义在一起,任何事件都被统一的处理,从而使编程接口变得更加简洁。内核每次修改的是结构体内的revents成员,而events成员保持不变,因此下次调用无需重置结构体的事件集参数。

<3>epoll则采用与select和poll完全不同的方式来管理用户注册的事件。它在内核维护一个事件表,并提供了一个独立的系统调用epoll_ctl来控制往其中添加、删除和修改事件。这样,每次epol_wait调用都直接从该内核事件表中取得用户注册的时间,而无需反复从用户空间读入这些事件。而且epoll_wait系统调用直接返回就绪的事件,这使得应用程序索引就绪文件描述符的时间复杂度为O(1)。而select和poll调用都返回整个用户注册的事件集合,所以应用程序索引就绪文件描述符的时间复杂度为O(n)。

<4>select和poll都采用的是轮询的方式,每次调用都要扫描整个注册文件描述符集合,将其中的文件描述符返回给用户程序,因此他们检测就绪事件的算法的时间复杂度都为O(n)。而epoll_wait采用回调的方式,内核检测到就绪的文件描述符时,将触发回调函数,回调函数就将该文件描述符上对应的事件插入内核就绪事件队列。内核最后在适当的时机将该就绪事件队列中的内容拷贝到用户空间。

<5>epoll和poll分别用nfds和maxevents参数指定最多监听的文件描述符和事件,这两个数目都为65535(cat/proc/sys/fs/file-max)。select允许监听的文件描述符数量通常有限制,用户可以修改。

<6>select和poll都只能工作在相对低效的LT模式,而epoll则可以采用ET高效模式,而且还支持EPOLLONESHOT事件,该事件能进一步减少可读、可写和异常事件被触发的次数。