[原]Linux服务器--I/O复用(select、poll、epoll)

闫钰晨 17/08/04 15:11:45

一、select系统调用

1.selectAPI

原型:

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

【1】nfds参数指定被监听的文件描述符的总数。

【2】readfds,writefds,exceptfds分别指向可读,可写和异常等事件对应的文件描述符集合。通过这三个参数传入自己感兴趣的文件描述符;select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。三个参数都是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 (8*(int)sizeof(__fd_mask))
typedef struct
{
#ifdef__USE_XOPEN
__fd_maskfds_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能容纳的文件描述符由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。
由于位操作太繁琐,可以使用下列的宏来访问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_ISDET (int fd,fd_set *fdset);       //测试fdset的位fd是否被设置

【3】timeout参数用来设置select函数的超时时间。内核将修改它以告诉应用程序select等待了多久。

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

给timeout变量的两个成员都传递0,则select将立即返回。如果给timeout传递NULL,select将一直阻塞,直到某个文件描述符就绪。

【4】select成功时返回就绪(可读、可写、异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0。失败返回-1并设置errno。如果在等待期间程序接收到信号,则select立即返回-1,并设置errno为EINTR.

2.文件描述符就绪条件

【1】socket可读:

  • socket内核接收缓存区中的字节数大于或等于其低水位标记SO_RECVLOWAT.
  • socket通信的对方关闭连接。此时对该socket的读操作返回0.
  • 监听socket上有新的连接请求.
  • socket上有未处理的错误.

【2】socket可写:

  • socket 内核发送缓存区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT.
    socket的写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号.
    socket使用非阻塞connect连接成功或失败(超时)之后.
    socket上有未处理的错误.

网络程序中,select能处理的异常情况只有一种:socket上接收到带外数据

二、poll系统调用

原型:

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

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

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

fd成员指定文件描述符;events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位或;revents成员则由内核修改,以通知应用程序fd上实际发生了哪些事件。poll支持的事件类型如下:

这里写图片描述

通常,应用程序需要根据recv调用的返回值来区分socket上接收的是有效数据还是对方关闭连接的请求,并做相应的处理。GNU为poll系统调用增加了一个POLLRDHUP事件,它在socket上接收到对方关闭连接的请求之后触发。但使用时,需在代码最开始处定义__GNU__SOUREC.

【2】nfds参数指定被监听事件集合fds的大小,其类型定义如下:

typedef unsigned long int nfds_t;

【3】timeout参数指定poll的超时值,单位是毫秒。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回。
poll系统调用的返回值含义与select相同。

三、epoll系列系统调用

1.内核事件表

【1】epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,所以用一个额外的文件描述符来唯一标识内核中的这个时间表。这个文件描述符使用如下epoll_create函数来创建:

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

size参数现在并不起作用,只是给内核一个提示,告诉它事件需要多大。该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。

【2】操作epoll的内核事件表函数

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
  • fd参数是要操作的文件描述符;
  • op参数指定操作类型,有如下三种:
EPOLL_CTL_ADD       //往事件表中注册fd上的事件             
EPOLL_CTL_MOD       //修改fd上的注册事件
EPOLL_CTL_DEL       //删除fd上的注册事件
  • event参数指定事件,是epoll_event结构指针类型
struct epoll_event  
{  
  uint32_t events;        //epoll事件
  epoll_data_t data;      //用户数据
};  
  • events成员描述事件类型。epoll支持的事件类型和poll基本相同,标识epoll 事件类型的宏是在poll对应的宏前加上“E”。epoll还有两个额外的事件类型——-EPOLLET和EPOLLONESHOT。
  • data成员用于存储用户数据,其类型epoll_data_t定义:
typedef union epoll_data
{
    void *ptr;  
    int fd;  
    uint32_t u32;  
    uint64_t u64;  
} epoll_data_t;  

epoll_ctl成功时返回0,失败返回-1,并设置errno。

2.epoll_wait函数

在一段超时时间内等待一组文件描述符上的时间
原型:

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

成功时返回就绪的文件描述符的个数,失败返回-1并设置errno

  • timeout与poll借口的timeout参数含义相同。
  • maxevents参数指定最多监听多少个事件,必须大于0.
  • epoll_wait检测到事件,就将所有就绪的事件从内核表中复制到它第二个参数events指向的数组中。这个数组只用于输出epoll_wait检测到的就绪事件。

poll和epoll在使用上的区别

poll:
int ret = poll(fds, MAX_EVENT_NUMBER, -1);
for (int i = 0; i < MAX_EVENT_NUMBER; ++i)   //必须遍历一遍已注册的文件描述符
{
    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.LT/ET模式

【1】epoll对文件描述的操作有两种模式:LT(Level Trigger, 电平触发)模式ET(Edge Trigger, 边沿触发)模式

【2】默认工作模式为LT模式,当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll就以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。

【3】LT模式的文件描述符,当epoll_wait检测到其上有事件发生并通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理。而ET模式下,当epoll_wait检测到其上有事件发生并通知应用程序后,应用程序必须立即处理该事件,后续将不再向应用程序通知。ET模式在很大程度上降低了同一个epoll事件被触发的次数

LT/ET模式区别代码

运行上面的代码,传输超过10字节大小的数据,结果如下:
LT模式
这里写图片描述

ET模式
这里写图片描述

4.EPOLLONESHOT模式

即使是ET模式下一个socket上某个事件还是可能被触发多次,比如一个线程在读取完socket上数据后去处理数据时,又有新数据可读,此时就会有另一个线程来读取这些数据,于是现在就是两个线程同时操作一个线程,这种情况就可以用EPOLLONESHOT事件处理
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或异常事件,且只触发一次,除非使用epoll_ctl重置该文件描述符上注册的EPOLLONESHOT事件。

所以注册了EPOLLONESHOT事件的socket,被一个线程处理完后就要立即重置这个socket上的EPOLLONESHOT事件。

作者:yyc794990923 发表于 2017/08/04 15:11:45 原文链接 https://blog.csdn.net/yyc794990923/article/details/76667368
阅读:174