epoll注册网络事件的流程
1.初始化epoll。epoll内部是红黑树和就绪队列(双向链表),所有被注册的fd都是红黑树的一个结点,当监听的事件发生时,会触发回调,这时该节点会被加入到队列中等待处理。
2.将监听socket加入epoll,注册读事件(不应该注册写事件)。使用listen实际上是维护了一个全连接队列,每次accept从这个全连接队列中取出一个连接即可,这里要注意如果全连接队列已满会拒绝客户端发起的连接。
3.新到来的连接加入epoll注册事件,监听其读事件(写事件在需要写入时才进行注册,在LT模式下写入结束的时候需要进行删除注册)。
为什么要使用非阻塞io
1.非阻塞io可以让主线程做其他事情。
2.在触发可读事件时,可能读缓冲区中并没有数据(阅读手册发现可能是因为校验和导致读事件被出发,但此时读缓冲区无数据)。这时如果使用阻塞io会导致一直阻塞。使用非阻塞io可以通过获取epoll事件类型来判断当前如何处理。
epoll的一些事件的注意事项
EPOLLRDHUP->读端关闭。收到这个事件可以选择关闭读端。
EPOLLHUP->读写端同时关闭。需要关闭fd。
EPOLLIN->可读事件,需要调用read读缓冲区,这时如果读失败(返回值<0),需要判断errno,如果errno==EINTR,也就是被信号中断,那么需要继续读。如果errno==EWOULDBLOCK,则说明读缓冲区为空(对应上面的2.),这时直接忽略这次事件即可。
EPOLLOUT->可写事件,这时需要写缓冲区。如果写成功,这时需要在事件中删除写事件的注册,因为如果不删除,在LT模式下会一直触发可写事件,而实际上需要写的数据已经写完。如果写失败,则同样判断errno。如果是EWOULDBLOCK,说明写缓存已满,这时需要继续写(如果是ET模式下则需要再次注册写事件,在可写的情况下继续尝试写入数据)。
单reactor下的一般步骤
1.写事件是可以多线程操作的,因为线程在使用锁拿到对应的缓冲之后,只需要在一次写完数据的情况下再返回即可,不会出现多个线程同时写的情况。
2.读事件需要在主线程操作,因为客户端发过来的数据可能分多个包,所以可能出现多个线程读同一个socket的一次发送数据但是不同分包的情况。
多reactor(one eventloop per thread)
使用1个reactor来监听其他reactor,根据监听事件类型(或者其他方案)的不同来将连接派发到不同的reactor中,这些reactor循环使用epoll处理相应的事件。
多reactor是否一定更好? 不一定,当临界资源访问频繁的时候,多reactor方案会出现大量锁的请求和释放导致的开销,从而导致性能下降。而在业务隔离的情况下(多个reactor之间相对独立),这种情况性能更优。