linux IO复用(epoll)小记

一、epoll简介

epoll是Linux内核为处理大批量文件描述符而作了改进的poll, 是Linux下多路复用IO接口select/poll的增强版本, 它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候, 它无须遍历整个被侦听的描述符集, 只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

二、epoll的API函数

1. 句柄创建函数

int epoll_create(int size);

创建一个epoll的句柄, size用来告诉内核这个监听的数目一共有多大。

int epoll_create1(int flag);

这个函数是在linux 2.6.27中加入的, 其实它和epoll_create差不多, 不同的是epoll_create1函数的参数是flag。

当flag是0时, 表示和epoll_create函数完全一样, 不需要size的提示了。

当flag = EPOLL_CLOEXEC, 创建的epfd会设置FD_CLOEXEC, 它是fd的一个标识说明, 用来设置文件close-on-exec状态的。

当flag = EPOLL_NONBLOCK, 创建的epfd会设置为非阻塞。

2. 事件操作函数

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

第一个参数epfd, 为epoll_create返回的的epoll文件描述符。

第二个参数op表示操作值。有三个操作类型:

EPOLL_CTL_ADD  //注册目标fd到epfd中, 同时关联内部event到fd上
 
EPOLL_CTL_MOD  //修改已经注册到fd的监听事件
 
EPOLL_CTL_DEL  //从epfd中删除/移除已注册的fd, event可以被忽略, 也可以为NULL

第三个参数fd表示需要监听的fd。

第四个参数event表示需要监听的事件。

event参数是一个枚举的集合, 可以用“|”来增加事件类型, 枚举如下:

// EPOLLIN: 表示关联的fd可以进行读操作了。
// EPOLLOUT: 表示关联的fd可以进行写操作了。
// EPOLLRDHUP(since Linux 2.6.17): 表示套接字关闭了连接, 或者关闭了正写一半的连接。
// EPOLLPRI: 表示关联的fd有紧急优先事件可以进行读操作了。
// EPOLLERR: 表示关联的fd发生了错误, epoll_wait会一直等待这个事件, 所以一般没必要设置这个属性。
// EPOLLHUP: 表示关联的fd挂起了, epoll_wait会一直等待这个事件, 所以一般没必要设置这个属性。
// EPOLLET: 设置关联的fd为ET的工作方式, epoll的默认工作方式是LT。
// EPOLLONESHOT(since Linux 2.6.2): 设置关联的fd为one-shot的工作方式。表示只监听一次事件, 如果要再次监听, 需要把socket放入到epoll队列中。

3. 事件等待函数

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout,  const sigset_t *sigmask);

上面两个函数的参数含义:

第一个参数:表示epoll_wait等待epfd上的事件。

第二个参数:events指针携带有epoll_data_t数据。

第三个参数:maxevents告诉内核events有多大, 该值必须大于0。

第四个参数:timeout表示超时时间(单位: 毫秒), 为0的时候表示马上返回, 为-1的时候表示一直等下去, 直到有事件返回, 为任意正整数的时候表示等这么长的时间, 如果一直没有事件, 则返回。一般情况下, 如果网络主循环是单独的线程的话, 可以用-1来等, 这样可以保证一些效率, 如果是和主逻辑在同一个线程的话, 则可以用0来保证主循环的效率。

epoll_pwait(since linux 2.6.19)允许一个应用程序安全的等待, 直到fd设备准备就绪, 或者捕获到一个信号量。其中sigmask表示要捕获的信号量。

函数如果等待成功, 则返回fd的数字; 0表示等待fd超时, 其他错误号请查看errno。

4. 句柄关闭函数

int close(int fd);

返回值: 若文件顺利关闭则返回0, 发生错误时返回-1。

三、epoll的2种触发模式

1. Level Triggered (LT) 水平触发

LT是epoll默认的触发方式, 如下:

socket接收缓冲区不为空, 有数据可读, 则读事件一直触发;

socket发送缓冲区不满, 可以继续写入数据, 则写事件一直触发;

LT的处理过程:

accept一个连接, 添加到epoll中监听EPOLLIN事件;

当EPOLLIN事件到达时, read fd中的数据并处理;

