











关于epoll的问题很早就像写文章讲讲自己的看法,但是由于ffrpc一直没有完工,所以也就拖下来了。Epoll主要在服务器编程中使用,本文主要探讨服务器程序中epoll的使用技巧。Epoll一般和异步io结合使用,故本文讨论基于以下应用场合:
Epoll是为异步io操作而设计的,epoll中IO事件被分为read事件和write事件,如果大家对于linux的驱动模块或者linux io 模型有接触的话,就会理解起来更容易。Linux中IO操作被抽象为read、write、close、ctrl几个操作,所以epoll只提供read、write、error事件,是和linux的io模型是统一的。
为什么要了解epoll的io模型呢,本文认为,某些情况下epoll操作的代码的复杂性是由于代码中的模型(或者类设计)与epoll io模型不匹配造成的。换句话说,如果我们的编码模型和epoll io模型匹配,那么非阻塞socket的编码就会很简单、清晰。
按照epoll模型构建的类关系为:
//! 文件描述符相关接口 typedef int socket_fd_t; class fd_i { public: virtual ~fd_i(){} virtual socket_fd_t socket() = 0; virtual int handle_epoll_read() = 0; virtual int handle_epoll_write() = 0; virtual int handle_epoll_del() = 0; virtual void close() = 0; }; int epoll_impl_t::event_loop() { int i = 0, nfds = 0; struct epoll_event ev_set[EPOLL_EVENTS_SIZE]; do { nfds = ::epoll_wait(m_efd, ev_set, EPOLL_EVENTS_SIZE, EPOLL_WAIT_TIME); if (nfds < 0 && EINTR == errno) { nfds = 0; continue; } for (i = 0; i < nfds; ++i) { epoll_event& cur_ev = ev_set[i]; fd_i* fd_ptr = (fd_i*)cur_ev.data.ptr; if (cur_ev.data.ptr == this)//! iterupte event { if (false == m_running) { return 0; } //! 删除那些已经出现error的socket 对象 fd_del_callback(); continue; } if (cur_ev.events & (EPOLLIN | EPOLLPRI)) { fd_ptr->handle_epoll_read(); } if(cur_ev.events & EPOLLOUT) { fd_ptr->handle_epoll_write(); } if (cur_ev.events & (EPOLLERR | EPOLLHUP)) { fd_ptr->close(); } } }while(nfds >= 0); return 0; }
先简单比较一下level trigger 和 edge trigger 模式的不同。
让我们换一个角度来理解ET模式,事实上,epoll的ET模式其实就是socket io完全状态机。

当socket由不可读变成可读时,epoll的ET模式返回read 事件。对于read 事件,开发者需要保证把读取缓冲区数据全部读出,man epoll可知:
示例代码
int socket_impl_t:: handle_epoll_read () { if (is_open()) { int nread = 0; char recv_buffer[RECV_BUFFER_SIZE]; do { nread = ::read(m_fd, recv_buffer, sizeof(recv_buffer) - 1); if (nread > 0) { recv_buffer[nread] = '\0'; m_sc->handle_read(this, recv_buffer, size_t(nread)); if (nread < int(sizeof(recv_buffer) - 1)) { break;//! equal EWOULDBLOCK } } else if (0 == nread) //! eof { this->close(); return -1; } else { if (errno == EINTR) { continue; } else if (errno == EWOULDBLOCK) { break; } else { this->close(); return -1; } } } while(1); } return 0; }

需要读者注意的是,socket模式是可写的,因为发送缓冲区初始时空的。故应用层有数据要发送时,直接调用write系统调用发送数据,若write系统调用返回EWouldBlock则表示socket变为不可写,或者write系统调用返回的数值小于传入的buffer参数的大小,这时需要把未发送的数据暂存在应用层待发送列表中,等待epoll返回write事件,再继续发送应用层待发送列表中的数据,同样若应用层待发送列表中的数据没有一次性发完,那么继续等待epoll返回write事件,如此循环往复。所以可以反推得到如下结论,若应用层待发送列表有数据,则该socket一定是不可写状态,那么这时候要发送新数据直接追加到待发送列表中。若待发送列表为空,则表示socket为可写状态,则可以直接调用write系统调用发送数据。总结如下:
示例代码:
void socket_impl_t::send_impl(const string& src_buff_) { string buff_ = src_buff_; if (false == is_open() || m_sc->check_pre_send(this, buff_)) { return; } //! socket buff is full, cache the data if (false == m_send_buffer.empty()) { m_send_buffer.push_back(buff_); return; } string left_buff; int ret = do_send(buff_, left_buff); if (ret < 0) { this ->close(); } else if (ret > 0) { m_send_buffer.push_back(left_buff); } else { //! send ok m_sc->handle_write_completed(this); } } int socket_impl_t:: handle_epoll_write () { int ret = 0; string left_buff; if (false == is_open() || true == m_send_buffer.empty()) { return 0; } do { const string& msg = m_send_buffer.front(); ret = do_send(msg, left_buff); if (ret < 0) { this ->close(); return -1; } else if (ret > 0) { m_send_buffer.pop_front(); m_send_buffer.push_front(left_buff); return 0; } else { m_send_buffer.pop_front(); } } while (false == m_send_buffer.empty()); m_sc->handle_write_completed(this); return 0; }
LT模式主要是读操作比较简单,但是对于ET模式并没有优势,因为将读取缓冲区数据全部读出并不是难事。而write操作,ET模式则流程非常的清晰,按照完全状态机来理解和实现就变得非常容易。而LT模式的write操作则复杂多了,要频繁的维护epoll的wail列表。
在代码编写时,把epoll ET当成状态机,当socket被创建完成(accept和connect系统调用返回的socket)时加入到epoll列表,之后就不用在从中删除了。为什么呢?man epoll中的FAQ告诉我们,当socket被close掉后,其自动从epoll中删除。对于监听socket简单说几点注意事项:
示例代码:
int acceptor_impl_t::handle_epoll_read() { struct sockaddr_storage addr; socklen_t addrlen = sizeof(addr); int new_fd = -1; do { if ((new_fd = ::accept(m_listen_fd, (struct sockaddr *)&addr, &addrlen)) == -1) { if (errno == EWOULDBLOCK) { return 0; } else if (errno == EINTR || errno == EMFILE || errno == ECONNABORTED || errno == ENFILE || errno == EPERM || errno == ENOBUFS || errno == ENOMEM) { perror("accept");//! if too many open files occur, need to restart epoll event m_epoll->mod_fd(this); return 0; } perror("accept"); return -1; } socket_i* socket = create_socket(new_fd); socket->open(); } while (true); return 0; }
GitHub :https://github.com/fanchy/FFRPC
ffrpc 介绍: http://www.cnblogs.com/zhiranok/p/ffrpc_summary.html
更多精彩文章 http://h2cloud.org
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。