同步I/O多路复用之select、poll和epoll

  1、select():

// 监控readfds、writefds和exceptfds,等待其中的一个或多个文件描述符“就绪”(可读、可写或异常)或到达超时时间
// 成功时返回就绪的描述符数,超时时返回0,出错时返回-1并设置errno
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

  timeout参数的三种可能:NULL:select()将永远阻塞直到有描述符准备好I/O;0:检查描述符后立即返回(轮询,polling);等待它指定的一段固定时间。

  readfds、writefds和exceptfds描述符集指定我们要让内核测试读、写和异常条件的描述符。以下是操作这些描述符集的四个宏:

void FD_CLR(int fd, fd_set *set);  // 从某个集合中移除描述符
int FD_ISSET(int fd, fd_set *set); // 测试描述符是否在某个集合中。通常在select()返回后使用
void FD_SET(int fd, fd_set *set);  // 将描述符加入到某个集合
void FD_ZERO(fd_set *set);         // 清空某个描述符集

  nfds参数是这三个集合中所有文件描述符的最大值加1。描述符0~nfds均将被测试。存在这个参数是为了效率:内核正是通过在进程与内核间不复制描述符集中不必要的部分,从而不测试总为0的那些位来提高效率的。

  例子:监控在5秒内标准输入是否准备好读

int main(void)
{
    fd_set rfds;
    FD_ZERO(&rfds);
    FD_SET(0, &rfds);    // 将标准输入描述符加入rfds

    struct timeval tv;    // 定义超时时间
    tv.tv_sec = 5;
    tv.tv_usec = 0;

    int retval = select(1, &rfds, NULL, NULL, &tv);
    if (retval == -1) { /* 出错 */ }
    else if (retval) { /* 有描述符就绪。此时FD_ISSET(0, &rfds)为真 */ }
    else { /* 超时返回*/ }

    return 0;
}

  select()存在的问题:

  1)select()监控的描述符集的大小有限,为FD_SETSIZE(一般是1024。描述符的值也必须小于1024)。

  2)描述符集内未就绪描述符对应的位在select()返回时均被清成0,因此每次重新调用该函数时都需要将关注的描述符的位置为1,然后再把描述符集从用户空间拷贝到内核。内核再遍历这些描述符以检查就绪条件。在描述符很多时,这些操作的开销较大。

 

  2、poll():提供的功能和select()类似。

// 成功时返回就绪描述符数(revents成员值非0的描述符数),超时时返回0,出错时返回-1并设置errno
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

  timeout参数的可能值:大于0:poll()将等待指定数目的毫秒数;0:立即返回;小于0:永远等待。

  fds参数指定被监控的描述符集(nfds指定集合的元素数)。以下是pollfd结构:

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

  其中,fd指定我们关注的文件描述符。如果我们不想再关注某个描述符,可以直接把它设置成负值。

  events指定了我们对于fd所感兴趣的事件,revents则是内核返回的实际发生的事件。一些用于events和revents的常值:

  POLLIN:有数据可读;POLLPRI:有紧急数据可读(如TCP的带外数据);POLLOUT:可写;POLLERR:发生错误(仅用在revents中);POLLHUP:发生挂起(仅用在revents中);POLLNVAL:描述符不是一个打开的文件(仅用在revents中)。

  poll()和select()的比较:

  1)poll()没有最大描述符数目的限制。

  2)poll()在处理流设备时,提供额外的信息。它识别三类数据:普通、优先级带(如TCP的带外数据)和高优先级。

  带外数据:一个连接的某端发生了重要的事情,而且该端希望迅速通告其对端,即这种通知应该在已排队等待发送的任何“普通”/“带内”数据之前发送。带外数据主要用于telnet、rlogin和FTP等远程非活跃应用中。

  3)poll()不需要在每次调用时都把关注的fd和events设置一遍,但需要在每次调用时都把关注的fd的pollfd结构拷贝到内核。

 

  3、epoll:以下的系统调用用来创建和管理epoll实例。

  1)epoll_create():创建一个epoll实例,并返回其文件描述符。