当需要写入数据时, 先直接把数据write到fd中; 如果数据较大, 无法一次性写入, 那么在epoll中监听EPOLLOUT事件;

当EPOLLOUT事件到达时, 继续把数据write到fd中; 如果数据写入完毕, 那么在epoll中关闭EPOLLOUT事件;

2. Edge Triggered (ET) 边沿触发

socket的接收缓冲区状态变化时触发读事件, 即空的接收缓冲区刚接收到数据时触发读事件;

socket的发送缓冲区状态变化时触发写事件, 即满的缓冲区刚空出空间时触发读事件;

仅在状态变化时触发事件

ET的处理过程:

accept一个连接, 添加到epoll中监听EPOLLIN|EPOLLOUT事件;

当EPOLLIN事件到达时, read fd中的数据并处理, read需要一直读, 直到返回EAGAIN为止;

当需要写出数据时, 把数据write到fd中, 直到数据全部写完, 或者write返回EAGAIN;

当EPOLLOUT事件到达时, 继续把数据write到fd中, 直到数据全部写完, 或者write返回EAGAIN;

ET模式下, 正确的accept要考虑2个问题:

(1) 阻塞模式下, accept存在的问题

考虑这种情况: TCP连接被客户端夭折, 即在服务器调用accept之前, 客户端主动发送RST终止连接, 导致刚刚建立的连接从就绪队列中移出, 如果套接口被设置成阻塞模式, 服务器就会一直阻塞在accept调用上, 直到其他某个客户建立一个新的连接为止。但是在此期间, 服务器单纯地阻塞在accept调用上, 就绪队列中的其他描述符都得不到处理。

解决办法是把监听套接口设置为非阻塞, 当客户在服务器调用accept之前中止某个连接时, accept调用可以立即返回-1, 这时源自Berkeley的实现会在内核中处理该事件, 并不会将该事件通知给epoll, 而其他实现把errno设置为ECONNABORTED或者EPROTO错误,我们应该忽略这两个错误。

(2)ET模式下accept存在的问题

考虑这种情况: 多个连接同时到达, 服务器的TCP就绪队列瞬间积累多个就绪连接, 由于是边缘触发模式, epoll只会通知一次, accept只处理一个连接, 导致TCP就绪队列中剩下的连接都得不到处理。

解决办法是用while循环抱住accept调用, 处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢? accept返回-1并且errno设置为EAGAIN就表示所有连接都处理完。

综合以上两种情况, 服务器应该使用非阻塞地accept, accept在ET模式下的正确使用方式为:

while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) {
    handle_client(conn_sock);
}
if (conn_sock == -1) {
    if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR)
    perror("accept");
}

3. 总结

从ET的处理过程中可以看到, ET的要求是需要一直读写, 直到返回EAGAIN, 否则就会遗漏事件。而LT的处理过程中, 直到返回EAGAIN不是硬性要求, 但通常的处理过程都会读写直到返回EAGAIN, 但LT比ET多了一个开关EPOLLOUT事件的步骤。LT的编程与poll/select接近,符合一直以来的习惯,不易出错。ET的编程可以做到更加简洁,某些场景下更加高效,但另一方面容易遗漏事件,容易产生bug。

时间: 2024-12-18 23:46:58

linux IO复用(epoll)小记的相关文章

[Z] linux基础编程:IO模型:阻塞/非阻塞/IO复用 同步/异步 Select/Epoll/AIO

原文链接:http://blog.csdn.net/colzer/article/details/8169075 IO概念 Linux的内核将所有外部设备都可以看做一个文件来操作.那么我们对与外部设备的操作都可以看做对文件进行操作.我们对一个文件的读写,都通过调用内核提供的系统调用:内核给我们返回一个file descriptor(fd,文件描述符).而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符).描述符就是一个数字,指向内核中一个结构体(文件路径,数据

linux基础编程:IO模型:阻塞/非阻塞/IO复用 同步/异步 Select/Epoll/AIO(转载)

IO概念 Linux的内核将所有外部设备都可以看做一个文件来操作.那么我们对与外部设备的操作都可以看做对文件进行操作.我们对一个文件的读写,都通过调用内核提供的系统调用:内核给我们返回一个file descriptor(fd,文件描述符).而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符).描述符就是一个数字,指向内核中一个结构体(文件路径,数据区,等一些属性).那么我们的应用程序对文件的读写就通过对描述符的读写完成. linux将内存分为内核区,用户区.l

