关于 select、poll、epoll 的区别

2019-07-13  本文已影响0人  _给我一支烟_

1. 函数说明

1.1 select

/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

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

void FD_ZERO(fd_set *set);          //清空集合
void FD_CLR(int fd, fd_set *set);   //将一个给定的文件描述符从集合中删除
void FD_SET(int fd, fd_set *set);   //将一个给定的文件描述符加入集合之中
int  FD_ISSET(int fd, fd_set *set); //检查集合中指定的文件描述符是否可以读写 

struct timeval {
   long    tv_sec;         /* seconds 秒*/
   long    tv_usec;        /* microseconds 微妙*/
};

该函数准许进程指示内核等待多个事件中的任何一个发生,并在有一个或多个事件发生或经历一段指定的时间后才被唤醒。

1.2 poll

#include <poll.h>

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

struct pollfd {
    int   fd;         /* file descriptor 文件描述符 */
    short events;     /* requested events 等待的事件 */
    short revents;    /* returned events 实际发生了的事件 */
};

每一个 struct pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,让 poll() 监视多个文件描述符。每个结构体的 events 域是监视该文件描述符的事件掩码,由用户来设置这个域。revents 域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events 域中请求的任何事件都可能在 revents 域中返回。

合法的事件如下:

常量 说明
POLLIN 普通或优先级带数据可读
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级数据可读
POLLOUT 普通数据可写
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件

POLLIN | POLLPRI 等价于 select() 的读事件
POLLOUT 等价于 select() 的写事件

① timeout 参数指定等待的毫秒数,在超时之前有 IO 事件就绪或者超时,poll 都会返回
② timeout 为负数值,表示一直阻塞直到一个指定事件发生,poll 才返回。
③ timeout 为0,poll 调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。

返回值和错误代码
成功时:poll() 返回结构体中 revents 域不为0的文件描述符个数。
超时后:如果在超时前没有任何事件发生,poll()返回0。
失败时:poll() 返回-1,并设置errno为下列值之一

EFAULT    指针指向的地址超出进程的地址空间。
EINTR    请求的事件之前产生一个信号,调用可以重新发起。
EINVAL    参数超出 PLIMIT_NOFILE 值。
ENOMEM   可用内存不足,无法完成请求。

1.3 epoll

#include <sys/epoll.h>

int epoll_create(int size);
int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

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

(1)创建 epoll 实例
现在一般使用 epoll_create1(EPOLL_CLOEXEC),原因:

(2)epoll 的事件注册
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
先注册要监听的事件类型。
第一个参数是 epoll_create1 的返回值
第二个参数表示动作,用三个宏来表示:

EPOLL_CTL_ADD:注册新的 fd 到 epfd 中
EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件
EPOLL_CTL_DEL :从 epfd 中删除一个 fd

第三个参数是需要监听的 fd
第四个参数是告诉内核需要监听什么事,struct epoll_event 结构见上面代码
events 可以是以下几个宏的集合:

常量 说明
EPOLLIN 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT 表示对应的文件描述符可以写
EPOLLPRI 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR 表示对应的文件描述符发生错误
EPOLLHUP 表示对应的文件描述符被挂断
EPOLLET 将 EPOLL 设为边缘触发 (Edge Triggered) 模式,这是相对于水平触发 (Level Triggered) 来说的,LT是缺省的工作方式
EPOLLONESHOT 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个 socket 加入到 EPOLL 队列里

LT (Level Triggered) 水平触发:默认的模式。内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你。

ET (Edge Triggered) 边缘触发:“高速”模式。内核只会提示一次,直到下次再有数据流入之前都不会再提示了,无论 fd 中是否还有数据可读。在ET模式下,read一个fd的时候一定要把数据读完。

(3)等待事件的产生
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
等待事件的产生,类似于 select() 调用。
参数 events 用来从内核得到事件的集合
参数 maxevents 告之内核这个 events 有多大
参数 timeout 是超时时间(毫秒,0会立即返回,-1一直阻塞直到有IO事件就绪)
该函数返回需要处理的事件数目,如返回0表示已超时。

2. 三者之间的关联和区别

关联

select,poll,epoll 都是 I/O 多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。

区别

关于最大连接数先理清楚2个概念:
⑴ 一个进程能打开的最大文件描述符,使用 ulimit -n 或者 cat /proc/进程号/limits |grep "Max open file"
❶ 使用 ulimit -n xxx 修改(只针对当前session有效),
❷ 通过 setrlimit 系统调用修改(只对当前进程有效),
❸ 修改 /etc/security/limits.conf 在该文件中添加以下两行:

    *      soft    nofile     100000
    *      hard    nofile     100000

这个值是可以修改的,但是最大不要超过系统所能打开的最大数,超过了也没有什么意义。

⑵ 一个系统所能打开的最大数也是有限的,跟内存大小有关,可以通过cat /proc/sys/fs/file-max 查看。

> 支持一个进程所能打开的最大连接数
> 连接数剧增后带来的 IO 效率问题

epoll 如何实现只处理活跃连接
epoll实现了eventpoll数据结构
数据结构中rdlist将活跃连接存储在链表中,当网卡发送报文时,增加节点,当读取一个事件后,链表删除节点,需要得到活跃连接就只需要遍历链表
数据结构中rdr使用红黑树(自平衡二叉树)将事件存储,例如:当有读事件时,就新增节点,事件复杂度为logN

一般来说编写高并发服务器程序都会首先 epoll 因为这种环境下其性能最好,但是在连接数少并且连接都十分活跃的情况下,select 和 poll 的性能可能比 epoll 好,毕竟 epoll 的通知机制需要很多函数回调。

上一篇下一篇

猜你喜欢

热点阅读