int epoll_create(int size);

  size参数最初是为了告诉内核调用者希望添加到epoll实例的文件描述符数,以此作为内核分配给相关数据结构内存空间大小的参考。自Linux 2.6.8起,size参数被忽略,但必须大于0以保证向后兼容(新的epoll应用运行在旧的内核上时)。内核会根据使用情况动态调整这部分内存空间的大小。

  2)epoll_ctl():在epfd指代的epoll实例上执行控制操作。

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

  op参数的可能值:EPOLL_CTL_ADD(将fd注册到epfd,并关联event和fd)、EPOLL_CTL_MOD(改变fd所关联的事件)、EPOLL_CTL_DEL(从epfd中删除fd。此时event可置为NULL)。

  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(指定文件描述符可读)、EPOLLOUT(可写)、EPOLLPRI(有紧急数据可读)、EPOLLET(使用边缘触发模式)等。

  当前注册到一个epoll实例的文件描述符的集合有时也被称为epoll set。

  3)epoll_wait():在epoll文件描述符epfd上等待I/O事件。

// 成功时返回I/O就绪的文件描述符数量,超时时返回0,出错时返回-1并设置errno
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

  events指向的结构包含了epoll_wait()返回时的就绪fd(最多maxevents个)。

  timeout参数指定epoll_wait()阻塞的最小毫秒数。-1表示epoll_wait()会永远等待;0表示立即返回。

  epoll的事件分发接口支持水平触发模式(LT,默认)和边缘触发模式(ET)。两者的区别如下:

   假设以下情景:管道读端的文件描述符rfd被注册到epoll实例 -> 写端往管道写2KB数据 -> epoll_wait()将rfd作为就绪的文件描述符返回 -> 管道读端从rfd读取1KB数据 -> 再次调用epoll_wait()。如果刚开始将rfd加入epoll实例时指定了EPOLLET(边缘触发),则尽管输入缓存区还有数据可读,最后一步的epoll_wait()还是会阻塞,直到下一次的事件发生。如果没有指定EPOLLET,则采用默认的水平触发模式,这种情况下只要有就绪的输入数据就会一直通知可读,即最后一步的epoll_wait()将立即返回rfd。

  因此,使用ET模式时,一般将相应的fd设置为非阻塞,并在epoll_wait()返回该fd时由我们自己判断何时停止读/写。比如,每次获取到一个读就绪的fd时,循环读取该fd直到出现EAGAIN(当一个fd设置了O_NONBLOCK,而对它的读操作将不得不阻塞时,返回EAGAIN),以读取所有就绪的输入数据。否则,未读完的数据会留到下一次epoll_wait()返回该fd时才读取。容易验证,信号驱动式I/O是边缘触发模式。

  而使用LT模式时,epoll只是一个更快的poll(),它们有相同的语意,epoll可以使用在poll()使用的任何场合。容易验证,select()和poll()都是水平触发模式。与ET模式相比,这种模式的编码较为简单,不过系统调用次数更多。

  epoll和select()/poll()的比较:

  1)epoll在需要监控大量文件描述符时有很好的扩展性。

  2)epoll不需要在每次调用时都把关注的fd和events设置一遍,也不用每次都把关注的fd的epoll_event结构拷贝到内核。

  一个例子:

// 省略了非关键代码和错误处理代码
#define MAX_EVENTS 10

void do_use_fd(int fd)
{
    while (true) { /* 循环读到EAGAIN */ }
}

int main()
{
    int epollfd = epoll_create(10);

    // 监听套接字使用默认的水平触发模式
    struct epoll_event ev, events[MAX_EVENTS];
    ev.events = EPOLLIN;
    ev.data.fd = listen_sock;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev);

    for (;;)
    {
        nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
        for (int n = 0; n < nfds; ++n) 
        {
            if (events[n].data.fd == listen_sock) 
            {
                conn_sock = accept(listen_sock, (struct sockaddr *) &local, &addrlen);
                // 将已连接套接字设置为非阻塞方式,使用边缘触发模式
                setnonblocking(conn_sock);
                ev.events = EPOLLIN | EPOLLET;
                ev.data.fd = conn_sock;
                epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev);
            } 
            else 
            {
                do_use_fd(events[n].data.fd);
            }
        }
    }

    return 0;
}

 

 

 

 

  参考资料:

  《Unix网络编程 卷1:套接字联网API》

  http://www.cnblogs.com/Anker/p/3265058.html

 

 

 

 

 

 

不断学习中。。。

你可能感兴趣的:(select)