摘要

关于epoll的问题很早就像写文章讲讲自己的看法,但是由于ffrpc一直没有完工,所以也就拖下来了。Epoll主要在服务器编程中使用,本文主要探讨服务器程序中epoll的使用技巧。Epoll一般和异步io结合使用,故本文讨论基于以下应用场合:

  • 主要讨论服务器程序中epoll的使用,主要涉及tcp socket的相关api。
  • Tcp socket 为异步模式,包括socket的异步读写,以及监听的异步操作。
  • 本文不会过多讨论API的细节,而是专注流程与设计。

Epoll 的io模型

Epoll是为异步io操作而设计的,epoll中IO事件被分为read事件和write事件,如果大家对于linux的驱动模块或者linux io 模型有接触的话,就会理解起来更容易。Linux中IO操作被抽象为read、write、close、ctrl几个操作,所以epoll只提供read、write、error事件,是和linux的io模型是统一的。

  • 当epoll通知read事件时,可以调用io系统调用read读取数据
  • 当epoll通知write事件时,可以调用io系统调用write发送数据
  • 当error事件时,可以close回收资源
  • Ctrl相关的接口则用来设置socket的非阻塞选项等。

为什么要了解epoll的io模型呢,本文认为,某些情况下epoll操作的代码的复杂性是由于代码中的模型(或者类设计)与epoll io模型不匹配造成的。换句话说,如果我们的编码模型和epoll io模型匹配,那么非阻塞socket的编码就会很简单、清晰。

按照epoll模型构建的类关系为:

  1. //! 文件描述符相关接口
  2. typedef int socket_fd_t;
  3. class fd_i
  4. {
  5. public:
  6. virtual ~fd_i(){}
  7.  
  8. virtual socket_fd_t socket() = ;
  9. virtual int handle_epoll_read() = ;
  10. virtual int handle_epoll_write() = ;
  11. virtual int handle_epoll_del() = ;
  12.  
  13. virtual void close() = ;
  14. };
  15. int epoll_impl_t::event_loop()
  16. {
  17. int i = , nfds = ;
  18. struct epoll_event ev_set[EPOLL_EVENTS_SIZE];
  19.  
  20. do
  21. {
  22. nfds = ::epoll_wait(m_efd, ev_set, EPOLL_EVENTS_SIZE, EPOLL_WAIT_TIME);
  23.  
  24. if (nfds < && EINTR == errno)
  25. {
  26. nfds = ;
  27. continue;
  28. }
  29. for (i = ; i < nfds; ++i)
  30. {
  31. epoll_event& cur_ev = ev_set[i];
  32. fd_i* fd_ptr = (fd_i*)cur_ev.data.ptr;
  33. if (cur_ev.data.ptr == this)//! iterupte event
  34. {
  35. if (false == m_running)
  36. {
  37. return ;
  38. }
  39.  
  40. //! 删除那些已经出现error的socket 对象
  41. fd_del_callback();
  42. continue;
  43. }
  44.  
  45. if (cur_ev.events & (EPOLLIN | EPOLLPRI))
  46. {
  47. fd_ptr->handle_epoll_read();
  48. }
  49.  
  50. if(cur_ev.events & EPOLLOUT)
  51. {
  52. fd_ptr->handle_epoll_write();
  53. }
  54.  
  55. if (cur_ev.events & (EPOLLERR | EPOLLHUP))
  56. {
  57. fd_ptr->close();
  58. }
  59. }
  60.  
  61. }while(nfds >= );
  62.  
  63. return ;
  64. }

Epoll的LT模式和ET模式的比较

先简单比较一下level trigger 和 edge trigger 模式的不同。

LT模式的特点是:

  • 若数据可读,epoll返回可读事件
  • 若开发者没有把数据完全读完,epoll会不断通知数据可读,直到数据全部被读取。
  • 若socket可写,epoll返回可写事件,而且是只要socket发送缓冲区未满,就一直通知可写事件。
  • 优点是对于read操作比较简单,只要有read事件就读,读多读少都可以。
  • 缺点是write相关操作较复杂,由于socket在空闲状态发送缓冲区一定是不满的,故若socket一直在epoll wait列表中,则epoll会一直通知write事件,所以必须保证没有数据要发送的时候,要把socket的write事件从epoll wait列表中删除。而在需要的时候在加入回去,这就是LT模式的最复杂部分。

