select,poll,epoll总结
还在持续学习中,本文是一个阶段性的总结。
一、select
select的核心是不停的遍历文件描述符,看看是否就绪。一般最多同时支持1024个文件描述符。
1.1 select函数
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
- nfds:最大的文件描述符加1
- readfds:监听可读集合
- writefds:监听可写集合
- exceptfds:监听异常集合
- timeout:超时时间
1.2 select流程
唤醒 当前进程的过程通常是在所监测文件的设备驱动内实现的,驱动程序维护了针对自身资源读写的等待队列。当设备驱动发现自身资源变为可读写并且有进程睡眠在该资源的等待队列上时,就会唤醒这个资源等待队列上的进程。
1.3 select问题
-
每次调用时要重复地从用户态读入参数。
-
每次调用时要重复地扫描文件描述符,遍历所有文件描述符。
-
每次在调用开始时,要把当前进程放入各个文件描述符的等待队列。在调用结束后,又把进程从各个等待队列中删除。
-
对文件描述符的数量有限制(一般为1024个)
-
用同一个参数来表示入参和最终的结果,内核直接修改入参(需要监控的fd)。
所以下次调用得重置入参fd,因为当事件发生后,入参已经被操作系统内核给改了。
再次把全量fd传送给操作系统。
select随着文件描述符数量的上升,性能会急剧下降
二、poll
poll的整体流程和select差不多,也是每次遍历所有文件描述符,性能不好,不过poll解决了select文件描述符数量的限制问题。
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
相比select,poll定义了一个pollfd用来指定我们感兴趣的文件描述符以及上面发生的事件。
pollfd结构
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生了的事件 */
} ;
这里还有一个核心的改变:操作系统没有直接修改入参(events),而是把实际ready的fd通过另外一个参数来返回(revents),这就减少了每次调用poll之前再次重置入参(events)的开销。
但是还是要重复地从用户态读入参数
三、epoll
select和poll运行效率的两个瓶颈已经找出,现在的问题是怎么改进。
- 首先,如果要监听1000个fd,每次poll都要把1000个fd 拷入内核,这是极大的浪费,内核干嘛不自己保存已经拷入的fd呢?**epoll就是自己保存拷入的fd **,它的API就已经说明了这一点,不是 epoll_wait的时候才传入fd,而是通过epoll_ctl把所有fd传入内核再一起"wait",这就省掉了不必要的重复拷贝。
- 其次,在 epoll_wait时,也不是把current轮流的加入fd对应的设备等待队列,而是在设备等待队列醒来时调用一个回调函数(当然,这就需要“唤醒回调”机制),把产生事件的fd归入一个链表,然后返回这个链表上的fd。
- 另外,epoll机制实现了自己特有的文件系统eventpoll filesystem
3.1 epoll 函数
epoll总共提供了三个函数
-
int epoll_create(int size);
epoll_create是为了创建一个epoll文件描述符,新创建的epoll文件描述符带有一个struct eventpoll结构,eventpoll结构如下。
struct eventpoll { spinlock_t lock; struct mutex mtx; wait_queue_head_t wq; /* Wait queue used by sys_epoll_wait() ,调用epoll_wait()时, 我们就是"睡"在了这个等待队列上*/ wait_queue_head_t poll_wait; /* Wait queue used by file->poll() , 这个用于epollfd本事被poll的时候*/ struct list_head rdllist; /* List of ready file descriptors, 所有已经ready的epitem都在这个链表里面*/ struct rb_root rbr; /* RB tree root used to store monitored fd structs, 所有要监听的epitem都在这里*/ epitem *ovflist; /*存放的epitem都是我们在传递数据给用户空间时监听到了事件*/. struct user_struct *user; /*这里保存了一些用户变量,比如fd监听数量的最大值等*/ };
这个结构上再挂一个红黑树,而这个红黑树就是每次epoll_ctl时fd存放的地方。
-
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);
此函数是等待条件满足,返回值为准备就绪的事件数
3.2 epoll 流程
3.3 LT和ET模式
LT(Level Trigger):
应用程序地哎呦epoll_wait后如果有事件产生,epoll将事件通知应用程序,但是应用程序可以不处理。应用程序下次调用epoll_wait后,epoll将再次通知应用程序,一直到改事件被处理。
ET(Edge Trigger):
epoll通知事件后,应用程序必须立即处理改事件,后续epoll将不再再次向应用程序通知这一事件。
ET很大成都上减少了同一个epoll事件被多次重复触发的次数,效率比LT高。
四、select、poll、epoll对比
【参考】
1.https://www.cnblogs.com/Anker/p/3265058.html
2.https://blog.csdn.net/weixin_42462202/article/details/95315926
3.https://blog.csdn.net/turkeyzhou/article/details/8609360
4.http://blog.chinaunix.net/uid-20643761-id-1594860.html
5.http://blog.chinaunix.net/uid-28541347-id-4236779.html
6.http://blog.chinaunix.net/uid-28541347-id-4238524.html