在socket网络编程为了避免“Broke Pipe”
,所以我们第一件事就应该处理SIG_PIPE
避免程序退出
broken pipe经常发生socket关闭之后(或者其他的描述符关闭之后)的write操作中,此时进程会收到
SIGPIPE
信号,默认动作是进程终止
谈及EPOLL首先必定涉及LT
和ET
的工作模式。在实际处理过程中,ET得效率高于LT,但是选择符合自己的才是最好的。下面给出两种的工作处理模式。
LT
LT:水平触发:
对于EPOLLIN
:只要数据可读则会一直触发EPOLLIN
直至用户将所有数据读取完毕。例如socket收到:ABCDEF,此时用户一次读取两个字符,则系统会通知用户3次,用于依次读取:AB->CD->EF,直至读缓冲区为空。
对于EPOLLOUT
:只要socke可写,则会一直触发EPOLLOUT
事件
读取数据处理逻辑:
ET
ET:边沿触发,比LT效率高。
对于EPOLLIN
:只有socket上的数据从无到有,EPOLLIN
才会触发,此时用户需要一次性将数据读取完毕,否则将导致数据延后甚至阻塞。例如socket收到:ABCDEF,此时用户一次只读取2个字符,那么当用户读取AB之后,系统并不会再次通知用户去读取CDEF,此时数据CDEF只能等待下次数据再次可读之后才能被取出。此时就会导致数据延迟并且导致缓冲区数据阻塞。
对于EPOLLOUT
:只有在socket写缓冲区从不可写变为可写,EPOLLOUT 才会触发(刚刚添加事件完成调用epoll_wait时或者缓冲区从满到不满)
读取数据处理逻辑:一次性读取所有数据
EPOLL事件类型
事件 | 描述 |
EPOLLIN | 数据可读时触发(包括普通数据和优先数据) |
EPOLLOUT | 数据可写时触发 |
EPOLLPRI | 优先数据可读,eg:tcp的带外数据 |
EPOLLRDHUP | tcp链接被对方关闭,或者关闭了些操作。由GNU引入 |
EPOLLERR | 错误引起 |
EPOLLHUP | 挂起,比如管道的写端被关闭后 |
EPOLLNVAL | 文件描述符没打开 |
EPOLLET | 边缘触发模式 |
这些事件中我们往往只需要关注EPOLLIN
进行读取数据即可。至于EPOLLOUT
往往不会被设置。如果在LT
模式设置了EPOLLOUT
该事件,只要缓冲区可写则会一直触发,容易导致cpu浪费。但是如果用户自己维护了数据发送缓冲区,则可以在该事件中进行数据发送,当数据发送完毕之后应该去掉该标记,避免cpu浪费,例如:解决short write问题
。
EPOLLONESHOT事件
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读,可写或异常事件,且只能触发一次。当事件(读,写,error)处理完成之后应该重置该事件。否则系统会认为上一次事件(读,写,error)没完成,后面的事件(读,写,error)将永远不会触发
如果我们在多线程开发中,建议设置该事件,否则可能导致很多意想不到的错误。例如在多线程中我们处理accpet
操作。
此时可能多个线程都会触发这段代码,但是只有一个线程能执行成功,其他的将报告Resource temporarily unavailable
。虽然我们可以忽略这段代码,但是并不是我们期望的。我们期望的是只触发一次。同样的对于读事件,虽然读可以保证read的原子性,但是多线程读取的数据顺序我们没办法保证。例如:客户端发送数据ABCDEF,服务端3个线程进行读取数据,每个线程每次读取两个字符。此时结果可能是,线程A:AB,线程B:EF,线程C:CD,此时数据顺序混乱了,导致数据错误。所以如果在多线程中操作socket的时候我们一定要设置EPOLLONESHOT
并在事件操作完之后重置。重置的时候尽可能的在处理事件的线程中处理,避免在其他线程中重置,否则会导致线程不安全,如果一定要在其他线程中重置,则需要开发者自行保证事件对应的线程安全性
。
多线程版本Epoll demo
- 处理SIGPIPE信号,并将socket设置为非阻塞模式
- 将listen socket 设置EPOLLONESHOT并加入epoll
- 多线程
epoll_wait
,等到事件处理完成之后重置EPOLLONESHOT。
这个demo没有考虑short write
问题,因为该代码只有监听套接字设置了非阻塞模式
,accept
的套接字没有设置非阻塞模式,所以使用send或者write
的时候不存在部分发送导致的short write
模式。
缓冲区可以参考:动画图解 socket 缓冲区的那些事儿 关于epoll
的多线程demo和short write
的解决方案可以参考:tcp 完美解决short write问题