ET模式的特点是:

  • 若socket可读,返回可读事件
  • 若开发者没有把所有数据读取完毕,epoll不会再次通知epoll read事件,也就是说存在一种隐患,如果开发者在读到可读事件时,如果没有全部读取所有数据,那么可能导致epoll在也不会通知该socket的read事件。(其实这个问题并没有听上去难,参见下文)。
  • 若发送缓冲区未满,epoll通知write事件,直到开发者填满发送缓冲区,epoll才会在下次发送缓冲区由满变成未满时通知write事件。
  • ET模式下,只有socket的状态发生变化时才会通知,也就是读取缓冲区由无数据到有数据时通知read事件,发送缓冲区由满变成未满通知write事件。
  • 缺点是epoll read事件触发时,必须保证socket的读取缓冲区数据全部读完(事实上这个要求很容易达到)
  • 优点:对于write事件,发送缓冲区由满到未满时才会通知,若无数据可写,忽略该事件,若有数据可写,直接写。Socket的write事件可以一直发在epoll的wait列表。Man epoll中我们知道,当向socket写数据,返回的值小于传入的buffer大小或者write系统调用返回EWouldBlock时,表示发送缓冲区已满。

让我们换一个角度来理解ET模式,事实上,epoll的ET模式其实就是socket io完全状态机。

先来看epoll中read 的状态图:

当socket由不可读变成可读时,epoll的ET模式返回read 事件。对于read 事件,开发者需要保证把读取缓冲区数据全部读出,man epoll可知:

  • Read系统调用返回EwouldBlock,表示读取缓冲区数据全部读出
  • Read系统调用返回的数值小于传入的buffer参数,表示读取缓冲区全部读出。

示例代码

  1. int socket_impl_t:: handle_epoll_read ()
  2. {
  3. if (is_open())
  4. {
  5. int nread = ;
  6. char recv_buffer[RECV_BUFFER_SIZE];
  7. do
  8. {
  9. nread = ::read(m_fd, recv_buffer, sizeof(recv_buffer) - );
  10. if (nread > )
  11. {
  12. recv_buffer[nread] = '\0';
  13. m_sc->handle_read(this, recv_buffer, size_t(nread));
  14. if (nread < int(sizeof(recv_buffer) - ))
  15. {
  16. break;//! equal EWOULDBLOCK
  17. }
  18. }
  19. else if ( == nread) //! eof
  20. {
  21. this->close();
  22. return -;
  23. }
  24. else
  25. {
  26. if (errno == EINTR)
  27. {
  28. continue;
  29. }
  30. else if (errno == EWOULDBLOCK)
  31. {
  32. break;
  33. }
  34. else
  35. {
  36. this->close();
  37. return -;
  38. }
  39. }
  40. } while();
  41. }
  42. return ;
  43. }
  1.  

再来看write 的状态机:

需要读者注意的是,socket模式是可写的,因为发送缓冲区初始时空的。故应用层有数据要发送时,直接调用write系统调用发送数据,若write系统调用返回EWouldBlock则表示socket变为不可写,或者write系统调用返回的数值小于传入的buffer参数的大小,这时需要把未发送的数据暂存在应用层待发送列表中,等待epoll返回write事件,再继续发送应用层待发送列表中的数据,同样若应用层待发送列表中的数据没有一次性发完,那么继续等待epoll返回write事件,如此循环往复。所以可以反推得到如下结论,若应用层待发送列表有数据,则该socket一定是不可写状态,那么这时候要发送新数据直接追加到待发送列表中。若待发送列表为空,则表示socket为可写状态,则可以直接调用write系统调用发送数据。总结如下:

  • 当发送数据时,若应用层待发送列表有数据,则将要发送的数据追加到待发送列表中。否则直接调用write系统调用。
  • Write系统调用发送数据时,检测write返回值,若返回数值>0且小于传入的buffer参数大小,或返回EWouldBlock错误码,表示,发送缓冲区已满,将未发送的数据追加到待发送列表
  • Epoll返回write事件后,检测待发送列表是否有数据,若有数据,依次尝试发送指导数据全部发送完毕或者发送缓冲区被填满。

