select
概述
通过轮询检查在文件描述符上设置的标识位来进行判断,select
的轮询相当于在数据库中查找一条记录没有建立索引,对所有的
socket 进行全部遍历,这对 CPU 是浪费的。另外 select 还有一个限制,
对于单个进程所能打开的文件描述符最大只能是 1024,那么基于 select 的
轮询技术最多也只能很好的处理 1000 并发的吞吐量,可以查看 上一个10年,
著名的C10K并发连接问题.
1 | 采用32个整数的32位,即32*32= 1024来标识,fd值为1-1024。 |
缺点
- 1.每次调用select/poll,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 2.同时每次调用select/poll都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- 3.针对select支持的文件描述符数量太小了,默认是1024
- 4.select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
- 5.select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,
那么之后每次select调用还是会将这些文件描述符通知进程。
poll
概述
poll 和 select 在实现上没有本质的区别,相比较 select,poll 基于链表来实现,没有了最大链接 1024 的限制。
但是当文件描述符多了之后,每次调用都会对链接进行线性遍历,性能还是十分低下的。
缺点
和select差不多
epoll
概述
通过 callbak 回调通知机制,不在是每次调用都对链接进行线性遍历,
这样就不会随着文件描述符的增加导致效率下降。在 1GB 内存的机器上能
监听大约 10 万个端口,远超过 select 的 1024 限制,具体可以在服务
器上查看 cat/proc/sys/fs/file-max
处理步骤
- 1.调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
- 2.调用epoll_ctl向epoll对象中添加这100万个连接的套接字
- 3.调用epoll_wait收集发生的事件的连接
底层实现
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向
epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就
可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应
的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将
发生的事件添加到rdlist双链表中。
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体
1
2
3
4
5struct eventpoll{
# 红黑树的根节点,储存着所有添加到epoll中需要监控的事件
struct rb_root rbr;
# 双链表存放将要通过epoll_wait返回给用户的满足条件的事件
struct list_head rdlist;在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:
1
2
3
4
5
6
7struct 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/poll的所有问题
- 我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,
在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一
个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表
里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表
没数据也返回。所以,epoll_wait非常高效。
LT和ET
LT, ET这件事怎么做到的呢?当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,
这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,
epoll_wait干了件事,就是检查这些socket,如果不是ET模式(就是LT模式的句柄了),并且这些socket上确
实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。所以,非ET的句柄,只要它上面还有事件,
epoll_wait每次都会返回这个句柄。从这段,可以看出,LT还有个回放的过程,低效了