深入理解select、poll和epoll

参考内容:
一角钱技术:彻底理解 IO 多路复用实现机制
lylhw13_:Linux 内核驱动 poll 函数解析
云栖号资讯小哥:浅谈select,poll和epoll的区别
firecat全宏:我读过的最好的epoll讲解
KiteRunner24:Linux epoll模型详解及源码分析
不董的编程之路:IO多路复用——深入浅出理解select、poll、epoll的实现
wulei_rita:select函数详解
Linux高性能服务器编程
Unix网络编程
TCP/IP网络编程
Manua pages
qq_53398102:事件触发模式 LT ET
For Nine:触发EPOLLIN 和 EPOLLOUT的所有情况

概述

select、poll和epoll均属于实现IO多路复用的方法,IO多路复用可以在一个线程中监听多个文件描述符,一旦某个描述符就绪,能够通知程序进行相应的操作。

select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间

select

实现机制

当select方法被调用:

  1. 将fd_set数组从用户空间拷贝到内核空间(内核/用户空间的内存拷贝),也就是向操作系统传递监视对象的信息
  2. 内核轮询检查fd_set数组 【内核调用poll机制(内核poll函数,而非IO多路复用的poll)】 直到有一个fd对应的事件产生(标记fd_set数组),或者超时,select方法返回
  3. 将修改后的fd_set数组从内核空间拷贝回用户空间,用户态再遍历fd_set数组找到发生事件的(被标记的)fd

poll机制:如果条件不满足,休眠指定的时间,休眠时间内条件满足唤醒,条件一直不满足时间到达自动唤醒。内核驱动 poll 函数是支撑 poll,epoll 和 select 内核函数的底层机制,它可以查询一个或多个文件描述符的读写状态;poll 函数可以返回一个基于位的mask 值,用来表示是否可以无阻塞的读或写,并且能够在可读或写的时候唤醒相关休眠进程;如果将poll 函数置为 NULL,表示该设备可以无阻塞的读和写

特点

  • 最大并发数限制:由于一个进程所打开的fd(文件描述符)是有限制的,由FD_SETSIZE设置,默认值是1024/2048,因此,select模型的最大并发数就被限制了
  • 内存拷贝:实现机制中的1和3,需要将整个fd_set数组进行拷贝
  • 效率:实现机制中的2和3,需要遍历整个fd_set数组

由于内存拷贝和效率问题,IO性能随着监听的文件描述的数量增长而下降

函数接口及示例

  1. select监视事件:读数据、写数据、异常
  2. select步骤:
    • 设置文件描述符(fd_set),指定监视范围,设定超时(timeval,每次循环需要更新(重设)该值)
    • 调用select(返回-1:发送错误;返回0,超时;返回大于0:该值为发生事件的文件描述符)
    • 查看并处理调用结果
  3. fd_set数组(结构体)
    • 最左端的位表示文件描述符0所在的位置
    • 某位为1,则代表该文件描述符为监视对象
    • fd_set变量的操作是以位为单位进行,需要利用宏操作进行处理

函数接口

#include <sys/select.h>
#include <sys/time.h>

// 数据结构 
typedef struct {
    unsigned long fds_bits[__FDSET_LONGS];
} fd_set;

struct timeval {
	time_t      tv_sec;         /* seconds */
	suseconds_t tv_usec;        /* microseconds */
};

// API
int select(
	int nfds,  // 监视的文件描述符范围
	fd_set *readfds, 
	fd_set *writefds,
    fd_set *exceptfds, 
    struct timeval *timeout
)                              // 返回值就绪描述符的数目

// 宏操作
void FD_ZERO(int fd, fd_set* fds)   // 清空指定集合中所有文件描述符
void FD_SET(int fd, fd_set* fds)    // 将给定的描述符加入集合
int FD_ISSET(int fd, fd_set* fds)  // 判断指定描述符是否在集合中 
void FD_CLR(int fd, fd_set* fds)    // 将给定的描述符从文件中删除  

示例

#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>

int main(void)
{
    fd_set rfds;
    struct timeval tv;
    int retval;

    // 监测标准输入0 stdin 
    FD_ZERO(&rfds);
    FD_SET(0, &rfds);

    // 设置超时时间
    tv.tv_sec = 5;
    tv.tv_usec = 0;
	
	// 调用select
    retval = select(1, &rfds, NULL, NULL, &tv);

    if (retval == -1)
        perror("select()");
    else if (retval)
        printf("Data is available now.\n");
        /* FD_ISSET(0, &rfds) will be true. */
    else
        printf("No data within five seconds.\n");

	exit(EXIT_SUCCESS);
}

图示

I/O多路复用-select

poll

poll本质上和select没有区别, 但是它没有最大并发数限制,原因是它是基于链表来存储文件描述符

epoll

epoll模型(可以理解为 event poll)采用被动通知,也就是当有事件发生时,被动接受通知,基于事件驱动,时间复杂度降低到了O(1)