示例代码:

  1. void socket_impl_t::send_impl(const string& src_buff_)
  2. {
  3. string buff_ = src_buff_;
  4.  
  5. if (false == is_open() || m_sc->check_pre_send(this, buff_))
  6. {
  7. return;
  8. }
  9. //! socket buff is full, cache the data
  10. if (false == m_send_buffer.empty())
  11. {
  12. m_send_buffer.push_back(buff_);
  13. return;
  14. }
  15.  
  16. string left_buff;
  17. int ret = do_send(buff_, left_buff);
  18.  
  19. if (ret < )
  20. {
  21. this ->close();
  22. }
  23. else if (ret > )
  24. {
  25. m_send_buffer.push_back(left_buff);
  26. }
  27. else
  28. {
  29. //! send ok
  30. m_sc->handle_write_completed(this);
  31. }
  32. }
  33. int socket_impl_t:: handle_epoll_write ()
  34. {
  35. int ret = ;
  36. string left_buff;
  37.  
  38. if (false == is_open() || true == m_send_buffer.empty())
  39. {
  40. return ;
  41. }
  42.  
  43. do
  44. {
  45. const string& msg = m_send_buffer.front();
  46. ret = do_send(msg, left_buff);
  47.  
  48. if (ret < )
  49. {
  50. this ->close();
  51. return -;
  52. }
  53. else if (ret > )
  54. {
  55. m_send_buffer.pop_front();
  56. m_send_buffer.push_front(left_buff);
  57. return ;
  58. }
  59. else
  60. {
  61. m_send_buffer.pop_front();
  62. }
  63. } while (false == m_send_buffer.empty());
  64.  
  65. m_sc->handle_write_completed(this);
  66. return ;
  67. }
  1.  

总结

  LT模式主要是读操作比较简单,但是对于ET模式并没有优势,因为将读取缓冲区数据全部读出并不是难事。而write操作,ET模式则流程非常的清晰,按照完全状态机来理解和实现就变得非常容易。而LT模式的write操作则复杂多了,要频繁的维护epoll的wail列表。

在代码编写时,把epoll ET当成状态机,当socket被创建完成(accept和connect系统调用返回的socket)时加入到epoll列表,之后就不用在从中删除了。为什么呢?man epoll中的FAQ告诉我们,当socket被close掉后,其自动从epoll中删除。对于监听socket简单说几点注意事项:

  • 监听socket的write事件忽略
  • 监听socket的read事件表示有新连接,调用accept接受连接,直到返回EWouldBlock。
  • 对于Error事件,有些错误是可以接受的错误,比如文件描述符用光的错误

示例代码:

  1. int acceptor_impl_t::handle_epoll_read()
  2. {
  3. struct sockaddr_storage addr;
  4. socklen_t addrlen = sizeof(addr);
  5.  
  6. int new_fd = -;
  7. do
  8. {
  9. if ((new_fd = ::accept(m_listen_fd, (struct sockaddr *)&addr, &addrlen)) == -)
  10. {
  11. if (errno == EWOULDBLOCK)
  12. {
  13. return ;
  14. }
  15. else if (errno == EINTR || errno == EMFILE || errno == ECONNABORTED || errno == ENFILE ||
  16. errno == EPERM || errno == ENOBUFS || errno == ENOMEM)
  17. {
  18. perror("accept");//! if too many open files occur, need to restart epoll event
  19. m_epoll->mod_fd(this);
  20. return ;
  21. }
  22. perror("accept");
  23. return -;
  24. }
  25.  
  26. socket_i* socket = create_socket(new_fd);
  27. socket->open();
  28. } while (true);
  29. return ;
  30. }
  1.  

GitHub :https://github.com/fanchy/FFRPC

ffrpc 介绍: http://www.cnblogs.com/zhiranok/p/ffrpc_summary.html

故,综上所述,服务器程序中推荐使用epoll 的ET 模式!!!!

更多精彩文章 http://h2cloud.org

