在前两篇中大致介绍了使用Reactor线程模型和epoll机制实现的高性能网络组件的思路。在我们产品中使用的思路与其类似,不过是基于select机制实现的。特别是从上一篇文章中知道了,main eventloop thread用于监听端口,处理accept事件。sub eventloop thread用于处理IO读写事件。本文一起来看以下几个问题如何解决:
sub eventloop没有读写事件,可能阻塞在dispatcher内的epoll_wait函数上,此时main eventloop接收了新连接,如何通知sub eventloop进行注册到自己的dispatcher上?
连接事件和IO读写事件如何区分?下面,我们将看看如何解决这些问题。
1、主线程注册listen fd
main eventloop 首先会将自己的listen fd包装成channel,注册到epoll_dispatcher上面,代码如下:
//开启监听void tcp_server_start(struct TCPserver *tcpServer) { struct acceptor *acceptor = tcpServer->acceptor; struct event_loop *eventLoop = tcpServer->eventLoop; //开启多个线程 thread_pool_start(tcpServer->threadPool); //acceptor主线程, 同时把tcpServer作为参数传给channel对象 struct channel *channel=channel_new(acceptor->listen_fd,EVENT_READ, handle_connection_established, NULL, tcpServer); event_loop_add_channel_event(eventLoop, channel->fd, channel); return;}从上面的代码可以看到,main eventloop在调用thread_pool_start启动其他线程后,会将自己的listen socket包装为一个新的channel,然后添加到自己的线程链表中。event_loop_add_channl_event函数就是由main函数所在的线程进行处理并注册到main-eventloop的epoll_dispatcher上面交给epoll去监听socket fd列表。
其中,我们尤为关注的是回调函数handle_connection_established函数,这里主要是调用accept函数,从listen的socket上获得新连接,获得的新连接包装成一个事件添加到子线程的eventloop中。
int handle_connection_established(void *data) { //省略其余代码 int connected_fd = accept(listenfd,(struct sockaddr *)&client_addr, &client_len); make_nonblocking(connected_fd); //从线程池中选取一个eventloop,将accept后的socket交给新的eventloop处理 struct event_loop *eventLoop =thread_pool_get_loop(tcpServer->threadPool); //省略其余代码...... return 0;}
我们看event_loop_add_channel_event函数,如何将新得到的socket封装成channel注册到子线程中。这里需要注意,此时的运行线程还在main函数中,因此将先获得main eventloop线程的锁,然后将socket设置为非阻塞。
pthread_mutex_lock(&eventLoop->mutex);//获取main-eventloop的锁assert(eventLoop->is_handle_pending == 0);event_loop_channel_buffer_nolock(eventLoop, fd, channel1, type);pthread_mutex_unlock(&eventLoop->mutex);// 释放锁if (!isInSameThread(eventLoop)){ event_loop_wakeup(eventLoop);} else { event_loop_handle_pending_channel(eventLoop); // 执行这里}event_loop_channel_buffer_nolock函数将当前新连接的socket包装成channel,然后放到子线程的eventloop中的链表上,此时子线程还没处理。因为这里还是main函数线程在执行,因此将进入event_loop_handle_pending_channel函数。该函数的作用就是最终将socket注册到自己的epoll上进行事件监听。到这里第一次main eventloop的初始化工作结束,然后进入event_loop_run死循环中。
如果main-eventloop收到了连接事件,同样会将新连接包装成channel,并将该事件放到sub eventloop的事件列表,然后调用event_loop_wakeup去唤醒子线程,这里就涉及到如何唤醒子线程了。
2、main eventloop通知sub eventloop注册
由于一开始创建main eventloop和sub eventloop的时候,创建了一对socket_pair,类似于管道。此时假设需要唤醒子线程,只需要写入一个字节。这样子线程就会收到读写事件,从epoll wait处进行返回。
void event_loop_wakeup(struct event_loop *eventLoop) { char one = 'a'; ssize_t n = write(eventLoop->socketPair[0], &one, sizeof one); if (n != sizeof one) { LOG_ERR("wakeup event loop thread failed"); }}子线程被唤醒,此时从epoll wait处收到读事件,将通过回调函数handle_wakeup处理一下。
int handleWakeup(void *data) { struct event_loop *eventLoop = (struct event_loop *) data; char one; ssize_t n = read(eventLoop->socketPair[1], &one, sizeof one); if (n != sizeof one) { LOG_ERR("handleWakeup failed"); }}处理完该读写事件,该线程将继续判断事件链表是否为空,如果不为空,则循环处理事件链表上的事件,将新连接的socket fd注册到自己的dispatcher上,利用epoll机制来得到IO读写事件。
最后,我们再回到epoll_dispatcher的dispatch方法看看:
int epoll_dispatch(struct event_loop *eventLoop, struct timeval *timeval) { epoll_dispatcher_data *epollDispatcherData = (epoll_dispatcher_data *) eventLoop->event_dispatcher_data; int i, n; n = epoll_wait(epollDispatcherData->efd, epollDispatcherData->events, MAXEVENTS, -1); for (i = 0; i < n; i++) { if ((epollDispatcherData->events[i].events & EPOLLERR) || (epollDispatcherData->events[i].events & EPOLLHUP)) { fprintf(stderr, "epoll error\n"); close(epollDispatcherData->events[i].data.fd); continue; } //连接事件/读事件 if (epollDispatcherData->events[i].events & EPOLLIN) { channel_event_activate(eventLoop, epollDispatcherData->events[i].data.fd, EVENT_READ); } // 写事件 if (epollDispatcherData->events[i].events & EPOLLOUT) { channel_event_activate(eventLoop, epollDispatcherData->events[i].data.fd, EVENT_WRITE); } } return 0;}通过这个方法,可以看到,如果是listen fd,那么新连接是EPOLLIN事件,此时会回调到初始化注册的handle_connection_established回调函数上。如果是connect socket,那么这个EPOLLIN注册的是读写事件,将会处理socket的数据读写。
3、总结
我们将整个流程大致画一个流程图,如下所示:
首先,main Eventloop首先将listen fd封装成一个channel,然后注册到自己的dispatcher上,当有epoll in事件的时候,其中回调函数handle_connection_established将会被调用。该函数内部将调用accept获得一个新的socket,然后将该socket fd封装为新的channel,然后从sub Eventloop上选择一个eventloop线程,并将新的channel作为事件添加到该eventloop的事件列表上。 同时,当main eventloop唤醒sub eventloop,主要是向其初始化创建的socket pair上写入一个字符,从而促发sub eventloop收到epoll in事件,然后唤醒了去处理自己的事件链表,当发现了链表不为空,则存在事件需要处理。因此,此时将该socket fd调用epoll dispatcher中的add回调方法注册到自己的dispatcher上面。