@myecho
2019-04-03T16:39:20.000000Z
字数 2912
阅读 1334
UNIX/Linux系统编程
select, poll, epoll 都是I/O多路复用的具体的实现,之所以有这三个鬼存在,其实是他们出现是有先后顺序的。
I/O多路复用这个概念被提出来以后, select是第一个实现 (1983 左右在BSD里面实现的)。
select 被实现以后,很快就暴露出了很多问题。
* select 会在内核空间和用户空间拷贝两次FD_SET,损耗巨大
* select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找,10几个sock可能还好,要是几万的sock每次都找一遍,这个无谓的开销就颇有海天盛筵的豪气了。
* select 只能监视1024个链接, 这个跟草榴没啥关系哦,linux 定义在头文件中的,参见FD_SETSIZE。
* select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程
于是14年以后(1997年)一帮人又实现了poll, poll 修复了select的很多问题,比如
* 相比select模型,poll使用链表保存文件描述符,去掉了1024个链接的限制,于是要多少链接呢, 主人你开心就好。
其实拖14年那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求。
于是5年以后, 在2002, 大神 Davide Libenzi 实现了epoll.
epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:
* epoll 现在是线程安全的。
* epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。
可是epoll 有个致命的缺点。。只有linux支持。比如BSD上面对应的实现是kqueue。
epoll的红黑树由一个互斥量保护,ready list是自旋锁保护的。因此epoll相关的三个操作都是线程安全的。
而根据POSIX文档中所描述,本身select和poll也是线程安全的函数。
而针对socket的竞态问题?
如果读取连接的主线程向从线程添加新的监听事件的时候会不会存在线程安全性的问题?epoll_ctl是线程安全的,但是若从线程正因epoll_wait阻塞的话,结果如何呢?
最权威的参考当然是Linux自带的man手册,原文如下:
While one thread is blocked in a call to epoll_pwait(), it is possible for another thread to add a file
descriptor to the waited-upon epoll instance. If the new file
descriptor becomes ready, it will cause the epoll_wait() call to
unblock. For a discussion of what may happen if a file descriptor in
an epoll instance being monitored by epoll_wait() is closed in another
thread, see select(2)
大意是,当一个线程阻塞在epoll_wait()上的时候,其他线程向其中添加新的文件描述符是没问题的,如果这个文件描述符就绪的话,阻塞线程的epoll_wait()会被唤醒。但是如果正在监听的某文件描述符被其他线程关闭的话详情见select。查阅select原文如下
If a file descriptor being monitored by select() is closed in
another thread, the result is unspecified. On some UNIX systems,
select() unblocks and returns, with an indication that the file
descriptor is ready (a subsequent I/O operation will likely fail with
an error, unless another the file descriptor reopened between the time
select() returned and the I/O operations was performed). On Linux (and
some other systems), closing the file descriptor in another thread has
no effect on select(). In summary, any application that relies on a
particular behavior in this scenario must be considered buggy
大意是,若一个文件描述符正被监听,其他线程关闭了的话,表现是未定义的。在有些
UNIX系统下,select会解除阻塞返回,而文件描述符会被认为就绪,然而对这个文件描述符进行IO操作会失败(除非这个文件描述符又被分配了),在Linux下,另一个线程关闭文件描述符没有任何影响。但不管怎样,这样做都是2B行为,应当尽量避免一个线程关闭另一个线程在监听的文件描述符。
关于是使用ET模式还是LT模式?
来自百度T9的答案:
在eventloop类型(包括各类fiber/coroutine)的程序中, 处理逻辑和epoll_wait都在一个线程,
ET相比LT没有太大的差别. 反而由于LT醒的更频繁, 可能时效性更好些. 在老式的多线程RPC实现中,(但是LT模式可能epoll_ctl调用次数更多)
消息的读取分割和epoll_wait在同一个线程中运行, 类似上面的原因, ET和LT的区别不大.但在更高并发的RPC实现中,
为了对大消息的反序列化也可以并行, 消息的读取和分割可能运行和epoll_wait不同的线程中, 这时ET是必须的, 否则在读完数据前,
epoll_wait会不停地无谓醒来.
LT是没读完就总是触发,如果处理的线程得不到及时的调度(比如工作线程都被打满了),epoll_wait所在的线程就会陷入疯狂的旋转。而ET是有消息来时才触发,和及时处理与否无关,频率低很多。
redis lighttpd以及nginx的监听套接字都是使用的LT模式,而nginx的其他部分都是使用的ET模式。
EPOLLONESHOT (since Linux 2.6.2)与 EPOLLEXCLUSIVE (since Linux 4.5)的区别在于前者是用于保证两次epoll_wait调用之间唤醒的线程之间对同一个fd的访问保证互斥,后者只是保证在内核层面解决了一个fd可能唤醒多个epoll线程的惊群问题,而前者在实际应用中一般不会使用,由于其每次都需要加锁改变红黑树,导致效率不高,解决方法是不设oneshot而用一个原子变量做去重,只有在原子加1前看到的是0才会启动读取线程,读取线程完成后置0。