﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-小阮的菜田-随笔分类-网络编程</title><link>http://www.cppblog.com/jericho/category/16670.html</link><description>一个人一种命，各安天命吧。</description><language>zh-cn</language><lastBuildDate>Thu, 21 Apr 2011 13:57:07 GMT</lastBuildDate><pubDate>Thu, 21 Apr 2011 13:57:07 GMT</pubDate><ttl>60</ttl><item><title>[转]epoll 经验谈 </title><link>http://www.cppblog.com/jericho/archive/2011/04/21/144748.html</link><dc:creator>小阮</dc:creator><author>小阮</author><pubDate>Thu, 21 Apr 2011 13:40:00 GMT</pubDate><guid>http://www.cppblog.com/jericho/archive/2011/04/21/144748.html</guid><wfw:comment>http://www.cppblog.com/jericho/comments/144748.html</wfw:comment><comments>http://www.cppblog.com/jericho/archive/2011/04/21/144748.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/jericho/comments/commentRss/144748.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/jericho/services/trackbacks/144748.html</trackback:ping><description><![CDATA[<p>1、首先需要一个内存池，目的在于：<br>&#183;减少频繁的分配和释放，提高性能的同时，还能避免内存碎片的问题；<br>&#183;能够存储变长的数据，不要很傻瓜地只能预分配一个最大长度；<br>&#183;基于SLAB算法实现内存池是一个好的思路：分配不同大小的多个块，请求时返回大于请求长度的最小块即可，对于容器而言，处理固定块的分配和回收，相当容易实现。当然，还要记得需要设计成线程安全的，自旋锁比较好，使用读写自旋锁就更好了。<br>&#183;分配内容的增长管理是一个问题，比如第一次需要1KB空间，随着数据源源不断的写入，第二次就需要4KB空间了。扩充空间容易实现，可是扩充的时候必然 涉及数据拷贝。甚至，扩充的需求很大，上百兆的数据，这样就不好办了。暂时没更好的想法，可以像STL一样，指数级增长的分配策略，拷贝数据虽不可避免， 但是起码重分配的几率越来越小了。<br>&#183;上面提到的，如果是上百兆的数据扩展需要，采用内存映射文件来管理是一个好的办法：映射文件后，虽然占了很大的虚拟内存，但是物理内存仅在写入的时候才会被分配，加上madvice()来加上顺序写的优化建议后，物理内存的消耗也会变小。<br>&#183;用string或者vector去管理内存并不明智，虽然很简单，但服务器软件开发中不适合使用STL，特别是对稳定性和性能要求很高的情况下。</p>
<p>2、第二个需要考虑的是对象池，与内存池类似： <br>&#183;减少对象的分配和释放。其实C++对象也就是struct，把构造和析构脱离出来手动初始化和清理，保持对同一个缓冲区的循环利用，也就不难了。<br>&#183;可以设计为一个对象池只能存放一种对象，则对象池的实现实际就是固定内存块的池化管理，非常简单。毕竟，对象的数量非常有限。</p>
<p>3、第三个需要的是队列： <br>&#183;如果可以预料到极限的处理能力，采用固定大小的环形队列来作为缓冲区是比较不错的。一个生产者一个消费者是常见的应用场景，环形队列有其经典的&#8220;锁无关&#8221;算法，在一个线程读一个线程写的场景下，实现简单，性能还高，还不涉及资源的分配和释放。好啊，实在是好！<br>&#183;涉及多个生产者消费者的时候，tbb::concurent_queue是不错的选择，线程安全，并发性也好，就是不知道资源的分配释放是否也管理得足够好。</p>
<p>4、第四个需要的是映射表，或者说hash表： <br>&#183;因为epoll是事件触发的，而一系列的流程可能是分散在多个事件中的，因此，必须保留下中间状态，使得下一个事件触发的时候，能够接着上次处理的位置继续处理。要简单的话，STL的hash_map还行，不过得自己处理锁的问题，多线程环境下使用起来很麻烦。<br>&#183;多线程环境下的hash表，最好的还是tbb::concurent_hash_map。</p>
<p>5、核心的线程是事件线程： <br>&#183;事件线程是调用epoll_wait()等待事件的线程。例子代码里面，一个线程干了所有的事情，而需要开发一个高性能的服务器的时候，事件线程应该专注于事件本身的处理，将触发事件的socket句柄放到对应的处理队列中去，由具体的处理线程负责具体的工作。</p>
<p>6、accept()单独一个线程： <br>&#183;服务端的socket句柄（就是调用bind()和listen()的这个）最好在单独的一个线程里面做accept()，阻塞还是非阻塞都无所谓，相比整个服务器的通讯，用户接入的动作只是很小一部分。而且，accept()不放在事件线程的循环里面，减少了判断。</p>
<p>7、接收线程单独一个： <br>&#183;接收线程从发生EPOLLIN事件的队列中取出socket句柄，然后在这个句柄上调用recv接收数据，直到缓冲区没有数据为止。接收到的数据写入以socket为键的hash表中，hash表中有一个自增长的缓冲区，保存了客户端发过来的数据。<br>&#183;这样的处理方式适合于客户端发来的数据很小的应用，比如HTTP服务器之类；假设是文件上传的服务器，则接受线程会一直处理某个连接的海量数据，其他客户端的数据处理产生了饥饿。所以，如果是文件上传服务器一类的场景，就不能这样设计。</p>
<p>8、发送线程单独一个： <br>&#183;发送线程从发送队列获取需要发送数据的SOCKET句柄，在这些句柄上调用send()将数据发到客户端。队列中指保存了SOCKET句柄，具体的信息 还需要通过socket句柄在hash表中查找，定位到具体的对象。如同上面所讲，客户端信息的对象不但有一个变长的接收数据缓冲区，还有一个变长的发送 数据缓冲区。具体的工作线程发送数据的时候并不直接调用send()函数，而是将数据写到发送数据缓冲区，然后把SOCKET句柄放到发送线程队列。<br>&#183;SOCKET句柄放到发送线程队列的另一种情况是：事件线程中发生了EPOLLOUT事件，说明TCP的发送缓冲区又有了可用的空间，这个时候可以把SOCKET句柄放到发送线程队列，一边触发send()的调用；<br>&#183;需要注意的是：发送线程发送大量数据的时候，当频繁调用send()直到TCP的发送缓冲区满后，便无法再发送了。这个时候如果循环等待，则其他用户的 发送工作受到影响；如果不继续发送，则EPOLL的ET模式可能不会再产生事件。解决这个问题的办法是在发送线程内再建立队列，或者在用户信息对象上设置 标志，等到线程空闲的时候，再去继续发送这些未发送完成的数据。</p>
<p>9、需要一个定时器线程： <br>&#183;一位将epoll使用的高手说道：&#8220;单纯靠epoll来管理描述符不泄露几乎是不可能的。完全解决方案很简单，就是对每个fd设置超时时间，如果超过timeout的时间，这个fd没有活跃过，就close掉&#8221;。<br>&#183;所以，定时器线程定期轮训整个hash表，检查socket是否在规定的时间内未活动。未活动的SOCKET认为是超时，然后服务器主动关闭句柄，回收资源。</p>
<p>10、多个工作线程： <br>&#183;工作线程由接收线程去触发：每次接收线程收到数据后，将有数据的SOCKET句柄放入一个工作队列中；工作线程再从工作队列获取SOCKET句柄，查询hash表，定位到用户信息对象，处理业务逻辑。<br>&#183;工作线程如果需要发送数据，先把数据写入用户信息对象的发送缓冲区，然后把SOCKET句柄放到发送线程队列中去。<br>&#183;对于任务队列，接收线程是生产者，多个工作线程是消费者；对于发送线程队列，多个工作线程是生产者，发送线程是消费者。在这里需要注意锁的问题，如果采用tbb::concurrent_queue，会轻松很多。</p>
<p>11、仅仅只用scoket句柄作为hash表的键，并不够： <br>&#183;假设这样一种情况：事件线程刚把某SOCKET因发生EPOLLIN事件放入了接收队列，可是随即客户端异常断开了，事件线程又因为EPOLLERR事 件删除了hash表中的这一项。假设接收队列很长，发生异常的SOCKET还在队列中，等到接收线程处理到这个SOCKET的时候，并不能通过 SOCKET句柄索引到hash表中的对象。<br>&#183;索引不到的情况也好处理，难点就在于，这个SOCKET句柄立即被另一个客户端使用了，接入线程为这个SCOKET建立了hash表中的某个对象。此时，句柄相同的两个SOCKET，其实已经是不同的两个客户端了。极端情况下，这种情况是可能发生的。<br>&#183;解决的办法是，使用socket fd + sequence为hash表的键，sequence由接入线程在每次accept()后将一个整型值累加而得到。这样，就算SOCKET句柄被重用，也不会发生问题了。</p>
<p>12、监控，需要考虑： <br>&#183;框架中最容易出问题的是工作线程：工作线程的处理速度太慢，就会使得各个队列暴涨，最终导致服务器崩溃。因此必须要限制每个队列允许的最大大小，且需要监视每个工作线程的处理时间，超过这个时间就应该采用某个办法结束掉工作线程。</p>
<img src ="http://www.cppblog.com/jericho/aggbug/144748.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/jericho/" target="_blank">小阮</a> 2011-04-21 21:40 <a href="http://www.cppblog.com/jericho/archive/2011/04/21/144748.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>