linux epoll 开发指南-【ffrpc源码解析】的更多相关文章

  1. 【Android应用开发】EasyDialog 源码解析

    示例源码下载 : http://download.csdn.net/detail/han1202012/9115227 EasyDialog 简介 : -- 作用 : 用于在界面进行一些介绍, 说明; ...

  2. odoo开发笔记 -- odoo源码解析

    odoo 源码解析:http://blog.csdn.net/weixin_35737303

  3. nginx开发笔记_ngx_hash源码解析

    ngx_hash源码解析 ngx_hash是nginx中的hash表结构,具有以下特点: 静态结构,hash表创建后无法动态添加/删除KV. 采用连续存储方式解决碰撞问题.即出现碰撞的KV存放在连续地 ...

  4. Android 开源项目源码解析(第二期)

    Android 开源项目源码解析(第二期) 阅读目录 android-Ultra-Pull-To-Refresh 源码解析 DynamicLoadApk 源码解析 NineOldAnimations ...

  5. linux线程池thrmgr源码解析

    linux线程池thrmgr源码解析 1         thrmgr线程池的作用 thrmgr线程池的作用是提高程序的并发处理能力,在多CPU的服务器上运行程序,可以并发执行多个任务. 2      ...

  6. 【源码解析】凭什么?spring boot 一个 jar 就能开发 web 项目

    问题 为什么开发web项目,spring-boot-starter-web 一个jar就搞定了?这个jar做了什么? 通过 spring-boot 工程可以看到所有开箱即用的的引导模块 spring- ...

  7. httprunner开发实践&源码解析

    上次作业讲解 排错 控制台查看报错信息 打开代理工具,调试脚本 注释掉其他接口,先跑一个接口 pip uninstall httprunner 修复断言100为int型问题 修复两次登陆问题 报告 p ...

  8. iOS开发SDWebImage源码解析之SDWebImageManager的注解

    最近看了两篇博客,写得很不错,关于SDWebImage源码解析之SDWebImageManager的注解: 1.http://www.jianshu.com/p/6ae6f99b6c4c 2.http ...

  9. Android开发——AsyncTask的使用以及源码解析

    .AsyncTask使用介绍  转载请标明出处:http://blog.csdn.net/seu_calvin/article/details/52172248 AsyncTask封装了Thread和 ...

随机推荐

  1. Xshell_Using X11 forwarding

    FROM:http://www.netsarang.com/tutorial/xshell/1018/Using_X11_forwarding The X11 forwarding feature i ...

  2. Oracle删除指定用户下所有对象

    --.sql脚本 --唯一注意的是下面的d:\dropuserobj.sql为操作的.sql; --用于删除当前用户的所有对象 --use for drop all objects in curren ...

  3. iis php5.3.8 默认文档无效 404 - 找不到文件或目录

    环境:WIN2008 R2 IIS7.5 / .NET4.X 新开1站点,使用php(5.3.8),默认首页文档已设置为index.php,网站所在目录的网站运行时用户权限正确,应用程序池是asp.n ...

  4. input text 的事件及方法

    事件 描述onactivate 当对象设置为活动元素时触发.onafterupdate 当成功更新数据源对象中的关联对象后在数据绑定对象上触发.onbeforeactivate 对象要被设置为当前元素 ...

  5. [JS10] 获取时间

    <html> <head> <meta http-equiv="Content-Type" content="text/html; char ...

  6. Linux:目录&文件基本操作

    - 表示上一次所在目录,- 通常表示当前用户的"home"目录.使用 pwd 命令可以获取当前所在路径(绝对路径). 新建文件:touch test创建目录:mkdir -p fa ...

  7. linxu ffmpeg 编译安装

    1.下载ffmpeg. 下载网址:http://www.ffmpeg.org/download.html 2.解压缩 tar -zxvf ffmpeg-2.0.1.tar.gz 3.配置,生成Make ...

  8. atitit.提升备份文件复制速度(3) ----建立同步删除脚本

    atitit.提升备份文件复制速度(3) ----建立同步删除脚本 1. 建立同步删除脚本两个方法.. 1 2. 1从回收站info2文件... 1 3. 清理结束在后snap比较 1 4. Npp  ...

  9. ServiceStack 概念

    目录 ServiceStack 概念 ServiceStack Web Service 创建与调用简单示列 ServiceStack ServiceStack是.Net和Mono的开源框架,相对WCF ...

  10. Linux Red hat修改主机名

    步骤一:修改etc/hosts文件内容为 [root@kypt01 /]# cd etc[root@kypt01 etc]# cat hosts127.0.0.1 localhost kypt01 l ...