《深入理解计算机系统》Tiny服务器4——epoll类型IO复用版Tiny

前几篇博客分别讲了基于多进程.select类型的IO复用.poll类型的IO复用以及多线程版本的Tiny服务器模型,并给出了主要的代码.至于剩下的epoll类型的IO复用版,本来打算草草带过,毕竟和其他两种IO复用模型差不太多.但今天在看Michael Kerrisk的<Linux/UNIX系统编程手册>时,看到了一章专门用来讲解epoll函数,及其IO复用模型.于是,自己也就动手把Tiny改版了一下.感兴趣的同学可以参考上述手册的下册1113页,有对于epoll比较详细的讲解. 前边针对IO

Linux中的IO复用接口简介(文件监视?)

I/O复用是Linux中的I/O模型之一.所谓I/O复用,指的是进程预先告诉内核,使得内核一旦发现进程指定的一个或多个I/O条件就绪,就通知进程进行处理,从而不会在单个I/O上导致阻塞. 在Linux中,提供了select.poll.epoll三类接口来实现I/O复用. select函数接口 select中主要就是一个select函数,用于监听指定事件的发生,原型如下: 12345 #include<sys/select.h>#include<sys/time.h>int sele

Linux网络编程-IO复用技术

IO复用是Linux中的IO模型之一,IO复用就是进程预先告诉内核需要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程进程处理,从而不会在单个IO上阻塞了.Linux中,提供了select.poll.epoll三种接口函数来实现IO复用. 1.select函数 #include <sys/select.h> #include <sys/time.h> int select(int nfds, fd_set *readfds, fd_set *writef

LINUX网络编程 IO 复用

参考<linux高性能服务器编程> LINUX下处理多个连接时候,仅仅使用多线程和原始socket函数,效率十分低下 于是就出现了selelct poll  epoll等IO复用函数. 这里讨论性能最优的epoll IO复用 用户将需要关注的socket连接使用IO复用函数放进一个事件表中,每当事件表中有一个或者多个SOCKET连接出现读写请求时候,则进行处理 事件表使用一个额外的文件描述符来标识.文件描述符使用 epoll_create函数创建 #inlclude <sys/epoll

Linux IO模式及 select、poll、epoll详解

注:本文是对众多博客的学习和总结,可能存在理解错误.请带着怀疑的眼光,同时如果有错误希望能指出. 同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的上下文下给出的答案是不同的.所以先限定一下本文的上下文. 本文讨论的背景是Linux环境下的network IO. 一 概念说明 在进行解释之前,首先要说明几个概念: - 用户空间和内核空间 - 进程切换 - 进程的阻塞 - 文件描述符 - 缓存 I/O 用户空间与内核空间 现在操作系统都是采用虚拟存储器,那么对32

IO复用之——epoll

一. 关于epoll 对于IO复用模型,前面谈论过了关于select和poll函数的使用,select提供给用户一个关于存储事件的数据结构fd_set来统一监测等待事件的就绪,分为读.写和异常事件集:而poll则是用一个个的pollfd类型的结构体管理事件的文件描述符和事件所关心的events,并通过结构体里面的输出型参数revents来通知用户事件的就绪状态: 但是对于上述两种函数,都是需要用户遍历所有的事件集合来确定到底是哪一个或者是哪些事件已经就绪可以进行数据的处理了,因此当要处理等待的事

IO复用(Reactor模式和Preactor模式)——用epoll来提高服务器并发能力

上篇线程/进程并发服务器中提到,提高服务器性能在IO层需要关注两个地方,一个是文件描述符处理,一个是线程调度. IO复用是什么?IO即Input/Output,在网络编程中,文件描述符就是一种IO操作. 为什么要IO复用? 1.网络编程中非常多函数是阻塞的,如connect,利用IO复用可以以非阻塞形式执行代码. 2.之前提到listen维护两个队列,完成握手的队列可能有多个就绪的描述符,IO复用可以批处理描述符. 3.有时候可能要同时处理TCP和UDP,同时监听多个端口,同时处理读写和连接等.