实现机制

  1. 调用epoll_create时:
    创建epoll例程(返回文件描述符,内核在epoll文件系统里建立file节点),在内核里建立红黑树,用于存储需要监听的文件描述符,建立list就绪链表,用于存储就绪事件
  2. 调用epoll_ctl时(以EPOLL_CTL_ADD为例,注册监视对象的文件描述符):
    将需要监控的文件描述符添加至红黑树(若已经存在则立即返回),向内核中断处理程序注册回调函数,用于当中断事件来临时(就绪事件发生),向就绪链表插入该文件描述符
  3. 调用epoll_wait时:
    检查list就绪链表,若有数据则直接返回事件数,并通过参数struct epoll_event * events将就绪事件从内核空间通过内存拷贝到用户空间(只负责拷贝,epoll_event指针所指内存需要用户自己提前分配好);若没有则sleep直到超时返回

函数接口及示例

函数接口

epoll为Linux下特有

#include <sys/epoll.h>

// 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
struct eventpoll {
    /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
    struct rb_root  rbr;
    /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
    struct list_head rdlist;
};

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 */
}


// API
int epoll_create(   
	int size        // 从Linux 2.6.8起,该参数将被忽略,但必须大于0
);  // 创建epoll实例,并返回其文件描述符

int epoll_ctl(
	int epfd, 
	int op,        //   EPOLL_CTL_ADD、EPOLL_CTL_MOD、 EPOLL_CTL_DEL(DEL时,event可以设置为NULL被忽略)
	int fd, 
	struct epoll_event *event
);  // epoll_ctl 将 文件描述符 增加、修改、删除到epoll实例中,成功返回0,发生错误返回-1并设置errno

int epoll_wait(
	int epfd, 
	struct epoll_event * events, 
	int maxevents, 
	int timeout
);  // 等待I/O事件,若没有则阻塞调用线程(可以认为从epoll实例的就绪链表中获取项目)
	// 阻塞直到:
	// - 文件描述符传递事件
	// - 被信号处理程序中断
	// - 超时
	

epoll_event的成员events可以保存的常量及所指的事件类型:

  • EPOLLIN:表示对应的文件描述符可读
    • EPOLLIN事件触发:
      • 有新数据到达(或有新的连接请求)
      • 对端socket正常关闭
      • (重新注册EPOLLIN事件)当读 buff 有数据可读时,不进行处理,调用epoll_ctl将fd重新注册到epoll事件池,这时也会触发EPOLLIN事件
  • EPOLLOUT:表示对应的文件描述符可写
    • EPOLLOUT事件触发:
      • 客户端connect上服务端后,得到fd,这时候把fd添加到epoll 事件池里面后,因为连接可写,会触发EPOLLOUT事件
      • 缓冲区从满到不满,会触发EPOLLOUT事件
      • (重新注册EPOLLOUT事件)如果当连接可用后,且缓存区不满的情况下,调用epoll_ctl将fd重新注册到epoll事件池(使用EPOLL_CTL_MOD),这时也会触发EPOLLOUT事件
  • EPOLLPRI:表示对应的文件描述符有紧急数据可读(带外数据)
  • EPOLLERR:表示对应的文件描述符发生错误
  • EPOLLHUP:表示对应的文件描述符被挂断
  • EPOLLET:将EPOLL设为边缘触发(Edge Triggered)
  • EPOLLONESHOT:发生一次事件后,相应的文件描述符不再收到事件通知。若需要监听,还需再次设置

关于ET模式、非阻塞套接字设置等,请参考我的另一篇文章
EPOLLET和EPOLLONESHOT

示例

#define EPOLL_SIZE 10
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;

/* Code to set up listening socket, 'listen_sock',
   (socket(), bind(), listen()) omitted */

epollfd = epoll_create(EPOLL_SIZE);
if (epollfd == -1) {
    perror("epoll_create1");
    exit(EXIT_FAILURE);
}

ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
    perror("epoll_ctl: listen_sock");
    exit(EXIT_FAILURE);
}

for (;;) {
    nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
    if (nfds == -1) {
        perror("epoll_wait");
        exit(EXIT_FAILURE);
    }
	// 遍历就绪事件
    for (n = 0; n < nfds; ++n) {
        if (events[n].data.fd == listen_sock) {
        	// 建立连接
            conn_sock = accept(listen_sock,
                               (struct sockaddr *) &addr, &addrlen);
            if (conn_sock == -1) {
                perror("accept");
                exit(EXIT_FAILURE);
            }
            // 设置套接字为非阻塞
            setnonblocking(conn_sock);
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = conn_sock;
            if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                        &ev) == -1) {
                perror("epoll_ctl: conn_sock");
                exit(EXIT_FAILURE);
            }
        } else {
        	// 处理事件
            do_use_fd(events[n].data.fd);
        }
    }
}

特点

epoll高效的本质在于:

  • 减少了用户态和内核态的文件描述符(句柄)的拷贝
  • 减少了对可读可写文件描述符的遍历
  • IO性能不会随着监听的文件描述的数量增长而下降
  • 使用红黑树存储fd,以及对应的回调函数,其插入,查找,删除的性能不错

图示

I/O多路复用-epoll

其他

  • 在select和poll方法中,内核都没有为fd准备存放其的数据结构,只是简单粗暴地把数组或者链表复制进来;而epoll则不一样,epoll_create会在内核建立一颗专门用来存放fd结点的红黑树,后续如果有新增的fd结点,都会注册到这个epoll红黑树上。

  • epoll相比于select并不是在所有情况下都要高效。例如一些简单场景(内网通信等),连接数较少,但活跃度高的情况等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值