Linux高性能服务器编程:I/O复用

1. select系统调用

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

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

nfds:指定被监听的文件描述符的总数。

readfds、writefds、exceptfds分别指向可读、可写和异常等事件对应的文件描述符集合。

FD_ZERO 清除fdset所有位

FD_SET  设置fdset的位fd

FD_CLR  清除fdset的位fd

FD_ISSET fdset的位fd是否设置

select成功时返回就绪(可读、可写和异常)文件描述符的总数。

 

可读:

内核接收缓冲区的字节数大于或等于其低水位标记SO_RCVLOWAT.可以无阻塞的读该socket。

socket通信的对方关闭连接,此时对该socket操作将返回9

监听socket上有新的连接请求

socket上有未处理的错误

 

可写:

socket内核发送缓冲区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT,可以无阻塞地写该socket。

socket写操作被关闭,对写操作关闭的socket执行写操作会触发一个SIGPIPE信号。

socket使用非阻塞connect连接成功或者失败之后。

socket上有未处理的错误。

 

2. poll调用

和select系统调用类似,在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。

int poll(struct pollfd* fds, nfds_t nfds, int timeout);

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

struct pollfd {

    int fd;  //文件描述符

    short events;  // 注册的事件

    short revents; //实际发生的事件,有内核填充

};

events告诉poll监听fd上的哪些事件。

POLLIN: 数据可读(普通和优先数据)

POLLRDNORM: 普通数据可读

POLLOUT:数据可写(普通和优先数据)

 

3. epoll系统调用

epoll使用一组函数来完成任务。epoll把用户关心的文件描述符上的事件放在内核的一个事件表中,无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。

int epoll_create(int size);

返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。

 

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

op参数指定操作类型:

EPOLL_CTL_ADD: 往事件表中注册fd上的事件。

EPOLL_CTL_MOD: 修改fd上的事件。

EPOLL_CTL_DEL: 删除fd上的注册事件。

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

 

epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout); 在一段时间内等待一组文件描述符上的事件。

epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表中(由epfd参数指定)复制到它的第二个参数events指向的数组中。此数组只用于输出epoll_wait检测到的就绪事件,而不像

poll和select的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。

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;
        //处理socket
    }
}

//epoll
int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
for (int i = 0; i < ret; i++) {
    int sockfd = events[i].data.fd;
    //sockfd肯定准备就绪
}

 

 

LT和ET模式

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

ET:对于采用ET模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知给应用程序后,应用程序必须立即处理该事件,后续的epoll_wait调用将不再向应用程序通知这一事件。(注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式操作该文件描述符), 每个使用ET模式的文件描述符都应该是非阻塞的,如果文件描述符是阻塞的,那么读或写操作将会因为没有后续的事件而一直处于阻塞状态。

 

EPOLLONESHOT:对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次。当一个线程在处理某个socket时,其他线程是不能有机会操作该socket的。

 

4.三组I/O复用函数的比较

select和poll都只能工作在相对低效的LT模式,而epoll则可以工作在ET高效模式。并且epoll还支持EPOLLONESHOT事件。

实现原理上看:select和poll采用轮询的方式,每次调用都要扫描整个注册文件描述符集合,并将其中就绪的文件描述符返回给用户程序,时间复杂度为O(n)。

epoll_wait采用回调的方式,内核检测到就绪的文件描述,将触发回调函数,回调函数就将该文件描述符上对应的事件插入内核就绪事件队列,内核最后在适当的时候将该就绪事件队列中的内容拷贝到用户空间,时间复杂度为O(1)。

 

5. I/O复用的高级应用:非阻塞的connect

connect连接出错时的一种errno值为EINPROGRESS,这种错误发生在对非阻塞的socket调用connect,而连接有没有建立时,这种情况下,可以调用select、poll等函数来监听这个连接失败的socket上的可写事件。当select、poll等函数返回后,

再利用getsockopt来读取错误码并清除该socket上的错误,如果错误码是0,表示连接成功建立,否则连接失败。

 

posted @ 2020-02-28 18:46  c++11  阅读(279)  评论(0编辑  收藏  举报