Select_poll_epoll详解
参考链接
epoll函数
注意: epoll不属于任何namespace。
#include <sys/epoll.h>
int epoll_create(int size); // return epollfd, 失败return -1
/*
op:
EPOLL_CTL_ADD
EPOLL_CTL_MOD
EPOLL_CTL_DEL 如果是delete的话, epoll_ctl的最后一个参数event可以是NULL
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 成功return0, 失败return -1
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout); // 成功return nready. 失败return -1
//epoll_event
/*
其实这个epoll_data只是给用户自行使用的,epoll不关心里面的内容。 这个dta回随着epoll_data 返回的epoll_event一并返回
*/
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 */
};
close
其实在外面关闭一个fd之后,就可以不用再在epoll list里面删除了,但是为了安全起见,还是用EPOLL_CTL_DEL删掉吧。详情可以看 epoll(7) man page FAQ。
epoll event
- EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
- EPOLLOUT:表示对应的文件描述符可以写;
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
- EPOLLERR:表示对应的文件描述符发生错误;
- EPOLLHUP:表示对应的文件描述符被挂断;
- EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
EL/LT
有关ET/LT 阻塞/非阻塞的操作,网络上基本都是错的,只要你安排的好,既可以用阻塞,也可以用非阻塞。(linux man page上也让你用阻塞)
ET Edge Trigger 边沿触发工作模式
- 必须使用非阻塞 工作模式,因为在循环调用epoll_wait的时候,有可能某个句柄已知会ready, 如果用阻塞操作,会导致一个文件句柄的阻塞操作把多个文件描述符饿死。
- 基于非阻塞文件句柄
- 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待(退出read/write返回epoll_wait)。但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时(即小于sizeof(buf)),就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
- 阻塞IO的事件处理原则:
-
recv() > 0
:(并且小于请求的数据长度sizeof(buf)
), 表示接收数据完毕,返回值即是接收到的字节数。 -
recv() == 0
: 表示链接已经正常断开,这个时候就可以把fd关掉,从epoll里面移除了 -
recv() < 0 && errno == EAGAIN
: 表示recv操作还未完成 -
recv() < 0 && errno != EAGAIN
: 表示操作遇到系统errno
-
- 边缘触发但是这种模式下在读数据的时候一定要注意,因为如果一次可写事件我们没有把数据读完,如果没有读完,在socket没有新的数据可读时epoll就不回返回了,只有在新的数据到来时,我们才能读取到上次没有读完的数据。最差的情况是client在发送的n个byte之后已经关闭了,但是epoll由于接收缓冲区没有清空,这个fd在服务端并不会关掉。
- 使用ET模式,就算接收缓冲区里的数据没有读完,如果再接收到新的数据, epoll_wait 还是会触发可读事件的。
- 设置为EPOLLET之后仍然会对同一事件多次触发的原因:
- 接收缓冲区过小,无法容纳所有发送过来的数据
- 用EPOLL_CTL_MOD更改了epollevent,会重置之前的触发(这个我自己没有复现出来)
LT Level Trigger 水平触发工作模式
- poll(), select() 都是水平触发
- 如果我们用水平触发不用担心数据有没有读完因为下次epoll返回时,没有读完的socket依然会被返回
- 但是要注意这种模式下的写事件,因为是水平触发,每次socket可写时epoll都会返回,当我们写的数据包过大时,一次写不完,要多次才能写完或者每次socket写都写一个很小的数据包时,每次写都会被epoll检测到,因此长期关注socket写事件会无故cpu消耗过大甚至导致cpu跑满,所以在水平触发模式下我们一般不关注socket可写事件而是通过调用socket write或者send api函数来写socket
- 我们可以看到这种模式在效率上是没有边缘触发高的,因为每个socket读或者写可能被返回两次甚至多次
epoll 源码解析
https://blog.csdn.net/wangyin159/article/details/48895287
epoll_wait
- 检查MAXEXENT参数
- 用access_ok() 检查event指针是否可写,如果这个指针是空指针或者指向内核态的指针,那么会设置errno EFAULT。
- Just because a pointer was supplied by userspace doesn't mean that it's definitely a userspace pointer - in many cases "kernel pointer" simply means that it's pointing within a particular region of the virtual address space.https://stackoverflow.com/questions/12357752/what-is-the-point-of-using-the-linux-macro-access-ok
- 获取epfd对应的eventpoll文件实例,如果取不到,errno:EBADF
- 检查eventpoll文件是不是真的是一个epoll文件, 如果不是说值errno EINVAL
- 其实epoll_wait 中如果出错了,那么基本上应该是程序本身的问题,比如陷入死循环之类
- 调用ep_epoll函数,这个函数在做一些配置之后就会主动让出处理器,进入睡眠状态,等待文件就绪(回调函数唤醒本进程)或者超时或者信号中断
- 缺省的工作模式
一道腾讯后台开发面试题
Q:使用Linux epoll模型,水平(LT)触发模式,当socket可写时,会不停的触发socket可写的事件,如何处理?
- 第一种最普遍的方式:
- 需要向socket写数据的时候才把socket加入epoll,等待可写事件。接受到可写事件后,调用write或者send发送数据。当所有数据都写完后,把socket移出epoll(用EPOLLONESHOT也行)。
- 这种方式的缺点是,即使发送很少的数据,也要把socket加入epoll,写完后在移出epoll,有一定操作代价。
- 一种改进的方式:
- 开始不把socket加入epoll,需要向socket写数据的时候,直接调用write或者send发送数据。如果返回EAGAIN(缓冲区满了,后面还需要继续发),把socket加入epoll,在epoll的驱动下写数据,全部数据发送完毕后,再移出epoll。
- 这种方式的优点是:数据不多的时候可以避免epoll的事件处理,提高效率。
ET/LT 比较
- 因为ET要基于非阻塞IO, LT在读写的时候不必等待EAGAIN的出现,可以节省系统调用次数,降低延迟
epoll 优点
-
对应select()的缺点, epoll都有解决的方法
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 使用epoll_ctl()函数,只有在注册、修改、删除的时候才会对内核进行操作。
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)
- select支持的文件描述符数量太小了,默认是1024
- epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,我的1GB内存阿里云ECS是999999,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
poll每次返回整个文件描述符数组, 用户需要遍历数组已找到哪些文件描述符上有IO事件。 而epoll_wait(2)返回的是活动fd的列表,需要遍历的数组通常会小很多,在并发连接数较大而活动连接比例不高时,epoll(4)比epoll(2)更高效。
epoll 源码解读
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
select()
select()简介
- select()函数是阻塞的, 只有某些端口状态转换了或者达到timeout才会返回
- 该函数可以允许进程指示等待多个事件中任何一个的发生
- select(), poll() 都是水平触发
为什么需要select()?
- 多路复用io mutiplexing
- 如果不采用多路复用,要么使用阻塞IO(会使线程长时间处于阻塞状态,无法执行任何计算或者响应任何网络请求),要么使用非阻塞IO:(要用while循环调用recv函数,大幅占用CPU资源), 复用的优势在于可以同时处理多个连接
select()函数
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1,fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
返回: 若有描述符就绪,则返回就绪描述符的数量,若超时则为0, 若出错则为1
1. timeout-->timeval
struct timeval {
long tv_sec; // seconds
log tv_usec; // microseconds
}
- 用于指定timeout的秒数和微秒数
- 如果输入为0,那么select函数会一直等下去一直到某个描述符准备好
- 如果输入这个参数,那么最长等待时间就确定了
- 如果输入这个结构,但是其中的两个值为0,那么就不等待-->轮询机制
延伸 gettimeofday()
- 用gettimeofday() 可以获得微秒(us)级别的时间。
- 会把目前的时间tv所指的结构返回,当地时区的信息则放到tz所指的结构中。
- 1970年1月1日到现在的时间
- 调用两次gettimeofday(), 前后做减法,从而达到计算时间的目的。
#include <sys/time.h>
int gettimeofday(struct timeval *tv,struct timezone *tz);
2. readset, writeset, exceptset
#include <sys/select.h>
struct fd_set myset;
//四个相关的宏函数
void FD_ZERO(fd_set *fdset); // clean all bits at fdset
void FD_SET(int fd, fd_set *fdset); // turn on the bit for fd in fdset
void FD_CLR(int fd, fd_set *fdset); // turn on the bit for fd in fdset
void FD_ISSET(int fd, fd_set *fdset); //is the bit for fd on in fdset? 如果set了,返回1
- fd_set 每一位表示一个fd, set其中的某一位就表示要监视某个fd.
- 指针输入, 输入的时候把我们所关心的fd置为1. 返回时,他将指示哪些描述符已经就绪了。因此,每次重新调用select时,我们都需要再次把所有我们关心的描述符置为1。
3. maxfdp1
- maxfdp1 = 最大描述符+1
- 最大描述符系统内是有定义的 FD_SETSIZE
例子
select\strcliselect01.c
void
str_cli(FILE *fp, int sockfd)
{
int maxfdp1;
fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE];
FD_ZERO(&rset);
for ( ; ; ) {
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
if (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */
if (Fgets(sendline, MAXLINE, fp) == NULL)
return; /* all done */
Writen(sockfd, sendline, strlen(sendline));
}
}
}
select 缺点
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小了,默认是1024
select() 文件描述符上限
这个问题的关键其实要先理解select关于文件描述符上限的原因
- linux系统本身就有文件描述符上限,文件描述符的建立会连带建立很多其它表项,具体可以搜索文件描述符的详解,也就是说文件描述符一定会占用资源,那在有限的硬件条件下,文件描述符必定会有上限,我在ubuntu14.04的ECS里通过
cat /proc/sys/fs/file-max //结果99999
- 进程文件描述符上限user limit中nofile的soft limit,实际上这是单个用户的文件描述符上限,通过
ulimit -n //结果65535
soft limit可以修改,但是不能超过hard limit
ulimit -Hn //结果65535
- select函数本身限制,主要是头文件中FD_SETSIZE的大小,一般来说是1024,这就限定了select函数中的文件描述符上限,当然可以做修改,但是需要重新编译内核,而且效果由于select的实现机制,会比较差
poll()
#include <poll.h>
#include <limits.h> /* for OPEN_MAX */ // 描述了poll的最大数量
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
返回: 若有描述符就绪,则返回就绪描述符的数量,若超时则为0, 若出错则为1
fdarray
指向一个数组结构第一个元素的指针,没一个元素都是一个pollfd结构,使用这个结构,避免了select中使用一个参数既表示我们关心的值,又表示结果。
struct pollfd {
int fd; // 描述符
short events; // 我们关心的状态
short revents; // 返回的结果
}
nfds
第一个参数中的数组元素的个数
timeout
timeout | 说明 |
---|---|
INFTIM | 永远等待 |
0 | 立即返回,不阻塞进程 |
> 0 | 等待指定的毫秒数 |
poll() 文件描述符上限
poll虽然不像select一样受到select() 中FD_SETSIZE 的限制,但是仍然受到ulimit中设定的一个进程所能打开的最大文件描述符的限制
ulimit -n //结果65535
poll()/select()的区别
- poll() 解决了select文件描述符最大只有1024的限制
- select和poll都需要自己不断轮询所有fd集合,直到设备就绪,(首先把所有的fd挂到对应的等待队列上,然后睡眠,在设备收到一条消息或者填写完文件数据之后,会唤醒设备等待队列上的进程,进程会再次扫描整个注册文件描述符的集合,并返回就绪文件描述符的数目给用户)期间可能要睡眠和唤醒多次交替(存疑),虽然epoll也需要唤醒,但是唤醒之后只需要检测就绪链表是否为空就行了。